Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 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\backup;
18
 
19
use advanced_testcase;
20
use backup_controller;
21
use restore_controller;
22
use quiz_question_helper_test_trait;
23
use backup;
24
 
25
defined('MOODLE_INTERNAL') || die();
26
 
27
global $CFG;
28
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
29
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
30
require_once($CFG->dirroot . '/question/engine/lib.php');
31
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
32
require_once($CFG->dirroot . '/course/lib.php');
33
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
34
 
35
/**
36
 * Test repeatedly restoring a quiz into another course.
37
 *
38
 * @package    mod_quiz
39
 * @category   test
40
 * @copyright  Julien Rädler
41
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42
 * @covers \restore_questions_parser_processor
43
 * @covers \restore_create_categories_and_questions
44
 */
45
final class repeated_restore_test extends advanced_testcase {
46
    use quiz_question_helper_test_trait;
47
 
48
    /**
49
     * Restore a quiz twice into the same target course, and verify the quiz uses the restored questions both times.
50
     */
51
    public function test_restore_quiz_into_other_course_twice(): void {
52
        global $USER;
53
        $this->resetAfterTest();
54
        $this->setAdminUser();
55
 
56
        // Step 1: Create two courses and a user with editing teacher capabilities.
57
        $generator = $this->getDataGenerator();
58
        $course1 = $generator->create_course();
59
        $course2 = $generator->create_course();
60
        $teacher = $USER;
61
        $generator->enrol_user($teacher->id, $course1->id, 'editingteacher');
62
        $generator->enrol_user($teacher->id, $course2->id, 'editingteacher');
63
 
64
        // Create a quiz with questions in the first course.
65
        $quiz = $this->create_test_quiz($course1);
66
        $qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]);
67
        $context = \context_module::instance($qbank->cmid);
68
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
69
 
70
        // Create a question category.
71
        $cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
72
 
73
        // Create a short answer question.
74
        $saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
75
        // Update the question to simulate editing.
76
        $questiongenerator->update_question($saq);
77
        // Add question to quiz.
78
        quiz_add_quiz_question($saq->id, $quiz);
79
 
80
        // Create a numerical question.
81
        $numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
82
        // Update the question to simulate multiple versions.
83
        $questiongenerator->update_question($numq);
84
        $questiongenerator->update_question($numq);
85
        // Add question to quiz.
86
        quiz_add_quiz_question($numq->id, $quiz);
87
 
88
        // Create a true false question.
89
        $tfq = $questiongenerator->create_question('truefalse', null, ['category' => $cat->id]);
90
        // Update the question to simulate multiple versions.
91
        $questiongenerator->update_question($tfq);
92
        $questiongenerator->update_question($tfq);
93
        // Add question to quiz.
94
        quiz_add_quiz_question($tfq->id, $quiz);
95
 
96
        // Capture original question IDs for verification after import.
97
        $modules1 = get_fast_modinfo($course1->id)->get_instances_of('quiz');
98
        $module1 = reset($modules1);
99
        $questionscourse1 = \mod_quiz\question\bank\qbank_helper::get_question_structure(
100
            $module1->instance, $module1->context);
101
 
102
        $originalquestionids = [];
103
        foreach ($questionscourse1 as $slot) {
104
            array_push($originalquestionids, intval($slot->questionid));
105
        }
106
 
107
        // Step 2: Backup the first course.
108
        $bc = new backup_controller(backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE,
109
            backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);
110
        $backupid = $bc->get_backupid();
111
        $bc->execute_plan();
112
        $bc->destroy();
113
 
114
        // Step 3: Import the backup into the second course.
115
        $rc = new restore_controller($backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
116
            $teacher->id, backup::TARGET_CURRENT_ADDING);
117
        $rc->execute_precheck();
118
        $rc->execute_plan();
119
        $rc->destroy();
120
 
121
        // Verify the question ids from the quiz in the original course are different
122
        // from the question ids in the duplicated quiz in the second course.
123
        $modules2 = get_fast_modinfo($course2->id)->get_instances_of('quiz');
