Proyectos de Subversion Moodle

Rev

Rev 11 | | 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
namespace mod_quiz;
18
 
1441 ariadna 19
use core\exception\coding_exception;
20
use core_question\local\bank\random_question_loader;
1 efrain 21
use core_question\question_reference_manager;
22
use mod_quiz\question\display_options;
23
 
24
defined('MOODLE_INTERNAL') || die();
25
 
26
global $CFG;
27
require_once(__DIR__ . '/quiz_question_helper_test_trait.php');
28
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
29
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
30
 
31
/**
32
 * Quiz backup and restore tests.
33
 *
34
 * @package    mod_quiz
35
 * @category   test
36
 * @copyright  2021 Catalyst IT Australia Pty Ltd
37
 * @author     Safat Shahin <safatshahin@catalyst-au.net>
38
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1441 ariadna 39
 * @covers \mod_quiz\question\bank\qbank_helper
40
 * @covers \restore_quiz_activity_structure_step
41
 * @covers \restore_question_set_reference_data_trait
1 efrain 42
 */
1441 ariadna 43
final class quiz_question_restore_test extends \advanced_testcase {
1 efrain 44
    use \quiz_question_helper_test_trait;
45
 
46
    /**
47
     * @var \stdClass test student user.
48
     */
49
    protected $student;
50
 
51
    /**
52
     * Called before every test.
53
     */
54
    public function setUp(): void {
55
        global $USER;
56
        parent::setUp();
57
        $this->setAdminUser();
58
        $this->course = $this->getDataGenerator()->create_course();
59
        $this->student = $this->getDataGenerator()->create_user();
60
        $this->user = $USER;
61
    }
62
 
63
    /**
1441 ariadna 64
     * Create a quiz with 2 questions and 1 random question, using question categories in the provided context.
1 efrain 65
     *
1441 ariadna 66
     * @param int $questioncontextid Context to create question categories in.
67
     * @return array The quiz, slots, random questions in each slot that has them, and the quiz context.
1 efrain 68
     */
1441 ariadna 69
    public function create_quiz_with_questions(int $questioncontextid): array {
70
        $quiz = $this->create_test_quiz($this->course);
71
        $quizcontext = \context_module::instance($quiz->cmid);
1 efrain 72
 
73
        // Test for questions from a different context.
74
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
1441 ariadna 75
        $this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $questioncontextid]);
76
        $this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $questioncontextid]);
77
        $slots = \mod_quiz\question\bank\qbank_helper::get_question_structure(
78
            $quiz->id, $quizcontext);
79
        $randomquestions = [];
80
        $loader = new random_question_loader(new \qubaid_list([]));
81
        foreach ($slots as $number => $slot) {
82
            if (!empty($slot->filtercondition)) {
83
                $randomquestions[$number] = $loader->get_filtered_questions($slot->filtercondition['filter']);
84
            }
85
        }
86
        return [
87
            $quiz,
88
            $slots,
89
            $randomquestions,
90
            $quizcontext,
91
        ];
92
    }
1 efrain 93
 
1441 ariadna 94
    /**
95
     * Verify the layout of the quiz on the provided course matches (or doesn't match) the expected layout.
96
     *
97
     * For $expectedslots, the value should be provided as an array keyed by slot number. Each slot can be provided as a single
98
     * number, which will considered a question ID, or an array with 'filter' and 'randomquestionids' which will be considered
99
     * a random question.
100
     *
101
     * For example,
102
     * [
103
     *  [1] => 123,
104
     *  [2] => [
105
     *           'filter' => ['name' => 'category', 'values' => ['345']],
106
     *           'randomquestionids' => ['101', '102'],
107
     *         ],
108
     *  [3] => 124,
109
     * ]
110
     * Will assert that the quiz contains (or doesn't contain if $expectequal is false):
111
     * - A question in slot 1 with ID 123,
112
     * - A random question in slot 2 with a filter for category ID 345, which matches questions with IDs 101 and 102,
113
     * - A question in slot 3 with ID 124.
114
     *
115
     * @param int $courseid The ID of the course to pick the quiz from.
116
     * @param array $expectedslots The layout of slots to compare to, see above for details of the structure.
117
     * @param bool $expectequal If true, assert that each slot is equal to the expected value. Otherwise assert it's not equal.
118
     */
