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 core_question;
18
 
1441 ariadna 19
use context_course;
20
use mod_quiz\quiz_settings;
21
use moodle_url;
22
use question_bank;
23
 
1 efrain 24
defined('MOODLE_INTERNAL') || die();
25
 
1441 ariadna 26
use backup;
27
use core_question\local\bank\question_bank_helper;
28
use restore_controller;
29
use restore_dbops;
30
 
1 efrain 31
global $CFG;
32
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
33
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
34
 
35
/**
36
 * Class core_question_backup_testcase
37
 *
38
 * @package    core_question
39
 * @category   test
40
 * @copyright  2018 Shamim Rezaie <shamim@moodle.com>
41
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1441 ariadna 42
 * @covers     \restore_qtype_plugin
43
 * @covers     \restore_create_categories_and_questions
44
 * @covers     \restore_move_module_questions_categories
1 efrain 45
 */
1441 ariadna 46
final class backup_test extends \advanced_testcase {
1 efrain 47
 
48
    /**
49
     * Makes a backup of the course.
50
     *
51
     * @param \stdClass $course The course object.
52
     * @return string Unique identifier for this backup.
53
     */
54
    protected function backup_course($course) {
55
        global $CFG, $USER;
56
 
57
        // Turn off file logging, otherwise it can't delete the file (Windows).
58
        $CFG->backup_file_logger_level = \backup::LOG_NONE;
59
 
60
        // Do backup with default settings. MODE_IMPORT means it will just
61
        // create the directory and not zip it.
62
        $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id,
63
                \backup::FORMAT_MOODLE, \backup::INTERACTIVE_NO, \backup::MODE_IMPORT,
64
                $USER->id);
65
        $backupid = $bc->get_backupid();
66
        $bc->execute_plan();
67
        $bc->destroy();
68
 
69
        return $backupid;
70
    }
71
 
72
    /**
1441 ariadna 73
     * Makes a backup of a course module.
74
     *
75
     * @param int $modid The course_module id.
76
     * @return string Unique identifier for this backup.
77
     */
78
    protected function backup_course_module(int $modid) {
79
        global $CFG, $USER;
80
 
81
        // Turn off file logging, otherwise it can't delete the file (Windows).
82
        $CFG->backup_file_logger_level = \backup::LOG_NONE;
83
 
84
        // Do backup with default settings. MODE_IMPORT means it will just
85
        // create the directory and not zip it.
86
        $bc = new \backup_controller(\backup::TYPE_1ACTIVITY, $modid,
87
            \backup::FORMAT_MOODLE, \backup::INTERACTIVE_NO, \backup::MODE_IMPORT,
88
            $USER->id);
89
        $backupid = $bc->get_backupid();
90
        $bc->execute_plan();
91
        $bc->destroy();
92
 
93
        return $backupid;
94
    }
95
 
96
    /**
1 efrain 97
     * Restores a backup that has been made earlier.
98
     *
99
     * @param string $backupid The unique identifier of the backup.
1441 ariadna 100
     * @param int $courseid Course id of where the restore is happening.
1 efrain 101
     * @param string[] $expectedprecheckwarning
102
     */
1441 ariadna 103
    protected function restore_to_course(string $backupid, int $courseid, array $expectedprecheckwarning = []): void {
1 efrain 104
        global $CFG, $USER;
105
 
106
        // Turn off file logging, otherwise it can't delete the file (Windows).
107
        $CFG->backup_file_logger_level = \backup::LOG_NONE;
108
 
1441 ariadna 109
        $rc = new \restore_controller($backupid, $courseid,
1 efrain 110
                \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
111
                \backup::TARGET_NEW_COURSE);
112
 
113
        $precheck = $rc->execute_precheck();
114
        if (!$expectedprecheckwarning) {
115
            $this->assertTrue($precheck);
116
        } else {
117
            $precheckresults = $rc->get_precheck_results();
118
            $this->assertEqualsCanonicalizing($expectedprecheckwarning, $precheckresults['warnings']);
119
            $this->assertCount(1, $precheckresults);
120
        }
121
        $rc->execute_plan();
122
        $rc->destroy();
123
    }
124
 
125
    /**
1441 ariadna 126
     * This function tests backup and restore of question tags.
1 efrain 127
     */
11 efrain 128
    public function test_backup_question_tags(): void {
1 efrain 129
        global $DB;
130
 
131
        $this->resetAfterTest();
132
        $this->setAdminUser();
133
 
1441 ariadna 134
        // Create a new course category and a new course in that.
1 efrain 135
        $category1 = $this->getDataGenerator()->create_category();
136
        $course = $this->getDataGenerator()->create_course(['category' => $category1->id]);
137
        $courseshortname = $course->shortname;
138
        $coursefullname = $course->fullname;
139
 
140
        // Create 2 questions.
141
        $qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
1441 ariadna 142
        $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id]);