124
        $module2 = reset($modules2);
125
        $questionscourse2firstimport = \mod_quiz\question\bank\qbank_helper::get_question_structure(
126
            $module2->instance, $module2->context);
127
 
128
        foreach ($questionscourse2firstimport as $slot) {
129
            $this->assertNotContains(intval($slot->questionid), $originalquestionids,
130
                "Question ID $slot->questionid should not be in the original course's question IDs.");
131
        }
132
 
133
        // Repeat the backup and import process to simulate a second import.
134
        $bc = new backup_controller(backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE,
135
                            backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);
136
        $backupid = $bc->get_backupid();
137
        $bc->execute_plan();
138
        $bc->destroy();
139
 
140
        $rc = new restore_controller($backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
141
            $teacher->id, backup::TARGET_CURRENT_ADDING);
142
        $rc->execute_precheck();
143
        $rc->execute_plan();
144
        $rc->destroy();
145
 
146
        // Verify that the second restore has used the same new questions that were created by the first restore.
147
        $modules3 = get_fast_modinfo($course2->id)->get_instances_of('quiz');
148
        $module3 = end($modules3);
149
        $questionscourse2secondimport = \mod_quiz\question\bank\qbank_helper::get_question_structure(
150
                $module3->instance, $module3->context);
151
 
152
        foreach ($questionscourse2secondimport as $slot) {
153
            $this->assertEquals($questionscourse2firstimport[$slot->slot]->questionid, $slot->questionid);
154
        }
155
    }
156
 
157
    /**
158
     * Restore a copy of a quiz to the same course, using questions that include line breaks in the text.
159
     */
160
    public function test_restore_question_with_linebreaks(): void {
161
        global $USER;
162
        $this->resetAfterTest();
163
        $this->setAdminUser();
164
 
165
        // Step 1: Create two courses and a user with editing teacher capabilities.
166
        $generator = $this->getDataGenerator();
167
        $course1 = $generator->create_course();
168
        $course2 = $generator->create_course();
169
        $teacher = $USER;
170
        $generator->enrol_user($teacher->id, $course1->id, 'editingteacher');
171
        $generator->enrol_user($teacher->id, $course2->id, 'editingteacher');
172
 
173
        // Create a quiz with questions in the first course.
174
        $quiz = $this->create_test_quiz($course1);
175
        $qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]);
176
        $context = \context_module::instance($qbank->cmid);
177
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
178
 
179
        // Create a question category.
180
        $cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
181
 
182
        // Create questions and add to the quiz.
183
        $q1 = $questiongenerator->create_question('truefalse', null, [
184
            'category' => $cat->id,
185
            'questiontext' => ['text' => "<p>Question</p>\r\n<p>One</p>", 'format' => FORMAT_MOODLE]
186
        ]);
187
        $q2 = $questiongenerator->create_question('truefalse', null, [
188
            'category' => $cat->id,
189
            'questiontext' => ['text' => "<p>Question</p>\n<p>Two</p>", 'format' => FORMAT_MOODLE]
190
        ]);
191
        // Add question to quiz.
192
        quiz_add_quiz_question($q1->id, $quiz);
193
        quiz_add_quiz_question($q2->id, $quiz);
194
 
195
        // Capture original question IDs for verification after import.
196
        $modules1 = get_fast_modinfo($course1->id)->get_instances_of('quiz');
197
        $module1 = reset($modules1);
198
        $originalslots = \mod_quiz\question\bank\qbank_helper::get_question_structure(
199
            $module1->instance, $module1->context);
200
 
201
        $originalquestionids = [];
202
        foreach ($originalslots as $slot) {
203
            array_push($originalquestionids, intval($slot->questionid));
204
        }
205
 
206
        $this->assertCount(2, get_questions_category($cat, false));
207
 
208
        // Step 2: Backup the quiz
209
        $bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz->cmid, backup::FORMAT_MOODLE,
210
            backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);
211
        $backupid = $bc->get_backupid();
212
        $bc->execute_plan();
213
        $bc->destroy();
