Skip to content

Commit 79e603f

Browse files
committed
WIP: start of the freetext input type to support fix of issue #1748.
1 parent 9342bee commit 79e603f

7 files changed

Lines changed: 403 additions & 3 deletions

File tree

doc/en/Authoring/Inputs/Text_input.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,23 @@ Some of the dynamic question blocks, e.g. the [Parsons block](../../Specialist_t
77
#### String input ####
88

99
This is a normal input into which students may type whatever they choose. It is always converted into a Maxima string internally.
10+
1011
Notes
1112

1213
1. There is no way whatsoever to parse the student's string into a Maxima expression. If you accept a string, then it will always remain a string! You can't later check for algebraic equivalence. The only tests available will be simple string matches, etc.
1314
2. An empty answer will be blank unless you use the `allowempty` option in which case the answer will be interpreted as an empty string, i.e. `""` rather than `EMPTYANSWER` as would be the case with other inputs.
1415
3. STACK does some sanitation on students' input within strings to stop students typing in HTML code. For example, you may find that a string <code>"a<b"</code> actually ends up in Maxima with the less-than sign inside the string changed into an html entity <code>&amp;lt;</code>, so your string inside Maxima becomes <code>"a&amp;lt;b"</code>. In cases where string matches unexpectedly fail, look at the testing page to see what is actually being compared within the PRT and re-build the teacher's answer to match.
1516

17+
#### Free-text input ####
18+
19+
This input is a text area into which students may type whatever they choose. This input is intended to capture text input of mathematical proof. It is always converted into a Maxima string internally. We accept, for now, there are limitations on the ability to automatically assess free-text proofs.
20+
21+
Notes
22+
23+
1. All the notes for "string" inputs above still apply.
24+
2. The freetext input has a special extra option `manualgraded`, and the default option value is `manualgraded:true`. If you specify `manualgraded:false` then the _whole STACK question_ will require manual grading!
25+
3. Part of this input can be linked to an automatically graded STACK input, e.g. an algebraic input (final answer), textarea (last lines) or units input.
26+
1627
#### JSON input ####
1728

1829
The JSXGraph, GeoGebra and Parsons blocks return a JSON object. When linking to a STACK input we recommend using the dedicated JSON/Geogebra/Parsons inputs rather than the string input. String inputs will continue to work (maintaining legacy JSXGraph questions), but using the JSON input for inputs linked to JSXGraph logically indicates the expected type of string.

doc/en/Developer/Development_track.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Issues with [github milestone 4.13.0](https://github.com/maths/moodle-qtype_stac
1212
1. Remove all "cte" code from Maxima - mostly install.
1313
2. Support for Maxima 5.47.0, 5.48.0, and 5.49.0. This includes a fix for issue #1281 from 5.48.0.
1414
3. Question tests can now test the whole route through a PRT, rather than just the final node. This is a significant improvement on the ability to test questions. This is back-compatible with older questions.
15+
4. Add in a new "free-text" input to allow student to input typed free-text proof.
1516

1617
--------------------------------------
1718

doc/en/Moodle/Semi-automatic_Marking.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ It is certainly possible to have students answer one of the other question types
66

77
Another option is to automatically mark students' short answers using a question type such as the [pattern match](https://moodle.org/plugins/qtype_pmatch) question type in Moodle.
88

9-
STACK provides the "notes" [input type](../Authoring/Inputs/index.md). There are some advantages to using the notes input, rather than an essay.
9+
STACK provides the "free-text" and "notes" [input type](../Authoring/Inputs/Text_input.md). There are some advantages to using these inputs, rather than an essay.
1010

1111
1. It is part of a STACK question, so students' answers can be between other parts of a multi-part question.
1212
2. When students "validate" their answer, any maths types in using LaTeX is displayed. If the teacher shows validation then students get a preview of their answer, and LaTeX will be displayed.
1313

1414
## Manual grading.
1515

16-
The notes input has a special extra option `manualgraded`, and the default option value is `manualgraded:false`. If you specify `manualgraded:true` then the _whole STACK question_ will require manual grading!
16+
The free-text and notes inputs have a special extra option `manualgraded`, and the default option value is `manualgraded:false`. If you specify `manualgraded:true` then the _whole STACK question_ will require manual grading!
1717

18-
There really is no way to mix automatic and manually graded parts of a question. Therefore, if you want automatic and manual marking you must have separate questions.
18+
There really is no way to mix automatic and manually graded parts of a question. Therefore, if you want automatic and manual marking you must have separate questions.

lang/en/qtype_stack.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@
206206
$string['inputtypenumerical'] = 'Numerical';
207207
$string['inputtypegeogebra'] = 'GeoGebra';
208208
$string['inputtypeparsons'] = 'Parsons';
209+
$string['inputtypefreetext'] = 'Free-text';
209210
$string['inputtypeparsons_incorrect_model_ans'] = 'The model answer field to the Parsons input is malformed. It should be one of the following: <ul> <li> <strong>Proof questions</strong>: a list of the form <code>[ta, proof_steps]</code> where <code>ta</code> is the correct answer variable and <code>proof_steps</code> is the variable containing all available proof steps.</li><li><strong>Grouping questions</strong> If the columns variable is set in the Parson\'s block, then an array of the form <code>[ta, steps, n]</code>, where <code>ta</code> is the correct answer variable, <code>steps</code> is the variable containing all available steps and <code>n</code> is the number of columns.</li> <li><strong>Grid questions</strong> If both the columns and row variables are set in the Parson\'s block, then an array of the form <code>[ta, steps, n, m]</code>, where <code>ta</code> is the correct answer variable, <code>steps</code> is the variable containing all available steps, <code>n</code> is the number of columns and <code>m</code> is the number of rows.</li>';
210211
$string['numericalinputmustnumber'] = 'This input expects a number.';
211212
$string['numericalinputvarsforbidden'] = 'This input expects a number, and so may not contain variables.';
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<quiz>
3+
<!-- question: 13 -->
4+
<question type="stack">
5+
<name>
6+
<text>Free-text</text>
7+
</name>
8+
<questiontext format="moodle_auto_format">
9+
<text><![CDATA[<p>Prove that \(\sum_{k=1}^n 2k − 1 = n^2\)</p>
10+
<p>[[input:ans1]] [[validation:ans1]]</p>]]></text>
11+
</questiontext>
12+
<generalfeedback format="moodle_auto_format">
13+
<text></text>
14+
</generalfeedback>
15+
<defaultgrade>1</defaultgrade>
16+
<penalty>0.1</penalty>
17+
<hidden>0</hidden>
18+
<idnumber></idnumber>
19+
<stackversion>
20+
<text>2026042402</text>
21+
</stackversion>
22+
<questionvariables>
23+
<text><![CDATA[ta1:"## Theorem: ∀ n ∈ ℕ, 1+3+5+7+...+(2n−1) = n².
24+
25+
Let P(n) be the statement `sum_(k=1)^n 2k − 1 = n²` ∀ n ∈ ℕ.
26+
27+
Since `sum_(k=1)^1 =1 =1^2`, we see that \\(P(1)\\) is true.
28+
29+
Assume that \\(P(n)\\) is true. Then
30+
````
31+
sum_(k=1)^(n+1) (2k−1) = sum_(k=1)^(n) (2k−1)+ (2(n+1)−1)
32+
= n² + 2n + 1
33+
= (n+1)².
34+
````
35+
36+
Since `P(1)` is true and `P(n+1)` follows from `P(n)`, we conclude
37+
that `P(n)` is true `forall n in NN` by the principle of mathematical induction.";]]></text>
38+
</questionvariables>
39+
<specificfeedback format="html">
40+
<text>[[feedback:prt1]]</text>
41+
</specificfeedback>
42+
<questionnote format="html">
43+
<text>{@ta1@}</text>
44+
</questionnote>
45+
<questiondescription format="html">
46+
<text>This is an example question for the free-text input type.
47+
48+
The teacher's answer contains newline white space, ASCIImath, LaTeX, unicode etc. as a comprehensive test of what students might be able to type in.</text>
49+
</questiondescription>
50+
<questionsimplify>1</questionsimplify>
51+
<assumepositive>0</assumepositive>
52+
<assumereal>0</assumereal>
53+
<prtcorrect format="html">
54+
<text><![CDATA[[[commonstring key="symbolicprtcorrectfeedback"/]] [[commonstring key="defaultprtcorrectfeedback"/]]]]></text>
55+
</prtcorrect>
56+
<prtpartiallycorrect format="html">
57+
<text><![CDATA[[[commonstring key="symbolicprtpartiallycorrectfeedback"/]] [[commonstring key="defaultprtpartiallycorrectfeedback"/]]]]></text>
58+
</prtpartiallycorrect>
59+
<prtincorrect format="html">
60+
<text><![CDATA[[[commonstring key="symbolicprtincorrectfeedback"/]] [[commonstring key="defaultprtincorrectfeedback"/]]]]></text>
61+
</prtincorrect>
62+
<decimals>.</decimals>
63+
<scientificnotation>*10</scientificnotation>
64+
<multiplicationsign>dot</multiplicationsign>
65+
<sqrtsign>1</sqrtsign>
66+
<complexno>i</complexno>
67+
<inversetrig>cos-1</inversetrig>
68+
<logicsymbol>lang</logicsymbol>
69+
<matrixparens>[</matrixparens>
70+
<isbroken>0</isbroken>
71+
<variantsselectionseed></variantsselectionseed>
72+
<input>
73+
<name>ans1</name>
74+
<type>freetext</type>
75+
<tans>ta1</tans>
76+
<boxsize>80</boxsize>
77+
<strictsyntax>1</strictsyntax>
78+
<insertstars>0</insertstars>
79+
<syntaxhint><![CDATA["Try a new proof"]]></syntaxhint>
80+
<syntaxattribute>1</syntaxattribute>
81+
<forbidwords></forbidwords>
82+
<allowwords></allowwords>
83+
<forbidfloat>1</forbidfloat>
84+
<requirelowestterms>0</requirelowestterms>
85+
<checkanswertype>0</checkanswertype>
86+
<mustverify>1</mustverify>
87+
<showvalidation>1</showvalidation>
88+
<options>manualgraded:true</options>
89+
</input>
90+
<prt>
91+
<name>prt1</name>
92+
<value>1.0000000</value>
93+
<autosimplify>1</autosimplify>
94+
<feedbackstyle>1</feedbackstyle>
95+
<feedbackvariables>
96+
<text></text>
97+
</feedbackvariables>
98+
<node>
99+
<name>0</name>
100+
<description></description>
101+
<answertest>AlgEquiv</answertest>
102+
<sans>ans1</sans>
103+
<tans>null</tans>
104+
<testoptions></testoptions>
105+
<quiet>0</quiet>
106+
<truescoremode>=</truescoremode>
107+
<truescore>1</truescore>
108+
<truepenalty></truepenalty>
109+
<truenextnode>-1</truenextnode>
110+
<trueanswernote>prt1-1-T</trueanswernote>
111+
<truefeedback format="html">
112+
<text><![CDATA[<pre>{#ans1#}</pre>]]></text>
113+
</truefeedback>
114+
<falsescoremode>=</falsescoremode>
115+
<falsescore>0</falsescore>
116+
<falsepenalty></falsepenalty>
117+
<falsenextnode>-1</falsenextnode>
118+
<falseanswernote>prt1-1-F</falseanswernote>
119+
<falsefeedback format="html">
120+
<text><![CDATA[<pre>{#ans1#}</pre>]]></text>
121+
</falsefeedback>
122+
</node>
123+
</prt>
124+
<qtest>
125+
<testcase>1</testcase>
126+
<description>Default test case: note this is a manually graded question so this PRT won't fire!</description>
127+
<testinput>
128+
<name>ans1</name>
129+
<value>ta1</value>
130+
</testinput>
131+
<expected>
132+
<name>prt1</name>
133+
<expectedscore>0.0000000</expectedscore>
134+
<expectedpenalty></expectedpenalty>
135+
<expectedanswernote>( prt1-1-F ]</expectedanswernote>
136+
</expected>
137+
</qtest>
138+
</question>
139+
140+
</quiz>
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
// This file is part of Stack - http://stack.maths.ed.ac.uk/
3+
//
4+
// Stack is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Stack is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Stack. If not, see <http://www.gnu.org/licenses/>.
16+
17+
defined('MOODLE_INTERNAL') || die();
18+
19+
require_once(__DIR__ . '/../algebraic/algebraic.class.php');
20+
require_once(__DIR__ . '/../string/string.class.php');
21+
22+
/**
23+
* A text-field input which is always interpreted as a Maxima string.
24+
* This is intended for students to type in a complete proof.
25+
*
26+
* @package qtype_stack
27+
* @copyright 2026 University of Edinburgh
28+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29+
*/
30+
class stack_freetext_input extends stack_string_input {
31+
// phpcs:ignore moodle.Commenting.VariableComment.Missing
32+
protected $extraoptions = [
33+
'hideanswer' => false,
34+
'allowempty' => false,
35+
'validator' => false,
36+
'manualgraded' => false,
37+
];
38+
39+
/**
40+
* Return the default values for the options. Using this is optional, in this
41+
* base class implementation, no default options are set.
42+
* @return array option => default value.
43+
*/
44+
public static function get_parameters_defaults() {
45+
return [
46+
'mustVerify' => true,
47+
'showValidation' => 1,
48+
'boxWidth' => 80,
49+
'insertStars' => 0,
50+
'syntaxHint' => '',
51+
'syntaxAttribute' => 0,
52+
'forbidWords' => '',
53+
'allowWords' => '',
54+
'forbidFloats' => true,
55+
'lowestTerms' => true,
56+
'sameType' => true,
57+
'options' => '',
58+
];
59+
}
60+
61+
// phpcs:ignore moodle.Commenting.MissingDocblock.Function
62+
public function render(stack_input_state $state, $fieldname, $readonly, $tavalue) {
63+
if ($this->errors) {
64+
return $this->render_error($this->errors);
65+
}
66+
67+
// Note that at the moment, $this->boxHeight and $this->boxWidth are only
68+
// used as minimums. If the current input is bigger, the box is expanded.
69+
$attributes = [
70+
'name' => $fieldname,
71+
'id' => $fieldname,
72+
'autocapitalize' => 'none',
73+
'spellcheck' => 'false',
74+
];
75+
76+
$value = stack_utils::maxima_string_to_php_string($this->contents_to_maxima($state->contents));
77+
if ($this->is_blank_response($state->contents)) {
78+
$value = stack_utils::maxima_string_to_php_string($this->parameters['syntaxHint']);
79+
if ($this->parameters['syntaxAttribute'] == '1') {
80+
$attributes['placeholder'] = $value;
81+
$value = '';
82+
}
83+
}
84+
85+
// TODO: sort out size of text area.
86+
$attributes['rows'] = 3;
87+
$attributes['cols'] = $this->parameters['boxWidth'];
88+
89+
if ($readonly) {
90+
$attributes['readonly'] = 'readonly';
91+
}
92+
93+
// Metadata for JS users.
94+
$attributes['data-stack-input-type'] = 'freetext';
95+
96+
return html_writer::tag('textarea', htmlspecialchars($value, ENT_COMPAT), $attributes) .
97+
html_writer::tag('div', "", ['class' => 'clearfix']);
98+
}
99+
100+
// phpcs:ignore moodle.Commenting.MissingDocblock.Function
101+
public function render_api_data($tavalue) {
102+
if ($this->errors) {
103+
throw new stack_exception("Error rendering input: " . implode(',', $this->errors));
104+
}
105+
106+
$data = [];
107+
108+
$data['type'] = 'freetext';
109+
$data['boxWidth'] = $this->parameters['boxWidth'];
110+
$data['syntaxHint'] = $this->parameters['syntaxHint'];
111+
112+
return $data;
113+
}
114+
115+
/**
116+
* This function constructs the display of variables during validation.
117+
*
118+
* @param stack_casstring $answer, the complete answer.
119+
* @return string any error messages describing validation failures. An empty
120+
* string if the input is valid - at least according to this test.
121+
*/
122+
protected function validation_display(
123+
$answer,
124+
$lvars,
125+
$caslines,
126+
$additionalvars,
127+
$valid,
128+
$errors,
129+
$castextprocessor,
130+
$inertdisplayform,
131+
$ilines,
132+
$notes
133+
) {
134+
// Always display something sensible.
135+
$display = htmlentities($this->contents_to_maxima($this->rawcontents));
136+
$display = substr($display, 1, strlen($display) - 2);
137+
if ($answer->is_correctly_evaluated()) {
138+
$display = stack_utils::maxima_string_to_php_string($answer->get_value());
139+
$display = nl2br($display);
140+
} else {
141+
$valid = false;
142+
}
143+
// TODO: something better here.
144+
$display = html_writer::tag('p', $display);
145+
return [$valid, $errors, $display, $notes];
146+
}
147+
}

0 commit comments

Comments
 (0)