@khanacademy/perseus-score 2.0.0 → 2.2.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 +419 -187
- package/dist/es/index.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +422 -185
- 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 +2 -0
- package/dist/widgets/group/score-group.d.ts +3 -0
- package/dist/widgets/group/validate-group.d.ts +3 -0
- 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/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,25 +12560,6 @@ function validateCategorizer(userInput, validationData) {
|
|
|
12545
12560
|
return null;
|
|
12546
12561
|
}
|
|
12547
12562
|
|
|
12548
|
-
function scoreCategorizer(userInput, rubric) {
|
|
12549
|
-
const validationError = validateCategorizer(userInput, rubric);
|
|
12550
|
-
if (validationError) {
|
|
12551
|
-
return validationError;
|
|
12552
|
-
}
|
|
12553
|
-
let allCorrect = true;
|
|
12554
|
-
rubric.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
12563
|
function scoreCSProgram(userInput) {
|
|
12568
12564
|
// The CS program can tell us whether it's correct or incorrect,
|
|
12569
12565
|
// and pass an optional message
|
|
@@ -12589,25 +12585,7 @@ function scoreCSProgram(userInput) {
|
|
|
12589
12585
|
};
|
|
12590
12586
|
}
|
|
12591
12587
|
|
|
12592
|
-
/**
|
|
12593
|
-
* Checks if the user has selected an item from the dropdown before scoring.
|
|
12594
|
-
* This is shown with a userInput value / index other than 0.
|
|
12595
|
-
*/
|
|
12596
|
-
function validateDropdown(userInput) {
|
|
12597
|
-
if (userInput.value === 0) {
|
|
12598
|
-
return {
|
|
12599
|
-
type: "invalid",
|
|
12600
|
-
message: null
|
|
12601
|
-
};
|
|
12602
|
-
}
|
|
12603
|
-
return null;
|
|
12604
|
-
}
|
|
12605
|
-
|
|
12606
12588
|
function scoreDropdown(userInput, rubric) {
|
|
12607
|
-
const validationError = validateDropdown(userInput);
|
|
12608
|
-
if (validationError) {
|
|
12609
|
-
return validationError;
|
|
12610
|
-
}
|
|
12611
12589
|
const correct = rubric.choices[userInput.value - 1].correct;
|
|
12612
12590
|
return {
|
|
12613
12591
|
type: "points",
|
|
@@ -12618,15 +12596,11 @@ function scoreDropdown(userInput, rubric) {
|
|
|
12618
12596
|
}
|
|
12619
12597
|
|
|
12620
12598
|
/**
|
|
12621
|
-
* Checks
|
|
12622
|
-
*
|
|
12623
|
-
* Note: Most of the expression widget's validation requires the Rubric because
|
|
12624
|
-
* of its use of KhanAnswerTypes as a core part of scoring.
|
|
12625
|
-
*
|
|
12626
|
-
* @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.
|
|
12627
12601
|
*/
|
|
12628
|
-
function
|
|
12629
|
-
if (userInput ===
|
|
12602
|
+
function validateDropdown(userInput) {
|
|
12603
|
+
if (userInput.value === 0) {
|
|
12630
12604
|
return {
|
|
12631
12605
|
type: "invalid",
|
|
12632
12606
|
message: null
|
|
@@ -12654,10 +12628,6 @@ function validateExpression(userInput) {
|
|
|
12654
12628
|
* - Otherwise, pass through the resulting points and message.
|
|
12655
12629
|
*/
|
|
12656
12630
|
function scoreExpression(userInput, rubric, locale) {
|
|
12657
|
-
const validationError = validateExpression(userInput);
|
|
12658
|
-
if (validationError) {
|
|
12659
|
-
return validationError;
|
|
12660
|
-
}
|
|
12661
12631
|
const options = _.clone(rubric);
|
|
12662
12632
|
_.extend(options, {
|
|
12663
12633
|
decimal_separator: getDecimalSeparator(locale)
|
|
@@ -12772,6 +12742,24 @@ function scoreExpression(userInput, rubric, locale) {
|
|
|
12772
12742
|
};
|
|
12773
12743
|
}
|
|
12774
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
|
+
|
|
12775
12763
|
function getCoefficientsByType(data) {
|
|
12776
12764
|
if (data.coords == null) {
|
|
12777
12765
|
return undefined;
|
|
@@ -12859,7 +12847,8 @@ function scoreIframe(userInput) {
|
|
|
12859
12847
|
const {
|
|
12860
12848
|
collinear,
|
|
12861
12849
|
canonicalSineCoefficients,
|
|
12862
|
-
similar
|
|
12850
|
+
similar,
|
|
12851
|
+
clockwise
|
|
12863
12852
|
} = geometry;
|
|
12864
12853
|
const {
|
|
12865
12854
|
getClockwiseAngle
|
|
@@ -13019,9 +13008,27 @@ function scoreInteractiveGraph(userInput, rubric) {
|
|
|
13019
13008
|
};
|
|
13020
13009
|
}
|
|
13021
13010
|
} else if (userInput.type === "angle" && rubric.correct.type === "angle") {
|
|
13022
|
-
const
|
|
13011
|
+
const coords = userInput.coords;
|
|
13023
13012
|
const correct = rubric.correct.coords;
|
|
13024
13013
|
const allowReflexAngles = rubric.correct.allowReflexAngles;
|
|
13014
|
+
|
|
13015
|
+
// While the angle graph should always have 3 points, our types
|
|
13016
|
+
// technically allow for null values. We'll check for that here.
|
|
13017
|
+
// TODO: (LEMS-2857) We would like to update the type of coords
|
|
13018
|
+
// to be non-nullable, as the graph should always have 3 points.
|
|
13019
|
+
if (!coords) {
|
|
13020
|
+
return {
|
|
13021
|
+
type: "invalid",
|
|
13022
|
+
message: null
|
|
13023
|
+
};
|
|
13024
|
+
}
|
|
13025
|
+
|
|
13026
|
+
// We need to check both the direction of the angle and the
|
|
13027
|
+
// whether the graph allows for reflexive angles in order to
|
|
13028
|
+
// to determine if we need to reverse the coords for scoring.
|
|
13029
|
+
const areClockwise = clockwise([coords[0], coords[2], coords[1]]);
|
|
13030
|
+
const shouldReverseCoords = areClockwise && !allowReflexAngles;
|
|
13031
|
+
const guess = shouldReverseCoords ? coords.slice().reverse() : coords;
|
|
13025
13032
|
let match;
|
|
13026
13033
|
if (rubric.correct.match === "congruent") {
|
|
13027
13034
|
const angles = _.map([guess, correct], function (coords) {
|
|
@@ -13035,13 +13042,7 @@ function scoreInteractiveGraph(userInput, rubric) {
|
|
|
13035
13042
|
match = approximateEqual(...angles);
|
|
13036
13043
|
} else {
|
|
13037
13044
|
/* exact */
|
|
13038
|
-
match =
|
|
13039
|
-
// @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
|
|
13040
|
-
approximateDeepEqual(guess[1], correct[1]) &&
|
|
13041
|
-
// @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
|
|
13042
|
-
collinear(correct[1], correct[0], guess[0]) &&
|
|
13043
|
-
// @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
|
|
13044
|
-
collinear(correct[1], correct[2], guess[2]);
|
|
13045
|
+
match = approximateDeepEqual(guess[1], correct[1]) && collinear(correct[1], correct[0], guess[0]) && collinear(correct[1], correct[2], guess[2]);
|
|
13045
13046
|
}
|
|
13046
13047
|
if (match) {
|
|
13047
13048
|
return {
|
|
@@ -13071,24 +13072,6 @@ function scoreInteractiveGraph(userInput, rubric) {
|
|
|
13071
13072
|
};
|
|
13072
13073
|
}
|
|
13073
13074
|
|
|
13074
|
-
function validateLabelImage(userInput) {
|
|
13075
|
-
let numAnswered = 0;
|
|
13076
|
-
for (let i = 0; i < userInput.markers.length; i++) {
|
|
13077
|
-
const userSelection = userInput.markers[i].selected;
|
|
13078
|
-
if (userSelection && userSelection.length > 0) {
|
|
13079
|
-
numAnswered++;
|
|
13080
|
-
}
|
|
13081
|
-
}
|
|
13082
|
-
// We expect all question markers to be answered before grading.
|
|
13083
|
-
if (numAnswered !== userInput.markers.length) {
|
|
13084
|
-
return {
|
|
13085
|
-
type: "invalid",
|
|
13086
|
-
message: null
|
|
13087
|
-
};
|
|
13088
|
-
}
|
|
13089
|
-
return null;
|
|
13090
|
-
}
|
|
13091
|
-
|
|
13092
13075
|
// Question state for marker as result of user selected answers.
|
|
13093
13076
|
|
|
13094
13077
|
function scoreLabelImageMarker(userInput, rubric) {
|
|
@@ -13111,10 +13094,6 @@ function scoreLabelImageMarker(userInput, rubric) {
|
|
|
13111
13094
|
return score;
|
|
13112
13095
|
}
|
|
13113
13096
|
function scoreLabelImage(userInput, rubric) {
|
|
13114
|
-
const validationError = validateLabelImage(userInput);
|
|
13115
|
-
if (validationError) {
|
|
13116
|
-
return validationError;
|
|
13117
|
-
}
|
|
13118
13097
|
let numCorrect = 0;
|
|
13119
13098
|
for (let i = 0; i < userInput.markers.length; i++) {
|
|
13120
13099
|
const score = scoreLabelImageMarker(userInput.markers[i].selected, rubric.markers[i].answers);
|
|
@@ -13142,35 +13121,7 @@ function scoreMatcher(userInput, rubric) {
|
|
|
13142
13121
|
};
|
|
13143
13122
|
}
|
|
13144
13123
|
|
|
13145
|
-
/**
|
|
13146
|
-
* Checks user input from the matrix widget to see if it is scorable.
|
|
13147
|
-
*
|
|
13148
|
-
* Note: The matrix widget cannot do much validation without the Scoring
|
|
13149
|
-
* Data because of its use of KhanAnswerTypes as a core part of scoring.
|
|
13150
|
-
*
|
|
13151
|
-
* @see `scoreMatrix()` for more details.
|
|
13152
|
-
*/
|
|
13153
|
-
function validateMatrix(userInput) {
|
|
13154
|
-
const supplied = userInput.answers;
|
|
13155
|
-
const suppliedSize = getMatrixSize(supplied);
|
|
13156
|
-
for (let row = 0; row < suppliedSize[0]; row++) {
|
|
13157
|
-
for (let col = 0; col < suppliedSize[1]; col++) {
|
|
13158
|
-
if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
|
|
13159
|
-
return {
|
|
13160
|
-
type: "invalid",
|
|
13161
|
-
message: ErrorCodes.FILL_ALL_CELLS_ERROR
|
|
13162
|
-
};
|
|
13163
|
-
}
|
|
13164
|
-
}
|
|
13165
|
-
}
|
|
13166
|
-
return null;
|
|
13167
|
-
}
|
|
13168
|
-
|
|
13169
13124
|
function scoreMatrix(userInput, rubric) {
|
|
13170
|
-
const validationError = validateMatrix(userInput);
|
|
13171
|
-
if (validationError != null) {
|
|
13172
|
-
return validationError;
|
|
13173
|
-
}
|
|
13174
13125
|
const solution = rubric.answers;
|
|
13175
13126
|
const supplied = userInput.answers;
|
|
13176
13127
|
const solutionSize = getMatrixSize(solution);
|
|
@@ -13215,30 +13166,30 @@ function scoreMatrix(userInput, rubric) {
|
|
|
13215
13166
|
}
|
|
13216
13167
|
|
|
13217
13168
|
/**
|
|
13218
|
-
* Checks user input
|
|
13219
|
-
*
|
|
13220
|
-
*
|
|
13221
|
-
*
|
|
13169
|
+
* Checks user input from the matrix widget to see if it is scorable.
|
|
13170
|
+
*
|
|
13171
|
+
* Note: The matrix widget cannot do much validation without the Scoring
|
|
13172
|
+
* Data because of its use of KhanAnswerTypes as a core part of scoring.
|
|
13173
|
+
*
|
|
13174
|
+
* @see `scoreMatrix()` for more details.
|
|
13222
13175
|
*/
|
|
13223
|
-
function
|
|
13224
|
-
const
|
|
13225
|
-
const
|
|
13226
|
-
|
|
13227
|
-
|
|
13228
|
-
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13176
|
+
function validateMatrix(userInput) {
|
|
13177
|
+
const supplied = userInput.answers;
|
|
13178
|
+
const suppliedSize = getMatrixSize(supplied);
|
|
13179
|
+
for (let row = 0; row < suppliedSize[0]; row++) {
|
|
13180
|
+
for (let col = 0; col < suppliedSize[1]; col++) {
|
|
13181
|
+
if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
|
|
13182
|
+
return {
|
|
13183
|
+
type: "invalid",
|
|
13184
|
+
message: ErrorCodes.FILL_ALL_CELLS_ERROR
|
|
13185
|
+
};
|
|
13186
|
+
}
|
|
13187
|
+
}
|
|
13233
13188
|
}
|
|
13234
13189
|
return null;
|
|
13235
13190
|
}
|
|
13236
13191
|
|
|
13237
13192
|
function scoreNumberLine(userInput, rubric) {
|
|
13238
|
-
const validationError = validateNumberLine(userInput);
|
|
13239
|
-
if (validationError) {
|
|
13240
|
-
return validationError;
|
|
13241
|
-
}
|
|
13242
13193
|
const range = rubric.range;
|
|
13243
13194
|
const start = rubric.initialX != null ? rubric.initialX : range[0];
|
|
13244
13195
|
const startRel = rubric.isInequality ? "ge" : "eq";
|
|
@@ -13267,6 +13218,26 @@ function scoreNumberLine(userInput, rubric) {
|
|
|
13267
13218
|
};
|
|
13268
13219
|
}
|
|
13269
13220
|
|
|
13221
|
+
/**
|
|
13222
|
+
* Checks user input is within the allowed range and not the same as the initial
|
|
13223
|
+
* state.
|
|
13224
|
+
* @param userInput
|
|
13225
|
+
* @see 'scoreNumberLine' for the scoring logic.
|
|
13226
|
+
*/
|
|
13227
|
+
function validateNumberLine(userInput) {
|
|
13228
|
+
const divisionRange = userInput.divisionRange;
|
|
13229
|
+
const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
|
|
13230
|
+
|
|
13231
|
+
// TODO: I don't think isTickCrtl is a thing anymore
|
|
13232
|
+
if (userInput.isTickCrtl && outsideAllowedRange) {
|
|
13233
|
+
return {
|
|
13234
|
+
type: "invalid",
|
|
13235
|
+
message: "Number of divisions is outside the allowed range."
|
|
13236
|
+
};
|
|
13237
|
+
}
|
|
13238
|
+
return null;
|
|
13239
|
+
}
|
|
13240
|
+
|
|
13270
13241
|
function _extends() {
|
|
13271
13242
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
13272
13243
|
for (var e = 1; e < arguments.length; e++) {
|
|
@@ -13540,27 +13511,7 @@ function scoreNumericInput(userInput, rubric) {
|
|
|
13540
13511
|
};
|
|
13541
13512
|
}
|
|
13542
13513
|
|
|
13543
|
-
/**
|
|
13544
|
-
* Checks user input from the orderer widget to see if the user has started
|
|
13545
|
-
* ordering the options, making the widget scorable.
|
|
13546
|
-
* @param userInput
|
|
13547
|
-
* @see `scoreOrderer` for more details.
|
|
13548
|
-
*/
|
|
13549
|
-
function validateOrderer(userInput) {
|
|
13550
|
-
if (userInput.current.length === 0) {
|
|
13551
|
-
return {
|
|
13552
|
-
type: "invalid",
|
|
13553
|
-
message: null
|
|
13554
|
-
};
|
|
13555
|
-
}
|
|
13556
|
-
return null;
|
|
13557
|
-
}
|
|
13558
|
-
|
|
13559
13514
|
function scoreOrderer(userInput, rubric) {
|
|
13560
|
-
const validationError = validateOrderer(userInput);
|
|
13561
|
-
if (validationError) {
|
|
13562
|
-
return validationError;
|
|
13563
|
-
}
|
|
13564
13515
|
const correct = _.isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
|
|
13565
13516
|
return {
|
|
13566
13517
|
type: "points",
|
|
@@ -13571,13 +13522,13 @@ function scoreOrderer(userInput, rubric) {
|
|
|
13571
13522
|
}
|
|
13572
13523
|
|
|
13573
13524
|
/**
|
|
13574
|
-
* Checks user input
|
|
13575
|
-
*
|
|
13576
|
-
*
|
|
13577
|
-
* @see
|
|
13525
|
+
* Checks user input from the orderer widget to see if the user has started
|
|
13526
|
+
* ordering the options, making the widget scorable.
|
|
13527
|
+
* @param userInput
|
|
13528
|
+
* @see `scoreOrderer` for more details.
|
|
13578
13529
|
*/
|
|
13579
|
-
function
|
|
13580
|
-
if (
|
|
13530
|
+
function validateOrderer(userInput) {
|
|
13531
|
+
if (userInput.current.length === 0) {
|
|
13581
13532
|
return {
|
|
13582
13533
|
type: "invalid",
|
|
13583
13534
|
message: null
|
|
@@ -13587,10 +13538,6 @@ function validatePlotter(userInput, validationData) {
|
|
|
13587
13538
|
}
|
|
13588
13539
|
|
|
13589
13540
|
function scorePlotter(userInput, rubric) {
|
|
13590
|
-
const validationError = validatePlotter(userInput, rubric);
|
|
13591
|
-
if (validationError) {
|
|
13592
|
-
return validationError;
|
|
13593
|
-
}
|
|
13594
13541
|
return {
|
|
13595
13542
|
type: "points",
|
|
13596
13543
|
earned: approximateDeepEqual(userInput, rubric.correct) ? 1 : 0,
|
|
@@ -13600,18 +13547,13 @@ function scorePlotter(userInput, rubric) {
|
|
|
13600
13547
|
}
|
|
13601
13548
|
|
|
13602
13549
|
/**
|
|
13603
|
-
* Checks
|
|
13604
|
-
*
|
|
13605
|
-
*
|
|
13606
|
-
*
|
|
13607
|
-
* @param userInput
|
|
13608
|
-
* @see `scoreRadio` for the additional validation logic and the scoring logic.
|
|
13550
|
+
* Checks user input to confirm it is not the same as the starting values for the graph.
|
|
13551
|
+
* This means the user has modified the graph, and the question can be scored.
|
|
13552
|
+
*
|
|
13553
|
+
* @see 'scorePlotter' for more details on scoring.
|
|
13609
13554
|
*/
|
|
13610
|
-
function
|
|
13611
|
-
|
|
13612
|
-
return sum + (selected ? 1 : 0);
|
|
13613
|
-
}, 0);
|
|
13614
|
-
if (numSelected === 0) {
|
|
13555
|
+
function validatePlotter(userInput, validationData) {
|
|
13556
|
+
if (approximateDeepEqual(userInput, validationData.starting)) {
|
|
13615
13557
|
return {
|
|
13616
13558
|
type: "invalid",
|
|
13617
13559
|
message: null
|
|
@@ -13621,10 +13563,6 @@ function validateRadio(userInput) {
|
|
|
13621
13563
|
}
|
|
13622
13564
|
|
|
13623
13565
|
function scoreRadio(userInput, rubric) {
|
|
13624
|
-
const validationError = validateRadio(userInput);
|
|
13625
|
-
if (validationError) {
|
|
13626
|
-
return validationError;
|
|
13627
|
-
}
|
|
13628
13566
|
const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
|
|
13629
13567
|
return sum + (selected ? 1 : 0);
|
|
13630
13568
|
}, 0);
|
|
@@ -13665,6 +13603,37 @@ function scoreRadio(userInput, rubric) {
|
|
|
13665
13603
|
};
|
|
13666
13604
|
}
|
|
13667
13605
|
|
|
13606
|
+
/**
|
|
13607
|
+
* Checks if the user has selected at least one option. Additional validation
|
|
13608
|
+
* is done in scoreRadio to check if the number of selected options is correct
|
|
13609
|
+
* and if the user has selected both a correct option and the "none of the above"
|
|
13610
|
+
* option.
|
|
13611
|
+
* @param userInput
|
|
13612
|
+
* @see `scoreRadio` for the additional validation logic and the scoring logic.
|
|
13613
|
+
*/
|
|
13614
|
+
function validateRadio(userInput) {
|
|
13615
|
+
const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
|
|
13616
|
+
return sum + (selected ? 1 : 0);
|
|
13617
|
+
}, 0);
|
|
13618
|
+
if (numSelected === 0) {
|
|
13619
|
+
return {
|
|
13620
|
+
type: "invalid",
|
|
13621
|
+
message: null
|
|
13622
|
+
};
|
|
13623
|
+
}
|
|
13624
|
+
return null;
|
|
13625
|
+
}
|
|
13626
|
+
|
|
13627
|
+
function scoreSorter(userInput, rubric) {
|
|
13628
|
+
const correct = approximateDeepEqual(userInput.options, rubric.correct);
|
|
13629
|
+
return {
|
|
13630
|
+
type: "points",
|
|
13631
|
+
earned: correct ? 1 : 0,
|
|
13632
|
+
total: 1,
|
|
13633
|
+
message: null
|
|
13634
|
+
};
|
|
13635
|
+
}
|
|
13636
|
+
|
|
13668
13637
|
/**
|
|
13669
13638
|
* Checks user input for the sorter widget to ensure that the user has made
|
|
13670
13639
|
* changes before attempting to score the widget.
|
|
@@ -13688,20 +13657,6 @@ function validateSorter(userInput) {
|
|
|
13688
13657
|
return null;
|
|
13689
13658
|
}
|
|
13690
13659
|
|
|
13691
|
-
function scoreSorter(userInput, rubric) {
|
|
13692
|
-
const validationError = validateSorter(userInput);
|
|
13693
|
-
if (validationError) {
|
|
13694
|
-
return validationError;
|
|
13695
|
-
}
|
|
13696
|
-
const correct = approximateDeepEqual(userInput.options, rubric.correct);
|
|
13697
|
-
return {
|
|
13698
|
-
type: "points",
|
|
13699
|
-
earned: correct ? 1 : 0,
|
|
13700
|
-
total: 1,
|
|
13701
|
-
message: null
|
|
13702
|
-
};
|
|
13703
|
-
}
|
|
13704
|
-
|
|
13705
13660
|
/**
|
|
13706
13661
|
* Filters the given table (modelled as a 2D array) to remove any rows that are
|
|
13707
13662
|
* completely empty.
|
|
@@ -13845,5 +13800,282 @@ function scoreInputNumber(userInput, rubric) {
|
|
|
13845
13800
|
};
|
|
13846
13801
|
}
|
|
13847
13802
|
|
|
13848
|
-
|
|
13803
|
+
/**
|
|
13804
|
+
* Several widgets don't have "right"/"wrong" scoring logic,
|
|
13805
|
+
* so this just says to move on past those widgets
|
|
13806
|
+
*
|
|
13807
|
+
* TODO(LEMS-2543) widgets that use this probably shouldn't have any
|
|
13808
|
+
* scoring logic and the thing scoring an exercise
|
|
13809
|
+
* should just know to skip these
|
|
13810
|
+
*/
|
|
13811
|
+
function scoreNoop(points = 0) {
|
|
13812
|
+
return {
|
|
13813
|
+
type: "points",
|
|
13814
|
+
earned: points,
|
|
13815
|
+
total: points,
|
|
13816
|
+
message: null
|
|
13817
|
+
};
|
|
13818
|
+
}
|
|
13819
|
+
|
|
13820
|
+
// The `group` widget is basically a widget hosting a full Perseus system in
|
|
13821
|
+
// it. As such, scoring a group means scoring all widgets it contains.
|
|
13822
|
+
function scoreGroup(userInput, rubric, locale) {
|
|
13823
|
+
const scores = scoreWidgetsFunctional(rubric.widgets, Object.keys(rubric.widgets), userInput, locale);
|
|
13824
|
+
return flattenScores(scores);
|
|
13825
|
+
}
|
|
13826
|
+
|
|
13827
|
+
/**
|
|
13828
|
+
* Checks the given user input to see if any answerable widgets have not been
|
|
13829
|
+
* "filled in" (ie. if they're empty). Another way to think about this
|
|
13830
|
+
* function is that its a check to see if we can score the provided input.
|
|
13831
|
+
*/
|
|
13832
|
+
function emptyWidgetsFunctional(widgets,
|
|
13833
|
+
// This is a port of old code, I'm not sure why
|
|
13834
|
+
// we need widgetIds vs the keys of the widgets object
|
|
13835
|
+
widgetIds, userInputMap, locale) {
|
|
13836
|
+
return widgetIds.filter(id => {
|
|
13837
|
+
const widget = widgets[id];
|
|
13838
|
+
if (!widget || widget.static === true) {
|
|
13839
|
+
// Static widgets shouldn't count as empty
|
|
13840
|
+
return false;
|
|
13841
|
+
}
|
|
13842
|
+
const validator = getWidgetValidator(widget.type);
|
|
13843
|
+
const userInput = userInputMap[id];
|
|
13844
|
+
const validationData = widget.options;
|
|
13845
|
+
const score = validator == null ? void 0 : validator(userInput, validationData, locale);
|
|
13846
|
+
if (score) {
|
|
13847
|
+
return scoreIsEmpty(score);
|
|
13848
|
+
}
|
|
13849
|
+
});
|
|
13850
|
+
}
|
|
13851
|
+
|
|
13852
|
+
function validateGroup(userInput, validationData, locale) {
|
|
13853
|
+
const emptyWidgets = emptyWidgetsFunctional(validationData.widgets, Object.keys(validationData.widgets), userInput, locale);
|
|
13854
|
+
if (emptyWidgets.length === 0) {
|
|
13855
|
+
return null;
|
|
13856
|
+
}
|
|
13857
|
+
return {
|
|
13858
|
+
type: "invalid",
|
|
13859
|
+
message: null
|
|
13860
|
+
};
|
|
13861
|
+
}
|
|
13862
|
+
|
|
13863
|
+
function validateLabelImage(userInput) {
|
|
13864
|
+
let numAnswered = 0;
|
|
13865
|
+
for (let i = 0; i < userInput.markers.length; i++) {
|
|
13866
|
+
const userSelection = userInput.markers[i].selected;
|
|
13867
|
+
if (userSelection && userSelection.length > 0) {
|
|
13868
|
+
numAnswered++;
|
|
13869
|
+
}
|
|
13870
|
+
}
|
|
13871
|
+
// We expect all question markers to be answered before grading.
|
|
13872
|
+
if (numAnswered !== userInput.markers.length) {
|
|
13873
|
+
return {
|
|
13874
|
+
type: "invalid",
|
|
13875
|
+
message: null
|
|
13876
|
+
};
|
|
13877
|
+
}
|
|
13878
|
+
return null;
|
|
13879
|
+
}
|
|
13880
|
+
|
|
13881
|
+
function validateMockWidget(userInput) {
|
|
13882
|
+
if (userInput.currentValue == null || userInput.currentValue === "") {
|
|
13883
|
+
return {
|
|
13884
|
+
type: "invalid",
|
|
13885
|
+
message: ""
|
|
13886
|
+
};
|
|
13887
|
+
}
|
|
13888
|
+
return null;
|
|
13889
|
+
}
|
|
13890
|
+
|
|
13891
|
+
function scoreMockWidget(userInput, rubric) {
|
|
13892
|
+
const validationResult = validateMockWidget(userInput);
|
|
13893
|
+
if (validationResult != null) {
|
|
13894
|
+
return validationResult;
|
|
13895
|
+
}
|
|
13896
|
+
return {
|
|
13897
|
+
type: "points",
|
|
13898
|
+
earned: userInput.currentValue === rubric.value ? 1 : 0,
|
|
13899
|
+
total: 1,
|
|
13900
|
+
message: ""
|
|
13901
|
+
};
|
|
13902
|
+
}
|
|
13903
|
+
|
|
13904
|
+
const widgets = {};
|
|
13905
|
+
function registerWidget(type, scorer, validator) {
|
|
13906
|
+
widgets[type] = {
|
|
13907
|
+
scorer,
|
|
13908
|
+
validator
|
|
13909
|
+
};
|
|
13910
|
+
}
|
|
13911
|
+
const getWidgetValidator = name => {
|
|
13912
|
+
var _widgets$name$validat, _widgets$name;
|
|
13913
|
+
return (_widgets$name$validat = (_widgets$name = widgets[name]) == null ? void 0 : _widgets$name.validator) != null ? _widgets$name$validat : null;
|
|
13914
|
+
};
|
|
13915
|
+
const getWidgetScorer = name => {
|
|
13916
|
+
var _widgets$name$scorer, _widgets$name2;
|
|
13917
|
+
return (_widgets$name$scorer = (_widgets$name2 = widgets[name]) == null ? void 0 : _widgets$name2.scorer) != null ? _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 PerseusError("PerseusScore with unknown type encountered", 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 = 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 = getUpgradedWidgetOptions(widgets);
|
|
14052
|
+
const gradedWidgetIds = widgetIds.filter(id => {
|
|
14053
|
+
const props = upgradedWidgets[id];
|
|
14054
|
+
const widgetIsGraded = (props == null ? void 0 : props.graded) == null || props.graded;
|
|
14055
|
+
const widgetIsStatic = !!(props != null && 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
|
+
var _validator;
|
|
14062
|
+
const widget = upgradedWidgets[id];
|
|
14063
|
+
if (!widget) {
|
|
14064
|
+
return;
|
|
14065
|
+
}
|
|
14066
|
+
const userInput = userInputMap[id];
|
|
14067
|
+
const validator = getWidgetValidator(widget.type);
|
|
14068
|
+
const scorer = getWidgetScorer(widget.type);
|
|
14069
|
+
|
|
14070
|
+
// We do validation (empty checks) first and then scoring. If
|
|
14071
|
+
// validation fails, it's result is itself a PerseusScore.
|
|
14072
|
+
const score = (_validator = validator == null ? void 0 : validator(userInput, widget.options, locale)) != null ? _validator : scorer == null ? void 0 : scorer(userInput, widget.options, locale);
|
|
14073
|
+
if (score != null) {
|
|
14074
|
+
widgetScores[id] = score;
|
|
14075
|
+
}
|
|
14076
|
+
});
|
|
14077
|
+
return widgetScores;
|
|
14078
|
+
}
|
|
14079
|
+
|
|
14080
|
+
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 };
|
|
13849
14081
|
//# sourceMappingURL=index.js.map
|