diff --git a/cypress/e2e/DoenetML/tagSpecific/answer.cy.js b/cypress/e2e/DoenetML/tagSpecific/answer.cy.js
index 8b9d851470..1256579885 100644
--- a/cypress/e2e/DoenetML/tagSpecific/answer.cy.js
+++ b/cypress/e2e/DoenetML/tagSpecific/answer.cy.js
@@ -22177,4 +22177,282 @@ describe('Answer Tag Tests', function () {
});
+ it('hand-graded answers', () => {
+ cy.window().then(async (win) => {
+ win.postMessage({
+ doenetML: `
+ a
+
Maths:
+
+
+
+ x
+
+
+
+
+ Submitted responses:
+
+
+
+
+
+
+
+
+ Credit achieved:
+
+
+
+
+
+
+
+
+ Texts:
+
+
+ hello
+ hello
+
+ Submitted responses:
+
+
+
+
+
+ Credit achieved:
+
+
+
+
+
+ Multiple inputs
+
+
+
+
+ Submitted responses:
+
+
+
+
+
+
+
+ Credit achieved:
+
+
+
+
+ Inputs outside answer
+ $oi1i1$oi1i2
+ $oi2i1$oi2i2
+ $oi3i1$oi3i2
+
+ Submitted responses:
+
+
+
+
+
+
+
+ Credit achieved:
+
+
+
+
+
+
+ `}, "*");
+ });
+
+
+ cy.get("#\\/_text1").should("have.text", "a");
+
+ cy.get('#\\/m1 textarea').type("x{enter}", { force: true })
+ cy.get('#\\/m2 textarea').type("x{enter}", { force: true })
+ cy.get('#\\/m3 textarea').type("x{enter}", { force: true })
+ cy.get('#\\/m4 textarea').type("x{enter}", { force: true })
+ cy.get('#\\/m5 textarea').type("x{enter}", { force: true })
+ cy.get('#\\/m6 textarea').type("x{enter}", { force: true })
+ cy.get('#\\/m7 textarea').type("x{enter}", { force: true })
+
+ cy.get('#\\/t1 input').type("hello{enter}")
+ cy.get('#\\/t2 input').type("hello{enter}")
+ cy.get('#\\/t3 input').type("hello{enter}")
+ cy.get('#\\/t4 input').type("hello{enter}")
+
+ cy.get('#\\/mi1 textarea').eq(0).type("x{enter}", { force: true })
+ cy.get('#\\/mi1 textarea').eq(1).type("y{enter}", { force: true })
+ cy.get('#\\/mi1_submit').click();
+ cy.get('#\\/mi2 input').eq(0).type("hello{enter}")
+ cy.get('#\\/mi2 input').eq(1).type("bye{enter}")
+ cy.get('#\\/mi2_submit').click();
+ cy.get('#\\/mi3 textarea').type("x{enter}", { force: true })
+ cy.get('#\\/mi3 input').type("bye{enter}")
+ cy.get('#\\/mi3_submit').click();
+
+
+ cy.get('#\\/oi1i1 textarea').type("x{enter}", { force: true })
+ cy.get('#\\/oi1i2 textarea').type("y{enter}", { force: true })
+ cy.get('#\\/oi1_submit').click();
+ cy.get('#\\/oi2i1 input').type("hello{enter}")
+ cy.get('#\\/oi2i2 input').type("bye{enter}")
+ cy.get('#\\/oi2_submit').click();
+ cy.get('#\\/oi3i1 textarea').type("x{enter}", { force: true })
+ cy.get('#\\/oi3i2 input').type("bye{enter}")
+ cy.get('#\\/oi3_submit').click();
+
+
+ cy.get('#\\/oi3sr2').should('have.text', 'bye')
+
+ cy.get('#\\/m1sr .mjx-mrow').eq(0).should('have.text', "x")
+ cy.get('#\\/m2sr .mjx-mrow').eq(0).should('have.text', "x")
+ cy.get('#\\/m3sr .mjx-mrow').eq(0).should('have.text', "x")
+ cy.get('#\\/m4sr .mjx-mrow').eq(0).should('have.text', "x")
+ cy.get('#\\/m5sr .mjx-mrow').eq(0).should('have.text', "x")
+ cy.get('#\\/m6sr .mjx-mrow').eq(0).should('have.text', "x")
+ cy.get('#\\/m7sr .mjx-mrow').eq(0).should('have.text', "x")
+
+ cy.get('#\\/t1sr').should('have.text', "hello")
+ cy.get('#\\/t2sr').should('have.text', "hello")
+ cy.get('#\\/t3sr').should('have.text', "hello")
+ cy.get('#\\/t4sr').should('have.text', "hello")
+
+ cy.get('#\\/mi1sr1 .mjx-mrow').eq(0).should('have.text', "x")
+ cy.get('#\\/mi1sr2 .mjx-mrow').eq(0).should('have.text', "y")
+ cy.get('#\\/mi2sr1').should('have.text', "hello")
+ cy.get('#\\/mi2sr2').should('have.text', "bye")
+ cy.get('#\\/mi3sr1 .mjx-mrow').eq(0).should('have.text', "x")
+ cy.get('#\\/mi3sr2').should('have.text', "bye")
+
+ cy.get('#\\/oi1sr1 .mjx-mrow').eq(0).should('have.text', "x")
+ cy.get('#\\/oi1sr2 .mjx-mrow').eq(0).should('have.text', "y")
+ cy.get('#\\/oi2sr1').should('have.text', "hello")
+ cy.get('#\\/oi2sr2').should('have.text', "bye")
+ cy.get('#\\/oi3sr1 .mjx-mrow').eq(0).should('have.text', "x")
+ cy.get('#\\/oi3sr2').should('have.text', "bye")
+
+
+ cy.get('#\\/m1ca').should('have.text', '0')
+ cy.get('#\\/m2ca').should('have.text', '0')
+ cy.get('#\\/m3ca').should('have.text', '0')
+ cy.get('#\\/m4ca').should('have.text', '0')
+ cy.get('#\\/m5ca').should('have.text', '0')
+ cy.get('#\\/m6ca').should('have.text', '0')
+ cy.get('#\\/m7ca').should('have.text', '0')
+
+ cy.get('#\\/t1ca').should('have.text', '0')
+ cy.get('#\\/t2ca').should('have.text', '0')
+ cy.get('#\\/t3ca').should('have.text', '0')
+ cy.get('#\\/t4ca').should('have.text', '0')
+
+ cy.get('#\\/mi1ca').should('have.text', '0')
+ cy.get('#\\/mi2ca').should('have.text', '0')
+ cy.get('#\\/mi3ca').should('have.text', '0')
+
+ cy.get('#\\/oi1ca').should('have.text', '0')
+ cy.get('#\\/oi2ca').should('have.text', '0')
+ cy.get('#\\/oi3ca').should('have.text', '0')
+
+
+ cy.log("revise answers and submit")
+
+ cy.get('#\\/m1 textarea').type("{end}y{enter}", { force: true })
+ cy.get('#\\/m2 textarea').type("{end}y{enter}", { force: true })
+ cy.get('#\\/m3 textarea').type("{end}y{enter}", { force: true })
+ cy.get('#\\/m4 textarea').type("{end}y{enter}", { force: true })
+ cy.get('#\\/m5 textarea').type("{end}y{enter}", { force: true })
+ cy.get('#\\/m6 textarea').type("{end}y{enter}", { force: true })
+ cy.get('#\\/m7 textarea').type("{end}y{enter}", { force: true })
+
+ cy.get('#\\/t1 input').type(" there{enter}")
+ cy.get('#\\/t2 input').type(" there{enter}")
+ cy.get('#\\/t3 input').type(" there{enter}")
+ cy.get('#\\/t4 input').type(" there{enter}")
+
+
+ cy.get('#\\/mi1 textarea').eq(0).type("{end}z{enter}", { force: true })
+ cy.get('#\\/mi1_submit').click();
+ cy.get('#\\/mi1 textarea').eq(1).type("{end}z{enter}", { force: true })
+ cy.get('#\\/mi1_submit').click();
+ cy.get('#\\/mi2 input').eq(0).type(" there{enter}")
+ cy.get('#\\/mi2_submit').click();
+ cy.get('#\\/mi2 input').eq(1).type(" now{enter}")
+ cy.get('#\\/mi2_submit').click();
+ cy.get('#\\/mi3 textarea').type("{end}y{enter}", { force: true })
+ cy.get('#\\/mi3_submit').click();
+ cy.get('#\\/mi3 input').type(" now{enter}")
+ cy.get('#\\/mi3_submit').click();
+
+
+
+ cy.get('#\\/oi1i1 textarea').type("{end}z{enter}", { force: true })
+ cy.get('#\\/oi1_submit').click();
+ cy.get('#\\/oi1i2 textarea').type("{end}z{enter}", { force: true })
+ cy.get('#\\/oi1_submit').click();
+ cy.get('#\\/oi2i1 input').type(" there{enter}")
+ cy.get('#\\/oi2_submit').click();
+ cy.get('#\\/oi2i2 input').type(" now{enter}")
+ cy.get('#\\/oi2_submit').click();
+ cy.get('#\\/oi3i1 textarea').type("{end}y{enter}", { force: true })
+ cy.get('#\\/oi3_submit').click();
+ cy.get('#\\/oi3i2 input').type(" now{enter}")
+ cy.get('#\\/oi3_submit').click();
+
+
+ cy.get('#\\/oi3sr2').should('have.text', 'bye now')
+
+ cy.get('#\\/m1sr .mjx-mrow').eq(0).should('have.text', "xy")
+ cy.get('#\\/m2sr .mjx-mrow').eq(0).should('have.text', "xy")
+ cy.get('#\\/m3sr .mjx-mrow').eq(0).should('have.text', "xy")
+ cy.get('#\\/m4sr .mjx-mrow').eq(0).should('have.text', "xy")
+ cy.get('#\\/m5sr .mjx-mrow').eq(0).should('have.text', "xy")
+ cy.get('#\\/m6sr .mjx-mrow').eq(0).should('have.text', "xy")
+ cy.get('#\\/m7sr .mjx-mrow').eq(0).should('have.text', "xy")
+
+ cy.get('#\\/t1sr').should('have.text', "hello there")
+ cy.get('#\\/t2sr').should('have.text', "hello there")
+ cy.get('#\\/t3sr').should('have.text', "hello there")
+ cy.get('#\\/t4sr').should('have.text', "hello there")
+
+ cy.get('#\\/mi1sr1 .mjx-mrow').eq(0).should('have.text', "xz")
+ cy.get('#\\/mi1sr2 .mjx-mrow').eq(0).should('have.text', "yz")
+ cy.get('#\\/mi2sr1').should('have.text', "hello there")
+ cy.get('#\\/mi2sr2').should('have.text', "bye now")
+ cy.get('#\\/mi3sr1 .mjx-mrow').eq(0).should('have.text', "xy")
+ cy.get('#\\/mi3sr2').should('have.text', "bye now")
+
+ cy.get('#\\/oi1sr1 .mjx-mrow').eq(0).should('have.text', "xz")
+ cy.get('#\\/oi1sr2 .mjx-mrow').eq(0).should('have.text', "yz")
+ cy.get('#\\/oi2sr1').should('have.text', "hello there")
+ cy.get('#\\/oi2sr2').should('have.text', "bye now")
+ cy.get('#\\/oi3sr1 .mjx-mrow').eq(0).should('have.text', "xy")
+ cy.get('#\\/oi3sr2').should('have.text', "bye now")
+
+
+ cy.get('#\\/m1ca').should('have.text', '0')
+ cy.get('#\\/m2ca').should('have.text', '0')
+ cy.get('#\\/m3ca').should('have.text', '0')
+ cy.get('#\\/m4ca').should('have.text', '0')
+ cy.get('#\\/m5ca').should('have.text', '0')
+ cy.get('#\\/m6ca').should('have.text', '0')
+ cy.get('#\\/m7ca').should('have.text', '0')
+
+ cy.get('#\\/t1ca').should('have.text', '0')
+ cy.get('#\\/t2ca').should('have.text', '0')
+ cy.get('#\\/t3ca').should('have.text', '0')
+ cy.get('#\\/t4ca').should('have.text', '0')
+
+ cy.get('#\\/mi1ca').should('have.text', '0')
+ cy.get('#\\/mi2ca').should('have.text', '0')
+ cy.get('#\\/mi3ca').should('have.text', '0')
+
+ cy.get('#\\/oi1ca').should('have.text', '0')
+ cy.get('#\\/oi2ca').should('have.text', '0')
+ cy.get('#\\/oi3ca').should('have.text', '0')
+ });
+
})
diff --git a/src/Core/components/Answer.js b/src/Core/components/Answer.js
index d3e9165179..434c4e27a3 100644
--- a/src/Core/components/Answer.js
+++ b/src/Core/components/Answer.js
@@ -36,6 +36,12 @@ export default class Answer extends InlineComponent {
defaultValue: 1,
public: true,
};
+ attributes.handGraded = {
+ createPrimitiveOfType: "boolean",
+ createStateVariable: "handGraded",
+ defaultValue: false,
+ public: true,
+ };
attributes.inline = {
createComponentOfType: "boolean",
createStateVariable: "inline",
@@ -266,6 +272,7 @@ export default class Answer extends InlineComponent {
let nChoicesFound = 0;
let definitelyDoNotAddInput = false, mayNeedInput = false;
let foundResponse = false;
+ let foundAward = false;
let childIsWrappable = [];
for (let child of matchedChildren) {
@@ -301,6 +308,7 @@ export default class Answer extends InlineComponent {
childIsWrappable.push(true);
nChoicesFound++;
} else if (componentIsSpecifiedType(child, "award")) {
+ foundAward = true;
childIsWrappable.push(false);
if (child.attributes?.sourcesAreResponses) {
foundResponse = true;
@@ -407,6 +415,10 @@ export default class Answer extends InlineComponent {
}
+ if (componentAttributes.handGraded && !foundAward) {
+ mayNeedInput = true;
+ }
+
if (!mayNeedInput && !foundResponse) {
// recurse to all descendants of awards to see if have a response
for (let child of matchedChildren) {
@@ -588,6 +600,10 @@ export default class Answer extends InlineComponent {
showCorrectnessFlag: {
dependencyType: "flag",
flagName: "showCorrectness"
+ },
+ handGraded: {
+ dependencyType: "stateVariable",
+ variableName: "handGraded"
}
}),
definition({ dependencyValues, usedDefault }) {
@@ -595,7 +611,7 @@ export default class Answer extends InlineComponent {
if (!usedDefault.showCorrectnessPreliminary) {
showCorrectness = dependencyValues.showCorrectnessPreliminary
} else {
- showCorrectness = dependencyValues.showCorrectnessFlag !== false;
+ showCorrectness = dependencyValues.showCorrectnessFlag !== false && !dependencyValues.handGraded;
}
return { setValue: { showCorrectness } }
}
@@ -645,6 +661,10 @@ export default class Answer extends InlineComponent {
haveAwardThatRequiresInput: {
dependencyType: "stateVariable",
variableName: "haveAwardThatRequiresInput"
+ },
+ handGraded: {
+ dependencyType: "stateVariable",
+ variableName: "handGraded"
}
};
@@ -674,7 +694,7 @@ export default class Answer extends InlineComponent {
let skipFirstSugaredInput =
inputChildren[0]?.componentType !== "choiceInput"
&& (
- !dependencyValues.haveAwardThatRequiresInput
+ !(dependencyValues.haveAwardThatRequiresInput || dependencyValues.handGraded)
|| dependencyValues.allInputChildrenIncludingSugared.length > 1
);
@@ -1252,7 +1272,7 @@ export default class Answer extends InlineComponent {
inputChildren: {
dependencyType: "child",
childGroups: ["inputs"],
- variableNames: ["creditAchievedIfSubmit"],
+ variableNames: ["creditAchievedIfSubmit", "value"], // include value so inputs always make dependency values change
childIndices: stateValues.inputChildIndices,
variablesOptional: true,
},
@@ -1747,6 +1767,9 @@ export default class Answer extends InlineComponent {
}
let creditAchieved = await this.stateValues.creditAchievedIfSubmit;
+ if (await this.stateValues.handGraded) {
+ creditAchieved = 0;
+ }
let awardsUsed = await this.stateValues.awardsUsedIfSubmit;
let inputUsed = await this.stateValues.inputUsedIfSubmit;