119
    public function verify_restored_quiz_layout(
120
        int $courseid,
121
        array $expectedslots,
122
        bool $expectequal
123
    ): void {
124
        $loader = new random_question_loader(new \qubaid_list([]));
125
        $modules = get_fast_modinfo($courseid)->get_instances_of('quiz');
126
        $module = reset($modules);
127
        $newslots = \mod_quiz\question\bank\qbank_helper::get_question_structure(
128
            $module->instance, $module->context);
129
        $this->assertCount(count($expectedslots), $newslots);
130
        foreach ($newslots as $number => $slot) {
131
            $randomquestionids = null;
132
            if (is_array($expectedslots[$number])) {
133
                // A random question, compare filters and filtered question ids.
134
                $actualcontents = $slot->filtercondition['filter'];
135
                $expectedcontents = $expectedslots[$number]['filter'];
136
                $randomquestions = $loader->get_filtered_questions($slot->filtercondition['filter']);
137
                $randomquestionids = array_keys($randomquestions);
138
                sort($randomquestionids);
139
                sort($expectedslots[$number]['randomquestionids']);
140
            } else if (is_numeric($expectedslots[$number])) {
141
                // A normal question, compare IDs.
142
                $actualcontents = $slot->questionid;
143
                $expectedcontents = $expectedslots[$number];
144
            } else {
145
                throw new coding_exception('Slots must either be a number or an array.');
146
            }
147
            if ($expectequal) {
148
                $this->assertEquals($expectedcontents, $actualcontents);
149
                if (!is_null($randomquestionids)) {
150
                    $this->assertEquals($expectedslots[$number]['randomquestionids'], $randomquestionids);
151
                }
152
            } else {
153
                $this->assertNotEquals($expectedcontents, $actualcontents);
154
                if (!is_null($randomquestionids)) {
155
                    $this->assertNotEquals($expectedslots[$number]['randomquestionids'], $randomquestionids);
156
                }
157
            }
158
        }
159
    }
160
 
161
    /**
162
     * Restore a quiz that used questions from a shared question bank on the same course, after the course is deleted.
163
     *
164
     * The restored quiz should use new copies of the questions.
165
     */
166
    public function test_quiz_restore_in_a_different_course_using_question_bank(): void {
167
        $this->resetAfterTest();
168
 
169
        // Create the test quiz.
170
        $qbank = self::getDataGenerator()->create_module('qbank', ['course' => $this->course]);
171
        $qbankcontext = \context_module::instance($qbank->cmid);
172
        [$quiz, $originalslots, $randomquestions, $quizcontext] = $this->create_quiz_with_questions($qbankcontext->id);
173
 
1 efrain 174
        // Make the backup.
175
        $backupid = $this->backup_quiz($quiz, $this->user);
176
 
177
        // Delete the current course to make sure there is no data.
178
        delete_course($this->course, false);
179
 
180
        // Check if the questions and associated data are deleted properly.
1441 ariadna 181
        $this->assertEquals(0, count(\mod_quiz\question\bank\qbank_helper::get_question_structure($quiz->id, $quizcontext)));
1 efrain 182
 
183
        // Restore the course.
184
        $newcourse = $this->getDataGenerator()->create_course();
185
        $this->restore_quiz($backupid, $newcourse, $this->user);
186
 
1441 ariadna 187
        $this->verify_restored_quiz_layout(
188
            courseid: $newcourse->id,
189
            expectedslots: [
190
                1 => $originalslots[1]->questionid,
191
                2 => $originalslots[2]->questionid,
192
                3 => [
193
                    'filter' => $originalslots[3]->filtercondition['filter'],
194
                    'randomquestionids' => array_keys($randomquestions[3]),
195
                ],
196
            ],
197
            expectequal: false,
198
        );
199
    }
200
 
201
    /**
202
     * Restore a quiz that used questions from a shared question bank on the same course.
203
     *
204
     * The restored quiz should reference the questions in the original question bank.
205
     */
206
    public function test_quiz_restore_in_a_different_course_reference_original_question_bank(): void {
207
        $this->resetAfterTest();
208
 
209
        // Create the test quiz.
210
        $qbank = self::getDataGenerator()->create_module('qbank', ['course' => $this->course]);
211
        $qbankcontext = \context_module::instance($qbank->cmid);
212
        [$quiz, $originalslots, $randomquestions] = $this->create_quiz_with_questions($qbankcontext->id);
213
 
214
        // Make the backup.
215
        $backupid = $this->backup_quiz($quiz, $this->user);
216
 
217
        // Restore the course.
218
        $newcourse = $this->getDataGenerator()->create_course();
219
        $this->restore_quiz($backupid, $newcourse, $this->user);
220
 
221
        $this->verify_restored_quiz_layout(
222
            courseid: $newcourse->id,
223
            expectedslots: [
224
                1 => $originalslots[1]->questionid,
225
                2 => $originalslots[2]->questionid,
226
                3 => [
227
                    'filter' => $originalslots[3]->filtercondition['filter'],
228
                    'randomquestionids' => array_keys($randomquestions[3]),
229
                ],
230
            ],
231
            expectequal: true,
232
        );
233
    }
234
 
235
    /**
236
     * Restore a quiz that used questions from a shared question bank on the same course, as a user who cannot access the course.
237
     *
238
     * The restored quiz should use new copies of the questions.
239
     */
240
    public function test_quiz_restore_in_a_different_course_cant_use_original_question_bank(): void {
241
        $this->resetAfterTest();
242
 
243
        // Create the test quiz.
244
        $qbank = self::getDataGenerator()->create_module('qbank', ['course' => $this->course]);
245
        $qbankcontext = \context_module::instance($qbank->cmid);
246
        [$quiz, $originalslots, $randomquestions] = $this->create_quiz_with_questions($qbankcontext->id);
247
 
248
        // Make the backup.
249
        $backupid = $this->backup_quiz($quiz, $this->user);
250
 
251
        // Restore the course as a new user without access to the original course.
252
        $newcourse = $this->getDataGenerator()->create_course();
253
        $newuser = self::getDataGenerator()->create_user();
254
        self::getDataGenerator()->enrol_user($newuser->id, $newcourse->id, 'manager');
255
        $this->setUser($newuser);
256
        $this->restore_quiz($backupid, $newcourse, $newuser);
257
 
258
        $this->verify_restored_quiz_layout(
259
            courseid: $newcourse->id,
260
            expectedslots: [
261
                1 => $originalslots[1]->questionid,
262
                2 => $originalslots[2]->questionid,
263
                3 => [
264
                    'filter' => $originalslots[3]->filtercondition['filter'],
265
                    'randomquestionids' => array_keys($randomquestions[3]),
266
                ],
267
            ],
268
            expectequal: false,
269
        );
270
    }
271
 
272
    /**
273
     * Restore a quiz that used questions from a shared question bank on another course, after that other course is deleted.
274
     *
275
     * The restored quiz should use new copies of the questions.
276
     */
277
    public function test_quiz_restore_in_a_different_course_question_course_has_been_deleted(): void {
278
        $this->resetAfterTest();
279
 
280
        // Create the test quiz.
281
        $qbankcourse = self::getDataGenerator()->create_course();
282
        $qbank = self::getDataGenerator()->create_module('qbank', ['course' => $qbankcourse]);
283
        $qbankcontext = \context_module::instance($qbank->cmid);
284
        [$quiz, $originalslots, $randomquestions] = $this->create_quiz_with_questions($qbankcontext->id);
285
 
286
        // Make the backup.
287
        $backupid = $this->backup_quiz($quiz, $this->user);
288
 
289
        // Delete the qbank course.
290
        delete_course($qbankcourse, false);
291
 
292
        // Restore the course as a new copy.
293
        $newcourse = $this->getDataGenerator()->create_course();
294
        $this->restore_quiz($backupid, $newcourse, $this->user);
295
 
1 efrain 296
        // Verify.
1441 ariadna 297
        $this->verify_restored_quiz_layout(
298
            courseid: $newcourse->id,
299
            expectedslots: [
300
                1 => $originalslots[1]->questionid,
301
                2 => $originalslots[2]->questionid,
302
                3 => [
303
                    'filter' => $originalslots[3]->filtercondition['filter'],
304
                    'randomquestionids' => array_keys($randomquestions[3]),
305
                ],
306
            ],
307
            expectequal: false,
308
        );
1 efrain 309
    }
310
 
311
    /**
312
     * Test a quiz backup and restore in a different course without attempts for quiz question bank.
313
     *
11 efrain 314
     * @covers \mod_quiz\question\bank\qbank_helper::get_question_structure
1 efrain 315
     */
11 efrain 316
    public function test_quiz_restore_in_a_different_course_using_quiz_question_bank(): void {
1 efrain 317
        $this->resetAfterTest();
318
 
319
        // Create the test quiz.
320
        $quiz = $this->create_test_quiz($this->course);
321
        // Test for questions from a different context.
322
        $quizcontext = \context_module::instance($quiz->cmid);
323
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
324
        $this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $quizcontext->id]);
325
        $this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $quizcontext->id]);
326
 
327
        // Make the backup.
328
        $backupid = $this->backup_quiz($quiz, $this->user);
329
 
330
        // Delete the current course to make sure there is no data.
331
        delete_course($this->course, false);
332
 
333
        // Check if the questions and associated datas are deleted properly.
334
        $this->assertEquals(0, count(\mod_quiz\question\bank\qbank_helper::get_question_structure(
335
                $quiz->id, $quizcontext)));
336
 
337
        // Restore the course.
338
        $newcourse = $this->getDataGenerator()->create_course();
339
        $this->restore_quiz($backupid, $newcourse, $this->user);
340
 
341
        // Verify.
342
        $modules = get_fast_modinfo($newcourse->id)->get_instances_of('quiz');
343
        $module = reset($modules);
344
        $this->assertEquals(3, count(\mod_quiz\question\bank\qbank_helper::get_question_structure(
345
                $module->instance, $module->context)));
346
    }
347
 
