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
4 changes: 2 additions & 2 deletions packages/number-line/controller/src/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ describe('controller', () => {
const assertOutcome = (label, question, session, env, expected) => {
it(label, async () => {
const result = await controller.outcome(question, session, env);
expect(result).toMatchObject(expected);
expect(result).toEqual(expect.objectContaining(expected));
});
};

Expand All @@ -178,7 +178,7 @@ describe('controller', () => {
};
const e = { mode: 'evaluate' };
const result = await controller.outcome(q, s, e);
expect(result).toEqual({ score: 1 });
expect(result.score).toEqual(1);
});
});

Expand Down
95 changes: 93 additions & 2 deletions packages/number-line/controller/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,100 @@ const accumulateAnswer = (correctResponse) => (total, answer) => {
return total + (isCorrectResponse ? 1 : 0);
};

/**
* Generates detailed trace log for scoring evaluation
* @param {Object} model - the question model
* @param {Object} session - the student session
* @param {Object} env - the environment
* @returns {Array} traceLog - array of trace messages
*/
export const getLogTrace = (model, session, env) => {
const traceLog = [];

const studentAnswers = session?.answer || [];
const correctResponse = model?.correctResponse || [];

if (!studentAnswers.length) {
return ['Student did not interact with the number line.'];
}

const corrected = getCorrected(studentAnswers, cloneDeep(correctResponse));
const { correct, incorrect, notInAnswer, noCorrectResponse } = corrected;

if (noCorrectResponse) {
return ['No correct response is defined for this item.'];
}

const correctCount = correct.length;
const incorrectCount = incorrect.length;
const missingCount = notInAnswer.length;

if (correctCount > 0) {
traceLog.push(`${correctCount} object(s) correctly placed.`);
}

if (incorrectCount > 0) {
traceLog.push(`${incorrectCount} incorrect object(s) placed.`);
}

if (missingCount > 0) {
traceLog.push(`${missingCount} expected object(s) were not placed.`);
}

const studentAnswersCopy = [...studentAnswers];
const correctResponseCopy = [...correctResponse];

correct.forEach((index) => {
const answer = studentAnswersCopy[index];
const objectType = answer?.type || 'object';
traceLog.push(`${objectType.charAt(0).toUpperCase() + objectType.slice(1)} at position ${answer.domainPosition} is correct.`);
});

incorrect.forEach((index) => {
const answer = studentAnswersCopy[index];
const objectType = answer?.type || 'object';
traceLog.push(`${objectType.charAt(0).toUpperCase() + objectType.slice(1)} at position ${answer.domainPosition} does not match the expected response.`);
});

notInAnswer.forEach((expectedObject) => {
const objectType = expectedObject?.type || 'object';
traceLog.push(`Expected ${objectType} at position ${expectedObject.domainPosition} was not placed by the student.`);
});

const partialScoringEnabled = partialScoring.enabled(model, env);

if (partialScoringEnabled) {
traceLog.push('Score calculated using partial scoring.');
traceLog.push(`Partial scoring is based on the number of correct objects, with deductions for extras.`);
} else {
traceLog.push('Score calculated using all-or-nothing scoring.');
}

const total = correctResponse.length || 1;
const extraPlacements =
studentAnswers.length > total ? studentAnswers.length - total : 0;

if (extraPlacements > 0) {
traceLog.push(
`${extraPlacements} extra object(s) beyond the required amount were placed and deducted.`,
);
}

const rawScore = Math.max(0, (correctCount - extraPlacements) / total);
const finalScore = partialScoringEnabled ? rawScore : rawScore === 1 ? 1 : 0;

traceLog.push(`Final score: ${finalScore}.`);

return traceLog;
};


/**
*/
export function outcome(model, session, env) {
return new Promise((resolve) => {
if (!session || isEmpty(session)) {
resolve({ score: 0, empty: true });
resolve({ score: 0, empty: true, logTrace: ['Student did not interact with the number line.'] });
} else {
const partialScoringEnabled = partialScoring.enabled(model, env);
const numCorrect = (session.answer || []).reduce(accumulateAnswer(model.correctResponse), 0);
Expand All @@ -62,7 +150,10 @@ export function outcome(model, session, env) {
score = 0;
}

resolve({ score: partialScoringEnabled ? score : score === 1 ? 1 : 0 });
resolve({
score: partialScoringEnabled ? score : score === 1 ? 1 : 0,
logTrace: getLogTrace(model, session, env)
});
}
});
}
Expand Down