Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { dispatch, store } from 'store';

import actions, { questionTypes } from '../../../constants';

const answerId = 3;

const buildPayload = ({ questionType, grade, correct, testCases } = {}) => ({
submission: {
pointsAwarded: null,
basePoints: 1000,
submittedAt: '2017-05-11T17:02:17.000+08:00',
bonusEndAt: '2017-05-11T17:02:17.000+08:00',
bonusPoints: 0,
},
assessment: {},
annotations: [],
posts: [],
topics: [],
questions: [{ id: 1, type: questionType, maximumGrade: 10 }],
answers: [
{
id: answerId,
fields: {
id: answerId,
questionId: 1,
},
questionId: 1,
grading: {
grade,
id: answerId,
},
explanation: correct !== undefined ? { correct } : undefined,
testCases,
},
],
});

const dispatchFetchSuccess = (payload) =>
dispatch({ type: actions.FETCH_SUBMISSION_SUCCESS, payload });

const getGrade = () =>
store.getState().assessments.submission.grading.questions[1].grade;

describe('getPrefilledGrade via FETCH_SUBMISSION_SUCCESS', () => {
describe('Question Types with ALWAYS_PREFILL_POLICY', () => {
it('prefills maximum grade for an ungraded correct answer', () => {
dispatchFetchSuccess(
buildPayload({
questionType: questionTypes.MultipleChoice,
grade: null,
correct: true,
}),
);

expect(getGrade()).toBe(10);
});

it('prefills 0 for an ungraded incorrect answer', () => {
dispatchFetchSuccess(
buildPayload({
questionType: questionTypes.MultipleChoice,
grade: null,
correct: false,
}),
);

expect(getGrade()).toBe(0);
});

it('leaves grade as null when there is no explanation', () => {
dispatchFetchSuccess(
buildPayload({
questionType: questionTypes.MultipleChoice,
grade: null,
correct: undefined,
}),
);

expect(getGrade()).toBeNull();
});

it('preserves existing grades even if answer is correct', () => {
dispatchFetchSuccess(
buildPayload({
questionType: questionTypes.MultipleChoice,
grade: 5,
correct: true,
}),
);

expect(getGrade()).toBe(5);
});

it('preserves existing grade even if answer is incorrect', () => {
dispatchFetchSuccess(
buildPayload({
questionType: questionTypes.MultipleChoice,
grade: 8,
correct: false,
}),
);

expect(getGrade()).toBe(8);
});
});

describe('Question Types with NEVER_PREFILL_POLICY', () => {
it('does not prefill for an ungraded correct answer', () => {
dispatchFetchSuccess(
buildPayload({
questionType: questionTypes.VoiceResponse,
grade: null,
correct: true,
}),
);

expect(getGrade()).toBeNull();
});

it('does not prefill for an ungraded incorrect answer', () => {
dispatchFetchSuccess(
buildPayload({
questionType: questionTypes.VoiceResponse,
grade: null,
correct: false,
}),
);

expect(getGrade()).toBeNull();
});

it('preserves existing grades', () => {
dispatchFetchSuccess(
buildPayload({
questionType: questionTypes.VoiceResponse,
grade: 5,
correct: true,
}),
);

expect(getGrade()).toBe(5);
});
});

describe('Question Types with ONLY_PREFILL_FULL_POLICY', () => {
it('prefills maximum grade for an ungraded correct answer', () => {
dispatchFetchSuccess(
buildPayload({
questionType: questionTypes.TextResponse,
grade: null,
correct: true,
}),
);

expect(getGrade()).toBe(10);
});

it('does not prefill 0 for an ungraded incorrect answer', () => {
dispatchFetchSuccess(
buildPayload({
questionType: questionTypes.TextResponse,
grade: null,
correct: false,
}),
);

expect(getGrade()).toBeNull();
});

it('leaves grade as null when there is no explanation', () => {
dispatchFetchSuccess(
buildPayload({
questionType: questionTypes.TextResponse,
grade: null,
correct: undefined,
}),
);

expect(getGrade()).toBeNull();
});

it('preserves existing grades', () => {
dispatchFetchSuccess(
buildPayload({
questionType: questionTypes.TextResponse,
grade: 5,
correct: true,
}),
);

expect(getGrade()).toBe(5);
});
});

describe('Programming', () => {
const withTestCases = {
public_test: [{ identifier: 'test1' }],
};

it('prefills maximum grade when test cases exist and answer is correct', () => {
dispatchFetchSuccess(
buildPayload({
questionType: questionTypes.Programming,
grade: null,
correct: true,
testCases: withTestCases,
}),
);

expect(getGrade()).toBe(10);
});

it('does not prefill 0 when test cases exist and answer is incorrect', () => {
dispatchFetchSuccess(
buildPayload({
questionType: questionTypes.Programming,
grade: null,
correct: false,
testCases: withTestCases,
}),
);

expect(getGrade()).toBeNull();
});

it('leaves grade as null when no test cases exist, even if answer is correct', () => {
dispatchFetchSuccess(
buildPayload({
questionType: questionTypes.Programming,
grade: null,
correct: true,
testCases: null,
}),
);

expect(getGrade()).toBeNull();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -39,58 +39,89 @@ const extractGrades = (answers) =>
return draft;
}, {});

const isSpecificAnswerGradePrefillableMap = {
[questionTypes.MultipleChoice]: () => true,
[questionTypes.MultipleResponse]: () => true,
[questionTypes.Programming]: (answer) => {
const { testCases } = answer;
const isPublicTestCasesExist = testCases?.public_test?.length > 0;
const isPrivateTestCasesExist = testCases?.private_test?.length > 0;
const isEvaluationTestCasesExist = testCases?.evaluation_test?.length > 0;
return (
isPublicTestCasesExist ||
isPrivateTestCasesExist ||
isEvaluationTestCasesExist
);
},
[questionTypes.RubricBasedResponse]: () => false,
[questionTypes.TextResponse]: () => false,
[questionTypes.Comprehension]: () => false,
[questionTypes.FileUpload]: () => false,
[questionTypes.Scribing]: () => false,
[questionTypes.VoiceResponse]: () => false,
[questionTypes.ForumPostResponse]: () => false,
const NEVER_PREFILL_POLICY = {
canPrefillFullCredit: () => false,
canPrefillZeroCredit: false,
};

const ALWAYS_PREFILL_POLICY = {
canPrefillFullCredit: () => true,
canPrefillZeroCredit: true,
};

const ONLY_PREFILL_FULL_POLICY = {
canPrefillFullCredit: () => true,
canPrefillZeroCredit: false,
};

const isAnswerGradePrefillable = (answer, questionType) => {
const isAnswerPrefillable =
answer.grading.grade === null && answer.explanation?.correct;
const isSpecificAnswerPrefillable =
isSpecificAnswerGradePrefillableMap[questionType](answer);
return isAnswerPrefillable && isSpecificAnswerPrefillable;
const PROGRAMMING_PREFILL_POLICY = {
canPrefillFullCredit: ({ testCases }) =>
(testCases?.public_test?.length ?? 0) > 0 ||
(testCases?.private_test?.length ?? 0) > 0 ||
(testCases?.evaluation_test?.length ?? 0) > 0,

// Partial grading is possible
canPrefillZeroCredit: false,
};

const prefillPolicies = {
[questionTypes.MultipleChoice]: ALWAYS_PREFILL_POLICY,
[questionTypes.MultipleResponse]: ALWAYS_PREFILL_POLICY,

[questionTypes.Programming]: PROGRAMMING_PREFILL_POLICY,

[questionTypes.TextResponse]: ONLY_PREFILL_FULL_POLICY,
[questionTypes.Comprehension]: ONLY_PREFILL_FULL_POLICY,

[questionTypes.RubricBasedResponse]: NEVER_PREFILL_POLICY,
[questionTypes.FileUpload]: NEVER_PREFILL_POLICY,
[questionTypes.Scribing]: NEVER_PREFILL_POLICY,
[questionTypes.VoiceResponse]: NEVER_PREFILL_POLICY,
[questionTypes.ForumPostResponse]: NEVER_PREFILL_POLICY,
};

const getPrefilledGrade = (answer, questionType, maxGrade) => {
const existingGrade = answer?.grading?.grade;
if (existingGrade != null) return existingGrade;

const policy = prefillPolicies[questionType];

if (
answer?.explanation?.correct === true &&
policy?.canPrefillFullCredit(answer)
) {
return maxGrade;
}

if (answer?.explanation?.correct === false && policy?.canPrefillZeroCredit) {
return 0;
}

return null;
};

/**
* Extracts grades from `payload.answer`, and pre-fills the maximum grade for correct
* answers that have not been graded. "Correct" follows the definition of
* `explanation.correct` from the server.
* Extracts grades from `payload.answer` and pre-fills:
* - maximum grade for correct answers
* - 0 for incorrect answers
* when they have not already been graded.
* "Correct" and "incorrect" follows the definition of `explanation.correct` from the server.
*/
const extractPrefillableGrades = (payload) => {
const mapQuestionIdToQuestion = arrayToObjectWithKey(payload.questions, 'id');

return payload.answers.reduce((draft, answer) => {
const { questionId, grading } = answer;
const prefillable = isAnswerGradePrefillable(
const prefilledGrade = getPrefilledGrade(
answer,
mapQuestionIdToQuestion[questionId].type,
mapQuestionIdToQuestion[questionId].maximumGrade,
);
draft[questionId] = {
...grading,
originalGrade: grading.grade,
grade: prefillable
? mapQuestionIdToQuestion[questionId].maximumGrade
: grading.grade,
prefilled: prefillable,
grade: prefilledGrade,
prefilled: grading.grade == null && prefilledGrade !== null,
};

return draft;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,13 @@

visit current_path

# Grade the submission with empty answer grade
expect(page).to have_button('Submit for Publishing', disabled: true)
find_field(class: 'grade').set(0)
wait_for_autosave
# Wrong MRQ answers are prefilled to 0 automatically, so the button is
# already enabled without staff needing to manually enter a grade.
expect(page).to have_button('Submit for Publishing', disabled: false)

find_field(class: 'exp').set(50)

click_button('Save')
expect(page).to have_button('Submit for Publishing', disabled: false)
click_button('Submit for Publishing')

expect(current_path).
Expand Down