fix: add grading method view

This commit is contained in:
Irtaza Akram
2026-01-07 14:22:31 +05:00
committed by Feanil Patel
parent 292a457834
commit 2e836a55cf
14 changed files with 116 additions and 7 deletions

1
.env
View File

@@ -45,7 +45,6 @@ HOTJAR_VERSION=6
HOTJAR_DEBUG=false HOTJAR_DEBUG=false
INVITE_STUDENTS_EMAIL_TO='' INVITE_STUDENTS_EMAIL_TO=''
ENABLE_CHECKLIST_QUALITY='' ENABLE_CHECKLIST_QUALITY=''
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
# "Multi-level" blocks are unsupported in libraries # "Multi-level" blocks are unsupported in libraries
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,library_content,itembank" LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,library_content,itembank"
# Fallback in local style files # Fallback in local style files

View File

@@ -48,7 +48,6 @@ HOTJAR_VERSION=6
HOTJAR_DEBUG=true HOTJAR_DEBUG=true
INVITE_STUDENTS_EMAIL_TO="someone@domain.com" INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
ENABLE_CHECKLIST_QUALITY=true ENABLE_CHECKLIST_QUALITY=true
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
# "Multi-level" blocks are unsupported in libraries # "Multi-level" blocks are unsupported in libraries
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,library_content,itembank" LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,library_content,itembank"
# Fallback in local style files # Fallback in local style files

View File

@@ -40,7 +40,6 @@ ENABLE_TAGGING_TAXONOMY_PAGES=true
BBB_LEARN_MORE_URL='' BBB_LEARN_MORE_URL=''
INVITE_STUDENTS_EMAIL_TO="someone@domain.com" INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
ENABLE_CHECKLIST_QUALITY=true ENABLE_CHECKLIST_QUALITY=true
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
# "Multi-level" blocks are unsupported in libraries # "Multi-level" blocks are unsupported in libraries
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,library_content,itembank" LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,library_content,itembank"
PARAGON_THEME_URLS= PARAGON_THEME_URLS=

View File

@@ -173,12 +173,18 @@ export const scoringCardHooks = (scoring, updateSettings, defaultValue) => {
updateSettings({ scoring: { ...scoring, weight } }); updateSettings({ scoring: { ...scoring, weight } });
}; };
const handleGradingMethodChange = (event) => {
const { value } = event.target;
updateSettings({ scoring: { ...scoring, gradingMethod: value } });
};
return { return {
attemptDisplayValue, attemptDisplayValue,
handleUnlimitedChange, handleUnlimitedChange,
handleMaxAttemptChange, handleMaxAttemptChange,
handleOnChange, handleOnChange,
handleWeightChange, handleWeightChange,
handleGradingMethodChange,
}; };
}; };

View File

