MDL-74910 core_files: Additional unit tests for get_conversions_for_file
[moodle.git] / tag / tests / taglib_test.php
blob91a4681c593b758a62baf635d02d981eebd63220
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.
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 namespace core_tag;
19 use core_tag_area;
20 use core_tag_collection;
21 use core_tag_tag;
23 /**
24 * Tag related unit tests.
26 * @package core_tag
27 * @category test
28 * @copyright 2014 Mark Nelson <markn@moodle.com>
29 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31 class taglib_test extends \advanced_testcase {
33 /**
34 * Test set up.
36 * This is executed before running any test in this file.
38 public function setUp(): void {
39 $this->resetAfterTest();
42 /**
43 * Test that the tag_set function throws an exception.
44 * This function was deprecated in 3.1
46 public function test_tag_set_get() {
47 $this->expectException('coding_exception');
48 $this->expectExceptionMessage('tag_set() can not be used anymore. Please use ' .
49 'core_tag_tag::set_item_tags().');
50 tag_set();
53 /**
54 * Test that tag_set_add function throws an exception.
55 * This function was deprecated in 3.1
57 public function test_tag_set_add() {
58 $this->expectException('coding_exception');
59 $this->expectExceptionMessage('tag_set_add() can not be used anymore. Please use ' .
60 'core_tag_tag::add_item_tag().');
61 tag_set_add();
64 /**
65 * Test that tag_set_delete function returns an exception.
66 * This function was deprecated in 3.1
68 public function test_tag_set_delete() {
69 $this->expectException('coding_exception');
70 $this->expectExceptionMessage('tag_set_delete() can not be used anymore. Please use ' .
71 'core_tag_tag::remove_item_tag().');
72 tag_set_delete();
75 /**
76 * Test the core_tag_tag::add_item_tag() and core_tag_tag::remove_item_tag() functions.
78 public function test_add_remove_item_tag() {
79 global $DB;
81 // Create a course to tag.
82 $course = $this->getDataGenerator()->create_course();
84 // Create the tag and tag instance we are going to delete.
85 core_tag_tag::add_item_tag('core', 'course', $course->id, \context_course::instance($course->id), 'A random tag');
87 $this->assertEquals(1, $DB->count_records('tag'));
88 $this->assertEquals(1, $DB->count_records('tag_instance'));
90 // Call the tag_set_delete function.
91 core_tag_tag::remove_item_tag('core', 'course', $course->id, 'A random tag');
93 // Now check that there are no tags or tag instances.
94 $this->assertEquals(0, $DB->count_records('tag'));
95 $this->assertEquals(0, $DB->count_records('tag_instance'));
98 /**
99 * Test add_item_tag function correctly calculates the ordering for a new tag.
101 public function test_add_tag_ordering_calculation() {
102 global $DB;
104 $user1 = $this->getDataGenerator()->create_user();
105 $course1 = $this->getDataGenerator()->create_course();
106 $course2 = $this->getDataGenerator()->create_course();
107 $book1 = $this->getDataGenerator()->create_module('book', array('course' => $course1->id));
108 $now = time();
109 $chapter1id = $DB->insert_record('book_chapters', (object) [
110 'bookid' => $book1->id,
111 'hidden' => 0,
112 'timecreated' => $now,
113 'timemodified' => $now,
114 'importsrc' => '',
115 'content' => '',
116 'contentformat' => FORMAT_HTML,
119 // Create a tag (ordering should start at 1).
120 $ti1 = core_tag_tag::add_item_tag('core', 'course', $course1->id,
121 \context_course::instance($course1->id), 'A random tag for course 1');
122 $this->assertEquals(1, $DB->get_field('tag_instance', 'ordering', ['id' => $ti1]));
124 // Create another tag with a common component, itemtype and itemid (should increase the ordering by 1).
125 $ti2 = core_tag_tag::add_item_tag('core', 'course', $course1->id,
126 \context_course::instance($course1->id), 'Another random tag for course 1');
127 $this->assertEquals(2, $DB->get_field('tag_instance', 'ordering', ['id' => $ti2]));
129 // Create a new tag with the same component and itemtype, but different itemid (should start counting from 1 again).
130 $ti3 = core_tag_tag::add_item_tag('core', 'course', $course2->id,
131 \context_course::instance($course2->id), 'A random tag for course 2');
132 $this->assertEquals(1, $DB->get_field('tag_instance', 'ordering', ['id' => $ti3]));
134 // Create a new tag with a different itemtype (should start counting from 1 again).
135 $ti4 = core_tag_tag::add_item_tag('core', 'user', $user1->id,
136 \context_user::instance($user1->id), 'A random tag for user 1');
137 $this->assertEquals(1, $DB->get_field('tag_instance', 'ordering', ['id' => $ti4]));
139 // Create a new tag with a different component (should start counting from 1 again).
140 $ti5 = core_tag_tag::add_item_tag('mod_book', 'book_chapters', $chapter1id,
141 \context_module::instance($book1->cmid), 'A random tag for a book chapter');
142 $this->assertEquals(1, $DB->get_field('tag_instance', 'ordering', ['id' => $ti5]));
146 * Test that tag_assign function throws an exception.
147 * This function was deprecated in 3.1
149 public function test_tag_assign() {
150 $this->expectException(\coding_exception::class);
151 $this->expectExceptionMessage('tag_assign() can not be used anymore. Please use core_tag_tag::set_item_tags() ' .
152 'or core_tag_tag::add_item_tag() instead.');
153 tag_assign();
157 * Test the tag cleanup function used by the cron.
159 public function test_tag_cleanup() {
160 global $DB;
162 $task = new \core\task\tag_cron_task();
164 // Create some users.
165 $users = array();
166 for ($i = 0; $i < 10; $i++) {
167 $users[] = $this->getDataGenerator()->create_user();
170 // Create a course to tag.
171 $course = $this->getDataGenerator()->create_course();
172 $context = \context_course::instance($course->id);
174 // Test clean up instances with tags that no longer exist.
175 $tags = array();
176 $tagnames = array();
177 for ($i = 0; $i < 10; $i++) {
178 $tags[] = $tag = $this->getDataGenerator()->create_tag(array('userid' => $users[0]->id));
179 $tagnames[] = $tag->rawname;
181 // Create instances with the tags.
182 core_tag_tag::set_item_tags('core', 'course', $course->id, $context, $tagnames);
183 // We should now have ten tag instances.
184 $coursetaginstances = $DB->count_records('tag_instance', array('itemtype' => 'course'));
185 $this->assertEquals(10, $coursetaginstances);
187 // Delete four tags
188 // Manual delete of tags is done as the function will remove the instances as well.
189 $DB->delete_records('tag', array('id' => $tags[6]->id));
190 $DB->delete_records('tag', array('id' => $tags[7]->id));
191 $DB->delete_records('tag', array('id' => $tags[8]->id));
192 $DB->delete_records('tag', array('id' => $tags[9]->id));
194 // Clean up the tags.
195 $task->cleanup();
196 // Check that we now only have six tag_instance records left.
197 $coursetaginstances = $DB->count_records('tag_instance', array('itemtype' => 'course'));
198 $this->assertEquals(6, $coursetaginstances);
200 // Test clean up with users that have been deleted.
201 // Create a tag for this course.
202 foreach ($users as $user) {
203 $context = \context_user::instance($user->id);
204 core_tag_tag::set_item_tags('core', 'user', $user->id, $context, array($tags[0]->rawname));
206 $usertags = $DB->count_records('tag_instance', array('itemtype' => 'user'));
207 $this->assertCount($usertags, $users);
208 // Remove three students.
209 // Using the proper function to delete the user will also remove the tags.
210 $DB->update_record('user', array('id' => $users[4]->id, 'deleted' => 1));
211 $DB->update_record('user', array('id' => $users[5]->id, 'deleted' => 1));
212 $DB->update_record('user', array('id' => $users[6]->id, 'deleted' => 1));
214 // Clean up the tags.
215 $task->cleanup();
216 $usertags = $DB->count_records('tag_instance', array('itemtype' => 'user'));
217 $usercount = $DB->count_records('user', array('deleted' => 0));
218 // Remove admin and guest from the count.
219 $this->assertEquals($usertags, ($usercount - 2));
221 // Test clean up where a course has been removed.
222 // Delete the course. This also needs to be this way otherwise the tags are removed by using the proper function.
223 $DB->delete_records('course', array('id' => $course->id));
224 $task->cleanup();
225 $coursetags = $DB->count_records('tag_instance', array('itemtype' => 'course'));
226 $this->assertEquals(0, $coursetags);
228 // Test clean up where a post has been removed.
229 // Create default post.
230 $post = new \stdClass();
231 $post->userid = $users[1]->id;
232 $post->content = 'test post content text';
233 $post->id = $DB->insert_record('post', $post);
234 $context = \context_system::instance();
235 core_tag_tag::set_item_tags('core', 'post', $post->id, $context, array($tags[0]->rawname));
237 // Add another one with a fake post id to be removed.
238 core_tag_tag::set_item_tags('core', 'post', 15, $context, array($tags[0]->rawname));
239 // Check that there are two tag instances.
240 $posttags = $DB->count_records('tag_instance', array('itemtype' => 'post'));
241 $this->assertEquals(2, $posttags);
242 // Clean up the tags.
243 $task->cleanup();
244 // We should only have one entry left now.
245 $posttags = $DB->count_records('tag_instance', array('itemtype' => 'post'));
246 $this->assertEquals(1, $posttags);
250 * Test deleting a group of tag instances.
252 public function test_tag_bulk_delete_instances() {
253 global $DB;
254 $task = new \core\task\tag_cron_task();
256 // Setup.
257 $user = $this->getDataGenerator()->create_user();
258 $course = $this->getDataGenerator()->create_course();
259 $context = \context_course::instance($course->id);
261 // Create some tag instances.
262 for ($i = 0; $i < 10; $i++) {
263 $tag = $this->getDataGenerator()->create_tag(array('userid' => $user->id));
264 core_tag_tag::add_item_tag('core', 'course', $course->id, $context, $tag->rawname);
266 // Get tag instances. tag name and rawname are required for the event fired in this function.
267 $sql = "SELECT ti.*, t.name, t.rawname
268 FROM {tag_instance} ti
269 JOIN {tag} t ON t.id = ti.tagid";
270 $taginstances = $DB->get_records_sql($sql);
271 $this->assertCount(10, $taginstances);
272 // Run the function.
273 $task->bulk_delete_instances($taginstances);
274 // Make sure they are gone.
275 $instancecount = $DB->count_records('tag_instance');
276 $this->assertEquals(0, $instancecount);
280 * Test that setting a list of tags for "tag" item type throws exception if userid specified
282 public function test_set_item_tags_with_invalid_userid(): void {
283 $user = $this->getDataGenerator()->create_user();
285 $this->expectException(\coding_exception::class);
286 $this->expectExceptionMessage('Related tags can not have tag instance userid');
287 core_tag_tag::set_item_tags('core', 'tag', 1, \context_system::instance(), ['all', 'night', 'long'], $user->id);
291 * Prepares environment for testing tag correlations
292 * @return core_tag_tag[] list of used tags
294 protected function prepare_correlated() {
295 global $DB;
297 $user = $this->getDataGenerator()->create_user();
298 $this->setUser($user);
300 $user1 = $this->getDataGenerator()->create_user();
301 $user2 = $this->getDataGenerator()->create_user();
302 $user3 = $this->getDataGenerator()->create_user();
303 $user4 = $this->getDataGenerator()->create_user();
304 $user5 = $this->getDataGenerator()->create_user();
305 $user6 = $this->getDataGenerator()->create_user();
307 // Several records have both 'cat' and 'cats' tags attached to them.
308 // This will make those tags automatically correlated.
309 // Same with 'dog', 'dogs' and 'puppy.
310 core_tag_tag::set_item_tags('core', 'user', $user1->id, \context_user::instance($user1->id), array('cat', 'cats'));
311 core_tag_tag::set_item_tags('core', 'user', $user2->id, \context_user::instance($user2->id), array('cat', 'cats', 'kitten'));
312 core_tag_tag::set_item_tags('core', 'user', $user3->id, \context_user::instance($user3->id), array('cat', 'cats'));
313 core_tag_tag::set_item_tags('core', 'user', $user4->id, \context_user::instance($user4->id), array('dog', 'dogs', 'puppy'));
314 core_tag_tag::set_item_tags('core', 'user', $user5->id, \context_user::instance($user5->id), array('dog', 'dogs', 'puppy'));
315 core_tag_tag::set_item_tags('core', 'user', $user6->id, \context_user::instance($user6->id), array('dog', 'dogs', 'puppy'));
316 $tags = core_tag_tag::get_by_name_bulk(core_tag_collection::get_default(),
317 array('cat', 'cats', 'dog', 'dogs', 'kitten', 'puppy'), '*');
319 // Add manual relation between tags 'cat' and 'kitten'.
320 core_tag_tag::get($tags['cat']->id)->set_related_tags(array('kitten'));
322 return $tags;
326 * Test for function compute_correlations() that is part of tag cron
328 public function test_correlations() {
329 global $DB;
330 $task = new \core\task\tag_cron_task();
332 $tags = array_map(function ($t) {
333 return $t->id;
334 }, $this->prepare_correlated());
336 $task->compute_correlations();
338 $this->assertEquals($tags['cats'],
339 $DB->get_field_select('tag_correlation', 'correlatedtags',
340 'tagid = ?', array($tags['cat'])));
341 $this->assertEquals($tags['cat'],
342 $DB->get_field_select('tag_correlation', 'correlatedtags',
343 'tagid = ?', array($tags['cats'])));
344 $this->assertEquals($tags['dogs'] . ',' . $tags['puppy'],
345 $DB->get_field_select('tag_correlation', 'correlatedtags',
346 'tagid = ?', array($tags['dog'])));
347 $this->assertEquals($tags['dog'] . ',' . $tags['puppy'],
348 $DB->get_field_select('tag_correlation', 'correlatedtags',
349 'tagid = ?', array($tags['dogs'])));
350 $this->assertEquals($tags['dog'] . ',' . $tags['dogs'],
351 $DB->get_field_select('tag_correlation', 'correlatedtags',
352 'tagid = ?', array($tags['puppy'])));
354 // Make sure get_correlated_tags() returns 'cats' as the only correlated tag to the 'cat'.
355 $correlatedtags = array_values(core_tag_tag::get($tags['cat'])->get_correlated_tags(true));
356 $this->assertCount(3, $correlatedtags); // This will return all existing instances but they all point to the same tag.
357 $this->assertEquals('cats', $correlatedtags[0]->rawname);
358 $this->assertEquals('cats', $correlatedtags[1]->rawname);
359 $this->assertEquals('cats', $correlatedtags[2]->rawname);
361 $correlatedtags = array_values(core_tag_tag::get($tags['cat'])->get_correlated_tags());
362 $this->assertCount(1, $correlatedtags); // Duplicates are filtered out here.
363 $this->assertEquals('cats', $correlatedtags[0]->rawname);
365 // Make sure get_correlated_tags() returns 'dogs' and 'puppy' as the correlated tags to the 'dog'.
366 $correlatedtags = core_tag_tag::get($tags['dog'])->get_correlated_tags(true);
367 $this->assertCount(6, $correlatedtags); // 2 tags times 3 instances.
369 $correlatedtags = array_values(core_tag_tag::get($tags['dog'])->get_correlated_tags());
370 $this->assertCount(2, $correlatedtags);
371 $this->assertEquals('dogs', $correlatedtags[0]->rawname);
372 $this->assertEquals('puppy', $correlatedtags[1]->rawname);
374 // Function get_related_tags() will return both related and correlated tags.
375 $relatedtags = array_values(core_tag_tag::get($tags['cat'])->get_related_tags());
376 $this->assertCount(2, $relatedtags);
377 $this->assertEquals('kitten', $relatedtags[0]->rawname);
378 $this->assertEquals('cats', $relatedtags[1]->rawname);
380 // Also test get_correlated_tags().
381 $correlatedtags = array_values(core_tag_tag::get($tags['cat'])->get_correlated_tags(true));
382 $this->assertCount(3, $correlatedtags); // This will return all existing instances but they all point to the same tag.
383 $this->assertEquals('cats', $correlatedtags[0]->rawname);
384 $this->assertEquals('cats', $correlatedtags[1]->rawname);
385 $this->assertEquals('cats', $correlatedtags[2]->rawname);
387 $correlatedtags = array_values(core_tag_tag::get($tags['cat'])->get_correlated_tags());
388 $this->assertCount(1, $correlatedtags); // Duplicates are filtered out here.
389 $this->assertEquals('cats', $correlatedtags[0]->rawname);
391 $correlatedtags = array_values(core_tag_tag::get($tags['dog'])->get_correlated_tags(true));
392 $this->assertCount(6, $correlatedtags); // 2 tags times 3 instances.
394 $correlatedtags = array_values(core_tag_tag::get($tags['dog'])->get_correlated_tags());
395 $this->assertCount(2, $correlatedtags);
396 $this->assertEquals('dogs', $correlatedtags[0]->rawname);
397 $this->assertEquals('puppy', $correlatedtags[1]->rawname);
399 $relatedtags = array_values(core_tag_tag::get($tags['cat'])->get_related_tags());
400 $this->assertCount(2, $relatedtags);
401 $this->assertEquals('kitten', $relatedtags[0]->rawname);
402 $this->assertEquals('cats', $relatedtags[1]->rawname);
403 // End of testing deprecated methods.
405 // If we then manually set 'cat' and 'cats' as related, get_related_tags() will filter out duplicates.
406 core_tag_tag::get($tags['cat'])->set_related_tags(array('kitten', 'cats'));
408 $relatedtags = array_values(core_tag_tag::get($tags['cat'])->get_related_tags());
409 $this->assertCount(2, $relatedtags);
410 $this->assertEquals('kitten', $relatedtags[0]->rawname);
411 $this->assertEquals('cats', $relatedtags[1]->rawname);
413 // Make sure core_tag_tag::get_item_tags(), core_tag_tag::get_correlated_tags() return the same set of fields.
414 $relatedtags = core_tag_tag::get_item_tags('core', 'tag', $tags['cat']);
415 $relatedtag = reset($relatedtags);
416 $correlatedtags = core_tag_tag::get($tags['cat'])->get_correlated_tags();
417 $correlatedtag = reset($correlatedtags);
418 $this->assertEquals(array_keys((array)$relatedtag->to_object()), array_keys((array)$correlatedtag->to_object()));
420 $relatedtags = core_tag_tag::get_item_tags(null, 'tag', $tags['cat']);
421 $relatedtag = reset($relatedtags);
422 $correlatedtags = core_tag_tag::get($tags['cat'])->get_correlated_tags();
423 $correlatedtag = reset($correlatedtags);
424 $this->assertEquals(array_keys((array)$relatedtag), array_keys((array)$correlatedtag));
428 * Test for function cleanup() that is part of tag cron
430 public function test_cleanup() {
431 global $DB;
432 $task = new \core\task\tag_cron_task();
434 $user = $this->getDataGenerator()->create_user();
435 $defaultcoll = core_tag_collection::get_default();
437 // Setting tags will create non-standard tags 'cat', 'dog' and 'fish'.
438 core_tag_tag::set_item_tags('core', 'user', $user->id, \context_user::instance($user->id), array('cat', 'dog', 'fish'));
440 $this->assertTrue($DB->record_exists('tag', array('name' => 'cat')));
441 $this->assertTrue($DB->record_exists('tag', array('name' => 'dog')));
442 $this->assertTrue($DB->record_exists('tag', array('name' => 'fish')));
444 // Make tag 'dog' standard.
445 $dogtag = core_tag_tag::get_by_name($defaultcoll, 'dog', '*');
446 $fishtag = core_tag_tag::get_by_name($defaultcoll, 'fish');
447 $dogtag->update(array('isstandard' => 1));
449 // Manually remove the instances pointing on tags 'dog' and 'fish'.
450 $DB->execute('DELETE FROM {tag_instance} WHERE tagid in (?,?)', array($dogtag->id, $fishtag->id));
452 $task->cleanup();
454 // Tag 'cat' is still present because it's used. Tag 'dog' is present because it's standard.
455 // Tag 'fish' was removed because it is not standard and it is no longer used by anybody.
456 $this->assertTrue($DB->record_exists('tag', array('name' => 'cat')));
457 $this->assertTrue($DB->record_exists('tag', array('name' => 'dog')));
458 $this->assertFalse($DB->record_exists('tag', array('name' => 'fish')));
460 // Delete user without using API function.
461 $DB->update_record('user', array('id' => $user->id, 'deleted' => 1));
463 $task->cleanup();
465 // Tag 'cat' was now deleted too.
466 $this->assertFalse($DB->record_exists('tag', array('name' => 'cat')));
468 // Assign tag to non-existing record. Make sure tag was created in the DB.
469 core_tag_tag::set_item_tags('core', 'course', 1231231, \context_system::instance(), array('bird'));
470 $this->assertTrue($DB->record_exists('tag', array('name' => 'bird')));
472 $task->cleanup();
474 // Tag 'bird' was now deleted because the related record does not exist in the DB.
475 $this->assertFalse($DB->record_exists('tag', array('name' => 'bird')));
477 // Now we have a tag instance pointing on 'sometag' tag.
478 $user = $this->getDataGenerator()->create_user();
479 core_tag_tag::set_item_tags('core', 'user', $user->id, \context_user::instance($user->id), array('sometag'));
480 $sometag = core_tag_tag::get_by_name($defaultcoll, 'sometag');
482 $this->assertTrue($DB->record_exists('tag_instance', array('tagid' => $sometag->id)));
484 // Some hacker removes the tag without using API.
485 $DB->delete_records('tag', array('id' => $sometag->id));
487 $task->cleanup();
489 // The tag instances were also removed.
490 $this->assertFalse($DB->record_exists('tag_instance', array('tagid' => $sometag->id)));
493 public function test_guess_tag() {
494 global $DB;
495 $user = $this->getDataGenerator()->create_user();
496 $this->setUser($user);
497 $tag1 = $this->getDataGenerator()->create_tag(array('name' => 'Cat'));
498 $tc = core_tag_collection::create((object)array('name' => 'tagcoll'));
499 $tag2 = $this->getDataGenerator()->create_tag(array('name' => 'Cat', 'tagcollid' => $tc->id));
500 $this->assertEquals(2, count($DB->get_records('tag')));
501 $this->assertEquals(2, count(core_tag_tag::guess_by_name('Cat')));
502 $this->assertEquals(core_tag_collection::get_default(), core_tag_tag::get_by_name(0, 'Cat')->tagcollid);
505 public function test_instances() {
506 global $DB;
507 $user = $this->getDataGenerator()->create_user();
508 $this->setUser($user);
510 // Create a course to tag.
511 $course = $this->getDataGenerator()->create_course();
512 $context = \context_course::instance($course->id);
514 $initialtagscount = $DB->count_records('tag');
516 core_tag_tag::set_item_tags('core', 'course', $course->id, $context, array('Tag 1', 'Tag 2'));
517 $tags = core_tag_tag::get_item_tags('core', 'course', $course->id);
518 $tagssimple = array_values($tags);
519 $this->assertEquals(2, count($tags));
520 $this->assertEquals('Tag 1', $tagssimple[0]->rawname);
521 $this->assertEquals('Tag 2', $tagssimple[1]->rawname);
522 $this->assertEquals($initialtagscount + 2, $DB->count_records('tag'));
524 core_tag_tag::set_item_tags('core', 'course', $course->id, $context, array('Tag 3', 'Tag 2', 'Tag 1'));
525 $tags = core_tag_tag::get_item_tags('core', 'course', $course->id);
526 $tagssimple = array_values($tags);
527 $this->assertEquals(3, count($tags));
528 $this->assertEquals('Tag 3', $tagssimple[0]->rawname);
529 $this->assertEquals('Tag 2', $tagssimple[1]->rawname);
530 $this->assertEquals('Tag 1', $tagssimple[2]->rawname);
531 $this->assertEquals($initialtagscount + 3, $DB->count_records('tag'));
533 core_tag_tag::set_item_tags('core', 'course', $course->id, $context, array('Tag 3'));
534 $tags = core_tag_tag::get_item_tags('core', 'course', $course->id);
535 $tagssimple = array_values($tags);
536 $this->assertEquals(1, count($tags));
537 $this->assertEquals('Tag 3', $tagssimple[0]->rawname);
539 // Make sure the unused tags were removed from tag table.
540 $this->assertEquals($initialtagscount + 1, $DB->count_records('tag'));
543 public function test_related_tags() {
544 global $DB;
545 $user = $this->getDataGenerator()->create_user();
546 $this->setUser($user);
547 $tagcollid = core_tag_collection::get_default();
548 $tag = $this->getDataGenerator()->create_tag(array('$tagcollid' => $tagcollid, 'rawname' => 'My tag'));
549 $tag = core_tag_tag::get($tag->id, '*');
551 $tag->set_related_tags(array('Synonym 1', 'Synonym 2'));
552 $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $tag->id));
553 $this->assertEquals(2, count($relatedtags));
554 $this->assertEquals('Synonym 1', $relatedtags[0]->rawname);
555 $this->assertEquals('Synonym 2', $relatedtags[1]->rawname);
557 $t1 = core_tag_tag::get_by_name($tagcollid, 'Synonym 1', '*');
558 $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $t1->id));
559 $this->assertEquals(1, count($relatedtags));
560 $this->assertEquals('My tag', $relatedtags[0]->rawname);
562 $t2 = core_tag_tag::get_by_name($tagcollid, 'Synonym 2', '*');
563 $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $t2->id));
564 $this->assertEquals(1, count($relatedtags));
565 $this->assertEquals('My tag', $relatedtags[0]->rawname);
567 $tag->set_related_tags(array('Synonym 3', 'Synonym 2', 'Synonym 1'));
568 $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $tag->id));
569 $this->assertEquals(3, count($relatedtags));
570 $this->assertEquals('Synonym 1', $relatedtags[0]->rawname);
571 $this->assertEquals('Synonym 2', $relatedtags[1]->rawname);
572 $this->assertEquals('Synonym 3', $relatedtags[2]->rawname);
574 $t3 = core_tag_tag::get_by_name($tagcollid, 'Synonym 3', '*');
575 $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $t3->id));
576 $this->assertEquals(1, count($relatedtags));
577 $this->assertEquals('My tag', $relatedtags[0]->rawname);
579 $tag->set_related_tags(array('Synonym 3', 'Synonym 2'));
580 $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $tag->id));
581 $this->assertEquals(2, count($relatedtags));
582 $this->assertEquals('Synonym 2', $relatedtags[0]->rawname);
583 $this->assertEquals('Synonym 3', $relatedtags[1]->rawname);
585 // Assert "Synonym 1" no longer links but is still present (will be removed by cron).
586 $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $t1->id));
587 $this->assertEquals(0, count($relatedtags));
591 * Very basic test for create/move/update/delete actions, without any itemtype movements.
593 public function test_tag_coll_basic() {
594 global $DB;
596 // Make sure there is one and only one tag coll that is marked as default.
597 $tagcolls = core_tag_collection::get_collections();
598 $this->assertEquals(1, count($DB->get_records('tag_coll', array('isdefault' => 1))));
599 $defaulttagcoll = core_tag_collection::get_default();
601 // Create a new tag coll to store user tags and something else.
602 $data = (object)array('name' => 'new tag coll');
603 $tagcollid1 = core_tag_collection::create($data)->id;
604 $tagcolls = core_tag_collection::get_collections();
605 $this->assertEquals('new tag coll', $tagcolls[$tagcollid1]->name);
607 // Create a new tag coll to store post tags.
608 $data = (object)array('name' => 'posts');
609 $tagcollid2 = core_tag_collection::create($data)->id;
610 $tagcolls = core_tag_collection::get_collections();
611 $this->assertEquals('posts', $tagcolls[$tagcollid2]->name);
612 $this->assertEquals($tagcolls[$tagcollid1]->sortorder + 1,
613 $tagcolls[$tagcollid2]->sortorder);
615 // Illegal tag colls sortorder changing.
616 $this->assertFalse(core_tag_collection::change_sortorder($tagcolls[$defaulttagcoll], 1));
617 $this->assertFalse(core_tag_collection::change_sortorder($tagcolls[$defaulttagcoll], -1));
618 $this->assertFalse(core_tag_collection::change_sortorder($tagcolls[$tagcollid2], 1));
620 // Move the very last tag coll one position up.
621 $this->assertTrue(core_tag_collection::change_sortorder($tagcolls[$tagcollid2], -1));
622 $tagcolls = core_tag_collection::get_collections();
623 $this->assertEquals($tagcolls[$tagcollid2]->sortorder + 1,
624 $tagcolls[$tagcollid1]->sortorder);
626 // Move the second last tag coll one position down.
627 $this->assertTrue(core_tag_collection::change_sortorder($tagcolls[$tagcollid2], 1));
628 $tagcolls = core_tag_collection::get_collections();
629 $this->assertEquals($tagcolls[$tagcollid1]->sortorder + 1,
630 $tagcolls[$tagcollid2]->sortorder);
632 // Edit tag coll.
633 $this->assertTrue(core_tag_collection::update($tagcolls[$tagcollid2],
634 (object)array('name' => 'posts2')));
635 $tagcolls = core_tag_collection::get_collections();
636 $this->assertEquals('posts2', $tagcolls[$tagcollid2]->name);
638 // Delete tag coll.
639 $count = $DB->count_records('tag_coll');
640 $this->assertFalse(core_tag_collection::delete($tagcolls[$defaulttagcoll]));
641 $this->assertTrue(core_tag_collection::delete($tagcolls[$tagcollid1]));
642 $this->assertEquals($count - 1, $DB->count_records('tag_coll'));
646 * Prepares environment for test_move_tags_* tests
648 protected function prepare_move_tags() {
649 global $CFG;
650 require_once($CFG->dirroot.'/blog/locallib.php');
651 $this->setUser($this->getDataGenerator()->create_user());
653 $collid1 = core_tag_collection::get_default();
654 $collid2 = core_tag_collection::create(array('name' => 'newcoll'))->id;
655 $user1 = $this->getDataGenerator()->create_user();
656 $user2 = $this->getDataGenerator()->create_user();
657 $blogpost = new \blog_entry(null, array('subject' => 'test'), null);
658 $states = \blog_entry::get_applicable_publish_states();
659 $blogpost->publishstate = reset($states);
660 $blogpost->add();
662 core_tag_tag::set_item_tags('core', 'user', $user1->id, \context_user::instance($user1->id),
663 array('Tag1', 'Tag2'));
664 core_tag_tag::set_item_tags('core', 'user', $user2->id, \context_user::instance($user2->id),
665 array('Tag2', 'Tag3'));
666 $this->getDataGenerator()->create_tag(array('rawname' => 'Tag4',
667 'tagcollid' => $collid1, 'isstandard' => 1));
668 $this->getDataGenerator()->create_tag(array('rawname' => 'Tag5',
669 'tagcollid' => $collid2, 'isstandard' => 1));
671 return array($collid1, $collid2, $user1, $user2, $blogpost);
674 public function test_move_tags_simple() {
675 global $DB;
676 list($collid1, $collid2, $user1, $user2, $blogpost) = $this->prepare_move_tags();
678 // Move 'user' area from collection 1 to collection 2, make sure tags were moved completely.
679 $tagarea = $DB->get_record('tag_area', array('itemtype' => 'user', 'component' => 'core'));
680 core_tag_area::update($tagarea, array('tagcollid' => $collid2));
682 $tagsaftermove = $DB->get_records('tag');
683 foreach ($tagsaftermove as $tag) {
684 // Confirm that the time modified has not been unset.
685 $this->assertNotEmpty($tag->timemodified);
688 $this->assertEquals(array('Tag4'),
689 $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid1)));
690 $this->assertEquals(array('Tag1', 'Tag2', 'Tag3', 'Tag5'),
691 $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid2)));
692 $this->assertEquals(array('Tag1', 'Tag2'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user1->id)));
693 $this->assertEquals(array('Tag2', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user2->id)));
696 public function test_move_tags_split_tag() {
697 global $DB;
698 list($collid1, $collid2, $user1, $user2, $blogpost) = $this->prepare_move_tags();
700 core_tag_tag::set_item_tags('core', 'post', $blogpost->id, \context_system::instance(),
701 array('Tag1', 'Tag3'));
703 // Move 'user' area from collection 1 to collection 2, make sure tag Tag2 was moved and tags Tag1 and Tag3 were duplicated.
704 $tagareauser = $DB->get_record('tag_area', array('itemtype' => 'user', 'component' => 'core'));
705 core_tag_area::update($tagareauser, array('tagcollid' => $collid2));
707 $tagsaftermove = $DB->get_records('tag');
708 foreach ($tagsaftermove as $tag) {
709 // Confirm that the time modified has not been unset.
710 $this->assertNotEmpty($tag->timemodified);
713 $this->assertEquals(array('Tag1', 'Tag3', 'Tag4'),
714 $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid1)));
715 $this->assertEquals(array('Tag1', 'Tag2', 'Tag3', 'Tag5'),
716 $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid2)));
717 $this->assertEquals(array('Tag1', 'Tag2'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user1->id)));
718 $this->assertEquals(array('Tag2', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user2->id)));
719 $this->assertEquals(array('Tag1', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'post', $blogpost->id)));
722 public function test_move_tags_merge_tag() {
723 global $DB;
724 list($collid1, $collid2, $user1, $user2, $blogpost) = $this->prepare_move_tags();
726 // Set collection for 'post' tag area to be collection 2 and add some tags there.
727 $tagareablog = $DB->get_record('tag_area', array('itemtype' => 'post', 'component' => 'core'));
728 core_tag_area::update($tagareablog, array('tagcollid' => $collid2));
730 core_tag_tag::set_item_tags('core', 'post', $blogpost->id, \context_system::instance(),
731 array('TAG1', 'Tag3'));
733 // Move 'user' area from collection 1 to collection 2,
734 // make sure tag Tag2 was moved and tags Tag1 and Tag3 were merged into existing.
735 $tagareauser = $DB->get_record('tag_area', array('itemtype' => 'user', 'component' => 'core'));
736 core_tag_area::update($tagareauser, array('tagcollid' => $collid2));
738 $tagsaftermove = $DB->get_records('tag');
739 foreach ($tagsaftermove as $tag) {
740 // Confirm that the time modified has not been unset.
741 $this->assertNotEmpty($tag->timemodified);
744 $this->assertEquals(array('Tag4'),
745 $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid1)));
746 $this->assertEquals(array('TAG1', 'Tag2', 'Tag3', 'Tag5'),
747 $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid2)));
748 $this->assertEquals(array('TAG1', 'Tag2'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user1->id)));
749 $this->assertEquals(array('Tag2', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user2->id)));
750 $this->assertEquals(array('TAG1', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'post', $blogpost->id)));
753 public function test_move_tags_with_related() {
754 global $DB;
755 list($collid1, $collid2, $user1, $user2, $blogpost) = $this->prepare_move_tags();
757 // Set Tag1 to be related to Tag2 and Tag4 (in collection 1).
758 core_tag_tag::get_by_name($collid1, 'Tag1')->set_related_tags(array('Tag2', 'Tag4'));
760 // Set collection for 'post' tag area to be collection 2 and add some tags there.
761 $tagareablog = $DB->get_record('tag_area', array('itemtype' => 'post', 'component' => 'core'));
762 core_tag_area::update($tagareablog, array('tagcollid' => $collid2));
764 core_tag_tag::set_item_tags('core', 'post', $blogpost->id, \context_system::instance(),
765 array('TAG1', 'Tag3'));
767 // Move 'user' area from collection 1 to collection 2, make sure tags were moved completely.
768 $tagarea = $DB->get_record('tag_area', array('itemtype' => 'user', 'component' => 'core'));
769 core_tag_area::update($tagarea, array('tagcollid' => $collid2));
771 $tagsaftermove = $DB->get_records('tag');
772 foreach ($tagsaftermove as $tag) {
773 // Confirm that the time modified has not been unset.
774 $this->assertNotEmpty($tag->timemodified);
777 $this->assertEquals(array('Tag1', 'Tag2', 'Tag4'),
778 $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid1)));
779 $this->assertEquals(array('TAG1', 'Tag2', 'Tag3', 'Tag4', 'Tag5'),
780 $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid2)));
781 $this->assertEquals(array('TAG1', 'Tag2'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user1->id)));
782 $this->assertEquals(array('Tag2', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user2->id)));
784 $tag11 = core_tag_tag::get_by_name($collid1, 'Tag1');
785 $related11 = core_tag_tag::get($tag11->id)->get_manual_related_tags();
786 $related11 = array_map('core_tag_tag::make_display_name', $related11);
787 sort($related11); // Order of related tags may be random.
788 $this->assertEquals('Tag2, Tag4', join(', ', $related11));
790 $tag21 = core_tag_tag::get_by_name($collid2, 'TAG1');
791 $related21 = core_tag_tag::get($tag21->id)->get_manual_related_tags();
792 $related21 = array_map('core_tag_tag::make_display_name', $related21);
793 sort($related21); // Order of related tags may be random.
794 $this->assertEquals('Tag2, Tag4', join(', ', $related21));
797 public function test_move_tags_corrupted() {
798 global $DB;
799 list($collid1, $collid2, $user1, $user2, $blogpost) = $this->prepare_move_tags();
800 $collid3 = core_tag_collection::create(array('name' => 'weirdcoll'))->id;
802 // We already have Tag1 in coll1, now let's create it in coll3.
803 $extratag1 = $this->getDataGenerator()->create_tag(array('rawname' => 'Tag1',
804 'tagcollid' => $collid3, 'isstandard' => 1));
806 // Artificially add 'Tag1' from coll3 to user2.
807 $DB->insert_record('tag_instance', array('tagid' => $extratag1->id, 'itemtype' => 'user',
808 'component' => 'core', 'itemid' => $user2->id, 'ordering' => 3));
810 // Now we have corrupted data: both users are tagged with 'Tag1', however these are two tags in different collections.
811 $user1tags = array_values(core_tag_tag::get_item_tags('core', 'user', $user1->id));
812 $user2tags = array_values(core_tag_tag::get_item_tags('core', 'user', $user2->id));
813 $this->assertEquals('Tag1', $user1tags[0]->rawname);
814 $this->assertEquals('Tag1', $user2tags[2]->rawname);
815 $this->assertNotEquals($user1tags[0]->tagcollid, $user2tags[2]->tagcollid);
817 // Move user interests tag area into coll2.
818 $tagarea = $DB->get_record('tag_area', array('itemtype' => 'user', 'component' => 'core'));
819 core_tag_area::update($tagarea, array('tagcollid' => $collid2));
821 $tagsaftermove = $DB->get_records('tag');
822 foreach ($tagsaftermove as $tag) {
823 // Confirm that the time modified has not been unset.
824 $this->assertNotEmpty($tag->timemodified);
827 // Now all tags are correctly moved to the new collection and both tags 'Tag1' were merged.
828 $user1tags = array_values(core_tag_tag::get_item_tags('core', 'user', $user1->id));
829 $user2tags = array_values(core_tag_tag::get_item_tags('core', 'user', $user2->id));
830 $this->assertEquals('Tag1', $user1tags[0]->rawname);
831 $this->assertEquals('Tag1', $user2tags[2]->rawname);
832 $this->assertEquals($collid2, $user1tags[0]->tagcollid);
833 $this->assertEquals($collid2, $user2tags[2]->tagcollid);
837 * Tests that tag_normalize function throws an exception.
838 * This function was deprecated in 3.1
840 public function test_normalize() {
841 $this->expectException(\coding_exception::class);
842 $this->expectExceptionMessage('tag_normalize() can not be used anymore. Please use ' .
843 'core_tag_tag::normalize().');
844 tag_normalize();
848 * Test functions core_tag_tag::create_if_missing() and core_tag_tag::get_by_name_bulk().
850 public function test_create_get() {
851 $tagset = array('Cat', ' Dog ', '<Mouse', '<>', 'mouse', 'Dog');
853 $collid = core_tag_collection::get_default();
854 $tags = core_tag_tag::create_if_missing($collid, $tagset);
855 $this->assertEquals(array('cat', 'dog', 'mouse'), array_keys($tags));
856 $this->assertEquals('Dog', $tags['dog']->rawname);
857 $this->assertEquals('mouse', $tags['mouse']->rawname); // Case of the last tag wins.
859 $tags2 = core_tag_tag::create_if_missing($collid, array('CAT', 'Elephant'));
860 $this->assertEquals(array('cat', 'elephant'), array_keys($tags2));
861 $this->assertEquals('Cat', $tags2['cat']->rawname);
862 $this->assertEquals('Elephant', $tags2['elephant']->rawname);
863 $this->assertEquals($tags['cat']->id, $tags2['cat']->id); // Tag 'cat' already existed and was not created again.
865 $tags3 = core_tag_tag::get_by_name_bulk($collid, $tagset);
866 $this->assertEquals(array('cat', 'dog', 'mouse'), array_keys($tags3));
867 $this->assertEquals('Dog', $tags3['dog']->rawname);
868 $this->assertEquals('mouse', $tags3['mouse']->rawname);
873 * Testing function core_tag_tag::combine_tags()
875 public function test_combine_tags() {
876 $initialtags = array(
877 array('Cat', 'Dog'),
878 array('Dog', 'Cat'),
879 array('Cats', 'Hippo'),
880 array('Hippo', 'Cats'),
881 array('Cat', 'Mouse', 'Kitten'),
882 array('Cats', 'Mouse', 'Kitten'),
883 array('Kitten', 'Mouse', 'Cat'),
884 array('Kitten', 'Mouse', 'Cats'),
885 array('Cats', 'Mouse', 'Kitten'),
886 array('Mouse', 'Hippo')
889 $finaltags = array(
890 array('Cat', 'Dog'),
891 array('Dog', 'Cat'),
892 array('Cat', 'Hippo'),
893 array('Hippo', 'Cat'),
894 array('Cat', 'Mouse'),
895 array('Cat', 'Mouse'),
896 array('Mouse', 'Cat'),
897 array('Mouse', 'Cat'),
898 array('Cat', 'Mouse'),
899 array('Mouse', 'Hippo')
902 $collid = core_tag_collection::get_default();
903 $context = \context_system::instance();
904 foreach ($initialtags as $id => $taglist) {
905 core_tag_tag::set_item_tags('core', 'course', $id + 10, $context, $initialtags[$id]);
908 core_tag_tag::get_by_name($collid, 'Cats', '*')->update(array('isstandard' => 1));
910 // Combine tags 'Cats' and 'Kitten' into 'Cat'.
911 $cat = core_tag_tag::get_by_name($collid, 'Cat', '*');
912 $cats = core_tag_tag::get_by_name($collid, 'Cats', '*');
913 $kitten = core_tag_tag::get_by_name($collid, 'Kitten', '*');
914 $cat->combine_tags(array($cats, $kitten));
916 foreach ($finaltags as $id => $taglist) {
917 $this->assertEquals($taglist,
918 array_values(core_tag_tag::get_item_tags_array('core', 'course', $id + 10)),
919 'Original array ('.join(', ', $initialtags[$id]).')');
922 // Ensure combined tags are deleted and 'Cat' is now official (because 'Cats' was official).
923 $this->assertEmpty(core_tag_tag::get_by_name($collid, 'Cats'));
924 $this->assertEmpty(core_tag_tag::get_by_name($collid, 'Kitten'));
925 $cattag = core_tag_tag::get_by_name($collid, 'Cat', '*');
926 $this->assertEquals(1, $cattag->isstandard);
930 * Testing function core_tag_tag::combine_tags() when related tags are present.
932 public function test_combine_tags_with_related() {
933 $collid = core_tag_collection::get_default();
934 $context = \context_system::instance();
935 core_tag_tag::set_item_tags('core', 'course', 10, $context, array('Cat', 'Cats', 'Dog'));
936 core_tag_tag::get_by_name($collid, 'Cat', '*')->set_related_tags(array('Kitty'));
937 core_tag_tag::get_by_name($collid, 'Cats', '*')->set_related_tags(array('Cat', 'Kitten', 'Kitty'));
939 // Combine tags 'Cats' into 'Cat'.
940 $cat = core_tag_tag::get_by_name($collid, 'Cat', '*');
941 $cats = core_tag_tag::get_by_name($collid, 'Cats', '*');
942 $cat->combine_tags(array($cats));
944 // Ensure 'Cat' is now related to 'Kitten' and 'Kitty' (order of related tags may be random).
945 $relatedtags = array_map(function($t) {return $t->rawname;}, $cat->get_manual_related_tags());
946 sort($relatedtags);
947 $this->assertEquals(array('Kitten', 'Kitty'), array_values($relatedtags));
951 * Testing function core_tag_tag::combine_tags() when correlated tags are present.
953 public function test_combine_tags_with_correlated() {
954 $task = new \core\task\tag_cron_task();
956 $tags = $this->prepare_correlated();
958 $task->compute_correlations();
959 // Now 'cat' is correlated with 'cats'.
960 // Also 'dog', 'dogs' and 'puppy' are correlated.
961 // There is a manual relation between 'cat' and 'kitten'.
962 // See function test_correlations() for assertions.
964 // Combine tags 'dog' and 'kitten' into 'cat' and make sure that cat is now correlated with dogs and puppy.
965 $tags['cat']->combine_tags(array($tags['dog'], $tags['kitten']));
967 $correlatedtags = $this->get_correlated_tags_names($tags['cat']);
968 $this->assertEquals(['cats', 'dogs', 'puppy'], $correlatedtags);
970 $correlatedtags = $this->get_correlated_tags_names($tags['dogs']);
971 $this->assertEquals(['cat', 'puppy'], $correlatedtags);
973 $correlatedtags = $this->get_correlated_tags_names($tags['puppy']);
974 $this->assertEquals(['cat', 'dogs'], $correlatedtags);
976 // Add tag that does not have any correlations.
977 $user7 = $this->getDataGenerator()->create_user();
978 core_tag_tag::set_item_tags('core', 'user', $user7->id, \context_user::instance($user7->id), array('hippo'));
979 $tags['hippo'] = core_tag_tag::get_by_name(core_tag_collection::get_default(), 'hippo', '*');
981 // Combine tag 'cat' into 'hippo'. Now 'hippo' should have the same correlations 'cat' used to have and also
982 // tags 'dogs' and 'puppy' should have 'hippo' in correlations.
983 $tags['hippo']->combine_tags(array($tags['cat']));
985 $correlatedtags = $this->get_correlated_tags_names($tags['hippo']);
986 $this->assertEquals(['cats', 'dogs', 'puppy'], $correlatedtags);
988 $correlatedtags = $this->get_correlated_tags_names($tags['dogs']);
989 $this->assertEquals(['hippo', 'puppy'], $correlatedtags);
991 $correlatedtags = $this->get_correlated_tags_names($tags['puppy']);
992 $this->assertEquals(['dogs', 'hippo'], $correlatedtags);
996 * get_tags_by_area_in_contexts should return an empty array if there
997 * are no tag instances for the area in the given context.
999 public function test_get_tags_by_area_in_contexts_empty() {
1000 $tagnames = ['foo'];
1001 $collid = core_tag_collection::get_default();
1002 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1003 $user = $this->getDataGenerator()->create_user();
1004 $context = \context_user::instance($user->id);
1005 $component = 'core';
1006 $itemtype = 'user';
1008 $result = core_tag_tag::get_tags_by_area_in_contexts($component, $itemtype, [$context]);
1009 $this->assertEmpty($result);
1013 * get_tags_by_area_in_contexts should return an array of tags that
1014 * have instances in the given context even when there is only a single
1015 * instance.
1017 public function test_get_tags_by_area_in_contexts_single_tag_one_context() {
1018 $tagnames = ['foo'];
1019 $collid = core_tag_collection::get_default();
1020 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1021 $user = $this->getDataGenerator()->create_user();
1022 $context = \context_user::instance($user->id);
1023 $component = 'core';
1024 $itemtype = 'user';
1025 core_tag_tag::set_item_tags($component, $itemtype, $user->id, $context, $tagnames);
1027 $result = core_tag_tag::get_tags_by_area_in_contexts($component, $itemtype, [$context]);
1028 $expected = array_map(function($t) {
1029 return $t->id;
1030 }, $tags);
1031 $actual = array_map(function($t) {
1032 return $t->id;
1033 }, $result);
1035 sort($expected);
1036 sort($actual);
1038 $this->assertEquals($expected, $actual);
1042 * get_tags_by_area_in_contexts should return all tags in an array
1043 * that have tag instances in for the area in the given context and
1044 * should ignore all tags that don't have an instance.
1046 public function test_get_tags_by_area_in_contexts_multiple_tags_one_context() {
1047 $tagnames = ['foo', 'bar', 'baz'];
1048 $collid = core_tag_collection::get_default();
1049 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1050 $user = $this->getDataGenerator()->create_user();
1051 $context = \context_user::instance($user->id);
1052 $component = 'core';
1053 $itemtype = 'user';
1054 core_tag_tag::set_item_tags($component, $itemtype, $user->id, $context, array_slice($tagnames, 0, 2));
1056 $result = core_tag_tag::get_tags_by_area_in_contexts($component, $itemtype, [$context]);
1057 $expected = ['foo', 'bar'];
1058 $actual = array_map(function($t) {
1059 return $t->name;
1060 }, $result);
1062 sort($expected);
1063 sort($actual);
1065 $this->assertEquals($expected, $actual);
1069 * get_tags_by_area_in_contexts should return the unique set of
1070 * tags for a area in the given contexts. Multiple tag instances of
1071 * the same tag don't result in duplicates in the result set.
1073 * Tags with tag instances in the same area with in difference contexts
1074 * should be ignored.
1076 public function test_get_tags_by_area_in_contexts_multiple_tags_multiple_contexts() {
1077 $tagnames = ['foo', 'bar', 'baz', 'bop', 'bam', 'bip'];
1078 $collid = core_tag_collection::get_default();
1079 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1080 $user1 = $this->getDataGenerator()->create_user();
1081 $user2 = $this->getDataGenerator()->create_user();
1082 $user3 = $this->getDataGenerator()->create_user();
1083 $context1 = \context_user::instance($user1->id);
1084 $context2 = \context_user::instance($user2->id);
1085 $context3 = \context_user::instance($user3->id);
1086 $component = 'core';
1087 $itemtype = 'user';
1089 // User 1 tags: 'foo', 'bar'.
1090 core_tag_tag::set_item_tags($component, $itemtype, $user1->id, $context1, array_slice($tagnames, 0, 2));
1091 // User 2 tags: 'bar', 'baz'.
1092 core_tag_tag::set_item_tags($component, $itemtype, $user2->id, $context2, array_slice($tagnames, 1, 2));
1093 // User 3 tags: 'bop', 'bam'.
1094 core_tag_tag::set_item_tags($component, $itemtype, $user3->id, $context3, array_slice($tagnames, 3, 2));
1096 $result = core_tag_tag::get_tags_by_area_in_contexts($component, $itemtype, [$context1, $context2]);
1097 // Both User 1 and 2 have tagged using 'bar' but we don't
1098 // expect duplicate tags in the result since they are the same
1099 // tag.
1101 // User 3 has tagged 'bop' and 'bam' but we aren't searching in
1102 // that context so they shouldn't be in the results.
1103 $expected = ['foo', 'bar', 'baz'];
1104 $actual = array_map(function($t) {
1105 return $t->name;
1106 }, $result);
1108 sort($expected);
1109 sort($actual);
1111 $this->assertEquals($expected, $actual);
1115 * get_items_tags should return an empty array if the tag area is disabled.
1117 public function test_get_items_tags_disabled_component() {
1118 global $CFG;
1120 $user1 = $this->getDataGenerator()->create_user();
1121 $context1 = \context_user::instance($user1->id);
1122 $component = 'core';
1123 $itemtype = 'user';
1124 $itemids = [$user1->id];
1126 // User 1 tags: 'foo', 'bar'.
1127 core_tag_tag::set_item_tags($component, $itemtype, $user1->id, $context1, ['foo']);
1128 // This mimics disabling tags for a component.
1129 $CFG->usetags = false;
1130 $result = core_tag_tag::get_items_tags($component, $itemtype, $itemids);
1131 $this->assertEmpty($result);
1135 * get_items_tags should return an empty array if the tag item ids list
1136 * is empty.
1138 public function test_get_items_tags_empty_itemids() {
1139 $user1 = $this->getDataGenerator()->create_user();
1140 $context1 = \context_user::instance($user1->id);
1141 $component = 'core';
1142 $itemtype = 'user';
1144 // User 1 tags: 'foo', 'bar'.
1145 core_tag_tag::set_item_tags($component, $itemtype, $user1->id, $context1, ['foo']);
1146 $result = core_tag_tag::get_items_tags($component, $itemtype, []);
1147 $this->assertEmpty($result);
1151 * get_items_tags should return an array indexed by the item ids with empty
1152 * arrays as the values when the component or itemtype is unknown.
1154 public function test_get_items_tags_unknown_component_itemtype() {
1155 $itemids = [1, 2, 3];
1156 $result = core_tag_tag::get_items_tags('someunknowncomponent', 'user', $itemids);
1157 foreach ($itemids as $itemid) {
1158 // Unknown component should return an array indexed by the item ids
1159 // with empty arrays as the values.
1160 $this->assertEmpty($result[$itemid]);
1163 $result = core_tag_tag::get_items_tags('core', 'someunknownitemtype', $itemids);
1164 foreach ($itemids as $itemid) {
1165 // Unknown item type should return an array indexed by the item ids
1166 // with empty arrays as the values.
1167 $this->assertEmpty($result[$itemid]);
1172 * get_items_tags should return an array indexed by the item ids with empty
1173 * arrays as the values for any item ids that don't have tag instances.
1175 * Data setup:
1176 * Users: 1, 2, 3
1177 * Tags: user 1 = ['foo', 'bar']
1178 * user 2 = ['baz', 'bop']
1179 * user 3 = []
1181 * Expected result:
1183 * 1 => [
1184 * 1 => 'foo',
1185 * 2 => 'bar'
1186 * ],
1187 * 2 => [
1188 * 3 => 'baz',
1189 * 4 => 'bop'
1190 * ],
1191 * 3 => []
1194 public function test_get_items_tags_missing_itemids() {
1195 $user1 = $this->getDataGenerator()->create_user();
1196 $user2 = $this->getDataGenerator()->create_user();
1197 $user3 = $this->getDataGenerator()->create_user();
1198 $context1 = \context_user::instance($user1->id);
1199 $context2 = \context_user::instance($user2->id);
1200 $component = 'core';
1201 $itemtype = 'user';
1202 $itemids = [$user1->id, $user2->id, $user3->id];
1203 $expecteduser1tagnames = ['foo', 'bar'];
1204 $expecteduser2tagnames = ['baz', 'bop'];
1205 $expecteduser3tagnames = [];
1207 // User 1 tags: 'foo', 'bar'.
1208 core_tag_tag::set_item_tags($component, $itemtype, $user1->id, $context1, $expecteduser1tagnames);
1209 // User 2 tags: 'bar', 'baz'.
1210 core_tag_tag::set_item_tags($component, $itemtype, $user2->id, $context2, $expecteduser2tagnames);
1212 $result = core_tag_tag::get_items_tags($component, $itemtype, $itemids);
1213 $actualuser1tagnames = array_map(function($taginstance) {
1214 return $taginstance->name;
1215 }, $result[$user1->id]);
1216 $actualuser2tagnames = array_map(function($taginstance) {
1217 return $taginstance->name;
1218 }, $result[$user2->id]);
1219 $actualuser3tagnames = $result[$user3->id];
1221 sort($expecteduser1tagnames);
1222 sort($expecteduser2tagnames);
1223 sort($actualuser1tagnames);
1224 sort($actualuser2tagnames);
1226 $this->assertEquals($expecteduser1tagnames, $actualuser1tagnames);
1227 $this->assertEquals($expecteduser2tagnames, $actualuser2tagnames);
1228 $this->assertEquals($expecteduser3tagnames, $actualuser3tagnames);
1232 * set_item_tags should remove any tags that aren't in the given list and should
1233 * add any instances that are missing.
1235 public function test_set_item_tags_no_multiple_context_add_remove_instances() {
1236 $tagnames = ['foo', 'bar', 'baz', 'bop'];
1237 $collid = core_tag_collection::get_default();
1238 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1239 $user1 = $this->getDataGenerator()->create_user();
1240 $context = \context_user::instance($user1->id);
1241 $component = 'core';
1242 $itemtype = 'user';
1243 $itemid = 1;
1244 $tagareas = core_tag_area::get_areas();
1245 $tagarea = $tagareas[$itemtype][$component];
1246 $newtagnames = ['bar', 'baz', 'bop'];
1248 // Make sure the tag area doesn't allow multiple contexts.
1249 core_tag_area::update($tagarea, ['multiplecontexts' => false]);
1251 // Create tag instances in separate contexts.
1252 $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1253 $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context);
1255 core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context, $newtagnames);
1257 $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1258 $actualtagnames = array_map(function($record) {
1259 return $record->name;
1260 }, $result);
1262 sort($newtagnames);
1263 sort($actualtagnames);
1265 // The list of tags should match the $newtagnames which means 'foo'
1266 // should have been removed while 'baz' and 'bop' were added. 'bar'
1267 // should remain as it was in the new list of tags.
1268 $this->assertEquals($newtagnames, $actualtagnames);
1272 * set_item_tags should set all of the tag instance context ids to the given
1273 * context if the tag area for the items doesn't allow multiple contexts for
1274 * the tag instances.
1276 public function test_set_item_tags_no_multiple_context_updates_context_of_instances() {
1277 $tagnames = ['foo', 'bar'];
1278 $collid = core_tag_collection::get_default();
1279 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1280 $user1 = $this->getDataGenerator()->create_user();
1281 $user2 = $this->getDataGenerator()->create_user();
1282 $context1 = \context_user::instance($user1->id);
1283 $context2 = \context_user::instance($user2->id);
1284 $component = 'core';
1285 $itemtype = 'user';
1286 $itemid = 1;
1287 $tagareas = core_tag_area::get_areas();
1288 $tagarea = $tagareas[$itemtype][$component];
1290 // Make sure the tag area doesn't allow multiple contexts.
1291 core_tag_area::update($tagarea, ['multiplecontexts' => false]);
1293 // Create tag instances in separate contexts.
1294 $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1295 $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context2);
1297 core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context1, $tagnames);
1299 $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1300 $this->assertCount(count($tagnames), $result);
1302 foreach ($result as $tag) {
1303 // The core user tag area doesn't allow multiple contexts for tag instances
1304 // so set_item_tags should have set all of the tag instance context ids
1305 // to match $context1.
1306 $this->assertEquals($context1->id, $tag->taginstancecontextid);
1311 * set_item_tags should delete all of the tag instances that don't match
1312 * the new set of tags, regardless of the context that the tag instance
1313 * is in.
1315 public function test_set_item_tags_no_multiple_contex_deletes_old_instancest() {
1316 $tagnames = ['foo', 'bar', 'baz', 'bop'];
1317 $collid = core_tag_collection::get_default();
1318 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1319 $user1 = $this->getDataGenerator()->create_user();
1320 $user2 = $this->getDataGenerator()->create_user();
1321 $context1 = \context_user::instance($user1->id);
1322 $context2 = \context_user::instance($user2->id);
1323 $component = 'core';
1324 $itemtype = 'user';
1325 $itemid = 1;
1326 $expectedtagnames = ['foo', 'baz'];
1327 $tagareas = core_tag_area::get_areas();
1328 $tagarea = $tagareas[$itemtype][$component];
1330 // Make sure the tag area doesn't allow multiple contexts.
1331 core_tag_area::update($tagarea, ['multiplecontexts' => false]);
1333 // Create tag instances in separate contexts.
1334 $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1335 $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context1);
1336 $this->add_tag_instance($tags['baz'], $component, $itemtype, $itemid, $context2);
1337 $this->add_tag_instance($tags['bop'], $component, $itemtype, $itemid, $context2);
1339 core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context1, $expectedtagnames);
1341 $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1342 $actualtagnames = array_map(function($record) {
1343 return $record->name;
1344 }, $result);
1346 sort($expectedtagnames);
1347 sort($actualtagnames);
1349 // The list of tags should match the $expectedtagnames.
1350 $this->assertEquals($expectedtagnames, $actualtagnames);
1352 foreach ($result as $tag) {
1353 // The core user tag area doesn't allow multiple contexts for tag instances
1354 // so set_item_tags should have set all of the tag instance context ids
1355 // to match $context1.
1356 $this->assertEquals($context1->id, $tag->taginstancecontextid);
1361 * set_item_tags should not change tag instances in a different context to the one
1362 * it's opertating on if the tag area allows instances from multiple contexts.
1364 public function test_set_item_tags_allow_multiple_context_doesnt_update_context() {
1365 global $DB;
1366 $tagnames = ['foo', 'bar', 'bop'];
1367 $collid = core_tag_collection::get_default();
1368 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1369 $user1 = $this->getDataGenerator()->create_user();
1370 $user2 = $this->getDataGenerator()->create_user();
1371 $context1 = \context_user::instance($user1->id);
1372 $context2 = \context_user::instance($user2->id);
1373 $component = 'core';
1374 $itemtype = 'user';
1375 $itemid = 1;
1376 $tagareas = core_tag_area::get_areas();
1377 $tagarea = $tagareas[$itemtype][$component];
1379 // Make sure the tag area allows multiple contexts.
1380 core_tag_area::update($tagarea, ['multiplecontexts' => true]);
1382 // Create tag instances in separate contexts.
1383 $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1384 $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context2);
1386 // Set the list of tags for $context1. This includes a tag that already exists
1387 // in that context and a new tag. There is another tag, 'bar', that exists in a
1388 // different context ($context2) that should be ignored.
1389 core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context1, ['foo', 'bop']);
1391 $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1392 $actualtagnames = array_map(function($record) {
1393 return $record->name;
1394 }, $result);
1396 sort($tagnames);
1397 sort($actualtagnames);
1398 // The list of tags should match the $tagnames.
1399 $this->assertEquals($tagnames, $actualtagnames);
1401 foreach ($result as $tag) {
1402 if ($tag->name == 'bar') {
1403 // The tag instance for 'bar' should have been left untouched
1404 // because it was in a different context.
1405 $this->assertEquals($context2->id, $tag->taginstancecontextid);
1406 } else {
1407 $this->assertEquals($context1->id, $tag->taginstancecontextid);
1413 * set_item_tags should delete all of the tag instances that don't match
1414 * the new set of tags only in the same context if the tag area allows
1415 * multiple contexts.
1417 public function test_set_item_tags_allow_multiple_context_deletes_instances_in_same_context() {
1418 $tagnames = ['foo', 'bar', 'baz', 'bop'];
1419 $collid = core_tag_collection::get_default();
1420 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1421 $user1 = $this->getDataGenerator()->create_user();
1422 $user2 = $this->getDataGenerator()->create_user();
1423 $context1 = \context_user::instance($user1->id);
1424 $context2 = \context_user::instance($user2->id);
1425 $component = 'core';
1426 $itemtype = 'user';
1427 $itemid = 1;
1428 $expectedtagnames = ['foo', 'bar', 'bop'];
1429 $tagareas = core_tag_area::get_areas();
1430 $tagarea = $tagareas[$itemtype][$component];
1432 // Make sure the tag area allows multiple contexts.
1433 core_tag_area::update($tagarea, ['multiplecontexts' => true]);
1435 // Create tag instances in separate contexts.
1436 $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1437 $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context1);
1438 $this->add_tag_instance($tags['baz'], $component, $itemtype, $itemid, $context1);
1439 $this->add_tag_instance($tags['bop'], $component, $itemtype, $itemid, $context2);
1441 core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context1, ['foo', 'bar']);
1443 $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1444 $actualtagnames = array_map(function($record) {
1445 return $record->name;
1446 }, $result);
1448 sort($expectedtagnames);
1449 sort($actualtagnames);
1451 // The list of tags should match the $expectedtagnames, which includes the
1452 // tag 'bop' because it was in a different context to the one being set
1453 // even though it wasn't in the new set of tags.
1454 $this->assertEquals($expectedtagnames, $actualtagnames);
1458 * set_item_tags should allow multiple instances of the same tag in different
1459 * contexts if the tag area allows multiple contexts.
1461 public function test_set_item_tags_allow_multiple_context_same_tag_multiple_contexts() {
1462 $tagnames = ['foo'];
1463 $collid = core_tag_collection::get_default();
1464 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1465 $user1 = $this->getDataGenerator()->create_user();
1466 $user2 = $this->getDataGenerator()->create_user();
1467 $context1 = \context_user::instance($user1->id);
1468 $context2 = \context_user::instance($user2->id);
1469 $component = 'core';
1470 $itemtype = 'user';
1471 $itemid = 1;
1472 $expectedtagnames = ['foo', 'bar', 'bop'];
1473 $tagareas = core_tag_area::get_areas();
1474 $tagarea = $tagareas[$itemtype][$component];
1476 // Make sure the tag area allows multiple contexts.
1477 core_tag_area::update($tagarea, ['multiplecontexts' => true]);
1479 // Create first instance of 'foo' in $context1.
1480 $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1482 core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context2, ['foo']);
1484 $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1485 $tagsbycontext = array_reduce($result, function($carry, $tag) {
1486 $contextid = $tag->taginstancecontextid;
1487 if (isset($carry[$contextid])) {
1488 $carry[$contextid][] = $tag;
1489 } else {
1490 $carry[$contextid] = [$tag];
1492 return $carry;
1493 }, []);
1495 // The result should be two tag instances of 'foo' in each of the
1496 // two contexts, $context1 and $context2.
1497 $this->assertCount(1, $tagsbycontext[$context1->id]);
1498 $this->assertCount(1, $tagsbycontext[$context2->id]);
1499 $this->assertEquals('foo', $tagsbycontext[$context1->id][0]->name);
1500 $this->assertEquals('foo', $tagsbycontext[$context2->id][0]->name);
1504 * delete_instances_as_record with an empty set of instances should do nothing.
1506 public function test_delete_instances_as_record_empty_set() {
1507 $user = $this->getDataGenerator()->create_user();
1508 $context = \context_user::instance($user->id);
1509 $component = 'core';
1510 $itemtype = 'user';
1511 $itemid = 1;
1513 core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context, ['foo']);
1514 // This shouldn't error.
1515 core_tag_tag::delete_instances_as_record([]);
1517 $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1518 // We should still have one tag.
1519 $this->assertCount(1, $tags);
1523 * delete_instances_as_record with an instance that doesn't exist should do
1524 * nothing.
1526 public function test_delete_instances_as_record_missing_set() {
1527 $tagnames = ['foo'];
1528 $collid = core_tag_collection::get_default();
1529 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1530 $user = $this->getDataGenerator()->create_user();
1531 $context = \context_user::instance($user->id);
1532 $component = 'core';
1533 $itemtype = 'user';
1534 $itemid = 1;
1536 $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1537 $taginstance->id++;
1539 // Delete an instance that doesn't exist should do nothing.
1540 core_tag_tag::delete_instances_as_record([$taginstance]);
1542 $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1543 // We should still have one tag.
1544 $this->assertCount(1, $tags);
1548 * delete_instances_as_record with a list of all tag instances should
1549 * leave no tags left.
1551 public function test_delete_instances_as_record_whole_set() {
1552 $tagnames = ['foo'];
1553 $collid = core_tag_collection::get_default();
1554 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1555 $user = $this->getDataGenerator()->create_user();
1556 $context = \context_user::instance($user->id);
1557 $component = 'core';
1558 $itemtype = 'user';
1559 $itemid = 1;
1561 $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1563 core_tag_tag::delete_instances_as_record([$taginstance]);
1565 $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1566 // There should be no tags left.
1567 $this->assertEmpty($tags);
1571 * delete_instances_as_record with a list of only some tag instances should
1572 * delete only the given tag instances and leave other tag instances.
1574 public function test_delete_instances_as_record_partial_set() {
1575 $tagnames = ['foo', 'bar'];
1576 $collid = core_tag_collection::get_default();
1577 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1578 $user = $this->getDataGenerator()->create_user();
1579 $context = \context_user::instance($user->id);
1580 $component = 'core';
1581 $itemtype = 'user';
1582 $itemid = 1;
1584 $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1585 $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context);
1587 core_tag_tag::delete_instances_as_record([$taginstance]);
1589 $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1590 // We should be left with a single tag, 'bar'.
1591 $this->assertCount(1, $tags);
1592 $tag = array_shift($tags);
1593 $this->assertEquals('bar', $tag->name);
1597 * delete_instances_by_id with an empty set of ids should do nothing.
1599 public function test_delete_instances_by_id_empty_set() {
1600 $user = $this->getDataGenerator()->create_user();
1601 $context = \context_user::instance($user->id);
1602 $component = 'core';
1603 $itemtype = 'user';
1604 $itemid = 1;
1606 core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context, ['foo']);
1607 // This shouldn't error.
1608 core_tag_tag::delete_instances_by_id([]);
1610 $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1611 // We should still have one tag.
1612 $this->assertCount(1, $tags);
1616 * delete_instances_by_id with an id that doesn't exist should do
1617 * nothing.
1619 public function test_delete_instances_by_id_missing_set() {
1620 $tagnames = ['foo'];
1621 $collid = core_tag_collection::get_default();
1622 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1623 $user = $this->getDataGenerator()->create_user();
1624 $context = \context_user::instance($user->id);
1625 $component = 'core';
1626 $itemtype = 'user';
1627 $itemid = 1;
1629 $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1631 // Delete an instance that doesn't exist should do nothing.
1632 core_tag_tag::delete_instances_by_id([$taginstance->id + 1]);
1634 $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1635 // We should still have one tag.
1636 $this->assertCount(1, $tags);
1640 * delete_instances_by_id with a list of all tag instance ids should
1641 * leave no tags left.
1643 public function test_delete_instances_by_id_whole_set() {
1644 $tagnames = ['foo'];
1645 $collid = core_tag_collection::get_default();
1646 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1647 $user = $this->getDataGenerator()->create_user();
1648 $context = \context_user::instance($user->id);
1649 $component = 'core';
1650 $itemtype = 'user';
1651 $itemid = 1;
1653 $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1655 core_tag_tag::delete_instances_by_id([$taginstance->id]);
1657 $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1658 // There should be no tags left.
1659 $this->assertEmpty($tags);
1663 * delete_instances_by_id with a list of only some tag instance ids should
1664 * delete only the given tag instance ids and leave other tag instances.
1666 public function test_delete_instances_by_id_partial_set() {
1667 $tagnames = ['foo', 'bar'];
1668 $collid = core_tag_collection::get_default();
1669 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1670 $user = $this->getDataGenerator()->create_user();
1671 $context = \context_user::instance($user->id);
1672 $component = 'core';
1673 $itemtype = 'user';
1674 $itemid = 1;
1676 $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1677 $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context);
1679 core_tag_tag::delete_instances_by_id([$taginstance->id]);
1681 $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1682 // We should be left with a single tag, 'bar'.
1683 $this->assertCount(1, $tags);
1684 $tag = array_shift($tags);
1685 $this->assertEquals('bar', $tag->name);
1689 * delete_instances should delete all tag instances for a component if given
1690 * only the component as a parameter.
1692 public function test_delete_instances_with_component() {
1693 global $DB;
1695 $tagnames = ['foo', 'bar'];
1696 $collid = core_tag_collection::get_default();
1697 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1698 $user = $this->getDataGenerator()->create_user();
1699 $context = \context_user::instance($user->id);
1700 $component = 'core';
1701 $itemtype1 = 'user';
1702 $itemtype2 = 'course';
1703 $itemid = 1;
1705 // Add 2 tag instances in the same $component but with different item types.
1706 $this->add_tag_instance($tags['foo'], $component, $itemtype1, $itemid, $context);
1707 $this->add_tag_instance($tags['bar'], $component, $itemtype2, $itemid, $context);
1709 // Delete all tag instances for the component.
1710 core_tag_tag::delete_instances($component);
1712 $taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance} WHERE component = ?', [$component]);
1713 // Both tag instances from the $component should have been deleted even though
1714 // they are in different item types.
1715 $this->assertEmpty($taginstances);
1719 * delete_instances should delete all tag instances for a component if given
1720 * only the component as a parameter.
1722 public function test_delete_instances_with_component_and_itemtype() {
1723 global $DB;
1725 $tagnames = ['foo', 'bar'];
1726 $collid = core_tag_collection::get_default();
1727 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1728 $user = $this->getDataGenerator()->create_user();
1729 $context = \context_user::instance($user->id);
1730 $component = 'core';
1731 $itemtype1 = 'user';
1732 $itemtype2 = 'course';
1733 $itemid = 1;
1735 // Add 2 tag instances in the same $component but with different item types.
1736 $this->add_tag_instance($tags['foo'], $component, $itemtype1, $itemid, $context);
1737 $this->add_tag_instance($tags['bar'], $component, $itemtype2, $itemid, $context);
1739 // Delete all tag instances for the component and itemtype.
1740 core_tag_tag::delete_instances($component, $itemtype1);
1742 $taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance} WHERE component = ?', [$component]);
1743 // Only the tag instances for $itemtype1 should have been deleted. We
1744 // should still be left with the instance for 'bar'.
1745 $this->assertCount(1, $taginstances);
1746 $taginstance = array_shift($taginstances);
1747 $this->assertEquals($itemtype2, $taginstance->itemtype);
1748 $this->assertEquals($tags['bar']->id, $taginstance->tagid);
1752 * delete_instances should delete all tag instances for a component in a context
1753 * if given both the component and context id as parameters.
1755 public function test_delete_instances_with_component_and_context() {
1756 global $DB;
1758 $tagnames = ['foo', 'bar', 'baz'];
1759 $collid = core_tag_collection::get_default();
1760 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1761 $user1 = $this->getDataGenerator()->create_user();
1762 $user2 = $this->getDataGenerator()->create_user();
1763 $context1 = \context_user::instance($user1->id);
1764 $context2 = \context_user::instance($user2->id);
1765 $component = 'core';
1766 $itemtype1 = 'user';
1767 $itemtype2 = 'course';
1768 $itemid = 1;
1770 // Add 3 tag instances in the same $component but with different contexts.
1771 $this->add_tag_instance($tags['foo'], $component, $itemtype1, $itemid, $context1);
1772 $this->add_tag_instance($tags['bar'], $component, $itemtype2, $itemid, $context1);
1773 $this->add_tag_instance($tags['baz'], $component, $itemtype2, $itemid, $context2);
1775 // Delete all tag instances for the component and context.
1776 core_tag_tag::delete_instances($component, null, $context1->id);
1778 $taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance} WHERE component = ?', [$component]);
1779 // Only the tag instances for $context1 should have been deleted. We
1780 // should still be left with the instance for 'baz'.
1781 $this->assertCount(1, $taginstances);
1782 $taginstance = array_shift($taginstances);
1783 $this->assertEquals($context2->id, $taginstance->contextid);
1784 $this->assertEquals($tags['baz']->id, $taginstance->tagid);
1788 * delete_instances should delete all tag instances for a component, item type
1789 * and context if given the component, itemtype, and context id as parameters.
1791 public function test_delete_instances_with_component_and_itemtype_and_context() {
1792 global $DB;
1794 $tagnames = ['foo', 'bar', 'baz'];
1795 $collid = core_tag_collection::get_default();
1796 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1797 $user1 = $this->getDataGenerator()->create_user();
1798 $user2 = $this->getDataGenerator()->create_user();
1799 $context1 = \context_user::instance($user1->id);
1800 $context2 = \context_user::instance($user2->id);
1801 $component = 'core';
1802 $itemtype1 = 'user';
1803 $itemtype2 = 'course';
1804 $itemid = 1;
1806 // Add 3 tag instances in the same $component but with different contexts.
1807 $this->add_tag_instance($tags['foo'], $component, $itemtype1, $itemid, $context1);
1808 $this->add_tag_instance($tags['bar'], $component, $itemtype2, $itemid, $context1);
1809 $this->add_tag_instance($tags['baz'], $component, $itemtype2, $itemid, $context2);
1811 // Delete all tag instances for the component and context.
1812 core_tag_tag::delete_instances($component, $itemtype2, $context1->id);
1814 $taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance} WHERE component = ?', [$component]);
1815 // Only the tag instances for $itemtype2 in $context1 should have been
1816 // deleted. We should still be left with the instance for 'foo' and 'baz'.
1817 $this->assertCount(2, $taginstances);
1818 $fooinstances = array_filter($taginstances, function($instance) use ($tags) {
1819 return $instance->tagid == $tags['foo']->id;
1821 $fooinstance = array_shift($fooinstances);
1822 $bazinstances = array_filter($taginstances, function($instance) use ($tags) {
1823 return $instance->tagid == $tags['baz']->id;
1825 $bazinstance = array_shift($bazinstances);
1826 $this->assertNotEmpty($fooinstance);
1827 $this->assertNotEmpty($bazinstance);
1828 $this->assertEquals($context1->id, $fooinstance->contextid);
1829 $this->assertEquals($context2->id, $bazinstance->contextid);
1833 * change_instances_context should not change any existing instance contexts
1834 * if not given any instance ids.
1836 public function test_change_instances_context_empty_set() {
1837 global $DB;
1839 $tagnames = ['foo'];
1840 $collid = core_tag_collection::get_default();
1841 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1842 $user1 = $this->getDataGenerator()->create_user();
1843 $user2 = $this->getDataGenerator()->create_user();
1844 $context1 = \context_user::instance($user1->id);
1845 $context2 = \context_user::instance($user2->id);
1846 $component = 'core';
1847 $itemtype = 'user';
1848 $itemid = 1;
1850 $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1852 core_tag_tag::change_instances_context([], $context2);
1854 $taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance}');
1855 // The existing tag instance should not have changed.
1856 $this->assertCount(1, $taginstances);
1857 $taginstance = array_shift($taginstances);
1858 $this->assertEquals($context1->id, $taginstance->contextid);
1862 * change_instances_context should only change the context of the given ids.
1864 public function test_change_instances_context_partial_set() {
1865 global $DB;
1867 $tagnames = ['foo', 'bar'];
1868 $collid = core_tag_collection::get_default();
1869 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1870 $user1 = $this->getDataGenerator()->create_user();
1871 $user2 = $this->getDataGenerator()->create_user();
1872 $context1 = \context_user::instance($user1->id);
1873 $context2 = \context_user::instance($user2->id);
1874 $component = 'core';
1875 $itemtype = 'user';
1876 $itemid = 1;
1878 $fooinstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1879 $barinstance = $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context1);
1881 core_tag_tag::change_instances_context([$fooinstance->id], $context2);
1883 // Reload the record.
1884 $fooinstance = $DB->get_record('tag_instance', ['id' => $fooinstance->id]);
1885 $barinstance = $DB->get_record('tag_instance', ['id' => $barinstance->id]);
1886 // Tag 'foo' context should be updated.
1887 $this->assertEquals($context2->id, $fooinstance->contextid);
1888 // Tag 'bar' context should not be changed.
1889 $this->assertEquals($context1->id, $barinstance->contextid);
1893 * change_instances_context should change multiple items from multiple contexts.
1895 public function test_change_instances_context_multiple_contexts() {
1896 global $DB;
1898 $tagnames = ['foo', 'bar'];
1899 $collid = core_tag_collection::get_default();
1900 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1901 $user1 = $this->getDataGenerator()->create_user();
1902 $user2 = $this->getDataGenerator()->create_user();
1903 $user3 = $this->getDataGenerator()->create_user();
1904 $context1 = \context_user::instance($user1->id);
1905 $context2 = \context_user::instance($user2->id);
1906 $context3 = \context_user::instance($user3->id);
1907 $component = 'core';
1908 $itemtype = 'user';
1909 $itemid = 1;
1911 // Two instances in different contexts.
1912 $fooinstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1913 $barinstance = $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context2);
1915 core_tag_tag::change_instances_context([$fooinstance->id, $barinstance->id], $context3);
1917 // Reload the record.
1918 $fooinstance = $DB->get_record('tag_instance', ['id' => $fooinstance->id]);
1919 $barinstance = $DB->get_record('tag_instance', ['id' => $barinstance->id]);
1920 // Tag 'foo' context should be updated.
1921 $this->assertEquals($context3->id, $fooinstance->contextid);
1922 // Tag 'bar' context should be updated.
1923 $this->assertEquals($context3->id, $barinstance->contextid);
1924 // There shouldn't be any tag instances left in $context1.
1925 $context1records = $DB->get_records('tag_instance', ['contextid' => $context1->id]);
1926 $this->assertEmpty($context1records);
1927 // There shouldn't be any tag instances left in $context2.
1928 $context2records = $DB->get_records('tag_instance', ['contextid' => $context2->id]);
1929 $this->assertEmpty($context2records);
1933 * change_instances_context moving an instance from one context into a context
1934 * that already has an instance of that tag should throw an exception.
1936 public function test_change_instances_context_conflicting_instances() {
1937 global $DB;
1939 $tagnames = ['foo'];
1940 $collid = core_tag_collection::get_default();
1941 $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1942 $user1 = $this->getDataGenerator()->create_user();
1943 $user2 = $this->getDataGenerator()->create_user();
1944 $context1 = \context_user::instance($user1->id);
1945 $context2 = \context_user::instance($user2->id);
1946 $component = 'core';
1947 $itemtype = 'user';
1948 $itemid = 1;
1950 // Two instances of 'foo' in different contexts.
1951 $fooinstance1 = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1952 $fooinstance2 = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context2);
1954 // There is already an instance of 'foo' in $context2 so the code
1955 // should throw an exception when we try to move another instance there.
1956 $this->expectException('Exception');
1957 core_tag_tag::change_instances_context([$fooinstance1->id], $context2);
1961 * Help method to return sorted array of names of correlated tags to use for assertions
1962 * @param core_tag $tag
1963 * @return string
1965 protected function get_correlated_tags_names($tag) {
1966 $rv = array_map(function($t) {
1967 return $t->rawname;
1968 }, $tag->get_correlated_tags());
1969 sort($rv);
1970 return array_values($rv);
1974 * Add a tag instance.
1976 * @param core_tag_tag $tag
1977 * @param string $component
1978 * @param string $itemtype
1979 * @param int $itemid
1980 * @param context $context
1981 * @return stdClass
1983 protected function add_tag_instance(core_tag_tag $tag, $component, $itemtype, $itemid, $context) {
1984 global $DB;
1985 $record = (array) $tag->to_object();
1986 $record['tagid'] = $record['id'];
1987 $record['component'] = $component;
1988 $record['itemtype'] = $itemtype;
1989 $record['itemid'] = $itemid;
1990 $record['contextid'] = $context->id;
1991 $record['tiuserid'] = 0;
1992 $record['ordering'] = 0;
1993 $record['timecreated'] = time();
1994 $record['id'] = $DB->insert_record('tag_instance', $record);
1995 return (object) $record;