Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('SettingsWidget', () => {
maxAttempts: 2,
showanswer: 'finished',
showResetButton: false,
gradingMethod: 'last_score',
},
images: {},
isLibrary: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 (
<SettingsOption
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"
>
<div className="mb-4">
<FormattedMessage {...messages.scoringSettingsLabel} />
<FormattedMessage {...messages.scoringSettingsLabelWithGradingMethod} />
</div>
<Form.Group>
<Form.Control
as="select"
value={scoring.gradingMethod || GradingMethodKeys.LAST_SCORE}
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.Control
type="number"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
} from '@src/testUtils';
import ScoringCard from './ScoringCard';
import { selectors } from '../../../../../../data/redux';
import { GradingMethodKeys } from '../../../../../../data/constants/problem';

const { app } = selectors;

Expand All @@ -14,6 +15,7 @@ describe('ScoringCard', () => {
unlimited: false,
number: 5,
},
gradingMethod: GradingMethodKeys.LAST_SCORE,
updateSettings: jest.fn().mockName('args.updateSettings'),
};

Expand Down Expand Up @@ -59,6 +61,17 @@ describe('ScoringCard', () => {
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', () => {
const scoringUnlimited = { ...scoring, attempts: { unlimited: true, number: 0 } };
render(<ScoringCard {...props} scoring={scoringUnlimited} />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
29 changes: 29 additions & 0 deletions src/editors/data/constants/problem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -374,6 +402,7 @@ export const settingsOlxAttributes = [
'@_show_reset_button',
'@_submission_wait_seconds',
'@_attempts_before_showanswer_button',
'@_grading_method',
] as const;

export const ignoredOlxAttributes = [
Expand Down
5 changes: 3 additions & 2 deletions src/editors/data/redux/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/editors/data/redux/problem/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 '..';

Expand All @@ -29,6 +29,7 @@ const initialState: EditorState['problem'] = {
unlimited: true,
number: null,
},
gradingMethod: GradingMethodKeys.LAST_SCORE,
},
hints: [],
timeBetween: 0,
Expand Down