@@ -147,6 +147,7 @@ describe('Problem settings hooks', () => {
unlimited: false, unlimited: false,
number: 5, number: 5,
}, },
gradingMethod: 'last_score',
}; };
const defaultValue = 1; const defaultValue = 1;
test('test scoringCardHooks initializes display value when attempts.number is null', () => { test('test scoringCardHooks initializes display value when attempts.number is null', () => {
@@ -269,6 +270,11 @@ describe('Problem settings hooks', () => {
output.handleWeightChange({ target: { value } }); output.handleWeightChange({ target: { value } });
expect(updateSettings).toHaveBeenCalledWith({ scoring: { ...scoring, weight: parseFloat(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', () => { describe('Show answer card hooks', () => {

View File

@@ -203,6 +203,7 @@ SettingsWidget.propTypes = {
showanswer: PropTypes.string, showanswer: PropTypes.string,
showResetButton: PropTypes.bool, showResetButton: PropTypes.bool,
rerandomize: PropTypes.string, rerandomize: PropTypes.string,
gradingMethod: PropTypes.string,
}).isRequired, }).isRequired,
images: PropTypes.shape({}).isRequired, images: PropTypes.shape({}).isRequired,
isLibrary: PropTypes.bool.isRequired, isLibrary: PropTypes.bool.isRequired,

View File

@@ -33,6 +33,7 @@ describe('SettingsWidget', () => {
maxAttempts: 2, maxAttempts: 2,
showanswer: 'finished', showanswer: 'finished',
showResetButton: false, showResetButton: false,
gradingMethod: 'last_score',
}, },
images: {}, images: {},
isLibrary: false, isLibrary: false,

View File

@@ -82,6 +82,16 @@ const messages = defineMessages({
defaultMessage: 'Points', defaultMessage: 'Points',
description: 'Scoring weight input label', 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: { unlimitedAttemptsSummary: {
id: 'authoring.problemeditor.settings.scoring.unlimited', id: 'authoring.problemeditor.settings.scoring.unlimited',
defaultMessage: 'Unlimited attempts', defaultMessage: 'Unlimited attempts',
@@ -107,6 +117,11 @@ const messages = defineMessages({
defaultMessage: 'Specify point weight and the number of answer attempts', defaultMessage: 'Specify point weight and the number of answer attempts',
description: 'Descriptive text for scoring settings', 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: { attemptsHint: {
id: 'authoring.problemeditor.settings.scoring.attempts.hint', id: 'authoring.problemeditor.settings.scoring.attempts.hint',
defaultMessage: 'If a default value is not set in advanced settings, unlimited attempts are allowed', 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', defaultMessage: 'If a value is not set, the problem is worth one point',
description: 'Summary text for scoring weight', 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: { showAnswerSettingsTitle: {
id: 'authoring.problemeditor.settings.showAnswer.title', id: 'authoring.problemeditor.settings.showAnswer.title',
defaultMessage: 'Show answer', defaultMessage: 'Show answer',

View File

@@ -8,6 +8,7 @@ import { selectors } from '../../../../../../data/redux';
import SettingsOption from '../SettingsOption'; import SettingsOption from '../SettingsOption';
import messages from '../messages'; import messages from '../messages';
import { scoringCardHooks } from '../hooks'; import { scoringCardHooks } from '../hooks';
import { GradingMethod, GradingMethodKeys } from '../../../../../../data/constants/problem';
const ScoringCard = ({ const ScoringCard = ({
scoring, scoring,
@@ -23,28 +24,62 @@ const ScoringCard = ({
handleUnlimitedChange, handleUnlimitedChange,
handleMaxAttemptChange, handleMaxAttemptChange,
handleWeightChange, handleWeightChange,
handleGradingMethodChange,
handleOnChange, handleOnChange,
attemptDisplayValue, attemptDisplayValue,
} = scoringCardHooks(scoring, updateSettings, defaultValue); } = scoringCardHooks(scoring, updateSettings, defaultValue);
const getScoringSummary = (weight, attempts, unlimited) => { const getScoringSummary = (weight, attempts, unlimited, gradingMethod) => {
let summary = intl.formatMessage(messages.weightSummary, { weight }); let summary = intl.formatMessage(messages.weightSummary, { weight });
summary += ` ${String.fromCharCode(183)} `; summary += ` ${String.fromCharCode(183)} `;
summary += unlimited summary += unlimited
? intl.formatMessage(messages.unlimitedAttemptsSummary) ? intl.formatMessage(messages.unlimitedAttemptsSummary)
: intl.formatMessage(messages.attemptsSummary, { attempts: attempts || defaultValue }); : intl.formatMessage(messages.attemptsSummary, { attempts: attempts || defaultValue });
const methodMessage = gradingMethod ? GradingMethod[gradingMethod] : null;
if (methodMessage) {
summary += ` ${String.fromCharCode(183)} `;
summary += intl.formatMessage(messages.gradingMethodSummary, {
gradingMethod: intl.formatMessage(methodMessage),
});
}
return summary; return summary;
}; };
return ( return (
<SettingsOption <SettingsOption
title={intl.formatMessage(messages.scoringSettingsTitle)} title={intl.formatMessage(messages.scoringSettingsTitle)}
summary={getScoringSummary(scoring.weight, scoring.attempts.number, scoring.attempts.unlimited)} summary={getScoringSummary(scoring.weight, scoring.attempts.number, scoring.attempts.unlimited, scoring.gradingMethod)}
className="scoringCard" className="scoringCard"
> >
<div className="mb-4"> <div className="mb-4">
<FormattedMessage {...messages.scoringSettingsLabel} /> <FormattedMessage {...messages.scoringSettingsLabelWithGradingMethod} />
</div> </div>
<Form.Group>
<Form.Control
as="select"
value={scoring.gradingMethod}
onChange={handleGradingMethodChange}
floatingLabel={intl.formatMessage(messages.scoringGradingMethodInputLabel)}
>
{Object.values(GradingMethodKeys).map((gradingMethod) => {
const optionDisplayName = GradingMethod[gradingMethod];
return (
<option
key={gradingMethod}
value={gradingMethod}
>
{intl.formatMessage(optionDisplayName)}
</option>
);
})}
</Form.Control>
<Form.Control.Feedback>
<FormattedMessage {...messages.gradingMethodHint} />
</Form.Control.Feedback>
</Form.Group>
<Form.Group> <Form.Group>
<Form.Control <Form.Control
type="number" type="number"

View File

@@ -4,6 +4,7 @@ import {
} from '@src/testUtils'; } from '@src/testUtils';
import ScoringCard from './ScoringCard'; import ScoringCard from './ScoringCard';
import { selectors } from '../../../../../../data/redux'; import { selectors } from '../../../../../../data/redux';
import { GradingMethodKeys } from '../../../../../../data/constants/problem';
const { app } = selectors; const { app } = selectors;
@@ -14,6 +15,7 @@ describe('ScoringCard', () => {
unlimited: false, unlimited: false,
number: 5, number: 5,
}, },
gradingMethod: GradingMethodKeys.LAST_SCORE,
updateSettings: jest.fn().mockName('args.updateSettings'), updateSettings: jest.fn().mockName('args.updateSettings'),
}; };
@@ -59,6 +61,17 @@ describe('ScoringCard', () => {
expect(props.updateSettings).toHaveBeenCalled(); expect(props.updateSettings).toHaveBeenCalled();
}); });
test('should call updateSettings when changing grading method', () => {
render(<ScoringCard {...props} />);
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', () => { test('should call updateSettings when clicking attempts button', () => {
const scoringUnlimited = { ...scoring, attempts: { unlimited: true, number: 0 } }; const scoringUnlimited = { ...scoring, attempts: { unlimited: true, number: 0 } };
render(<ScoringCard {...props} scoring={scoringUnlimited} />); render(<ScoringCard {...props} scoring={scoringUnlimited} />);

View File

@@ -33,6 +33,7 @@ class ReactStateSettingsParser {
settings = popuplateItem(settings, 'number', 'max_attempts', stateSettings.scoring.attempts, defaultSettings?.maxAttempts, true); settings = popuplateItem(settings, 'number', 'max_attempts', stateSettings.scoring.attempts, defaultSettings?.maxAttempts, true);
settings = popuplateItem(settings, 'weight', 'weight', stateSettings.scoring); 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); settings = popuplateItem(settings, 'on', 'showanswer', stateSettings.showAnswer, defaultSettings?.showanswer, true);
if (includes(numberOfAttemptsChoice, stateSettings.showAnswer.on)) { if (includes(numberOfAttemptsChoice, stateSettings.showAnswer.on)) {
settings = popuplateItem(settings, 'afterAttempts', 'attempts_before_showanswer_button', stateSettings.showAnswer); settings = popuplateItem(settings, 'afterAttempts', 'attempts_before_showanswer_button', stateSettings.showAnswer);

View File

@@ -43,6 +43,7 @@ export const parseScoringSettings = (metadata, defaultSettings) => {
scoring = { ...scoring, attempts }; scoring = { ...scoring, attempts };
scoring = popuplateItem(scoring, 'weight', 'weight', metadata); scoring = popuplateItem(scoring, 'weight', 'weight', metadata);
scoring = popuplateItem(scoring, 'grading_method', 'gradingMethod', metadata);
return scoring; return scoring;
}; };

View File

@@ -364,6 +364,32 @@ export const RandomizationTypes = StrictDict({
}, },
} as const); } as const);
export const GradingMethodKeys = StrictDict({
LAST_SCORE: 'last_score',
HIGHEST_SCORE: 'highest_score',
AVERAGE_SCORE: 'average_score',
FIRST_SCORE: 'first_score',
});
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 RichTextProblems = [ProblemTypeKeys.SINGLESELECT, ProblemTypeKeys.MULTISELECT] as const;
export const settingsOlxAttributes = [ export const settingsOlxAttributes = [
@@ -374,6 +400,7 @@ export const settingsOlxAttributes = [
'@_show_reset_button', '@_show_reset_button',
'@_submission_wait_seconds', '@_submission_wait_seconds',
'@_attempts_before_showanswer_button', '@_attempts_before_showanswer_button',
'@_grading_method',
] as const; ] as const;
export const ignoredOlxAttributes = [ export const ignoredOlxAttributes = [

View File

@@ -2,7 +2,7 @@ import { has } from 'lodash';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import { indexToLetterMap } from '../../../containers/ProblemEditor/data/OLXParser'; import { indexToLetterMap } from '../../../containers/ProblemEditor/data/OLXParser';
import { StrictDict } from '../../../utils'; 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 { ToleranceTypes } from '../../../containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Tolerance/constants';
import type { EditorState } from '..'; import type { EditorState } from '..';
@@ -29,6 +29,7 @@ const initialState: EditorState['problem'] = {
unlimited: true, unlimited: true,
number: null, number: null,
}, },
gradingMethod: GradingMethodKeys.LAST_SCORE
}, },
hints: [], hints: [],
timeBetween: 0, timeBetween: 0,