214
 
215
        // Step 3: Import the backup into the same course.
216
        $rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
217
            $teacher->id, backup::TARGET_CURRENT_ADDING);
218
        $rc->execute_precheck();
219
        $rc->execute_plan();
220
        $rc->destroy();
221
 
222
        // Verify the question ids from the new quiz match the first.
223
        $modules2 = get_fast_modinfo($course1->id)->get_instances_of('quiz');
224
        $this->assertCount(2, $modules2);
225
        $module2 = end($modules2);
226
        $copyslots = \mod_quiz\question\bank\qbank_helper::get_question_structure(
227
            $module2->instance, $module2->context);
228
 
229
        foreach ($copyslots as $slot) {
230
            $this->assertContains(intval($slot->questionid), $originalquestionids);
231
        }
232
 
233
        // The category should still only contain 2 question, neither question should be duplicated.
234
        $this->assertCount(2, get_questions_category($cat, false));
235
    }
236
 
237
    /**
238
     * Return a list of qtypes with valid generators in their helper class.
239
     *
240
     * This will check all installed qtypes for a test helper class, then find a defined test question which has a corresponding
241
     * form_data method and return it. If the helper doesn't have a form_data method for any test question, it will return a
242
     * null test question name for that qtype.
243
     *
244
     * @return array
245
     */
246
    public static function get_qtype_generators(): array {
247
        global $CFG;
248
        $generators = [];
249
        foreach (\core\plugin_manager::instance()->get_plugins_of_type('qtype') as $qtype) {
250
            if ($qtype->name == 'random') {
251
                continue;
252
            }
253
            $helperpath = "{$CFG->dirroot}/question/type/{$qtype->name}/tests/helper.php";
254
            if (!file_exists($helperpath)) {
255
                continue;
256
            }
257
            require_once($helperpath);
258
            $helperclass = "qtype_{$qtype->name}_test_helper";
259
            if (!class_exists($helperclass)) {
260
                continue;
261
            }
262
            $helper = new $helperclass();
263
            $testquestion = null;
264
            foreach ($helper->get_test_questions() as $question) {
265
                if (method_exists($helper, "get_{$qtype->name}_question_form_data_{$question}")) {
266
                    $testquestion = $question;
267
                    break;
268
                }
269
            }
270
            $generators[$qtype->name] = [
271
                'qtype' => $qtype->name,
272
                'testquestion' => $testquestion,
273
            ];
274
        }
275
        return $generators;
276
    }
277
 
278
    /**
279
     * Restore a quiz with questions of same stamp into the same course, but different answers.
280
     *
281
     * @dataProvider get_qtype_generators
282
     * @param string $qtype The name of the qtype plugin to test
283
     * @param ?string $testquestion The test question to generate for the plugin. If null, the plugin will be skipped
284
     *      with a message.
285
     */
286
    public function test_restore_quiz_with_same_stamp_questions(string $qtype, ?string $testquestion): void {
287
        global $DB, $USER;
288
        if (is_null($testquestion)) {
289
            $this->markTestSkipped(
290
                "Cannot test qtype_{$qtype} as there is no test question with a form_data method in the " .
291
                "test helper class."
292
            );
293
        }
294
        $this->resetAfterTest();
295
        $this->setAdminUser();
296
 
297
        // Create a course and a user with editing teacher capabilities.
298
        $generator = $this->getDataGenerator();
299
        $course1 = $generator->create_course();
300
        $teacher = $USER;
301
        $generator->enrol_user($teacher->id, $course1->id, 'editingteacher');
302
        $qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]);
303
 
304
        $context = \context_module::instance($qbank->cmid);
305
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
306
 
307
        // Create a question category.
308
        $cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
309
 
310
        // Create 2 quizzes with 2 questions multichoice.
311
        $quiz1 = $this->create_test_quiz($course1);
312
        $question1 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);
313
        quiz_add_quiz_question($question1->id, $quiz1, 0);
314
 
315
        $question2 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);
316
        quiz_add_quiz_question($question2->id, $quiz1, 0);
