MDL-63738 question bank: a link to download a single question
[moodle.git] / lib / tests / questionlib_test.php
CommitLineData
a3d5830a
PS
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17/**
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 */
dd69ecb6
RW
25use core_tag\output\tag;
26
a3d5830a
PS
27
28defined('MOODLE_INTERNAL') || die();
29
3faa26e5 30global $CFG;
cc033d48 31
3faa26e5 32require_once($CFG->libdir . '/questionlib.php');
7f7144fd 33require_once($CFG->dirroot . '/mod/quiz/locallib.php');
3faa26e5 34
cc033d48
MN
35// Get the necessary files to perform backup and restore.
36require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
37require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
a3d5830a
PS
38
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 */
cc033d48
MN
45class core_questionlib_testcase extends advanced_testcase {
46
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 }
a3d5830a 55
7f7144fd
TB
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 }
67
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();
77
78 // Create course.
79c9ad62
RW
79 $course = $this->getDataGenerator()->create_course(array(
80 'numsections' => 5,
81 'category' => $category->id
82 ));
7f7144fd
TB
83
84 $options = array(
85 'course' => $course->id,
86 'duedate' => time(),
87 );
88
89 // Generate an assignment with due date (will generate a course event).
90 $quiz = $this->getDataGenerator()->create_module('quiz', $options);
91
92 $qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
93
79c9ad62
RW
94 switch ($type) {
95 case 'course':
96 $context = context_course::instance($course->id);
97 break;
98
99 case 'category':
100 $context = context_coursecat::instance($category->id);
101 break;
102
103 case 'system':
104 $context = context_system::instance();
105 break;
106
107 default:
108 $context = context_module::instance($quiz->cmid);
109 break;
7f7144fd
TB
110 }
111
112 $qcat = $qgen->create_question_category(array('contextid' => $context->id));
113
114 $questions = array(
115 $qgen->create_question('shortanswer', null, array('category' => $qcat->id)),
116 $qgen->create_question('shortanswer', null, array('category' => $qcat->id)),
117 );
118
119 quiz_add_quiz_question($questions[0]->id, $quiz);
120
121 return array($category, $course, $quiz, $qcat, $questions);
122 }
123
a3d5830a 124 public function test_question_reorder_qtypes() {
3faa26e5
PS
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));
a3d5830a
PS
140 }
141
aa5f0511
TH
142 public function test_match_grade_options() {
143 $gradeoptions = question_bank::fraction_options_full();
144
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'));
149
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'));
154
155 $this->assertEquals(-0.1428571, match_grade_options($gradeoptions, -0.15, 'nearest'));
156 }
cc033d48
MN
157
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;
164
165 // Set to admin user.
166 $this->setAdminUser();
167
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();
172
173 // Create a couple of categories and questions.
b355a1c9
MG
174 $context1 = context_coursecat::instance($coursecat1->id);
175 $context2 = context_coursecat::instance($coursecat2->id);
cc033d48
MN
176 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
177 $questioncat1 = $questiongenerator->create_question_category(array('contextid' =>
b355a1c9 178 $context1->id));
cc033d48 179 $questioncat2 = $questiongenerator->create_question_category(array('contextid' =>
b355a1c9 180 $context2->id));
cc033d48
MN
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));
185
186 // Now lets tag these questions.
b355a1c9
MG
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'));
cc033d48
MN
191
192 // Test moving the questions to another category.
193 question_move_questions_to_category(array($question1->id, $question2->id), $questioncat2->id);
194
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)));
198
199 // Test moving them back.
200 question_move_questions_to_category(array($question1->id, $question2->id), $questioncat1->id);
201
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)));
207
208 // Now test moving a whole question category to another context.
209 question_move_category_to_context($questioncat1->id, $questioncat1->contextid, $questioncat2->contextid);
210
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)));
214
215 // Now test moving them back.
216 question_move_category_to_context($questioncat1->id, $questioncat2->contextid,
217 context_coursecat::instance($coursecat1->id)->id);
218
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)));
224
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);
227
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)));
231
232 // Create a course.
233 $course = $this->getDataGenerator()->create_course();
234
235 // Create some question categories and questions in this course.
b355a1c9 236 $coursecontext = context_course::instance($course->id);
cc033d48 237 $questioncat = $questiongenerator->create_question_category(array('contextid' =>
b355a1c9 238 $coursecontext->id));
cc033d48
MN
239 $question1 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat->id));
240 $question2 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat->id));
241
242 // Add some tags to these questions.
b355a1c9
MG
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'));
cc033d48
MN
245
246 // Create a course that we are going to restore the other course to.
247 $course2 = $this->getDataGenerator()->create_course();
248
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'];
00219425 255 $fp = get_file_packer('application/vnd.moodle.backup');
cc033d48
MN
256 $filepath = $CFG->dataroot . '/temp/backup/test-restore-course';
257 $file->extract_to_pathname($fp, $filepath);
258 $bc->destroy();
cc033d48
MN
259
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();
265
266 // Get the created question category.
869320c7
SR
267 $restoredcategory = $DB->get_record_select('question_categories', 'contextid = ? AND parent <> 0',
268 array(context_course::instance($course2->id)->id), '*', MUST_EXIST);
cc033d48
MN
269
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)));
07a069f1
EL
272
273 $rc->destroy();
cc033d48 274 }
7f7144fd
TB
275
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();
283
284 list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
285
286 question_category_delete_safe($qcat);
287
288 // Verify category deleted.
289 $criteria = array('id' => $qcat->id);
290 $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
291
292 // Verify questions deleted or moved.
293 $criteria = array('category' => $qcat->id);
294 $this->assertEquals(0, $DB->count_records('question', $criteria));
295
296 // Verify question not deleted.
297 $criteria = array('id' => $questions[0]->id);
298 $this->assertEquals(1, $DB->count_records('question', $criteria));
299 }
300
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();
311
312 list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
313
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);
320
321 // Verify category deleted.
322 $criteria = array('id' => $qcat->id);
323 $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
324
325 // Verify questions deleted or moved.
326 $criteria = array('category' => $qcat->id);
327 $this->assertEquals(0, $DB->count_records('question', $criteria));
328 }
329
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();
337
338 list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
339
340 // Get the module context id.
341 $result = question_delete_context($qcat->contextid);
342
343 // Verify category deleted.
344 $criteria = array('id' => $qcat->id);
345 $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
346
347 // Verify questions deleted or moved.
348 $criteria = array('category' => $qcat->id);
349 $this->assertEquals(0, $DB->count_records('question', $criteria));
350
351 // Test that the feedback works.
869320c7 352 $expected[] = array('top', get_string('unusedcategorydeleted', 'question'));
7f7144fd
TB
353 $expected[] = array($qcat->name, get_string('unusedcategorydeleted', 'question'));
354 $this->assertEquals($expected, $result);
355 }
356
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();
367
368 list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
369
370 // Test that the feedback works.
371 if ($feedback) {
372 $this->expectOutputRegex('|'.get_string('unusedcategorydeleted', 'question').'|');
373 }
374 question_delete_course($course, $feedback);
375
376 // Verify category deleted.
377 $criteria = array('id' => $qcat->id);
378 $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
379
380 // Verify questions deleted or moved.
381 $criteria = array('category' => $qcat->id);
382 $this->assertEquals(0, $DB->count_records('question', $criteria));
383 }
384
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();
395
396 list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
397
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);
403
404 // Verify category deleted.
405 $criteria = array('id' => $qcat->id);
406 $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
407
408 // Verify questions deleted or moved.
409 $criteria = array('category' => $qcat->id);
410 $this->assertEquals(0, $DB->count_records('question', $criteria));
411 }
31031e98 412
15f80bf5
SR
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();
423
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');
426
427 $questionsinqcat1 = count($questions1);
428 $questionsinqcat2 = count($questions2);
429
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);
438
439 // Verify category not deleted.
440 $criteria = array('id' => $qcat1->id);
441 $this->assertEquals(1, $DB->count_records('question_categories', $criteria));
442
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);
451
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));
455
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 }
460
94fe904e
SR
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();
469
470 list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
471
472 $context = context::instance_by_id($qcat->contextid);
473
474 $newcat = question_save_from_deletion(array_column($questions, 'id'),
475 $context->get_parent_context()->id, $context->get_context_name());
476
477 // Verify that the newcat itself is not a tep level category.
478 $this->assertNotEquals(0, $newcat->parent);
479
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 }
483
31031e98
FM
484 public function test_question_remove_stale_questions_from_category() {
485 global $DB;
486 $this->resetAfterTest(true);
4fa49cc6
SR
487 $this->setAdminUser();
488
31031e98
FM
489 $dg = $this->getDataGenerator();
490 $course = $dg->create_course();
491 $quiz = $dg->create_module('quiz', ['course' => $course->id]);
492
493 $qgen = $dg->get_plugin_generator('core_question');
494 $context = context_system::instance();
495
496 $qcat1 = $qgen->create_question_category(['contextid' => $context->id]);
497 $q1a = $qgen->create_question('shortanswer', null, ['category' => $qcat1->id]); // Will be hidden.
31031e98
FM
498 $DB->set_field('question', 'hidden', 1, ['id' => $q1a->id]);
499
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.
31031e98
FM
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);
4fa49cc6
SR
506 quiz_add_random_questions($quiz, 0, $qcat2->id, 1, false);
507
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);
515
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.
31031e98
FM
520
521 $this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
522 $this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
523
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]));
528
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]));
535
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 }
dd69ecb6
RW
545
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);
552
553 get_question_options($question);
554
555 $this->assertEquals($qcat, $question->categoryobject);
556 }
557
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');
564
565 get_question_options($questions);
566
567 foreach ($questions as $question) {
568 $this->assertEquals($qcat, $question->categoryobject);
569 }
570 }
571
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);
580
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']);
583
584 get_question_options($questions, true);
585
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 }
593
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);
597
598 $actualtagobjects = $question->tagobjects;
599 sort($tags);
600 sort($actualtagobjects);
601
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 }
608
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);
617
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']);
620
621 get_question_options($questions, true);
622
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 }
630
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);
634
635 $actualcoursetagobjects = $question->coursetagobjects;
636 sort($tags);
637 sort($actualcoursetagobjects);
638
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 }
646
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);
656
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']);
661
662 get_question_options($questions, true);
663
664 foreach ($questions as $question) {
665 $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
666
667 $actualtagobjects = $question->tagobjects;
668 sort($tags);
669 sort($actualtagobjects);
670
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 }
679
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);
690
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']);
695
696 get_question_options($questions, true);
697
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 });
706
707 $expectedtags = [];
708 $actualtags = $question->tags;
709 foreach ($tags as $tag) {
710 $expectedtags[$tag->id] = $tag->get_display_name();
711 }
712
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);
716
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);
722
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 }
730
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();
742
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']);
745
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);
750
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);
755
756 get_question_options($questions, true);
757
758 foreach ($questions as $question) {
759 $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
760
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 }
765
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 }
773
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);
783
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']);
786
787 get_question_options($questions, true);
788
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);
795
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 }
803
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);
815
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']);
821
822 get_question_options($questions, true);
823
824 foreach ($questions as $question) {
825 $this->assertCount(2, $question->coursetagobjects);
826
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 }
836
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);
848
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']);
855
856 // Ask to only receive course tags from $course (ignoring $siblingcourse tags).
857 get_question_options($questions, true, [$course]);
858
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 }
79c9ad62
RW
867
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);
879
880 foreach ($questions as $question) {
881 $question->contextid = $qcat->contextid;
882 }
883
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']);
887
888 question_move_question_tags_to_new_context($questions, $newcontext);
889
890 foreach ($questions as $question) {
891 $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
892
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 }
899
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);
912
913 foreach ($questions as $question) {
914 $question->contextid = $qcat->contextid;
915 }
916
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']);
923
924 question_move_question_tags_to_new_context($questions, $newcontext);
925
926 foreach ($questions as $question) {
927 $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
928
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 }
940
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();
951
952 foreach ($questions as $question) {
953 $question->contextid = $qcat->contextid;
954 }
955
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']);
959
960 question_move_question_tags_to_new_context($questions, $newcontext);
961
962 foreach ($questions as $question) {
963 $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
964
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 }
971
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();
984
985 foreach ($questions as $question) {
986 $question->contextid = $qcat->contextid;
987 }
988
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']);
995
996 question_move_question_tags_to_new_context($questions, $newcontext);
997
998 foreach ($questions as $question) {
999 $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1000
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 }
1012
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;
1024
1025 foreach ($questions as $question) {
1026 $question->contextid = $qcat->contextid;
1027 }
1028
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']);
1035
1036 question_move_question_tags_to_new_context($questions, $newcontext);
1037
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);
1042
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 }
1049
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;
1066
1067 foreach ($questions as $question) {
1068 $question->contextid = $qcat->contextid;
1069 }
1070
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']);
1081
1082 question_move_question_tags_to_new_context($questions, $newcontext);
1083
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);
1088
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 }
1099
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);
1112
1113 foreach ($questions as $question) {
1114 $question->contextid = $qcat->contextid;
1115 }
1116
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']);
1120
1121 question_move_question_tags_to_new_context($questions, $newcontext);
1122
1123 foreach ($questions as $question) {
1124 $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1125
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 }
1132
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);
1146
1147 foreach ($questions as $question) {
1148 $question->contextid = $qcat->contextid;
1149 }
1150
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']);
1159
1160 question_move_question_tags_to_new_context($questions, $newcontext);
1161
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);
1166
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 }
1174
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);
1187
1188 foreach ($questions as $question) {
1189 $question->contextid = $qcat->contextid;
1190 }
1191
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']);
1199
1200 question_move_question_tags_to_new_context($questions, $newcontext);
1201
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);
1206
1207 foreach ($tags as $tag) {
1208 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1209 }
1210 }
1211 }
1212
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);
1228
1229 foreach ($questions as $question) {
1230 $question->contextid = $qcat->contextid;
1231 }
1232
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']);
1242
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);
1246
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);
1251
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 }
1259
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);
1270
1271 foreach ($questions as $question) {
1272 $question->contextid = $qcat->contextid;
1273 }
1274
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']);
1278
1279 question_move_question_tags_to_new_context($questions, $newcontext);
1280
1281 foreach ($questions as $question) {
1282 $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1283
1284 foreach ($tags as $tag) {
1285 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1286 }
1287 }
1288 }
1289
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);
1300
1301 foreach ($questions as $question) {
1302 $question->contextid = $qcat->contextid;
1303 }
1304
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']);
1308
1309 question_move_question_tags_to_new_context($questions, $newcontext);
1310
1311 foreach ($questions as $question) {
1312 $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
1313
1314 foreach ($tags as $tag) {
1315 $this->assertEquals($newcontext->id, $tag->taginstancecontextid);
1316 }
1317 }
1318 }
1319
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);
1340
1341 foreach ($questions as $question) {
1342 $question->contextid = $qcat->contextid;
1343 }
1344
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']);
1357
1358 question_move_question_tags_to_new_context($questions, $newcontext);
1359
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);
1364
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']);
1369
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 }
2ee6e02e
SL
1378
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() {
1383
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);
1388
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']);
1391
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 }
1401
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);
1405
1406 $actualtagobjects = $tagobjects->tagobjects;
1407 sort($tags);
1408 sort($actualtagobjects);
1409
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 }
1416
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;
1422
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);
1427
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']);
1430
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);
1434
1435 $expectedtags = [];
1436 $actualtags = $tagobjects->coursetags;
1437 foreach ($actualtags as $coursetagid => $coursetagname) {
1438 $expectedtags[$coursetagid] = $coursetagname;
1439 }
1440
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);
1444
1445 $actualtagobjects = $tagobjects->coursetagobjects;
1446 sort($tags);
1447 sort($actualtagobjects);
1448
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 }
1455
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;
1461
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);
1469
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']);
1475
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);
1480
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 }
1490
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;
1496
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);
1504
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']);
1511
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) {
1516
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 }
1523
ca18d567
AN
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 }
1645
1646 /**
02b1868c 1647 * Tests for the deprecated question_has_capability_on function when passing a stdClass as parameter.
ca18d567
AN
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
02b1868c
SL
1652 * @param bool $isowner Whether the user to create the question should be the owner or not.
1653 * @param bool $expect The expected result.
ca18d567 1654 */
02b1868c 1655 public function test_question_has_capability_on_using_stdclass($capabilities, $capability, $isowner, $expect) {
ca18d567
AN
1656 $this->resetAfterTest();
1657
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);
1664
1665 // Assign the user to the role.
1666 role_assign($roleid, $user->id, $context->id);
1667
1668 // Assign the capabilities to the role.
1669 foreach ($capabilities as $capname => $capvalue) {
1670 assign_capability($capname, $capvalue, $roleid, $context->id);
1671 }
ca18d567
AN
1672
1673 $this->setUser($user);
1674
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 ];
1679
1680 if ($isowner) {
1681 $fakequestion->createdby = $user->id;
1682 } else {
1683 $fakequestion->createdby = $otheruser->id;
1684 }
1685
1686 $result = question_has_capability_on($fakequestion, $capability);
1687 $this->assertEquals($expect, $result);
1688 }
1689
1690 /**
02b1868c 1691 * Tests for the deprecated question_has_capability_on function when using question definition.
ca18d567
AN
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
02b1868c
SL
1696 * @param bool $isowner Whether the user to create the question should be the owner or not.
1697 * @param bool $expect The expected result.
ca18d567
AN
1698 */
1699 public function test_question_has_capability_on_using_question_definition($capabilities, $capability, $isowner, $expect) {
1700 $this->resetAfterTest();
1701
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 ]);
1713
1714 // Assign the user to the role.
1715 role_assign($roleid, $user->id, $context->id);
1716
1717 // Assign the capabilities to the role.
1718 foreach ($capabilities as $capname => $capvalue) {
1719 assign_capability($capname, $capvalue, $roleid, $context->id);
1720 }
ca18d567
AN
1721
1722 // Create the question.
1723 $qtype = 'truefalse';
1724 $overrides = [
1725 'category' => $questioncat->id,
1726 ];
1727
1728 $question = $questiongenerator->create_question($qtype, null, $overrides);
1729
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);
1735
1736 $this->setUser($user);
1737 $result = question_has_capability_on($question, $capability);
1738 $this->assertEquals($expect, $result);
1739 }
1740
1741 /**
02b1868c 1742 * Tests for the deprecated question_has_capability_on function when using a real question id.
ca18d567
AN
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
02b1868c
SL
1747 * @param bool $isowner Whether the user to create the question should be the owner or not.
1748 * @param bool $expect The expected result.
ca18d567
AN
1749 */
1750 public function test_question_has_capability_on_using_question_id($capabilities, $capability, $isowner, $expect) {
1751 $this->resetAfterTest();
1752
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 ]);
1764
1765 // Assign the user to the role.
1766 role_assign($roleid, $user->id, $context->id);
1767
1768 // Assign the capabilities to the role.
1769 foreach ($capabilities as $capname => $capvalue) {
1770 assign_capability($capname, $capvalue, $roleid, $context->id);
1771 }
ca18d567
AN
1772
1773 // Create the question.
1774 $qtype = 'truefalse';
1775 $overrides = [
1776 'category' => $questioncat->id,
1777 ];
1778
1779 $question = $questiongenerator->create_question($qtype, null, $overrides);
1780
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);
1786
1787 $this->setUser($user);
1788 $result = question_has_capability_on($question->id, $capability);
1789 $this->assertEquals($expect, $result);
1790 }
1791
1792 /**
02b1868c 1793 * Tests for the deprecated question_has_capability_on function when using a string as question id.
ca18d567
AN
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
02b1868c
SL
1798 * @param bool $isowner Whether the user to create the question should be the owner or not.
1799 * @param bool $expect The expected result.
ca18d567
AN
1800 */
1801 public function test_question_has_capability_on_using_question_string_id($capabilities, $capability, $isowner, $expect) {
1802 $this->resetAfterTest();
1803
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 ]);
1815
1816 // Assign the user to the role.
1817 role_assign($roleid, $user->id, $context->id);
1818
1819 // Assign the capabilities to the role.
1820 foreach ($capabilities as $capname => $capvalue) {
1821 assign_capability($capname, $capvalue, $roleid, $context->id);
1822 }
ca18d567
AN
1823
1824 // Create the question.
1825 $qtype = 'truefalse';
1826 $overrides = [
1827 'category' => $questioncat->id,
1828 ];
1829
1830 $question = $questiongenerator->create_question($qtype, null, $overrides);
1831
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);
1837
1838 $this->setUser($user);
1839 $result = question_has_capability_on((string) $question->id, $capability);
1840 $this->assertEquals($expect, $result);
1841 }
1842
1843 /**
02b1868c 1844 * Tests for the question_has_capability_on function when using a moved question.
ca18d567
AN
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
02b1868c
SL
1849 * @param bool $isowner Whether the user to create the question should be the owner or not.
1850 * @param bool $expect The expected result.
ca18d567
AN
1851 */
1852 public function test_question_has_capability_on_using_moved_question($capabilities, $capability, $isowner, $expect) {
1853 $this->resetAfterTest();
1854
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 ]);
1866
1867 $newcategory = $generator->create_category();
1868 $newcontext = context_coursecat::instance($newcategory->id);
1869 $newquestioncat = $questiongenerator->create_question_category([
1870 'contextid' => $newcontext->id,
1871 ]);
1872
1873 // Assign the user to the role in the _new_ context..
1874 role_assign($roleid, $user->id, $newcontext->id);
1875
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 }
ca18d567
AN
1880
1881 // Create the question.
1882 $qtype = 'truefalse';
1883 $overrides = [
1884 'category' => $questioncat->id,
1885 ];
1886
1887 $question = $questiongenerator->create_question($qtype, null, $overrides);
1888
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);
1894
1895 // Move the question.
1896 question_move_questions_to_category([$question->id], $newquestioncat->id);
1897
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 }
1903
1904 /**
02b1868c 1905 * Tests for the question_has_capability_on function when using a real question.
ca18d567
AN
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
02b1868c
SL
1910 * @param bool $isowner Whether the user to create the question should be the owner or not.
1911 * @param bool $expect The expected result.
ca18d567
AN
1912 */
1913 public function test_question_has_capability_on_using_question($capabilities, $capability, $isowner, $expect) {
1914 $this->resetAfterTest();
1915
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 ]);
1927
1928 // Assign the user to the role.
1929 role_assign($roleid, $user->id, $context->id);
1930
1931 // Assign the capabilities to the role.
1932 foreach ($capabilities as $capname => $capvalue) {
1933 assign_capability($capname, $capvalue, $roleid, $context->id);
1934 }
ca18d567
AN
1935
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);
1941
1942 // The question generator does not support setting of the createdby for some reason.
1943 $question->createdby = ($isowner) ? $user->id : $otheruser->id;
1944
1945 $this->setUser($user);
1946 $result = question_has_capability_on($question, $capability);
1947 $this->assertEquals($expect, $result);
1948 }
15cd0090
SL
1949
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();
1958
1959 $category = $generator->create_category();
1960 $context = context_coursecat::instance($category->id);
1961 $questioncat = $questiongenerator->create_question_category([
1962 'contextid' => $context->id,
1963 ]);
1964
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);
1970
1971 // The question generator does not support setting of the createdby for some reason.
1972 $question->createdby = $user->id;
1973
1974 $this->setUser($user);
1975 $result = question_has_capability_on((string)$question->id, 'tag');
1976 $this->assertFalse($result);
1977
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 }
d0a60444
JB
1982
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 }
616442a2
TH
2003
2004 public function test_question_get_export_single_question_url() {
2005 $generator = $this->getDataGenerator();
2006
2007 // Create a course and an activity.
2008 $course = $generator->create_course();
2009 $quiz = $generator->create_module('quiz', ['course' => $course->id]);
2010
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]);
2019
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)));
2024
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)));
2028
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 }
a3d5830a 2033}