348
    /**
349
     * Count the questions for the context.
350
     *
351
     * @param int $contextid
352
     * @param string $extracondition
353
     * @return int the number of questions.
354
     */
355
    protected function question_count(int $contextid, string $extracondition = ''): int {
356
        global $DB;
357
        return $DB->count_records_sql(
358
            "SELECT COUNT(q.id)
359
               FROM {question} q
360
               JOIN {question_versions} qv ON qv.questionid = q.id
361
               JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
362
               JOIN {question_categories} qc on qc.id = qbe.questioncategoryid
363
              WHERE qc.contextid = ?
364
              $extracondition", [$contextid]);
365
    }
366
 
367
    /**
1441 ariadna 368
     * Duplicate a quiz that uses questions from a shared bank on the same course.
1 efrain 369
     *
1441 ariadna 370
     * The new quiz should reference the same questions.
1 efrain 371
     */
1441 ariadna 372
    public function test_quiz_duplicate_does_not_duplicate_questions_from_shared_banks(): void {
1 efrain 373
        $this->resetAfterTest();
1441 ariadna 374
        // Test for questions from a qbank context.
375
        $qbankcourse = self::getDataGenerator()->create_course();
376
        $qbank = self::getDataGenerator()->create_module('qbank', ['course' => $qbankcourse]);
377
        $context = \context_module::instance($qbank->cmid);
378
        [$quiz, $originalslots, $randomquestions] = $this->create_quiz_with_questions($context->id);
379
        // Count the questions in qbank context.
1 efrain 380
        $this->assertEquals(7, $this->question_count($context->id));
1441 ariadna 381
        $this->assertCount(3, $originalslots);
1 efrain 382
        $newquiz = $this->duplicate_quiz($this->course, $quiz);
383
        $this->assertEquals(7, $this->question_count($context->id));
1441 ariadna 384
        $newquizcontext = \context_module::instance($newquiz->id);
1 efrain 385
        // Count the questions in the quiz context.
1441 ariadna 386
        $this->assertEquals(0, $this->question_count($newquizcontext->id));
387
 
388
        $this->verify_restored_quiz_layout(
389
            courseid: $this->course->id,
390
            expectedslots: [
391
                1 => $originalslots[1]->questionid,
392
                2 => $originalslots[2]->questionid,
393
                3 => [
394
                    'filter' => $originalslots[3]->filtercondition['filter'],
395
                    'randomquestionids' => array_keys($randomquestions[3]),
396
                ],
397
            ],
398
            expectequal: true,
399
        );
1 efrain 400
    }
401
 
402
    /**
403
     * Test quiz duplicate for quiz question bank.
404
     *
405
     * @covers ::duplicate_module
406
     */
11 efrain 407
    public function test_quiz_duplicate_for_quiz_question_bank_questions(): void {
1 efrain 408
        $this->resetAfterTest();
409
        $quiz = $this->create_test_quiz($this->course);
410
        // Test for questions from a different context.
411
        $context = \context_module::instance($quiz->cmid);
412
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
413
        $this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $context->id]);
414
        $this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $context->id]);
415
        // Count the questions in course context.
416
        $this->assertEquals(7, $this->question_count($context->id));
417
        $newquiz = $this->duplicate_quiz($this->course, $quiz);
418
        $this->assertEquals(7, $this->question_count($context->id));
419
        $context = \context_module::instance($newquiz->id);
420
        // Count the questions in the quiz context.
421
        $this->assertEquals(7, $this->question_count($context->id));
422
    }
423
 
424
    /**
425
     * Test quiz restore with attempts.
426
     *
11 efrain 427
     * @covers \mod_quiz\question\bank\qbank_helper::get_question_structure
1 efrain 428
     */
11 efrain 429
    public function test_quiz_restore_with_attempts(): void {
1 efrain 430
        $this->resetAfterTest();
431
 
432
        // Create a quiz.
433
        $quiz = $this->create_test_quiz($this->course);
434
        $quizcontext = \context_module::instance($quiz->cmid);
435
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
436
        $this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $quizcontext->id]);
437
        $this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $quizcontext->id]);
438
 
439
        // Attempt it as a student, and check.
440
        /** @var \question_usage_by_activity $quba */
441
        [, $quba] = $this->attempt_quiz($quiz, $this->student);
442
        $this->assertEquals(3, $quba->question_count());
443
        $this->assertCount(1, quiz_get_user_attempts($quiz->id, $this->student->id));
444
 
445
        // Make the backup.
446
        $backupid = $this->backup_quiz($quiz, $this->user);
447
 
448
        // Delete the current course to make sure there is no data.
449
        delete_course($this->course, false);
450
 
451
        // Restore the backup.
452
        $newcourse = $this->getDataGenerator()->create_course();
453
        $this->restore_quiz($backupid, $newcourse, $this->user);
454
 
455
        // Verify.