143
        $context = \context_module::instance($qbank->cmid);
144
        $qcat = question_get_default_category($context->id);
1 efrain 145
        $question1 = $qgen->create_question('shortanswer', null, ['category' => $qcat->id, 'idnumber' => 'q1']);
146
        $question2 = $qgen->create_question('shortanswer', null, ['category' => $qcat->id, 'idnumber' => 'q2']);
147
 
1441 ariadna 148
        // Tag the questions with 2 question tags.
1 efrain 149
        $qcontext = \context::instance_by_id($qcat->contextid);
1441 ariadna 150
        $coursecontext = context_course::instance($course->id);
1 efrain 151
        \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['qtag1', 'qtag2']);
152
        \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['qtag3', 'qtag4']);
153
 
154
        // Create a quiz and add one of the questions to that.
155
        $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
156
        quiz_add_quiz_question($question1->id, $quiz);
157
 
158
        // Backup the course twice for future use.
159
        $backupid1 = $this->backup_course($course);
160
        $backupid2 = $this->backup_course($course);
161
 
162
        // Now delete almost everything.
163
        delete_course($course, false);
164
        question_delete_question($question1->id);
165
        question_delete_question($question2->id);
166
 
167
        // Restore the backup we had made earlier into a new course.
1441 ariadna 168
        // Do restore to new course with default settings.
169
        $courseid2 = \restore_dbops::create_new_course($coursefullname, $courseshortname . '_2', $category1->id);
170
        $this->restore_to_course($backupid1, $courseid2);
171
        $modinfo = get_fast_modinfo($courseid2);
172
        $qbanks = $modinfo->get_instances_of('qbank');
173
        $qbanks = array_filter($qbanks, static fn($qbank) => $qbank->get_name() === 'Question bank 1');
174
        $this->assertCount(1, $qbanks);
175
        $qbank = reset($qbanks);
176
        $qbankcontext = \context_module::instance($qbank->id);
177
        $cats = $DB->get_records_select('question_categories', 'parent <> 0 AND contextid = ?', [$qbankcontext->id]);
178
        $this->assertCount(1, $cats);
179
        $cat = reset($cats);
1 efrain 180
 
1441 ariadna 181
        // The questions should be restored to a mod_qbank context in the new course.
1 efrain 182
        $sql = 'SELECT q.*,
183
                       qbe.idnumber
184
                  FROM {question} q
185
                  JOIN {question_versions} qv ON qv.questionid = q.id
186
                  JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
187
                 WHERE qbe.questioncategoryid = ?
188
                 ORDER BY qbe.idnumber';
1441 ariadna 189
        $questions = $DB->get_records_sql($sql, [$cat->id]);
1 efrain 190
        $this->assertCount(2, $questions);
191
 
192
        // Retrieve tags for each question and check if they are assigned at the right context.
193
        $qcount = 1;
194
        foreach ($questions as $question) {
195
            $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
196
 
197
            // Each question is tagged with 4 tags (2 question tags + 2 course tags).
1441 ariadna 198
            $this->assertCount(2, $tags);
1 efrain 199
 
200
            foreach ($tags as $tag) {
1441 ariadna 201
                $this->assertEquals($qbankcontext->id, $tag->taginstancecontextid);
1 efrain 202
            }
203
 
204
            // Also check idnumbers have been backed up and restored.
205
            $this->assertEquals('q' . $qcount, $question->idnumber);
206
            $qcount++;
207
        }
208
 
209
        // Now, again, delete everything including the course category.
210
        delete_course($courseid2, false);
211
        foreach ($questions as $question) {
212
            question_delete_question($question->id);
213
        }
214
        $category1->delete_full(false);
215
 
216
        // Create a new course category to restore the backup file into it.
217
        $category2 = $this->getDataGenerator()->create_category();
218
 
219
        // Restore to a new course in the new course category.
1441 ariadna 220
        $courseid3 = \restore_dbops::create_new_course($coursefullname, $courseshortname . '_3', $category2->id);
221
        $this->restore_to_course($backupid2, $courseid3);
222
        $modinfo = get_fast_modinfo($courseid3);
223
        $qbanks = $modinfo->get_instances_of('qbank');
224
        $qbanks = array_filter($qbanks, static fn($qbank) => $qbank->get_name() === 'Question bank 1');
225
        $this->assertCount(1, $qbanks);
226
        $qbank = reset($qbanks);
227
        $context = \context_module::instance($qbank->id);
1 efrain 228
 
229
        // The questions should have been moved to a question category that belongs to a course context.