317
 
318
        // Update question2 to have the same stamp as question1.
319
        $DB->set_field('question', 'stamp', $question1->stamp, ['id' => $question2->id]);
320
 
321
        // Change the answers of the question2 to be different to question1.
322
        $question2data = \question_bank::load_question_data($question2->id);
323
        if (!isset($question2data->options->answers) || empty($question2data->options->answers)) {
324
            $this->markTestSkipped(
325
                "Cannot test edited answers for qtype_{$qtype} as it does not use answers.",
326
            );
327
        }
328
        foreach ($question2data->options->answers as $answer) {
329
            $DB->set_field('question_answers', 'answer', 'edited', ['id' => $answer->id]);
330
        }
331
 
332
        // Backup quiz1.
333
        $bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz1->cmid, backup::FORMAT_MOODLE,
334
            backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);
335
        $backupid = $bc->get_backupid();
336
        $bc->execute_plan();
337
        $bc->destroy();
338
 
339
        // Restore the backup into the same course.
340
        $rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
341
            $teacher->id, backup::TARGET_CURRENT_ADDING);
342
        $rc->execute_precheck();
343
        $rc->execute_plan();
344
        $rc->destroy();
345
 
346
        // Verify that the newly-restored quiz uses the same question as quiz2.
347
        $modules = get_fast_modinfo($course1->id)->get_instances_of('quiz');
348
        $this->assertCount(2, $modules);
349
        $quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure(
350
            $quiz1->id,
351
            \context_module::instance($quiz1->cmid),
352
        );
353
        $quiz2 = end($modules);
354
        $quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->instance, $quiz2->context);
355
        $this->assertEquals($quiz2structure[1]->questionid, $quiz2structure[1]->questionid);
356
        $this->assertEquals($quiz2structure[2]->questionid, $quiz2structure[2]->questionid);
357
    }
358
 
359
    /**
360
     * Restore a quiz with duplicate questions (same stamp and questions) into the same course.
361
     *
362
     * This is a contrived case, but this test serves as a control for the other tests in this class, proving that the hashing
363
     * process will match an identical question.
364
     *
365
     * @dataProvider get_qtype_generators
366
     * @param string $qtype The name of the qtype plugin to test
367
     * @param ?string $testquestion The test question to generate for the plugin. If null, the plugin will be skipped
368
     *       with a message.
369
     */
370
    public function test_restore_quiz_with_duplicate_questions(string $qtype, ?string $testquestion): void {
371
        global $DB, $USER;
372
        if (is_null($testquestion)) {
373
            $this->markTestSkipped(
374
                "Cannot test qtype_{$qtype} as there is no test question with a form_data method in the " .
375
                "test helper class."
376
            );
377
        }
378
        $this->resetAfterTest();
379
        $this->setAdminUser();
380
 
381
        // Create a course and a user with editing teacher capabilities.
382
        $generator = $this->getDataGenerator();
383
        $course1 = $generator->create_course();
384
        $teacher = $USER;
385
        $generator->enrol_user($teacher->id, $course1->id, 'editingteacher');
386
        $qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]);
387
        $context = \context_module::instance($qbank->cmid);
388
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
389
 
390
        // Create a question category.
391
        $cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
392
 
393
        // Create a quiz with 2 identical but separate questions.
394
        $quiz1 = $this->create_test_quiz($course1);
395
        $question1 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);
396
        quiz_add_quiz_question($question1->id, $quiz1, 0);
397
        $question2 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);
398
        quiz_add_quiz_question($question2->id, $quiz1, 0);
399
 
400
        // Update question2 to have the same times and stamp as question1.
401
        $DB->update_record('question', [
402
            'id' => $question2->id,
403
            'stamp' => $question1->stamp,
404
            'timecreated' => $question1->timecreated,
405
            'timemodified' => $question1->timemodified,
406
        ]);
407
 
408
        // Backup quiz.
409
        $bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz1->cmid, backup::FORMAT_MOODLE,
410
            backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);
411
        $backupid = $bc->get_backupid();
412
        $bc->execute_plan();
413
        $bc->destroy();
414
 
415
        // Restore the backup into the same course.
416
        $rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
417
            $teacher->id, backup::TARGET_CURRENT_ADDING);
418
        $rc->execute_precheck();
419
        $rc->execute_plan();
420
        $rc->destroy();
421
 
422
        // Expect that the restored quiz will have the second question in both its slots
423
        // by virtue of identical stamp, version, and hash of question answer texts.
424
        $modules = get_fast_modinfo($course1->id)->get_instances_of('quiz');
425
        $this->assertCount(2, $modules);
426
        $quiz2 = end($modules);
427
        $quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->instance, $quiz2->context);
428
        $this->assertEquals($quiz2structure[1]->questionid, $quiz2structure[2]->questionid);
429
    }
430
 
431
    /**
432
     * Restore a quiz with questions that have the same stamp but different text.
433
     *
434
     * @dataProvider get_qtype_generators
435
     * @param string $qtype The name of the qtype plugin to test
436
     * @param ?string $testquestion The test question to generate for the plugin. If null, the plugin will be skipped
437
     *       with a message.
438
     */
439
    public function test_restore_quiz_with_edited_questions(string $qtype, ?string $testquestion): void {
440
        global $DB, $USER;
441
        if (is_null($testquestion)) {
442
            $this->markTestSkipped(
443
                "Cannot test qtype_{$qtype} as there is no test question with a form_data method in the " .
444
                    "test helper class."
445
            );
446
        }
447
        $this->resetAfterTest();
448
        $this->setAdminUser();
449
 
450
        // Create a course and a user with editing teacher capabilities.
451
        $generator = $this->getDataGenerator();
452
        $course1 = $generator->create_course();
453
        $teacher = $USER;
454
        $generator->enrol_user($teacher->id, $course1->id, 'editingteacher');
455
        $qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]);
456
        $context = \context_module::instance($qbank->cmid);
457
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
458
 
459
        // Create a question category.
460
        $cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
461
 
462
        // Create a quiz with 2 identical but separate questions.
463
        $quiz1 = $this->create_test_quiz($course1);
464
        $question1 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);
465
        quiz_add_quiz_question($question1->id, $quiz1);
466
        $question2 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);
467
        // Edit question 2 to have the same stamp and times as question1, but different text.
468
        $DB->update_record('question', [
469
            'id' => $question2->id,
470
            'questiontext' => 'edited',
471
            'stamp' => $question1->stamp,
472
            'timecreated' => $question1->timecreated,
473
            'timemodified' => $question1->timemodified,
474
        ]);
475
        quiz_add_quiz_question($question2->id, $quiz1);
476
 
477
        // Backup quiz.
478
        $bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz1->cmid, backup::FORMAT_MOODLE,
479
            backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);
480
        $backupid = $bc->get_backupid();
481
        $bc->execute_plan();
482
        $bc->destroy();
483
 
484
        // Restore the backup into the same course.
485
        $rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
486
            $teacher->id, backup::TARGET_CURRENT_ADDING);
487
        $rc->execute_precheck();
488
        $rc->execute_plan();
489
        $rc->destroy();
490
 
491
        // The quiz should contain both questions, as they have different text.
492
        $modules = get_fast_modinfo($course1->id)->get_instances_of('quiz');
493
        $this->assertCount(2, $modules);
494
        $quiz2 = end($modules);
495
        $quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->instance, $quiz2->context);
496
        $this->assertEquals($quiz2structure[1]->questionid, $question1->id);
497
        $this->assertEquals($quiz2structure[2]->questionid, $question2->id);
498
    }
499
 
500
    /**
501
     * Restore a course to another course having questions with the same stamp in a shared question bank context category.
502
     *
503
     * @dataProvider get_qtype_generators
504
     * @param string $qtype The name of the qtype plugin to test
505
     * @param ?string $testquestion The test question to generate for the plugin. If null, the plugin will be skipped
506
     *      with a message.
507
     */
