Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle 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
// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
use qtype_ordering\question_hint_ordering;
18
 
19
/**
20
 * The ordering question type.
21
 *
22
 * @package    qtype_ordering
23
 * @copyright  2013 Gordon Bateson (gordon.bateson@gmail.com)
24
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25
 */
26
class qtype_ordering extends question_type {
27
 
28
    /** @var int Number of hints default. */
29
    const DEFAULT_NUM_HINTS = 2;
30
 
11 efrain 31
    /**
32
     * Determine if the question type can have HTML answers.
33
     *
34
     * @return bool
35
     * @codeCoverageIgnore
36
     */
1 efrain 37
    public function has_html_answers(): bool {
38
        return true;
39
    }
40
 
41
    public function extra_question_fields(): array {
42
        return [
43
            'qtype_ordering_options',
44
            'layouttype', 'selecttype', 'selectcount',
45
            'gradingtype', 'showgrading', 'numberingstyle',
46
        ];
47
    }
48
 
49
    protected function initialise_question_instance(question_definition $question, $questiondata): void {
50
        global $CFG;
51
 
52
        parent::initialise_question_instance($question, $questiondata);
53
 
54
        $question->answers = $questiondata->options->answers;
55
        foreach ($question->answers as $answerid => $answer) {
56
            $question->answers[$answerid]->md5key = 'ordering_item_' . md5(($CFG->passwordsaltmain ?? '') . $answer->answer);
57
        }
58
 
59
        $this->initialise_combined_feedback($question, $questiondata, true);
60
    }
61
 
62
    public function save_defaults_for_new_questions(stdClass $fromform): void {
63
        parent::save_defaults_for_new_questions($fromform);
64
        $this->set_default_value('layouttype', $fromform->layouttype);
65
        $this->set_default_value('selecttype', $fromform->selecttype);
66
        $this->set_default_value('selectcount', $fromform->selectcount);
67
        $this->set_default_value('gradingtype', $fromform->gradingtype);
68
        $this->set_default_value('showgrading', $fromform->showgrading);
69
        $this->set_default_value('numberingstyle', $fromform->numberingstyle);
70
    }
71
 
72
    public function save_question_options($question): bool|stdClass {
73
        global $DB;
74
 
75
        $result = new stdClass();
76
        $context = $question->context;
77
 
78
        // Remove empty answers.
79
        $question->answer = array_filter($question->answer, [$this, 'is_not_blank']);
80
        $question->answer = array_values($question->answer); // Make keys sequential.
81
 
82
        // Count how many answers we have.
83
        $countanswers = count($question->answer);
84
 
85
        // Search/replace strings to reduce simple <p>...</p> to plain text.
86
        $psearch = '/^\s*<p>\s*(.*?)(\s*<br\s*\/?>)*\s*<\/p>\s*$/';
87
        $preplace = '$1';
88
 
89
        // Search/replace strings to standardize vertical align of <img> tags.
90
        $imgsearch = '/(<img[^>]*)\bvertical-align:\s*[a-zA-Z0-9_-]+([^>]*>)/';
91
        $imgreplace = '$1'.'vertical-align:text-top'.'$2';
92
 
93
        // Check at least two answers exist.
94
        if ($countanswers < 2) {
95
            $result->notice = get_string('notenoughanswers', 'qtype_ordering', '2');
96
            return $result;
97
        }
98
 
99
        $question->feedback = range(1, $countanswers);
100
 
101
        if ($answerids = $DB->get_records('question_answers', ['question' => $question->id], 'id ASC', 'id,question')) {
102
            $answerids = array_keys($answerids);
103
        } else {
104
            $answerids = [];
105
        }
106
 
107
        // Insert all the new answers.
108
        foreach ($question->answer as $i => $answer) {
109
            $answertext = '';
110
            $answerformat = 0;
111
            $answeritemid = null;
112
 
113
            // Extract $answer fields.
114
            if (is_string($answer)) {
115
                // Import from file.
116
                $answertext = $answer;
117
            } else if (is_array($answer)) {
118
                // Input from browser.
119
                if (isset($answer['text'])) {
120
                    $answertext = $answer['text'];
121
                }
122
                if (isset($answer['format'])) {
123
                    $answerformat = $answer['format'];
124
                }
125
                if (isset($answer['itemid'])) {
126
                    $answeritemid = $answer['itemid'];
127
                }
128
            }
129
 
130
            // Reduce simple <p>...</p> to plain text.
131
            if (substr_count($answertext, '<p>') == 1) {
132
                $answertext = preg_replace($psearch, $preplace, $answertext);
133
            }
134
            $answertext = trim($answertext);
135
 
136
            // Skip empty answers.
137
            if ($answertext == '') {
138
                continue;
139
            }
140
 
141
            // Standardize vertical align of img tags.
142
            $answertext = preg_replace($imgsearch, $imgreplace, $answertext);
143
 
144
            // Prepare the $answer object.
145
            $answer = (object) [
146
                'question' => $question->id,
147
                'fraction' => ($i + 1), // Start at 1.
148
                'answer' => $answertext,
149
                'answerformat' => $answerformat,
150
                'feedback' => '',
151
                'feedbackformat' => FORMAT_MOODLE,
152
            ];
153
 
154
            // Add/insert $answer into the database.
155
            if ($answer->id = array_shift($answerids)) {
156
                if (!$DB->update_record('question_answers', $answer)) {
157
                    $result->error = get_string('cannotupdaterecord', 'error', 'question_answers (id='.$answer->id.')');
158
                    return $result;
159
                }
160
            } else {
161
                unset($answer->id);
162
                if (!$answer->id = $DB->insert_record('question_answers', $answer)) {
163
                    $result->error = get_string('cannotinsertrecord', 'error', 'question_answers');
164
                    return $result;
165
                }
166
            }
167
 
168
            // Copy files across from draft files area.
169
            // Note: we must do this AFTER inserting the answer record
170
            // because the answer id is used as the file's "itemid".
171
            if ($answeritemid) {
172
                $answertext = file_save_draft_area_files($answeritemid, $context->id, 'question', 'answer', $answer->id,
173
                    $this->fileoptions, $answertext);
174
                $DB->set_field('question_answers', 'answer', $answertext, ['id' => $answer->id]);
175
            }
176
        }
177
        // Create $options for this ordering question.
178
        $options = (object) [
179
            'questionid' => $question->id,
180
            'layouttype' => $question->layouttype,
181
            'selecttype' => $question->selecttype,
182
            'selectcount' => $question->selectcount,
183
            'gradingtype' => $question->gradingtype,
184
            'showgrading' => $question->showgrading,
185
            'numberingstyle' => $question->numberingstyle,
186
        ];
187
        $options = $this->save_combined_feedback_helper($options, $question, $context, true);
188
        $this->save_hints($question, true);
189
 
190
        // Add/update $options for this ordering question.
191
        if ($options->id = $DB->get_field('qtype_ordering_options', 'id', ['questionid' => $question->id])) {
192
            if (!$DB->update_record('qtype_ordering_options', $options)) {
193
                $result->error = get_string('cannotupdaterecord', 'error', 'qtype_ordering_options (id='.$options->id.')');
194
                return $result;
195
            }
196
        } else {
197
            unset($options->id);
198
            if (!$options->id = $DB->insert_record('qtype_ordering_options', $options)) {
199
                $result->error = get_string('cannotinsertrecord', 'error', 'qtype_ordering_options');
200
                return $result;
201
            }
202
        }
203
 
204
        // Delete old answer records, if any.
205
        if (count($answerids)) {
206
            $fs = get_file_storage();
207
            foreach ($answerids as $answerid) {
208
                $fs->delete_area_files($context->id, 'question', 'answer', $answerid);
209
                $DB->delete_records('question_answers', ['id' => $answerid]);
210
            }
211
        }
212
 
213
        return true;
214
    }
215
 
216
    protected function count_hints_on_form($formdata, $withparts): int {
217
        $numhints = parent::count_hints_on_form($formdata, $withparts);
218
 
219
        if (!empty($formdata->hintoptions)) {
220
            $numhints = max($numhints, max(array_keys($formdata->hintoptions)) + 1);
221
        }
222
 
223
        return $numhints;
224
    }
225
 
226
    protected function is_hint_empty_in_form_data($formdata, $number, $withparts): bool {
227
        return parent::is_hint_empty_in_form_data($formdata, $number, $withparts) &&
228
            empty($formdata->hintoptions[$number]);
229
    }
230
 
231
    protected function save_hint_options($formdata, $number, $withparts): bool {
232
        return !empty($formdata->hintoptions[$number]);
233
    }
234
 
235
    protected function make_hint($hint): question_hint_ordering {
236
        return question_hint_ordering::load_from_record($hint);
237
    }
238
 
239
    public function get_possible_responses($questiondata): array {
240
        $responseclasses = [];
241
        $itemcount = count($questiondata->options->answers);
242
 
243
        $position = 0;
244
        foreach ($questiondata->options->answers as $answer) {
245
            $position += 1;
246
            $classes = [];
247
            for ($i = 1; $i <= $itemcount; $i++) {
248
                $classes[$i] = new question_possible_response(
249
                    get_string('positionx', 'qtype_ordering', $i),
250
                    ($i === $position) / $itemcount);
251
            }
252
 
253
            $subqid = question_utils::to_plain_text($answer->answer, $answer->answerformat);
254
            $subqid = core_text::substr($subqid, 0, 100); // Ensure not more than 100 chars.
255
            $responseclasses[$subqid] = $classes;
256
        }
257
 
258
        return $responseclasses;
259
    }
260
 
261
    /**
262
     * Callback function for filtering answers with array_filter
263
     *
264
     * @param mixed $value
265
     * @return bool If true, this item should be saved.
266
     */
267
    public function is_not_blank(mixed $value): bool {
268
        if (is_array($value)) {
269
            $value = $value['text'];
270
        }
271
        $value = trim($value);
272
        return ($value || $value === '0');
273
    }
274
 
275
    public function get_question_options($question): bool {
276
        global $DB, $OUTPUT;
277
 
278
        // Load the options.
279
        if (!$question->options = $DB->get_record('qtype_ordering_options', ['questionid' => $question->id])) {
280
            echo $OUTPUT->notification('Error: Missing question options!');
281
            return false;
282
        }
283
 
284
        // Load the answers - "fraction" is used to signify the order of the answers,
285
        // with id as a tie-break which should not be required.
286
        if (!$question->options->answers = $DB->get_records('question_answers',
287
                ['question' => $question->id], 'fraction, id')) {
288
            echo $OUTPUT->notification('Error: Missing question answers for ordering question ' . $question->id . '!');
289
            return false;
290
        }
291
 
292
        parent::get_question_options($question);
293
        return true;
294
    }
295
 
296
    public function delete_question($questionid, $contextid): void {
297
        global $DB;
298
        $DB->delete_records('qtype_ordering_options', ['questionid' => $questionid]);
299
        parent::delete_question($questionid, $contextid);
300
    }
301
 
302
    /**
303
     * Import question from GIFT format
304
     *
305
     * @param array $lines
306
     * @param stdClass|null $question
307
     * @param qformat_gift $format
308
     * @param string|null $extra (optional, default=null)
309
     * @return stdClass|bool Question instance
310
     */
311
    public function import_from_gift(array $lines, ?stdClass $question, qformat_gift $format, string $extra = null): bool|stdClass {
312
        global $CFG;
313
        require_once($CFG->dirroot.'/question/type/ordering/question.php');
314
 
315
        // Extract question info from GIFT file $lines.
316
        $selectcount = '\d+';
317
        $selecttype  = '(?:ALL|EXACT|'.
318
            'RANDOM|REL|'.
319
            'CONTIGUOUS|CONTIG)?';
320
        $layouttype  = '(?:HORIZONTAL|HORI|H|1|'.
321
            'VERTICAL|VERT|V|0)?';
322
        $gradingtype = '(?:ALL_OR_NOTHING|'.
323
            'ABSOLUTE_POSITION|'.
324
            'ABSOLUTE|ABS|'.
325
            'RELATIVE_NEXT_EXCLUDE_LAST|'.
326
            'RELATIVE_NEXT_INCLUDE_LAST|'.
327
            'RELATIVE_ONE_PREVIOUS_AND_NEXT|'.
328
            'RELATIVE_ALL_PREVIOUS_AND_NEXT|'.
329
            'RELATIVE_TO_CORRECT|'.
330
            'RELATIVE|REL'.
331
            'LONGEST_ORDERED_SUBSET|'.
332
            'LONGEST_CONTIGUOUS_SUBSET)?';
333
        $showgrading = '(?:SHOW|TRUE|YES|1|HIDE|FALSE|NO|0)?';
334
        $numberingstyle = '(?:none|123|abc|ABCD|iii|IIII)?';
335
        $search = '/^\s*>\s*('.$selectcount.')\s*'.
336
            '('.$selecttype.')\s*'.
337
            '('.$layouttype.')\s*'.
338
            '('.$gradingtype.')\s*'.
339
            '('.$showgrading.')\s*'.
340
            '('.$numberingstyle.')\s*'.
341
            '(.*?)\s*$/s';
342
        // Item $1 the number of items to be shown.
343
        // Item $2 the extraction/grading type.
344
        // Item $3 the layout type.
345
        // Item $4 the grading type.
346
        // Item $5 show the grading details (SHOW/HIDE).
347
        // Item $6 the numbering style (none/123/abc/...).
348
        // Item $7 the lines of items to be ordered.
349
        if (!$extra) {
350
            return false; // Format not recognized.
351
        }
352
        if (!preg_match($search, $extra, $matches)) {
353
            return false; // Format not recognized.
354
        }
355
 
356
        $selectcount = trim($matches[1]);
357
        $selecttype = trim($matches[2]);
358
        $layouttype = trim($matches[3]);
359
        $gradingtype = trim($matches[4]);
360
        $showgrading = trim($matches[5]);
361
        $numberingstyle = trim($matches[6]);
362
 
363
        $answers = preg_split('/[\r\n]+/', $matches[7]);
364
        $answers = array_filter($answers);
365
 
366
        if (empty($question)) {
367
            $text = implode(PHP_EOL, $lines);
368
            $text = trim($text);
369
            if ($pos = strpos($text, '{')) {
370
                $text = substr($text, 0, $pos);
371
            }
372
 
373
            // Extract name.
374
            $name = false;
375
            if (str_starts_with($text, '::')) {
376
                $text = substr($text, 2);
377
                $pos = strpos($text, '::');
378
                if (is_numeric($pos)) {
379
                    $name = substr($text, 0, $pos);
380
                    $name = $format->clean_question_name($name);
381
                    $text = trim(substr($text, $pos + 2));
382
                }
383
            }
384
 
385
            // Extract question text format.
386
            $format = FORMAT_MOODLE;
387
            if (str_starts_with($text, '[')) {
388
                $text = substr($text, 1);
389
                $pos = strpos($text, ']');
390
                if (is_numeric($pos)) {
391
                    $format = substr($text, 0, $pos);
392
                    switch ($format) {
393
                        case 'html':
394
                            $format = FORMAT_HTML;
395
                            break;
396
                        case 'plain':
397
                            $format = FORMAT_PLAIN;
398
                            break;
399
                        case 'markdown':
400
                            $format = FORMAT_MARKDOWN;
401
                            break;
402
                        case 'moodle':
403
                            $format = FORMAT_MOODLE;
404
                            break;
405
                    }
406
                    $text = trim(substr($text, $pos + 1)); // Remove name from text.
407
                }
408
            }
409
 
410
            $question = new stdClass();
411
            $question->name = $name;
412
            $question->questiontext = $text;
413
            $question->questiontextformat = $format;
414
            $question->generalfeedback = '';
415
            $question->generalfeedbackformat = FORMAT_MOODLE;
416
        }
417
 
418
        $question->qtype = 'ordering';
419
 
420
        // Set "selectcount" field from $selectcount.
421
        if (is_numeric($selectcount) && $selectcount >= qtype_ordering_question::MIN_SUBSET_ITEMS &&
422
                $selectcount <= count($answers)) {
423
            $selectcount = intval($selectcount);
424
        } else {
425
            $selectcount = min(6, count($answers));
426
        }
427
        $this->set_options_for_import($question, $layouttype, $selecttype, $selectcount,
428
            $gradingtype, $showgrading, $numberingstyle);
429
 
430
        // Remove blank items.
431
        $answers = array_map('trim', $answers);
432
        $answers = array_filter($answers); // Remove blanks.
433
 
434
        // Set up answer arrays.
435
        $question->answer = [];
436
        $question->answerformat = [];
437
        $question->fraction = [];
438
        $question->feedback = [];
439
        $question->feedbackformat = [];
440
 
441
        // Note that "fraction" field is used to denote sort order
442
        // "fraction" fields will be set to correct values later
443
        // in the save_question_options() method of this class.
444
 
445
        foreach ($answers as $i => $answer) {
446
            $question->answer[$i] = $answer;
447
            $question->answerformat[$i] = FORMAT_MOODLE;
448
            $question->fraction[$i] = 1; // Will be reset later in save_question_options().
449
            $question->feedback[$i] = '';
450
            $question->feedbackformat[$i] = FORMAT_MOODLE;
451
        }
452
 
453
        return $question;
454
    }
455
 
456
    /**
457
     * Given question object, returns array with array layouttype, selecttype, selectcount, gradingtype, showgrading
458
     * where layouttype, selecttype, gradingtype and showgrading are string representations.
459
     *
460
     * @param stdClass $question
461
     * @return array(layouttype, selecttype, selectcount, gradingtype, $showgrading, $numberingstyle)
462
     */
463
    public function extract_options_for_export(stdClass $question): array {
464
 
465
        $layouttype = match ($question->options->layouttype) {
466
            qtype_ordering_question::LAYOUT_VERTICAL => 'VERTICAL',
467
            qtype_ordering_question::LAYOUT_HORIZONTAL => 'HORIZONTAL',
468
            default => '', // Shouldn't happen !!
469
        };
470
 
471
        $selecttype = match ($question->options->selecttype) {
472
            qtype_ordering_question::SELECT_ALL => 'ALL',
473
            qtype_ordering_question::SELECT_RANDOM => 'RANDOM',
474
            qtype_ordering_question::SELECT_CONTIGUOUS => 'CONTIGUOUS',
475
            default => '', // Shouldn't happen !!
476
        };
477
 
478
        $gradingtype = match ($question->options->gradingtype) {
479
            qtype_ordering_question::GRADING_ALL_OR_NOTHING => 'ALL_OR_NOTHING',
480
            qtype_ordering_question::GRADING_ABSOLUTE_POSITION => 'ABSOLUTE_POSITION',
481
            qtype_ordering_question::GRADING_RELATIVE_NEXT_EXCLUDE_LAST => 'RELATIVE_NEXT_EXCLUDE_LAST',
482
            qtype_ordering_question::GRADING_RELATIVE_NEXT_INCLUDE_LAST => 'RELATIVE_NEXT_INCLUDE_LAST',
483
            qtype_ordering_question::GRADING_RELATIVE_ONE_PREVIOUS_AND_NEXT => 'RELATIVE_ONE_PREVIOUS_AND_NEXT',
484
            qtype_ordering_question::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT => 'RELATIVE_ALL_PREVIOUS_AND_NEXT',
485
            qtype_ordering_question::GRADING_LONGEST_ORDERED_SUBSET => 'LONGEST_ORDERED_SUBSET',
486
            qtype_ordering_question::GRADING_LONGEST_CONTIGUOUS_SUBSET => 'LONGEST_CONTIGUOUS_SUBSET',
487
            qtype_ordering_question::GRADING_RELATIVE_TO_CORRECT => 'RELATIVE_TO_CORRECT',
488
            default => '', // Shouldn't happen !!
489
        };
490
 
491
        $showgrading = match ($question->options->showgrading) {
492
 
493
            1 => 'SHOW',
494
            default => '', // Shouldn't happen !!
495
        };
496
 
497
        if (empty($question->options->numberingstyle)) {
498
            $numberingstyle = qtype_ordering_question::NUMBERING_STYLE_DEFAULT;
499
        } else {
500
            $numberingstyle = $question->options->numberingstyle;
501
        }
502
 
503
        // Note: this used to be (selectcount + 2).
504
        $selectcount = $question->options->selectcount;
505
 
506
        return [$layouttype, $selecttype, $selectcount, $gradingtype, $showgrading, $numberingstyle];
507
    }
508
 
509
    /**
510
     * Exports question to GIFT format
511
     *
512
     * @param stdClass $question
513
     * @param qformat_gift $format
514
     * @param string|null $extra (optional, default=null)
515
     * @return string GIFT representation of question
516
     */
517
    public function export_to_gift(stdClass $question, qformat_gift $format, string $extra = null): string {
518
        global $CFG;
519
        require_once($CFG->dirroot.'/question/type/ordering/question.php');
520
 
521
        $output = '';
522
 
523
        if ($question->name) {
524
            $output .= '::'.$question->name.'::';
525
        }
526
 
527
        $output .= match ($question->questiontextformat) {
528
            FORMAT_HTML => '[html]',
529
            FORMAT_PLAIN => '[plain]',
530
            FORMAT_MARKDOWN => '[markdown]',
531
            FORMAT_MOODLE => '[moodle]',
532
            default => '',
533
        };
534
 
535
        $output .= $question->questiontext.'{';
536
 
537
        list($layouttype, $selecttype, $selectcount, $gradingtype, $showgrading, $numberingstyle) =
538
            $this->extract_options_for_export($question);
539
        $output .= ">$selectcount $selecttype $layouttype $gradingtype $showgrading $numberingstyle".PHP_EOL;
540
 
541
        foreach ($question->options->answers as $answer) {
542
            $output .= $answer->answer.PHP_EOL;
543
        }
544
 
545
        $output .= '}';
546
        return $output;
547
    }
548
 
549
    public function export_to_xml($question, qformat_xml $format, $extra = null): string {
550
        global $CFG;
551
        require_once($CFG->dirroot.'/question/type/ordering/question.php');
552
 
553
        list($layouttype, $selecttype, $selectcount, $gradingtype, $showgrading, $numberingstyle) =
554
            $this->extract_options_for_export($question);
555
 
556
        $output = '';
557
        $output .= "    <layouttype>$layouttype</layouttype>\n";
558
        $output .= "    <selecttype>$selecttype</selecttype>\n";
559
        $output .= "    <selectcount>$selectcount</selectcount>\n";
560
        $output .= "    <gradingtype>$gradingtype</gradingtype>\n";
561
        $output .= "    <showgrading>$showgrading</showgrading>\n";
562
        $output .= "    <numberingstyle>$numberingstyle</numberingstyle>\n";
563
        $output .= $format->write_combined_feedback($question->options, $question->id, $question->contextid);
564
 
565
        $shownumcorrect = $question->options->shownumcorrect;
566
        if (!empty($question->options->shownumcorrect)) {
567
            $output = str_replace("    <shownumcorrect/>\n", "", $output);
568
        }
569
        $output .= "    <shownumcorrect>$shownumcorrect</shownumcorrect>\n";
570
 
571
        foreach ($question->options->answers as $answer) {
572
            $output .= '    <answer fraction="'.$answer->fraction.'" '.$format->format($answer->answerformat).">\n";
573
            $output .= $format->writetext($answer->answer, 3);
574
            if (trim($answer->feedback)) { // Usually there is no feedback.
575
                $output .= '      <feedback '.$format->format($answer->feedbackformat).">\n";
576
                $output .= $format->writetext($answer->feedback, 4);
577
                $output .= $format->write_files($answer->feedbackfiles);
578
                $output .= "      </feedback>\n";
579
            }
580
            $output .= "    </answer>\n";
581
        }
582
 
583
        return $output;
584
    }
585
 
586
    public function import_from_xml($data, $question, qformat_xml $format, $extra = null): object|bool {
587
        global $CFG;
588
        require_once($CFG->dirroot.'/question/type/ordering/question.php');
589
 
590
        $questiontype = $format->getpath($data, ['@', 'type'], '');
591
 
592
        if ($questiontype != 'ordering') {
593
            return false;
594
        }
595
 
596
        $newquestion = $format->import_headers($data);
597
        $newquestion->qtype = $questiontype;
598
 
599
        // Extra fields - "selecttype" and "selectcount"
600
        // (these fields used to be called "logical" and "studentsee").
601
        if (isset($data['#']['selecttype'])) {
602
            $selecttype = 'selecttype';
603
            $selectcount = 'selectcount';
604
        } else {
605
            $selecttype = 'logical';
606
            $selectcount = 'studentsee';
607
        }
608
        $layouttype = $format->getpath($data, ['#', 'layouttype', 0, '#'], 'VERTICAL');
609
        $selecttype = $format->getpath($data, ['#', $selecttype, 0, '#'], 'RANDOM');
610
        $selectcount = $format->getpath($data, ['#', $selectcount, 0, '#'], 6);
611
        $gradingtype = $format->getpath($data, ['#', 'gradingtype', 0, '#'], 'RELATIVE');
612
        $showgrading = $format->getpath($data, ['#', 'showgrading', 0, '#'], '1');
613
        $numberingstyle = $format->getpath($data, ['#', 'numberingstyle', 0, '#'], '1');
614
        $this->set_options_for_import($newquestion, $layouttype, $selecttype, $selectcount,
615
            $gradingtype, $showgrading, $numberingstyle);
616
 
617
        $newquestion->answer = [];
618
        $newquestion->answerformat = [];
619
        $newquestion->fraction = [];
620
        $newquestion->feedback = [];
621
        $newquestion->feedbackformat = [];
622
 
623
        $i = 0;
624
        while ($answer = $format->getpath($data, ['#', 'answer', $i], '')) {
625
            $ans = $format->import_answer($answer, true, $format->get_format($newquestion->questiontextformat));
626
            $newquestion->answer[$i] = $ans->answer;
627
            $newquestion->fraction[$i] = 1; // Will be reset later in save_question_options().
628
            $newquestion->feedback[$i] = $ans->feedback;
629
            $i++;
630
        }
631
 
632
        $format->import_combined_feedback($newquestion, $data);
633
        $newquestion->shownumcorrect = $format->getpath($data, ['#', 'shownumcorrect', 0, '#'], null);
634
 
635
        $format->import_hints($newquestion, $data, true, true);
636
 
637
        if (!isset($newquestion->shownumcorrect)) {
638
            $newquestion->shownumcorrect = 1;
639
            $counthintshownumcorrect = self::DEFAULT_NUM_HINTS;
640
            $counthintoptions = self::DEFAULT_NUM_HINTS;
641
 
642
            if (isset($newquestion->hintshownumcorrect)) {
643
                $counthintshownumcorrect = max(self::DEFAULT_NUM_HINTS, count($newquestion->hintshownumcorrect));
644
            }
645
 
646
            if (isset($newquestion->hintoptions)) {
647
                $counthintoptions = max(self::DEFAULT_NUM_HINTS, count($newquestion->hintoptions));
648
            }
649
 
650
            $newquestion->hintshownumcorrect  = array_fill(0, $counthintshownumcorrect, 1);
651
            $newquestion->hintoptions  = array_fill(0, $counthintoptions, 1);
652
        }
653
 
654
        return $newquestion;
655
    }
656
 
657
    /**
658
     * Set layouttype, selecttype, selectcount, gradingtype, showgrading based on their textual representation
659
     *
660
     * @param stdClass $question the question object
661
     * @param string $layouttype the layout type
662
     * @param string $selecttype the select type
663
     * @param string $selectcount the number of items to display
664
     * @param string $gradingtype the grading type
665
     * @param string $showgrading the grading details or not
666
     * @param string $numberingstyle the numbering style
667
     */
668
    public function set_options_for_import(stdClass $question, string $layouttype, string $selecttype, string $selectcount,
669
            string $gradingtype, string $showgrading, string $numberingstyle): void {
670
 
671
        // Set "layouttype" option.
672
        $question->layouttype = match (strtoupper($layouttype)) {
673
            'HORIZONTAL', 'HORI', 'H', '1' => qtype_ordering_question::LAYOUT_HORIZONTAL,
674
            default => qtype_ordering_question::LAYOUT_VERTICAL,
675
        };
676
 
677
        // Set "selecttype" option.
678
        $question->selecttype = match (strtoupper($selecttype)) {
679
            'ALL', 'EXACT' => qtype_ordering_question::SELECT_ALL,
680
            'CONTIGUOUS', 'CONTIG' => qtype_ordering_question::SELECT_CONTIGUOUS,
681
            default => qtype_ordering_question::SELECT_RANDOM,
682
        };
683
 
684
        // Set "selectcount" option - this used to be ($count - 2).
685
        if (is_numeric($selectcount) && $selectcount >= qtype_ordering_question::MIN_SUBSET_ITEMS) {
686
            $question->selectcount = intval($selectcount);
687
        } else {
688
            $question->selectcount = qtype_ordering_question::MIN_SUBSET_ITEMS; // Default!
689
        }
690
 
691
        // Set "gradingtype" option.
692
        $question->gradingtype = match (strtoupper($gradingtype)) {
693
            'ALL_OR_NOTHING' => qtype_ordering_question::GRADING_ALL_OR_NOTHING,
694
            'ABS', 'ABSOLUTE', 'ABSOLUTE_POSITION' => qtype_ordering_question::GRADING_ABSOLUTE_POSITION,
695
            'RELATIVE_NEXT_INCLUDE_LAST' => qtype_ordering_question::GRADING_RELATIVE_NEXT_INCLUDE_LAST,
696
            'RELATIVE_ONE_PREVIOUS_AND_NEXT' => qtype_ordering_question::GRADING_RELATIVE_ONE_PREVIOUS_AND_NEXT,
697
            'RELATIVE_ALL_PREVIOUS_AND_NEXT' => qtype_ordering_question::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT,
698
            'LONGEST_ORDERED_SUBSET' => qtype_ordering_question::GRADING_LONGEST_ORDERED_SUBSET,
699
            'LONGEST_CONTIGUOUS_SUBSET' => qtype_ordering_question::GRADING_LONGEST_CONTIGUOUS_SUBSET,
700
            'RELATIVE_TO_CORRECT' => qtype_ordering_question::GRADING_RELATIVE_TO_CORRECT,
701
            default => qtype_ordering_question::GRADING_RELATIVE_NEXT_EXCLUDE_LAST,
702
        };
703
 
704
        // Set "showgrading" option.
705
        $question->showgrading = match (strtoupper($showgrading)) {
706
            'HIDE', 'FALSE', 'NO' => 0,
707
            default => 1,
708
        };
709
 
710
        // Set "numberingstyle" option.
711
        $question->numberingstyle = match ($numberingstyle) {
712
            'none', '123', 'abc', 'ABCD', 'iii', 'IIII' => $numberingstyle,
713
            default => qtype_ordering_question::NUMBERING_STYLE_DEFAULT,
714
        };
715
    }
716
 
717
    /**
718
     * Return the answer numbering style.
719
     * This method is used by "tests/questiontype_test.php".
720
     *
721
     * @param stdClass $questiondata
722
     * @return string
723
     */
724
    public function get_numberingstyle(stdClass $questiondata): string {
725
        return $questiondata->options->numberingstyle;
726
    }
727
}