230
        $questions = $DB->get_records_sql("SELECT q.*
231
                                                FROM {question} q
232
                                                JOIN {question_versions} qv ON qv.questionid = q.id
233
                                                JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
234
                                                JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
1441 ariadna 235
                                               WHERE qc.contextid = ?", [$context->id]);
1 efrain 236
        $this->assertCount(2, $questions);
237
 
238
        // Now, retrieve tags for each question and check if they are assigned at the right context.
239
        foreach ($questions as $question) {
240
            $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
241
 
1441 ariadna 242
            // Each question is tagged with 2 tags (all are question context tags now).
243
            $this->assertCount(2, $tags);
1 efrain 244
 
245
            foreach ($tags as $tag) {
1441 ariadna 246
                $this->assertEquals($context->id, $tag->taginstancecontextid);
1 efrain 247
            }
248
        }
249
 
250
    }
251
 
252
    /**
253
     * Test that the question author is retained when they are enrolled in to the course.
254
     */
11 efrain 255
    public function test_backup_question_author_retained_when_enrolled(): void {
1 efrain 256
        global $DB, $USER, $CFG;
257
        $this->resetAfterTest();
258
        $this->setAdminUser();
259
 
260
        // Create a course, a category and a user.
261
        $course = $this->getDataGenerator()->create_course();
262
        $category = $this->getDataGenerator()->create_category();
263
        $user = $this->getDataGenerator()->create_user();
264
 
265
        // Create a question.
266
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
267
        $questioncategory = $questiongenerator->create_question_category();
268
        $overrides = ['name' => 'Test question', 'category' => $questioncategory->id,
269
                'createdby' => $user->id, 'modifiedby' => $user->id];
270
        $question = $questiongenerator->create_question('truefalse', null, $overrides);
271
 
272
        // Create a quiz and a questions.
273
        $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
274
        quiz_add_quiz_question($question->id, $quiz);
275
 
276
        // Enrol user with a teacher role.
277
        $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
278
        $this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id, 'manual');
279
 
280
        // Backup the course.
281
        $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE,
282
            \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id);
283
        $backupid = $bc->get_backupid();
284
        $bc->execute_plan();
285
        $results = $bc->get_results();
286
        $file = $results['backup_destination'];
287
        $fp = get_file_packer('application/vnd.moodle.backup');
288
        $filepath = $CFG->dataroot . '/temp/backup/' . $backupid;
289
        $file->extract_to_pathname($fp, $filepath);
290
        $bc->destroy();
291
 
292
        // Delete the original course and related question.
293
        delete_course($course, false);
294
        question_delete_question($question->id);
295
 
296
        // Restore the course.
297
        $restoredcourseid = \restore_dbops::create_new_course($course->fullname, $course->shortname . '_1', $category->id);
298
        $rc = new \restore_controller($backupid, $restoredcourseid, \backup::INTERACTIVE_NO,
299
            \backup::MODE_GENERAL, $USER->id, \backup::TARGET_NEW_COURSE);
300
        $rc->execute_precheck();
301
        $rc->execute_plan();
302
        $rc->destroy();
303
 
304
        // Test the question author.
305
        $questions = $DB->get_records('question', ['name' => 'Test question']);
306
        $this->assertCount(1, $questions);
307
        $question3 = array_shift($questions);
308
        $this->assertEquals($user->id, $question3->createdby);
309
        $this->assertEquals($user->id, $question3->modifiedby);
310
    }
311
 
312
    /**
313
     * Test that the question author is retained when they are not enrolled in to the course,
314
     * but we are restoring the backup at the same site.
315
     */
11 efrain 316
    public function test_backup_question_author_retained_when_not_enrolled(): void {
1 efrain 317
        global $DB, $USER, $CFG;
318
        $this->resetAfterTest();
319
        $this->setAdminUser();
320
 
321
        // Create a course, a category and a user.
322
        $course = $this->getDataGenerator()->create_course();
323
        $category = $this->getDataGenerator()->create_category();
324
        $user = $this->getDataGenerator()->create_user();
325
 
326
        // Create a question.
327
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
328
        $questioncategory = $questiongenerator->create_question_category();
329
        $overrides = ['name' => 'Test question', 'category' => $questioncategory->id,
330
                'createdby' => $user->id, 'modifiedby' => $user->id];
331
        $question = $questiongenerator->create_question('truefalse', null, $overrides);
332
 
333
        // Create a quiz and a questions.
334
        $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
335
        quiz_add_quiz_question($question->id, $quiz);
336
 
337
        // Backup the course.
338
        $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE,
339
            \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id);
340
        $backupid = $bc->get_backupid();
341
        $bc->execute_plan();
342
        $results = $bc->get_results();
343
        $file = $results['backup_destination'];
344
        $fp = get_file_packer('application/vnd.moodle.backup');
345
        $filepath = $CFG->dataroot . '/temp/backup/' . $backupid;
346
        $file->extract_to_pathname($fp, $filepath);
347
        $bc->destroy();
348
 
349
        // Delete the original course and related question.
350
        delete_course($course, false);
351
        question_delete_question($question->id);
352
 
353
        // Restore the course.
