diff --git a/api/controller/GradingController.php b/api/controller/GradingController.php index 0088b90529c..70c2fe61f83 100644 --- a/api/controller/GradingController.php +++ b/api/controller/GradingController.php @@ -79,50 +79,51 @@ public function __invoke(Request $request, Response $response, array $args): Res $storeprefix = uniqid(); $gradingresponse = new StackGradingResponse(); $gradingresponse->isgradable = true; - + + if (!$question->is_gradable_response($data['answers'])) { + $gradingresponse->isgradable = false; + $response->getBody()->write(json_encode($gradingresponse)); + return $response->withHeader('Content-Type', 'application/json'); + } + $scores = []; foreach ($question->prts as $index => $prt) { $result = $question->get_prt_result($index, $data['answers'], true); + $scores[$index] = $result->get_score(); - // If not all inputs required for the prt have been filled out, - // or the prt evaluation caused an error, we abort the grading, - // and indicate that this input state is not gradable. - if ($result->get_errors() || !$question->has_necessary_prt_inputs($prt, $data['answers'], true)) { - $gradingresponse = new StackGradingResponse(); - $gradingresponse->isgradable = false; - - $response->getBody()->write(json_encode($gradingresponse)); - return $response->withHeader('Content-Type', 'application/json'); - } - - $feedbackstyle = $prt->get_feedbackstyle(); - - $feedback = $result->apply_placeholder_holder($result->get_feedback()); - $standardfeedback = $this->standard_prt_feedback($question, $result, $feedbackstyle); - - switch ($feedbackstyle) { - // Formative. - case 0: - $overallfeedback = $feedback; - break; - // Standard. - case 1: - case 2: - $overallfeedback = $standardfeedback . $feedback; - break; - // Compact. - // Symbolic. - case 3: - $overallfeedback = $standardfeedback; - break; - // Invalid. - default: - $overallfeedback = "Invalid Feedback style"; - break; + $errors = $result->get_errors(); + if ($errors) { + $overallfeedback = $errors; + } else if (!$question->has_necessary_prt_inputs($prt, $data['answers'], true)) { + continue; + } else { + $feedbackstyle = $prt->get_feedbackstyle(); + + $feedback = $result->apply_placeholder_holder($result->get_feedback()); + $standardfeedback = $this->standard_prt_feedback($question, $result, $feedbackstyle); + + switch ($feedbackstyle) { + // Formative. + case 0: + $overallfeedback = $feedback; + break; + // Standard. + case 1: + case 2: + $overallfeedback = $standardfeedback . $feedback; + break; + // Compact. + // Symbolic. + case 3: + $overallfeedback = $standardfeedback; + break; + // Invalid. + default: + $overallfeedback = "Invalid Feedback style"; + break; + } } - $scores[$index] = $result->get_score(); - $gradingresponse->prts[$index] = $translate->filter( \stack_maths::process_display_castext($overallfeedback), $language diff --git a/api/public/stackshared.js b/api/public/stackshared.js index c2035801fc3..c8ada40c6b2 100644 --- a/api/public/stackshared.js +++ b/api/public/stackshared.js @@ -269,8 +269,9 @@ function answer() { document.getElementById('errors').innerText = ''; } if (!json.isgradable) { + // Should we display this or nothing (like Moodle)? document.getElementById('stackapi_validity').innerText - = ' Please enter valid answers for all parts of the question.'; + = ' Please supply additional valid answers.'; return; } renameIframeHolders(); @@ -307,7 +308,7 @@ function answer() { const elements = document.getElementsByName(`${feedbackPrefix + name}`); if (elements.length > 0) { const element = elements[0]; - if (json.scores[name] !== undefined) { + if (json.scores[name] !== undefined && json.scoreweights[name]) { fb = fb + `
Marks for this submission: ${(json.scores[name] * json.scoreweights[name] * json.scoreweights.total).toFixed(2)} / ${(json.scoreweights[name] * json.scoreweights.total).toFixed(2)}.
`; diff --git a/api/questiondefaults.yml b/api/questiondefaults.yml index deda08420ea..280659a31ae 100644 --- a/api/questiondefaults.yml +++ b/api/questiondefaults.yml @@ -87,7 +87,7 @@ qtest: description: testinput: name: 'ans1' - value: 'ta1' + value: '' expected: name: 'prt1' expectedscore: diff --git a/api/util/StackQuestionLoader.php b/api/util/StackQuestionLoader.php index f20491cc2e9..0df570b1f97 100644 --- a/api/util/StackQuestionLoader.php +++ b/api/util/StackQuestionLoader.php @@ -495,7 +495,7 @@ public static function loadxml($xml, $includetests = false) { $testiname = isset($testinput->name) ? (string) $testinput->name : self::get_default('testinput', 'name', 'ans1'); $testivalue = (array) $testinput->value ? (string) $testinput->value : - self::get_default('testinput', 'value', 'ta1'); + self::get_default('testinput', 'value', ''); $testinputs[$testiname] = $testivalue; } $testdescription = isset($test->description) ? (string) $test->description : diff --git a/tests/api_controller_test.php b/tests/api_controller_test.php index 76c7b280202..2cd9422021e 100644 --- a/tests/api_controller_test.php +++ b/tests/api_controller_test.php @@ -246,7 +246,73 @@ public function test_grade(): void { $this->assertEquals('

[[feedback:prt1]]

', $this->output->specificfeedback); $this->assertStringContainsString('correct', $this->output->prts->prt1); $this->assertEquals(0, count((array)$this->output->gradingassets)); - $this->assertEquals('Seed: 86; ans1: matrix([35,30],[28,24]) [valid]; prt1: !', $this->output->responsesummary); + $this->assertEquals( + 'Seed: 86; ans1: matrix([35,30],[28,24]) [score]; prt1: # = 1 | | 1-0-T', + $this->output->responsesummary + ); + $this->assertEquals(0, count($this->output->iframes)); + } + + public function test_grade_multipleprts(): void { + $this->requestdata['questionDefinition'] = stack_api_test_data::get_question_string('multipleprts'); + $this->requestdata['answers'] = (array) json_decode(stack_api_test_data::get_answer_string('multipleprts_correct')); + $gc = new GradingController(); + $gc->__invoke($this->request, $this->response, []); + $this->assertEquals(true, $this->output->isgradable); + $this->assertEquals(1, $this->output->score); + $this->assertEquals(1, $this->output->scores->even); + $this->assertEquals(1, $this->output->scores->odd); + $this->assertEquals(1, $this->output->scores->oddeven); + $this->assertEquals(1, $this->output->scores->poly); + $this->assertEquals(1, $this->output->scores->unique); + $this->assertEquals(1, $this->output->scores->total); + $this->assertEqualsWithDelta(0.2, $this->output->scoreweights->even, 0.0001); + $this->assertEqualsWithDelta(0.2, $this->output->scoreweights->odd, 0.0001); + $this->assertEqualsWithDelta(0.4, $this->output->scoreweights->oddeven, 0.0001); + $this->assertEquals(null, $this->output->scoreweights->poly ?? null); + $this->assertEqualsWithDelta(0.2, $this->output->scoreweights->unique, 0.0001); + $this->assertEquals(5, $this->output->scoreweights->total); + $this->assertEquals('', $this->output->specificfeedback); + $this->assertEquals( + '

Perhaps you could think of some non-polynomial examples as well?

', + $this->output->prts->poly + ); + $this->assertEquals(0, count((array)$this->output->gradingassets)); + $this->assertEquals( + 'Seed: -1; ans1: x^3 [score]; ans2: x^2 [score]; ans3: 0 [score]; ans4: true [score]; ' . + 'even: # = 1 | even-0-T; odd: # = 1 | odd-0-T; oddeven: # = 1 | ODD | EVEN; ' . + 'poly: # = 1 [formative] | ATLogic_True. | poly-1-T; unique: # = 1 | ATLogic_True. | unique-0-T', + $this->output->responsesummary + ); + $this->assertEquals(0, count($this->output->iframes)); + } + + public function test_grade_some_answers_multipleprt(): void { + $this->requestdata['questionDefinition'] = stack_api_test_data::get_question_string('multipleprts'); + // Including unsupplied and invalid answers. + $this->requestdata['answers'] = (array) json_decode(stack_api_test_data::get_answer_string('multipleprts_some')); + $gc = new GradingController(); + $gc->__invoke($this->request, $this->response, []); + $this->assertEquals(true, $this->output->isgradable); + $this->assertEqualsWithDelta(0.4, $this->output->score, 0.0001); + $this->assertEquals(null, $this->output->scores->even); + $this->assertEquals(1, $this->output->scores->odd); + $this->assertEquals(null, $this->output->scores->oddeven); + $this->assertEquals(null, $this->output->scores->poly); + $this->assertEquals(1, $this->output->scores->unique); + $this->assertEqualsWithDelta(0.4, $this->output->scores->total, 0.0001); + $this->assertEquals('', $this->output->specificfeedback); + $this->assertStringContainsString('Correct answer', $this->output->prts->odd); + $this->assertStringContainsString('Correct answer', $this->output->prts->unique); + $this->assertEquals(null, $this->output->prts->even ?? null); + $this->assertEquals(null, $this->output->prts->poly ?? null); + $this->assertEquals(null, $this->output->prts->oddeven ?? null); + $this->assertEquals(0, count((array)$this->output->gradingassets)); + $this->assertEquals( + 'Seed: -1; ans1: x^3 [score]; ans2: * [invalid]; ans4: true [score]; even: !; ' . + 'odd: # = 1 | odd-0-T; oddeven: !; poly: !; unique: # = 1 | ATLogic_True. | unique-0-T', + $this->output->responsesummary + ); $this->assertEquals(0, count($this->output->iframes)); } @@ -272,6 +338,10 @@ public function test_default(): void { $this->assertEquals(1, $this->output->scoreweights->total); $this->assertEquals('

[[feedback:prt1]]

', $this->output->specificfeedback); $this->assertStringContainsString('correct', $this->output->prts->prt1); + $this->assertEquals( + 'Seed: -1; ans1: 1 [score]; prt1: # = 1 | prt1-1-T', + $this->output->responsesummary + ); } public function test_grade_scores(): void { @@ -293,6 +363,11 @@ public function test_grade_scores(): void { $this->assertEqualsWithDelta(0.1, $this->output->scoreweights->prt3, 0.0001); $this->assertEqualsWithDelta(0.1, $this->output->scoreweights->prt4, 0.0001); $this->assertEquals(10, $this->output->scoreweights->total); + $this->assertEquals( + 'Seed: -1; ans1: c [score]; ans2: 1 [score]; ans3: 0 [score]; ans4: 0 [score]; prt1: # = 1 | prt1-1-T; ' . + 'prt2: # = 1 | prt2-1-T; prt3: # = 0 | prt3-1-F; prt4: # = 1 | prt4-1-T', + $this->output->responsesummary + ); } public function test_download(): void { diff --git a/tests/fixtures/apifixtures.class.php b/tests/fixtures/apifixtures.class.php index fdedec264dd..df12c590333 100644 --- a/tests/fixtures/apifixtures.class.php +++ b/tests/fixtures/apifixtures.class.php @@ -31,6 +31,441 @@ class stack_api_test_data { ', + 'multipleprts' => + ' + + + Odd and even functions + + + 1. Give an example of an odd function by typing an expression which represents it. \(f_1(x)=\) [[input:ans1]]. [[validation:ans1]] [[feedback:odd]]

+

2. Give an example of an even function. \(f_2(x)=\) [[input:ans2]]. [[validation:ans2]] [[feedback:even]]

+

3. Give an example of a function which is odd and even. \(f_3(x)=\) [[input:ans3]]. [[validation:ans3]] [[feedback:oddeven]]

+

[[feedback:poly]]

+

4. Is the answer to 3. unique? [[input:ans4]] (Or are there many different possibilities.) [[validation:ans4]] [[feedback:unique]]

]]>
+
+ + A function \(f\) is odd if + \[ f(x)=-f(-x) \forall x.\] + An example is \(f(x)=4x^3\).  Indeed, polynomials with only odd powers are fine.

+

A function \(f\) is even if \[ f(x)=f(-x) \forall x.\] + An example is \(f(x)=5x^4\).  Indeed, polynomials with only even powers are fine.

+

It is possible to have both \[ f(x)=f(-x)=-f(-x) \] in which case \(f(x)=0\) for all \(x\). This example is unique. +

]]>
+
+ 5 + 0.3333333 + 0 + + + 2026010500 + + + + + + + + + + + + + + 1 + 0 + 0 + + Correct answer, well done.]]> + + + Your answer is partially correct.]]> + + + Incorrect answer.]]> + + . + *10 + dot + 1 + i + cos-1 + lang + [ + 0 + + + ans1 + algebraic + x^3 + 15 + 1 + 0 + + 0 + + + 1 + 1 + 1 + 1 + 3 + + + + ans2 + algebraic + x^4 + 15 + 1 + 0 + + 0 + + + 1 + 1 + 1 + 1 + 3 + + + + ans3 + algebraic + 0 + 15 + 1 + 0 + + 0 + + + 1 + 1 + 1 + 1 + 3 + + + + ans4 + boolean + true + 15 + 1 + 0 + + 0 + + + 1 + 1 + 1 + 0 + 0 + + + + even + 1.0000000 + 1 + 1 + + sa:ans2-subst(x=-x,ans2); + + + 0 + + AlgEquiv + sa + 0 + + 0 + = + 1 + + -1 + even-0-T + + + + = + 0 + + -1 + even-0-F + + Your answer is not an even function. Look, \[ f(x)-f(-x)={@sa@} \neq 0.\]

]]>
+
+
+
+ + odd + 1.0000000 + 0 + 1 + + sa:ev(subst(x=-x,ans1)+ans1,simp); + + + 0 + + AlgEquiv + sa + 0 + + 0 + = + 1 + + -1 + odd-0-T + + + + = + 0 + + -1 + odd-0-F + + Your answer is not an odd function. Look, + \[ f(x)+f(-x)={@ans1@} + {@subst(x=-x,ans1)@} \]

+ \[ ={@ans1@} + {@ev(subst(x=-x,ans1),simp)@}={@sa@} \neq 0.\]

]]>
+
+
+
+ + oddeven + 2.0000000 + 1 + 1 + + sa1:subst(x=-x,ans3)+ans3; + sa2:ans3-subst(x=-x,ans3); + + + 0 + + AlgEquiv + sa1 + 0 + + 0 + = + 0.5 + + 1 + ODD + + + + = + 0 + + 1 + oddeven-0-F + + Your answer is not an odd function. Look, \[ f(x)+f(-x)={@sa1@} \neq 0.\]

]]>
+
+
+ + 1 + + AlgEquiv + sa2 + 0 + + 0 + + + 0.5 + + -1 + EVEN + + + + + + 0 + + -1 + oddeven-1-F + + Your answer is not an even function. Look, \[ f(x)-f(-x)={@sa2@} \neq 0.\]

]]>
+
+
+
+ + poly + 1.0000000 + 1 + 0 + + sa:all_listp(polynomialpsimp,[ans1,ans2]); + + + 0 + + AlgEquiv + sa + true + + 0 + = + 1 + + -1 + poly-1-T + + Perhaps you could think of some non-polynomial examples as well?

]]>
+
+ = + 0 + + -1 + poly-1-F + + + +
+
+ + unique + 1.0000000 + 1 + 1 + + + + + 0 + + AlgEquiv + ans4 + true + + 0 + = + 1 + + -1 + unique-0-T + + + + = + 0 + + -1 + unique-0-F + + + + + + + 1 + + + ans1 + x^3 + + + ans2 + cos(x) + + + ans3 + 0 + + + ans4 + true + + + even + 1.0000000 + 0.0000000 + even-0-T + + + odd + 1.0000000 + 0.0000000 + odd-0-T + + + oddeven + 1.0000000 + 0.0000000 + EVEN + + + poly + 0.0000000 + 0.3333333 + poly-1-F + + + unique + 1.0000000 + 0.0000000 + unique-0-T + + + + 2 + + + ans1 + x^2 + + + ans2 + x^3 + + + ans3 + x^3 + + + ans4 + false + + + even + 0.0000000 + 0.3333333 + even-0-F + + + odd + 0.0000000 + 0.3333333 + odd-0-F + + + oddeven + 0.5000000 + 0.3333333 + oddeven-1-F + + + poly + 1.0000000 + 0.0000000 + poly-1-T + + + unique + 0.0000000 + 0.3333333 + unique-0-F + + +
+
', 'matrices' => ' @@ -1519,7 +1954,7 @@ class stack_api_test_data { 2 ans1 - + 2 prt1 @@ -1742,9 +2177,14 @@ class stack_api_test_data { // phpcs:ignore moodle.Commenting.VariableComment.Missing protected static array $answers = [ - 'matrices_correct' => '{"ans1_sub_0_0": "35", "ans1_sub_0_1": "30", "ans1_sub_1_0": "28", "ans1_sub_1_1": "24"}', - 'multiple_mixed' => '{"ans1": "3", "ans2": "1", "ans3": "0", "ans4": "0"}', - 'empty' => '{"ans1": "1"}', + 'multipleprts_correct' => '{"ans1": "x^3", "ans2": "x^2", "ans3": "0", "ans1_val": "x^3", ' . + '"ans2_val": "x^2", "ans3_val": "0", "ans4": "true"}', + 'multipleprts_some' => '{"ans1": "x^3", "ans2": "*", "ans1_val": "x^3", "ans2_val": "*", "ans3": "", "ans4": "true"}', + 'matrices_correct' => '{"ans1_sub_0_0": "35", "ans1_sub_0_1": "30", "ans1_sub_1_0": "28", "ans1_sub_1_1": "24", ' . + '"ans1_val": "matrix([35,30],[28,24])"}', + 'multiple_mixed' => '{"ans1": "3", "ans2": "1", "ans3": "0", "ans4": "0", ' . + '"ans2_val": "1", "ans3_val": "0", "ans4_val": "0"}', + 'empty' => '{"ans1": "1", "ans1_val": "1"}', ]; // phpcs:ignore moodle.Commenting.MissingDocblock.Function