508
    public function test_restore_course_with_same_stamp_questions(string $qtype, ?string $testquestion): void {
509
        global $DB, $USER;
510
        if (is_null($testquestion)) {
511
            $this->markTestSkipped(
512
                "Cannot test qtype_{$qtype} as there is no test question with a form_data method in the " .
513
                "test helper class."
514
            );
515
        }
516
        $this->resetAfterTest();
517
        $this->setAdminUser();
518
 
519
        // Create two courses and a user with editing teacher capabilities.
520
        $generator = $this->getDataGenerator();
521
        $course1 = $generator->create_course();
522
        $course2 = $generator->create_course();
523
        $qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course2->id]);
524
        $teacher = $USER;
525
        $generator->enrol_user($teacher->id, $course1->id, 'editingteacher');
526
        $generator->enrol_user($teacher->id, $course2->id, 'editingteacher');
527
 
528
        $context = \context_module::instance($qbank->cmid);
529
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
530
 
531
        // Create a question category.
532
        $cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
533
 
534
        // Create quiz with question.
535
        $quiz1 = $this->create_test_quiz($course1);
536
        $question1 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);
537
        quiz_add_quiz_question($question1->id, $quiz1, 0);
538
 
539
        $quiz2 = $this->create_test_quiz($course1);
540
        $question2 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);
541
        quiz_add_quiz_question($question2->id, $quiz2, 0);
542
 
543
        // Update question2 to have the same stamp as question1.
544
        $DB->set_field('question', 'stamp', $question1->stamp, ['id' => $question2->id]);
545
 
546
        // Change the answers of the question2 to be different to question1.
547
        $question2data = \question_bank::load_question_data($question2->id);
548
        if (!isset($question2data->options->answers) || empty($question2data->options->answers)) {
549
            $this->markTestSkipped(
550
                "Cannot test edited answers for qtype_{$qtype} as it does not use answers.",
551
            );
552
        }
553
        if ($DB->count_records('question_answers') === 0) {
554
            $this->markTestSkipped(
555
                "Cannot test edited answers for qtype_{$qtype} as it does not use the question_answers table.",
556
            );
557
        }
558
        foreach ($question2data->options->answers as $answer) {
559
            $answer->answer = 'New answer ' . $answer->id;
560
            $DB->update_record('question_answers', $answer);
561
        }
562
 
563
        $course1q1structure = \mod_quiz\question\bank\qbank_helper::get_question_structure(
564
            $quiz1->id, \context_module::instance($quiz1->cmid));
565
        $this->assertEquals($question1->id, $course1q1structure[1]->questionid);
566
        $course1q2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure(
567
            $quiz2->id, \context_module::instance($quiz2->cmid));
568
        $this->assertEquals($question2->id, $course1q2structure[1]->questionid);
569
 
570
        // Backup course1.
571
        $bc = new backup_controller(backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE,
572
            backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);
573
        $backupid = $bc->get_backupid();
574
        $bc->execute_plan();
575
        $bc->destroy();
576
 
577
        // Restore the backup, adding to course2.
578
        $rc = new restore_controller($backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
579
            $teacher->id, backup::TARGET_CURRENT_ADDING);
580
        $rc->execute_precheck();
581
        $rc->execute_plan();
582
        $rc->destroy();
583
 
584
        // Verify that the newly-restored course's quizzes use the same questions as their counterparts of course1.
585
        $modules = get_fast_modinfo($course2->id)->get_instances_of('quiz');
586
        $course1q1structure = \mod_quiz\question\bank\qbank_helper::get_question_structure(
587
                $quiz1->id, \context_module::instance($quiz1->cmid));
588
        $course2quiz1 = array_shift($modules);
589
        $course2q1structure = \mod_quiz\question\bank\qbank_helper::get_question_structure(
590
                $course2quiz1->instance, $course2quiz1->context);
591
        $this->assertEquals($question1->id, $course1q1structure[1]->questionid);
592
        $this->assertEquals($question1->id, $course2q1structure[1]->questionid);
593
 