354
        $restoredcourseid = \restore_dbops::create_new_course($course->fullname, $course->shortname . '_1', $category->id);
355
        $rc = new \restore_controller($backupid, $restoredcourseid, \backup::INTERACTIVE_NO,
356
            \backup::MODE_GENERAL, $USER->id, \backup::TARGET_NEW_COURSE);
357
        $rc->execute_precheck();
358
        $rc->execute_plan();
359
        $rc->destroy();
360
 
361
        // Test the question author.
362
        $questions = $DB->get_records('question', ['name' => 'Test question']);
363
        $this->assertCount(1, $questions);
364
        $question = array_shift($questions);
365
        $this->assertEquals($user->id, $question->createdby);
366
        $this->assertEquals($user->id, $question->modifiedby);
367
    }
368
 
369
    /**
370
     * Test that the current user is set as a question author when we are restoring the backup
371
     * at the another site and the question author is not enrolled in to the course.
372
     */
11 efrain 373
    public function test_backup_question_author_reset(): void {
1 efrain 374
        global $DB, $USER, $CFG;
375
        $this->resetAfterTest();
376
        $this->setAdminUser();
377
 
378
        // Create a course, a category and a user.
379
        $course = $this->getDataGenerator()->create_course();
380
        $category = $this->getDataGenerator()->create_category();
381
        $user = $this->getDataGenerator()->create_user();
382
 
383
        // Create a question.
384
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
385
        $questioncategory = $questiongenerator->create_question_category();
386
        $overrides = ['name' => 'Test question', 'category' => $questioncategory->id,
387
                'createdby' => $user->id, 'modifiedby' => $user->id];
388
        $question = $questiongenerator->create_question('truefalse', null, $overrides);
389
 
390
        // Create a quiz and a questions.
391
        $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
392
        quiz_add_quiz_question($question->id, $quiz);
393
 
394
        // Backup the course.
395
        $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE,
396
            \backup::INTERACTIVE_NO, \backup::MODE_SAMESITE, $USER->id);
397
        $backupid = $bc->get_backupid();
398
        $bc->execute_plan();
399
        $results = $bc->get_results();
400
        $file = $results['backup_destination'];
401
        $fp = get_file_packer('application/vnd.moodle.backup');
402
        $filepath = $CFG->dataroot . '/temp/backup/' . $backupid;
403
        $file->extract_to_pathname($fp, $filepath);
404
        $bc->destroy();
405
 
406
        // Delete the original course and related question.
407
        delete_course($course, false);
408
        question_delete_question($question->id);
409
 
410
        // Emulate restoring to a different site.
411
        set_config('siteidentifier', random_string(32) . 'not the same site');
412
 
413
        // Restore the course.
414
        $restoredcourseid = \restore_dbops::create_new_course($course->fullname, $course->shortname . '_1', $category->id);
415
        $rc = new \restore_controller($backupid, $restoredcourseid, \backup::INTERACTIVE_NO,
416
            \backup::MODE_SAMESITE, $USER->id, \backup::TARGET_NEW_COURSE);
417
        $rc->execute_precheck();
418
        $rc->execute_plan();
419
        $rc->destroy();
420
 
421
        // Test the question author.
422
        $questions = $DB->get_records('question', ['name' => 'Test question']);
423
        $this->assertCount(1, $questions);
424
        $question = array_shift($questions);
425
        $this->assertEquals($USER->id, $question->createdby);
426
        $this->assertEquals($USER->id, $question->modifiedby);
427
    }
1441 ariadna 428
 