456
        $modules = get_fast_modinfo($newcourse->id)->get_instances_of('quiz');
457
        $module = reset($modules);
458
        $this->assertCount(1, quiz_get_user_attempts($module->instance, $this->student->id));
459
        $this->assertCount(3, \mod_quiz\question\bank\qbank_helper::get_question_structure(
460
                $module->instance, $module->context));
461
    }
462
 
463
    /**
464
     * Test pre 4.0 quiz restore for regular questions.
465
     *
466
     * Also, for efficiency, tests restore of the review options.
467
     *
11 efrain 468
     * @covers \restore_quiz_activity_structure_step::process_quiz_question_legacy_instance
1 efrain 469
     */
11 efrain 470
    public function test_pre_4_quiz_restore_for_regular_questions(): void {
1 efrain 471
        global $USER, $DB;
472
        $this->resetAfterTest();
473
        $backupid = 'abc';
474
        $backuppath = make_backup_temp_directory($backupid);
475
        get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
476
            __DIR__ . "/fixtures/moodle_28_quiz.mbz", $backuppath);
477
 
478
        // Do the restore to new course with default settings.
479
        $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
480
        $newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
481
        $rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
482
            \backup::TARGET_NEW_COURSE);
483
 
484
        $this->assertTrue($rc->execute_precheck());
485
        $rc->execute_plan();
486
        $rc->destroy();
487
 
488
        // Get the information about the resulting course and check that it is set up correctly.
489
        $modinfo = get_fast_modinfo($newcourseid);
490
        $quiz = array_values($modinfo->get_instances_of('quiz'))[0];
491
        $quizobj = \mod_quiz\quiz_settings::create($quiz->instance);
492
        $structure = structure::create_for_quiz($quizobj);
493
 
494
        // Verify the restored review options setting.
495
        $this->assertEquals(display_options::DURING |
496
                    display_options::IMMEDIATELY_AFTER |
497
                    display_options::LATER_WHILE_OPEN |
498
                    display_options::AFTER_CLOSE, $quizobj->get_quiz()->reviewmaxmarks);
499
 
500
        // Are the correct slots returned?
501
        $slots = $structure->get_slots();
502
        $this->assertCount(2, $slots);
503
 
504
        $quizobj->preload_questions();
505
        $quizobj->load_questions();
506
        $questions = $quizobj->get_questions();
507
        $this->assertCount(2, $questions);
508
 
509
        // Count the questions in quiz qbank.
510
        $this->assertEquals(2, $this->question_count($quizobj->get_context()->id));
511
    }
512
 
513
    /**
514
     * Test pre 4.0 quiz restore for random questions.
515
     *
11 efrain 516
     * @covers \restore_quiz_activity_structure_step::process_quiz_question_legacy_instance
1 efrain 517
     */
11 efrain 518
    public function test_pre_4_quiz_restore_for_random_questions(): void {
1 efrain 519
        global $USER, $DB;
520
        $this->resetAfterTest();
521
 
522
        $backupid = 'abc';
523
        $backuppath = make_backup_temp_directory($backupid);
524
        get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
525
            __DIR__ . "/fixtures/random_by_tag_quiz.mbz", $backuppath);
526
 
527
        // Do the restore to new course with default settings.
528
        $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
529
        $newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
530
        $rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
531
            \backup::TARGET_NEW_COURSE);
532
 
1441 ariadna 533
        $rc->execute_precheck();
534
        $results = $rc->get_precheck_results();
535
        // Backup contains categories attached to deprecated contexts so the results should only contain warnings for these.
536
        $this->assertCount(2, $results['warnings']);
537
        foreach ($results['warnings'] as $warning) {
538
            $this->assertStringContainsString('will be created at a question bank module context by restore', $warning);
539
        }
540
        $this->assertArrayNotHasKey('errors', $results);
541
 
1 efrain 542
        $rc->execute_plan();
543
        $rc->destroy();
544
 
545
        // Get the information about the resulting course and check that it is set up correctly.
546
        $modinfo = get_fast_modinfo($newcourseid);
1441 ariadna 547
        $qbanks = $modinfo->get_instances_of('qbank');
548
        $this->assertCount(1, $qbanks);
549
        $qbank = reset($qbanks);
550
        $this->assertEquals(get_string('systembank', 'question'), $qbank->name);
1 efrain 551
        $quiz = array_values($modinfo->get_instances_of('quiz'))[0];
552
        $quizobj = \mod_quiz\quiz_settings::create($quiz->instance);
553
        $structure = structure::create_for_quiz($quizobj);
554
 
555
        // Are the correct slots returned?
556
        $slots = $structure->get_slots();
557
        $this->assertCount(1, $slots);
558
 
559
        $quizobj->preload_questions();
560
        $quizobj->load_questions();
561
        $questions = $quizobj->get_questions();
