@khanacademy/perseus-score 1.1.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/index.js +424 -196
- package/dist/es/index.js.map +1 -1
- package/dist/index.d.ts +14 -0
- package/dist/index.js +437 -194
- package/dist/index.js.map +1 -1
- package/dist/score.d.ts +21 -0
- package/dist/util/score-noop.d.ts +11 -0
- package/dist/util/test-helpers.d.ts +14 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validation.types.d.ts +136 -32
- package/dist/validation.typetest.d.ts +1 -0
- package/dist/widgets/categorizer/score-categorizer.d.ts +2 -2
- package/dist/widgets/cs-program/score-cs-program.d.ts +1 -1
- package/dist/widgets/expression/score-expression.d.ts +1 -1
- package/dist/widgets/grapher/score-grapher.d.ts +1 -1
- package/dist/widgets/group/score-group.d.ts +3 -0
- package/dist/widgets/group/validate-group.d.ts +3 -0
- package/dist/widgets/interactive-graph/score-interactive-graph.d.ts +1 -1
- package/dist/widgets/label-image/score-label-image.d.ts +2 -3
- package/dist/widgets/label-image/validate-label-image.d.ts +3 -0
- package/dist/widgets/matcher/score-matcher.d.ts +1 -1
- package/dist/widgets/matrix/score-matrix.d.ts +1 -1
- package/dist/widgets/matrix/validate-matrix.d.ts +2 -2
- package/dist/widgets/mock-widget/mock-widget-validation.types.d.ts +6 -0
- package/dist/widgets/mock-widget/score-mock-widget.d.ts +4 -0
- package/dist/widgets/mock-widget/validate-mock-widget.d.ts +4 -0
- package/dist/widgets/number-line/score-number-line.d.ts +2 -2
- package/dist/widgets/plotter/score-plotter.d.ts +2 -2
- package/dist/widgets/sorter/score-sorter.d.ts +1 -1
- package/dist/widgets/table/score-table.d.ts +1 -1
- package/dist/widgets/widget-registry.d.ts +4 -0
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -12551,6 +12551,21 @@ const KhanAnswerTypes = {
|
|
|
12551
12551
|
}
|
|
12552
12552
|
};
|
|
12553
12553
|
|
|
12554
|
+
function scoreCategorizer(userInput, rubric) {
|
|
12555
|
+
let allCorrect = true;
|
|
12556
|
+
rubric.values.forEach((value, i) => {
|
|
12557
|
+
if (userInput.values[i] !== value) {
|
|
12558
|
+
allCorrect = false;
|
|
12559
|
+
}
|
|
12560
|
+
});
|
|
12561
|
+
return {
|
|
12562
|
+
type: "points",
|
|
12563
|
+
earned: allCorrect ? 1 : 0,
|
|
12564
|
+
total: 1,
|
|
12565
|
+
message: null
|
|
12566
|
+
};
|
|
12567
|
+
}
|
|
12568
|
+
|
|
12554
12569
|
/**
|
|
12555
12570
|
* Checks userInput from the categorizer widget to see if the user has selected
|
|
12556
12571
|
* a category for each item.
|
|
@@ -12570,43 +12585,23 @@ function validateCategorizer(userInput, validationData) {
|
|
|
12570
12585
|
return null;
|
|
12571
12586
|
}
|
|
12572
12587
|
|
|
12573
|
-
function
|
|
12574
|
-
const validationError = validateCategorizer(userInput, scoringData);
|
|
12575
|
-
if (validationError) {
|
|
12576
|
-
return validationError;
|
|
12577
|
-
}
|
|
12578
|
-
let allCorrect = true;
|
|
12579
|
-
scoringData.values.forEach((value, i) => {
|
|
12580
|
-
if (userInput.values[i] !== value) {
|
|
12581
|
-
allCorrect = false;
|
|
12582
|
-
}
|
|
12583
|
-
});
|
|
12584
|
-
return {
|
|
12585
|
-
type: "points",
|
|
12586
|
-
earned: allCorrect ? 1 : 0,
|
|
12587
|
-
total: 1,
|
|
12588
|
-
message: null
|
|
12589
|
-
};
|
|
12590
|
-
}
|
|
12591
|
-
|
|
12592
|
-
// TODO: merge this with scoreIframe, it's the same code
|
|
12593
|
-
function scoreCSProgram(state) {
|
|
12588
|
+
function scoreCSProgram(userInput) {
|
|
12594
12589
|
// The CS program can tell us whether it's correct or incorrect,
|
|
12595
12590
|
// and pass an optional message
|
|
12596
|
-
if (
|
|
12591
|
+
if (userInput.status === "correct") {
|
|
12597
12592
|
return {
|
|
12598
12593
|
type: "points",
|
|
12599
12594
|
earned: 1,
|
|
12600
12595
|
total: 1,
|
|
12601
|
-
message:
|
|
12596
|
+
message: userInput.message || null
|
|
12602
12597
|
};
|
|
12603
12598
|
}
|
|
12604
|
-
if (
|
|
12599
|
+
if (userInput.status === "incorrect") {
|
|
12605
12600
|
return {
|
|
12606
12601
|
type: "points",
|
|
12607
12602
|
earned: 0,
|
|
12608
12603
|
total: 1,
|
|
12609
|
-
message:
|
|
12604
|
+
message: userInput.message || null
|
|
12610
12605
|
};
|
|
12611
12606
|
}
|
|
12612
12607
|
return {
|
|
@@ -12615,25 +12610,7 @@ function scoreCSProgram(state) {
|
|
|
12615
12610
|
};
|
|
12616
12611
|
}
|
|
12617
12612
|
|
|
12618
|
-
/**
|
|
12619
|
-
* Checks if the user has selected an item from the dropdown before scoring.
|
|
12620
|
-
* This is shown with a userInput value / index other than 0.
|
|
12621
|
-
*/
|
|
12622
|
-
function validateDropdown(userInput) {
|
|
12623
|
-
if (userInput.value === 0) {
|
|
12624
|
-
return {
|
|
12625
|
-
type: "invalid",
|
|
12626
|
-
message: null
|
|
12627
|
-
};
|
|
12628
|
-
}
|
|
12629
|
-
return null;
|
|
12630
|
-
}
|
|
12631
|
-
|
|
12632
12613
|
function scoreDropdown(userInput, rubric) {
|
|
12633
|
-
const validationError = validateDropdown(userInput);
|
|
12634
|
-
if (validationError) {
|
|
12635
|
-
return validationError;
|
|
12636
|
-
}
|
|
12637
12614
|
const correct = rubric.choices[userInput.value - 1].correct;
|
|
12638
12615
|
return {
|
|
12639
12616
|
type: "points",
|
|
@@ -12644,15 +12621,11 @@ function scoreDropdown(userInput, rubric) {
|
|
|
12644
12621
|
}
|
|
12645
12622
|
|
|
12646
12623
|
/**
|
|
12647
|
-
* Checks
|
|
12648
|
-
*
|
|
12649
|
-
* Note: Most of the expression widget's validation requires the Rubric because
|
|
12650
|
-
* of its use of KhanAnswerTypes as a core part of scoring.
|
|
12651
|
-
*
|
|
12652
|
-
* @see `scoreExpression()` for more details.
|
|
12624
|
+
* Checks if the user has selected an item from the dropdown before scoring.
|
|
12625
|
+
* This is shown with a userInput value / index other than 0.
|
|
12653
12626
|
*/
|
|
12654
|
-
function
|
|
12655
|
-
if (userInput ===
|
|
12627
|
+
function validateDropdown(userInput) {
|
|
12628
|
+
if (userInput.value === 0) {
|
|
12656
12629
|
return {
|
|
12657
12630
|
type: "invalid",
|
|
12658
12631
|
message: null
|
|
@@ -12679,13 +12652,7 @@ function validateExpression(userInput) {
|
|
|
12679
12652
|
* show the user an error. TODO(joel) - what error?
|
|
12680
12653
|
* - Otherwise, pass through the resulting points and message.
|
|
12681
12654
|
*/
|
|
12682
|
-
function scoreExpression(userInput, rubric,
|
|
12683
|
-
// TODO: remove strings as a param for scorers
|
|
12684
|
-
strings, locale) {
|
|
12685
|
-
const validationError = validateExpression(userInput);
|
|
12686
|
-
if (validationError) {
|
|
12687
|
-
return validationError;
|
|
12688
|
-
}
|
|
12655
|
+
function scoreExpression(userInput, rubric, locale) {
|
|
12689
12656
|
const options = _.clone(rubric);
|
|
12690
12657
|
_.extend(options, {
|
|
12691
12658
|
decimal_separator: perseusCore.getDecimalSeparator(locale)
|
|
@@ -12701,7 +12668,11 @@ strings, locale) {
|
|
|
12701
12668
|
// in the function variables list for the expression.
|
|
12702
12669
|
if (!expression.parsed) {
|
|
12703
12670
|
/* c8 ignore next */
|
|
12704
|
-
throw new perseusCore.PerseusError("Unable to parse solution answer for expression", perseusCore.Errors.InvalidInput
|
|
12671
|
+
throw new perseusCore.PerseusError("Unable to parse solution answer for expression", perseusCore.Errors.InvalidInput, {
|
|
12672
|
+
metadata: {
|
|
12673
|
+
rubric: JSON.stringify(rubric)
|
|
12674
|
+
}
|
|
12675
|
+
});
|
|
12705
12676
|
}
|
|
12706
12677
|
return KhanAnswerTypes.expression.createValidatorFunctional(expression.expr, _({}).extend(options, {
|
|
12707
12678
|
simplify: answer.simplify,
|
|
@@ -12796,6 +12767,24 @@ strings, locale) {
|
|
|
12796
12767
|
};
|
|
12797
12768
|
}
|
|
12798
12769
|
|
|
12770
|
+
/**
|
|
12771
|
+
* Checks user input from the expression widget to see if it is scorable.
|
|
12772
|
+
*
|
|
12773
|
+
* Note: Most of the expression widget's validation requires the Rubric because
|
|
12774
|
+
* of its use of KhanAnswerTypes as a core part of scoring.
|
|
12775
|
+
*
|
|
12776
|
+
* @see `scoreExpression()` for more details.
|
|
12777
|
+
*/
|
|
12778
|
+
function validateExpression(userInput) {
|
|
12779
|
+
if (userInput === "") {
|
|
12780
|
+
return {
|
|
12781
|
+
type: "invalid",
|
|
12782
|
+
message: null
|
|
12783
|
+
};
|
|
12784
|
+
}
|
|
12785
|
+
return null;
|
|
12786
|
+
}
|
|
12787
|
+
|
|
12799
12788
|
function getCoefficientsByType(data) {
|
|
12800
12789
|
if (data.coords == null) {
|
|
12801
12790
|
return undefined;
|
|
@@ -13097,47 +13086,33 @@ function scoreInteractiveGraph(userInput, rubric) {
|
|
|
13097
13086
|
|
|
13098
13087
|
// Question state for marker as result of user selected answers.
|
|
13099
13088
|
|
|
13100
|
-
function scoreLabelImageMarker(
|
|
13089
|
+
function scoreLabelImageMarker(userInput, rubric) {
|
|
13101
13090
|
const score = {
|
|
13102
13091
|
hasAnswers: false,
|
|
13103
13092
|
isCorrect: false
|
|
13104
13093
|
};
|
|
13105
|
-
if (
|
|
13094
|
+
if (userInput && userInput.length > 0) {
|
|
13106
13095
|
score.hasAnswers = true;
|
|
13107
13096
|
}
|
|
13108
|
-
if (
|
|
13109
|
-
if (
|
|
13097
|
+
if (rubric.length > 0) {
|
|
13098
|
+
if (userInput && userInput.length === rubric.length) {
|
|
13110
13099
|
// All correct answers are selected by the user.
|
|
13111
|
-
score.isCorrect =
|
|
13100
|
+
score.isCorrect = userInput.every(choice => rubric.includes(choice));
|
|
13112
13101
|
}
|
|
13113
|
-
} else if (!
|
|
13102
|
+
} else if (!userInput || userInput.length === 0) {
|
|
13114
13103
|
// Correct as no answers should be selected by the user.
|
|
13115
13104
|
score.isCorrect = true;
|
|
13116
13105
|
}
|
|
13117
13106
|
return score;
|
|
13118
13107
|
}
|
|
13119
|
-
|
|
13120
|
-
// TODO(LEMS-2440): May need to pull answers out of PerseusLabelImageWidgetOptions[markers] for the rubric
|
|
13121
13108
|
function scoreLabelImage(userInput, rubric) {
|
|
13122
|
-
let numAnswered = 0;
|
|
13123
13109
|
let numCorrect = 0;
|
|
13124
|
-
for (
|
|
13125
|
-
const score = scoreLabelImageMarker(
|
|
13126
|
-
if (score.hasAnswers) {
|
|
13127
|
-
numAnswered++;
|
|
13128
|
-
}
|
|
13110
|
+
for (let i = 0; i < userInput.markers.length; i++) {
|
|
13111
|
+
const score = scoreLabelImageMarker(userInput.markers[i].selected, rubric.markers[i].answers);
|
|
13129
13112
|
if (score.isCorrect) {
|
|
13130
13113
|
numCorrect++;
|
|
13131
13114
|
}
|
|
13132
13115
|
}
|
|
13133
|
-
|
|
13134
|
-
// We expect all question markers to be answered before grading.
|
|
13135
|
-
if (numAnswered !== userInput.markers.length) {
|
|
13136
|
-
return {
|
|
13137
|
-
type: "invalid",
|
|
13138
|
-
message: null
|
|
13139
|
-
};
|
|
13140
|
-
}
|
|
13141
13116
|
return {
|
|
13142
13117
|
type: "points",
|
|
13143
13118
|
// Markers with no expected answers are graded as correct if user
|
|
@@ -13148,8 +13123,8 @@ function scoreLabelImage(userInput, rubric) {
|
|
|
13148
13123
|
};
|
|
13149
13124
|
}
|
|
13150
13125
|
|
|
13151
|
-
function scoreMatcher(
|
|
13152
|
-
const correct = _.isEqual(
|
|
13126
|
+
function scoreMatcher(userInput, rubric) {
|
|
13127
|
+
const correct = _.isEqual(userInput.left, rubric.left) && _.isEqual(userInput.right, rubric.right);
|
|
13153
13128
|
return {
|
|
13154
13129
|
type: "points",
|
|
13155
13130
|
earned: correct ? 1 : 0,
|
|
@@ -13158,35 +13133,7 @@ function scoreMatcher(state, rubric) {
|
|
|
13158
13133
|
};
|
|
13159
13134
|
}
|
|
13160
13135
|
|
|
13161
|
-
/**
|
|
13162
|
-
* Checks user input from the matrix widget to see if it is scorable.
|
|
13163
|
-
*
|
|
13164
|
-
* Note: The matrix widget cannot do much validation without the Scoring
|
|
13165
|
-
* Data because of its use of KhanAnswerTypes as a core part of scoring.
|
|
13166
|
-
*
|
|
13167
|
-
* @see `scoreMatrix()` for more details.
|
|
13168
|
-
*/
|
|
13169
|
-
function validateMatrix(userInput, validationData) {
|
|
13170
|
-
const supplied = userInput.answers;
|
|
13171
|
-
const suppliedSize = perseusCore.getMatrixSize(supplied);
|
|
13172
|
-
for (let row = 0; row < suppliedSize[0]; row++) {
|
|
13173
|
-
for (let col = 0; col < suppliedSize[1]; col++) {
|
|
13174
|
-
if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
|
|
13175
|
-
return {
|
|
13176
|
-
type: "invalid",
|
|
13177
|
-
message: ErrorCodes.FILL_ALL_CELLS_ERROR
|
|
13178
|
-
};
|
|
13179
|
-
}
|
|
13180
|
-
}
|
|
13181
|
-
}
|
|
13182
|
-
return null;
|
|
13183
|
-
}
|
|
13184
|
-
|
|
13185
13136
|
function scoreMatrix(userInput, rubric) {
|
|
13186
|
-
const validationResult = validateMatrix(userInput);
|
|
13187
|
-
if (validationResult != null) {
|
|
13188
|
-
return validationResult;
|
|
13189
|
-
}
|
|
13190
13137
|
const solution = rubric.answers;
|
|
13191
13138
|
const supplied = userInput.answers;
|
|
13192
13139
|
const solutionSize = perseusCore.getMatrixSize(solution);
|
|
@@ -13231,35 +13178,35 @@ function scoreMatrix(userInput, rubric) {
|
|
|
13231
13178
|
}
|
|
13232
13179
|
|
|
13233
13180
|
/**
|
|
13234
|
-
* Checks user input
|
|
13235
|
-
*
|
|
13236
|
-
*
|
|
13237
|
-
*
|
|
13181
|
+
* Checks user input from the matrix widget to see if it is scorable.
|
|
13182
|
+
*
|
|
13183
|
+
* Note: The matrix widget cannot do much validation without the Scoring
|
|
13184
|
+
* Data because of its use of KhanAnswerTypes as a core part of scoring.
|
|
13185
|
+
*
|
|
13186
|
+
* @see `scoreMatrix()` for more details.
|
|
13238
13187
|
*/
|
|
13239
|
-
function
|
|
13240
|
-
const
|
|
13241
|
-
const
|
|
13242
|
-
|
|
13243
|
-
|
|
13244
|
-
|
|
13245
|
-
|
|
13246
|
-
|
|
13247
|
-
|
|
13248
|
-
|
|
13188
|
+
function validateMatrix(userInput) {
|
|
13189
|
+
const supplied = userInput.answers;
|
|
13190
|
+
const suppliedSize = perseusCore.getMatrixSize(supplied);
|
|
13191
|
+
for (let row = 0; row < suppliedSize[0]; row++) {
|
|
13192
|
+
for (let col = 0; col < suppliedSize[1]; col++) {
|
|
13193
|
+
if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
|
|
13194
|
+
return {
|
|
13195
|
+
type: "invalid",
|
|
13196
|
+
message: ErrorCodes.FILL_ALL_CELLS_ERROR
|
|
13197
|
+
};
|
|
13198
|
+
}
|
|
13199
|
+
}
|
|
13249
13200
|
}
|
|
13250
13201
|
return null;
|
|
13251
13202
|
}
|
|
13252
13203
|
|
|
13253
|
-
function scoreNumberLine(userInput,
|
|
13254
|
-
const
|
|
13255
|
-
|
|
13256
|
-
|
|
13257
|
-
|
|
13258
|
-
const
|
|
13259
|
-
const start = scoringData.initialX != null ? scoringData.initialX : range[0];
|
|
13260
|
-
const startRel = scoringData.isInequality ? "ge" : "eq";
|
|
13261
|
-
const correctRel = scoringData.correctRel || "eq";
|
|
13262
|
-
const correctPos = kmath.number.equal(userInput.numLinePosition, scoringData.correctX || 0);
|
|
13204
|
+
function scoreNumberLine(userInput, rubric) {
|
|
13205
|
+
const range = rubric.range;
|
|
13206
|
+
const start = rubric.initialX != null ? rubric.initialX : range[0];
|
|
13207
|
+
const startRel = rubric.isInequality ? "ge" : "eq";
|
|
13208
|
+
const correctRel = rubric.correctRel || "eq";
|
|
13209
|
+
const correctPos = kmath.number.equal(userInput.numLinePosition, rubric.correctX || 0);
|
|
13263
13210
|
if (correctPos && correctRel === userInput.rel) {
|
|
13264
13211
|
return {
|
|
13265
13212
|
type: "points",
|
|
@@ -13283,6 +13230,26 @@ function scoreNumberLine(userInput, scoringData) {
|
|
|
13283
13230
|
};
|
|
13284
13231
|
}
|
|
13285
13232
|
|
|
13233
|
+
/**
|
|
13234
|
+
* Checks user input is within the allowed range and not the same as the initial
|
|
13235
|
+
* state.
|
|
13236
|
+
* @param userInput
|
|
13237
|
+
* @see 'scoreNumberLine' for the scoring logic.
|
|
13238
|
+
*/
|
|
13239
|
+
function validateNumberLine(userInput) {
|
|
13240
|
+
const divisionRange = userInput.divisionRange;
|
|
13241
|
+
const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
|
|
13242
|
+
|
|
13243
|
+
// TODO: I don't think isTickCrtl is a thing anymore
|
|
13244
|
+
if (userInput.isTickCrtl && outsideAllowedRange) {
|
|
13245
|
+
return {
|
|
13246
|
+
type: "invalid",
|
|
13247
|
+
message: "Number of divisions is outside the allowed range."
|
|
13248
|
+
};
|
|
13249
|
+
}
|
|
13250
|
+
return null;
|
|
13251
|
+
}
|
|
13252
|
+
|
|
13286
13253
|
/*
|
|
13287
13254
|
* In this file, an `expression` is some portion of valid TeX enclosed in
|
|
13288
13255
|
* curly brackets.
|
|
@@ -13545,6 +13512,16 @@ function scoreNumericInput(userInput, rubric) {
|
|
|
13545
13512
|
};
|
|
13546
13513
|
}
|
|
13547
13514
|
|
|
13515
|
+
function scoreOrderer(userInput, rubric) {
|
|
13516
|
+
const correct = _.isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
|
|
13517
|
+
return {
|
|
13518
|
+
type: "points",
|
|
13519
|
+
earned: correct ? 1 : 0,
|
|
13520
|
+
total: 1,
|
|
13521
|
+
message: null
|
|
13522
|
+
};
|
|
13523
|
+
}
|
|
13524
|
+
|
|
13548
13525
|
/**
|
|
13549
13526
|
* Checks user input from the orderer widget to see if the user has started
|
|
13550
13527
|
* ordering the options, making the widget scorable.
|
|
@@ -13561,15 +13538,10 @@ function validateOrderer(userInput) {
|
|
|
13561
13538
|
return null;
|
|
13562
13539
|
}
|
|
13563
13540
|
|
|
13564
|
-
function
|
|
13565
|
-
const validateError = validateOrderer(userInput);
|
|
13566
|
-
if (validateError) {
|
|
13567
|
-
return validateError;
|
|
13568
|
-
}
|
|
13569
|
-
const correct = _.isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
|
|
13541
|
+
function scorePlotter(userInput, rubric) {
|
|
13570
13542
|
return {
|
|
13571
13543
|
type: "points",
|
|
13572
|
-
earned: correct ? 1 : 0,
|
|
13544
|
+
earned: perseusCore.approximateDeepEqual(userInput, rubric.correct) ? 1 : 0,
|
|
13573
13545
|
total: 1,
|
|
13574
13546
|
message: null
|
|
13575
13547
|
};
|
|
@@ -13591,45 +13563,7 @@ function validatePlotter(userInput, validationData) {
|
|
|
13591
13563
|
return null;
|
|
13592
13564
|
}
|
|
13593
13565
|
|
|
13594
|
-
function scorePlotter(userInput, scoringData) {
|
|
13595
|
-
const validationError = validatePlotter(userInput, scoringData);
|
|
13596
|
-
if (validationError) {
|
|
13597
|
-
return validationError;
|
|
13598
|
-
}
|
|
13599
|
-
return {
|
|
13600
|
-
type: "points",
|
|
13601
|
-
earned: perseusCore.approximateDeepEqual(userInput, scoringData.correct) ? 1 : 0,
|
|
13602
|
-
total: 1,
|
|
13603
|
-
message: null
|
|
13604
|
-
};
|
|
13605
|
-
}
|
|
13606
|
-
|
|
13607
|
-
/**
|
|
13608
|
-
* Checks if the user has selected at least one option. Additional validation
|
|
13609
|
-
* is done in scoreRadio to check if the number of selected options is correct
|
|
13610
|
-
* and if the user has selected both a correct option and the "none of the above"
|
|
13611
|
-
* option.
|
|
13612
|
-
* @param userInput
|
|
13613
|
-
* @see `scoreRadio` for the additional validation logic and the scoring logic.
|
|
13614
|
-
*/
|
|
13615
|
-
function validateRadio(userInput) {
|
|
13616
|
-
const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
|
|
13617
|
-
return sum + (selected ? 1 : 0);
|
|
13618
|
-
}, 0);
|
|
13619
|
-
if (numSelected === 0) {
|
|
13620
|
-
return {
|
|
13621
|
-
type: "invalid",
|
|
13622
|
-
message: null
|
|
13623
|
-
};
|
|
13624
|
-
}
|
|
13625
|
-
return null;
|
|
13626
|
-
}
|
|
13627
|
-
|
|
13628
13566
|
function scoreRadio(userInput, rubric) {
|
|
13629
|
-
const validationError = validateRadio(userInput);
|
|
13630
|
-
if (validationError) {
|
|
13631
|
-
return validationError;
|
|
13632
|
-
}
|
|
13633
13567
|
const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
|
|
13634
13568
|
return sum + (selected ? 1 : 0);
|
|
13635
13569
|
}, 0);
|
|
@@ -13670,6 +13604,37 @@ function scoreRadio(userInput, rubric) {
|
|
|
13670
13604
|
};
|
|
13671
13605
|
}
|
|
13672
13606
|
|
|
13607
|
+
/**
|
|
13608
|
+
* Checks if the user has selected at least one option. Additional validation
|
|
13609
|
+
* is done in scoreRadio to check if the number of selected options is correct
|
|
13610
|
+
* and if the user has selected both a correct option and the "none of the above"
|
|
13611
|
+
* option.
|
|
13612
|
+
* @param userInput
|
|
13613
|
+
* @see `scoreRadio` for the additional validation logic and the scoring logic.
|
|
13614
|
+
*/
|
|
13615
|
+
function validateRadio(userInput) {
|
|
13616
|
+
const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
|
|
13617
|
+
return sum + (selected ? 1 : 0);
|
|
13618
|
+
}, 0);
|
|
13619
|
+
if (numSelected === 0) {
|
|
13620
|
+
return {
|
|
13621
|
+
type: "invalid",
|
|
13622
|
+
message: null
|
|
13623
|
+
};
|
|
13624
|
+
}
|
|
13625
|
+
return null;
|
|
13626
|
+
}
|
|
13627
|
+
|
|
13628
|
+
function scoreSorter(userInput, rubric) {
|
|
13629
|
+
const correct = perseusCore.approximateDeepEqual(userInput.options, rubric.correct);
|
|
13630
|
+
return {
|
|
13631
|
+
type: "points",
|
|
13632
|
+
earned: correct ? 1 : 0,
|
|
13633
|
+
total: 1,
|
|
13634
|
+
message: null
|
|
13635
|
+
};
|
|
13636
|
+
}
|
|
13637
|
+
|
|
13673
13638
|
/**
|
|
13674
13639
|
* Checks user input for the sorter widget to ensure that the user has made
|
|
13675
13640
|
* changes before attempting to score the widget.
|
|
@@ -13693,20 +13658,6 @@ function validateSorter(userInput) {
|
|
|
13693
13658
|
return null;
|
|
13694
13659
|
}
|
|
13695
13660
|
|
|
13696
|
-
function scoreSorter(userInput, rubric) {
|
|
13697
|
-
const validationError = validateSorter(userInput);
|
|
13698
|
-
if (validationError) {
|
|
13699
|
-
return validationError;
|
|
13700
|
-
}
|
|
13701
|
-
const correct = perseusCore.approximateDeepEqual(userInput.options, rubric.correct);
|
|
13702
|
-
return {
|
|
13703
|
-
type: "points",
|
|
13704
|
-
earned: correct ? 1 : 0,
|
|
13705
|
-
total: 1,
|
|
13706
|
-
message: null
|
|
13707
|
-
};
|
|
13708
|
-
}
|
|
13709
|
-
|
|
13710
13661
|
/**
|
|
13711
13662
|
* Filters the given table (modelled as a 2D array) to remove any rows that are
|
|
13712
13663
|
* completely empty.
|
|
@@ -13850,9 +13801,289 @@ function scoreInputNumber(userInput, rubric) {
|
|
|
13850
13801
|
};
|
|
13851
13802
|
}
|
|
13852
13803
|
|
|
13804
|
+
/**
|
|
13805
|
+
* Several widgets don't have "right"/"wrong" scoring logic,
|
|
13806
|
+
* so this just says to move on past those widgets
|
|
13807
|
+
*
|
|
13808
|
+
* TODO(LEMS-2543) widgets that use this probably shouldn't have any
|
|
13809
|
+
* scoring logic and the thing scoring an exercise
|
|
13810
|
+
* should just know to skip these
|
|
13811
|
+
*/
|
|
13812
|
+
function scoreNoop() {
|
|
13813
|
+
let points = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
|
|
13814
|
+
return {
|
|
13815
|
+
type: "points",
|
|
13816
|
+
earned: points,
|
|
13817
|
+
total: points,
|
|
13818
|
+
message: null
|
|
13819
|
+
};
|
|
13820
|
+
}
|
|
13821
|
+
|
|
13822
|
+
// The `group` widget is basically a widget hosting a full Perseus system in
|
|
13823
|
+
// it. As such, scoring a group means scoring all widgets it contains.
|
|
13824
|
+
function scoreGroup(userInput, rubric, locale) {
|
|
13825
|
+
const scores = scoreWidgetsFunctional(rubric.widgets, Object.keys(rubric.widgets), userInput, locale);
|
|
13826
|
+
return flattenScores(scores);
|
|
13827
|
+
}
|
|
13828
|
+
|
|
13829
|
+
/**
|
|
13830
|
+
* Checks the given user input to see if any answerable widgets have not been
|
|
13831
|
+
* "filled in" (ie. if they're empty). Another way to think about this
|
|
13832
|
+
* function is that its a check to see if we can score the provided input.
|
|
13833
|
+
*/
|
|
13834
|
+
function emptyWidgetsFunctional(widgets,
|
|
13835
|
+
// This is a port of old code, I'm not sure why
|
|
13836
|
+
// we need widgetIds vs the keys of the widgets object
|
|
13837
|
+
widgetIds, userInputMap, locale) {
|
|
13838
|
+
return widgetIds.filter(id => {
|
|
13839
|
+
const widget = widgets[id];
|
|
13840
|
+
if (!widget || widget.static === true) {
|
|
13841
|
+
// Static widgets shouldn't count as empty
|
|
13842
|
+
return false;
|
|
13843
|
+
}
|
|
13844
|
+
const validator = getWidgetValidator(widget.type);
|
|
13845
|
+
const userInput = userInputMap[id];
|
|
13846
|
+
const validationData = widget.options;
|
|
13847
|
+
const score = validator?.(userInput, validationData, locale);
|
|
13848
|
+
if (score) {
|
|
13849
|
+
return scoreIsEmpty(score);
|
|
13850
|
+
}
|
|
13851
|
+
});
|
|
13852
|
+
}
|
|
13853
|
+
|
|
13854
|
+
function validateGroup(userInput, validationData, locale) {
|
|
13855
|
+
const emptyWidgets = emptyWidgetsFunctional(validationData.widgets, Object.keys(validationData.widgets), userInput, locale);
|
|
13856
|
+
if (emptyWidgets.length === 0) {
|
|
13857
|
+
return null;
|
|
13858
|
+
}
|
|
13859
|
+
return {
|
|
13860
|
+
type: "invalid",
|
|
13861
|
+
message: null
|
|
13862
|
+
};
|
|
13863
|
+
}
|
|
13864
|
+
|
|
13865
|
+
function validateLabelImage(userInput) {
|
|
13866
|
+
let numAnswered = 0;
|
|
13867
|
+
for (let i = 0; i < userInput.markers.length; i++) {
|
|
13868
|
+
const userSelection = userInput.markers[i].selected;
|
|
13869
|
+
if (userSelection && userSelection.length > 0) {
|
|
13870
|
+
numAnswered++;
|
|
13871
|
+
}
|
|
13872
|
+
}
|
|
13873
|
+
// We expect all question markers to be answered before grading.
|
|
13874
|
+
if (numAnswered !== userInput.markers.length) {
|
|
13875
|
+
return {
|
|
13876
|
+
type: "invalid",
|
|
13877
|
+
message: null
|
|
13878
|
+
};
|
|
13879
|
+
}
|
|
13880
|
+
return null;
|
|
13881
|
+
}
|
|
13882
|
+
|
|
13883
|
+
function validateMockWidget(userInput) {
|
|
13884
|
+
if (userInput.currentValue == null || userInput.currentValue === "") {
|
|
13885
|
+
return {
|
|
13886
|
+
type: "invalid",
|
|
13887
|
+
message: ""
|
|
13888
|
+
};
|
|
13889
|
+
}
|
|
13890
|
+
return null;
|
|
13891
|
+
}
|
|
13892
|
+
|
|
13893
|
+
function scoreMockWidget(userInput, rubric) {
|
|
13894
|
+
const validationResult = validateMockWidget(userInput);
|
|
13895
|
+
if (validationResult != null) {
|
|
13896
|
+
return validationResult;
|
|
13897
|
+
}
|
|
13898
|
+
return {
|
|
13899
|
+
type: "points",
|
|
13900
|
+
earned: userInput.currentValue === rubric.value ? 1 : 0,
|
|
13901
|
+
total: 1,
|
|
13902
|
+
message: ""
|
|
13903
|
+
};
|
|
13904
|
+
}
|
|
13905
|
+
|
|
13906
|
+
const widgets = {};
|
|
13907
|
+
function registerWidget(type, scorer, validator) {
|
|
13908
|
+
widgets[type] = {
|
|
13909
|
+
scorer,
|
|
13910
|
+
validator
|
|
13911
|
+
};
|
|
13912
|
+
}
|
|
13913
|
+
const getWidgetValidator = name => {
|
|
13914
|
+
return widgets[name]?.validator ?? null;
|
|
13915
|
+
};
|
|
13916
|
+
const getWidgetScorer = name => {
|
|
13917
|
+
return widgets[name]?.scorer ?? null;
|
|
13918
|
+
};
|
|
13919
|
+
registerWidget("categorizer", scoreCategorizer, validateCategorizer);
|
|
13920
|
+
registerWidget("cs-program", scoreCSProgram);
|
|
13921
|
+
registerWidget("dropdown", scoreDropdown, validateDropdown);
|
|
13922
|
+
registerWidget("expression", scoreExpression, validateExpression);
|
|
13923
|
+
registerWidget("grapher", scoreGrapher);
|
|
13924
|
+
registerWidget("group", scoreGroup, validateGroup);
|
|
13925
|
+
registerWidget("iframe", scoreIframe);
|
|
13926
|
+
registerWidget("input-number", scoreInputNumber);
|
|
13927
|
+
registerWidget("interactive-graph", scoreInteractiveGraph);
|
|
13928
|
+
registerWidget("label-image", scoreLabelImage, validateLabelImage);
|
|
13929
|
+
registerWidget("matcher", scoreMatcher);
|
|
13930
|
+
registerWidget("matrix", scoreMatrix, validateMatrix);
|
|
13931
|
+
registerWidget("mock-widget", scoreMockWidget, scoreMockWidget);
|
|
13932
|
+
registerWidget("number-line", scoreNumberLine, validateNumberLine);
|
|
13933
|
+
registerWidget("numeric-input", scoreNumericInput);
|
|
13934
|
+
registerWidget("orderer", scoreOrderer, validateOrderer);
|
|
13935
|
+
registerWidget("plotter", scorePlotter, validatePlotter);
|
|
13936
|
+
registerWidget("radio", scoreRadio, validateRadio);
|
|
13937
|
+
registerWidget("sorter", scoreSorter, validateSorter);
|
|
13938
|
+
registerWidget("table", scoreTable, validateTable);
|
|
13939
|
+
registerWidget("deprecated-standin", () => scoreNoop(1));
|
|
13940
|
+
registerWidget("measurer", () => scoreNoop(1));
|
|
13941
|
+
registerWidget("definition", scoreNoop);
|
|
13942
|
+
registerWidget("explanation", scoreNoop);
|
|
13943
|
+
registerWidget("image", scoreNoop);
|
|
13944
|
+
registerWidget("interaction", scoreNoop);
|
|
13945
|
+
registerWidget("molecule", scoreNoop);
|
|
13946
|
+
registerWidget("passage", scoreNoop);
|
|
13947
|
+
registerWidget("passage-ref", scoreNoop);
|
|
13948
|
+
registerWidget("passage-ref-target", scoreNoop);
|
|
13949
|
+
registerWidget("video", scoreNoop);
|
|
13950
|
+
|
|
13951
|
+
const noScore = {
|
|
13952
|
+
type: "points",
|
|
13953
|
+
earned: 0,
|
|
13954
|
+
total: 0,
|
|
13955
|
+
message: null
|
|
13956
|
+
};
|
|
13957
|
+
|
|
13958
|
+
/**
|
|
13959
|
+
* If a widget says that it is empty once it is graded.
|
|
13960
|
+
* Trying to encapsulate references to the score format.
|
|
13961
|
+
*/
|
|
13962
|
+
function scoreIsEmpty(score) {
|
|
13963
|
+
// HACK(benkomalo): ugh. this isn't great; the Perseus score objects
|
|
13964
|
+
// overload the type "invalid" for what should probably be three
|
|
13965
|
+
// distinct cases:
|
|
13966
|
+
// - truly empty or not fully filled out
|
|
13967
|
+
// - invalid or malformed inputs
|
|
13968
|
+
// - "almost correct" like inputs where the widget wants to give
|
|
13969
|
+
// feedback (e.g. a fraction needs to be reduced, or `pi` should
|
|
13970
|
+
// be used instead of 3.14)
|
|
13971
|
+
//
|
|
13972
|
+
// Unfortunately the coercion happens all over the place, as these
|
|
13973
|
+
// Perseus style score objects are created *everywhere* (basically
|
|
13974
|
+
// in every widget), so it's hard to change now. We assume that
|
|
13975
|
+
// anything with a "message" is not truly empty, and one of the
|
|
13976
|
+
// latter two cases for now.
|
|
13977
|
+
return score.type === "invalid" && (!score.message || score.message.length === 0);
|
|
13978
|
+
}
|
|
13979
|
+
|
|
13980
|
+
/**
|
|
13981
|
+
* Combine two score objects.
|
|
13982
|
+
*
|
|
13983
|
+
* Given two score objects for two different widgets, combine them so that
|
|
13984
|
+
* if one is wrong, the total score is wrong, etc.
|
|
13985
|
+
*/
|
|
13986
|
+
function combineScores(scoreA, scoreB) {
|
|
13987
|
+
let message;
|
|
13988
|
+
if (scoreA.type === "points" && scoreB.type === "points") {
|
|
13989
|
+
if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
|
|
13990
|
+
// TODO(alpert): Figure out how to combine messages usefully
|
|
13991
|
+
message = null;
|
|
13992
|
+
} else {
|
|
13993
|
+
message = scoreA.message || scoreB.message;
|
|
13994
|
+
}
|
|
13995
|
+
return {
|
|
13996
|
+
type: "points",
|
|
13997
|
+
earned: scoreA.earned + scoreB.earned,
|
|
13998
|
+
total: scoreA.total + scoreB.total,
|
|
13999
|
+
message: message
|
|
14000
|
+
};
|
|
14001
|
+
}
|
|
14002
|
+
if (scoreA.type === "points" && scoreB.type === "invalid") {
|
|
14003
|
+
return scoreB;
|
|
14004
|
+
}
|
|
14005
|
+
if (scoreA.type === "invalid" && scoreB.type === "points") {
|
|
14006
|
+
return scoreA;
|
|
14007
|
+
}
|
|
14008
|
+
if (scoreA.type === "invalid" && scoreB.type === "invalid") {
|
|
14009
|
+
if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
|
|
14010
|
+
// TODO(alpert): Figure out how to combine messages usefully
|
|
14011
|
+
message = null;
|
|
14012
|
+
} else {
|
|
14013
|
+
message = scoreA.message || scoreB.message;
|
|
14014
|
+
}
|
|
14015
|
+
return {
|
|
14016
|
+
type: "invalid",
|
|
14017
|
+
message: message
|
|
14018
|
+
};
|
|
14019
|
+
}
|
|
14020
|
+
|
|
14021
|
+
/**
|
|
14022
|
+
* The above checks cover all combinations of score type, so if we get here
|
|
14023
|
+
* then something is amiss with our inputs.
|
|
14024
|
+
*/
|
|
14025
|
+
throw new perseusCore.PerseusError("PerseusScore with unknown type encountered", perseusCore.Errors.InvalidInput, {
|
|
14026
|
+
metadata: {
|
|
14027
|
+
scoreA: JSON.stringify(scoreA),
|
|
14028
|
+
scoreB: JSON.stringify(scoreB)
|
|
14029
|
+
}
|
|
14030
|
+
});
|
|
14031
|
+
}
|
|
14032
|
+
function flattenScores(widgetScoreMap) {
|
|
14033
|
+
return Object.values(widgetScoreMap).reduce(combineScores, noScore);
|
|
14034
|
+
}
|
|
14035
|
+
|
|
14036
|
+
// once scorePerseusItem is the only one calling scoreWidgetsFunctional
|
|
14037
|
+
function scorePerseusItem(perseusRenderData, userInputMap, locale) {
|
|
14038
|
+
// There seems to be a chance that PerseusRenderer.widgets might include
|
|
14039
|
+
// widget data for widgets that are not in PerseusRenderer.content,
|
|
14040
|
+
// so this checks that the widgets are being used before scoring them
|
|
14041
|
+
const usedWidgetIds = perseusCore.getWidgetIdsFromContent(perseusRenderData.content);
|
|
14042
|
+
const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, locale);
|
|
14043
|
+
return flattenScores(scores);
|
|
14044
|
+
}
|
|
14045
|
+
|
|
14046
|
+
// TODO: combine scorePerseusItem with scoreWidgetsFunctional
|
|
14047
|
+
function scoreWidgetsFunctional(widgets,
|
|
14048
|
+
// This is a port of old code, I'm not sure why
|
|
14049
|
+
// we need widgetIds vs the keys of the widgets object
|
|
14050
|
+
widgetIds, userInputMap, locale) {
|
|
14051
|
+
const upgradedWidgets = perseusCore.getUpgradedWidgetOptions(widgets);
|
|
14052
|
+
const gradedWidgetIds = widgetIds.filter(id => {
|
|
14053
|
+
const props = upgradedWidgets[id];
|
|
14054
|
+
const widgetIsGraded = props?.graded == null || props.graded;
|
|
14055
|
+
const widgetIsStatic = !!props?.static;
|
|
14056
|
+
// Ungraded widgets or widgets set to static shouldn't be graded.
|
|
14057
|
+
return widgetIsGraded && !widgetIsStatic;
|
|
14058
|
+
});
|
|
14059
|
+
const widgetScores = {};
|
|
14060
|
+
gradedWidgetIds.forEach(id => {
|
|
14061
|
+
const widget = upgradedWidgets[id];
|
|
14062
|
+
if (!widget) {
|
|
14063
|
+
return;
|
|
14064
|
+
}
|
|
14065
|
+
const userInput = userInputMap[id];
|
|
14066
|
+
const validator = getWidgetValidator(widget.type);
|
|
14067
|
+
const scorer = getWidgetScorer(widget.type);
|
|
14068
|
+
|
|
14069
|
+
// We do validation (empty checks) first and then scoring. If
|
|
14070
|
+
// validation fails, it's result is itself a PerseusScore.
|
|
14071
|
+
const score = validator?.(userInput, widget.options, locale) ?? scorer?.(userInput, widget.options, locale);
|
|
14072
|
+
if (score != null) {
|
|
14073
|
+
widgetScores[id] = score;
|
|
14074
|
+
}
|
|
14075
|
+
});
|
|
14076
|
+
return widgetScores;
|
|
14077
|
+
}
|
|
14078
|
+
|
|
13853
14079
|
exports.ErrorCodes = ErrorCodes;
|
|
13854
14080
|
exports.KhanAnswerTypes = KhanAnswerTypes;
|
|
14081
|
+
exports.emptyWidgetsFunctional = emptyWidgetsFunctional;
|
|
14082
|
+
exports.flattenScores = flattenScores;
|
|
14083
|
+
exports.getWidgetScorer = getWidgetScorer;
|
|
14084
|
+
exports.getWidgetValidator = getWidgetValidator;
|
|
13855
14085
|
exports.inputNumberAnswerTypes = inputNumberAnswerTypes;
|
|
14086
|
+
exports.registerWidget = registerWidget;
|
|
13856
14087
|
exports.scoreCSProgram = scoreCSProgram;
|
|
13857
14088
|
exports.scoreCategorizer = scoreCategorizer;
|
|
13858
14089
|
exports.scoreDropdown = scoreDropdown;
|
|
@@ -13868,8 +14099,20 @@ exports.scoreMatrix = scoreMatrix;
|
|
|
13868
14099
|
exports.scoreNumberLine = scoreNumberLine;
|
|
13869
14100
|
exports.scoreNumericInput = scoreNumericInput;
|
|
13870
14101
|
exports.scoreOrderer = scoreOrderer;
|
|
14102
|
+
exports.scorePerseusItem = scorePerseusItem;
|
|
13871
14103
|
exports.scorePlotter = scorePlotter;
|
|
13872
14104
|
exports.scoreRadio = scoreRadio;
|
|
13873
14105
|
exports.scoreSorter = scoreSorter;
|
|
13874
14106
|
exports.scoreTable = scoreTable;
|
|
14107
|
+
exports.scoreWidgetsFunctional = scoreWidgetsFunctional;
|
|
14108
|
+
exports.validateCategorizer = validateCategorizer;
|
|
14109
|
+
exports.validateDropdown = validateDropdown;
|
|
14110
|
+
exports.validateExpression = validateExpression;
|
|
14111
|
+
exports.validateMatrix = validateMatrix;
|
|
14112
|
+
exports.validateNumberLine = validateNumberLine;
|
|
14113
|
+
exports.validateOrderer = validateOrderer;
|
|
14114
|
+
exports.validatePlotter = validatePlotter;
|
|
14115
|
+
exports.validateRadio = validateRadio;
|
|
14116
|
+
exports.validateSorter = validateSorter;
|
|
14117
|
+
exports.validateTable = validateTable;
|
|
13875
14118
|
//# sourceMappingURL=index.js.map
|