MDL-61410 question: unit tests for question_sort_tags
[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     public function test_question_remove_stale_questions_from_category() {
414         global $DB;
415         $this->resetAfterTest(true);
416         $this->setAdminUser();
418         $dg = $this->getDataGenerator();
419         $course = $dg->create_course();
420         $quiz = $dg->create_module('quiz', ['course' => $course->id]);
422         $qgen = $dg->get_plugin_generator('core_question');
423         $context = context_system::instance();
425         $qcat1 = $qgen->create_question_category(['contextid' => $context->id]);
426         $q1a = $qgen->create_question('shortanswer', null, ['category' => $qcat1->id]);     // Will be hidden.
427         $DB->set_field('question', 'hidden', 1, ['id' => $q1a->id]);
429         $qcat2 = $qgen->create_question_category(['contextid' => $context->id]);
430         $q2a = $qgen->create_question('shortanswer', null, ['category' => $qcat2->id]);     // Will be hidden.
431         $q2b = $qgen->create_question('shortanswer', null, ['category' => $qcat2->id]);     // Will be hidden but used.
432         $DB->set_field('question', 'hidden', 1, ['id' => $q2a->id]);
433         $DB->set_field('question', 'hidden', 1, ['id' => $q2b->id]);
434         quiz_add_quiz_question($q2b->id, $quiz);
435         quiz_add_random_questions($quiz, 0, $qcat2->id, 1, false);
437         // We added one random question to the quiz and we expect the quiz to have only one random question.
438         $q2d = $DB->get_record_sql("SELECT q.*
439                                       FROM {question} q
440                                       JOIN {quiz_slots} s ON s.questionid = q.id
441                                      WHERE q.qtype = :qtype
442                                            AND s.quizid = :quizid",
443                 array('qtype' => 'random', 'quizid' => $quiz->id), MUST_EXIST);
445         // The following 2 lines have to be after the quiz_add_random_questions() call above.
446         // Otherwise, quiz_add_random_questions() will to be "smart" and use them instead of creating a new "random" question.
447         $q1b = $qgen->create_question('random', null, ['category' => $qcat1->id]);          // Will not be used.
448         $q2c = $qgen->create_question('random', null, ['category' => $qcat2->id]);          // Will not be used.
450         $this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
451         $this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
453         // Non-existing category, nothing will happen.
454         question_remove_stale_questions_from_category(0);
455         $this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
456         $this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
458         // First category, should be empty afterwards.
459         question_remove_stale_questions_from_category($qcat1->id);
460         $this->assertEquals(0, $DB->count_records('question', ['category' => $qcat1->id]));
461         $this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
462         $this->assertFalse($DB->record_exists('question', ['id' => $q1a->id]));
463         $this->assertFalse($DB->record_exists('question', ['id' => $q1b->id]));
465         // Second category, used questions should be left untouched.
466         question_remove_stale_questions_from_category($qcat2->id);
467         $this->assertEquals(0, $DB->count_records('question', ['category' => $qcat1->id]));
468         $this->assertEquals(2, $DB->count_records('question', ['category' => $qcat2->id]));
469         $this->assertFalse($DB->record_exists('question', ['id' => $q2a->id]));
470         $this->assertTrue($DB->record_exists('question', ['id' => $q2b->id]));
471         $this->assertFalse($DB->record_exists('question', ['id' => $q2c->id]));
472         $this->assertTrue($DB->record_exists('question', ['id' => $q2d->id]));
473     }
475     /**
476      * get_question_options should add the category object to the given question.
477      */
478     public function test_get_question_options_includes_category_object_single_question() {
479         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
480         $question = array_shift($questions);
482         get_question_options($question);
484         $this->assertEquals($qcat, $question->categoryobject);
485     }
487     /**
488      * get_question_options should add the category object to all of the questions in
489      * the given list.
490      */
491     public function test_get_question_options_includes_category_object_multiple_questions() {
492         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
494         get_question_options($questions);
496         foreach ($questions as $question) {
497             $this->assertEquals($qcat, $question->categoryobject);
498         }
499     }
501     /**
502      * get_question_options includes the tags for all questions in the list.
503      */
504     public function test_get_question_options_includes_question_tags() {
505         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
506         $question1 = $questions[0];
507         $question2 = $questions[1];
508         $qcontext = context::instance_by_id($qcat->contextid);
510         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
511         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['baz', 'bop']);
513         get_question_options($questions, true);
515         foreach ($questions as $question) {
516             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
517             $expectedtags = [];
518             $actualtags = $question->tags;
519             foreach ($tags as $tag) {
520                 $expectedtags[$tag->id] = $tag->get_display_name();
521             }
523             // The question should have a tags property populated with each tag id
524             // and display name as a key vale pair.
525             $this->assertEquals($expectedtags, $actualtags);
527             $actualtagobjects = $question->tagobjects;
528             sort($tags);
529             sort($actualtagobjects);
531             // The question should have a full set of each tag object.
532             $this->assertEquals($tags, $actualtagobjects);
533             // The question should not have any course tags.
534             $this->assertEmpty($question->coursetagobjects);
535         }
536     }
538     /**
539      * get_question_options includes the course tags for all questions in the list.
540      */
541     public function test_get_question_options_includes_course_tags() {
542         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
543         $question1 = $questions[0];
544         $question2 = $questions[1];
545         $coursecontext = context_course::instance($course->id);
547         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo', 'bar']);
548         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['baz', 'bop']);
550         get_question_options($questions, true);
552         foreach ($questions as $question) {
553             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
554             $expectedcoursetags = [];
555             $actualcoursetags = $question->coursetags;
556             foreach ($tags as $tag) {
557                 $expectedcoursetags[$tag->id] = $tag->get_display_name();
558             }
560             // The question should have a coursetags property populated with each tag id
561             // and display name as a key vale pair.
562             $this->assertEquals($expectedcoursetags, $actualcoursetags);
564             $actualcoursetagobjects = $question->coursetagobjects;
565             sort($tags);
566             sort($actualcoursetagobjects);
568             // The question should have a full set of the course tag objects.
569             $this->assertEquals($tags, $actualcoursetagobjects);
570             // The question should not have any other tags.
571             $this->assertEmpty($question->tagobjects);
572             $this->assertEmpty($question->tags);
573         }
574     }
576     /**
577      * get_question_options only categorises a tag as a course tag if it is in a
578      * course context that is different from the question context.
579      */
580     public function test_get_question_options_course_tags_in_course_question_context() {
581         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
582         $question1 = $questions[0];
583         $question2 = $questions[1];
584         $coursecontext = context_course::instance($course->id);
586         // Create course level tags in the course context that matches the question
587         // course context.
588         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo', 'bar']);
589         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['baz', 'bop']);
591         get_question_options($questions, true);
593         foreach ($questions as $question) {
594             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
596             $actualtagobjects = $question->tagobjects;
597             sort($tags);
598             sort($actualtagobjects);
600             // The tags should not be considered course tags because they are in
601             // the same context as the question. That makes them question tags.
602             $this->assertEmpty($question->coursetagobjects);
603             // The course context tags should be returned in the regular tag object
604             // list.
605             $this->assertEquals($tags, $actualtagobjects);
606         }
607     }
609     /**
610      * get_question_options includes the tags and course tags for all questions in the list
611      * if each question has course and question level tags.
612      */
613     public function test_get_question_options_includes_question_and_course_tags() {
614         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
615         $question1 = $questions[0];
616         $question2 = $questions[1];
617         $qcontext = context::instance_by_id($qcat->contextid);
618         $coursecontext = context_course::instance($course->id);
620         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
621         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['cfoo', 'cbar']);
622         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['baz', 'bop']);
623         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['cbaz', 'cbop']);
625         get_question_options($questions, true);
627         foreach ($questions as $question) {
628             $alltags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
629             $tags = array_filter($alltags, function($tag) use ($qcontext) {
630                 return $tag->taginstancecontextid == $qcontext->id;
631             });
632             $coursetags = array_filter($alltags, function($tag) use ($coursecontext) {
633                 return $tag->taginstancecontextid == $coursecontext->id;
634             });
636             $expectedtags = [];
637             $actualtags = $question->tags;
638             foreach ($tags as $tag) {
639                 $expectedtags[$tag->id] = $tag->get_display_name();
640             }
642             // The question should have a tags property populated with each tag id
643             // and display name as a key vale pair.
644             $this->assertEquals($expectedtags, $actualtags);
646             $actualtagobjects = $question->tagobjects;
647             sort($tags);
648             sort($actualtagobjects);
649             // The question should have a full set of each tag object.
650             $this->assertEquals($tags, $actualtagobjects);
652             $actualcoursetagobjects = $question->coursetagobjects;
653             sort($coursetags);
654             sort($actualcoursetagobjects);
655             // The question should have a full set of course tag objects.
656             $this->assertEquals($coursetags, $actualcoursetagobjects);
657         }
658     }
660     /**
661      * get_question_options should update the context id to the question category
662      * context id for any non-course context tag that isn't in the question category
663      * context.
664      */
665     public function test_get_question_options_normalises_question_tags() {
666         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
667         $question1 = $questions[0];
668         $question2 = $questions[1];
669         $qcontext = context::instance_by_id($qcat->contextid);
670         $systemcontext = context_system::instance();
672         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
673         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['baz', 'bop']);
675         $q1tags = core_tag_tag::get_item_tags('core_question', 'question', $question1->id);
676         $q2tags = core_tag_tag::get_item_tags('core_question', 'question', $question2->id);
677         $q1tag = array_shift($q1tags);
678         $q2tag = array_shift($q2tags);
680         // Change two of the tag instances to be a different (non-course) context to the
681         // question tag context. These tags should then be normalised back to the question
682         // tag context.
683         core_tag_tag::change_instances_context([$q1tag->taginstanceid, $q2tag->taginstanceid], $systemcontext);
685         get_question_options($questions, true);
687         foreach ($questions as $question) {
688             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
690             // The database should have been updated with the correct context id.
691             foreach ($tags as $tag) {
692                 $this->assertEquals($qcontext->id, $tag->taginstancecontextid);
693             }
695             // The tag objects on the question should have been updated with the
696             // correct context id.
697             foreach ($question->tagobjects as $tag) {
698                 $this->assertEquals($qcontext->id, $tag->taginstancecontextid);
699             }
700         }
701     }
703     /**
704      * get_question_options if the question is a course level question then tags
705      * in that context should not be consdered course tags, they are question tags.
706      */
707     public function test_get_question_options_includes_course_context_question_tags() {
708         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
709         $question1 = $questions[0];
710         $question2 = $questions[1];
711         $coursecontext = context_course::instance($course->id);
713         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo', 'bar']);
714         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['baz', 'bop']);
716         get_question_options($questions, true);
718         foreach ($questions as $question) {
719             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
720             // Tags in a course context that matches the question context should
721             // not be considered course tags.
722             $this->assertEmpty($question->coursetagobjects);
723             $this->assertEmpty($question->coursetags);
725             $actualtagobjects = $question->tagobjects;
726             sort($tags);
727             sort($actualtagobjects);
728             // The tags should be considered question tags not course tags.
729             $this->assertEquals($tags, $actualtagobjects);
730         }
731     }
733     /**
734      * get_question_options should return tags from all course contexts by default.
735      */
736     public function test_get_question_options_includes_multiple_courses_tags() {
737         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
738         $question1 = $questions[0];
739         $question2 = $questions[1];
740         $coursecontext = context_course::instance($course->id);
741         // Create a sibling course.
742         $siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
743         $siblingcoursecontext = context_course::instance($siblingcourse->id);
745         // Create course tags.
746         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['c1']);
747         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['c1']);
748         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['c2']);
749         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['c2']);
751         get_question_options($questions, true);
753         foreach ($questions as $question) {
754             $this->assertCount(2, $question->coursetagobjects);
756             foreach ($question->coursetagobjects as $tag) {
757                 if ($tag->name == 'c1') {
758                     $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
759                 } else {
760                     $this->assertEquals($siblingcoursecontext->id, $tag->taginstancecontextid);
761                 }
762             }
763         }
764     }
766     /**
767      * get_question_options should filter the course tags by the given list of courses.
768      */
769     public function test_get_question_options_includes_filter_course_tags() {
770         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
771         $question1 = $questions[0];
772         $question2 = $questions[1];
773         $coursecontext = context_course::instance($course->id);
774         // Create a sibling course.
775         $siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
776         $siblingcoursecontext = context_course::instance($siblingcourse->id);
778         // Create course tags.
779         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo']);
780         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['bar']);
781         // Create sibling course tags. These should be filtered out.
782         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['filtered1']);
783         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['filtered2']);
785         // Ask to only receive course tags from $course (ignoring $siblingcourse tags).
786         get_question_options($questions, true, [$course]);
788         foreach ($questions as $question) {
789             foreach ($question->coursetagobjects as $tag) {
790                 // We should only be seeing course tags from $course. The tags from
791                 // $siblingcourse should have been filtered out.
792                 $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
793             }
794         }
795     }
797     /**
798      * question_move_question_tags_to_new_context should update all of the
799      * question tags contexts when they are moving down (from system to course
800      * category context).
801      */
802     public function test_question_move_question_tags_to_new_context_system_to_course_cat_qtags() {
803         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('system');
804         $question1 = $questions[0];
805         $question2 = $questions[1];
806         $qcontext = context::instance_by_id($qcat->contextid);
807         $newcontext = context_coursecat::instance($category->id);
809         foreach ($questions as $question) {
810             $question->contextid = $qcat->contextid;
811         }
813         // Create tags in the system context.
814         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
815         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo', 'bar']);
817         question_move_question_tags_to_new_context($questions, $newcontext);
819         foreach ($questions as $question) {
820             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
822             // All of the tags should have their context id set to the new context.
823             foreach ($tags as $tag) {
824                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
825             }
826         }
827     }
829     /**
830      * question_move_question_tags_to_new_context should update all of the question tags
831      * contexts when they are moving down (from system to course category context)
832      * but leave any tags in the course context where they are.
833      */
834     public function test_question_move_question_tags_to_new_context_system_to_course_cat_qtags_and_course_tags() {
835         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('system');
836         $question1 = $questions[0];
837         $question2 = $questions[1];
838         $qcontext = context::instance_by_id($qcat->contextid);
839         $coursecontext = context_course::instance($course->id);
840         $newcontext = context_coursecat::instance($category->id);
842         foreach ($questions as $question) {
843             $question->contextid = $qcat->contextid;
844         }
846         // Create tags in the system context.
847         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
848         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
849         // Create tags in the course context.
850         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
851         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
853         question_move_question_tags_to_new_context($questions, $newcontext);
855         foreach ($questions as $question) {
856             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
858             foreach ($tags as $tag) {
859                 if ($tag->name == 'ctag') {
860                     // Course tags should remain in the course context.
861                     $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
862                 } else {
863                     // Other tags should be updated.
864                     $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
865                 }
866             }
867         }
868     }
870     /**
871      * question_move_question_tags_to_new_context should update all of the question
872      * contexts tags when they are moving up (from course category to system context).
873      */
874     public function test_question_move_question_tags_to_new_context_course_cat_to_system_qtags() {
875         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
876         $question1 = $questions[0];
877         $question2 = $questions[1];
878         $qcontext = context::instance_by_id($qcat->contextid);
879         $newcontext = context_system::instance();
881         foreach ($questions as $question) {
882             $question->contextid = $qcat->contextid;
883         }
885         // Create tags in the course category context.
886         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
887         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo', 'bar']);
889         question_move_question_tags_to_new_context($questions, $newcontext);
891         foreach ($questions as $question) {
892             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
894             // All of the tags should have their context id set to the new context.
895             foreach ($tags as $tag) {
896                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
897             }
898         }
899     }
901     /**
902      * question_move_question_tags_to_new_context should update all of the question
903      * tags contexts when they are moving up (from course category context to system
904      * context) but leave any tags in the course context where they are.
905      */
906     public function test_question_move_question_tags_to_new_context_course_cat_to_system_qtags_and_course_tags() {
907         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
908         $question1 = $questions[0];
909         $question2 = $questions[1];
910         $qcontext = context::instance_by_id($qcat->contextid);
911         $coursecontext = context_course::instance($course->id);
912         $newcontext = context_system::instance();
914         foreach ($questions as $question) {
915             $question->contextid = $qcat->contextid;
916         }
918         // Create tags in the system context.
919         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
920         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
921         // Create tags in the course context.
922         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
923         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
925         question_move_question_tags_to_new_context($questions, $newcontext);
927         foreach ($questions as $question) {
928             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
930             foreach ($tags as $tag) {
931                 if ($tag->name == 'ctag') {
932                     // Course tags should remain in the course context.
933                     $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
934                 } else {
935                     // Other tags should be updated.
936                     $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
937                 }
938             }
939         }
940     }
942     /**
943      * question_move_question_tags_to_new_context should merge all tags into the course
944      * context when moving down from course category context into course context.
945      */
946     public function test_question_move_question_tags_to_new_context_course_cat_to_coures_qtags_and_course_tags() {
947         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
948         $question1 = $questions[0];
949         $question2 = $questions[1];
950         $qcontext = context::instance_by_id($qcat->contextid);
951         $coursecontext = context_course::instance($course->id);
952         $newcontext = $coursecontext;
954         foreach ($questions as $question) {
955             $question->contextid = $qcat->contextid;
956         }
958         // Create tags in the system context.
959         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
960         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
961         // Create tags in the course context.
962         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
963         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
965         question_move_question_tags_to_new_context($questions, $newcontext);
967         foreach ($questions as $question) {
968             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
969             // Each question should have 2 tags.
970             $this->assertCount(2, $tags);
972             foreach ($tags as $tag) {
973                 // All tags should be updated to the course context and merged in.
974                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
975             }
976         }
977     }
979     /**
980      * question_move_question_tags_to_new_context should delete all of the tag
981      * instances from sibling courses when moving the context of a question down
982      * from a course category into a course context because the other courses will
983      * no longer have access to the question.
984      */
985     public function test_question_move_question_tags_to_new_context_remove_other_course_tags() {
986         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
987         // Create a sibling course.
988         $siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
989         $question1 = $questions[0];
990         $question2 = $questions[1];
991         $qcontext = context::instance_by_id($qcat->contextid);
992         $coursecontext = context_course::instance($course->id);
993         $siblingcoursecontext = context_course::instance($siblingcourse->id);
994         $newcontext = $coursecontext;
996         foreach ($questions as $question) {
997             $question->contextid = $qcat->contextid;
998         }
1000         // Create tags in the system context.
1001         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1002         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1003         // Create tags in the target course context.
1004         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
1005         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
1006         // Create tags in the sibling course context. These should be deleted as
1007         // part of the move.
1008         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['stag']);
1009         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['stag']);
1011         question_move_question_tags_to_new_context($questions, $newcontext);
1013         foreach ($questions as $question) {
1014             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1015             // Each question should have 2 tags, 'foo' and 'ctag'.
1016             $this->assertCount(2, $tags);
1018             foreach ($tags as $tag) {
1019                 $tagname = $tag->name;
1020                 // The 'stag' should have been deleted because it's in a sibling
1021                 // course context.
1022                 $this->assertContains($tagname, ['foo', 'ctag']);
1023                 // All tags should be in the course context now.
1024                 $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
1025             }
1026         }
1027     }
1029     /**
1030      * question_move_question_tags_to_new_context should update all of the question
1031      * tags to be the course category context when moving the tags from a course
1032      * context to a course category context.
1033      */
1034     public function test_question_move_question_tags_to_new_context_course_to_course_cat() {
1035         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
1036         $question1 = $questions[0];
1037         $question2 = $questions[1];
1038         $qcontext = context::instance_by_id($qcat->contextid);
1039         // Moving up into the course category context.
1040         $newcontext = context_coursecat::instance($category->id);
1042         foreach ($questions as $question) {
1043             $question->contextid = $qcat->contextid;
1044         }
1046         // Create tags in the course context.
1047         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1048         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1050         question_move_question_tags_to_new_context($questions, $newcontext);
1052         foreach ($questions as $question) {
1053             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1055             // All of the tags should have their context id set to the new context.
1056             foreach ($tags as $tag) {
1057                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1058             }
1059         }
1060     }
1062     /**
1063      * question_move_question_tags_to_new_context should update all of the
1064      * question tags contexts when they are moving down (from system to course
1065      * category context).
1066      */
1067     public function test_question_move_question_tags_to_new_context_orphaned_tag_contexts() {
1068         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('system');
1069         $question1 = $questions[0];
1070         $question2 = $questions[1];
1071         $othercategory = $this->getDataGenerator()->create_category();
1072         $qcontext = context::instance_by_id($qcat->contextid);
1073         $newcontext = context_coursecat::instance($category->id);
1074         $othercategorycontext = context_coursecat::instance($othercategory->id);
1076         foreach ($questions as $question) {
1077             $question->contextid = $qcat->contextid;
1078         }
1080         // Create tags in the system context.
1081         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1082         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1083         // Create tags in the other course category context. These should be
1084         // update to the next context id because they represent erroneous data
1085         // from a time before context id was mandatory in the tag API.
1086         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $othercategorycontext, ['bar']);
1087         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $othercategorycontext, ['bar']);
1089         question_move_question_tags_to_new_context($questions, $newcontext);
1091         foreach ($questions as $question) {
1092             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1093             // Each question should have two tags, 'foo' and 'bar'.
1094             $this->assertCount(2, $tags);
1096             // All of the tags should have their context id set to the new context
1097             // (course category context).
1098             foreach ($tags as $tag) {
1099                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1100             }
1101         }
1102     }
1104     /**
1105      * When moving from a course category context down into an activity context
1106      * all question context tags and course tags (where the course is a parent of
1107      * the activity) should move into the new context.
1108      */
1109     public function test_question_move_question_tags_to_new_context_course_cat_to_activity_qtags_and_course_tags() {
1110         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1111         $question1 = $questions[0];
1112         $question2 = $questions[1];
1113         $qcontext = context::instance_by_id($qcat->contextid);
1114         $coursecontext = context_course::instance($course->id);
1115         $newcontext = context_module::instance($quiz->cmid);
1117         foreach ($questions as $question) {
1118             $question->contextid = $qcat->contextid;
1119         }
1121         // Create tags in the course category context.
1122         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1123         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1124         // Move the questions to the activity context which is a child context of
1125         // $coursecontext.
1126         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
1127         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
1129         question_move_question_tags_to_new_context($questions, $newcontext);
1131         foreach ($questions as $question) {
1132             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1133             // Each question should have 2 tags.
1134             $this->assertCount(2, $tags);
1136             foreach ($tags as $tag) {
1137                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1138             }
1139         }
1140     }
1142     /**
1143      * When moving from a course category context down into an activity context
1144      * all question context tags and course tags (where the course is a parent of
1145      * the activity) should move into the new context. Tags in course contexts
1146      * that are not a parent of the activity context should be deleted.
1147      */
1148     public function test_question_move_question_tags_to_new_context_course_cat_to_activity_orphaned_tags() {
1149         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1150         $question1 = $questions[0];
1151         $question2 = $questions[1];
1152         $qcontext = context::instance_by_id($qcat->contextid);
1153         $coursecontext = context_course::instance($course->id);
1154         $newcontext = context_module::instance($quiz->cmid);
1155         $othercourse = $this->getDataGenerator()->create_course();
1156         $othercoursecontext = context_course::instance($othercourse->id);
1158         foreach ($questions as $question) {
1159             $question->contextid = $qcat->contextid;
1160         }
1162         // Create tags in the course category context.
1163         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1164         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1165         // Create tags in the course context.
1166         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
1167         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
1168         // Create tags in the other course context. These should be deleted.
1169         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $othercoursecontext, ['delete']);
1170         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $othercoursecontext, ['delete']);
1172         // Move the questions to the activity context which is a child context of
1173         // $coursecontext.
1174         question_move_question_tags_to_new_context($questions, $newcontext);
1176         foreach ($questions as $question) {
1177             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1178             // Each question should have 2 tags.
1179             $this->assertCount(2, $tags);
1181             foreach ($tags as $tag) {
1182                 // Make sure we don't have any 'delete' tags.
1183                 $this->assertContains($tag->name, ['foo', 'ctag']);
1184                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1185             }
1186         }
1187     }
1189     /**
1190      * When moving from a course context down into an activity context all of the
1191      * course tags should move into the activity context.
1192      */
1193     public function test_question_move_question_tags_to_new_context_course_to_activity_qtags() {
1194         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
1195         $question1 = $questions[0];
1196         $question2 = $questions[1];
1197         $qcontext = context::instance_by_id($qcat->contextid);
1198         $newcontext = context_module::instance($quiz->cmid);
1200         foreach ($questions as $question) {
1201             $question->contextid = $qcat->contextid;
1202         }
1204         // Create tags in the course context.
1205         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1206         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1208         question_move_question_tags_to_new_context($questions, $newcontext);
1210         foreach ($questions as $question) {
1211             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1213             foreach ($tags as $tag) {
1214                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1215             }
1216         }
1217     }
1219     /**
1220      * When moving from a course context down into an activity context all of the
1221      * course tags should move into the activity context.
1222      */
1223     public function test_question_move_question_tags_to_new_context_activity_to_course_qtags() {
1224         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
1225         $question1 = $questions[0];
1226         $question2 = $questions[1];
1227         $qcontext = context::instance_by_id($qcat->contextid);
1228         $newcontext = context_course::instance($course->id);
1230         foreach ($questions as $question) {
1231             $question->contextid = $qcat->contextid;
1232         }
1234         // Create tags in the activity context.
1235         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1236         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1238         question_move_question_tags_to_new_context($questions, $newcontext);
1240         foreach ($questions as $question) {
1241             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1243             foreach ($tags as $tag) {
1244                 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1245             }
1246         }
1247     }
1249     /**
1250      * question_move_question_tags_to_new_context should update all of the
1251      * question tags contexts when they are moving down (from system to course
1252      * category context).
1253      *
1254      * Course tags within the new category context should remain while any course
1255      * tags in course contexts that can no longer access the question should be
1256      * deleted.
1257      */
1258     public function test_question_move_question_tags_to_new_context_system_to_course_cat_with_orphaned_tags() {
1259         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('system');
1260         $question1 = $questions[0];
1261         $question2 = $questions[1];
1262         $othercategory = $this->getDataGenerator()->create_category();
1263         $othercourse = $this->getDataGenerator()->create_course(['category' => $othercategory->id]);
1264         $qcontext = context::instance_by_id($qcat->contextid);
1265         $newcontext = context_coursecat::instance($category->id);
1266         $othercategorycontext = context_coursecat::instance($othercategory->id);
1267         $coursecontext = context_course::instance($course->id);
1268         $othercoursecontext = context_course::instance($othercourse->id);
1270         foreach ($questions as $question) {
1271             $question->contextid = $qcat->contextid;
1272         }
1274         // Create tags in the system context.
1275         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
1276         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
1277         // Create tags in the child course context of the new context.
1278         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['bar']);
1279         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['bar']);
1280         // Create tags in the other course context. These should be deleted when
1281         // the question moves to the new course category context because this
1282         // course belongs to a different category, which means it will no longer
1283         // have access to the question.
1284         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $othercoursecontext, ['delete']);
1285         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $othercoursecontext, ['delete']);
1287         question_move_question_tags_to_new_context($questions, $newcontext);
1289         foreach ($questions as $question) {
1290             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1291             // Each question should have two tags, 'foo' and 'bar'.
1292             $this->assertCount(2, $tags);
1294             // All of the tags should have their context id set to the new context
1295             // (course category context).
1296             foreach ($tags as $tag) {
1297                 $this->assertContains($tag->name, ['foo', 'bar']);
1299                 if ($tag->name == 'foo') {
1300                     $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1301                 } else {
1302                     $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
1303                 }
1304             }
1305         }
1306     }
1308     /**
1309      * question_sort_tags() includes the tags for all questions in the list.
1310      */
1311     public function test_question_sort_tags_includes_question_tags() {
1313         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1314         $question1 = $questions[0];
1315         $question2 = $questions[1];
1316         $qcontext = context::instance_by_id($qcat->contextid);
1318         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
1319         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['baz', 'bop']);
1321         foreach ($questions as $question) {
1322             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1323             $categorycontext = context::instance_by_id($qcat->contextid);
1324             $tagobjects = question_sort_tags($tags, $categorycontext);
1325             $expectedtags = [];
1326             $actualtags = $tagobjects->tags;
1327             foreach ($tagobjects->tagobjects as $tag) {
1328                 $expectedtags[$tag->id] = $tag->name;
1329             }
1331             // The question should have a tags property populated with each tag id
1332             // and display name as a key vale pair.
1333             $this->assertEquals($expectedtags, $actualtags);
1335             $actualtagobjects = $tagobjects->tagobjects;
1336             sort($tags);
1337             sort($actualtagobjects);
1339             // The question should have a full set of each tag object.
1340             $this->assertEquals($tags, $actualtagobjects);
1341             // The question should not have any course tags.
1342             $this->assertEmpty($tagobjects->coursetagobjects);
1343         }
1344     }
1346     /**
1347      * question_sort_tags() includes course tags for all questions in the list.
1348      */
1349     public function test_question_sort_tags_includes_question_course_tags() {
1350         global $DB;
1352         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1353         $question1 = $questions[0];
1354         $question2 = $questions[1];
1355         $coursecontext = context_course::instance($course->id);
1357         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo', 'bar']);
1358         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['baz', 'bop']);
1360         foreach ($questions as $question) {
1361             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1362             $tagobjects = question_sort_tags($tags, $qcat);
1364             $expectedtags = [];
1365             $actualtags = $tagobjects->coursetags;
1366             foreach ($actualtags as $coursetagid => $coursetagname) {
1367                 $expectedtags[$coursetagid] = $coursetagname;
1368             }
1370             // The question should have a tags property populated with each tag id
1371             // and display name as a key vale pair.
1372             $this->assertEquals($expectedtags, $actualtags);
1374             $actualtagobjects = $tagobjects->coursetagobjects;
1375             sort($tags);
1376             sort($actualtagobjects);
1378             // The question should have a full set of each tag object.
1379             $this->assertEquals($tags, $actualtagobjects);
1380             // The question should not have any course tags.
1381             $this->assertEmpty($tagobjects->tagobjects);
1382         }
1383     }
1385     /**
1386      * question_sort_tags() should return tags from all course contexts by default.
1387      */
1388     public function test_question_sort_tags_includes_multiple_courses_tags() {
1389         global $DB;
1391         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1392         $question1 = $questions[0];
1393         $question2 = $questions[1];
1394         $coursecontext = context_course::instance($course->id);
1395         // Create a sibling course.
1396         $siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
1397         $siblingcoursecontext = context_course::instance($siblingcourse->id);
1399         // Create course tags.
1400         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['c1']);
1401         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['c1']);
1402         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['c2']);
1403         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['c2']);
1405         foreach ($questions as $question) {
1406             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1407             $tagobjects = question_sort_tags($tags, $qcat);
1408             $this->assertCount(2, $tagobjects->coursetagobjects);
1410             foreach ($tagobjects->coursetagobjects as $tag) {
1411                 if ($tag->name == 'c1') {
1412                     $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
1413                 } else {
1414                     $this->assertEquals($siblingcoursecontext->id, $tag->taginstancecontextid);
1415                 }
1416             }
1417         }
1418     }
1420     /**
1421      * question_sort_tags() should filter the course tags by the given list of courses.
1422      */
1423     public function test_question_sort_tags_includes_filter_course_tags() {
1424         global $DB;
1426         list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
1427         $question1 = $questions[0];
1428         $question2 = $questions[1];
1429         $coursecontext = context_course::instance($course->id);
1430         // Create a sibling course.
1431         $siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
1432         $siblingcoursecontext = context_course::instance($siblingcourse->id);
1434         // Create course tags.
1435         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo']);
1436         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['bar']);
1437         // Create sibling course tags. These should be filtered out.
1438         core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['filtered1']);
1439         core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['filtered2']);
1441         foreach ($questions as $question) {
1442             $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1443             $tagobjects = question_sort_tags($tags, $qcat, [$course]);
1444             foreach ($tagobjects->coursetagobjects as $tag) {
1446                 // We should only be seeing course tags from $course. The tags from
1447                 // $siblingcourse should have been filtered out.
1448                 $this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
1449             }
1450         }
1451     }