From bef2f8b0e318cf4ed11eeffb30348001dce2e8e6 Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Mon, 1 Aug 2022 12:50:06 -0400 Subject: [PATCH 1/4] adds partial scoring for pick-all questions --- .../server/mcassessment.js | 13 +++++++---- .../server/mcassessment.test.js | 4 ++-- .../viewer-component.js | 22 ++++++++++++------- .../viewer-component.test.js | 8 +++---- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/packages/obonode/obojobo-chunks-multiple-choice-assessment/server/mcassessment.js b/packages/obonode/obojobo-chunks-multiple-choice-assessment/server/mcassessment.js index 5ff85e4ae2..1f2d20bf3a 100644 --- a/packages/obonode/obojobo-chunks-multiple-choice-assessment/server/mcassessment.js +++ b/packages/obonode/obojobo-chunks-multiple-choice-assessment/server/mcassessment.js @@ -24,12 +24,17 @@ class MCAssessment extends DraftNode { const responseIds = new Set(responseRecord.response.ids) - if (correctIds.size !== responseIds.size) return setScore(0) + let score, + numCorrect = 0 - let score = 100 - correctIds.forEach(id => { - if (!responseIds.has(id)) score = 0 + responseIds.forEach(id => { + if (correctIds.has(id)) numCorrect++ + else numCorrect-- }) + + if (numCorrect <= 0) score = 0 + else score = (100 * numCorrect) / correctIds.size + setScore(score) break } diff --git a/packages/obonode/obojobo-chunks-multiple-choice-assessment/server/mcassessment.test.js b/packages/obonode/obojobo-chunks-multiple-choice-assessment/server/mcassessment.test.js index b1261b7847..9093ca4b3e 100644 --- a/packages/obonode/obojobo-chunks-multiple-choice-assessment/server/mcassessment.test.js +++ b/packages/obonode/obojobo-chunks-multiple-choice-assessment/server/mcassessment.test.js @@ -66,14 +66,14 @@ describe('MCAssessment', () => { expect(setScore).toHaveBeenCalledWith(0) }) - test('onCalculateScore sets score to 0 if number of chosen !== number of correct answers (pick-all)', () => { + test('onCalculateScore determines partial score if number of chosen !== number of correct answers (pick-all)', () => { const question = { contains: () => true } const responseRecord = { response: { ids: ['test'] } } mcAssessment.node.content = { responseType: 'pick-all' } expect(setScore).not.toHaveBeenCalled() mcAssessment.onCalculateScore({}, question, responseRecord, setScore) - expect(setScore).toHaveBeenCalledWith(0) + expect(setScore).toHaveBeenCalledWith(50) }) test('onCalculateScore sets score to 0 if any chosen answers are not the correct answer (pick-all)', () => { diff --git a/packages/obonode/obojobo-chunks-multiple-choice-assessment/viewer-component.js b/packages/obonode/obojobo-chunks-multiple-choice-assessment/viewer-component.js index 8e18d7da8b..a6ce61ceeb 100644 --- a/packages/obonode/obojobo-chunks-multiple-choice-assessment/viewer-component.js +++ b/packages/obonode/obojobo-chunks-multiple-choice-assessment/viewer-component.js @@ -119,17 +119,23 @@ export default class MCAssessment extends OboQuestionAssessmentComponent { switch (this.props.model.modelState.responseType) { case 'pick-all': { - if (correct.size !== responses.size) { - return { score: 0, details: null } - } - - let score = 100 - correct.forEach(function(id) { - if (!responses.has(id)) { - score = 0 + let score, + numCorrect = 0 + + responses.forEach(function(id) { + if (correct.has(id)) { + numCorrect++ + } else { + numCorrect-- } }) + if (numCorrect <= 0) { + score = 0 + } else { + score = (100 * numCorrect) / correct.size + } + return { score, details: null } } diff --git a/packages/obonode/obojobo-chunks-multiple-choice-assessment/viewer-component.test.js b/packages/obonode/obojobo-chunks-multiple-choice-assessment/viewer-component.test.js index 0f4e1da696..74a9276465 100644 --- a/packages/obonode/obojobo-chunks-multiple-choice-assessment/viewer-component.test.js +++ b/packages/obonode/obojobo-chunks-multiple-choice-assessment/viewer-component.test.js @@ -238,8 +238,8 @@ describe('MCAssessmentViewerComponent', () => { ${'pick-one-multiple-correct'} | ${['b', 'c', 'd']} | ${100} ${'pick-one-multiple-correct'} | ${['a', 'b', 'c', 'd']} | ${100} ${'pick-all'} | ${[]} | ${0} - ${'pick-all'} | ${['a']} | ${0} - ${'pick-all'} | ${['b']} | ${0} + ${'pick-all'} | ${['a']} | ${50} + ${'pick-all'} | ${['b']} | ${50} ${'pick-all'} | ${['c']} | ${0} ${'pick-all'} | ${['d']} | ${0} ${'pick-all'} | ${['a', 'b']} | ${100} @@ -248,8 +248,8 @@ describe('MCAssessmentViewerComponent', () => { ${'pick-all'} | ${['b', 'c']} | ${0} ${'pick-all'} | ${['b', 'd']} | ${0} ${'pick-all'} | ${['c', 'd']} | ${0} - ${'pick-all'} | ${['a', 'b', 'c']} | ${0} - ${'pick-all'} | ${['a', 'b', 'd']} | ${0} + ${'pick-all'} | ${['a', 'b', 'c']} | ${50} + ${'pick-all'} | ${['a', 'b', 'd']} | ${50} ${'pick-all'} | ${['a', 'c', 'd']} | ${0} ${'pick-all'} | ${['b', 'c', 'd']} | ${0} ${'pick-all'} | ${['a', 'b', 'c', 'd']} | ${0} From b76faba35b0151eee5aa031c423b92a40b0f33c2 Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Fri, 19 Aug 2022 14:44:19 -0400 Subject: [PATCH 2/4] adds a toggle to turn partial scoring on or off, defaults to off --- .../__snapshots__/adapter.test.js.snap | 6 + .../adapter.js | 3 + .../adapter.test.js | 15 +- .../converter.js | 3 +- .../empty-node.json | 3 +- .../server/mcassessment.js | 3 + .../server/mcassessment.test.js | 12 +- .../editor-component.js | 30 +++- .../editor-component.scss | 4 + .../editor-component.test.js | 137 ++++++++++++++++-- .../obojobo-chunks-question/empty-node.json | 3 +- .../__snapshots__/basic-review.test.js.snap | 3 + 12 files changed, 199 insertions(+), 23 deletions(-) diff --git a/packages/obonode/obojobo-chunks-multiple-choice-assessment/__snapshots__/adapter.test.js.snap b/packages/obonode/obojobo-chunks-multiple-choice-assessment/__snapshots__/adapter.test.js.snap index 90146ee0ec..a5430cc36c 100644 --- a/packages/obonode/obojobo-chunks-multiple-choice-assessment/__snapshots__/adapter.test.js.snap +++ b/packages/obonode/obojobo-chunks-multiple-choice-assessment/__snapshots__/adapter.test.js.snap @@ -3,6 +3,7 @@ exports[`MCAssessment adapter can convert to JSON 1`] = ` Object { "content": Object { + "partialScoring": false, "responseType": "pick-one", "shuffle": true, }, @@ -12,6 +13,7 @@ Object { exports[`MCAssessment adapter can convert to JSON WITH attributes 1`] = ` Object { "content": Object { + "partialScoring": false, "responseType": "pick-one", "shuffle": true, }, @@ -21,6 +23,7 @@ Object { exports[`MCAssessment adapter construct builds with attributes 1`] = ` Object { "modelState": Object { + "partialScoring": false, "responseType": "pick-one", "shuffle": false, }, @@ -30,6 +33,7 @@ Object { exports[`MCAssessment adapter construct builds with responseType 1`] = ` Object { "modelState": Object { + "partialScoring": false, "responseType": "pick-one", "shuffle": true, }, @@ -39,6 +43,7 @@ Object { exports[`MCAssessment adapter construct builds with shuffle 1`] = ` Object { "modelState": Object { + "partialScoring": false, "responseType": "pick-one", "shuffle": true, }, @@ -48,6 +53,7 @@ Object { exports[`MCAssessment adapter construct builds without attributes 1`] = ` Object { "modelState": Object { + "partialScoring": false, "responseType": "pick-one", "shuffle": true, }, diff --git a/packages/obonode/obojobo-chunks-multiple-choice-assessment/adapter.js b/packages/obonode/obojobo-chunks-multiple-choice-assessment/adapter.js index 05cc36fafb..5a0bfc46a6 100644 --- a/packages/obonode/obojobo-chunks-multiple-choice-assessment/adapter.js +++ b/packages/obonode/obojobo-chunks-multiple-choice-assessment/adapter.js @@ -4,16 +4,19 @@ const Adapter = { model.modelState.responseType = content.responseType || 'pick-one' model.modelState.shuffle = content.shuffle !== false + model.modelState.partialScoring = content.partialScoring === true }, clone(model, clone) { clone.modelState.responseType = model.modelState.responseType clone.modelState.shuffle = model.modelState.shuffle + clone.modelState.partialScoring = model.modelState.partialScoring }, toJSON(model, json) { json.content.responseType = model.modelState.responseType json.content.shuffle = model.modelState.shuffle + json.content.partialScoring = model.modelState.partialScoring } } diff --git a/packages/obonode/obojobo-chunks-multiple-choice-assessment/adapter.test.js b/packages/obonode/obojobo-chunks-multiple-choice-assessment/adapter.test.js index f73058af9a..3549d6dc8e 100644 --- a/packages/obonode/obojobo-chunks-multiple-choice-assessment/adapter.test.js +++ b/packages/obonode/obojobo-chunks-multiple-choice-assessment/adapter.test.js @@ -12,7 +12,8 @@ describe('MCAssessment adapter', () => { const attrs = { content: { responseType: 'pick-one', - shuffle: false + shuffle: false, + partialScoring: false } } @@ -62,19 +63,22 @@ describe('MCAssessment adapter', () => { const a = { modelState: { responseType: 'pick-one', - shuffle: true + shuffle: true, + partialScoring: false } } const attrs = { content: { responseType: 'pick-one', - shuffle: true + shuffle: true, + partialScoring: false } } const b = { modelState: { responseType: null, - shuffle: false + shuffle: false, + partialScoring: false } } @@ -101,7 +105,8 @@ describe('MCAssessment adapter', () => { const attrs = { content: { responseType: 'pick-one', - shuffle: true + shuffle: true, + partialScoring: false } } const json = { content: {} } diff --git a/packages/obonode/obojobo-chunks-multiple-choice-assessment/converter.js b/packages/obonode/obojobo-chunks-multiple-choice-assessment/converter.js index 5bf591dd15..41d37f78ab 100644 --- a/packages/obonode/obojobo-chunks-multiple-choice-assessment/converter.js +++ b/packages/obonode/obojobo-chunks-multiple-choice-assessment/converter.js @@ -37,7 +37,8 @@ const slateToObo = node => { content: withoutUndefined({ triggers: node.content.triggers, responseType, - shuffle: node.content.shuffle + shuffle: node.content.shuffle, + partialScoring: node.content.partialScoring }) } } diff --git a/packages/obonode/obojobo-chunks-multiple-choice-assessment/empty-node.json b/packages/obonode/obojobo-chunks-multiple-choice-assessment/empty-node.json index 292182baeb..fb43eec519 100644 --- a/packages/obonode/obojobo-chunks-multiple-choice-assessment/empty-node.json +++ b/packages/obonode/obojobo-chunks-multiple-choice-assessment/empty-node.json @@ -2,7 +2,8 @@ "type": "ObojoboDraft.Chunks.MCAssessment", "content": { "responseType": "pick-one", - "shuffle": true + "shuffle": true, + "partialScoring": false }, "children": [ { diff --git a/packages/obonode/obojobo-chunks-multiple-choice-assessment/server/mcassessment.js b/packages/obonode/obojobo-chunks-multiple-choice-assessment/server/mcassessment.js index 1f2d20bf3a..1891a8b5b5 100644 --- a/packages/obonode/obojobo-chunks-multiple-choice-assessment/server/mcassessment.js +++ b/packages/obonode/obojobo-chunks-multiple-choice-assessment/server/mcassessment.js @@ -22,6 +22,7 @@ class MCAssessment extends DraftNode { }) ) + const partialScoring = this.node.content.partialScoring || false const responseIds = new Set(responseRecord.response.ids) let score, @@ -32,6 +33,8 @@ class MCAssessment extends DraftNode { else numCorrect-- }) + if (!partialScoring && numCorrect !== correctIds.size) return setScore(0) + if (numCorrect <= 0) score = 0 else score = (100 * numCorrect) / correctIds.size diff --git a/packages/obonode/obojobo-chunks-multiple-choice-assessment/server/mcassessment.test.js b/packages/obonode/obojobo-chunks-multiple-choice-assessment/server/mcassessment.test.js index 9093ca4b3e..0aeb9fff57 100644 --- a/packages/obonode/obojobo-chunks-multiple-choice-assessment/server/mcassessment.test.js +++ b/packages/obonode/obojobo-chunks-multiple-choice-assessment/server/mcassessment.test.js @@ -69,13 +69,23 @@ describe('MCAssessment', () => { test('onCalculateScore determines partial score if number of chosen !== number of correct answers (pick-all)', () => { const question = { contains: () => true } const responseRecord = { response: { ids: ['test'] } } - mcAssessment.node.content = { responseType: 'pick-all' } + mcAssessment.node.content = { responseType: 'pick-all', partialScoring: true } expect(setScore).not.toHaveBeenCalled() mcAssessment.onCalculateScore({}, question, responseRecord, setScore) expect(setScore).toHaveBeenCalledWith(50) }) + test('onCalculateScore does not give negative score with partial scoring (pick-all)', () => { + const question = { contains: () => true } + const responseRecord = { response: { ids: ['test2', 'test3'] } } + mcAssessment.node.content = { responseType: 'pick-all', partialScoring: true } + + expect(setScore).not.toHaveBeenCalled() + mcAssessment.onCalculateScore({}, question, responseRecord, setScore) + expect(setScore).toHaveBeenCalledWith(0) + }) + test('onCalculateScore sets score to 0 if any chosen answers are not the correct answer (pick-all)', () => { const question = { contains: () => true } const responseRecord = { response: { ids: ['test', 'test2'] } } diff --git a/packages/obonode/obojobo-chunks-question/editor-component.js b/packages/obonode/obojobo-chunks-question/editor-component.js index 8a7c3b8e62..2863bca28e 100644 --- a/packages/obonode/obojobo-chunks-question/editor-component.js +++ b/packages/obonode/obojobo-chunks-question/editor-component.js @@ -24,6 +24,7 @@ class Question extends React.Component { this.addSolution = this.addSolution.bind(this) this.delete = this.delete.bind(this) this.onSetType = this.onSetType.bind(this) + this.onSetScoring = this.onSetScoring.bind(this) this.onSetAssessmentType = this.onSetAssessmentType.bind(this) this.isInAssessment = this.getIsInAssessment() } @@ -61,6 +62,24 @@ class Question extends React.Component { ) } + onSetScoring(event) { + const hasSolution = this.getHasSolution() + let assessmentNode + + if (hasSolution) { + assessmentNode = this.props.element.children[this.props.element.children.length - 2] + } else { + assessmentNode = this.props.element.children[this.props.element.children.length - 1] + } + + const path = ReactEditor.findPath(this.props.editor, assessmentNode) + return Transforms.setNodes( + this.props.editor, + { content: { ...assessmentNode.content, partialScoring: event.target.checked } }, + { at: path } + ) + } + getHasSolution() { return ( this.props.element.children[this.props.element.children.length - 1].subtype === SOLUTION_NODE @@ -166,13 +185,16 @@ class Question extends React.Component { const hasSolution = this.getHasSolution() let questionType + let partialScoring - // The question type is determined by the MCAssessment or the NumericAssessement + // The question type is determined by the MCAssessment or the NumericAssessment // This is either the last node or the second to last node if (hasSolution) { questionType = element.children[element.children.length - 2].type + partialScoring = element.children[element.children.length - 2].content.partialScoring || false } else { questionType = element.children[element.children.length - 1].type + partialScoring = element.children[element.children.length - 1].content.partialScoring || false } return ( @@ -196,6 +218,12 @@ class Question extends React.Component { + {questionType === MCASSESSMENT_NODE ? ( + + ) : null}