562
        $this->assertCount(1, $questions);
563
 
1441 ariadna 564
        // Count the questions for new course mod_qbank question bank.
565
        $this->assertEquals(6, $this->question_count(\context_module::instance($qbank->id)->id));
566
        $this->assertEquals(6, $this->question_count(\context_module::instance($qbank->id)->id, "AND q.qtype <> 'random'"));
1 efrain 567
 
568
        // Count the questions in quiz qbank.
569
        $this->assertEquals(0, $this->question_count($quizobj->get_context()->id));
570
    }
571
 
572
    /**
573
     * Test pre 4.0 quiz restore for random question tags.
574
     *
11 efrain 575
     * @covers \restore_quiz_activity_structure_step::process_quiz_question_legacy_instance
1 efrain 576
     */
11 efrain 577
    public function test_pre_4_quiz_restore_for_random_question_tags(): void {
1 efrain 578
        global $USER, $DB;
579
        $this->resetAfterTest();
580
        $randomtags = [
581
            '1' => ['first question', 'one', 'number one'],
582
            '2' => ['first question', 'one', 'number one'],
583
            '3' => ['one', 'number one', 'second question'],
584
        ];
585
        $backupid = 'abc';
586
        $backuppath = make_backup_temp_directory($backupid);
587
        get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
588
            __DIR__ . "/fixtures/moodle_311_quiz.mbz", $backuppath);
589
 
590
        // Do the restore to new course with default settings.
591
        $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
592
        $newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
593
        $rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
594
            \backup::TARGET_NEW_COURSE);
595
 
596
        $this->assertTrue($rc->execute_precheck());
597
        $rc->execute_plan();
598
        $rc->destroy();
599
 
600
        // Get the information about the resulting course and check that it is set up correctly.
601
        $modinfo = get_fast_modinfo($newcourseid);
602
        $quiz = array_values($modinfo->get_instances_of('quiz'))[0];
603
        $quizobj = \mod_quiz\quiz_settings::create($quiz->instance);
604
        $structure = \mod_quiz\structure::create_for_quiz($quizobj);
605
 
606
        // Count the questions in quiz qbank.
607
        $context = \context_module::instance(get_coursemodule_from_instance("quiz", $quizobj->get_quizid(), $newcourseid)->id);
608
        $this->assertEquals(2, $this->question_count($context->id));
609
 
610
        // Are the correct slots returned?
611
        $slots = $structure->get_slots();
612
        $this->assertCount(3, $slots);
613
 
614
        // Check if the tags match with the actual restored data.
615
        foreach ($slots as $slot) {
616
            $setreference = $DB->get_record('question_set_references',
617
                ['itemid' => $slot->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
618
            $filterconditions = json_decode($setreference->filtercondition);
619
            $tags = [];
620
            foreach ($filterconditions->tags as $tagstring) {
621
                $tag = explode(',', $tagstring);
622
                $tags[] = $tag[1];
623
            }
624
            $this->assertEquals([], array_diff($randomtags[$slot->slot], $tags));
625
        }
626
 
627
    }
628
 
629
    /**
630
     * Test pre 4.0 quiz restore for random question used on multiple quizzes.
631
     *
11 efrain 632
     * @covers \restore_quiz_activity_structure_step::process_quiz_question_legacy_instance
1 efrain 633
     */
11 efrain 634
    public function test_pre_4_quiz_restore_shared_random_question(): void {
1 efrain 635
        global $USER, $DB;
636
        $this->resetAfterTest();
637
 
638
        $backupid = 'abc';
639
        $backuppath = make_backup_temp_directory($backupid);
640
        get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
641
                __DIR__ . "/fixtures/pre-40-shared-random-question.mbz", $backuppath);
642
 
643
        // Do the restore to new course with default settings.
644
        $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
645
        $newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
646
        $rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
647
                \backup::TARGET_NEW_COURSE);
648
 
1441 ariadna 649
        $rc->execute_precheck();
650
        $results = $rc->get_precheck_results();
651
        // Backup contains categories attached to deprecated contexts, so we should only have warnings for those.
652
        $this->assertCount(1, $results['warnings']);
653
        $this->assertStringContainsString('will be created at a question bank module context by restore', $results['warnings'][0]);
654
        $this->assertArrayNotHasKey('errors', $results);
655
 
1 efrain 656
        $rc->execute_plan();
657
        $rc->destroy();
658
 
659
        // Get the information about the resulting course and check that it is set up correctly.
660
        // Each quiz should contain an instance of the random question.
661
        $modinfo = get_fast_modinfo($newcourseid);
1441 ariadna 662
        $qbank = array_values($modinfo->get_instances_of('qbank'))[0];
1 efrain 663
        $quizzes = $modinfo->get_instances_of('quiz');
664
        $this->assertCount(2, $quizzes);