594
        $course1q2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure(
595
                $quiz2->id, \context_module::instance($quiz2->cmid));
596
        $course2quiz2 = array_shift($modules);
597
        $course2q2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure(
598
                $course2quiz2->instance, $course2quiz2->context);
599
        $this->assertEquals($question2->id, $course1q2structure[1]->questionid);
600
        $this->assertEquals($question2->id, $course2q2structure[1]->questionid);
601
    }
602
 
603
    /**
604
     * Restore a quiz with questions of same stamp into the same course, but different hints.
605
     *
606
     * @dataProvider get_qtype_generators
607
     * @param string $qtype The name of the qtype plugin to test
608
     * @param ?string $testquestion The test question to generate for the plugin. If null, the plugin will be skipped
609
     *     with a message.
610
     */
611
    public function test_restore_quiz_with_same_stamp_questions_edited_hints(string $qtype, ?string $testquestion): void {
612
        global $DB, $USER;
613
        if (is_null($testquestion)) {
614
            $this->markTestSkipped(
615
                "Cannot test qtype_{$qtype} as there is no test question with a form_data method in the " .
616
                    "test helper class."
617
            );
618
        }
619
        $this->resetAfterTest();
620
        $this->setAdminUser();
621
 
622
        // Create a course and a user with editing teacher capabilities.
623
        $generator = $this->getDataGenerator();
624
        $course1 = $generator->create_course();
625
        $teacher = $USER;
626
        $generator->enrol_user($teacher->id, $course1->id, 'editingteacher');
627
        $qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]);
628
        $context = \context_module::instance($qbank->cmid);
629
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
630
 
631
        // Create a question category.
632
        $cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
633
 
634
        // Create 2 questions multichoice.
635
        $quiz1 = $this->create_test_quiz($course1);
636
        $question1 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);
637
        quiz_add_quiz_question($question1->id, $quiz1, 0);
638
 
639
        $question2 = $questiongenerator->create_question($qtype, $testquestion, ['category' => $cat->id]);
640
        quiz_add_quiz_question($question2->id, $quiz1, 0);
641
 
642
        // Update question2 to have the same stamp as question1.
643
        $DB->set_field('question', 'stamp', $question1->stamp, ['id' => $question2->id]);
644
 
645
        // Change the hints of the question2 to be different to question1.
646
        $hints = $DB->get_records('question_hints', ['questionid' => $question2->id]);
647
        if (empty($hints)) {
648
            $this->markTestSkipped(
649
                "Cannot test edited hints for qtype_{$qtype} as test question {$testquestion} does not use hints.",
650
            );
651
        }
652
        foreach ($hints as $hint) {
653
            $DB->set_field('question_hints', 'hint', "{$hint->hint} edited", ['id' => $hint->id]);
654
        }
655
 
656
        // Backup quiz1.
657
        $bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz1->cmid, backup::FORMAT_MOODLE,
658
            backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);
659
        $backupid = $bc->get_backupid();
660
        $bc->execute_plan();
661
        $bc->destroy();
662
 
663
        // Restore the backup into the same course.
664
        $rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
665
            $teacher->id, backup::TARGET_CURRENT_ADDING);
666
        $rc->execute_precheck();
667
        $rc->execute_plan();
668
        $rc->destroy();
669
 
670
        // Verify that the newly-restored quiz uses the same question as quiz2.
671
        $modules = get_fast_modinfo($course1->id)->get_instances_of('quiz');
672
        $this->assertCount(2, $modules);
673
        $quiz1structure = \mod_quiz\question\bank\qbank_helper::get_question_structure(
674
            $quiz1->id,
675
            \context_module::instance($quiz1->cmid),
676
        );
677
        $quiz2 = end($modules);
678
        $quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->instance, $quiz2->context);
679
        $this->assertEquals($quiz1structure[1]->questionid, $quiz2structure[1]->questionid);
680
        $this->assertEquals($quiz1structure[2]->questionid, $quiz2structure[2]->questionid);
681
 
682
    }
683
 
