-
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
Refactor LabelImage to separate out answers from userInput into scoringData #1965
base: main
Are you sure you want to change the base?
Refactor LabelImage to separate out answers from userInput into scoringData #1965
Conversation
PerseusLabelImageMarker was a duplicate type of InteractiveMarkerType. Copy over some of the comments that are still useful.
npm Snapshot: PublishedGood news!! We've packaged up the latest commit from this PR (415f32a) and published it to npm. You Example: yarn add @khanacademy/perseus@PR1965 If you are working in Khan Academy's webapp, you can run: ./dev/tools/bump_perseus_version.sh -t PR1965 |
Size Change: +77 B (+0.01%) Total Size: 1.27 MB
ℹ️ View Unchanged
|
export type PerseusLabelImageScoringData = { | ||
markers: ReadonlyArray<MarkerAnswers>; | ||
}; | ||
|
||
export type PerseusLabelImageUserInput = { | ||
markers: ReadonlyArray<InteractiveMarkerType>; | ||
markers: ReadonlyArray<Omit<InteractiveMarkerType, "answers">>; |
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.
These are the main changes. Changed PerseusLabelImageUserInput
so it no longer includes answers
in the array. Then I moved the parts of InteractiveMarkerType
needed for scoring to PerseusLabelImageScoringData
.
// TODO: there's some weirdness in our types between | ||
// PerseusLabelImageMarker and InteractiveMarkerType |
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 able to resolve this by removing PerseusLabelImageMarker
const combinedData = userInput.markers.map((marker, index) => { | ||
return { | ||
...marker, | ||
answers: scoringData.markers[index].answers, | ||
}; | ||
}); | ||
const allMarkerData = {markers: combinedData}; | ||
|
||
for (const marker of userInput.markers) { | ||
for (const marker of allMarkerData.markers) { |
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.
Data was combined here so that marker
can have all the available information as scoreMarker
requires an object of InteractiveMarkerType
, which includes answers.
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.
No action needed, but my gut feeling is that this is needless misdirection. I kind of feel we should just change scoreMarker
:
function scoreMarker(
userInput: APieceOfUserInputType,
scoringData: APieceOfScoringDataType,
): InteractiveMarkerScore;
I'm fine punting on this though.
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.
Fixed!
// TODO(LEMS-2544): Investigate impact on scoring; possibly pull out &/or remove rubric parameter. | ||
showRationalesForCurrentlySelectedChoices(rubric: PerseusLabelImageRubric) { | ||
// TODO(LEMS-2544): Investigate impact on scoring; possibly pull out &/or remove scoringData parameter. | ||
showRationalesForCurrentlySelectedChoices( |
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 bit of a problem as it requires scoringData on the client-side it seems like. Since this has a TODO associated with it, I left it for now.
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 known issue: https://khanacademy.atlassian.net/browse/LEMS-2544
Luckily AX won't use rationales so we can punt on this for now.
GeraldRequired Reviewers
Don't want to be involved in this pull request? Comment |
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 this is the right direction. I'm wondering though:
- Will LabelImage render if we provide it with PerseusData sans-
answers
? - Will
getUserInput
work sans-answers
?
Knowing how good you are about manual testing, I'm guessing the answer is "yes." I'd just like to see some automated tests to confirm.
// TODO(LEMS-2544): Investigate impact on scoring; possibly pull out &/or remove rubric parameter. | ||
showRationalesForCurrentlySelectedChoices(rubric: PerseusLabelImageRubric) { | ||
// TODO(LEMS-2544): Investigate impact on scoring; possibly pull out &/or remove scoringData parameter. | ||
showRationalesForCurrentlySelectedChoices( |
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 known issue: https://khanacademy.atlassian.net/browse/LEMS-2544
Luckily AX won't use rationales so we can punt on this for now.
|
||
export type PerseusLabelImageUserInput = { | ||
markers: ReadonlyArray<InteractiveMarkerType>; | ||
markers: ReadonlyArray<Omit<InteractiveMarkerType, "answers">>; |
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.
It's a little higher-risk, but I think we should make this test case pass:
// label-image.test.ts
describe("getUserInput", () => {
it("doesn't include answer in getUserInput", async () => {
// render component
const {renderer} = renderQuestion(textQuestion);
const userInput = renderer.getUserInputMap();
expect(userInput).toEqual({
"label-image 1": {
markers: [
{
label: "The fourth unlabeled bar line.",
x: 25,
y: 17.7,
},
{
label: "The third unlabeled bar line.",
x: 25,
y: 35.3,
},
{
label: "The second unlabeled bar line.",
x: 25,
y: 53,
},
{
label: "The first unlabeled bar line.",
x: 25,
y: 70.3,
},
],
},
});
});
});
I think there should be another set of tests too:
- Render a LabelImage widget using ItemData that has no answers (to simulate how it will be on the FE)
- Answer the LabelImage (both wrong and right)
- Use
getUserInput
on the Renderer to get the user input - Use
scorePerseusItem
to score the user input
More than anything, I'm just surprised that this ticket ended up being more of a types change whereas I was thinking it would end up being more of a logic change. Not saying you're wrong, it just makes me worried we're missing something.
.changeset/many-penguins-hug.md
Outdated
@@ -0,0 +1,6 @@ | |||
--- | |||
"@khanacademy/perseus": minor |
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.
Since you changed PerseusTypes in a way that's not backwards compatible, I'd call this a major/breaking change.
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.
Updated to major. Thanks for pointing this out!
const combinedData = userInput.markers.map((marker, index) => { | ||
return { | ||
...marker, | ||
answers: scoringData.markers[index].answers, | ||
}; | ||
}); | ||
const allMarkerData = {markers: combinedData}; | ||
|
||
for (const marker of userInput.markers) { | ||
for (const marker of allMarkerData.markers) { |
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.
No action needed, but my gut feeling is that this is needless misdirection. I kind of feel we should just change scoreMarker
:
function scoreMarker(
userInput: APieceOfUserInputType,
scoringData: APieceOfScoringDataType,
): InteractiveMarkerScore;
I'm fine punting on this though.
@@ -1059,17 +1060,6 @@ export type PerseusLabelImageWidgetOptions = { | |||
static: boolean; | |||
}; | |||
|
|||
export type PerseusLabelImageMarker = { |
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.
@jeremywiebe is particular when it comes to changing perseus-types.ts
, I would have him sign off first.
@@ -1,19 +1,21 @@ | |||
// Base marker, with the props that are set by the editor. | |||
export type MarkerType = { | |||
// The list of correct answers expected for the marker. | |||
export type MarkerAnswers = { |
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.
These types feel off to me. I'm wondering if it's actually something like:
// stuff to render
type LabelImageMarker = {
// Reveal the correctness state of the user selected answers for the marker.
showCorrectness: "correct" | "incorrect";
focused: boolean;
// Translatable Text; The text to show for the marker. Not displayed directly to the user
label: string;
// The marker coordinates on the question image as percent of image size.
x: number;
y: number;
}
type LabelImageMarkerScoringData = {
// The list of correct answers expected for the marker. Often only one but can have multiple
answers: ReadonlyArray<string>;
}
type LabelImageMarkerUserInput = {
// The user selected list of answers, used to grade the question.
selected: ReadonlyArray<string>;
}
type LabelImageFullMarker = LabelImageMarker & LabelImageMarkerScoringData & LabelImageMarkerUserInput;
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.
Updated! Still need to add recommended tests and do additional manual testing though.
Based on PR feedback
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.
Overall I like the direction of this PR. One change I'd like to see is that we keep the type defined in perseus-types.ts
and then define the ScoringData for it in validation.types.ts
.
@@ -1059,17 +1060,6 @@ export type PerseusLabelImageWidgetOptions = { | |||
static: boolean; | |||
}; | |||
|
|||
export type PerseusLabelImageMarker = { |
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 assuming you got rid of this type because it was a duplicate of what's in the widget? Could you instead, make this type the source of truth? perseus-types.ts
is the source of truth for the data that we serialize out of Perseus and I've been working to keep all non-trivial type definitions for widget options here instead of in the widget folders.
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.
Further, because TypeScript types match if they are the same shape, I was thinking about ScoringData and ValidationData for each widget as a matching subset of the widget options in this file.
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.
Do you mean use the types in this file (perseus-types.ts) to define the ScoringData and ValidationData types (in validation.types.ts)? Also, with Matthew's suggestion, I ended up modifying the types in the label-image/types.ts file. I'll review what I did and what was here to see if I can make them work together still. Or maybe I can just leave this file alone 😂
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 code looks good to me! Though I would encourage you to get an approval from Matthew/Jeremy as they have more context into the scoring code 😉
@@ -28,7 +31,7 @@ type Props = { | |||
imageWidth: number; | |||
imageHeight: number; | |||
// The list of label markers on the question image. | |||
markers: ReadonlyArray<MarkerType>; | |||
markers: ReadonlyArray<LabelImageMarker & LabelImageMarkerScoringData>; |
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.
Nit: I notice your creating this union in a lot of files. Can you combine them in the types object so you don't have to keep joining them in every file?
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.
Great suggestion! I'll set that up. :)
We are moving scoring data and user input to validation data only
We build the wanted object instead of remove the unneeded property. The removal method was used previously to change as little as possible from the incoming markers object.
Don't want to use the ScoringData type here. Would be widget options type if the parameter returns
…ithout-rubric # Conflicts: # packages/perseus/src/index.ts
Add tests for scoring (invalid, correct, and incorrect), test for rendering without answers, and a test to confirm getUserInput works
Just confirms text from the question has been rendered on the page
@@ -709,4 +714,110 @@ describe("LabelImage", function () { | |||
}); | |||
}); | |||
}); | |||
|
|||
describe("getUserInput", () => { | |||
it("doesn't include answer in getUserInput from getUserInputMap", () => { |
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.
🤔 Can we make this test slightly more generic? I think we're less concerned about answer
specifically and more concerned about getUserInputMap()
returning the right data overall. So if I were to take a stab at naming this test, I would do something like:
it("doesn't include answer in getUserInput from getUserInputMap", () => { | |
it("should return the current user input on initial render", () => { |
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 the focus was supposed to be making sure answer was not included, but it's also correct to confirm it's just returning the correct userInput, especially since I didn't write any expect statements specifically checking that answers are absent. I do think it's important to emphasize that the goal is to have answers out of user input, but I guess that will be obvious from the shape of UserInput. Updated :)
}); | ||
|
||
describe("textWithoutAnswersQuestion", () => { | ||
it("should render a text question without answers", () => { |
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.
Nice! Great idea for a test as we get ready to start splitting things!!
return { | ||
markers: this.props.markers.map((marker) => ({ | ||
selected: marker.selected, | ||
label: marker.label, | ||
})), | ||
}; |
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.
@handeyeco for visibility, Tamara and I re-wrote this function to build a return value by explicitly picking the pieces we need instead of simply return a value out of props. This help stop guarantee we're really only using values in the type and not depending elsewhere on keys that may get stripped when we do the Scoring Data stripping stuff.
x: 0, | ||
y: 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 think we dropped x
and y
from this type, didn't we?
x: 0, | ||
y: 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 think we dropped x
and y
from this type, didn't we?
markers: [ | ||
{ | ||
...emptyMarker, | ||
...emptyUserInput, |
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 once we update this object to only have what's in the user input type, we might not need it. It might be more clear to inline the single selected: []
bit here instead of spreading this object at that point. I'll leave it to you to decide which way you want to go. :)
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.
Updated the tests to remove these empty types.
Summary:
This PR updates LabelImage so that answers are no longer available in the userInput object. This allows the scoring function to have both a userInput parameter and a scoringData parameter to keep answers separate from the user's input with the goal of supporting server side scoring.
PerseusLabelImageMarker and MarkerType contained the same properties. As such, I simplified the code a bit and removed PerseusLabelImageMarker.
In addition, several locations were referencing the wrong types, so those were updated to reference the correct ones. Also, new tests were added confirming the output of
getUserInput
does not contain answers, thatscorePerseusItem
returns the correct results, and that the widget renders correctly if answers are not present in the JSON blob.Issue: LEMS-2440
Test plan: