Skip to content

Commit 89a2594

Browse files
committed
sugar - Add answer test summary options
1 parent 9fdd513 commit 89a2594

File tree

4 files changed

+253
-19
lines changed

4 files changed

+253
-19
lines changed

classes/yaml_converter.php

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -297,10 +297,15 @@ public static function loadyaml($yaml, $defaults) {
297297
return $xmldata;
298298
}
299299

300+
/**
301+
* Splits an answertest string into its components and adds the fields to the node.
302+
* @param mixed $node
303+
* @return void
304+
*/
300305
public static function parse_answertest(&$node) {
301306
if (substr($node->answertest, 0, 2) === 'AT') {
302307
[$answertest, $sans, $tans, $testoptions] = self::split_answertest($node->answertest);
303-
self::set_field($node, 'answertest', $answertest);
308+
self::set_field($node, 'answertest', substr($answertest, 2));
304309
self::set_field($node, 'sans', $sans);
305310
self::set_field($node, 'tans', $tans);
306311
self::set_field($node, 'testoptions', $testoptions);
@@ -555,10 +560,11 @@ public static function detect_differences($xml, $defaults) {
555560
'AT' . $node['answertest'] : self::split_answertest(self::get_default('node', 'answertest'))[0];
556561
$diffsans = isset($node['sans']) ? $node['sans'] : self::get_default('node', 'sans');
557562
$difftans = isset($node['tans']) ? $node['tans'] : self::get_default('node', 'tans');
558-
$difftestoptions = isset($node->testoptions) ?
559-
$node->testoptions : self::get_default('node', 'testoptions');
563+
$difftestoptions = isset($node['testoptions']) ?
564+
$node['testoptions'] : self::get_default('node', 'testoptions');
560565
$diffnode['answertest'] =
561-
"{$diffanswertest}({$diffsans},{$difftans}" . ($difftestoptions !== '' ? ",{$difftestoptions}" : '') . ')';
566+
"{$diffanswertest}({$diffsans},{$difftans}" .
567+
($difftestoptions !== '' ? ",{$difftestoptions}" : '') . ')';
562568
unset($diffnode['sans']);
563569
unset($diffnode['tans']);
564570
unset($diffnode['testoptions']);
@@ -704,19 +710,19 @@ public static function split_answertest($answertest) {
704710
$firstbracketpos = strpos($answertest, '(');
705711
$result[] = substr($answertest, 0, $firstbracketpos);
706712
$testprops = substr($answertest, $firstbracketpos + 1, strrpos($answertest, ')') - $firstbracketpos - 1);
707-
$bracket_level = 0;
713+
$bracketlevel = 0;
708714
$current = '';
709715
$count = 0;
710716
$len = strlen($testprops);
711717
for ($i = 0; $i < $len; $i++) {
712718
$char = $testprops[$i];
713719
if ($char === '(') {
714-
$bracket_level++;
720+
$bracketlevel++;
715721
$current .= $char;
716722
} else if ($char === ')') {
717-
$bracket_level--;
723+
$bracketlevel--;
718724
$current .= $char;
719-
} else if ($char === ',' && $bracket_level === 0 && $count < 2) {
725+
} else if ($char === ',' && $bracketlevel === 0 && $count < 2) {
720726
$result[] = trim($current);
721727
$current = '';
722728
$count++;
@@ -725,7 +731,7 @@ public static function split_answertest($answertest) {
725731
}
726732
}
727733
$result[] = trim($current);
728-
// Ensure always 4 items
734+
// Ensure always 4 items.
729735
while (count($result) < 4) {
730736
$result[] = '';
731737
}

doc/useyaml.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,21 @@ specificfeedbackformat: html
5252

5353
See [test question](../tests/fixtures/fullquestion.yml) for sample YAML layout.
5454

55+
Additionally you can set your default file so that the answer tests for your questions are stored
56+
in a summary format:
57+
```
58+
ATanswertest(sans,tans,testoptions)
59+
```
60+
Default file example:
61+
```
62+
answertest: ATAlgEquiv(ans1,ta1)
63+
# sans, tans, testoptions will not be used but need to be here for diff compatibility
64+
# with questions which have them rather than the summary answertest.
65+
sans:
66+
tans:
67+
testoptions:
68+
```
69+
5570
When importing to Moodle, Gitsync adds all the missing fields to the YAML representation from the defaults file, converts it to XML, creates a temporary XML file in the repo directory and then uploads this to Moodle. The defaults file is selected in the same way as for export so you can use different defaults if required.
5671

5772
Obviously, using different defaults for import and export should be handled with care to avoid information loss! It makes most sense for altering site-specific settings that will be the same for every question e.g. decimal separator. Normally Gitsync skips importing questions that have not changed in the repo since the last import so if you want to update them using different defaults you will need to use `-z` to force import of questions that have not changed themselves.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
name: Test question
2+
questiontext: |
3+
<p>Question</p><p>[[input:ans1]] [[validation:ans1]]</p>
4+
<p>[[input:ans2]] [[validation:ans2]]</p>
5+
questiontextformat: html
6+
generalfeedback:
7+
generalfeedbackformat: html
8+
defaultgrade: 1
9+
penalty: 0.1
10+
hidden: 0
11+
idnumber:
12+
stackversion: 2025042500
13+
questionvariables: ta1:1;ta2:2;
14+
specificfeedback: <p>[[feedback:prt1]]</p>
15+
specificfeedbackformat: html
16+
questionnote: <p>{@ta1@}</p>
17+
questionnoteformat: html
18+
questiondescription:
19+
questiondescriptionformat: html
20+
questionsimplify: 1
21+
assumepositive: 0
22+
assumereal: 0
23+
prtcorrect: <p><i class="fa fa-check"></i> Correct answer*, well done.</p>
24+
prtcorrectformat: html
25+
prtpartiallycorrect: <p><i class="fa fa-adjust"></i> Your answer is partially correct.</p>
26+
prtpartiallycorrectformat: html
27+
prtincorrect: <p><i class="fa fa-times"></i> Incorrect answer.</p>
28+
prtincorrectformat: html
29+
decimals: .
30+
scientificnotation: '*10'
31+
multiplicationsign: cross
32+
sqrtsign: 1
33+
complexno: i
34+
inversetrig: cos-1
35+
logicsymbol: lang
36+
matrixparens: '['
37+
isbroken: 0
38+
variantsselectionseed:
39+
deployedseed:
40+
- 1
41+
- 2
42+
- 3
43+
input:
44+
- name: ans1
45+
type: algebraic
46+
tans: ta1
47+
boxsize: 25
48+
strictsyntax: 1
49+
insertstars: 0
50+
syntaxhint:
51+
syntaxattribute: 0
52+
forbidwords:
53+
allowwords:
54+
forbidfloat: 1
55+
requirelowestterms: 0
56+
checkanswertype: 0
57+
mustverify: 1
58+
showvalidation: 1
59+
options:
60+
- name: ans2
61+
type: algebraic
62+
tans: ta2
63+
boxsize: 15
64+
strictsyntax: 1
65+
insertstars: 0
66+
syntaxhint:
67+
syntaxattribute: 0
68+
forbidwords:
69+
allowwords:
70+
forbidfloat: 1
71+
requirelowestterms: 0
72+
checkanswertype: 0
73+
mustverify: 1
74+
showvalidation: 1
75+
options:
76+
prt:
77+
- name: prt1
78+
value: 2.0000000
79+
autosimplify: 1
80+
feedbackstyle: 1
81+
feedbackvariables:
82+
node:
83+
- name: 0
84+
description:
85+
answertest: Diff
86+
sans: ans1
87+
tans: diff(p,v)
88+
testoptions: v
89+
quiet: 1
90+
truescoremode: =
91+
truescore: 1
92+
truepenalty:
93+
truenextnode: -1
94+
trueanswernote: prt1-1-T
95+
truefeedback:
96+
truefeedbackformat: html
97+
falsescoremode: =
98+
falsescore: 0
99+
falsepenalty:
100+
falsenextnode: -1
101+
falseanswernote: prt1-1-F
102+
falsefeedback:
103+
falsefeedbackformat: html
104+
- name: prt2
105+
value: 1.0000001
106+
autosimplify: 1
107+
feedbackstyle: 1
108+
feedbackvariables:
109+
node:
110+
- name: 0
111+
description:
112+
answertest: AlgEquiv
113+
sans: ans2
114+
tans: -rdm*sin(v)
115+
testoptions:
116+
quiet: 0
117+
truescoremode: =
118+
truescore: 1
119+
truepenalty:
120+
truenextnode: -1
121+
trueanswernote: prt1-1-T
122+
truefeedback:
123+
truefeedbackformat: html
124+
falsescoremode: =
125+
falsescore: 1
126+
falsepenalty:
127+
falsenextnode: -1
128+
falseanswernote: prt1-1-F
129+
falsefeedback:
130+
falsefeedbackformat: html
131+
qtest:
132+
- testcase: '1'
133+
description: 'A test'
134+
testinput:
135+
- name: 'ans1'
136+
value: 'ta1'
137+
- name: 'ans2'
138+
value: 'ta2'
139+
expected:
140+
- name: 'prt1'
141+
expectedscore: '1.0000000'
142+
expectedpenalty: '0.0000000'
143+
expectedanswernote: '1-0-T'
144+
- name: 'prt2'
145+
expectedscore: '1.0000000'
146+
expectedpenalty: '0.0000000'
147+
expectedanswernote: '2-0-T'

tests/yaml_converter_test.php

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,72 @@ public function test_loadxml(): void {
190190
$this->assertEquals('2-0-T', (string) $qtest->expected[1]->expectedanswernote);
191191
}
192192

193+
public function test_loadxml_summary_default(): void {
194+
if (!defined('Symfony\Component\Yaml\Yaml::DUMP_COMPACT_NESTED_MAPPING')) {
195+
$this->markTestSkipped('Symfony YAML extension is not available.');
196+
return;
197+
}
198+
$defaults = Yaml::parseFile(__DIR__ . '/fixtures/questiondefaultssugar.yml');
199+
$questionyaml = file_get_contents(__DIR__ . '/fixtures/fullquestion.yml');
200+
$xml = \qbank_gitsync\yaml_converter::loadyaml($questionyaml , $defaults);
201+
202+
// Check prt fields.
203+
$this->assertCount(2, $xml->question->prt);
204+
$prt1 = $xml->question->prt[0];
205+
$prt2 = $xml->question->prt[1];
206+
$this->assertCount(1, $prt1->node);
207+
$this->assertEquals('0', (string) $prt1->node[0]->name);
208+
$this->assertEquals('', (string) $prt1->node[0]->description);
209+
$this->assertEquals('AlgEquiv', (string) $prt1->node[0]->answertest);
210+
$this->assertEquals('ans1', (string) $prt1->node[0]->sans);
211+
$this->assertEquals('ta1', (string) $prt1->node[0]->tans);
212+
$this->assertEquals('', (string) $prt1->node[0]->testoptions);
213+
$this->assertEquals('1', (string) $prt1->node[0]->quiet);
214+
$this->assertEquals('=', (string) $prt1->node[0]->truescoremode);
215+
216+
$this->assertCount(1, $prt2->node);
217+
$this->assertEquals('0', (string) $prt2->node[0]->name);
218+
$this->assertEquals('AlgEquiv', (string) $prt2->node[0]->answertest);
219+
$this->assertEquals('ans2', (string) $prt2->node[0]->sans);
220+
$this->assertEquals('ta2', (string) $prt2->node[0]->tans);
221+
$this->assertEquals('0', (string) $prt2->node[0]->quiet);
222+
$this->assertEquals('1', (string) $prt2->node[0]->falsescore);
223+
}
224+
225+
226+
public function test_loadxml_summary(): void {
227+
if (!defined('Symfony\Component\Yaml\Yaml::DUMP_COMPACT_NESTED_MAPPING')) {
228+
$this->markTestSkipped('Symfony YAML extension is not available.');
229+
return;
230+
}
231+
$defaults = Yaml::parseFile(__DIR__ . '/fixtures/questiondefaultssugar.yml');
232+
$questionyaml = file_get_contents(__DIR__ . '/fixtures/fullquestionsummary.yml');
233+
$xml = \qbank_gitsync\yaml_converter::loadyaml($questionyaml , $defaults);
234+
235+
// Check prt fields.
236+
$this->assertCount(2, $xml->question->prt);
237+
$prt1 = $xml->question->prt[0];
238+
$prt2 = $xml->question->prt[1];
239+
$this->assertCount(1, $prt1->node);
240+
$this->assertEquals('0', (string) $prt1->node[0]->name);
241+
$this->assertEquals('', (string) $prt1->node[0]->description);
242+
$this->assertEquals('Diff', (string) $prt1->node[0]->answertest);
243+
$this->assertEquals('ans1', (string) $prt1->node[0]->sans);
244+
$this->assertEquals('diff(p,v)', (string) $prt1->node[0]->tans);
245+
$this->assertEquals('v', (string) $prt1->node[0]->testoptions);
246+
$this->assertEquals('1', (string) $prt1->node[0]->quiet);
247+
$this->assertEquals('=', (string) $prt1->node[0]->truescoremode);
248+
249+
$this->assertCount(1, $prt2->node);
250+
$this->assertEquals('0', (string) $prt2->node[0]->name);
251+
$this->assertEquals('AlgEquiv', (string) $prt2->node[0]->answertest);
252+
$this->assertEquals('ans2', (string) $prt2->node[0]->sans);
253+
$this->assertEquals('-rdm*sin(v)', (string) $prt2->node[0]->tans);
254+
$this->assertEquals('', (string) $prt2->node[0]->testoptions);
255+
$this->assertEquals('0', (string) $prt2->node[0]->quiet);
256+
$this->assertEquals('1', (string) $prt2->node[0]->falsescore);
257+
}
258+
193259
public function test_yaml_to_xml(): void {
194260
if (!defined('Symfony\Component\Yaml\Yaml::DUMP_COMPACT_NESTED_MAPPING')) {
195261
$this->markTestSkipped('Symfony YAML extension is not available.');
@@ -520,57 +586,57 @@ public function test_detect_difference_yml(): void {
520586
$this->assertEqualsCanonicalizing($expected, $diffarray);
521587
}
522588

523-
public function test_split_answertest_basic() {
589+
public function test_split_answertest_basic(): void {
524590
$input = 'ATAlgEquiv(x^2+2x+1, (x+1)^2, 1, ignoreorder)';
525591
$expected = [
526592
'ATAlgEquiv',
527593
'x^2+2x+1',
528594
'(x+1)^2',
529-
'1, ignoreorder'
595+
'1, ignoreorder',
530596
];
531597
$this->assertEquals($expected, yaml_converter::split_answertest($input));
532598
}
533599

534-
public function test_split_answertest_nested_parentheses() {
600+
public function test_split_answertest_nested_parentheses(): void {
535601
$input = 'ATTest(foo(bar, baz), qux, quux, corge)';
536602
$expected = [
537603
'ATTest',
538604
'foo(bar, baz)',
539605
'qux',
540-
'quux, corge'
606+
'quux, corge',
541607
];
542608
$this->assertEquals($expected, yaml_converter::split_answertest($input));
543609
}
544610

545-
public function test_split_answertest_missing_items() {
611+
public function test_split_answertest_missing_items(): void {
546612
$input = 'ATTest(foo)';
547613
$expected = [
548614
'ATTest',
549615
'foo',
550616
'',
551-
''
617+
'',
552618
];
553619
$this->assertEquals($expected, yaml_converter::split_answertest($input));
554620
}
555621

556-
public function test_split_answertest_extra_commas() {
622+
public function test_split_answertest_extra_commas(): void {
557623
$input = 'ATTest(foo, bar, baz, qux, quux)';
558624
$expected = [
559625
'ATTest',
560626
'foo',
561627
'bar',
562-
'baz, qux, quux'
628+
'baz, qux, quux',
563629
];
564630
$this->assertEquals($expected, yaml_converter::split_answertest($input));
565631
}
566632

567-
public function test_split_answertest_spaces() {
633+
public function test_split_answertest_spaces(): void {
568634
$input = 'ATTest( foo ( bar ), baz , qux )';
569635
$expected = [
570636
'ATTest',
571637
'foo ( bar )',
572638
'baz',
573-
'qux'
639+
'qux',
574640
];
575641
$this->assertEquals($expected, yaml_converter::split_answertest($input));
576642
}

0 commit comments

Comments
 (0)