665
        foreach ($quizzes as $quiz) {
666
            $quizobj = \mod_quiz\quiz_settings::create($quiz->instance);
667
            $structure = structure::create_for_quiz($quizobj);
668
 
669
            // Are the correct slots returned?
670
            $slots = $structure->get_slots();
671
            $this->assertCount(1, $slots);
672
 
673
            $quizobj->preload_questions();
674
            $quizobj->load_questions();
675
            $questions = $quizobj->get_questions();
676
            $this->assertCount(1, $questions);
677
        }
678
 
1441 ariadna 679
        // Count the questions for new course mod_qbank question bank.
1 efrain 680
        // We should have a single question, the random question should have been deleted after the restore.
1441 ariadna 681
        $this->assertEquals(1, $this->question_count(\context_module::instance($qbank->id)->id));
682
        $this->assertEquals(1, $this->question_count(\context_module::instance($qbank->id)->id,
1 efrain 683
                "AND q.qtype <> 'random'"));
684
 
685
        // Count the questions in quiz qbank.
686
        $this->assertEquals(0, $this->question_count($quizobj->get_context()->id));
687
    }
688
 
689
    /**
690
     * Ensure that question slots are correctly backed up and restored with all properties.
691
     *
692
     * @covers \backup_quiz_activity_structure_step::define_structure()
693
     * @return void
694
     */
695
    public function test_backup_restore_question_slots(): void {
696
        $this->resetAfterTest(true);
697
 
698
        $course1 = $this->getDataGenerator()->create_course();
699
        $course2 = $this->getDataGenerator()->create_course();
700
 
701
        $user1 = $this->getDataGenerator()->create_and_enrol($course1, 'editingteacher');
702
        $this->getDataGenerator()->enrol_user($user1->id, $course2->id, 'editingteacher');
703
 
704
        // Make a quiz.
705
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
706
 
707
        $quiz = $quizgenerator->create_instance(['course' => $course1->id, 'questionsperpage' => 0, 'grade' => 100.0,
708
                'sumgrades' => 3]);
709
 
710
        // Create some fixed and random questions.
711
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
712
 
713
        $cat = $questiongenerator->create_question_category();
714
        $saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
715
        $numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
716
        $matchq = $questiongenerator->create_question('match', null, ['category' => $cat->id]);
717
        $randomcat = $questiongenerator->create_question_category();
718
        $questiongenerator->create_question('shortanswer', null, ['category' => $randomcat->id]);
719
        $questiongenerator->create_question('numerical', null, ['category' => $randomcat->id]);
720
        $questiongenerator->create_question('match', null, ['category' => $randomcat->id]);
721
 
722
        // Add them to the quiz.
723
        quiz_add_quiz_question($saq->id, $quiz, 1, 3);
724
        quiz_add_quiz_question($numq->id, $quiz, 2, 2);
725
        quiz_add_quiz_question($matchq->id, $quiz, 3, 1);
726
        $this->add_random_questions($quiz->id, 3, $randomcat->id, 2);
727
 
728
        $quizobj = quiz_settings::create($quiz->id, $user1->id);
729
        $originalstructure = \mod_quiz\structure::create_for_quiz($quizobj);
730
 
731
        // Set one slot to a non-default display number.
732
        $originalslots = $originalstructure->get_slots();
733
        $firstslot = reset($originalslots);
734
        $originalstructure->update_slot_display_number($firstslot->id, rand(5, 10));
735
 
736
        // Set one slot to requireprevious.
737
        $lastslot = end($originalslots);
738
        $originalstructure->update_question_dependency($lastslot->id, true);
739
 
740
        // Backup and restore the quiz.
741
        $backupid = $this->backup_quiz($quiz, $user1);
742
        $this->restore_quiz($backupid, $course2, $user1);
743
 
744
        // Ensure the restored slots match the original slots.
745
        $modinfo = get_fast_modinfo($course2);
746
        $quizzes = $modinfo->get_instances_of('quiz');
747
        $restoredquiz = reset($quizzes);
748
        $restoredquizobj = quiz_settings::create($restoredquiz->instance, $user1->id);
749
        $restoredstructure = \mod_quiz\structure::create_for_quiz($restoredquizobj);
750
        $restoredslots = array_values($restoredstructure->get_slots());
751
        $originalstructure = \mod_quiz\structure::create_for_quiz($quizobj);
752
        $originalslots = array_values($originalstructure->get_slots());
753
        foreach ($restoredslots as $key => $restoredslot) {
754
            $originalslot = $originalslots[$key];
755
            $this->assertEquals($originalslot->quizid, $quiz->id);
756
            $this->assertEquals($restoredslot->quizid, $restoredquiz->instance);
757
            $this->assertEquals($originalslot->slot, $restoredslot->slot);
758
            $this->assertEquals($originalslot->page, $restoredslot->page);
759
            $this->assertEquals($originalslot->displaynumber, $restoredslot->displaynumber);
760
            $this->assertEquals($originalslot->requireprevious, $restoredslot->requireprevious);
761
            $this->assertEquals($originalslot->maxmark, $restoredslot->maxmark);
762
        }
763
    }
