Skip to content

Commit

Permalink
This is a feature-branch pull-request from `release/server-side-scori…
Browse files Browse the repository at this point in the history
…ng` to `main` (#1926)

## Summary:
This PR includes the following commits:
- Numeric-Input: Extract validation from scorer (#1882)
- Orderer: Extract validation out of scoring (#1869)
- Sorter: Extract validation out of scoring (#1876)
- Dropdown: Extract validation out of scoring (#1898)
- Plotter: Extract validation out of scoring (#1899)
- Radio: Extract validation out of scoring (#1902)
- Categorizer: Extract validation out of scoring (#1862)

Issue: <none>

## Test plan:

Author: jeremywiebe

Reviewers: lillialexis, #perseus

Required Reviewers:

Approved By: lillialexis

Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald, ✅ Publish npm snapshot (ubuntu-latest, 20.x), 🚫 Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), 🚫 Check builds for changes in size (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald, ✅ .github/dependabot.yml

Pull Request URL: #1926
  • Loading branch information
jeremywiebe authored Nov 27, 2024
2 parents 84d8a9f + 53441fe commit 8042ac6
Show file tree
Hide file tree
Showing 36 changed files with 606 additions and 205 deletions.
5 changes: 5 additions & 0 deletions .changeset/famous-horses-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": minor
---

Introduces a validation function for the dropdown widget (extracted from dropdown scoring function).
5 changes: 5 additions & 0 deletions .changeset/green-ghosts-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": minor
---

Split out validation function for the `categorizer` widget. This can be used to check if the user selected an answer for every row, confirming the question is ready to be scored.
5 changes: 5 additions & 0 deletions .changeset/honest-avocados-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": minor
---

Introduces a validation function for the numeric-input widget (extracted from numeric-input scoring function).
5 changes: 5 additions & 0 deletions .changeset/nice-fans-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": minor
---

Introduces a validation function for the plotter widget (extracted from the scoring function).
5 changes: 5 additions & 0 deletions .changeset/sharp-radios-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": minor
---

Split out validation function for the `orderer` widget. This can be used to check if the user has ordered at least one option, confirming the question is ready to be scored.
5 changes: 5 additions & 0 deletions .changeset/spotty-moles-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": minor
---

Introduces a validation function for the radio widget (extracted from the scoring function), though not all validation logic can be extracted.
5 changes: 5 additions & 0 deletions .changeset/sweet-trainers-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": minor
---

Split out validation function for the `sorter` widget. This can be used to check if the user has made any changes to the sorting order, confirming whether or not the question can be scored.
27 changes: 20 additions & 7 deletions packages/perseus/src/validation.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import type {
PerseusNumberLineWidgetOptions,
PerseusNumericInputAnswer,
PerseusOrdererWidgetOptions,
PerseusPlotterWidgetOptions,
PerseusRadioChoice,
PerseusGraphCorrectType,
} from "./perseus-types";
Expand All @@ -50,15 +49,21 @@ import type {Relationship} from "./widgets/number-line/number-line";

export type UserInputStatus = "correct" | "incorrect" | "incomplete";

export type PerseusCategorizerRubric = {
export type PerseusCategorizerScoringData = {
// The correct answers where index relates to the items and value relates
// to the category. e.g. [0, 1, 0, 1, 2]
values: ReadonlyArray<number>;
};
} & PerseusCategorizerValidationData;

export type PerseusCategorizerUserInput = {
values: PerseusCategorizerRubric["values"];
values: PerseusCategorizerScoringData["values"];
};

export type PerseusCategorizerValidationData = {
// Translatable text; a list of items to categorize. e.g. ["banana", "yellow", "apple", "purple", "shirt"]
items: ReadonlyArray<string>;
};

// TODO(LEMS-2440): Can possibly be removed during 2440?
// This is not used for grading at all. The only place it is used is to define
// Props type in cs-program.tsx, but RenderProps already contains WidgetOptions
Expand Down Expand Up @@ -188,7 +193,15 @@ export type PerseusOrdererUserInput = {
current: ReadonlyArray<string>;
};

export type PerseusPlotterRubric = PerseusPlotterWidgetOptions;
export type PerseusPlotterScoringData = {
// The Y values that represent the correct answer expected
correct: ReadonlyArray<number>;
} & PerseusPlotterValidationData;

export type PerseusPlotterValidationData = {
// The Y values the graph should start with
starting: ReadonlyArray<number>;
};

export type PerseusPlotterUserInput = ReadonlyArray<number>;

Expand Down Expand Up @@ -219,7 +232,7 @@ export type PerseusTableRubric = {
export type PerseusTableUserInput = ReadonlyArray<ReadonlyArray<string>>;

export type Rubric =
| PerseusCategorizerRubric
| PerseusCategorizerScoringData
| PerseusCSProgramRubric
| PerseusDropdownRubric
| PerseusExpressionRubric
Expand All @@ -236,7 +249,7 @@ export type Rubric =
| PerseusNumberLineRubric
| PerseusNumericInputRubric
| PerseusOrdererRubric
| PerseusPlotterRubric
| PerseusPlotterScoringData
| PerseusRadioRubric
| PerseusSorterRubric
| PerseusTableRubric;
Expand Down
4 changes: 2 additions & 2 deletions packages/perseus/src/widgets/categorizer/categorizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ import scoreCategorizer from "./score-categorizer";
import type {PerseusCategorizerWidgetOptions} from "../../perseus-types";
import type {Widget, WidgetExports, WidgetProps} from "../../types";
import type {
PerseusCategorizerRubric,
PerseusCategorizerScoringData,
PerseusCategorizerUserInput,
} from "../../validation.types";
import type {CategorizerPromptJSON} from "../../widget-ai-utils/categorizer/categorizer-ai-utils";

type Props = WidgetProps<RenderProps, PerseusCategorizerRubric> & {
type Props = WidgetProps<RenderProps, PerseusCategorizerScoringData> & {
values: ReadonlyArray<string>;
};

Expand Down
27 changes: 7 additions & 20 deletions packages/perseus/src/widgets/categorizer/score-categorizer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,34 @@ import {mockStrings} from "../../strings";

import scoreCategorizer from "./score-categorizer";

import type {PerseusCategorizerRubric} from "../../validation.types";
import type {PerseusCategorizerScoringData} from "../../validation.types";

describe("scoreCategorizer", () => {
it("gives points when the answer is correct", () => {
const rubric: PerseusCategorizerRubric = {
const scoringData: PerseusCategorizerScoringData = {
values: [1, 3],
items: ["apples", "oranges"],
};

const userInput = {
values: [1, 3],
} as const;
const score = scoreCategorizer(userInput, rubric, mockStrings);
const score = scoreCategorizer(userInput, scoringData, mockStrings);

expect(score).toHaveBeenAnsweredCorrectly();
});

it("does not give points when incorrectly answered", () => {
const rubric: PerseusCategorizerRubric = {
const scoringData: PerseusCategorizerScoringData = {
values: [1, 3],
items: ["apples", "oranges"],
};

const userInput = {
values: [2, 3],
} as const;
const score = scoreCategorizer(userInput, rubric, mockStrings);
const score = scoreCategorizer(userInput, scoringData, mockStrings);

expect(score).toHaveBeenAnsweredIncorrectly();
});

it("tells the learner its not complete if not selected", () => {
const rubric: PerseusCategorizerRubric = {
values: [1, 3],
};

const userInput = {
values: [2],
} as const;
const score = scoreCategorizer(userInput, rubric, mockStrings);

expect(score).toHaveInvalidInput(
"Make sure you select something for every row.",
);
});
});
27 changes: 14 additions & 13 deletions packages/perseus/src/widgets/categorizer/score-categorizer.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
import validateCategorizer from "./validate-categorizer";

import type {PerseusStrings} from "../../strings";
import type {PerseusScore} from "../../types";
import type {
PerseusCategorizerRubric,
PerseusCategorizerScoringData,
PerseusCategorizerUserInput,
} from "../../validation.types";

function scoreCategorizer(
userInput: PerseusCategorizerUserInput,
rubric: PerseusCategorizerRubric,
scoringData: PerseusCategorizerScoringData,
strings: PerseusStrings,
): PerseusScore {
let completed = true;
const validationError = validateCategorizer(
userInput,
scoringData,
strings,
);
if (validationError) {
return validationError;
}

let allCorrect = true;
rubric.values.forEach((value, i) => {
if (userInput.values[i] == null) {
completed = false;
}
scoringData.values.forEach((value, i) => {
if (userInput.values[i] !== value) {
allCorrect = false;
}
});
if (!completed) {
return {
type: "invalid",
message: strings.invalidSelection,
};
}
return {
type: "points",
earned: allCorrect ? 1 : 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {mockStrings} from "../../strings";

import validateCategorizer from "./validate-categorizer";

import type {PerseusCategorizerValidationData} from "../../validation.types";

describe("validateCategorizer", () => {
it("tells the learner its not complete if not selected", () => {
const validationData: PerseusCategorizerValidationData = {
items: ["apples", "oranges"],
};

const userInput = {
values: [2],
} as const;
const score = validateCategorizer(
userInput,
validationData,
mockStrings,
);

expect(score).toHaveInvalidInput(
"Make sure you select something for every row.",
);
});

it("returns null if the userInput is valid", () => {
const validationData: PerseusCategorizerValidationData = {
items: ["apples", "oranges"],
};

const userInput = {
values: [2, 4],
} as const;
const score = validateCategorizer(
userInput,
validationData,
mockStrings,
);

expect(score).toBeNull();
});
});
34 changes: 34 additions & 0 deletions packages/perseus/src/widgets/categorizer/validate-categorizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type {PerseusStrings} from "../../strings";
import type {PerseusScore} from "../../types";
import type {
PerseusCategorizerUserInput,
PerseusCategorizerValidationData,
} from "../../validation.types";

/**
* Checks userInput from the categorizer widget to see if the user has selected
* a category for each item.
* @param userInput - The user's input corresponding to an array of indices that
* represent the selected category for each row/item.
* @param validationData - An array of strings corresponding to each row/item
* @param strings - Used to provide a validation message
*/
function validateCategorizer(
userInput: PerseusCategorizerUserInput,
validationData: PerseusCategorizerValidationData,
strings: PerseusStrings,
): Extract<PerseusScore, {type: "invalid"}> | null {
const incomplete = validationData.items.some(
(_, i) => userInput.values[i] == null,
);

if (incomplete) {
return {
type: "invalid",
message: strings.invalidSelection,
};
}
return null;
}

export default validateCategorizer;
24 changes: 4 additions & 20 deletions packages/perseus/src/widgets/dropdown/score-dropdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,6 @@ import type {
} from "../../validation.types";

describe("scoreDropdown", () => {
it("returns invalid for user input of 0", () => {
// Arrange
const userInput: PerseusDropdownUserInput = {
value: 0,
};
const rubric: PerseusDropdownRubric = {
choices: question1.widgets["dropdown 1"].options.choices,
};

// Act
const result = scoreDropdown(userInput, rubric);

// Assert
expect(result).toHaveInvalidInput();
});

it("returns 0 points for incorrect answer", () => {
// Arrange
const userInput: PerseusDropdownUserInput = {
Expand All @@ -33,10 +17,10 @@ describe("scoreDropdown", () => {
};

// Act
const result = scoreDropdown(userInput, rubric);
const score = scoreDropdown(userInput, rubric);

// Assert
expect(result).toHaveBeenAnsweredIncorrectly();
expect(score).toHaveBeenAnsweredIncorrectly();
});

it("returns 1 point for correct answer", () => {
Expand All @@ -49,9 +33,9 @@ describe("scoreDropdown", () => {
};

// Act
const result = scoreDropdown(userInput, rubric);
const score = scoreDropdown(userInput, rubric);

// Assert
expect(result).toHaveBeenAnsweredCorrectly();
expect(score).toHaveBeenAnsweredCorrectly();
});
});
13 changes: 6 additions & 7 deletions packages/perseus/src/widgets/dropdown/score-dropdown.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import validateDropdown from "./validate-dropdown";

import type {PerseusScore} from "../../types";
import type {
PerseusDropdownRubric,
Expand All @@ -8,14 +10,11 @@ function scoreDropdown(
userInput: PerseusDropdownUserInput,
rubric: PerseusDropdownRubric,
): PerseusScore {
const selected = userInput.value;
if (selected === 0) {
return {
type: "invalid",
message: null,
};
const validationError = validateDropdown(userInput);
if (validationError) {
return validationError;
}
const correct = rubric.choices[selected - 1].correct;
const correct = rubric.choices[userInput.value - 1].correct;
return {
type: "points",
earned: correct ? 1 : 0,
Expand Down
Loading

0 comments on commit 8042ac6

Please sign in to comment.