-
Notifications
You must be signed in to change notification settings - Fork 350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Move main Scorer and Validator to PerseusScore #2165
base: main
Are you sure you want to change the base?
Conversation
Size Change: -765 B (-0.05%) Total Size: 1.48 MB
ℹ️ View Unchanged
|
@@ -324,7 +324,6 @@ function GradableRenderer(props: QuestionRendererProps) { | |||
const score = scorePerseusItem( | |||
question, | |||
rendererRef.current.getUserInputMap(), | |||
mockStrings, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now all validators and scorers should be sending error codes instead of error messages, so we don't need strings anymore.
name: "graded-group-set", | ||
defaultWidgetOptions, | ||
}; | ||
|
||
export default GradedGroupSetWidgetLogic; | ||
export default gradedGroupSetWidgetLogic; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This were just mistakes from previous PRs.
@@ -26,6 +26,8 @@ const defaultWidgetOptions: ImageDefaultWidgetOptions = { | |||
const imageWidgetLogic: WidgetLogic = { | |||
name: "image", | |||
defaultWidgetOptions, | |||
supportedAlignments: ["block", "full-width"], | |||
defaultAlignment: "block", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alignment is used during the upgrade process, that's why I moved all of these.
|
||
// Unversioned widgets (pre-July 2014) are all implicitly 0.0 | ||
const initialVersion = oldWidgetInfo.version || {major: 0, minor: 0}; | ||
const latestVersion = getCurrentVersion(type); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a change, basically just adding a helper here.
// some point, and we would like to not break when that happens). | ||
let newEditorOptions = _.clone(oldWidgetInfo.options) || {}; | ||
|
||
const upgradePropsMap = getWidgetOptionsUpgrades(type); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a change, basically just adding a helper here.
return widgets[name]?.scorer ?? null; | ||
}; | ||
|
||
registerWidget( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Jeremy and I are talking about merging PerseusScore and PerseusCore so there's only one registry for the logic (along with a registry for the editors and a registry for the UI). In the meantime...
@@ -569,9 +538,6 @@ export type WidgetExports< | |||
* This key defaults to `{major: 0, minor: 0}` if not provided. | |||
*/ | |||
version?: Version; | |||
supportedAlignments?: ReadonlyArray<Alignment>; | |||
defaultAlignment?: Alignment; | |||
getDefaultAlignment?: () => Alignment; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got rid of getDefaultAlignment
. It didn't seem like any of the components used it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right! There's a shared getDefaultAlignment
that's used, but nothing calls this function on individual widgets.
it("should always return 0 points", async () => { | ||
const result = ExplanationWidgetExports?.scorer?.(); | ||
|
||
expect(result).toHaveBeenAnsweredCorrectly({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
idk, this seemed to be more of a test for noopScorer
which has its own tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Doesn't actually seem like a useful test.
"countChoices": undefined, | ||
"deselectEnabled": undefined, | ||
"countChoices": false, | ||
"deselectEnabled": false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, this is worth pointing out. I have no idea why this changed and I can't figure it out. To me it seemed that these should have been false
though? So IDK.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this test end up upgrading the widget options ... and in doing so now seat some defaults that it didn't before? I'll pull the branch and look a bit more..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. That's it. The Renderer upgrades the widget options and now that you've pulled the defaultOptions from the editors, they're actually being applied during the upgrade. Previously, we didn't register the widget editors at all for these tests, so minor version upgrades never changed anything! I think your change is correct (or at least restores the original intention of this upgrade code).
registerWidget("deprecated-standin", () => scoreNoop(1) as any); | ||
registerWidget("measurer", () => scoreNoop(1) as any); | ||
|
||
registerWidget("definition", scoreNoop as any); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to get rid of scoreNoop
but I didn't feel like now was the right moment.
npm Snapshot: PublishedGood news!! We've packaged up the latest commit from this PR (852109c) and published it to npm. You Example: yarn add @khanacademy/perseus@PR2165 If you are working in Khan Academy's webapp, you can run: ./dev/tools/bump_perseus_version.sh -t PR2165 |
PerseusWidgetsMap, | ||
} from "@khanacademy/perseus-core"; | ||
|
||
describe("flattenScores", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of this is copied as-is except for the removal of strings
and the refactor I pointed out.
@@ -0,0 +1,186 @@ | |||
// TODO: combine scorePerseusItem with scoreWidgetsFunctional | |||
|
|||
import { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic is mostly copied as-is except the removal of strings
|
||
describe("scoreExpression", () => { | ||
it("should be correctly answerable if validation passes", function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed tests related to validation from scoring tests
@@ -16,118 +16,3 @@ describe("isCorrect", () => { | |||
expect(isCorrect(score)).toBe(false); | |||
}); | |||
}); | |||
|
|||
describe("flattenScores", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was moved
); | ||
} | ||
|
||
export function flattenScores(widgetScoreMap: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All moved
* Supported alignments are given as an array of strings in the exports of | ||
* a widget's module. | ||
*/ | ||
export const getSupportedAlignments = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All moved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yahoo! I made it through. Some assorted comments for you, but this looks good to me.
packages/perseus-core/src/index.ts
Outdated
export type * from "./widgets/logic-export.types"; | ||
|
||
export * as WidgetLogic from "./widgets/widget-registry"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The naming difference here feels like perhaps this should be exported as WidgetRegistry
. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I renamed the export and file to CoreWidgetRegistry
/core-widget-registry
throw new PerseusError( | ||
"No upgrade found for widget. Cannot render.", | ||
Errors.Internal, | ||
{ | ||
metadata: { | ||
type, | ||
fromMajorVersion: nextVersion - 1, | ||
toMajorVersion: nextVersion, | ||
}, | ||
}, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's reasonable to throw here. Perhaps we could include information from the widget options that might help us trace which content is causing this? Maybe just add JSON.serialize(oldWidgetInfo)
to metadata
here?
return _.extend({}, oldWidgetInfo, { | ||
// maintain other info, like type | ||
// After upgrading we guarantee that the version is up-to-date | ||
version: latestVersion, | ||
// Default graded to true (so null/undefined becomes true): | ||
graded: oldWidgetInfo.graded != null ? oldWidgetInfo.graded : true, | ||
alignment: alignment, | ||
static: widgetStatic, | ||
options: newEditorOptions, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated to the changes you're making, but this feels like a really easy improvement:
return _.extend({}, oldWidgetInfo, { | |
// maintain other info, like type | |
// After upgrading we guarantee that the version is up-to-date | |
version: latestVersion, | |
// Default graded to true (so null/undefined becomes true): | |
graded: oldWidgetInfo.graded != null ? oldWidgetInfo.graded : true, | |
alignment: alignment, | |
static: widgetStatic, | |
options: newEditorOptions, | |
}); | |
return { | |
...oldWidgetInfo, | |
// maintain other info, like type | |
// After upgrading we guarantee that the version is up-to-date | |
version: latestVersion, | |
// Default graded to true (so null/undefined becomes true): | |
graded: oldWidgetInfo.graded != null ? oldWidgetInfo.graded : true, | |
alignment: alignment, | |
static: widgetStatic, | |
options: newEditorOptions, | |
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, but I ran into a hairy type issue so I had to cast as any
// TODO: why does widget have no type? | ||
// We don't want to derive type from widget ID | ||
// see: LEMS-1845 | ||
newValues.type = widgetId.split(" ")[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be tempted to start throwing in the face of a missing widget type. Right now, we limp along and gloss over this data this is quite broken (and I suspect oooooold).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can make this change, but I wasn't sure if you were asking me to or not. I personally would like to avoid making this PR more risky than it already is, but I don't feel strongly about it either way. I agree that it seems low risk to throw here.
// select box. If the widget only supports one alignment, the | ||
// alignment value will likely just end up as "default". | ||
if (alignment == null || alignment === "default") { | ||
alignment = getSupportedAlignments(type)[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool! The array access makes me a bit paranoid. Could we add a safety mechanism in getSupportedAlignments
to throw if there are no supported alignments? Fully aware that this is extremely paranoid and you might just want to land this as is. :)
"countChoices": undefined, | ||
"deselectEnabled": undefined, | ||
"countChoices": false, | ||
"deselectEnabled": false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this test end up upgrading the widget options ... and in doing so now seat some defaults that it didn't before? I'll pull the branch and look a bit more..
"countChoices": undefined, | ||
"deselectEnabled": undefined, | ||
"countChoices": false, | ||
"deselectEnabled": false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. That's it. The Renderer upgrades the widget options and now that you've pulled the defaultOptions from the editors, they're actually being applied during the upgrade. Previously, we didn't register the widget editors at all for these tests, so minor version upgrades never changed anything! I think your change is correct (or at least restores the original intention of this upgrade code).
it("should always return 0 points", async () => { | ||
const result = ExplanationWidgetExports?.scorer?.(); | ||
|
||
expect(result).toHaveBeenAnsweredCorrectly({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Doesn't actually seem like a useful test.
@@ -569,9 +538,6 @@ export type WidgetExports< | |||
* This key defaults to `{major: 0, minor: 0}` if not provided. | |||
*/ | |||
version?: Version; | |||
supportedAlignments?: ReadonlyArray<Alignment>; | |||
defaultAlignment?: Alignment; | |||
getDefaultAlignment?: () => Alignment; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right! There's a shared getDefaultAlignment
that's used, but nothing calls this function on individual widgets.
value: string; | ||
}; | ||
|
||
export type PerseusMockWidgetUserInput = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm tempted to pull it into its own package (it could be a private package that's used anywhere in our Perseus repo, but it'd be structured as a real package to help guide us). I don't think this is a rush but might be something to try to see how it affects other organization (and move us to better support external widgets).
Summary:
You don't have to look at this alone. I would be happy to give you a tour.
I'm sorry, I didn't see a way to break this up into pieces that could be moved independent of one another.
It's honestly not a lot of changes, just changes that needed to happen while moving things.
The main idea:
scorePerseusItem
That's pretty much it: moved scoring logic, moved stuff scoring logic depended on, updated imports, and cleaned up WidgetExports.
Issue: LEMS-2737
Test plan:
Virtually no user-facing changes should occur.