diff --git a/.env b/.env index 22852cf813..7b9229b9a3 100644 --- a/.env +++ b/.env @@ -45,7 +45,6 @@ HOTJAR_VERSION=6 HOTJAR_DEBUG=false INVITE_STUDENTS_EMAIL_TO='' ENABLE_CHECKLIST_QUALITY='' -ENABLE_GRADING_METHOD_IN_PROBLEMS=false # "Multi-level" blocks are unsupported in libraries LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,library_content,itembank" # Fallback in local style files diff --git a/.env.development b/.env.development index 8771be2830..9e25a53b35 100644 --- a/.env.development +++ b/.env.development @@ -48,7 +48,6 @@ HOTJAR_VERSION=6 HOTJAR_DEBUG=true INVITE_STUDENTS_EMAIL_TO="someone@domain.com" ENABLE_CHECKLIST_QUALITY=true -ENABLE_GRADING_METHOD_IN_PROBLEMS=false # "Multi-level" blocks are unsupported in libraries LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,library_content,itembank" # Fallback in local style files diff --git a/.env.test b/.env.test index 0421e80990..617eacb32f 100644 --- a/.env.test +++ b/.env.test @@ -40,7 +40,6 @@ ENABLE_TAGGING_TAXONOMY_PAGES=true BBB_LEARN_MORE_URL='' INVITE_STUDENTS_EMAIL_TO="someone@domain.com" ENABLE_CHECKLIST_QUALITY=true -ENABLE_GRADING_METHOD_IN_PROBLEMS=false # "Multi-level" blocks are unsupported in libraries LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,library_content,itembank" PARAGON_THEME_URLS= diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.js index b86772923c..04ed2ccf29 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.js @@ -173,12 +173,18 @@ export const scoringCardHooks = (scoring, updateSettings, defaultValue) => { updateSettings({ scoring: { ...scoring, weight } }); }; + const handleGradingMethodChange = (event) => { + const { value } = event.target; + updateSettings({ scoring: { ...scoring, gradingMethod: value } }); + }; + return { attemptDisplayValue, handleUnlimitedChange, handleMaxAttemptChange, handleOnChange, handleWeightChange, + handleGradingMethodChange, }; }; diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.test.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.test.js index 079506716f..beba825ed6 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.test.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.test.js @@ -147,6 +147,7 @@ describe('Problem settings hooks', () => { unlimited: false, number: 5, }, + gradingMethod: 'last_score', }; const defaultValue = 1; test('test scoringCardHooks initializes display value when attempts.number is null', () => { @@ -269,6 +270,11 @@ describe('Problem settings hooks', () => { output.handleWeightChange({ target: { value } }); expect(updateSettings).toHaveBeenCalledWith({ scoring: { ...scoring, weight: parseFloat(value) } }); }); + test('test handleGradingMethodChange', () => { + const value = 'first_score'; + output.handleGradingMethodChange({ target: { value } }); + expect(updateSettings).toHaveBeenCalledWith({ scoring: { ...scoring, gradingMethod: value } }); + }); }); describe('Show answer card hooks', () => { diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx index fd2d40b6ef..2e917acad7 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx @@ -203,6 +203,7 @@ SettingsWidget.propTypes = { showanswer: PropTypes.string, showResetButton: PropTypes.bool, rerandomize: PropTypes.string, + gradingMethod: PropTypes.string, }).isRequired, images: PropTypes.shape({}).isRequired, isLibrary: PropTypes.bool.isRequired, diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.test.tsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.test.tsx index 5a9e96341a..af216d5272 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.test.tsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.test.tsx @@ -33,6 +33,7 @@ describe('SettingsWidget', () => { maxAttempts: 2, showanswer: 'finished', showResetButton: false, + gradingMethod: 'last_score', }, images: {}, isLibrary: false, diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/messages.ts b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/messages.ts index e915032539..a854ac67fc 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/messages.ts +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/messages.ts @@ -82,6 +82,16 @@ const messages = defineMessages({ defaultMessage: 'Points', description: 'Scoring weight input label', }, + scoringGradingMethodInputLabel: { + id: 'authoring.problemeditor.settings.scoring.grading.method.inputLabel', + defaultMessage: 'Grading Method', + description: 'Grading method input label', + }, + gradingMethodSummary: { + id: 'authoring.problemeditor.settings.scoring.grading.method', + defaultMessage: '{gradingMethod}', + description: 'Summary text for scoring grading method', + }, unlimitedAttemptsSummary: { id: 'authoring.problemeditor.settings.scoring.unlimited', defaultMessage: 'Unlimited attempts', @@ -107,6 +117,11 @@ const messages = defineMessages({ defaultMessage: 'Specify point weight and the number of answer attempts', description: 'Descriptive text for scoring settings', }, + scoringSettingsLabelWithGradingMethod: { + id: 'authoring.problemeditor.settings.scoring.label.withGradingMethod', + defaultMessage: 'Specify grading method, point weight and the number of answer attempts', + description: 'Descriptive text for scoring settings when grading method is enabled', + }, attemptsHint: { id: 'authoring.problemeditor.settings.scoring.attempts.hint', defaultMessage: 'If a default value is not set in advanced settings, unlimited attempts are allowed', @@ -117,6 +132,11 @@ const messages = defineMessages({ defaultMessage: 'If a value is not set, the problem is worth one point', description: 'Summary text for scoring weight', }, + gradingMethodHint: { + id: 'authoring.problemeditor.settings.scoring.grading.method.hint', + defaultMessage: 'Define the grading method for this problem. By default, it is the score of the last submission made by the student.', + description: 'Summary text for scoring grading method', + }, showAnswerSettingsTitle: { id: 'authoring.problemeditor.settings.showAnswer.title', defaultMessage: 'Show answer', diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ScoringCard.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ScoringCard.jsx index 6b12e552a2..f93880e5f4 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ScoringCard.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ScoringCard.jsx @@ -8,6 +8,7 @@ import { selectors } from '../../../../../../data/redux'; import SettingsOption from '../SettingsOption'; import messages from '../messages'; import { scoringCardHooks } from '../hooks'; +import { GradingMethod, GradingMethodKeys } from '../../../../../../data/constants/problem'; const ScoringCard = ({ scoring, @@ -23,28 +24,67 @@ const ScoringCard = ({ handleUnlimitedChange, handleMaxAttemptChange, handleWeightChange, + handleGradingMethodChange, handleOnChange, attemptDisplayValue, } = scoringCardHooks(scoring, updateSettings, defaultValue); - const getScoringSummary = (weight, attempts, unlimited) => { + const getScoringSummary = (weight, attempts, unlimited, gradingMethod) => { let summary = intl.formatMessage(messages.weightSummary, { weight }); summary += ` ${String.fromCharCode(183)} `; summary += unlimited ? intl.formatMessage(messages.unlimitedAttemptsSummary) : intl.formatMessage(messages.attemptsSummary, { attempts: attempts || defaultValue }); + + const methodMessage = GradingMethod[gradingMethod || GradingMethodKeys.LAST_SCORE]; + + if (methodMessage) { + summary += ` ${String.fromCharCode(183)} `; + summary += intl.formatMessage(messages.gradingMethodSummary, { + gradingMethod: intl.formatMessage(methodMessage), + }); + } + return summary; }; return (
- +
+ + + {Object.values(GradingMethodKeys).map((gradingMethod) => { + const optionDisplayName = GradingMethod[gradingMethod]; + return ( + + ); + })} + + + + + { unlimited: false, number: 5, }, + gradingMethod: GradingMethodKeys.LAST_SCORE, updateSettings: jest.fn().mockName('args.updateSettings'), }; @@ -59,6 +61,17 @@ describe('ScoringCard', () => { expect(props.updateSettings).toHaveBeenCalled(); }); + test('should call updateSettings when changing grading method', () => { + render(); + fireEvent.click(screen.getByText('Scoring')); + const gradingSelect = screen.getByRole('combobox', { name: 'Grading Method' }); + expect(gradingSelect).toBeInTheDocument(); + expect(gradingSelect.value).toBe(GradingMethodKeys.LAST_SCORE); + + fireEvent.change(gradingSelect, { target: { value: GradingMethodKeys.HIGHEST_SCORE } }); + expect(props.updateSettings).toHaveBeenCalled(); + }); + test('should call updateSettings when clicking attempts button', () => { const scoringUnlimited = { ...scoring, attempts: { unlimited: true, number: 0 } }; render(); diff --git a/src/editors/containers/ProblemEditor/data/ReactStateSettingsParser.js b/src/editors/containers/ProblemEditor/data/ReactStateSettingsParser.js index 7cad5b16ad..b8d292b35a 100644 --- a/src/editors/containers/ProblemEditor/data/ReactStateSettingsParser.js +++ b/src/editors/containers/ProblemEditor/data/ReactStateSettingsParser.js @@ -33,6 +33,7 @@ class ReactStateSettingsParser { settings = popuplateItem(settings, 'number', 'max_attempts', stateSettings.scoring.attempts, defaultSettings?.maxAttempts, true); settings = popuplateItem(settings, 'weight', 'weight', stateSettings.scoring); + settings = popuplateItem(settings, 'gradingMethod', 'grading_method', stateSettings.scoring); settings = popuplateItem(settings, 'on', 'showanswer', stateSettings.showAnswer, defaultSettings?.showanswer, true); if (includes(numberOfAttemptsChoice, stateSettings.showAnswer.on)) { settings = popuplateItem(settings, 'afterAttempts', 'attempts_before_showanswer_button', stateSettings.showAnswer); diff --git a/src/editors/containers/ProblemEditor/data/SettingsParser.js b/src/editors/containers/ProblemEditor/data/SettingsParser.js index 55a84349f6..78a6959761 100644 --- a/src/editors/containers/ProblemEditor/data/SettingsParser.js +++ b/src/editors/containers/ProblemEditor/data/SettingsParser.js @@ -43,6 +43,7 @@ export const parseScoringSettings = (metadata, defaultSettings) => { scoring = { ...scoring, attempts }; scoring = popuplateItem(scoring, 'weight', 'weight', metadata); + scoring = popuplateItem(scoring, 'grading_method', 'gradingMethod', metadata); return scoring; }; diff --git a/src/editors/data/constants/problem.ts b/src/editors/data/constants/problem.ts index ab622123be..a0be26389d 100644 --- a/src/editors/data/constants/problem.ts +++ b/src/editors/data/constants/problem.ts @@ -364,6 +364,34 @@ export const RandomizationTypes = StrictDict({ }, } as const); +export const GradingMethodKeys = StrictDict({ + LAST_SCORE: 'last_score', + HIGHEST_SCORE: 'highest_score', + AVERAGE_SCORE: 'average_score', + FIRST_SCORE: 'first_score', +}); + +export type GradingMethodKey = typeof GradingMethodKeys[keyof typeof GradingMethodKeys]; + +export const GradingMethod = StrictDict({ + [GradingMethodKeys.LAST_SCORE]: { + id: 'authoring.problemeditor.settings.gradingmethod.last_score', + defaultMessage: 'Last score (Default)', + }, + [GradingMethodKeys.HIGHEST_SCORE]: { + id: 'authoring.problemeditor.settings.gradingmethod.highest_score', + defaultMessage: 'Highest score', + }, + [GradingMethodKeys.AVERAGE_SCORE]: { + id: 'authoring.problemeditor.settings.gradingmethod.average_score', + defaultMessage: 'Average score', + }, + [GradingMethodKeys.FIRST_SCORE]: { + id: 'authoring.problemeditor.settings.gradingmethod.first_score', + defaultMessage: 'First score', + }, +}); + export const RichTextProblems = [ProblemTypeKeys.SINGLESELECT, ProblemTypeKeys.MULTISELECT] as const; export const settingsOlxAttributes = [ @@ -374,6 +402,7 @@ export const settingsOlxAttributes = [ '@_show_reset_button', '@_submission_wait_seconds', '@_attempts_before_showanswer_button', + '@_grading_method', ] as const; export const ignoredOlxAttributes = [ diff --git a/src/editors/data/redux/index.ts b/src/editors/data/redux/index.ts index effe1a7164..195b63606f 100644 --- a/src/editors/data/redux/index.ts +++ b/src/editors/data/redux/index.ts @@ -9,7 +9,7 @@ import * as video from './video'; import * as problem from './problem'; import * as game from './game'; import type { RequestKeys, RequestStates } from '../constants/requests'; -import { AdvancedProblemType, ProblemType } from '../constants/problem'; +import { AdvancedProblemType, type GradingMethodKey, ProblemType } from '../constants/problem'; export { default as thunkActions } from './thunkActions'; @@ -175,7 +175,8 @@ export interface EditorState { randomization: null | any; // Not sure what type this field has scoring: { weight: number; - attempts: { unlimited: boolean; number: number | null; } + attempts: { unlimited: boolean; number: number | null; }; + gradingMethod: GradingMethodKey; }, hints: any[]; timeBetween: number; diff --git a/src/editors/data/redux/problem/reducers.ts b/src/editors/data/redux/problem/reducers.ts index 95189dadb7..e7fc0a4cba 100644 --- a/src/editors/data/redux/problem/reducers.ts +++ b/src/editors/data/redux/problem/reducers.ts @@ -2,7 +2,7 @@ import { has } from 'lodash'; import { createSlice } from '@reduxjs/toolkit'; import { indexToLetterMap } from '../../../containers/ProblemEditor/data/OLXParser'; import { StrictDict } from '../../../utils'; -import { ProblemTypeKeys, RichTextProblems } from '../../constants/problem'; +import { GradingMethodKeys, ProblemTypeKeys, RichTextProblems } from '../../constants/problem'; import { ToleranceTypes } from '../../../containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Tolerance/constants'; import type { EditorState } from '..'; @@ -29,6 +29,7 @@ const initialState: EditorState['problem'] = { unlimited: true, number: null, }, + gradingMethod: GradingMethodKeys.LAST_SCORE, }, hints: [], timeBetween: 0,