Skip to content

Commit

Permalink
Flag unanswered questions in Quiz review mode, implement nohighlight (#…
Browse files Browse the repository at this point in the history
…179)

* Refactor question and answer status methods out of QuizProgress and into Quiz.

Don't make highlights if answer disallows highlights.

* Configure Quiz to use new 'nohighlight' option.

Flag unfinished questions with red box in Quiz review mode.

Systematize question and answer status functions and use consistently in Quiz.
  • Loading branch information
normangilmore authored Nov 23, 2017
1 parent 47136ea commit 347ae2c
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 89 deletions.
7 changes: 7 additions & 0 deletions app/actions/quiz.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ export function activeQuestion(q_id) {
};
}

export function activeAnswer(answer_id) {
return {
type: 'UPDATE_ACTIVE_ANSWER',
answer_id
};
}

export function storeQuizTask(task) {
return (dispatch, getState) => {
dispatch({
Expand Down
44 changes: 21 additions & 23 deletions app/components/Question/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import { Map as ImmutableMap } from 'immutable';

import { SingleDatePicker } from 'react-dates';
import moment from 'moment';
import { selectAnswer, removeAnswer } from 'actions/quiz';
import * as quizActions from 'actions/quiz';

import { styles } from './styles.scss';
import 'react-dates/lib/css/_datepicker.css';
Expand All @@ -27,17 +29,6 @@ const mapStateToProps = state => {
};
}

const mapDispatchToProps = dispatch => {
return {
selectAnswer: (type, q_id, a_id, text) => {
dispatch(selectAnswer(type, q_id, a_id, text))
},
removeAnswer: (type, q_id, a_id) => {
dispatch(removeAnswer(type, q_id, a_id))
},
};
}

function renderInkWell(color, selected, clickHandler) {
var style = {
"display": "inline-block",
Expand Down Expand Up @@ -73,10 +64,12 @@ class Question extends Component {
}

static propTypes = {
question: React.PropTypes.object.isRequired,
answer_id: React.PropTypes.number.isRequired,
answers: React.PropTypes.instanceOf(ImmutableMap).isRequired,
answer_colors: React.PropTypes.instanceOf(ImmutableMap).isRequired,
answer_id: PropTypes.number.isRequired,
answers: PropTypes.instanceOf(ImmutableMap).isRequired,
answer_colors: PropTypes.instanceOf(ImmutableMap).isRequired,

question: PropTypes.object.isRequired,
quizMethods: PropTypes.object.isRequired,
}

activeAnswer(answer_id) {
Expand All @@ -100,12 +93,14 @@ class Question extends Component {
getAnswerColorState(answer_id) {
let answerColor = '';
let selected = false;
// answer_colors is a Map
if (this.props.answer_colors.has(answer_id)) {
answerColor = this.props.answer_colors.get(answer_id);
};
if (this.props.answer_id === answer_id) {
selected = true;
if (this.props.quizMethods.answerAllowsHighlights(answer_id)) {
// answer_colors is a Map
if (this.props.answer_colors.has(answer_id)) {
answerColor = this.props.answer_colors.get(answer_id);
};
if (this.props.answer_id === answer_id) {
selected = true;
};
};
return { answerColor, selected };
}
Expand All @@ -120,6 +115,9 @@ class Question extends Component {
this.props.selectAnswer(TYPES.RADIO, this.props.question.id,
answer.id, answer.answer_content);
};
var hoverHandler = () => {
this.props.activeAnswer(answer.id);
};
return (
<div key={answer.id}
style={{ "color": answerColor }}>
Expand Down Expand Up @@ -271,5 +269,5 @@ class Question extends Component {

export default connect(
mapStateToProps,
mapDispatchToProps
dispatch => bindActionCreators(quizActions, dispatch)
)(Question);
133 changes: 104 additions & 29 deletions app/components/Quiz/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,25 @@ export class Quiz extends Component {
constructor(props) {
super(props);

this.componentDidMount = this.componentDidMount.bind(this);
this.componentDidUpdate = this.componentDidUpdate.bind(this);
this.componentWillUnmount = this.componentWillUnmount.bind(this);
this.startTutorialOnTaskLoad = this.startTutorialOnTaskLoad.bind(this);
this.restartTutorial = this.restartTutorial.bind(this);
this.setReviewMode = this.setReviewMode.bind(this);
this.answerAllowsHighlights = this.answerAllowsHighlights.bind(this);
this.answerStatusGreen = this.answerStatusGreen.bind(this);
this.noAnswerRequired = this.noAnswerRequired.bind(this);
this.questionInProgress = this.questionInProgress.bind(this);
this.questionStatusGreen = this.questionStatusGreen.bind(this);
this.allQuestionsAnswered = this.allQuestionsAnswered.bind(this);
this.loadEditorState = this.loadEditorState.bind(this);
this.wrapSpan = this.wrapSpan.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleScroll = this.handleScroll.bind(this);
this.handleKeyUp = this.handleKeyUp.bind(this);
this.handleDeleteKey = this.handleDeleteKey.bind(this);
this.handleSelect = this.handleSelect.bind(this);
this.componentDidMount = this.componentDidMount.bind(this);
this.componentDidUpdate = this.componentDidUpdate.bind(this);
this.startTutorialOnTaskLoad = this.startTutorialOnTaskLoad.bind(this);
this.restartTutorial = this.restartTutorial.bind(this);
this.setContextWords = this.setContextWords.bind(this);
this.getStyle = this.getStyle.bind(this);
this.intro = introJs();
Expand Down Expand Up @@ -159,6 +166,12 @@ export class Quiz extends Component {
this.startTutorialOnTaskLoad();
}

componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('keyup', this.handleKeyUp);
this.intro.exit();
}

startTutorialOnTaskLoad() {
if (this.props.displayState === displayStates.TASK_LOADED) {
if ( ! this.introStarted) {
Expand All @@ -175,16 +188,77 @@ export class Quiz extends Component {
this.introStarted = true;
}

componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('keyup', this.handleKeyUp);
this.intro.exit();
}

setReviewMode(reviewMode) {
this.setState({reviewMode: reviewMode === true});
}

answerAllowsHighlights(answer_id) {
let answerDB = this.props.db.entities.answer;
let options = answerDB[answer_id].options;
return (options.NOHIGHLIGHT === false);
}

// Returns true if the answer does not require highlights
// OR returns true if the answer requires highlights and has at
// least one highlight.
answerStatusGreen(answer_id) {
const answerDB = this.props.db.entities.answer;
const options = answerDB[answer_id].options;
if ((options.NOHIGHLIGHT === true) ||
(options.OPTIONALHIGHLIGHT === true)) {
return true;
};
// highlight is required for this answer. Check if it
// has one or more highlights.
let answerState = this.state.answerState;
if (answerState.hasAnswer(answer_id)) {
const annotations = answerState.getAnswerAnnotations(answer_id);
return (annotations.length > 0);
};
return false;
}

noAnswerRequired(question_id) {
const questionDB = this.props.db.entities.question;
// Checkbox questions should be required to have None of the Above
// but for now they do not require an answer.
return (
questionDB[question_id].question_type === "CHECKBOX" ||
questionDB[question_id].question_type === "SELECT_SUBTOPIC"
);
}

questionInProgress(question_id) {
return (
this.noAnswerRequired(question_id) ||
this.props.answer_selected.has(question_id)
);
}

questionStatusGreen(question_id) {
if (this.noAnswerRequired(question_id)) {
return true;
};
const answer_selected = this.props.answer_selected;
if (! answer_selected.has(question_id)) {
return false;
};
const answerMap = answer_selected.get(question_id);
for (const answer of answerMap.values()) {
if (! this.answerStatusGreen(answer.answer_id)) {
return false;
};
};
return true;
}

allQuestionsAnswered() {
const queue = this.props.queue;
return queue.every( (question_id) => {
return this.questionStatusGreen(question_id);
});
};

loadEditorState(currTask) {
const article_text = this.props.currTask.article.text;
let editorState = EditorState.createEmpty();
Expand Down Expand Up @@ -212,17 +286,6 @@ export class Quiz extends Component {
});
}

allQuestionsAnswered = () => {
const queue = this.props.queue;
const answer_selected = this.props.answer_selected;
const questionDB = this.props.db.entities.question;
// Every question must have an answer, except checkboxes
return queue.every( (question_id) =>
answer_selected.has(question_id) ||
questionDB[question_id].question_type === "CHECKBOX" ||
questionDB[question_id].question_type === "SELECT_SUBTOPIC");
};

// Babel plugin transform-class-properties allows us to use
// ES2016 property initializer syntax. So the arrow function
// will bind 'this' of the class. (React.createClass does automatically.)
Expand Down Expand Up @@ -278,11 +341,20 @@ export class Quiz extends Component {
{ prev_button }
{ next_button }
</div>
: <div className="quiz-prev-next-buttons"></div>;
: <div></div>;
let questionStyle = { padding: '6px', border: '1px solid white' };
if (this.state.reviewMode === true &&
! this.questionStatusGreen(question.id)) {
Object.assign(questionStyle, { border: '1px solid red' });
};
return (
<div key={question.id}>
{ buttons }
<Question question={question} />
<div style={questionStyle}>
<Question question={question}
quizMethods={this}
/>
</div>
{ buttons }
</div>
);
Expand Down Expand Up @@ -415,14 +487,18 @@ export class Quiz extends Component {

handleMouseUp(blockMaker) {
let articleHighlight = handleMakeHighlight();
let question_id = this.props.question_id;
let question = this.props.db.entities.question[question_id];
let question_number = question.question_number;
// Don't make highlights if no answer selected
let answer_id = this.props.answer_id;
// Don't make highlights if no answer selected
if (answer_id < 0) {
return;
};
// Don't make highlights if answer doesn't allow
if ( ! this.answerAllowsHighlights(answer_id) ) {
return;
};
let question_id = this.props.question_id;
let question = this.props.db.entities.question[question_id];
let question_number = question.question_number;
articleHighlight = moveToTokenBoundaries(blockMaker, articleHighlight);
if (articleHighlight !== null) {
let {start, end} = articleHighlight;
Expand Down Expand Up @@ -549,9 +625,8 @@ export class Quiz extends Component {
</button>
<Progress />
<QuizProgress
quizMethods={this}
style={{clear: 'right', marginBottom: '6px'}}
setActiveFn={this.props.activeQuestion}
answerState={this.state.answerState}
/>
<div className="look-in-bold-text">
Look for answers in the <b>bolded</b> text in the article at left.
Expand Down
Loading

0 comments on commit 347ae2c

Please sign in to comment.