684
    /**
685
     * Return a set of options fields and new values.
686
     *
687
     * @return array
688
     */
689
    public static function get_edited_option_fields(): array {
690
        return [
691
            'single' => [
692
                'single',
693
                '0',
694
            ],
695
            'shuffleanswers' => [
696
                'shuffleanswers',
697
                '0',
698
            ],
699
            'answernumbering' => [
700
                'answernumbering',
701
                'ABCD',
702
            ],
703
            'shownumcorrect' => [
704
                'shownumcorrect',
705
                '0',
706
            ],
707
            'showstandardinstruction' => [
708
                'showstandardinstruction',
709
                '1',
710
            ],
711
            'correctfeedback' => [
712
                'correctfeedback',
713
                'edited',
714
            ],
715
            'partiallycorrectfeedback' => [
716
                'partiallycorrectfeedback',
717
                'edited',
718
            ],
719
            'incorrectfeedback' => [
720
                'incorrectfeedback',
721
                'edited',
722
            ],
723
        ];
724
    }
725
 
726
    /**
727
     * Restore a quiz with questions of same stamp into the same course, but different qtype-specific options.
728
     *
729
     * @dataProvider get_edited_option_fields
730
     * @param string $field The answer field to edit
731
     * @param string $value The value to set
732
     */
733
    public function test_restore_quiz_with_same_stamp_questions_edited_options(string $field, string $value): void {
734
        global $DB, $USER;
735
        $this->resetAfterTest();
736
        $this->setAdminUser();
737
 
738
        // Create a course and a user with editing teacher capabilities.
739
        $generator = $this->getDataGenerator();
740
        $course1 = $generator->create_course();
741
        $teacher = $USER;
742
        $generator->enrol_user($teacher->id, $course1->id, 'editingteacher');
743
        $qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]);
744
        $context = \context_module::instance($qbank->cmid);
745
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
746
 
747
        // Create a question category.
748
        $cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
749
 
750
        // A quiz with 2 multichoice questions.
751
        $quiz1 = $this->create_test_quiz($course1);
752
        $question1 = $questiongenerator->create_question('multichoice', 'one_of_four', ['category' => $cat->id]);
753
        quiz_add_quiz_question($question1->id, $quiz1, 0);
754
 
755
        $question2 = $questiongenerator->create_question('multichoice', 'one_of_four', ['category' => $cat->id]);
756
        quiz_add_quiz_question($question2->id, $quiz1, 0);
757
 
758
        // Update question2 to have the same stamp as question1.
759
        $DB->set_field('question', 'stamp', $question1->stamp, ['id' => $question2->id]);
760
 
761
        // Change the options of question2 to be different to question1.
762
        $DB->set_field('qtype_multichoice_options', $field, $value, ['questionid' => $question2->id]);
763
 
764
        // Backup quiz.
765
        $bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz1->cmid, backup::FORMAT_MOODLE,
766
            backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);
767
        $backupid = $bc->get_backupid();
768
        $bc->execute_plan();
769
        $bc->destroy();
770
 
771
        // Restore the backup into the same course.
772
        $rc = new restore_controller($backupid, $course1->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
773
            $teacher->id, backup::TARGET_CURRENT_ADDING);
774
        $rc->execute_precheck();
775
        $rc->execute_plan();
776
        $rc->destroy();
777
 
778
        // Verify that the newly-restored quiz questions match their quiz1 counterparts.
779
        $modules = get_fast_modinfo($course1->id)->get_instances_of('quiz');
780
        $this->assertCount(2, $modules);
781
        $quiz1structure = \mod_quiz\question\bank\qbank_helper::get_question_structure(
782
            $quiz1->id,
783
            \context_module::instance($quiz1->cmid),
784
        );
785
        $quiz2 = end($modules);
786
        $quiz2structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz2->instance, $quiz2->context);
787
        $this->assertEquals($quiz1structure[1]->questionid, $quiz2structure[1]->questionid);
788
        $this->assertEquals($quiz1structure[2]->questionid, $quiz2structure[2]->questionid);
789
    }
790
}