764
 
765
    /**
766
     * Test pre 4.3 quiz restore for random question filter conditions.
767
     *
768
     * @covers \restore_question_set_reference_data_trait::process_question_set_reference
769
     */
11 efrain 770
    public function test_pre_43_quiz_restore_for_random_question_filtercondition(): void {
1 efrain 771
        global $USER, $DB;
772
        $this->resetAfterTest();
773
        $backupid = 'abc';
774
        $backuppath = make_backup_temp_directory($backupid);
775
        get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
776
                __DIR__ . "/fixtures/moodle_42_random_question.mbz", $backuppath);
777
 
778
        // Do the restore to new course with default settings.
779
        $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
780
        $newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
781
        $rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
782
                \backup::TARGET_NEW_COURSE);
783
 
784
        $this->assertTrue($rc->execute_precheck());
785
        $rc->execute_plan();
786
        $rc->destroy();
787
 
788
        // Get the information about the resulting course and check that it is set up correctly.
789
        $modinfo = get_fast_modinfo($newcourseid);
790
        $quiz = array_values($modinfo->get_instances_of('quiz'))[0];
791
        $quizobj = \mod_quiz\quiz_settings::create($quiz->instance);
792
        $structure = \mod_quiz\structure::create_for_quiz($quizobj);
793
 
794
        // Count the questions in quiz qbank.
795
        $context = \context_module::instance(get_coursemodule_from_instance("quiz", $quizobj->get_quizid(), $newcourseid)->id);
796
        $this->assertEquals(2, $this->question_count($context->id));
797
 
798
        // Are the correct slots returned?
799
        $slots = $structure->get_slots();
800
        $this->assertCount(1, $slots);
801
 
802
        // Check that the filtercondition now matches the 4.3 structure.
803
        foreach ($slots as $slot) {
804
            $setreference = $DB->get_record('question_set_references',
805
                    ['itemid' => $slot->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
806
            $filterconditions = json_decode($setreference->filtercondition, true);
807
            $this->assertArrayHasKey('cat', $filterconditions);
808
            $this->assertArrayHasKey('jointype', $filterconditions);
809
            $this->assertArrayHasKey('qpage', $filterconditions);
810
            $this->assertArrayHasKey('qperpage', $filterconditions);
811
            $this->assertArrayHasKey('filter', $filterconditions);
812
            $this->assertArrayHasKey('category', $filterconditions['filter']);
813
            $this->assertArrayHasKey('qtagids', $filterconditions['filter']);
814
            $this->assertArrayHasKey('filteroptions', $filterconditions['filter']['category']);
815
            $this->assertArrayHasKey('includesubcategories', $filterconditions['filter']['category']['filteroptions']);
816
 
817
            // MDL-79708: Bad filter conversion check.
818
            $this->assertArrayNotHasKey('includesubcategories', $filterconditions['filter']['category']);
819
 
820
            $this->assertArrayNotHasKey('questioncategoryid', $filterconditions);
821
            $this->assertArrayNotHasKey('tags', $filterconditions);
822
            $expectedtags = \core_tag_tag::get_by_name_bulk(1, ['foo', 'bar']);
823
            $expectedtagids = array_values(array_map(fn($expectedtag) => $expectedtag->id, $expectedtags));
824
            $this->assertEquals($expectedtagids, $filterconditions['filter']['qtagids']['values']);
825
            $expectedcategory = $DB->get_record('question_categories', ['idnumber' => 'RAND']);
826
            $this->assertEquals($expectedcategory->id, $filterconditions['filter']['category']['values'][0]);
827
            $expectedcat = implode(',', [$expectedcategory->id, $expectedcategory->contextid]);
828
            $this->assertEquals($expectedcat, $filterconditions['cat']);
829
 
830
            // MDL-79708: Try to convert already converted filter.
831
            $filterconditionsold = $filterconditions;
832
            $filterconditions = question_reference_manager::convert_legacy_set_reference_filter_condition($filterconditions);
833
            // Check that the filtercondition didn't change.
834
            $this->assertEquals($filterconditionsold, $filterconditions);
835
 
836
            // MDL-79708: Try to convert a filter with previously bad conversion.
837
            $filterconditions['filter']['category']['includesubcategories'] = 0;
838
            unset($filterconditions['filter']['category']['filteroptions']);
839
            $filterconditions = question_reference_manager::convert_legacy_set_reference_filter_condition($filterconditions);
840
            $this->assertEquals($filterconditionsold, $filterconditions);
841
        }
842
    }
843
}