MDL-63738 question bank: a link to download a single question
[moodle.git] / lib / tests / questionlib_test.php
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/>.
17 /**
18  * Unit tests for (some of) ../questionlib.php.
19  *
20  * @package    core_question
21  * @category   phpunit
22  * @copyright  2006 The Open University
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
25 use core_tag\output\tag;
28 defined('MOODLE_INTERNAL') || die();
30 global $CFG;
32 require_once($CFG->libdir . '/questionlib.php');
33 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
35 // Get the necessary files to perform backup and restore.
36 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
37 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
39 /**
40  * Unit tests for (some of) ../questionlib.php.
41  *
42  * @copyright  2006 The Open University
43  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44  */
45 class core_questionlib_testcase extends advanced_testcase {
47     /**
48      * Test set up.
49      *
50      * This is executed before running any test in this file.
51      */
52     public function setUp() {
53         $this->resetAfterTest();
54     }
56     /**
57      * Return true and false to test functions with feedback on and off.
58      *
59      * @return array Test data
60      */
61     public function provider_feedback() {
62         return array(
63             'Feedback test' => array(true),
64             'No feedback test' => array(false)
65         );
66     }
68     /**
69      * Setup a course, a quiz, a question category and a question for testing.
70      *
71      * @param string $type The type of question category to create.
72      * @return array The created data objects
73      */
74     public function setup_quiz_and_questions($type = 'module') {
75         // Create course category.
76         $category = $this->getDataGenerator()->create_category();
78         // Create course.
79         $course = $this->getDataGenerator()->create_course(array(
80             'numsections' => 5,
81             'category' => $category->id
82         ));
84         $options = array(
85             'course' => $course->id,
86             'duedate' => time(),
87         );
89         // Generate an assignment with due date (will generate a course event).
90         $quiz = $this->getDataGenerator()->create_module('quiz', $options);
92         $qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
94         switch ($type) {
95             case 'course':
96                 $context = context_course::instance($course->id);
97                 break;
99             case 'category':
100                 $context = context_coursecat::instance($category->id);
101                 break;
103             case 'system':
104                 $context = context_system::instance();
105                 break;
107             default:
108                 $context = context_module::instance($quiz->cmid);
109                 break;
110         }
112         $qcat = $qgen->create_question_category(array('contextid' => $context->id));
114         $questions = array(
115                 $qgen->create_question('shortanswer', null, array('category' => $qcat->id)),
116                 $qgen->create_question('shortanswer', null, array('category' => $qcat->id)),
117         );
119         quiz_add_quiz_question($questions[0]->id, $quiz);
121         return array($category, $course, $quiz, $qcat, $questions);
122     }
124     public function test_question_reorder_qtypes() {
125         $this->assertEquals(
126             array(0 => 't2', 1 => 't1', 2 => 't3'),
127             question_reorder_qtypes(array('t1' => '', 't2' => '', 't3' => ''), 't1', +1));
128         $this->assertEquals(
129             array(0 => 't1', 1 => 't2', 2 => 't3'),
130             question_reorder_qtypes(array('t1' => '', 't2' => '', 't3' => ''), 't1', -1));
131         $this->assertEquals(
132             array(0 => 't2', 1 => 't1', 2 => 't3'),
133             question_reorder_qtypes(array('t1' => '', 't2' => '', 't3' => ''), 't2', -1));
134         $this->assertEquals(
135             array(0 => 't1', 1 => 't2', 2 => 't3'),
136             question_reorder_qtypes(array('t1' => '', 't2' => '', 't3' => ''), 't3', +1));
137         $this->assertEquals(
138             array(0 => 't1', 1 => 't2', 2 => 't3'),
139             question_reorder_qtypes(array('t1' => '', 't2' => '', 't3' => ''), 'missing', +1));
140     }
142     public function test_match_grade_options() {
143         $gradeoptions = question_bank::fraction_options_full();
145         $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.3333333, 'error'));
146         $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.333333, 'error'));
147         $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.33333, 'error'));
148         $this->assertFalse(match_grade_options($gradeoptions, 0.3333, 'error'));
150         $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.3333333, 'nearest'));
151         $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.333333, 'nearest'));
152         $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.33333, 'nearest'));
153         $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.33, 'nearest'));
155         $this->assertEquals(-0.1428571, match_grade_options($gradeoptions, -0.15, 'nearest'));
156     }
158     /**
159      * This function tests that the functions responsible for moving questions to
160      * different contexts also updates the tag instances associated with the questions.
161      */
162     public function test_altering_tag_instance_context() {
163         global $CFG, $DB;
165         // Set to admin user.
166         $this->setAdminUser();
168         // Create two course categories - we are going to delete one of these later and will expect
169         // all the questions belonging to the course in the deleted category to be moved.
170         $coursecat1 = $this->getDataGenerator()->create_category();
171         $coursecat2 = $this->getDataGenerator()->create_category();
173         // Create a couple of categories and questions.
174         $context1 = context_coursecat::instance($coursecat1->id);
175         $context2 = context_coursecat::instance($coursecat2->id);
176         $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
177         $questioncat1 = $questiongenerator->create_question_category(array('contextid' =>
178             $context1->id));
179         $questioncat2 = $questiongenerator->create_question_category(array('contextid' =>
180             $context2->id));
181         $question1 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat1->id));
182         $question2 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat1->id));
183         $question3 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat2->id));
184         $question4 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat2->id));
186         // Now lets tag these questions.
187         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $context1, array('tag 1', 'tag 2'));
188         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $context1, array('tag 3', 'tag 4'));
189         core_tag_tag::set_item_tags('core_question', 'question', $question3->id, $context2, array('tag 5', 'tag 6'));
190         core_tag_tag::set_item_tags('core_question', 'question', $question4->id, $context2, array('tag 7', 'tag 8'));
192         // Test moving the questions to another category.
193         question_move_questions_to_category(array($question1->id, $question2->id), $questioncat2->id);
195         // Test that all tag_instances belong to one context.
196         $this->assertEquals(8, $DB->count_records('tag_instance', array('component' => 'core_question',
197             'contextid' => $questioncat2->contextid)));
199         // Test moving them back.
200         question_move_questions_to_category(array($question1->id, $question2->id), $questioncat1->id);
202         // Test that all tag_instances are now reset to how they were initially.
203         $this->assertEquals(4, $DB->count_records('tag_instance', array('component' => 'core_question',
204             'contextid' => $questioncat1->contextid)));
205         $this->assertEquals(4, $DB->count_records('tag_instance', array('component' => 'core_question',
206             'contextid' => $questioncat2->contextid)));
208         // Now test moving a whole question category to another context.
209         question_move_category_to_context($questioncat1->id, $questioncat1->contextid, $questioncat2->contextid);
211         // Test that all tag_instances belong to one context.
212         $this->assertEquals(8, $DB->count_records('tag_instance', array('component' => 'core_question',
213             'contextid' => $questioncat2->contextid)));
215         // Now test moving them back.
216         question_move_category_to_context($questioncat1->id, $questioncat2->contextid,
217             context_coursecat::instance($coursecat1->id)->id);
219         // Test that all tag_instances are now reset to how they were initially.
220         $this->assertEquals(4, $DB->count_records('tag_instance', array('component' => 'core_question',
221             'contextid' => $questioncat1->contextid)));
222         $this->assertEquals(4, $DB->count_records('tag_instance', array('component' => 'core_question',
223             'contextid' => $questioncat2->contextid)));
225         // Now we want to test deleting the course category and moving the questions to another category.
226         question_delete_course_category($coursecat1, $coursecat2, false);
228         // Test that all tag_instances belong to one context.
229         $this->assertEquals(8, $DB->count_records('tag_instance', array('component' => 'core_question',
230             'contextid' => $questioncat2->contextid)));
232         // Create a course.
233         $course = $this->getDataGenerator()->create_course();
235         // Create some question categories and questions in this course.
236         $coursecontext = context_course::instance($course->id);
237         $questioncat = $questiongenerator->create_question_category(array('contextid' =>
238             $coursecontext->id));
239         $question1 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat->id));
240         $question2 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat->id));
242         // Add some tags to these questions.
243         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, array('tag 1', 'tag 2'));
244         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, array('tag 1', 'tag 2'));
246         // Create a course that we are going to restore the other course to.
247         $course2 = $this->getDataGenerator()->create_course();
249         // Create backup file and save it to the backup location.
250         $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
251             backup::INTERACTIVE_NO, backup::MODE_GENERAL, 2);
252         $bc->execute_plan();
253         $results = $bc->get_results();
254         $file = $results['backup_destination'];
255         $fp = get_file_packer('application/vnd.moodle.backup');
256         $filepath = $CFG->dataroot . '/temp/backup/test-restore-course';
257         $file->extract_to_pathname($fp, $filepath);
258         $bc->destroy();
260         // Now restore the course.
261         $rc = new restore_controller('test-restore-course', $course2->id, backup::INTERACTIVE_NO,
262             backup::MODE_GENERAL, 2, backup::TARGET_NEW_COURSE);
263         $rc->execute_precheck();
264         $rc->execute_plan();
266         // Get the created question category.
267         $restoredcategory = $DB->get_record_select('question_categories', 'contextid = ? AND parent <> 0',
268                 array(context_course::instance($course2->id)->id), '*', MUST_EXIST);
270         // Check that there are two questions in the restored to course's context.
271         $this->assertEquals(2, $DB->count_records('question', array('category' => $restoredcategory->id)));
273         $rc->destroy();
274     }
276     /**
277      * This function tests the question_category_delete_safe function.
278      */
279     public function test_question_category_delete_safe() {
280         global $DB;
281         $this->resetAfterTest(true);
282         $this->setAdminUser();
284         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
286         question_category_delete_safe($qcat);
288         // Verify category deleted.
289         $criteria = array('id' => $qcat->id);
290         $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
292         // Verify questions deleted or moved.
293         $criteria = array('category' => $qcat->id);
294         $this->assertEquals(0, $DB->count_records('question', $criteria));
296         // Verify question not deleted.
297         $criteria = array('id' => $questions[0]->id);
298         $this->assertEquals(1, $DB->count_records('question', $criteria));
299     }
301     /**
302      * This function tests the question_delete_activity function.
303      *
304      * @param bool $feedback Whether to return feedback
305      * @dataProvider provider_feedback
306      */
307     public function test_question_delete_activity($feedback) {
308         global $DB;
309         $this->resetAfterTest(true);
310         $this->setAdminUser();
312         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
314         $cm = get_coursemodule_from_instance('quiz', $quiz->id);
315         // Test that the feedback works.
316         if ($feedback) {
317             $this->expectOutputRegex('|'.get_string('unusedcategorydeleted', 'question').'|');
318         }
319         question_delete_activity($cm, $feedback);
321         // Verify category deleted.
322         $criteria = array('id' => $qcat->id);
323         $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
325         // Verify questions deleted or moved.
326         $criteria = array('category' => $qcat->id);
327         $this->assertEquals(0, $DB->count_records('question', $criteria));
328     }
330     /**
331      * This function tests the question_delete_context function.
332      */
333     public function test_question_delete_context() {
334         global $DB;
335         $this->resetAfterTest(true);
336         $this->setAdminUser();
338         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
340         // Get the module context id.
341         $result = question_delete_context($qcat->contextid);
343         // Verify category deleted.
344         $criteria = array('id' => $qcat->id);
345         $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
347         // Verify questions deleted or moved.
348         $criteria = array('category' => $qcat->id);
349         $this->assertEquals(0, $DB->count_records('question', $criteria));
351         // Test that the feedback works.
352         $expected[] = array('top', get_string('unusedcategorydeleted', 'question'));
353         $expected[] = array($qcat->name, get_string('unusedcategorydeleted', 'question'));
354         $this->assertEquals($expected, $result);
355     }
357     /**
358      * This function tests the question_delete_course function.
359      *
360      * @param bool $feedback Whether to return feedback
361      * @dataProvider provider_feedback
362      */
363     public function test_question_delete_course($feedback) {
364         global $DB;
365         $this->resetAfterTest(true);
366         $this->setAdminUser();
368         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
370         // Test that the feedback works.
371         if ($feedback) {
372             $this->expectOutputRegex('|'.get_string('unusedcategorydeleted', 'question').'|');
373         }
374         question_delete_course($course, $feedback);
376         // Verify category deleted.
377         $criteria = array('id' => $qcat->id);
378         $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
380         // Verify questions deleted or moved.
381         $criteria = array('category' => $qcat->id);
382         $this->assertEquals(0, $DB->count_records('question', $criteria));
383     }
385     /**
386      * This function tests the question_delete_course_category function.
387      *
388      * @param bool $feedback Whether to return feedback
389      * @dataProvider provider_feedback
390      */
391     public function test_question_delete_course_category($feedback) {
392         global $DB;
393         $this->resetAfterTest(true);
394         $this->setAdminUser();
396         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
398         // Test that the feedback works.
399         if ($feedback) {
400             $this->expectOutputRegex('|'.get_string('unusedcategorydeleted', 'question').'|');
401         }
402         question_delete_course_category($category, 0, $feedback);
404         // Verify category deleted.
405         $criteria = array('id' => $qcat->id);
406         $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
408         // Verify questions deleted or moved.
409         $criteria = array('category' => $qcat->id);
410         $this->assertEquals(0, $DB->count_records('question', $criteria));
411     }
413     /**
414      * This function tests the question_delete_course_category function when it is supposed to move question categories.
415      *
416      * @param bool $feedback Whether to return feedback
417      * @dataProvider provider_feedback
418      */
419     public function test_question_delete_course_category_move_qcats($feedback) {
420         global $DB;
421         $this->resetAfterTest(true);
422         $this->setAdminUser();
424         list($category1, $course1, $quiz1, $qcat1, $questions1) = $this->setup_quiz_and_questions('category');
425         list($category2, $course2, $quiz2, $qcat2, $questions2) = $this->setup_quiz_and_questions('category');
427         $questionsinqcat1 = count($questions1);
428         $questionsinqcat2 = count($questions2);
430         // Test that the feedback works.
431         if ($feedback) {
432             $a = new stdClass();
433             $a->oldplace = context::instance_by_id($qcat1->contextid)->get_context_name();
434             $a->newplace = context::instance_by_id($qcat2->contextid)->get_context_name();
435             $this->expectOutputRegex('|'.get_string('movedquestionsandcategories', 'question', $a).'|');
436         }
437         question_delete_course_category($category1, $category2, $feedback);
439         // Verify category not deleted.
440         $criteria = array('id' => $qcat1->id);
441         $this->assertEquals(1, $DB->count_records('question_categories', $criteria));
443         // Verify questions are moved.
444         $criteria = array('category' => $qcat1->id);
445         $params = array($qcat2->contextid);
446         $actualquestionscount = $DB->count_records_sql("SELECT COUNT(*)
447                                                           FROM {question} q
448                                                           JOIN {question_categories} qc ON q.category = qc.id
449                                                          WHERE qc.contextid = ?", $params, $criteria);
450         $this->assertEquals($questionsinqcat1 + $questionsinqcat2, $actualquestionscount);
452         // Verify there is just a single top-level category.
453         $criteria = array('contextid' => $qcat2->contextid, 'parent' => 0);
454         $this->assertEquals(1, $DB->count_records('question_categories', $criteria));
456         // Verify there is no question category in previous context.
457         $criteria = array('contextid' => $qcat1->contextid);
458         $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
459     }
461     /**
462      * This function tests the question_save_from_deletion function when it is supposed to make a new category and
463      * move question categories to that new category.
464      */
465     public function test_question_save_from_deletion() {
466         global $DB;
467         $this->resetAfterTest(true);
468         $this->setAdminUser();
470         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
472         $context = context::instance_by_id($qcat->contextid);
474         $newcat = question_save_from_deletion(array_column($questions, 'id'),
475                 $context->get_parent_context()->id, $context->get_context_name());
477         // Verify that the newcat itself is not a tep level category.
478         $this->assertNotEquals(0, $newcat->parent);
480         // Verify there is just a single top-level category.
481         $this->assertEquals(1, $DB->count_records('question_categories', ['contextid' => $qcat->contextid, 'parent' => 0]));
482     }
484     public function test_question_remove_stale_questions_from_category() {
485         global $DB;
486         $this->resetAfterTest(true);
487         $this->setAdminUser();
489         $dg = $this->getDataGenerator();
490         $course = $dg->create_course();
491         $quiz = $dg->create_module('quiz', ['course' => $course->id]);
493         $qgen = $dg->get_plugin_generator('core_question');
494         $context = context_system::instance();
496         $qcat1 = $qgen->create_question_category(['contextid' => $context->id]);
497         $q1a = $qgen->create_question('shortanswer', null, ['category' => $qcat1->id]);     // Will be hidden.
498         $DB->set_field('question', 'hidden', 1, ['id' => $q1a->id]);
500         $qcat2 = $qgen->create_question_category(['contextid' => $context->id]);
501         $q2a = $qgen->create_question('shortanswer', null, ['category' => $qcat2->id]);     // Will be hidden.
502         $q2b = $qgen->create_question('shortanswer', null, ['category' => $qcat2->id]);     // Will be hidden but used.
503         $DB->set_field('question', 'hidden', 1, ['id' => $q2a->id]);
504         $DB->set_field('question', 'hidden', 1, ['id' => $q2b->id]);
505         quiz_add_quiz_question($q2b->id, $quiz);
506         quiz_add_random_questions($quiz, 0, $qcat2->id, 1, false);
508         // We added one random question to the quiz and we expect the quiz to have only one random question.
509         $q2d = $DB->get_record_sql("SELECT q.*
510                                       FROM {question} q
511                                       JOIN {quiz_slots} s ON s.questionid = q.id
512                                      WHERE q.qtype = :qtype
513                                            AND s.quizid = :quizid",
514                 array('qtype' => 'random', 'quizid' => $quiz->id), MUST_EXIST);
516         // The following 2 lines have to be after the quiz_add_random_questions() call above.
517         // Otherwise, quiz_add_random_questions() will to be "smart" and use them instead of creating a new "random" question.
518         $q1b = $qgen->create_question('random', null, ['category' => $qcat1->id]);          // Will not be used.
519         $q2c = $qgen->create_question('random', null, ['category' => $qcat2->id]);          // Will not be used.
521         $this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
522         $this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
524         // Non-existing category, nothing will happen.
525         question_remove_stale_questions_from_category(0);
526         $this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
527         $this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
529         // First category, should be empty afterwards.
530         question_remove_stale_questions_from_category($qcat1->id);
531         $this->assertEquals(0, $DB->count_records('question', ['category' => $qcat1->id]));
532         $this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
533         $this->assertFalse($DB->record_exists('question', ['id' => $q1a->id]));
534         $this->assertFalse($DB->record_exists('question', ['id' => $q1b->id]));
536         // Second category, used questions should be left untouched.
537         question_remove_stale_questions_from_category($qcat2->id);
538         $this->assertEquals(0, $DB->count_records('question', ['category' => $qcat1->id]));
539         $this->assertEquals(2, $DB->count_records('question', ['category' => $qcat2->id]));
540         $this->assertFalse($DB->record_exists('question', ['id' => $q2a->id]));
541         $this->assertTrue($DB->record_exists('question', ['id' => $q2b->id]));
542         $this->assertFalse($DB->record_exists('question', ['id' => $q2c->id]));
543         $this->assertTrue($DB->record_exists('question', ['id' => $q2d->id]));
544     }
546     /**
547      * get_question_options should add the category object to the given question.
548      */
549     public function test_get_question_options_includes_category_object_single_question() {
550         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
551         $question = array_shift($questions);
553         get_question_options($question);
555         $this->assertEquals($qcat, $question->categoryobject);
556     }
558     /**
559      * get_question_options should add the category object to all of the questions in
560      * the given list.
561      */
562     public function test_get_question_options_includes_category_object_multiple_questions() {
563         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
565         get_question_options($questions);
567         foreach ($questions as $question) {
568             $this->assertEquals($qcat, $question->categoryobject);
569         }
570     }
572     /**
573      * get_question_options includes the tags for all questions in the list.
574      */
575     public function test_get_question_options_includes_question_tags() {
576         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
577         $question1 = $questions[0];
578         $question2 = $questions[1];
579         $qcontext = context::instance_by_id($qcat->contextid);
581         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
582         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['baz', 'bop']);
584         get_question_options($questions, true);
586         foreach ($questions as $question) {
587             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
588             $expectedtags = [];
589             $actualtags = $question->tags;
590             foreach ($tags as $tag) {
591                 $expectedtags[$tag->id] = $tag->get_display_name();
592             }
594             // The question should have a tags property populated with each tag id
595             // and display name as a key vale pair.
596             $this->assertEquals($expectedtags, $actualtags);
598             $actualtagobjects = $question->tagobjects;
599             sort($tags);
600             sort($actualtagobjects);
602             // The question should have a full set of each tag object.
603             $this->assertEquals($tags, $actualtagobjects);
604             // The question should not have any course tags.
605             $this->assertEmpty($question->coursetagobjects);
606         }
607     }
609     /**
610      * get_question_options includes the course tags for all questions in the list.
611      */
612     public function test_get_question_options_includes_course_tags() {
613         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
614         $question1 = $questions[0];
615         $question2 = $questions[1];
616         $coursecontext = context_course::instance($course->id);
618         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo', 'bar']);
619         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['baz', 'bop']);
621         get_question_options($questions, true);
623         foreach ($questions as $question) {
624             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
625             $expectedcoursetags = [];
626             $actualcoursetags = $question->coursetags;
627             foreach ($tags as $tag) {
628                 $expectedcoursetags[$tag->id] = $tag->get_display_name();
629             }
631             // The question should have a coursetags property populated with each tag id
632             // and display name as a key vale pair.
633             $this->assertEquals($expectedcoursetags, $actualcoursetags);
635             $actualcoursetagobjects = $question->coursetagobjects;
636             sort($tags);
637             sort($actualcoursetagobjects);
639             // The question should have a full set of the course tag objects.
640             $this->assertEquals($tags, $actualcoursetagobjects);
641             // The question should not have any other tags.
642             $this->assertEmpty($question->tagobjects);
643             $this->assertEmpty($question->tags);
644         }
645     }
647     /**
648      * get_question_options only categorises a tag as a course tag if it is in a
649      * course context that is different from the question context.
650      */
651     public function test_get_question_options_course_tags_in_course_question_context() {
652         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
653         $question1 = $questions[0];
654         $question2 = $questions[1];
655         $coursecontext = context_course::instance($course->id);
657         // Create course level tags in the course context that matches the question
658         // course context.
659         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo', 'bar']);
660         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['baz', 'bop']);
662         get_question_options($questions, true);
664         foreach ($questions as $question) {
665             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
667             $actualtagobjects = $question->tagobjects;
668             sort($tags);
669             sort($actualtagobjects);
671             // The tags should not be considered course tags because they are in
672             // the same context as the question. That makes them question tags.
673             $this->assertEmpty($question->coursetagobjects);
674             // The course context tags should be returned in the regular tag object
675             // list.
676             $this->assertEquals($tags, $actualtagobjects);
677         }
678     }
680     /**
681      * get_question_options includes the tags and course tags for all questions in the list
682      * if each question has course and question level tags.
683      */
684     public function test_get_question_options_includes_question_and_course_tags() {
685         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
686         $question1 = $questions[0];
687         $question2 = $questions[1];
688         $qcontext = context::instance_by_id($qcat->contextid);
689         $coursecontext = context_course::instance($course->id);
691         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
692         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['cfoo', 'cbar']);
693         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['baz', 'bop']);
694         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['cbaz', 'cbop']);
696         get_question_options($questions, true);
698         foreach ($questions as $question) {
699             $alltags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
700             $tags = array_filter($alltags, function($tag) use ($qcontext) {
701                 return $tag->taginstancecontextid == $qcontext->id;
702             });
703             $coursetags = array_filter($alltags, function($tag) use ($coursecontext) {
704                 return $tag->taginstancecontextid == $coursecontext->id;
705             });
707             $expectedtags = [];
708             $actualtags = $question->tags;
709             foreach ($tags as $tag) {
710                 $expectedtags[$tag->id] = $tag->get_display_name();
711             }
713             // The question should have a tags property populated with each tag id
714             // and display name as a key vale pair.
715             $this->assertEquals($expectedtags, $actualtags);
717             $actualtagobjects = $question->tagobjects;
718             sort($tags);
719             sort($actualtagobjects);
720             // The question should have a full set of each tag object.
721             $this->assertEquals($tags, $actualtagobjects);
723             $actualcoursetagobjects = $question->coursetagobjects;
724             sort($coursetags);
725             sort($actualcoursetagobjects);
726             // The question should have a full set of course tag objects.
727             $this->assertEquals($coursetags, $actualcoursetagobjects);
728         }
729     }
731     /**
732      * get_question_options should update the context id to the question category
733      * context id for any non-course context tag that isn't in the question category
734      * context.
735      */
736     public function test_get_question_options_normalises_question_tags() {
737         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
738         $question1 = $questions[0];
739         $question2 = $questions[1];
740         $qcontext = context::instance_by_id($qcat->contextid);
741         $systemcontext = context_system::instance();
743         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
744         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['baz', 'bop']);
746         $q1tags = core_tag_tag::get_item_tags('core_question', 'question', $question1->id);
747         $q2tags = core_tag_tag::get_item_tags('core_question', 'question', $question2->id);
748         $q1tag = array_shift($q1tags);
749         $q2tag = array_shift($q2tags);
751         // Change two of the tag instances to be a different (non-course) context to the
752         // question tag context. These tags should then be normalised back to the question
753         // tag context.
754         core_tag_tag::change_instances_context([$q1tag->taginstanceid, $q2tag->taginstanceid], $systemcontext);
756         get_question_options($questions, true);
758         foreach ($questions as $question) {
759             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
761             // The database should have been updated with the correct context id.
762             foreach ($tags as $tag) {
763                 $this->assertEquals($qcontext->id, $tag->taginstancecontextid);
764             }
766             // The tag objects on the question should have been updated with the
767             // correct context id.
768             foreach ($question->tagobjects as $tag) {
769                 $this->assertEquals($qcontext->id, $tag->taginstancecontextid);
770             }
771         }
772     }
774     /**
775      * get_question_options if the question is a course level question then tags
776      * in that context should not be consdered course tags, they are question tags.
777      */
778     public function test_get_question_options_includes_course_context_question_tags() {
779         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
780         $question1 = $questions[0];
781         $question2 = $questions[1];
782         $coursecontext = context_course::instance($course->id);
784         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo', 'bar']);
785         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['baz', 'bop']);
787         get_question_options($questions, true);
789         foreach ($questions as $question) {
790             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
791             // Tags in a course context that matches the question context should
792             // not be considered course tags.
793             $this->assertEmpty($question->coursetagobjects);
794             $this->assertEmpty($question->coursetags);
796             $actualtagobjects = $question->tagobjects;
797             sort($tags);
798             sort($actualtagobjects);
799             // The tags should be considered question tags not course tags.
800             $this->assertEquals($tags, $actualtagobjects);
801         }
802     }
804     /**
805      * get_question_options should return tags from all course contexts by default.
806      */
807     public function test_get_question_options_includes_multiple_courses_tags() {
808         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
809         $question1 = $questions[0];
810         $question2 = $questions[1];
811         $coursecontext = context_course::instance($course->id);
812         // Create a sibling course.
813         $siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
814         $siblingcoursecontext = context_course::instance($siblingcourse->id);
816         // Create course tags.
817         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['c1']);
818         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['c1']);
819         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['c2']);
820         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['c2']);
822         get_question_options($questions, true);
824         foreach ($questions as $question) {
825             $this->assertCount(2, $question->coursetagobjects);
827             foreach ($question->coursetagobjects as $tag) {
828                 if ($tag->name == 'c1') {
829                     $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
830                 } else {
831                     $this->assertEquals($siblingcoursecontext->id, $tag->taginstancecontextid);
832                 }
833             }
834         }
835     }
837     /**
838      * get_question_options should filter the course tags by the given list of courses.
839      */
840     public function test_get_question_options_includes_filter_course_tags() {
841         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
842         $question1 = $questions[0];
843         $question2 = $questions[1];
844         $coursecontext = context_course::instance($course->id);
845         // Create a sibling course.
846         $siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
847         $siblingcoursecontext = context_course::instance($siblingcourse->id);
849         // Create course tags.
850         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo']);
851         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['bar']);
852         // Create sibling course tags. These should be filtered out.
853         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['filtered1']);
854         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['filtered2']);
856         // Ask to only receive course tags from $course (ignoring $siblingcourse tags).
857         get_question_options($questions, true, [$course]);
859         foreach ($questions as $question) {
860             foreach ($question->coursetagobjects as $tag) {
861                 // We should only be seeing course tags from $course. The tags from
862                 // $siblingcourse should have been filtered out.
863                 $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
864             }
865         }
866     }
868     /**
869      * question_move_question_tags_to_new_context should update all of the
870      * question tags contexts when they are moving down (from system to course
871      * category context).
872      */
873     public function test_question_move_question_tags_to_new_context_system_to_course_cat_qtags() {
874         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('system');
875         $question1 = $questions[0];
876         $question2 = $questions[1];
877         $qcontext = context::instance_by_id($qcat->contextid);
878         $newcontext = context_coursecat::instance($category->id);
880         foreach ($questions as $question) {
881             $question->contextid = $qcat->contextid;
882         }
884         // Create tags in the system context.
885         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
886         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo', 'bar']);
888         question_move_question_tags_to_new_context($questions, $newcontext);
890         foreach ($questions as $question) {
891             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
893             // All of the tags should have their context id set to the new context.
894             foreach ($tags as $tag) {
895                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
896             }
897         }
898     }
900     /**
901      * question_move_question_tags_to_new_context should update all of the question tags
902      * contexts when they are moving down (from system to course category context)
903      * but leave any tags in the course context where they are.
904      */
905     public function test_question_move_question_tags_to_new_context_system_to_course_cat_qtags_and_course_tags() {
906         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('system');
907         $question1 = $questions[0];
908         $question2 = $questions[1];
909         $qcontext = context::instance_by_id($qcat->contextid);
910         $coursecontext = context_course::instance($course->id);
911         $newcontext = context_coursecat::instance($category->id);
913         foreach ($questions as $question) {
914             $question->contextid = $qcat->contextid;
915         }
917         // Create tags in the system context.
918         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
919         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
920         // Create tags in the course context.
921         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
922         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
924         question_move_question_tags_to_new_context($questions, $newcontext);
926         foreach ($questions as $question) {
927             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
929             foreach ($tags as $tag) {
930                 if ($tag->name == 'ctag') {
931                     // Course tags should remain in the course context.
932                     $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
933                 } else {
934                     // Other tags should be updated.
935                     $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
936                 }
937             }
938         }
939     }
941     /**
942      * question_move_question_tags_to_new_context should update all of the question
943      * contexts tags when they are moving up (from course category to system context).
944      */
945     public function test_question_move_question_tags_to_new_context_course_cat_to_system_qtags() {
946         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
947         $question1 = $questions[0];
948         $question2 = $questions[1];
949         $qcontext = context::instance_by_id($qcat->contextid);
950         $newcontext = context_system::instance();
952         foreach ($questions as $question) {
953             $question->contextid = $qcat->contextid;
954         }
956         // Create tags in the course category context.
957         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
958         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo', 'bar']);
960         question_move_question_tags_to_new_context($questions, $newcontext);
962         foreach ($questions as $question) {
963             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
965             // All of the tags should have their context id set to the new context.
966             foreach ($tags as $tag) {
967                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
968             }
969         }
970     }
972     /**
973      * question_move_question_tags_to_new_context should update all of the question
974      * tags contexts when they are moving up (from course category context to system
975      * context) but leave any tags in the course context where they are.
976      */
977     public function test_question_move_question_tags_to_new_context_course_cat_to_system_qtags_and_course_tags() {
978         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
979         $question1 = $questions[0];
980         $question2 = $questions[1];
981         $qcontext = context::instance_by_id($qcat->contextid);
982         $coursecontext = context_course::instance($course->id);
983         $newcontext = context_system::instance();
985         foreach ($questions as $question) {
986             $question->contextid = $qcat->contextid;
987         }
989         // Create tags in the system context.
990         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
991         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
992         // Create tags in the course context.
993         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
994         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
996         question_move_question_tags_to_new_context($questions, $newcontext);
998         foreach ($questions as $question) {
999             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1001             foreach ($tags as $tag) {
1002                 if ($tag->name == 'ctag') {
1003                     // Course tags should remain in the course context.
1004                     $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
1005                 } else {
1006                     // Other tags should be updated.
1007                     $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1008                 }
1009             }
1010         }
1011     }
1013     /**
1014      * question_move_question_tags_to_new_context should merge all tags into the course
1015      * context when moving down from course category context into course context.
1016      */
1017     public function test_question_move_question_tags_to_new_context_course_cat_to_coures_qtags_and_course_tags() {
1018         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1019         $question1 = $questions[0];
1020         $question2 = $questions[1];
1021         $qcontext = context::instance_by_id($qcat->contextid);
1022         $coursecontext = context_course::instance($course->id);
1023         $newcontext = $coursecontext;
1025         foreach ($questions as $question) {
1026             $question->contextid = $qcat->contextid;
1027         }
1029         // Create tags in the system context.
1030         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1031         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1032         // Create tags in the course context.
1033         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
1034         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
1036         question_move_question_tags_to_new_context($questions, $newcontext);
1038         foreach ($questions as $question) {
1039             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1040             // Each question should have 2 tags.
1041             $this->assertCount(2, $tags);
1043             foreach ($tags as $tag) {
1044                 // All tags should be updated to the course context and merged in.
1045                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1046             }
1047         }
1048     }
1050     /**
1051      * question_move_question_tags_to_new_context should delete all of the tag
1052      * instances from sibling courses when moving the context of a question down
1053      * from a course category into a course context because the other courses will
1054      * no longer have access to the question.
1055      */
1056     public function test_question_move_question_tags_to_new_context_remove_other_course_tags() {
1057         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1058         // Create a sibling course.
1059         $siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
1060         $question1 = $questions[0];
1061         $question2 = $questions[1];
1062         $qcontext = context::instance_by_id($qcat->contextid);
1063         $coursecontext = context_course::instance($course->id);
1064         $siblingcoursecontext = context_course::instance($siblingcourse->id);
1065         $newcontext = $coursecontext;
1067         foreach ($questions as $question) {
1068             $question->contextid = $qcat->contextid;
1069         }
1071         // Create tags in the system context.
1072         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1073         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1074         // Create tags in the target course context.
1075         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
1076         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
1077         // Create tags in the sibling course context. These should be deleted as
1078         // part of the move.
1079         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['stag']);
1080         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['stag']);
1082         question_move_question_tags_to_new_context($questions, $newcontext);
1084         foreach ($questions as $question) {
1085             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1086             // Each question should have 2 tags, 'foo' and 'ctag'.
1087             $this->assertCount(2, $tags);
1089             foreach ($tags as $tag) {
1090                 $tagname = $tag->name;
1091                 // The 'stag' should have been deleted because it's in a sibling
1092                 // course context.
1093                 $this->assertContains($tagname, ['foo', 'ctag']);
1094                 // All tags should be in the course context now.
1095                 $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
1096             }
1097         }
1098     }
1100     /**
1101      * question_move_question_tags_to_new_context should update all of the question
1102      * tags to be the course category context when moving the tags from a course
1103      * context to a course category context.
1104      */
1105     public function test_question_move_question_tags_to_new_context_course_to_course_cat() {
1106         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
1107         $question1 = $questions[0];
1108         $question2 = $questions[1];
1109         $qcontext = context::instance_by_id($qcat->contextid);
1110         // Moving up into the course category context.
1111         $newcontext = context_coursecat::instance($category->id);
1113         foreach ($questions as $question) {
1114             $question->contextid = $qcat->contextid;
1115         }
1117         // Create tags in the course context.
1118         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1119         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1121         question_move_question_tags_to_new_context($questions, $newcontext);
1123         foreach ($questions as $question) {
1124             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1126             // All of the tags should have their context id set to the new context.
1127             foreach ($tags as $tag) {
1128                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1129             }
1130         }
1131     }
1133     /**
1134      * question_move_question_tags_to_new_context should update all of the
1135      * question tags contexts when they are moving down (from system to course
1136      * category context).
1137      */
1138     public function test_question_move_question_tags_to_new_context_orphaned_tag_contexts() {
1139         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('system');
1140         $question1 = $questions[0];
1141         $question2 = $questions[1];
1142         $othercategory = $this->getDataGenerator()->create_category();
1143         $qcontext = context::instance_by_id($qcat->contextid);
1144         $newcontext = context_coursecat::instance($category->id);
1145         $othercategorycontext = context_coursecat::instance($othercategory->id);
1147         foreach ($questions as $question) {
1148             $question->contextid = $qcat->contextid;
1149         }
1151         // Create tags in the system context.
1152         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1153         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1154         // Create tags in the other course category context. These should be
1155         // update to the next context id because they represent erroneous data
1156         // from a time before context id was mandatory in the tag API.
1157         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $othercategorycontext, ['bar']);
1158         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $othercategorycontext, ['bar']);
1160         question_move_question_tags_to_new_context($questions, $newcontext);
1162         foreach ($questions as $question) {
1163             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1164             // Each question should have two tags, 'foo' and 'bar'.
1165             $this->assertCount(2, $tags);
1167             // All of the tags should have their context id set to the new context
1168             // (course category context).
1169             foreach ($tags as $tag) {
1170                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1171             }
1172         }
1173     }
1175     /**
1176      * When moving from a course category context down into an activity context
1177      * all question context tags and course tags (where the course is a parent of
1178      * the activity) should move into the new context.
1179      */
1180     public function test_question_move_question_tags_to_new_context_course_cat_to_activity_qtags_and_course_tags() {
1181         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1182         $question1 = $questions[0];
1183         $question2 = $questions[1];
1184         $qcontext = context::instance_by_id($qcat->contextid);
1185         $coursecontext = context_course::instance($course->id);
1186         $newcontext = context_module::instance($quiz->cmid);
1188         foreach ($questions as $question) {
1189             $question->contextid = $qcat->contextid;
1190         }
1192         // Create tags in the course category context.
1193         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1194         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1195         // Move the questions to the activity context which is a child context of
1196         // $coursecontext.
1197         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
1198         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
1200         question_move_question_tags_to_new_context($questions, $newcontext);
1202         foreach ($questions as $question) {
1203             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1204             // Each question should have 2 tags.
1205             $this->assertCount(2, $tags);
1207             foreach ($tags as $tag) {
1208                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1209             }
1210         }
1211     }
1213     /**
1214      * When moving from a course category context down into an activity context
1215      * all question context tags and course tags (where the course is a parent of
1216      * the activity) should move into the new context. Tags in course contexts
1217      * that are not a parent of the activity context should be deleted.
1218      */
1219     public function test_question_move_question_tags_to_new_context_course_cat_to_activity_orphaned_tags() {
1220         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1221         $question1 = $questions[0];
1222         $question2 = $questions[1];
1223         $qcontext = context::instance_by_id($qcat->contextid);
1224         $coursecontext = context_course::instance($course->id);
1225         $newcontext = context_module::instance($quiz->cmid);
1226         $othercourse = $this->getDataGenerator()->create_course();
1227         $othercoursecontext = context_course::instance($othercourse->id);
1229         foreach ($questions as $question) {
1230             $question->contextid = $qcat->contextid;
1231         }
1233         // Create tags in the course category context.
1234         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1235         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1236         // Create tags in the course context.
1237         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
1238         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
1239         // Create tags in the other course context. These should be deleted.
1240         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $othercoursecontext, ['delete']);
1241         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $othercoursecontext, ['delete']);
1243         // Move the questions to the activity context which is a child context of
1244         // $coursecontext.
1245         question_move_question_tags_to_new_context($questions, $newcontext);
1247         foreach ($questions as $question) {
1248             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1249             // Each question should have 2 tags.
1250             $this->assertCount(2, $tags);
1252             foreach ($tags as $tag) {
1253                 // Make sure we don't have any 'delete' tags.
1254                 $this->assertContains($tag->name, ['foo', 'ctag']);
1255                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1256             }
1257         }
1258     }
1260     /**
1261      * When moving from a course context down into an activity context all of the
1262      * course tags should move into the activity context.
1263      */
1264     public function test_question_move_question_tags_to_new_context_course_to_activity_qtags() {
1265         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
1266         $question1 = $questions[0];
1267         $question2 = $questions[1];
1268         $qcontext = context::instance_by_id($qcat->contextid);
1269         $newcontext = context_module::instance($quiz->cmid);
1271         foreach ($questions as $question) {
1272             $question->contextid = $qcat->contextid;
1273         }
1275         // Create tags in the course context.
1276         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1277         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1279         question_move_question_tags_to_new_context($questions, $newcontext);
1281         foreach ($questions as $question) {
1282             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1284             foreach ($tags as $tag) {
1285                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1286             }
1287         }
1288     }
1290     /**
1291      * When moving from a course context down into an activity context all of the
1292      * course tags should move into the activity context.
1293      */
1294     public function test_question_move_question_tags_to_new_context_activity_to_course_qtags() {
1295         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
1296         $question1 = $questions[0];
1297         $question2 = $questions[1];
1298         $qcontext = context::instance_by_id($qcat->contextid);
1299         $newcontext = context_course::instance($course->id);
1301         foreach ($questions as $question) {
1302             $question->contextid = $qcat->contextid;
1303         }
1305         // Create tags in the activity context.
1306         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1307         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1309         question_move_question_tags_to_new_context($questions, $newcontext);
1311         foreach ($questions as $question) {
1312             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1314             foreach ($tags as $tag) {
1315                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1316             }
1317         }
1318     }
1320     /**
1321      * question_move_question_tags_to_new_context should update all of the
1322      * question tags contexts when they are moving down (from system to course
1323      * category context).
1324      *
1325      * Course tags within the new category context should remain while any course
1326      * tags in course contexts that can no longer access the question should be
1327      * deleted.
1328      */
1329     public function test_question_move_question_tags_to_new_context_system_to_course_cat_with_orphaned_tags() {
1330         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('system');
1331         $question1 = $questions[0];
1332         $question2 = $questions[1];
1333         $othercategory = $this->getDataGenerator()->create_category();
1334         $othercourse = $this->getDataGenerator()->create_course(['category' => $othercategory->id]);
1335         $qcontext = context::instance_by_id($qcat->contextid);
1336         $newcontext = context_coursecat::instance($category->id);
1337         $othercategorycontext = context_coursecat::instance($othercategory->id);
1338         $coursecontext = context_course::instance($course->id);
1339         $othercoursecontext = context_course::instance($othercourse->id);
1341         foreach ($questions as $question) {
1342             $question->contextid = $qcat->contextid;
1343         }
1345         // Create tags in the system context.
1346         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1347         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1348         // Create tags in the child course context of the new context.
1349         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['bar']);
1350         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['bar']);
1351         // Create tags in the other course context. These should be deleted when
1352         // the question moves to the new course category context because this
1353         // course belongs to a different category, which means it will no longer
1354         // have access to the question.
1355         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $othercoursecontext, ['delete']);
1356         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $othercoursecontext, ['delete']);
1358         question_move_question_tags_to_new_context($questions, $newcontext);
1360         foreach ($questions as $question) {
1361             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1362             // Each question should have two tags, 'foo' and 'bar'.
1363             $this->assertCount(2, $tags);
1365             // All of the tags should have their context id set to the new context
1366             // (course category context).
1367             foreach ($tags as $tag) {
1368                 $this->assertContains($tag->name, ['foo', 'bar']);
1370                 if ($tag->name == 'foo') {
1371                     $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1372                 } else {
1373                     $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
1374                 }
1375             }
1376         }
1377     }
1379     /**
1380      * question_sort_tags() includes the tags for all questions in the list.
1381      */
1382     public function test_question_sort_tags_includes_question_tags() {
1384         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1385         $question1 = $questions[0];
1386         $question2 = $questions[1];
1387         $qcontext = context::instance_by_id($qcat->contextid);
1389         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
1390         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['baz', 'bop']);
1392         foreach ($questions as $question) {
1393             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1394             $categorycontext = context::instance_by_id($qcat->contextid);
1395             $tagobjects = question_sort_tags($tags, $categorycontext);
1396             $expectedtags = [];
1397             $actualtags = $tagobjects->tags;
1398             foreach ($tagobjects->tagobjects as $tag) {
1399                 $expectedtags[$tag->id] = $tag->name;
1400             }
1402             // The question should have a tags property populated with each tag id
1403             // and display name as a key vale pair.
1404             $this->assertEquals($expectedtags, $actualtags);
1406             $actualtagobjects = $tagobjects->tagobjects;
1407             sort($tags);
1408             sort($actualtagobjects);
1410             // The question should have a full set of each tag object.
1411             $this->assertEquals($tags, $actualtagobjects);
1412             // The question should not have any course tags.
1413             $this->assertEmpty($tagobjects->coursetagobjects);
1414         }
1415     }
1417     /**
1418      * question_sort_tags() includes course tags for all questions in the list.
1419      */
1420     public function test_question_sort_tags_includes_question_course_tags() {
1421         global $DB;
1423         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1424         $question1 = $questions[0];
1425         $question2 = $questions[1];
1426         $coursecontext = context_course::instance($course->id);
1428         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo', 'bar']);
1429         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['baz', 'bop']);
1431         foreach ($questions as $question) {
1432             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1433             $tagobjects = question_sort_tags($tags, $qcat);
1435             $expectedtags = [];
1436             $actualtags = $tagobjects->coursetags;
1437             foreach ($actualtags as $coursetagid => $coursetagname) {
1438                 $expectedtags[$coursetagid] = $coursetagname;
1439             }
1441             // The question should have a tags property populated with each tag id
1442             // and display name as a key vale pair.
1443             $this->assertEquals($expectedtags, $actualtags);
1445             $actualtagobjects = $tagobjects->coursetagobjects;
1446             sort($tags);
1447             sort($actualtagobjects);
1449             // The question should have a full set of each tag object.
1450             $this->assertEquals($tags, $actualtagobjects);
1451             // The question should not have any course tags.
1452             $this->assertEmpty($tagobjects->tagobjects);
1453         }
1454     }
1456     /**
1457      * question_sort_tags() should return tags from all course contexts by default.
1458      */
1459     public function test_question_sort_tags_includes_multiple_courses_tags() {
1460         global $DB;
1462         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1463         $question1 = $questions[0];
1464         $question2 = $questions[1];
1465         $coursecontext = context_course::instance($course->id);
1466         // Create a sibling course.
1467         $siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
1468         $siblingcoursecontext = context_course::instance($siblingcourse->id);
1470         // Create course tags.
1471         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['c1']);
1472         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['c1']);
1473         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['c2']);
1474         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['c2']);
1476         foreach ($questions as $question) {
1477             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1478             $tagobjects = question_sort_tags($tags, $qcat);
1479             $this->assertCount(2, $tagobjects->coursetagobjects);
1481             foreach ($tagobjects->coursetagobjects as $tag) {
1482                 if ($tag->name == 'c1') {
1483                     $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
1484                 } else {
1485                     $this->assertEquals($siblingcoursecontext->id, $tag->taginstancecontextid);
1486                 }
1487             }
1488         }
1489     }
1491     /**
1492      * question_sort_tags() should filter the course tags by the given list of courses.
1493      */
1494     public function test_question_sort_tags_includes_filter_course_tags() {
1495         global $DB;
1497         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1498         $question1 = $questions[0];
1499         $question2 = $questions[1];
1500         $coursecontext = context_course::instance($course->id);
1501         // Create a sibling course.
1502         $siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
1503         $siblingcoursecontext = context_course::instance($siblingcourse->id);
1505         // Create course tags.
1506         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo']);
1507         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['bar']);
1508         // Create sibling course tags. These should be filtered out.
1509         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['filtered1']);
1510         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['filtered2']);
1512         foreach ($questions as $question) {
1513             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1514             $tagobjects = question_sort_tags($tags, $qcat, [$course]);
1515             foreach ($tagobjects->coursetagobjects as $tag) {
1517                 // We should only be seeing course tags from $course. The tags from
1518                 // $siblingcourse should have been filtered out.
1519                 $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
1520             }
1521         }
1522     }
1524     /**
1525      * Data provider for tests of question_has_capability_on_context and question_require_capability_on_context.
1526      *
1527      * @return  array
1528      */
1529     public function question_capability_on_question_provider() {
1530         return [
1531             'Unrelated capability which is present' => [
1532                 'capabilities' => [
1533                     'moodle/question:config' => CAP_ALLOW,
1534                 ],
1535                 'testcapability' => 'config',
1536                 'isowner' => true,
1537                 'expect' => true,
1538             ],
1539             'Unrelated capability which is present (not owner)' => [
1540                 'capabilities' => [
1541                     'moodle/question:config' => CAP_ALLOW,
1542                 ],
1543                 'testcapability' => 'config',
1544                 'isowner' => false,
1545                 'expect' => true,
1546             ],
1547             'Unrelated capability which is not set' => [
1548                 'capabilities' => [
1549                 ],
1550                 'testcapability' => 'config',
1551                 'isowner' => true,
1552                 'expect' => false,
1553             ],
1554             'Unrelated capability which is not set (not owner)' => [
1555                 'capabilities' => [
1556                 ],
1557                 'testcapability' => 'config',
1558                 'isowner' => false,
1559                 'expect' => false,
1560             ],
1561             'Unrelated capability which is prevented' => [
1562                 'capabilities' => [
1563                     'moodle/question:config' => CAP_PREVENT,
1564                 ],
1565                 'testcapability' => 'config',
1566                 'isowner' => true,
1567                 'expect' => false,
1568             ],
1569             'Unrelated capability which is prevented (not owner)' => [
1570                 'capabilities' => [
1571                     'moodle/question:config' => CAP_PREVENT,
1572                 ],
1573                 'testcapability' => 'config',
1574                 'isowner' => false,
1575                 'expect' => false,
1576             ],
1577             'Related capability which is not set' => [
1578                 'capabilities' => [
1579                 ],
1580                 'testcapability' => 'edit',
1581                 'isowner' => true,
1582                 'expect' => false,
1583             ],
1584             'Related capability which is not set (not owner)' => [
1585                 'capabilities' => [
1586                 ],
1587                 'testcapability' => 'edit',
1588                 'isowner' => false,
1589                 'expect' => false,
1590             ],
1591             'Related capability which is allowed at all, unset at mine' => [
1592                 'capabilities' => [
1593                     'moodle/question:editall' => CAP_ALLOW,
1594                 ],
1595                 'testcapability' => 'edit',
1596                 'isowner' => true,
1597                 'expect' => true,
1598             ],
1599             'Related capability which is allowed at all, unset at mine (not owner)' => [
1600                 'capabilities' => [
1601                     'moodle/question:editall' => CAP_ALLOW,
1602                 ],
1603                 'testcapability' => 'edit',
1604                 'isowner' => false,
1605                 'expect' => true,
1606             ],
1607             'Related capability which is allowed at all, prevented at mine' => [
1608                 'capabilities' => [
1609                     'moodle/question:editall' => CAP_ALLOW,
1610                     'moodle/question:editmine' => CAP_PREVENT,
1611                 ],
1612                 'testcapability' => 'edit',
1613                 'isowner' => true,
1614                 'expect' => true,
1615             ],
1616             'Related capability which is allowed at all, prevented at mine (not owner)' => [
1617                 'capabilities' => [
1618                     'moodle/question:editall' => CAP_ALLOW,
1619                     'moodle/question:editmine' => CAP_PREVENT,
1620                 ],
1621                 'testcapability' => 'edit',
1622                 'isowner' => false,
1623                 'expect' => true,
1624             ],
1625             'Related capability which is unset all, allowed at mine' => [
1626                 'capabilities' => [
1627                     'moodle/question:editall' => CAP_PREVENT,
1628                     'moodle/question:editmine' => CAP_ALLOW,
1629                 ],
1630                 'testcapability' => 'edit',
1631                 'isowner' => true,
1632                 'expect' => true,
1633             ],
1634             'Related capability which is unset all, allowed at mine (not owner)' => [
1635                 'capabilities' => [
1636                     'moodle/question:editall' => CAP_PREVENT,
1637                     'moodle/question:editmine' => CAP_ALLOW,
1638                 ],
1639                 'testcapability' => 'edit',
1640                 'isowner' => false,
1641                 'expect' => false,
1642             ],
1643         ];
1644     }
1646     /**
1647      * Tests for the deprecated question_has_capability_on function when passing a stdClass as parameter.
1648      *
1649      * @dataProvider question_capability_on_question_provider
1650      * @param   array   $capabilities The capability assignments to set.
1651      * @param   string  $capability The capability to test
1652      * @param   bool    $isowner Whether the user to create the question should be the owner or not.
1653      * @param   bool    $expect The expected result.
1654      */
1655     public function test_question_has_capability_on_using_stdclass($capabilities, $capability, $isowner, $expect) {
1656         $this->resetAfterTest();
1658         // Create the test data.
1659         $user = $this->getDataGenerator()->create_user();
1660         $otheruser = $this->getDataGenerator()->create_user();
1661         $roleid = $this->getDataGenerator()->create_role();
1662         $category = $this->getDataGenerator()->create_category();
1663         $context = context_coursecat::instance($category->id);
1665         // Assign the user to the role.
1666         role_assign($roleid, $user->id, $context->id);
1668         // Assign the capabilities to the role.
1669         foreach ($capabilities as $capname => $capvalue) {
1670             assign_capability($capname, $capvalue, $roleid, $context->id);
1671         }
1673         $this->setUser($user);
1675         // The current fake question we make use of is always a stdClass and typically has no ID.
1676         $fakequestion = (object) [
1677             'contextid' => $context->id,
1678         ];
1680         if ($isowner) {
1681             $fakequestion->createdby = $user->id;
1682         } else {
1683             $fakequestion->createdby = $otheruser->id;
1684         }
1686         $result = question_has_capability_on($fakequestion, $capability);
1687         $this->assertEquals($expect, $result);
1688     }
1690     /**
1691      * Tests for the deprecated question_has_capability_on function when using question definition.
1692      *
1693      * @dataProvider question_capability_on_question_provider
1694      * @param   array   $capabilities The capability assignments to set.
1695      * @param   string  $capability The capability to test
1696      * @param   bool    $isowner Whether the user to create the question should be the owner or not.
1697      * @param   bool    $expect The expected result.
1698      */
1699     public function test_question_has_capability_on_using_question_definition($capabilities, $capability, $isowner, $expect) {
1700         $this->resetAfterTest();
1702         // Create the test data.
1703         $generator = $this->getDataGenerator();
1704         $questiongenerator = $generator->get_plugin_generator('core_question');
1705         $user = $generator->create_user();
1706         $otheruser = $generator->create_user();
1707         $roleid = $generator->create_role();
1708         $category = $generator->create_category();
1709         $context = context_coursecat::instance($category->id);
1710         $questioncat = $questiongenerator->create_question_category([
1711             'contextid' => $context->id,
1712         ]);
1714         // Assign the user to the role.
1715         role_assign($roleid, $user->id, $context->id);
1717         // Assign the capabilities to the role.
1718         foreach ($capabilities as $capname => $capvalue) {
1719             assign_capability($capname, $capvalue, $roleid, $context->id);
1720         }
1722         // Create the question.
1723         $qtype = 'truefalse';
1724         $overrides = [
1725             'category' => $questioncat->id,
1726         ];
1728         $question = $questiongenerator->create_question($qtype, null, $overrides);
1730         // The question generator does not support setting of the createdby for some reason.
1731         $question->createdby = ($isowner) ? $user->id : $otheruser->id;
1732         $fromform = test_question_maker::get_question_form_data($qtype, null);
1733         $fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
1734         question_bank::get_qtype($qtype)->save_question($question, $fromform);
1736         $this->setUser($user);
1737         $result = question_has_capability_on($question, $capability);
1738         $this->assertEquals($expect, $result);
1739     }
1741     /**
1742      * Tests for the deprecated question_has_capability_on function when using a real question id.
1743      *
1744      * @dataProvider question_capability_on_question_provider
1745      * @param   array   $capabilities The capability assignments to set.
1746      * @param   string  $capability The capability to test
1747      * @param   bool    $isowner Whether the user to create the question should be the owner or not.
1748      * @param   bool    $expect The expected result.
1749      */
1750     public function test_question_has_capability_on_using_question_id($capabilities, $capability, $isowner, $expect) {
1751         $this->resetAfterTest();
1753         // Create the test data.
1754         $generator = $this->getDataGenerator();
1755         $questiongenerator = $generator->get_plugin_generator('core_question');
1756         $user = $generator->create_user();
1757         $otheruser = $generator->create_user();
1758         $roleid = $generator->create_role();
1759         $category = $generator->create_category();
1760         $context = context_coursecat::instance($category->id);
1761         $questioncat = $questiongenerator->create_question_category([
1762             'contextid' => $context->id,
1763         ]);
1765         // Assign the user to the role.
1766         role_assign($roleid, $user->id, $context->id);
1768         // Assign the capabilities to the role.
1769         foreach ($capabilities as $capname => $capvalue) {
1770             assign_capability($capname, $capvalue, $roleid, $context->id);
1771         }
1773         // Create the question.
1774         $qtype = 'truefalse';
1775         $overrides = [
1776             'category' => $questioncat->id,
1777         ];
1779         $question = $questiongenerator->create_question($qtype, null, $overrides);
1781         // The question generator does not support setting of the createdby for some reason.
1782         $question->createdby = ($isowner) ? $user->id : $otheruser->id;
1783         $fromform = test_question_maker::get_question_form_data($qtype, null);
1784         $fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
1785         question_bank::get_qtype($qtype)->save_question($question, $fromform);
1787         $this->setUser($user);
1788         $result = question_has_capability_on($question->id, $capability);
1789         $this->assertEquals($expect, $result);
1790     }
1792     /**
1793      * Tests for the deprecated question_has_capability_on function when using a string as question id.
1794      *
1795      * @dataProvider question_capability_on_question_provider
1796      * @param   array   $capabilities The capability assignments to set.
1797      * @param   string  $capability The capability to test
1798      * @param   bool    $isowner Whether the user to create the question should be the owner or not.
1799      * @param   bool    $expect The expected result.
1800      */
1801     public function test_question_has_capability_on_using_question_string_id($capabilities, $capability, $isowner, $expect) {
1802         $this->resetAfterTest();
1804         // Create the test data.
1805         $generator = $this->getDataGenerator();
1806         $questiongenerator = $generator->get_plugin_generator('core_question');
1807         $user = $generator->create_user();
1808         $otheruser = $generator->create_user();
1809         $roleid = $generator->create_role();
1810         $category = $generator->create_category();
1811         $context = context_coursecat::instance($category->id);
1812         $questioncat = $questiongenerator->create_question_category([
1813             'contextid' => $context->id,
1814         ]);
1816         // Assign the user to the role.
1817         role_assign($roleid, $user->id, $context->id);
1819         // Assign the capabilities to the role.
1820         foreach ($capabilities as $capname => $capvalue) {
1821             assign_capability($capname, $capvalue, $roleid, $context->id);
1822         }
1824         // Create the question.
1825         $qtype = 'truefalse';
1826         $overrides = [
1827             'category' => $questioncat->id,
1828         ];
1830         $question = $questiongenerator->create_question($qtype, null, $overrides);
1832         // The question generator does not support setting of the createdby for some reason.
1833         $question->createdby = ($isowner) ? $user->id : $otheruser->id;
1834         $fromform = test_question_maker::get_question_form_data($qtype, null);
1835         $fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
1836         question_bank::get_qtype($qtype)->save_question($question, $fromform);
1838         $this->setUser($user);
1839         $result = question_has_capability_on((string) $question->id, $capability);
1840         $this->assertEquals($expect, $result);
1841     }
1843     /**
1844      * Tests for the question_has_capability_on function when using a moved question.
1845      *
1846      * @dataProvider question_capability_on_question_provider
1847      * @param   array   $capabilities The capability assignments to set.
1848      * @param   string  $capability The capability to test
1849      * @param   bool    $isowner Whether the user to create the question should be the owner or not.
1850      * @param   bool    $expect The expected result.
1851      */
1852     public function test_question_has_capability_on_using_moved_question($capabilities, $capability, $isowner, $expect) {
1853         $this->resetAfterTest();
1855         // Create the test data.
1856         $generator = $this->getDataGenerator();
1857         $questiongenerator = $generator->get_plugin_generator('core_question');
1858         $user = $generator->create_user();
1859         $otheruser = $generator->create_user();
1860         $roleid = $generator->create_role();
1861         $category = $generator->create_category();
1862         $context = context_coursecat::instance($category->id);
1863         $questioncat = $questiongenerator->create_question_category([
1864             'contextid' => $context->id,
1865         ]);
1867         $newcategory = $generator->create_category();
1868         $newcontext = context_coursecat::instance($newcategory->id);
1869         $newquestioncat = $questiongenerator->create_question_category([
1870             'contextid' => $newcontext->id,
1871         ]);
1873         // Assign the user to the role in the _new_ context..
1874         role_assign($roleid, $user->id, $newcontext->id);
1876         // Assign the capabilities to the role in the _new_ context.
1877         foreach ($capabilities as $capname => $capvalue) {
1878             assign_capability($capname, $capvalue, $roleid, $newcontext->id);
1879         }
1881         // Create the question.
1882         $qtype = 'truefalse';
1883         $overrides = [
1884             'category' => $questioncat->id,
1885         ];
1887         $question = $questiongenerator->create_question($qtype, null, $overrides);
1889         // The question generator does not support setting of the createdby for some reason.
1890         $question->createdby = ($isowner) ? $user->id : $otheruser->id;
1891         $fromform = test_question_maker::get_question_form_data($qtype, null);
1892         $fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
1893         question_bank::get_qtype($qtype)->save_question($question, $fromform);
1895         // Move the question.
1896         question_move_questions_to_category([$question->id], $newquestioncat->id);
1898         // Test that the capability is correct after the question has been moved.
1899         $this->setUser($user);
1900         $result = question_has_capability_on($question->id, $capability);
1901         $this->assertEquals($expect, $result);
1902     }
1904     /**
1905      * Tests for the question_has_capability_on function when using a real question.
1906      *
1907      * @dataProvider question_capability_on_question_provider
1908      * @param   array   $capabilities The capability assignments to set.
1909      * @param   string  $capability The capability to test
1910      * @param   bool    $isowner Whether the user to create the question should be the owner or not.
1911      * @param   bool    $expect The expected result.
1912      */
1913     public function test_question_has_capability_on_using_question($capabilities, $capability, $isowner, $expect) {
1914         $this->resetAfterTest();
1916         // Create the test data.
1917         $generator = $this->getDataGenerator();
1918         $questiongenerator = $generator->get_plugin_generator('core_question');
1919         $user = $generator->create_user();
1920         $otheruser = $generator->create_user();
1921         $roleid = $generator->create_role();
1922         $category = $generator->create_category();
1923         $context = context_coursecat::instance($category->id);
1924         $questioncat = $questiongenerator->create_question_category([
1925             'contextid' => $context->id,
1926         ]);
1928         // Assign the user to the role.
1929         role_assign($roleid, $user->id, $context->id);
1931         // Assign the capabilities to the role.
1932         foreach ($capabilities as $capname => $capvalue) {
1933             assign_capability($capname, $capvalue, $roleid, $context->id);
1934         }
1936         // Create the question.
1937         $question = $questiongenerator->create_question('truefalse', null, [
1938             'category' => $questioncat->id,
1939         ]);
1940         $question = question_bank::load_question_data($question->id);
1942         // The question generator does not support setting of the createdby for some reason.
1943         $question->createdby = ($isowner) ? $user->id : $otheruser->id;
1945         $this->setUser($user);
1946         $result = question_has_capability_on($question, $capability);
1947         $this->assertEquals($expect, $result);
1948     }
1950     /**
1951      * Tests that question_has_capability_on throws an exception for wrong parameter types.
1952      */
1953     public function test_question_has_capability_on_wrong_param_type() {
1954         // Create the test data.
1955         $generator = $this->getDataGenerator();
1956         $questiongenerator = $generator->get_plugin_generator('core_question');
1957         $user = $generator->create_user();
1959         $category = $generator->create_category();
1960         $context = context_coursecat::instance($category->id);
1961         $questioncat = $questiongenerator->create_question_category([
1962             'contextid' => $context->id,
1963         ]);
1965         // Create the question.
1966         $question = $questiongenerator->create_question('truefalse', null, [
1967             'category' => $questioncat->id,
1968         ]);
1969         $question = question_bank::load_question_data($question->id);
1971         // The question generator does not support setting of the createdby for some reason.
1972         $question->createdby = $user->id;
1974         $this->setUser($user);
1975         $result = question_has_capability_on((string)$question->id, 'tag');
1976         $this->assertFalse($result);
1978         $this->expectException('coding_exception');
1979         $this->expectExceptionMessage('$questionorid parameter needs to be an integer or an object.');
1980         question_has_capability_on('one', 'tag');
1981     }
1983     /**
1984      * Test of question_categorylist_parents function.
1985      */
1986     public function test_question_categorylist_parents() {
1987         $this->resetAfterTest();
1988         $generator = $this->getDataGenerator();
1989         $questiongenerator = $generator->get_plugin_generator('core_question');
1990         $category = $generator->create_category();
1991         $context = context_coursecat::instance($category->id);
1992         // Create a top category.
1993         $cat0 = question_get_top_category($context->id, true);
1994         // Add sub-categories.
1995         $cat1 = $questiongenerator->create_question_category(['parent' => $cat0->id]);
1996         $cat2 = $questiongenerator->create_question_category(['parent' => $cat1->id]);
1997         // Test the 'get parents' function.
1998         $parentcategories = question_categorylist_parents($cat2->id);
1999         $this->assertEquals($cat0->id, $parentcategories[0]);
2000         $this->assertEquals($cat1->id, $parentcategories[1]);
2001         $this->assertCount(2, $parentcategories);
2002     }
2004     public function test_question_get_export_single_question_url() {
2005         $generator = $this->getDataGenerator();
2007         // Create a course and an activity.
2008         $course = $generator->create_course();
2009         $quiz = $generator->create_module('quiz', ['course' => $course->id]);
2011         // Create a question in each place.
2012         $questiongenerator = $generator->get_plugin_generator('core_question');
2013         $courseqcat = $questiongenerator->create_question_category(['contextid' => context_course::instance($course->id)->id]);
2014         $courseq = $questiongenerator->create_question('truefalse', null, ['category' => $courseqcat->id]);
2015         $quizqcat = $questiongenerator->create_question_category(['contextid' => context_module::instance($quiz->cmid)->id]);
2016         $quizq = $questiongenerator->create_question('truefalse', null, ['category' => $quizqcat->id]);
2017         $systemqcat = $questiongenerator->create_question_category();
2018         $systemq = $questiongenerator->create_question('truefalse', null, ['category' => $systemqcat->id]);
2020         // Verify some URLs.
2021         $this->assertEquals(new moodle_url('/question/exportone.php',
2022                 ['id' => $courseq->id, 'courseid' => $course->id, 'sesskey' => sesskey()]),
2023                 question_get_export_single_question_url(question_bank::load_question_data($courseq->id)));
2025         $this->assertEquals(new moodle_url('/question/exportone.php',
2026                 ['id' => $quizq->id, 'cmid' => $quiz->cmid, 'sesskey' => sesskey()]),
2027                 question_get_export_single_question_url(question_bank::load_question($quizq->id)));
2029         $this->assertEquals(new moodle_url('/question/exportone.php',
2030                 ['id' => $systemq->id, 'courseid' => SITEID, 'sesskey' => sesskey()]),
2031                 question_get_export_single_question_url(question_bank::load_question($systemq->id)));
2032     }