@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/es/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as KAS from '@khanacademy/kas';
|
|
2
2
|
import { KhanMath, number, geometry, angles, coefficients } from '@khanacademy/kmath';
|
|
3
|
-
import { PerseusError, Errors, getDecimalSeparator, GrapherUtil, approximateDeepEqual, approximateEqual, deepClone, getMatrixSize } from '@khanacademy/perseus-core';
|
|
3
|
+
import { PerseusError, Errors, getDecimalSeparator, GrapherUtil, approximateDeepEqual, approximateEqual, deepClone, getMatrixSize, getWidgetIdsFromContent, getUpgradedWidgetOptions } from '@khanacademy/perseus-core';
|
|
4
4
|
import { ErrorCodes as ErrorCodes$1 } from '@khanacademy/perseus-score';
|
|
5
5
|
|
|
6
6
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
@@ -12526,6 +12526,21 @@ const KhanAnswerTypes = {
|
|
|
12526
12526
|
}
|
|
12527
12527
|
};
|
|
12528
12528
|
|
|
12529
|
+
function scoreCategorizer(userInput, rubric) {
|
|
12530
|
+
let allCorrect = true;
|
|
12531
|
+
rubric.values.forEach((value, i) => {
|
|
12532
|
+
if (userInput.values[i] !== value) {
|
|
12533
|
+
allCorrect = false;
|
|
12534
|
+
}
|
|
12535
|
+
});
|
|
12536
|
+
return {
|
|
12537
|
+
type: "points",
|
|
12538
|
+
earned: allCorrect ? 1 : 0,
|
|
12539
|
+
total: 1,
|
|
12540
|
+
message: null
|
|
12541
|
+
};
|
|
12542
|
+
}
|
|
12543
|
+
|
|
12529
12544
|
/**
|
|
12530
12545
|
* Checks userInput from the categorizer widget to see if the user has selected
|
|
12531
12546
|
* a category for each item.
|
|
@@ -12545,43 +12560,23 @@ function validateCategorizer(userInput, validationData) {
|
|
|
12545
12560
|
return null;
|
|
12546
12561
|
}
|
|
12547
12562
|
|
|
12548
|
-
function
|
|
12549
|
-
const validationError = validateCategorizer(userInput, scoringData);
|
|
12550
|
-
if (validationError) {
|
|
12551
|
-
return validationError;
|
|
12552
|
-
}
|
|
12553
|
-
let allCorrect = true;
|
|
12554
|
-
scoringData.values.forEach((value, i) => {
|
|
12555
|
-
if (userInput.values[i] !== value) {
|
|
12556
|
-
allCorrect = false;
|
|
12557
|
-
}
|
|
12558
|
-
});
|
|
12559
|
-
return {
|
|
12560
|
-
type: "points",
|
|
12561
|
-
earned: allCorrect ? 1 : 0,
|
|
12562
|
-
total: 1,
|
|
12563
|
-
message: null
|
|
12564
|
-
};
|
|
12565
|
-
}
|
|
12566
|
-
|
|
12567
|
-
// TODO: merge this with scoreIframe, it's the same code
|
|
12568
|
-
function scoreCSProgram(state) {
|
|
12563
|
+
function scoreCSProgram(userInput) {
|
|
12569
12564
|
// The CS program can tell us whether it's correct or incorrect,
|
|
12570
12565
|
// and pass an optional message
|
|
12571
|
-
if (
|
|
12566
|
+
if (userInput.status === "correct") {
|
|
12572
12567
|
return {
|
|
12573
12568
|
type: "points",
|
|
12574
12569
|
earned: 1,
|
|
12575
12570
|
total: 1,
|
|
12576
|
-
message:
|
|
12571
|
+
message: userInput.message || null
|
|
12577
12572
|
};
|
|
12578
12573
|
}
|
|
12579
|
-
if (
|
|
12574
|
+
if (userInput.status === "incorrect") {
|
|
12580
12575
|
return {
|
|
12581
12576
|
type: "points",
|
|
12582
12577
|
earned: 0,
|
|
12583
12578
|
total: 1,
|
|
12584
|
-
message:
|
|
12579
|
+
message: userInput.message || null
|
|
12585
12580
|
};
|
|
12586
12581
|
}
|
|
12587
12582
|
return {
|
|
@@ -12590,25 +12585,7 @@ function scoreCSProgram(state) {
|
|
|
12590
12585
|
};
|
|
12591
12586
|
}
|
|
12592
12587
|
|
|
12593
|
-
/**
|
|
12594
|
-
* Checks if the user has selected an item from the dropdown before scoring.
|
|
12595
|
-
* This is shown with a userInput value / index other than 0.
|
|
12596
|
-
*/
|
|
12597
|
-
function validateDropdown(userInput) {
|
|
12598
|
-
if (userInput.value === 0) {
|
|
12599
|
-
return {
|
|
12600
|
-
type: "invalid",
|
|
12601
|
-
message: null
|
|
12602
|
-
};
|
|
12603
|
-
}
|
|
12604
|
-
return null;
|
|
12605
|
-
}
|
|
12606
|
-
|
|
12607
12588
|
function scoreDropdown(userInput, rubric) {
|
|
12608
|
-
const validationError = validateDropdown(userInput);
|
|
12609
|
-
if (validationError) {
|
|
12610
|
-
return validationError;
|
|
12611
|
-
}
|
|
12612
12589
|
const correct = rubric.choices[userInput.value - 1].correct;
|
|
12613
12590
|
return {
|
|
12614
12591
|
type: "points",
|
|
@@ -12619,15 +12596,11 @@ function scoreDropdown(userInput, rubric) {
|
|
|
12619
12596
|
}
|
|
12620
12597
|
|
|
12621
12598
|
/**
|
|
12622
|
-
* Checks
|
|
12623
|
-
*
|
|
12624
|
-
* Note: Most of the expression widget's validation requires the Rubric because
|
|
12625
|
-
* of its use of KhanAnswerTypes as a core part of scoring.
|
|
12626
|
-
*
|
|
12627
|
-
* @see `scoreExpression()` for more details.
|
|
12599
|
+
* Checks if the user has selected an item from the dropdown before scoring.
|
|
12600
|
+
* This is shown with a userInput value / index other than 0.
|
|
12628
12601
|
*/
|
|
12629
|
-
function
|
|
12630
|
-
if (userInput ===
|
|
12602
|
+
function validateDropdown(userInput) {
|
|
12603
|
+
if (userInput.value === 0) {
|
|
12631
12604
|
return {
|
|
12632
12605
|
type: "invalid",
|
|
12633
12606
|
message: null
|
|
@@ -12654,13 +12627,7 @@ function validateExpression(userInput) {
|
|
|
12654
12627
|
* show the user an error. TODO(joel) - what error?
|
|
12655
12628
|
* - Otherwise, pass through the resulting points and message.
|
|
12656
12629
|
*/
|
|
12657
|
-
function scoreExpression(userInput, rubric,
|
|
12658
|
-
// TODO: remove strings as a param for scorers
|
|
12659
|
-
strings, locale) {
|
|
12660
|
-
const validationError = validateExpression(userInput);
|
|
12661
|
-
if (validationError) {
|
|
12662
|
-
return validationError;
|
|
12663
|
-
}
|
|
12630
|
+
function scoreExpression(userInput, rubric, locale) {
|
|
12664
12631
|
const options = _.clone(rubric);
|
|
12665
12632
|
_.extend(options, {
|
|
12666
12633
|
decimal_separator: getDecimalSeparator(locale)
|
|
@@ -12676,7 +12643,11 @@ strings, locale) {
|
|
|
12676
12643
|
// in the function variables list for the expression.
|
|
12677
12644
|
if (!expression.parsed) {
|
|
12678
12645
|
/* c8 ignore next */
|
|
12679
|
-
throw new PerseusError("Unable to parse solution answer for expression", Errors.InvalidInput
|
|
12646
|
+
throw new PerseusError("Unable to parse solution answer for expression", Errors.InvalidInput, {
|
|
12647
|
+
metadata: {
|
|
12648
|
+
rubric: JSON.stringify(rubric)
|
|
12649
|
+
}
|
|
12650
|
+
});
|
|
12680
12651
|
}
|
|
12681
12652
|
return KhanAnswerTypes.expression.createValidatorFunctional(expression.expr, _({}).extend(options, {
|
|
12682
12653
|
simplify: answer.simplify,
|
|
@@ -12771,6 +12742,24 @@ strings, locale) {
|
|
|
12771
12742
|
};
|
|
12772
12743
|
}
|
|
12773
12744
|
|
|
12745
|
+
/**
|
|
12746
|
+
* Checks user input from the expression widget to see if it is scorable.
|
|
12747
|
+
*
|
|
12748
|
+
* Note: Most of the expression widget's validation requires the Rubric because
|
|
12749
|
+
* of its use of KhanAnswerTypes as a core part of scoring.
|
|
12750
|
+
*
|
|
12751
|
+
* @see `scoreExpression()` for more details.
|
|
12752
|
+
*/
|
|
12753
|
+
function validateExpression(userInput) {
|
|
12754
|
+
if (userInput === "") {
|
|
12755
|
+
return {
|
|
12756
|
+
type: "invalid",
|
|
12757
|
+
message: null
|
|
12758
|
+
};
|
|
12759
|
+
}
|
|
12760
|
+
return null;
|
|
12761
|
+
}
|
|
12762
|
+
|
|
12774
12763
|
function getCoefficientsByType(data) {
|
|
12775
12764
|
if (data.coords == null) {
|
|
12776
12765
|
return undefined;
|
|
@@ -13072,47 +13061,33 @@ function scoreInteractiveGraph(userInput, rubric) {
|
|
|
13072
13061
|
|
|
13073
13062
|
// Question state for marker as result of user selected answers.
|
|
13074
13063
|
|
|
13075
|
-
function scoreLabelImageMarker(
|
|
13064
|
+
function scoreLabelImageMarker(userInput, rubric) {
|
|
13076
13065
|
const score = {
|
|
13077
13066
|
hasAnswers: false,
|
|
13078
13067
|
isCorrect: false
|
|
13079
13068
|
};
|
|
13080
|
-
if (
|
|
13069
|
+
if (userInput && userInput.length > 0) {
|
|
13081
13070
|
score.hasAnswers = true;
|
|
13082
13071
|
}
|
|
13083
|
-
if (
|
|
13084
|
-
if (
|
|
13072
|
+
if (rubric.length > 0) {
|
|
13073
|
+
if (userInput && userInput.length === rubric.length) {
|
|
13085
13074
|
// All correct answers are selected by the user.
|
|
13086
|
-
score.isCorrect =
|
|
13075
|
+
score.isCorrect = userInput.every(choice => rubric.includes(choice));
|
|
13087
13076
|
}
|
|
13088
|
-
} else if (!
|
|
13077
|
+
} else if (!userInput || userInput.length === 0) {
|
|
13089
13078
|
// Correct as no answers should be selected by the user.
|
|
13090
13079
|
score.isCorrect = true;
|
|
13091
13080
|
}
|
|
13092
13081
|
return score;
|
|
13093
13082
|
}
|
|
13094
|
-
|
|
13095
|
-
// TODO(LEMS-2440): May need to pull answers out of PerseusLabelImageWidgetOptions[markers] for the rubric
|
|
13096
13083
|
function scoreLabelImage(userInput, rubric) {
|
|
13097
|
-
let numAnswered = 0;
|
|
13098
13084
|
let numCorrect = 0;
|
|
13099
|
-
for (
|
|
13100
|
-
const score = scoreLabelImageMarker(
|
|
13101
|
-
if (score.hasAnswers) {
|
|
13102
|
-
numAnswered++;
|
|
13103
|
-
}
|
|
13085
|
+
for (let i = 0; i < userInput.markers.length; i++) {
|
|
13086
|
+
const score = scoreLabelImageMarker(userInput.markers[i].selected, rubric.markers[i].answers);
|
|
13104
13087
|
if (score.isCorrect) {
|
|
13105
13088
|
numCorrect++;
|
|
13106
13089
|
}
|
|
13107
13090
|
}
|
|
13108
|
-
|
|
13109
|
-
// We expect all question markers to be answered before grading.
|
|
13110
|
-
if (numAnswered !== userInput.markers.length) {
|
|
13111
|
-
return {
|
|
13112
|
-
type: "invalid",
|
|
13113
|
-
message: null
|
|
13114
|
-
};
|
|
13115
|
-
}
|
|
13116
13091
|
return {
|
|
13117
13092
|
type: "points",
|
|
13118
13093
|
// Markers with no expected answers are graded as correct if user
|
|
@@ -13123,8 +13098,8 @@ function scoreLabelImage(userInput, rubric) {
|
|
|
13123
13098
|
};
|
|
13124
13099
|
}
|
|
13125
13100
|
|
|
13126
|
-
function scoreMatcher(
|
|
13127
|
-
const correct = _.isEqual(
|
|
13101
|
+
function scoreMatcher(userInput, rubric) {
|
|
13102
|
+
const correct = _.isEqual(userInput.left, rubric.left) && _.isEqual(userInput.right, rubric.right);
|
|
13128
13103
|
return {
|
|
13129
13104
|
type: "points",
|
|
13130
13105
|
earned: correct ? 1 : 0,
|
|
@@ -13133,35 +13108,7 @@ function scoreMatcher(state, rubric) {
|
|
|
13133
13108
|
};
|
|
13134
13109
|
}
|
|
13135
13110
|
|
|
13136
|
-
/**
|
|
13137
|
-
* Checks user input from the matrix widget to see if it is scorable.
|
|
13138
|
-
*
|
|
13139
|
-
* Note: The matrix widget cannot do much validation without the Scoring
|
|
13140
|
-
* Data because of its use of KhanAnswerTypes as a core part of scoring.
|
|
13141
|
-
*
|
|
13142
|
-
* @see `scoreMatrix()` for more details.
|
|
13143
|
-
*/
|
|
13144
|
-
function validateMatrix(userInput, validationData) {
|
|
13145
|
-
const supplied = userInput.answers;
|
|
13146
|
-
const suppliedSize = getMatrixSize(supplied);
|
|
13147
|
-
for (let row = 0; row < suppliedSize[0]; row++) {
|
|
13148
|
-
for (let col = 0; col < suppliedSize[1]; col++) {
|
|
13149
|
-
if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
|
|
13150
|
-
return {
|
|
13151
|
-
type: "invalid",
|
|
13152
|
-
message: ErrorCodes.FILL_ALL_CELLS_ERROR
|
|
13153
|
-
};
|
|
13154
|
-
}
|
|
13155
|
-
}
|
|
13156
|
-
}
|
|
13157
|
-
return null;
|
|
13158
|
-
}
|
|
13159
|
-
|
|
13160
13111
|
function scoreMatrix(userInput, rubric) {
|
|
13161
|
-
const validationResult = validateMatrix(userInput);
|
|
13162
|
-
if (validationResult != null) {
|
|
13163
|
-
return validationResult;
|
|
13164
|
-
}
|
|
13165
13112
|
const solution = rubric.answers;
|
|
13166
13113
|
const supplied = userInput.answers;
|
|
13167
13114
|
const solutionSize = getMatrixSize(solution);
|
|
@@ -13206,35 +13153,35 @@ function scoreMatrix(userInput, rubric) {
|
|
|
13206
13153
|
}
|
|
13207
13154
|
|
|
13208
13155
|
/**
|
|
13209
|
-
* Checks user input
|
|
13210
|
-
*
|
|
13211
|
-
*
|
|
13212
|
-
*
|
|
13156
|
+
* Checks user input from the matrix widget to see if it is scorable.
|
|
13157
|
+
*
|
|
13158
|
+
* Note: The matrix widget cannot do much validation without the Scoring
|
|
13159
|
+
* Data because of its use of KhanAnswerTypes as a core part of scoring.
|
|
13160
|
+
*
|
|
13161
|
+
* @see `scoreMatrix()` for more details.
|
|
13213
13162
|
*/
|
|
13214
|
-
function
|
|
13215
|
-
const
|
|
13216
|
-
const
|
|
13217
|
-
|
|
13218
|
-
|
|
13219
|
-
|
|
13220
|
-
|
|
13221
|
-
|
|
13222
|
-
|
|
13223
|
-
|
|
13163
|
+
function validateMatrix(userInput) {
|
|
13164
|
+
const supplied = userInput.answers;
|
|
13165
|
+
const suppliedSize = getMatrixSize(supplied);
|
|
13166
|
+
for (let row = 0; row < suppliedSize[0]; row++) {
|
|
13167
|
+
for (let col = 0; col < suppliedSize[1]; col++) {
|
|
13168
|
+
if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
|
|
13169
|
+
return {
|
|
13170
|
+
type: "invalid",
|
|
13171
|
+
message: ErrorCodes.FILL_ALL_CELLS_ERROR
|
|
13172
|
+
};
|
|
13173
|
+
}
|
|
13174
|
+
}
|
|
13224
13175
|
}
|
|
13225
13176
|
return null;
|
|
13226
13177
|
}
|
|
13227
13178
|
|
|
13228
|
-
function scoreNumberLine(userInput,
|
|
13229
|
-
const
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
const
|
|
13234
|
-
const start = scoringData.initialX != null ? scoringData.initialX : range[0];
|
|
13235
|
-
const startRel = scoringData.isInequality ? "ge" : "eq";
|
|
13236
|
-
const correctRel = scoringData.correctRel || "eq";
|
|
13237
|
-
const correctPos = number.equal(userInput.numLinePosition, scoringData.correctX || 0);
|
|
13179
|
+
function scoreNumberLine(userInput, rubric) {
|
|
13180
|
+
const range = rubric.range;
|
|
13181
|
+
const start = rubric.initialX != null ? rubric.initialX : range[0];
|
|
13182
|
+
const startRel = rubric.isInequality ? "ge" : "eq";
|
|
13183
|
+
const correctRel = rubric.correctRel || "eq";
|
|
13184
|
+
const correctPos = number.equal(userInput.numLinePosition, rubric.correctX || 0);
|
|
13238
13185
|
if (correctPos && correctRel === userInput.rel) {
|
|
13239
13186
|
return {
|
|
13240
13187
|
type: "points",
|
|
@@ -13258,6 +13205,26 @@ function scoreNumberLine(userInput, scoringData) {
|
|
|
13258
13205
|
};
|
|
13259
13206
|
}
|
|
13260
13207
|
|
|
13208
|
+
/**
|
|
13209
|
+
* Checks user input is within the allowed range and not the same as the initial
|
|
13210
|
+
* state.
|
|
13211
|
+
* @param userInput
|
|
13212
|
+
* @see 'scoreNumberLine' for the scoring logic.
|
|
13213
|
+
*/
|
|
13214
|
+
function validateNumberLine(userInput) {
|
|
13215
|
+
const divisionRange = userInput.divisionRange;
|
|
13216
|
+
const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
|
|
13217
|
+
|
|
13218
|
+
// TODO: I don't think isTickCrtl is a thing anymore
|
|
13219
|
+
if (userInput.isTickCrtl && outsideAllowedRange) {
|
|
13220
|
+
return {
|
|
13221
|
+
type: "invalid",
|
|
13222
|
+
message: "Number of divisions is outside the allowed range."
|
|
13223
|
+
};
|
|
13224
|
+
}
|
|
13225
|
+
return null;
|
|
13226
|
+
}
|
|
13227
|
+
|
|
13261
13228
|
function _extends() {
|
|
13262
13229
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
13263
13230
|
for (var e = 1; e < arguments.length; e++) {
|
|
@@ -13531,6 +13498,16 @@ function scoreNumericInput(userInput, rubric) {
|
|
|
13531
13498
|
};
|
|
13532
13499
|
}
|
|
13533
13500
|
|
|
13501
|
+
function scoreOrderer(userInput, rubric) {
|
|
13502
|
+
const correct = _.isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
|
|
13503
|
+
return {
|
|
13504
|
+
type: "points",
|
|
13505
|
+
earned: correct ? 1 : 0,
|
|
13506
|
+
total: 1,
|
|
13507
|
+
message: null
|
|
13508
|
+
};
|
|
13509
|
+
}
|
|
13510
|
+
|
|
13534
13511
|
/**
|
|
13535
13512
|
* Checks user input from the orderer widget to see if the user has started
|
|
13536
13513
|
* ordering the options, making the widget scorable.
|
|
@@ -13547,15 +13524,10 @@ function validateOrderer(userInput) {
|
|
|
13547
13524
|
return null;
|
|
13548
13525
|
}
|
|
13549
13526
|
|
|
13550
|
-
function
|
|
13551
|
-
const validateError = validateOrderer(userInput);
|
|
13552
|
-
if (validateError) {
|
|
13553
|
-
return validateError;
|
|
13554
|
-
}
|
|
13555
|
-
const correct = _.isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
|
|
13527
|
+
function scorePlotter(userInput, rubric) {
|
|
13556
13528
|
return {
|
|
13557
13529
|
type: "points",
|
|
13558
|
-
earned: correct ? 1 : 0,
|
|
13530
|
+
earned: approximateDeepEqual(userInput, rubric.correct) ? 1 : 0,
|
|
13559
13531
|
total: 1,
|
|
13560
13532
|
message: null
|
|
13561
13533
|
};
|
|
@@ -13577,45 +13549,7 @@ function validatePlotter(userInput, validationData) {
|
|
|
13577
13549
|
return null;
|
|
13578
13550
|
}
|
|
13579
13551
|
|
|
13580
|
-
function scorePlotter(userInput, scoringData) {
|
|
13581
|
-
const validationError = validatePlotter(userInput, scoringData);
|
|
13582
|
-
if (validationError) {
|
|
13583
|
-
return validationError;
|
|
13584
|
-
}
|
|
13585
|
-
return {
|
|
13586
|
-
type: "points",
|
|
13587
|
-
earned: approximateDeepEqual(userInput, scoringData.correct) ? 1 : 0,
|
|
13588
|
-
total: 1,
|
|
13589
|
-
message: null
|
|
13590
|
-
};
|
|
13591
|
-
}
|
|
13592
|
-
|
|
13593
|
-
/**
|
|
13594
|
-
* Checks if the user has selected at least one option. Additional validation
|
|
13595
|
-
* is done in scoreRadio to check if the number of selected options is correct
|
|
13596
|
-
* and if the user has selected both a correct option and the "none of the above"
|
|
13597
|
-
* option.
|
|
13598
|
-
* @param userInput
|
|
13599
|
-
* @see `scoreRadio` for the additional validation logic and the scoring logic.
|
|
13600
|
-
*/
|
|
13601
|
-
function validateRadio(userInput) {
|
|
13602
|
-
const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
|
|
13603
|
-
return sum + (selected ? 1 : 0);
|
|
13604
|
-
}, 0);
|
|
13605
|
-
if (numSelected === 0) {
|
|
13606
|
-
return {
|
|
13607
|
-
type: "invalid",
|
|
13608
|
-
message: null
|
|
13609
|
-
};
|
|
13610
|
-
}
|
|
13611
|
-
return null;
|
|
13612
|
-
}
|
|
13613
|
-
|
|
13614
13552
|
function scoreRadio(userInput, rubric) {
|
|
13615
|
-
const validationError = validateRadio(userInput);
|
|
13616
|
-
if (validationError) {
|
|
13617
|
-
return validationError;
|
|
13618
|
-
}
|
|
13619
13553
|
const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
|
|
13620
13554
|
return sum + (selected ? 1 : 0);
|
|
13621
13555
|
}, 0);
|
|
@@ -13656,6 +13590,37 @@ function scoreRadio(userInput, rubric) {
|
|
|
13656
13590
|
};
|
|
13657
13591
|
}
|
|
13658
13592
|
|
|
13593
|
+
/**
|
|
13594
|
+
* Checks if the user has selected at least one option. Additional validation
|
|
13595
|
+
* is done in scoreRadio to check if the number of selected options is correct
|
|
13596
|
+
* and if the user has selected both a correct option and the "none of the above"
|
|
13597
|
+
* option.
|
|
13598
|
+
* @param userInput
|
|
13599
|
+
* @see `scoreRadio` for the additional validation logic and the scoring logic.
|
|
13600
|
+
*/
|
|
13601
|
+
function validateRadio(userInput) {
|
|
13602
|
+
const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
|
|
13603
|
+
return sum + (selected ? 1 : 0);
|
|
13604
|
+
}, 0);
|
|
13605
|
+
if (numSelected === 0) {
|
|
13606
|
+
return {
|
|
13607
|
+
type: "invalid",
|
|
13608
|
+
message: null
|
|
13609
|
+
};
|
|
13610
|
+
}
|
|
13611
|
+
return null;
|
|
13612
|
+
}
|
|
13613
|
+
|
|
13614
|
+
function scoreSorter(userInput, rubric) {
|
|
13615
|
+
const correct = approximateDeepEqual(userInput.options, rubric.correct);
|
|
13616
|
+
return {
|
|
13617
|
+
type: "points",
|
|
13618
|
+
earned: correct ? 1 : 0,
|
|
13619
|
+
total: 1,
|
|
13620
|
+
message: null
|
|
13621
|
+
};
|
|
13622
|
+
}
|
|
13623
|
+
|
|
13659
13624
|
/**
|
|
13660
13625
|
* Checks user input for the sorter widget to ensure that the user has made
|
|
13661
13626
|
* changes before attempting to score the widget.
|
|
@@ -13679,20 +13644,6 @@ function validateSorter(userInput) {
|
|
|
13679
13644
|
return null;
|
|
13680
13645
|
}
|
|
13681
13646
|
|
|
13682
|
-
function scoreSorter(userInput, rubric) {
|
|
13683
|
-
const validationError = validateSorter(userInput);
|
|
13684
|
-
if (validationError) {
|
|
13685
|
-
return validationError;
|
|
13686
|
-
}
|
|
13687
|
-
const correct = approximateDeepEqual(userInput.options, rubric.correct);
|
|
13688
|
-
return {
|
|
13689
|
-
type: "points",
|
|
13690
|
-
earned: correct ? 1 : 0,
|
|
13691
|
-
total: 1,
|
|
13692
|
-
message: null
|
|
13693
|
-
};
|
|
13694
|
-
}
|
|
13695
|
-
|
|
13696
13647
|
/**
|
|
13697
13648
|
* Filters the given table (modelled as a 2D array) to remove any rows that are
|
|
13698
13649
|
* completely empty.
|
|
@@ -13836,5 +13787,282 @@ function scoreInputNumber(userInput, rubric) {
|
|
|
13836
13787
|
};
|
|
13837
13788
|
}
|
|
13838
13789
|
|
|
13839
|
-
|
|
13790
|
+
/**
|
|
13791
|
+
* Several widgets don't have "right"/"wrong" scoring logic,
|
|
13792
|
+
* so this just says to move on past those widgets
|
|
13793
|
+
*
|
|
13794
|
+
* TODO(LEMS-2543) widgets that use this probably shouldn't have any
|
|
13795
|
+
* scoring logic and the thing scoring an exercise
|
|
13796
|
+
* should just know to skip these
|
|
13797
|
+
*/
|
|
13798
|
+
function scoreNoop(points = 0) {
|
|
13799
|
+
return {
|
|
13800
|
+
type: "points",
|
|
13801
|
+
earned: points,
|
|
13802
|
+
total: points,
|
|
13803
|
+
message: null
|
|
13804
|
+
};
|
|
13805
|
+
}
|
|
13806
|
+
|
|
13807
|
+
// The `group` widget is basically a widget hosting a full Perseus system in
|
|
13808
|
+
// it. As such, scoring a group means scoring all widgets it contains.
|
|
13809
|
+
function scoreGroup(userInput, rubric, locale) {
|
|
13810
|
+
const scores = scoreWidgetsFunctional(rubric.widgets, Object.keys(rubric.widgets), userInput, locale);
|
|
13811
|
+
return flattenScores(scores);
|
|
13812
|
+
}
|
|
13813
|
+
|
|
13814
|
+
/**
|
|
13815
|
+
* Checks the given user input to see if any answerable widgets have not been
|
|
13816
|
+
* "filled in" (ie. if they're empty). Another way to think about this
|
|
13817
|
+
* function is that its a check to see if we can score the provided input.
|
|
13818
|
+
*/
|
|
13819
|
+
function emptyWidgetsFunctional(widgets,
|
|
13820
|
+
// This is a port of old code, I'm not sure why
|
|
13821
|
+
// we need widgetIds vs the keys of the widgets object
|
|
13822
|
+
widgetIds, userInputMap, locale) {
|
|
13823
|
+
return widgetIds.filter(id => {
|
|
13824
|
+
const widget = widgets[id];
|
|
13825
|
+
if (!widget || widget.static === true) {
|
|
13826
|
+
// Static widgets shouldn't count as empty
|
|
13827
|
+
return false;
|
|
13828
|
+
}
|
|
13829
|
+
const validator = getWidgetValidator(widget.type);
|
|
13830
|
+
const userInput = userInputMap[id];
|
|
13831
|
+
const validationData = widget.options;
|
|
13832
|
+
const score = validator == null ? void 0 : validator(userInput, validationData, locale);
|
|
13833
|
+
if (score) {
|
|
13834
|
+
return scoreIsEmpty(score);
|
|
13835
|
+
}
|
|
13836
|
+
});
|
|
13837
|
+
}
|
|
13838
|
+
|
|
13839
|
+
function validateGroup(userInput, validationData, locale) {
|
|
13840
|
+
const emptyWidgets = emptyWidgetsFunctional(validationData.widgets, Object.keys(validationData.widgets), userInput, locale);
|
|
13841
|
+
if (emptyWidgets.length === 0) {
|
|
13842
|
+
return null;
|
|
13843
|
+
}
|
|
13844
|
+
return {
|
|
13845
|
+
type: "invalid",
|
|
13846
|
+
message: null
|
|
13847
|
+
};
|
|
13848
|
+
}
|
|
13849
|
+
|
|
13850
|
+
function validateLabelImage(userInput) {
|
|
13851
|
+
let numAnswered = 0;
|
|
13852
|
+
for (let i = 0; i < userInput.markers.length; i++) {
|
|
13853
|
+
const userSelection = userInput.markers[i].selected;
|
|
13854
|
+
if (userSelection && userSelection.length > 0) {
|
|
13855
|
+
numAnswered++;
|
|
13856
|
+
}
|
|
13857
|
+
}
|
|
13858
|
+
// We expect all question markers to be answered before grading.
|
|
13859
|
+
if (numAnswered !== userInput.markers.length) {
|
|
13860
|
+
return {
|
|
13861
|
+
type: "invalid",
|
|
13862
|
+
message: null
|
|
13863
|
+
};
|
|
13864
|
+
}
|
|
13865
|
+
return null;
|
|
13866
|
+
}
|
|
13867
|
+
|
|
13868
|
+
function validateMockWidget(userInput) {
|
|
13869
|
+
if (userInput.currentValue == null || userInput.currentValue === "") {
|
|
13870
|
+
return {
|
|
13871
|
+
type: "invalid",
|
|
13872
|
+
message: ""
|
|
13873
|
+
};
|
|
13874
|
+
}
|
|
13875
|
+
return null;
|
|
13876
|
+
}
|
|
13877
|
+
|
|
13878
|
+
function scoreMockWidget(userInput, rubric) {
|
|
13879
|
+
const validationResult = validateMockWidget(userInput);
|
|
13880
|
+
if (validationResult != null) {
|
|
13881
|
+
return validationResult;
|
|
13882
|
+
}
|
|
13883
|
+
return {
|
|
13884
|
+
type: "points",
|
|
13885
|
+
earned: userInput.currentValue === rubric.value ? 1 : 0,
|
|
13886
|
+
total: 1,
|
|
13887
|
+
message: ""
|
|
13888
|
+
};
|
|
13889
|
+
}
|
|
13890
|
+
|
|
13891
|
+
const widgets = {};
|
|
13892
|
+
function registerWidget(type, scorer, validator) {
|
|
13893
|
+
widgets[type] = {
|
|
13894
|
+
scorer,
|
|
13895
|
+
validator
|
|
13896
|
+
};
|
|
13897
|
+
}
|
|
13898
|
+
const getWidgetValidator = name => {
|
|
13899
|
+
var _widgets$name$validat, _widgets$name;
|
|
13900
|
+
return (_widgets$name$validat = (_widgets$name = widgets[name]) == null ? void 0 : _widgets$name.validator) != null ? _widgets$name$validat : null;
|
|
13901
|
+
};
|
|
13902
|
+
const getWidgetScorer = name => {
|
|
13903
|
+
var _widgets$name$scorer, _widgets$name2;
|
|
13904
|
+
return (_widgets$name$scorer = (_widgets$name2 = widgets[name]) == null ? void 0 : _widgets$name2.scorer) != null ? _widgets$name$scorer : null;
|
|
13905
|
+
};
|
|
13906
|
+
registerWidget("categorizer", scoreCategorizer, validateCategorizer);
|
|
13907
|
+
registerWidget("cs-program", scoreCSProgram);
|
|
13908
|
+
registerWidget("dropdown", scoreDropdown, validateDropdown);
|
|
13909
|
+
registerWidget("expression", scoreExpression, validateExpression);
|
|
13910
|
+
registerWidget("grapher", scoreGrapher);
|
|
13911
|
+
registerWidget("group", scoreGroup, validateGroup);
|
|
13912
|
+
registerWidget("iframe", scoreIframe);
|
|
13913
|
+
registerWidget("input-number", scoreInputNumber);
|
|
13914
|
+
registerWidget("interactive-graph", scoreInteractiveGraph);
|
|
13915
|
+
registerWidget("label-image", scoreLabelImage, validateLabelImage);
|
|
13916
|
+
registerWidget("matcher", scoreMatcher);
|
|
13917
|
+
registerWidget("matrix", scoreMatrix, validateMatrix);
|
|
13918
|
+
registerWidget("mock-widget", scoreMockWidget, scoreMockWidget);
|
|
13919
|
+
registerWidget("number-line", scoreNumberLine, validateNumberLine);
|
|
13920
|
+
registerWidget("numeric-input", scoreNumericInput);
|
|
13921
|
+
registerWidget("orderer", scoreOrderer, validateOrderer);
|
|
13922
|
+
registerWidget("plotter", scorePlotter, validatePlotter);
|
|
13923
|
+
registerWidget("radio", scoreRadio, validateRadio);
|
|
13924
|
+
registerWidget("sorter", scoreSorter, validateSorter);
|
|
13925
|
+
registerWidget("table", scoreTable, validateTable);
|
|
13926
|
+
registerWidget("deprecated-standin", () => scoreNoop(1));
|
|
13927
|
+
registerWidget("measurer", () => scoreNoop(1));
|
|
13928
|
+
registerWidget("definition", scoreNoop);
|
|
13929
|
+
registerWidget("explanation", scoreNoop);
|
|
13930
|
+
registerWidget("image", scoreNoop);
|
|
13931
|
+
registerWidget("interaction", scoreNoop);
|
|
13932
|
+
registerWidget("molecule", scoreNoop);
|
|
13933
|
+
registerWidget("passage", scoreNoop);
|
|
13934
|
+
registerWidget("passage-ref", scoreNoop);
|
|
13935
|
+
registerWidget("passage-ref-target", scoreNoop);
|
|
13936
|
+
registerWidget("video", scoreNoop);
|
|
13937
|
+
|
|
13938
|
+
const noScore = {
|
|
13939
|
+
type: "points",
|
|
13940
|
+
earned: 0,
|
|
13941
|
+
total: 0,
|
|
13942
|
+
message: null
|
|
13943
|
+
};
|
|
13944
|
+
|
|
13945
|
+
/**
|
|
13946
|
+
* If a widget says that it is empty once it is graded.
|
|
13947
|
+
* Trying to encapsulate references to the score format.
|
|
13948
|
+
*/
|
|
13949
|
+
function scoreIsEmpty(score) {
|
|
13950
|
+
// HACK(benkomalo): ugh. this isn't great; the Perseus score objects
|
|
13951
|
+
// overload the type "invalid" for what should probably be three
|
|
13952
|
+
// distinct cases:
|
|
13953
|
+
// - truly empty or not fully filled out
|
|
13954
|
+
// - invalid or malformed inputs
|
|
13955
|
+
// - "almost correct" like inputs where the widget wants to give
|
|
13956
|
+
// feedback (e.g. a fraction needs to be reduced, or `pi` should
|
|
13957
|
+
// be used instead of 3.14)
|
|
13958
|
+
//
|
|
13959
|
+
// Unfortunately the coercion happens all over the place, as these
|
|
13960
|
+
// Perseus style score objects are created *everywhere* (basically
|
|
13961
|
+
// in every widget), so it's hard to change now. We assume that
|
|
13962
|
+
// anything with a "message" is not truly empty, and one of the
|
|
13963
|
+
// latter two cases for now.
|
|
13964
|
+
return score.type === "invalid" && (!score.message || score.message.length === 0);
|
|
13965
|
+
}
|
|
13966
|
+
|
|
13967
|
+
/**
|
|
13968
|
+
* Combine two score objects.
|
|
13969
|
+
*
|
|
13970
|
+
* Given two score objects for two different widgets, combine them so that
|
|
13971
|
+
* if one is wrong, the total score is wrong, etc.
|
|
13972
|
+
*/
|
|
13973
|
+
function combineScores(scoreA, scoreB) {
|
|
13974
|
+
let message;
|
|
13975
|
+
if (scoreA.type === "points" && scoreB.type === "points") {
|
|
13976
|
+
if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
|
|
13977
|
+
// TODO(alpert): Figure out how to combine messages usefully
|
|
13978
|
+
message = null;
|
|
13979
|
+
} else {
|
|
13980
|
+
message = scoreA.message || scoreB.message;
|
|
13981
|
+
}
|
|
13982
|
+
return {
|
|
13983
|
+
type: "points",
|
|
13984
|
+
earned: scoreA.earned + scoreB.earned,
|
|
13985
|
+
total: scoreA.total + scoreB.total,
|
|
13986
|
+
message: message
|
|
13987
|
+
};
|
|
13988
|
+
}
|
|
13989
|
+
if (scoreA.type === "points" && scoreB.type === "invalid") {
|
|
13990
|
+
return scoreB;
|
|
13991
|
+
}
|
|
13992
|
+
if (scoreA.type === "invalid" && scoreB.type === "points") {
|
|
13993
|
+
return scoreA;
|
|
13994
|
+
}
|
|
13995
|
+
if (scoreA.type === "invalid" && scoreB.type === "invalid") {
|
|
13996
|
+
if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
|
|
13997
|
+
// TODO(alpert): Figure out how to combine messages usefully
|
|
13998
|
+
message = null;
|
|
13999
|
+
} else {
|
|
14000
|
+
message = scoreA.message || scoreB.message;
|
|
14001
|
+
}
|
|
14002
|
+
return {
|
|
14003
|
+
type: "invalid",
|
|
14004
|
+
message: message
|
|
14005
|
+
};
|
|
14006
|
+
}
|
|
14007
|
+
|
|
14008
|
+
/**
|
|
14009
|
+
* The above checks cover all combinations of score type, so if we get here
|
|
14010
|
+
* then something is amiss with our inputs.
|
|
14011
|
+
*/
|
|
14012
|
+
throw new PerseusError("PerseusScore with unknown type encountered", Errors.InvalidInput, {
|
|
14013
|
+
metadata: {
|
|
14014
|
+
scoreA: JSON.stringify(scoreA),
|
|
14015
|
+
scoreB: JSON.stringify(scoreB)
|
|
14016
|
+
}
|
|
14017
|
+
});
|
|
14018
|
+
}
|
|
14019
|
+
function flattenScores(widgetScoreMap) {
|
|
14020
|
+
return Object.values(widgetScoreMap).reduce(combineScores, noScore);
|
|
14021
|
+
}
|
|
14022
|
+
|
|
14023
|
+
// once scorePerseusItem is the only one calling scoreWidgetsFunctional
|
|
14024
|
+
function scorePerseusItem(perseusRenderData, userInputMap, locale) {
|
|
14025
|
+
// There seems to be a chance that PerseusRenderer.widgets might include
|
|
14026
|
+
// widget data for widgets that are not in PerseusRenderer.content,
|
|
14027
|
+
// so this checks that the widgets are being used before scoring them
|
|
14028
|
+
const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
|
|
14029
|
+
const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, locale);
|
|
14030
|
+
return flattenScores(scores);
|
|
14031
|
+
}
|
|
14032
|
+
|
|
14033
|
+
// TODO: combine scorePerseusItem with scoreWidgetsFunctional
|
|
14034
|
+
function scoreWidgetsFunctional(widgets,
|
|
14035
|
+
// This is a port of old code, I'm not sure why
|
|
14036
|
+
// we need widgetIds vs the keys of the widgets object
|
|
14037
|
+
widgetIds, userInputMap, locale) {
|
|
14038
|
+
const upgradedWidgets = getUpgradedWidgetOptions(widgets);
|
|
14039
|
+
const gradedWidgetIds = widgetIds.filter(id => {
|
|
14040
|
+
const props = upgradedWidgets[id];
|
|
14041
|
+
const widgetIsGraded = (props == null ? void 0 : props.graded) == null || props.graded;
|
|
14042
|
+
const widgetIsStatic = !!(props != null && props.static);
|
|
14043
|
+
// Ungraded widgets or widgets set to static shouldn't be graded.
|
|
14044
|
+
return widgetIsGraded && !widgetIsStatic;
|
|
14045
|
+
});
|
|
14046
|
+
const widgetScores = {};
|
|
14047
|
+
gradedWidgetIds.forEach(id => {
|
|
14048
|
+
var _validator;
|
|
14049
|
+
const widget = upgradedWidgets[id];
|
|
14050
|
+
if (!widget) {
|
|
14051
|
+
return;
|
|
14052
|
+
}
|
|
14053
|
+
const userInput = userInputMap[id];
|
|
14054
|
+
const validator = getWidgetValidator(widget.type);
|
|
14055
|
+
const scorer = getWidgetScorer(widget.type);
|
|
14056
|
+
|
|
14057
|
+
// We do validation (empty checks) first and then scoring. If
|
|
14058
|
+
// validation fails, it's result is itself a PerseusScore.
|
|
14059
|
+
const score = (_validator = validator == null ? void 0 : validator(userInput, widget.options, locale)) != null ? _validator : scorer == null ? void 0 : scorer(userInput, widget.options, locale);
|
|
14060
|
+
if (score != null) {
|
|
14061
|
+
widgetScores[id] = score;
|
|
14062
|
+
}
|
|
14063
|
+
});
|
|
14064
|
+
return widgetScores;
|
|
14065
|
+
}
|
|
14066
|
+
|
|
14067
|
+
export { ErrorCodes, KhanAnswerTypes, emptyWidgetsFunctional, flattenScores, getWidgetScorer, getWidgetValidator, inputNumberAnswerTypes, registerWidget, scoreCSProgram, scoreCategorizer, scoreDropdown, scoreExpression, scoreGrapher, scoreIframe, scoreInputNumber, scoreInteractiveGraph, scoreLabelImage, scoreLabelImageMarker, scoreMatcher, scoreMatrix, scoreNumberLine, scoreNumericInput, scoreOrderer, scorePerseusItem, scorePlotter, scoreRadio, scoreSorter, scoreTable, scoreWidgetsFunctional, validateCategorizer, validateDropdown, validateExpression, validateMatrix, validateNumberLine, validateOrderer, validatePlotter, validateRadio, validateSorter, validateTable };
|
|
13840
14068
|
//# sourceMappingURL=index.js.map
|