@@ -32,6 +32,12 @@ class qtype_coderunner_combinator_grader_outcome extends qtype_coderunner_testin
32
32
/** @var ?string Html that is displayed after the result table. */
33
33
public $ prologuehtml ;
34
34
35
+ /** @var array A map from filename to feedback file contents, base64 */
36
+ public $ files ;
37
+
38
+ /** @var array A map from filename to the URLs of the saved files */
39
+ public $ fileurls ;
40
+
35
41
/** @var array A per-column array of %s (string) or %h (html) values to control column formatting */
36
42
public $ columnformats ;
37
43
@@ -50,14 +56,14 @@ class qtype_coderunner_combinator_grader_outcome extends qtype_coderunner_testin
50
56
/** @var array Array where each item is a rows of test result table */
51
57
public $ testresults ;
52
58
53
- /** @var ?string The feedback for a given question attempt */
59
+ /** @var ?string The feedback for a given question attempt (legacy support only) */
54
60
public $ feedbackhtml ;
55
61
56
62
/** @var bool Whether or no show differences is selected */
57
63
public $ showdifferences ;
58
64
59
65
// A list of the allowed attributes in the combinator template grader return value.
60
- public $ allowedfields = ['fraction ' , 'prologuehtml ' , 'testresults ' , 'epiloguehtml ' ,
66
+ public $ allowedfields = ['fraction ' , 'prologuehtml ' , 'testresults ' , 'files ' , ' epiloguehtml ' ,
61
67
'feedbackhtml ' , 'columnformats ' , 'showdifferences ' ,
62
68
'showoutputonly ' , 'graderstate ' , 'instructorhtml ' ,
63
69
];
@@ -74,20 +80,109 @@ public function __construct($isprecheck) {
74
80
}
75
81
76
82
83
+ /**
84
+ * Process all the files in $files, saving them to the Moodle file area
85
+ * and generating an URL to reference the saved file. The URLs are
86
+ * time-stamped to allow re-use of the same name over
87
+ * multiple submissions of a question.
88
+ * @param array $files An associate array mapping filenames to base64-encoded contents.
89
+ * @param array An associate array mapping filenames to URLs that reference that file.
90
+ */
91
+ private function save_files ($ files ) {
92
+ global $ USER ;
93
+
94
+ $ fileurls = [];
95
+ foreach ($ files as $ filename => $ base64 ) {
96
+ $ decoded = base64_decode ($ base64 );
97
+
98
+ // Prepare file record object
99
+ $ contextid = context_user::instance ($ USER ->id )->id ;
100
+ $ fileinfo = array (
101
+ 'contextid ' => $ contextid ,
102
+ 'component ' => 'qtype_coderunner ' ,
103
+ 'filearea ' => 'feedbackfiles ' , // Custom file area for your plugin
104
+ 'itemid ' => time (), // Item ID - use Unix time stamp
105
+ 'filepath ' => '/ ' , // File path within the context
106
+ 'filename ' => $ filename ); // Desired name of the file
107
+
108
+ // Get the file storage object
109
+ $ fs = get_file_storage ();
110
+
111
+ // Check if the file already exists to avoid duplicates
112
+ if (!$ fs ->file_exists ($ contextid , $ fileinfo ['component ' ], $ fileinfo ['filearea ' ],
113
+ $ fileinfo ['itemid ' ], $ fileinfo ['filepath ' ], $ fileinfo ['filename ' ])) {
114
+
115
+ // Create the file in Moodle's filesystem
116
+ $ file = $ fs ->create_file_from_string ($ fileinfo , $ decoded );
117
+ }
118
+
119
+ // Generate a URL to the saved file
120
+ $ url = moodle_url::make_pluginfile_url ($ contextid , $ fileinfo ['component ' ], $ fileinfo ['filearea ' ],
121
+ $ fileinfo ['itemid ' ], $ fileinfo ['filepath ' ], $ fileinfo ['filename ' ], false );
122
+
123
+ $ fileurls [$ filename ] = $ url ;
124
+ }
125
+ return $ fileurls ;
126
+ }
127
+
128
+
129
+ /**
130
+ * Replace any occurrences of substrings of the form src="filename" or
131
+ * href=filename within the given html string to reference the URL of
132
+ * the file, if the filename is found in $this->fileurls.
133
+ * No action is taken for non-matching filename.
134
+ * @param string html The html string to be updates.
135
+ * @param array $urls An associative array mapping from filenames to URLs
136
+ * @return string The html string with the above-described substitutions made.
137
+ */
138
+ private function insert_file_urls ($ html , $ urls ) {
139
+ if ($ urls ) {
140
+ foreach ($ urls as $ filename => $ url ) {
141
+ $ filename = preg_quote ($ filename , '/ ' );
142
+ $ patterns = [
143
+ "src *= *' $ filename' " ,
144
+ "src *= * \"$ filename \"" ,
145
+ "href *= *' $ filename' " ,
146
+ "href *= * \"$ filename \""
147
+ ];
148
+ foreach ($ patterns as $ pat ) {
149
+ if (strpos ($ pat , 'src ' ) !== false ) {
150
+ $ html = preg_replace ("/ $ pat/ " , "src= \"$ url \"" , $ html );
151
+ } else {
152
+ $ html = preg_replace ("/ $ pat/ " , "href= \"$ url \"" , $ html );
153
+ }
154
+ }
155
+ }
156
+ }
157
+ return $ html ; // return the modified HTML
158
+ }
159
+
160
+
77
161
/**
78
162
* Method to set the mark and the various feedback values (prologuehtml,
79
163
* testresults, columnformats, epiloguehtml, graderstate).
80
164
* @param float $markfraction the mark in the range 0 - 1
81
- * @param array $feedback Associative array of attributes to add to the
82
- * outcome object, usually zero or more of prologuehtml, testresults,
83
- * columnformats and epiloguehtml.
165
+ * @param array $feedback Associative array of additional attributes as
166
+ * listed in the $this->allowedfields.
84
167
*/
85
168
public function set_mark_and_feedback ($ markfraction , $ feedback ) {
86
169
$ this ->actualmark = $ markfraction ; // Combinators work in the range 0 - 1.
87
- foreach ($ feedback as $ key => $ value ) {
88
- $ this ->$ key = $ value ;
170
+ $ columnformats = $ feedback ['columnformats ' ] ?? null ;
171
+ $ testresults = $ feedback ['testresults ' ] ?? null ;
172
+ $ files = $ feedback ['files ' ] ?? null ;
173
+ $ urls = null ;
174
+ if ($ this ->valid_table_formats ($ testresults , $ columnformats )) {
175
+ if ($ files ) {
176
+ $ urls = $ this ->save_files ($ files );
177
+ $ htmlfields = ['feedbackhtml ' , 'prologuehtml ' , 'epiloguehtml ' , 'instructorhtml ' ];
178
+ foreach ($ htmlfields as $ field ) {
179
+ if ($ this ->$ field ) {
180
+ $ this ->$ field = $ this ->insert_file_urls ($ this ->$ field , $ urls );
181
+ }
182
+ };
183
+ }
184
+ $ this ->format_results_table ($ testresults , $ columnformats , $ urls );
89
185
}
90
- $ this ->validate_table_formats ();
91
186
}
92
187
93
188
@@ -253,29 +348,33 @@ public function validation_error_message() {
253
348
*/
254
349
public function get_test_results (qtype_coderunner_question $ q ) {
255
350
if (empty ($ this ->testresults ) || self ::can_view_hidden ()) {
256
- return $ this ->format_table ( $ this -> testresults ) ;
351
+ return $ this ->testresults ;
257
352
} else {
258
- return $ this -> format_table ( $ this -> visible_rows ($ this ->testresults ) );
353
+ return self :: visible_rows ($ this ->testresults );
259
354
}
260
355
}
261
356
262
- // Function to apply the formatting specified in $this->columnformats
263
- // to the given table. This simply wraps cells in columns with a '%h' format
264
- // specifier in html_wrapper objects leaving other cells unchanged.
265
- // ishidden and iscorrect columns are copied across unchanged.
266
- private function format_table ($ table ) {
267
- if (empty ($ table )) {
268
- return $ table ;
269
- }
270
- if (!$ this ->columnformats ) {
271
- $ newtable = $ table ;
357
+ /**
358
+ * Build the testresults table from the initial given testresults by
359
+ * applying all the column formats. Cells with '%h' format are first
360
+ * processed to replace any src or href assignments with references to
361
+ * the $urls of the saved files. Then the cells are wrapped in
362
+ * html_wrapper objects. All other cells are unchanged.
363
+ * ishidden and iscorrect columns are copied across unchanged.
364
+ * @param array $testresults The 'raw' $testresults from the run.
365
+ * @param array $columnformats The array of '%s' or '%h' column formats
366
+ * @param array $urls. The map from filename to URL
367
+ */
368
+ private function format_results_table ($ testresults , $ columnformats , $ urls ) {
369
+ if (!$ testresults || !$ columnformats ) {
370
+ $ this ->testresults = $ testresults ;
272
371
} else {
273
- $ formats = $ this -> columnformats ;
274
- $ columnheaders = $ table [0 ];
372
+ $ formats = $ columnformats ;
373
+ $ columnheaders = $ testresults [0 ];
275
374
$ newtable = [$ columnheaders ];
276
- $ nrows = count ($ table );
375
+ $ nrows = count ($ testresults );
277
376
for ($ i = 1 ; $ i < $ nrows ; $ i ++) {
278
- $ row = $ table [$ i ];
377
+ $ row = $ testresults [$ i ];
279
378
$ newrow = [];
280
379
$ formatindex = 0 ;
281
380
$ ncols = count ($ row );
@@ -284,8 +383,10 @@ private function format_table($table) {
284
383
if (in_array ($ columnheaders [$ col ], ['ishidden ' , 'iscorrect ' ])) {
285
384
$ newrow [] = $ cell ; // Copy control column values directly.
286
385
} else {
386
+
287
387
// Non-control columns are either '%s' or '%h' format.
288
388
if ($ formats [$ formatindex ++] === '%h ' ) {
389
+ $ cell = $ this ->insert_file_urls ($ cell , $ urls );
289
390
$ newrow [] = new qtype_coderunner_html_wrapper ($ cell );
290
391
} else {
291
392
$ newrow [] = $ cell ;
@@ -295,7 +396,7 @@ private function format_table($table) {
295
396
$ newtable [] = $ newrow ;
296
397
}
297
398
}
298
- return $ newtable ;
399
+ $ this -> testresults = $ newtable ;
299
400
}
300
401
301
402
public function get_prologue () {
@@ -326,45 +427,65 @@ public function get_grader_state() {
326
427
}
327
428
328
429
329
- // Check that if a columnformats field is supplied
330
- // the number of entries is correct and that each entry is either '%s'
331
- // or '%h'. If not, an appropriate status error message is set.
332
- private function validate_table_formats () {
333
- if ($ this ->columnformats && $ this ->testresults ) {
430
+ /**
431
+ * Check if the given values of column formats and test results are
432
+ * valid in the sense that the number of entries in column formats
433
+ * matches the number of columns in the table and that each column
434
+ * foramt is either '%s'
435
+ * or '%h'. If not, an appropriate status error message is set and
436
+ * the return value is false. Otherwise the return value is true.
437
+ * @param array $testresults An array of test results, where the
438
+ * first row is the column headers and the remaining rows are the
439
+ * results of each test.
440
+ * @param array $columnformats An array of strings specifying the
441
+ * column formats, each either %s or %h.
442
+ * @return bool True if either of $columnformats or $testresults is
443
+ * null or empty or if all column formats are valid. Otherwise
444
+ * return false, and set an error status.
445
+ */
446
+
447
+ private static function valid_table_formats ($ testresults ,$ columnformats , ) {
448
+ $ ok = true ;
449
+ if ($ columnformats && $ testresults ) {
334
450
$ numcols = 0 ;
335
- foreach ($ this -> testresults [0 ] as $ colhdr ) {
451
+ foreach ($ testresults [0 ] as $ colhdr ) {
336
452
// Count columns in header, excluding iscorrect and ishidden.
337
453
if ($ colhdr !== 'iscorrect ' && $ colhdr !== 'ishidden ' ) {
338
454
$ numcols += 1 ;
339
455
}
340
456
}
341
- if (count ($ this -> columnformats ) !== $ numcols ) {
457
+ if (count ($ columnformats ) !== $ numcols ) {
342
458
$ error = get_string (
343
459
'wrongnumberofformats ' ,
344
460
'qtype_coderunner ' ,
345
- ['expected ' => $ numcols , 'got ' => count ($ this -> columnformats )]
461
+ ['expected ' => $ numcols , 'got ' => count ($ columnformats )]
346
462
);
347
463
$ this ->set_status (self ::STATUS_BAD_COMBINATOR , $ error );
464
+ $ ok = false ;
348
465
} else {
349
- foreach ($ this -> columnformats as $ format ) {
466
+ foreach ($ columnformats as $ format ) {
350
467
if ($ format !== '%s ' && $ format !== '%h ' ) {
351
468
$ error = get_string (
352
469
'illegalformat ' ,
353
470
'qtype_coderunner ' ,
354
471
['format ' => $ format ]
355
472
);
356
473
$ this ->set_status (self ::STATUS_BAD_COMBINATOR , $ error );
474
+ $ ok = false ;
357
475
break ;
358
476
}
359
477
}
360
478
}
361
479
}
480
+ return $ ok ;
362
481
}
363
482
364
483
365
- // Private method to filter result table so only visible rows are shown
366
- // to students. Only called if the user is not allowed to see hidden rows
367
- // And if there is a non-null non-empty resulttable.
484
+ /**
485
+ * Filter the given result table to return only the visible rows.
486
+ * Only called if the user is not allowed to see hidden rows
487
+ * And if there is a non-null non-empty resulttable.
488
+ * */
368
489
private static function visible_rows ($ resulttable ) {
369
490
$ header = $ resulttable [0 ];
370
491
$ ishiddencolumn = -1 ;
0 commit comments