429
    public function test_backup_and_restore_recodes_links_in_questions(): void {
430
        global $DB, $USER, $CFG;
431
        $this->resetAfterTest();
432
        $this->setAdminUser();
433
 
434
        // Create a course and a category.
435
        $course = $this->getDataGenerator()->create_course();
436
        $qbank = self::getDataGenerator()->create_module('qbank', ['course' => $course->id]);
437
        $category = $this->getDataGenerator()->create_category();
438
 
439
        // Create a question with links in all the places that should be recoded.
440
        $testlink = new moodle_url('/course/view.php', ['id' => $course->id]);
441
        $testcontent = 'Look at <a href="' . $testlink . '">the course</a>.';
442
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
443
        $questioncategory = $questiongenerator->create_question_category(
444
            ['contextid' => \context_module::instance($qbank->cmid)->id]);
445
        $question = $questiongenerator->create_question('multichoice', null, [
446
            'name' => 'Test question',
447
            'category' => $questioncategory->id,
448
            'questiontext' => ['text' => 'This is the question. ' . $testcontent],
449
            'generalfeedback' => ['text' => 'Why is this right? ' . $testcontent],
450
            'answer' => [
451
                '0' => ['text' => 'Choose me! ' . $testcontent],
452
            ],
453
            'feedback' => [
454
                '0' => ['text' => 'The reason: ' . $testcontent],
455
            ],
456
            'hint' => [
457
                '0' => ['text' => 'Hint: ' . $testcontent],
458
            ],
459
        ]);
460
 
461
        // Create a quiz and add the question.
462
        $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
463
        quiz_add_quiz_question($question->id, $quiz);
464
 
465
        // Backup and restore the course.
466
        $backupid = $this->backup_course($course);
467
        $newcourse = $this->getDataGenerator()->create_course();
468
        $this->restore_to_course($backupid, $newcourse->id);
469
        $modinfo = get_fast_modinfo($newcourse);
470
        $qbanks = $modinfo->get_instances_of('qbank');
471
        $qbank = reset($qbanks);
472
 
473
        // Get the question from the restored course - we are expecting just one, but that is not the real test here.
474
        $restoredquestions = $DB->get_records_sql("
475
                SELECT q.id, q.name
476
                  FROM {question} q
477
                  JOIN {question_versions} qv ON qv.questionid = q.id
478
                  JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
479
                  JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
480
                 WHERE qc.contextid = ?
481
            ", [\context_module::instance($qbank->id)->id]);
482
        $this->assertCount(1, $restoredquestions);
483
        $questionid = array_key_first($restoredquestions);
484
        $this->assertEquals('Test question', $restoredquestions[$questionid]->name);
485
 
486
        // Verify the links have been recoded.
487
        $restoredquestion = question_bank::load_question_data($questionid);
488
        $recodedlink = new moodle_url('/course/view.php', ['id' => $newcourse->id]);
489
        $recodedcontent = 'Look at <a href="' . $recodedlink . '">the course</a>.';
490
        $firstanswerid = array_key_first($restoredquestion->options->answers);
491
        $firsthintid = array_key_first($restoredquestion->hints);
492
 
493
        $this->assertEquals('This is the question. ' . $recodedcontent, $restoredquestion->questiontext);
494
        $this->assertEquals('Why is this right? ' . $recodedcontent, $restoredquestion->generalfeedback);
495
        $this->assertEquals('Choose me! ' . $recodedcontent, $restoredquestion->options->answers[$firstanswerid]->answer);
496
        $this->assertEquals('The reason: ' . $recodedcontent, $restoredquestion->options->answers[$firstanswerid]->feedback);
497
        $this->assertEquals('Hint: ' . $recodedcontent, $restoredquestion->hints[$firsthintid]->hint);
498
    }
499
 
500
    /**
501
     * Boilerplate setup for the tests. Creates a course, a quiz, and a qbank module. It adds a category to each module context
502
     * and adds a question to each category. Finally, it adds the 2 questions to the quiz.
503
     *
504
     * @return \stdClass
505
     */
506
    private function add_course_quiz_and_qbank() {
507
        $qgen = self::getDataGenerator()->get_plugin_generator('core_question');
508
 
509
        // Create a new course.
510
        $course = self::getDataGenerator()->create_course();
511
 
512
        // Create a question bank module instance, a category for that module, and a question for that category.
513
        $qbank = self::getDataGenerator()->create_module(
514
            'qbank',
515
            ['type' => question_bank_helper::TYPE_STANDARD, 'course' => $course->id]
516
        );
517
        $qbankcontext = \context_module::instance($qbank->cmid);
518
        $bankqcat = question_get_default_category($qbankcontext->id);
519
        $bankquestion = $qgen->create_question('shortanswer',
520
            null,
521
            ['name' => 'bank question', 'category' => $bankqcat->id, 'idnumber' => 'bankq1']
522
        );
523
 
524
        // Create a quiz module instance, a category for that module, and a question for that category.
525
        $quiz = self::getDataGenerator()->create_module('quiz', ['course' => $course->id]);
526
        $quizcontext = \context_module::instance($quiz->cmid);
527
        $quizqcat = question_get_default_category($quizcontext->id);
528
        $quizquestion = $qgen->create_question('shortanswer',
529
            null,
530
            ['name' => 'quiz question', 'category' => $quizqcat->id, 'idnumber' => 'quizq1']
531
        );
532
 
533
        quiz_add_quiz_question($bankquestion->id, $quiz);
534
        quiz_add_quiz_question($quizquestion->id, $quiz);
535
 
536
        $data = new \stdClass();
537
        $data->course = $course;
538
        $data->qbank = $qbank;
539
        $data->qbankcategory = $bankqcat;
540
        $data->qbankquestion = $bankquestion;
541
        $data->quiz = $quiz;
542
        $data->quizcategory = $quizqcat;
543
        $data->quizquestion = $quizquestion;
544
 
545
        return $data;
546
    }
547
 
548
    /**
549
     * If the backup contains ONLY a quiz but that quiz uses questions from a qbank module and itself,
550
     * and the original course does not exist on the target system,
551
     * then the non-quiz context categories and questions should restore to a default qbank module on the new course
552
     * if the old qbank no longer exists.
553
     */
554
    public function test_quiz_activity_restore_to_new_course(): void {
555
        global $DB;
556
 
557
        $this->resetAfterTest();
558
        self::setAdminUser();
559
 
560
        // Create a course to make a backup.
561
        $data = $this->add_course_quiz_and_qbank();
562
        $oldquiz = $data->quiz;
563
 
564
        // Backup ONLY the quiz module.
565
        $backupid = $this->backup_course_module($oldquiz->cmid);
566
 
567
        // Create a new course to restore to.
568
        $newcourse = self::getDataGenerator()->create_course();
569
        delete_course($data->course->id, false);
570
 
571
        $this->restore_to_course($backupid, $newcourse->id);
572
        $modinfo = get_fast_modinfo($newcourse);
573
 
574
        // Assert we have our quiz including the category and question.
575
        $newquizzes = $modinfo->get_instances_of('quiz');
576
        $this->assertCount(1, $newquizzes);
577
        $newquiz = reset($newquizzes);
578
        $newquizcontext = \context_module::instance($newquiz->id);
579
 
580
        $quizcats = $DB->get_records_select('question_categories',
581
            'parent <> 0 AND contextid = :contextid',
582
            ['contextid' => $newquizcontext->id]
583
        );
584
        $this->assertCount(1, $quizcats);
585
        $quizcat = reset($quizcats);
586
        $quizcatqs = get_questions_category($quizcat, false);
587
        $this->assertCount(1, $quizcatqs);
588
        $quizq = reset($quizcatqs);
589
        $this->assertEquals('quiz question', $quizq->name);
590
 
591
        // The backup did not contain the qbank that held the categories, but it is dependant.
592
        // So make sure the categories and questions got restored to a 'system' type default qbank module on the course.
593
        $defaultbanks = $modinfo->get_instances_of('qbank');
594
        $this->assertCount(1, $defaultbanks);
595
        $defaultbank = reset($defaultbanks);
596
        $defaultbankcontext = \context_module::instance($defaultbank->id);
597
        $bankcats = $DB->get_records_select('question_categories',
598
            'parent <> 0 AND contextid = :contextid',
599
            ['contextid' => $defaultbankcontext->id]
600
        );
601
        $bankcat = reset($bankcats);
602
        $bankqs = get_questions_category($bankcat, false);
603
        $this->assertCount(1, $bankqs);
604
        $bankq = reset($bankqs);
605
        $this->assertEquals('bank question', $bankq->name);
606
    }
607
 
608
    /**
609
     * If the backup contains ONLY a quiz but that quiz uses questions from a qbank module and itself,
610
     * and the original course does exist on the target system but you dont have permission to view the original qbank,
611
     * then the non-quiz context categories and questions should restore to a default qbank module on the new course
612
     * if the old qbank no longer exists.
613
     */
614
    public function test_quiz_activity_restore_to_new_course_no_permission(): void {
615
        global $DB;
616
 
617
        $this->resetAfterTest();
618
        self::setAdminUser();
619
 
620
        // Create a course to make a backup.
621
        $data = $this->add_course_quiz_and_qbank();
622
        $oldquiz = $data->quiz;
623
 
624
        // Backup ONLY the quiz module.
625
        $backupid = $this->backup_course_module($oldquiz->cmid);
626
 
627
        // Create a new course to restore to.
628
        $newcourse = self::getDataGenerator()->create_course();
629
        $restoreuser = self::getDataGenerator()->create_user();
630
        self::getDataGenerator()->enrol_user($restoreuser->id, $newcourse->id, 'manager');
631
        $this->setUser($restoreuser);
632
 
633
        $this->restore_to_course($backupid, $newcourse->id);
634
        $modinfo = get_fast_modinfo($newcourse);
635
 
636
        // Assert we have our quiz including the category and question.
637
        $newquizzes = $modinfo->get_instances_of('quiz');
638
        $this->assertCount(1, $newquizzes);
639
        $newquiz = reset($newquizzes);
640
        $newquizcontext = \context_module::instance($newquiz->id);
641
 
642
        $quizcats = $DB->get_records_select('question_categories',
643
            'parent <> 0 AND contextid = :contextid',
644
            ['contextid' => $newquizcontext->id]
645
        );
646
        $this->assertCount(1, $quizcats);
647
        $quizcat = reset($quizcats);
648
        $quizcatqs = get_questions_category($quizcat, false);
649
        $this->assertCount(1, $quizcatqs);
650
        $quizq = reset($quizcatqs);
651
        $this->assertEquals('quiz question', $quizq->name);
652
 
653
        // The backup did not contain the qbank that held the categories, but it is dependant.
654
        // So make sure the categories and questions got restored to a qbank module on the course.
655
        $defaultbanks = $modinfo->get_instances_of('qbank');
656
        $this->assertCount(1, $defaultbanks);
657
        $defaultbank = reset($defaultbanks);
658
        $defaultbankcontext = \context_module::instance($defaultbank->id);
659
        $bankcats = $DB->get_records_select('question_categories',
660
            'parent <> 0 AND contextid = :contextid',
661
            ['contextid' => $defaultbankcontext->id]
662
        );
663
        $bankcat = reset($bankcats);
664
        $bankqs = get_questions_category($bankcat, false);
665
        $this->assertCount(1, $bankqs);
666
        $bankq = reset($bankqs);
667
        $this->assertEquals('bank question', $bankq->name);
668
    }
669
 
670
    /**
671
     * If the backup contains ONLY a quiz but that quiz uses questions from a qbank module and itself,
672
     * and that qbank still exists on the system, and the restoring user can access that qbank, then
673
     * the quiz should be restored with a copy of the quiz question, and a reference to the original qbank question.
674
     */
675
    public function test_quiz_activity_restore_to_new_course_by_reference(): void {
676
        global $DB;
677
 
678
        $this->resetAfterTest();
679
        self::setAdminUser();
680
 
681
        // Create a course to make a backup.
682
        $data = $this->add_course_quiz_and_qbank();
683
        $oldquiz = $data->quiz;
684
 
685
        // Backup ONLY the quiz module.
686
        $backupid = $this->backup_course_module($oldquiz->cmid);
687
 
688
        // Create a new course to restore to.
689
        $newcourse = self::getDataGenerator()->create_course();
690
 
691
        $this->restore_to_course($backupid, $newcourse->id);
692
        $modinfo = get_fast_modinfo($newcourse);
693
 
694
        // Assert we have our new quiz with the expected questions.
695
        $newquizzes = $modinfo->get_instances_of('quiz');
696
        $this->assertCount(1, $newquizzes);
697
        /** @var \cm_info $newquiz */
698
        $newquiz = reset($newquizzes);
699
        $quiz = $DB->get_record('quiz', ['id' => $newquiz->instance], '*', MUST_EXIST);
700
        [$course, $cm] = get_course_and_cm_from_instance($quiz, 'quiz');
701
        $newquizsettings = new quiz_settings($quiz, $cm, $course);
702
        $newq1 = $newquizsettings->get_structure()->get_question_in_slot(1);
703
        $newq2 = $newquizsettings->get_structure()->get_question_in_slot(2);
704
 
705
        $newquizcontext = \context_module::instance($newquiz->id);
706
        $qbankcontext = \context_module::instance($data->qbank->cmid);
707
 
708
        // Check we've got a copy of the quiz question in the new context.
709
        $this->assertEquals($data->quizquestion->name, $newq2->name);
710
        $this->assertEquals($newquizcontext->id, $newq2->contextid);
711
        // Check we've got a reference to the qbank question in the original context.
712
        $this->assertEquals($data->qbankquestion->name, $newq1->name);
713
        $this->assertEquals($qbankcontext->id, $newq1->contextid);
714
        // Check we have the expected restored categories.
715
        $this->assertEquals(2, $DB->count_records('question_categories', ['stamp' => $data->quizcategory->stamp]));
716
        $this->assertEquals(1, $DB->count_records('question_categories', ['stamp' => $data->qbankcategory->stamp]));
717
    }
718
 
719
    /**
720
     * If the backup contains BOTH a quiz and a qbank module and the quiz uses questions from the qbank module and itself,
721
     * then we need to restore the categories and questions to the qbank and quiz modules included in the backup on the new course.
722
     *
723
     * @return void
724
     * @covers \restore_controller::execute_plan()
725
     */
726
    public function test_bank_and_quiz_activity_restore_to_new_course(): void {
727
        // Create a new course.
728
        global $DB;
729
 
730
        $this->resetAfterTest();
731
        self::setAdminUser();
732
 
733
        // Create a course to make a backup from.
734
        $data = $this->add_course_quiz_and_qbank();
735
        $oldcourse = $data->course;
736
 
737
        // Backup the course.
738
        $backupid = $this->backup_course($oldcourse);
739
 
740
        // Create a new course to restore to.
741
        $newcourse = self::getDataGenerator()->create_course();
742
 
743
        // Restore it.
744
        $this->restore_to_course($backupid, $newcourse->id);
745
 
746
        // Assert the quiz got its question catregories restored.
747
        $modinfo = get_fast_modinfo($newcourse);
748
        $newquizzes = $modinfo->get_instances_of('quiz');
749
        $this->assertCount(1, $newquizzes);
750
        $newquiz = reset($newquizzes);
751
        $newquizcontext = \context_module::instance($newquiz->id);
752
        $quizcats = $DB->get_records_select('question_categories',
753
            'parent <> 0 AND contextid = :contextid',
754
            ['contextid' => $newquizcontext->id]
755
        );
756
        $quizcat = reset($quizcats);
757
        $quizcatqs = get_questions_category($quizcat, false);
758
        $this->assertCount(1, $quizcatqs);
759
        $quizcatq = reset($quizcatqs);
760
        $this->assertEquals('quiz question', $quizcatq->name);
761
 
762
        // Assert the qbank got its questions restored to the module in the backup.
763
        $qbanks = $modinfo->get_instances_of('qbank');
764
        $qbanks = array_filter($qbanks, static function($bank) {
765
            global $DB;
766
            $modrecord = $DB->get_record('qbank', ['id' => $bank->instance]);
767
            return $modrecord->type === question_bank_helper::TYPE_STANDARD;
768
        });
769
        $this->assertCount(1, $qbanks);
770
        $qbank = reset($qbanks);
771
        $bankcats = $DB->get_records_select('question_categories',
772
            'parent <> 0 AND contextid = :contextid',
773
            ['contextid' => \context_module::instance($qbank->id)->id]
774
        );
775
        $bankcat = reset($bankcats);
776
        $bankqs = get_questions_category($bankcat, false);
777
        $this->assertCount(1, $bankqs);
778
        $bankq = reset($bankqs);
779
        $this->assertEquals('bank question', $bankq->name);
780
    }
781
 
782
    /**
783
     * The course backup file contains question banks and a quiz module.
784
     * There is 1 question bank category per deprecated context level i.e. CONTEXT_SYSTEM, CONTEXT_COURSECAT, and CONTEXT_COURSE.
785
     * The quiz included in the backup uses a question in each category.
786
     *
787
     * @return void
788
     * @covers \restore_controller::execute_plan()
789
     */
790
    public function test_pre_46_course_restore_to_new_course(): void {
791
        global $DB, $USER;
792
        self::setAdminUser();
793
        $this->resetAfterTest();
794
 
795
        $backupid = 'question_category_45_format';
796
        $backuppath = make_backup_temp_directory($backupid);
797
        get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
798
            __DIR__ . "/fixtures/{$backupid}.mbz",
799
            $backuppath
800
        );
801
 
802
        // Do restore to new course with default settings.
803
        $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
804
        $newcourseid = restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
805
        $rc = new restore_controller($backupid, $newcourseid,
806
            backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
807
            backup::TARGET_NEW_COURSE
808
        );
809
 
810
        $rc->execute_precheck();
811
        $rc->execute_plan();
812
        $rc->destroy();
813
 
814
        $modinfo = get_fast_modinfo($newcourseid);
815
 
816
        $qbanks = $modinfo->get_instances_of('qbank');
817
        $qbanks = array_filter($qbanks, static function($bank) {
818
            global $DB;
819
            $modrecord = $DB->get_record('qbank', ['id' => $bank->instance]);
820
            return $modrecord->type === question_bank_helper::TYPE_SYSTEM;
821
        });
822
        $this->assertCount(1, $qbanks);
823
        $qbank = reset($qbanks);
824
        $qbankcontext = \context_module::instance($qbank->id);
825
        $bankcats = $DB->get_records_select('question_categories',
826
            'parent <> 0 AND contextid = :contextid',
827
            ['contextid' => $qbankcontext->id],
828
            'name ASC'
829
        );
830
        // The categories and questions in the 3 deprecated contexts
831
        // all got moved to the new default qbank module instance on the new course.
832
        $this->assertCount(3, $bankcats);
833
        $expectedidentifiers = [
834
            'Default for Category 1',
835
            'Default for System',
836
            'Default for Test Course 1',
837
            'Default for Quiz',
838
        ];
839
        $i = 0;
840
 
841
        foreach ($bankcats as $bankcat) {
842
            $identifer = $expectedidentifiers[$i];
843
            $this->assertEquals($identifer, $bankcat->name);
844
            $bankcatqs = get_questions_category($bankcat, false);
845
            $this->assertCount(1, $bankcatqs);
846
            $bankcatq = reset($bankcatqs);
847
            $this->assertEquals($identifer, $bankcatq->name);
848
            $i++;
849
        }
850
 
851
        // The question category and question attached to the quiz got restored to its own context correctly.
852
        $newquizzes = $modinfo->get_instances_of('quiz');
853
        $this->assertCount(1, $newquizzes);
854
        $newquiz = reset($newquizzes);
855
        $newquizcontext = \context_module::instance($newquiz->id);
856
        $quizcats = $DB->get_records_select('question_categories',
857
            'parent <> 0 AND contextid = :contextid',
858
            ['contextid' => $newquizcontext->id]
859
        );
860
        $quizcat = reset($quizcats);
861
        $quizcatqs = get_questions_category($quizcat, false);
862
        $this->assertCount(1, $quizcatqs);
863
        $quizcatq = reset($quizcatqs);
864
        $this->assertEquals($expectedidentifiers[$i], $quizcatq->name);
865
    }
1 efrain 866
}