Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Partial scoring for pick-all questions #2020

Open
wants to merge 5 commits into
base: dev/33-serpierite
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
Expand Up @@ -6221,13 +6221,13 @@
{
"data": {},
"text": {
"value": "Pick all correct answers questions allow a student to select as many answers as they want. A student must select all the correct choices to get the points for that question.",
"value": "Pick all correct answers questions allow a student to select as many answers as they want. By default, a student must select all the correct choices to get the points for that question.",
"styleList": [
{
"end": 105,
"end": 117,
"data": {},
"type": "b",
"start": 100
"start": 112
}
]
}
Expand All @@ -6236,6 +6236,29 @@
},
"children": []
},
{
"id": "e081e958-6130-40b6-b4c4-30b956a61143",
"type": "ObojoboDraft.Chunks.Text",
"content": {
"textGroup": [
{
"data": {},
"text": {
"value": "Alternatively, Partial Scoring can be turned on which allows students to receive partial credit for the question based on how many correct and incorrect choices they select.",
"styleList": null
}
},
{
"data": {},
"text": {
"value": "In the question below, selecting only one correct answer would result in a 50%, and selecting one correct answer along with the incorrect answer would result in a 0%. Selecting all three answer choices would result in a 50% as well, as the incorrect choice cancels out one of the correct choices.",
"styleList": null
}
}
]
},
"children": []
},
{
"id": "a687a99d-c517-4dc9-9bdd-4c3352d3618f",
"type": "ObojoboDraft.Chunks.Question",
Expand All @@ -6261,6 +6284,15 @@
}
]
}
},
{
"data": {
"indent": 0
},
"text": {
"value": "If Partial Scoring is enabled, then the student can receive partial credit even if they didn't get it completely correct.",
"styleList": null
}
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
exports[`MCAssessment adapter can convert to JSON 1`] = `
Object {
"content": Object {
"partialScoring": false,
"responseType": "pick-one",
"shuffle": true,
},
Expand All @@ -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,
},
Expand All @@ -21,6 +23,7 @@ Object {
exports[`MCAssessment adapter construct builds with attributes 1`] = `
Object {
"modelState": Object {
"partialScoring": false,
"responseType": "pick-one",
"shuffle": false,
},
Expand All @@ -30,6 +33,7 @@ Object {
exports[`MCAssessment adapter construct builds with responseType 1`] = `
Object {
"modelState": Object {
"partialScoring": false,
"responseType": "pick-one",
"shuffle": true,
},
Expand All @@ -39,6 +43,7 @@ Object {
exports[`MCAssessment adapter construct builds with shuffle 1`] = `
Object {
"modelState": Object {
"partialScoring": false,
"responseType": "pick-one",
"shuffle": true,
},
Expand All @@ -48,6 +53,7 @@ Object {
exports[`MCAssessment adapter construct builds without attributes 1`] = `
Object {
"modelState": Object {
"partialScoring": false,
"responseType": "pick-one",
"shuffle": true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ describe('MCAssessment adapter', () => {
const attrs = {
content: {
responseType: 'pick-one',
shuffle: false
shuffle: false,
partialScoring: false
}
}

Expand Down Expand Up @@ -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
}
}

Expand All @@ -101,7 +105,8 @@ describe('MCAssessment adapter', () => {
const attrs = {
content: {
responseType: 'pick-one',
shuffle: true
shuffle: true,
partialScoring: false
}
}
const json = { content: {} }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"type": "ObojoboDraft.Chunks.MCAssessment",
"content": {
"responseType": "pick-one",
"shuffle": true
"shuffle": true,
"partialScoring": false
},
"children": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,22 @@ class MCAssessment extends DraftNode {
})
)

const partialScoring = this.node.content.partialScoring || false
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 (!partialScoring && numCorrect !== correctIds.size) return setScore(0)

if (numCorrect <= 0) score = 0
else score = (100 * numCorrect) / correctIds.size

setScore(score)
break
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,20 @@ 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' }
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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}
Expand Down
59 changes: 49 additions & 10 deletions packages/obonode/obojobo-chunks-question/editor-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import withSlateWrapper from 'obojobo-document-engine/src/scripts/oboeditor/comp
import React from 'react'
import Node from 'obojobo-document-engine/src/scripts/oboeditor/components/node/editor-component'

const { Button } = Common.components
const { Button, MoreInfoButton } = Common.components
const QUESTION_NODE = 'ObojoboDraft.Chunks.Question'
const SOLUTION_NODE = 'ObojoboDraft.Chunks.Question.Solution'
const MCASSESSMENT_NODE = 'ObojoboDraft.Chunks.MCAssessment'
Expand All @@ -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()
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -165,15 +184,12 @@ class Question extends React.Component {
const isTypeSurvey = content.type === 'survey'

const hasSolution = this.getHasSolution()
let questionType

// The question type is determined by the MCAssessment or the NumericAssessement
// This is either the last node or the second to last node
if (hasSolution) {
questionType = element.children[element.children.length - 2].type
} else {
questionType = element.children[element.children.length - 1].type
}
// The question type is determined by the MCAssessment or the NumericAssessment
// This is either the last node or the second to last node depending on whether the
// 'explanation' area is visible
const questionElement = element.children[element.children.length - (hasSolution ? 2 : 1)]
const questionType = questionElement.type
const partialScoring = questionElement.content.partialScoring || false

return (
<Node
Expand All @@ -196,6 +212,29 @@ class Question extends React.Component {
<option value={MCASSESSMENT_NODE}>Multiple choice</option>
<option value={NUMERIC_ASSESSMENT_NODE}>Input a number</option>
</select>
{questionType === MCASSESSMENT_NODE &&
questionElement.content.responseType === 'pick-all' ? (
<React.Fragment>
<span className="scoring-explanation">
<MoreInfoButton>
<div className="text-container">
<p className="text">
Students will earn partial credit based on how many of the correct
answers they select.
</p>
</div>
</MoreInfoButton>
</span>
<label className="question-type scoring" contentEditable={false}>
<input
type="checkbox"
checked={partialScoring}
onChange={this.onSetScoring}
/>
Partial Scoring
</label>
</React.Fragment>
) : null}
<label className="question-type" contentEditable={false}>
<input type="checkbox" checked={isTypeSurvey} onChange={this.onSetType} />
Survey Only
Expand Down
Loading