UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

346 lines (310 loc) • 11.5 kB
/** @jsx jsx */ import {Component} from 'react' import PropTypes from 'prop-types' import get from 'lodash/fp/get' import {Text} from '@instructure/ui-text' import {jsx} from '@instructure/emotion' import t from '@instructure/quiz-i18n/format-message' import {ItemBodyWrapper} from '@instructure/quiz-rce/components/ItemBodyWrapper/index' import {FeedbackWrapper} from '@instructure/quiz-results-feedback' import CategoriesContainer from '../common/CategoriesContainer' import {getSortedCategories} from '../common/utils' import generateStyle from './styles' import generateComponentTheme from './theme' import {withStyleOverrides} from '@instructure/quiz-common/util/withStyleOverrides' /** --- category: Categorization --- Categorization Result component ```jsx_example <SettingsSwitcher locales={LOCALES}> <CategorizationResult itemBody="Match the name of the celestial body on the left with its correct classification:" interactionData={{ categoryOrder: ['category_uuid1', 'category_uuid2', 'category_uuid3', 'category_uuid4'], categories: { category_uuid1: { id: 'category_uuid1', itemBody: 'Planet' }, category_uuid2: { id: 'category_uuid2', itemBody: 'Moon' }, category_uuid3: { id: 'category_uuid3', itemBody: 'Quasar' }, category_uuid4: { id: 'category_uuid4', itemBody: 'Stars' }, }, distractors: { answer_uuid3: { id: 'answer_uuid3', itemBody: 'Mars' }, answer_uuid4: { id: 'answer_uuid4', itemBody: 'Europa' }, answer_uuid5: { id: 'answer_uuid5', itemBody: 'Venus' }, answer_uuid6: { id: 'answer_uuid6', itemBody: 'Phobos' }, answer_uuid7: { id: 'answer_uuid7', itemBody: 'Deimos' }, answer_uuid8: { id: 'answer_uuid8', itemBody: 'Jupiter' }, answer_uuid9: { id: 'answer_uuid9', itemBody: 'America' }, answer_uuid10: { id: 'answer_uuid10', itemBody: 'Asia' }, answer_uuid11: { id: 'answer_uuid11', itemBody: 'The Sun' } } }} scoredData={{ value: { category_uuid1: { value: { answer_uuid3: { resultScore: 1, userResponded: false }, answer_uuid5: { resultScore: 1, userResponded: true }, answer_uuid8: { resultScore: 1, userResponded: false }, answer_uuid7: { resultScore: 0, userResponded: true } } }, category_uuid3: { value: {} }, category_uuid4: { value: { answer_uuid11: { resultScore: 1, userResponded: false } } }, category_uuid2: { value: { answer_uuid4: { resultScore: 1, userResponded: true }, answer_uuid6: { resultScore: 1, userResponded: true }, answer_uuid7: { resultScore: 1, userResponded: false }, answer_uuid8: { resultScore: 0, userResponded: true }, answer_uuid9: { resultScore: 0, userResponded: true } } } } }} /> </SettingsSwitcher> ``` **/ @withStyleOverrides(generateStyle, generateComponentTheme) export default class CategorizationResult extends Component { static displayName = 'CategorizationResult' static componentId = `Quizzes${this.displayName}` static propTypes = { interactionData: PropTypes.object.isRequired, itemBody: PropTypes.string.isRequired, scoredData: PropTypes.shape({ correct: PropTypes.bool, uncategorized: PropTypes.objectOf( PropTypes.shape({ resultScore: PropTypes.number, userResponded: PropTypes.bool, }), ), value: PropTypes.objectOf( PropTypes.shape({ value: PropTypes.objectOf( PropTypes.shape({ resultScore: PropTypes.number, userResponded: PropTypes.bool, }), ), }), ), }).isRequired, styles: PropTypes.object, } constructor(props) { super(props) const {categories, categoryOrder} = props.interactionData this.state = { sortedCategories: getSortedCategories(categories, categoryOrder), } } // =========== // UTILS // =========== hasMissedCorrectAnswerInObject(answers) { return ( answers && Object.keys(answers).some(answerId => { return ( answers[answerId] && answers[answerId].resultScore > 0 && answers[answerId].userResponded === false ) }) ) } hasMissedCorrectAnswerInUncategorized() { const {uncategorized} = this.props.scoredData return this.hasMissedCorrectAnswerInObject(uncategorized) } hasMissedCorrectAnswerInCategories() { const categories = this.props.scoredData.value return ( categories && Object.keys(categories).some(categoryKey => { const answers = categories[categoryKey].value return this.hasMissedCorrectAnswerInObject(answers) }) ) } includesAllCorrectAnswers() { // we know a response includes all the correct answers if it is either // correct, or it contains correct responses not chosen by the user const {scoredData} = this.props if (scoredData.correct) { return true } return this.hasMissedCorrectAnswerInUncategorized() || this.hasMissedCorrectAnswerInCategories() } getCorrectAnswer(answerId, categoryId) { const {scoredData} = this.props const correctCategoryId = scoredData.value && Object.keys(scoredData.value).find(categoryKey => { if (categoryKey !== categoryId) { return get(`value[${categoryKey}].value[${answerId}].resultScore`, scoredData) } else { return null } }) if (correctCategoryId) { return this.props.interactionData.categories[correctCategoryId].itemBody } } getUnansweredAnswersIds() { if (this.responseHidden()) return [] const distractorsArray = Object.keys(this.props.interactionData.distractors) return distractorsArray.filter(answerId => { const category = Object.values(this.props.scoredData.value).find(scoredDataCateg => get(`value[${answerId}].userResponded`, scoredDataCateg), ) return category === void 0 }) } responseHidden() { const {scoredData} = this.props return !scoredData.value || Object.keys(scoredData.value).length === 0 } correctnessKnown() { return this.props.scoredData.correct != null } // =========== // RENDER // =========== renderCategoryAnswer(answerId, resultScore, categoryId) { let status, correctAnswerText if (resultScore > 0) { status = 'correct' } else if (resultScore <= 0) { status = 'incorrect' if (this.correctnessKnown()) { correctAnswerText = this.getCorrectAnswer(answerId) if (!correctAnswerText) { // If a correct answer can't be found, it means that either the // answer should have been left uncategorized, or the results are // restricted and the correct answer is hidden. In the first case, we // want to display "No Category" feedback, but in the latter case we // don't want to provide any feedback beyond whether the answer was // correct. To distinguish between these 2 cases, we check whether // any missed correct answers are present in the response, and whether // the `uncategorized` scoredData attribute exists, which is essentially a // category for answers that should remain uncategorized. If the // `uncategorized` attribute is absent, but we show other correct answers, // we display the "No Category" feedback for backward-compatibility. const {uncategorized} = this.props.scoredData if ( this.includesAllCorrectAnswers() && (!uncategorized || get(`${answerId}.resultScore`, uncategorized)) ) { correctAnswerText = t('No Category') } } } } else { status = 'unknown' } return ( <div key={answerId} css={this.props.styles.feedbackWrapper}> <FeedbackWrapper correctAnswer={correctAnswerText} hiddenCorrectAnswerText={t('Correct answer: ')} hiddenIncorrectAnswerText={t('Incorrect answer: ')} status={status} > <div css={this.props.styles.userResponse}> <Text color="primary">{this.props.interactionData.distractors[answerId].itemBody}</Text> </div> </FeedbackWrapper> </div> ) } renderCategoryBody = categoryId => { const categoryValue = get(`scoredData.value[${categoryId}].value`, this.props) || {} const answers = Object.entries(categoryValue).filter(([_, answer]) => answer.userResponded) if (answers.length) { return ( <div className="categoryBody fs-mask" css={this.props.styles.categoryBody}> {answers.map(([answerId, {resultScore}]) => this.renderCategoryAnswer(answerId, resultScore, categoryId), )} </div> ) } return ( <div className="categoryBody fs-mask" css={this.props.styles.categoryBody}> <div css={this.props.styles.noChoicesText}> <Text color="primary"> {this.responseHidden() ? t('Choices not displayed') : t('No Choices Selected')} </Text> </div> </div> ) } renderUnansweredChoice = answerId => { const {itemBody} = this.props.interactionData.distractors[answerId] let status = 'unknown', correctAnswerText if (this.correctnessKnown()) { const {scoredData} = this.props correctAnswerText = this.getCorrectAnswer(answerId) if (correctAnswerText) { status = 'incorrect' } else if (!scoredData.uncategorized) { if (this.includesAllCorrectAnswers()) { status = 'correct' // for backward-compatibility } else { status = 'unknown' } } else { status = get(`uncategorized[${answerId}].resultScore`, scoredData) ? 'correct' : 'incorrect' } } return ( <div key={answerId} css={this.props.styles.unanswered}> <div css={this.props.styles.feedbackWrapper}> <FeedbackWrapper correctAnswer={correctAnswerText} hiddenCorrectAnswerText={t('Correct answer: ')} hiddenIncorrectAnswerText={t('Incorrect answer: ')} status={status} > <div css={this.props.styles.userResponse}> <Text color="primary">{itemBody}</Text> </div> </FeedbackWrapper> </div> </div> ) } render() { const unansweredList = this.getUnansweredAnswersIds() return ( <ItemBodyWrapper itemBody={this.props.itemBody}> <CategoriesContainer sortedCategories={this.state.sortedCategories} categoryBody={this.renderCategoryBody} distractors={this.props.interactionData.distractors} /> {unansweredList.length === 0 ? null : ( <div css={this.props.styles.mainContainer}> <Text color="primary">{t('Uncategorized answers')}</Text> <div className="categoryBody fs-mask" css={this.props.styles.categoryBody}> {unansweredList.map(this.renderUnansweredChoice)} </div> </div> )} </ItemBodyWrapper> ) } }