MDL-51883 libraries: 3.1 final deprecation in lib/deprecatedlib.php
[moodle.git] / tag / tests / taglib_test.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Tag related unit tests.
19  *
20  * @package core_tag
21  * @category test
22  * @copyright 2014 Mark Nelson <markn@moodle.com>
23  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 global $CFG;
30 class core_tag_taglib_testcase extends advanced_testcase {
32     /**
33      * Test set up.
34      *
35      * This is executed before running any test in this file.
36      */
37     public function setUp() {
38         $this->resetAfterTest();
39     }
41     /**
42      * Test that the tag_set function throws an exception.
43      * This function was deprecated in 3.1
44      */
45     public function test_tag_set_get() {
46         $this->expectException('coding_exception');
47         $this->expectExceptionMessage('tag_set() can not be used anymore. Please use ' .
48             'core_tag_tag::set_item_tags().');
49         tag_set();
50     }
52     /**
53      * Test that tag_set_add function throws an exception.
54      * This function was deprecated in 3.1
55      */
56     public function test_tag_set_add() {
57         $this->expectException('coding_exception');
58         $this->expectExceptionMessage('tag_set_add() can not be used anymore. Please use ' .
59             'core_tag_tag::add_item_tag().');
60         tag_set_add();
61     }
63     /**
64      * Test that tag_set_delete function returns an exception.
65      * This function was deprecated in 3.1
66      */
67     public function test_tag_set_delete() {
68         $this->expectException('coding_exception');
69         $this->expectExceptionMessage('tag_set_delete() can not be used anymore. Please use ' .
70             'core_tag_tag::remove_item_tag().');
71         tag_set_delete();
72     }
74     /**
75      * Test the core_tag_tag::add_item_tag() and core_tag_tag::remove_item_tag() functions.
76      */
77     public function test_add_remove_item_tag() {
78         global $DB;
80         // Create a course to tag.
81         $course = $this->getDataGenerator()->create_course();
83         // Create the tag and tag instance we are going to delete.
84         core_tag_tag::add_item_tag('core', 'course', $course->id, context_course::instance($course->id), 'A random tag');
86         $this->assertEquals(1, $DB->count_records('tag'));
87         $this->assertEquals(1, $DB->count_records('tag_instance'));
89         // Call the tag_set_delete function.
90         core_tag_tag::remove_item_tag('core', 'course', $course->id, 'A random tag');
92         // Now check that there are no tags or tag instances.
93         $this->assertEquals(0, $DB->count_records('tag'));
94         $this->assertEquals(0, $DB->count_records('tag_instance'));
95     }
97     /**
98      * Test that tag_assign function throws an exception.
99      * This function was deprecated in 3.1
100      */
101     public function test_tag_assign() {
102         $this->expectException('coding_exception');
103         $this->expectExceptionMessage('tag_assign() can not be used anymore. Please use core_tag_tag::set_item_tags() ' .
104             'or core_tag_tag::add_item_tag() instead.');
105         tag_assign();
106     }
108     /**
109      * Test the tag cleanup function used by the cron.
110      */
111     public function test_tag_cleanup() {
112         global $DB;
114         $task = new \core\task\tag_cron_task();
116         // Create some users.
117         $users = array();
118         for ($i = 0; $i < 10; $i++) {
119             $users[] = $this->getDataGenerator()->create_user();
120         }
122         // Create a course to tag.
123         $course = $this->getDataGenerator()->create_course();
124         $context = context_course::instance($course->id);
126         // Test clean up instances with tags that no longer exist.
127         $tags = array();
128         $tagnames = array();
129         for ($i = 0; $i < 10; $i++) {
130             $tags[] = $tag = $this->getDataGenerator()->create_tag(array('userid' => $users[0]->id));
131             $tagnames[] = $tag->rawname;
132         }
133         // Create instances with the tags.
134         core_tag_tag::set_item_tags('core', 'course', $course->id, $context, $tagnames);
135         // We should now have ten tag instances.
136         $coursetaginstances = $DB->count_records('tag_instance', array('itemtype' => 'course'));
137         $this->assertEquals(10, $coursetaginstances);
139         // Delete four tags
140         // Manual delete of tags is done as the function will remove the instances as well.
141         $DB->delete_records('tag', array('id' => $tags[6]->id));
142         $DB->delete_records('tag', array('id' => $tags[7]->id));
143         $DB->delete_records('tag', array('id' => $tags[8]->id));
144         $DB->delete_records('tag', array('id' => $tags[9]->id));
146         // Clean up the tags.
147         $task->cleanup();
148         // Check that we now only have six tag_instance records left.
149         $coursetaginstances = $DB->count_records('tag_instance', array('itemtype' => 'course'));
150         $this->assertEquals(6, $coursetaginstances);
152         // Test clean up with users that have been deleted.
153         // Create a tag for this course.
154         foreach ($users as $user) {
155             $context = context_user::instance($user->id);
156             core_tag_tag::set_item_tags('core', 'user', $user->id, $context, array($tags[0]->rawname));
157         }
158         $usertags = $DB->count_records('tag_instance', array('itemtype' => 'user'));
159         $this->assertCount($usertags, $users);
160         // Remove three students.
161         // Using the proper function to delete the user will also remove the tags.
162         $DB->update_record('user', array('id' => $users[4]->id, 'deleted' => 1));
163         $DB->update_record('user', array('id' => $users[5]->id, 'deleted' => 1));
164         $DB->update_record('user', array('id' => $users[6]->id, 'deleted' => 1));
166         // Clean up the tags.
167         $task->cleanup();
168         $usertags = $DB->count_records('tag_instance', array('itemtype' => 'user'));
169         $usercount = $DB->count_records('user', array('deleted' => 0));
170         // Remove admin and guest from the count.
171         $this->assertEquals($usertags, ($usercount - 2));
173         // Test clean up where a course has been removed.
174         // Delete the course. This also needs to be this way otherwise the tags are removed by using the proper function.
175         $DB->delete_records('course', array('id' => $course->id));
176         $task->cleanup();
177         $coursetags = $DB->count_records('tag_instance', array('itemtype' => 'course'));
178         $this->assertEquals(0, $coursetags);
180         // Test clean up where a post has been removed.
181         // Create default post.
182         $post = new stdClass();
183         $post->userid = $users[1]->id;
184         $post->content = 'test post content text';
185         $post->id = $DB->insert_record('post', $post);
186         $context = context_system::instance();
187         core_tag_tag::set_item_tags('core', 'post', $post->id, $context, array($tags[0]->rawname));
189         // Add another one with a fake post id to be removed.
190         core_tag_tag::set_item_tags('core', 'post', 15, $context, array($tags[0]->rawname));
191         // Check that there are two tag instances.
192         $posttags = $DB->count_records('tag_instance', array('itemtype' => 'post'));
193         $this->assertEquals(2, $posttags);
194         // Clean up the tags.
195         $task->cleanup();
196         // We should only have one entry left now.
197         $posttags = $DB->count_records('tag_instance', array('itemtype' => 'post'));
198         $this->assertEquals(1, $posttags);
199     }
201     /**
202      * Test deleting a group of tag instances.
203      */
204     public function test_tag_bulk_delete_instances() {
205         global $DB;
206         $task = new \core\task\tag_cron_task();
208         // Setup.
209         $user = $this->getDataGenerator()->create_user();
210         $course = $this->getDataGenerator()->create_course();
211         $context = context_course::instance($course->id);
213         // Create some tag instances.
214         for ($i = 0; $i < 10; $i++) {
215             $tag = $this->getDataGenerator()->create_tag(array('userid' => $user->id));
216             core_tag_tag::add_item_tag('core', 'course', $course->id, $context, $tag->rawname);
217         }
218         // Get tag instances. tag name and rawname are required for the event fired in this function.
219         $sql = "SELECT ti.*, t.name, t.rawname
220                   FROM {tag_instance} ti
221                   JOIN {tag} t ON t.id = ti.tagid";
222         $taginstances = $DB->get_records_sql($sql);
223         $this->assertCount(10, $taginstances);
224         // Run the function.
225         $task->bulk_delete_instances($taginstances);
226         // Make sure they are gone.
227         $instancecount = $DB->count_records('tag_instance');
228         $this->assertEquals(0, $instancecount);
229     }
231     /**
232      * Prepares environment for testing tag correlations
233      * @return core_tag_tag[] list of used tags
234      */
235     protected function prepare_correlated() {
236         global $DB;
238         $user = $this->getDataGenerator()->create_user();
239         $this->setUser($user);
241         $user1 = $this->getDataGenerator()->create_user();
242         $user2 = $this->getDataGenerator()->create_user();
243         $user3 = $this->getDataGenerator()->create_user();
244         $user4 = $this->getDataGenerator()->create_user();
245         $user5 = $this->getDataGenerator()->create_user();
246         $user6 = $this->getDataGenerator()->create_user();
248         // Several records have both 'cat' and 'cats' tags attached to them.
249         // This will make those tags automatically correlated.
250         // Same with 'dog', 'dogs' and 'puppy.
251         core_tag_tag::set_item_tags('core', 'user', $user1->id, context_user::instance($user1->id), array('cat', 'cats'));
252         core_tag_tag::set_item_tags('core', 'user', $user2->id, context_user::instance($user2->id), array('cat', 'cats', 'kitten'));
253         core_tag_tag::set_item_tags('core', 'user', $user3->id, context_user::instance($user3->id), array('cat', 'cats'));
254         core_tag_tag::set_item_tags('core', 'user', $user4->id, context_user::instance($user4->id), array('dog', 'dogs', 'puppy'));
255         core_tag_tag::set_item_tags('core', 'user', $user5->id, context_user::instance($user5->id), array('dog', 'dogs', 'puppy'));
256         core_tag_tag::set_item_tags('core', 'user', $user6->id, context_user::instance($user6->id), array('dog', 'dogs', 'puppy'));
258         $tags = core_tag_tag::get_by_name_bulk(core_tag_collection::get_default(),
259             array('cat', 'cats', 'dog', 'dogs', 'kitten', 'puppy'), '*');
261         // Add manual relation between tags 'cat' and 'kitten'.
262         core_tag_tag::get($tags['cat']->id)->set_related_tags(array('kitten'));
264         return $tags;
265     }
267     /**
268      * Test for function compute_correlations() that is part of tag cron
269      */
270     public function test_correlations() {
271         global $DB;
272         $task = new \core\task\tag_cron_task();
274         $tags = array_map(function ($t) {
275             return $t->id;
276         }, $this->prepare_correlated());
278         $task->compute_correlations();
280         $this->assertEquals($tags['cats'],
281             $DB->get_field_select('tag_correlation', 'correlatedtags',
282                 'tagid = ?', array($tags['cat'])));
283         $this->assertEquals($tags['cat'],
284             $DB->get_field_select('tag_correlation', 'correlatedtags',
285                 'tagid = ?', array($tags['cats'])));
286         $this->assertEquals($tags['dogs'] . ',' . $tags['puppy'],
287             $DB->get_field_select('tag_correlation', 'correlatedtags',
288                 'tagid = ?', array($tags['dog'])));
289         $this->assertEquals($tags['dog'] . ',' . $tags['puppy'],
290             $DB->get_field_select('tag_correlation', 'correlatedtags',
291                 'tagid = ?', array($tags['dogs'])));
292         $this->assertEquals($tags['dog'] . ',' . $tags['dogs'],
293             $DB->get_field_select('tag_correlation', 'correlatedtags',
294                 'tagid = ?', array($tags['puppy'])));
296         // Make sure get_correlated_tags() returns 'cats' as the only correlated tag to the 'cat'.
297         $correlatedtags = array_values(core_tag_tag::get($tags['cat'])->get_correlated_tags(true));
298         $this->assertCount(3, $correlatedtags); // This will return all existing instances but they all point to the same tag.
299         $this->assertEquals('cats', $correlatedtags[0]->rawname);
300         $this->assertEquals('cats', $correlatedtags[1]->rawname);
301         $this->assertEquals('cats', $correlatedtags[2]->rawname);
303         $correlatedtags = array_values(core_tag_tag::get($tags['cat'])->get_correlated_tags());
304         $this->assertCount(1, $correlatedtags); // Duplicates are filtered out here.
305         $this->assertEquals('cats', $correlatedtags[0]->rawname);
307         // Make sure get_correlated_tags() returns 'dogs' and 'puppy' as the correlated tags to the 'dog'.
308         $correlatedtags = core_tag_tag::get($tags['dog'])->get_correlated_tags(true);
309         $this->assertCount(6, $correlatedtags); // 2 tags times 3 instances.
311         $correlatedtags = array_values(core_tag_tag::get($tags['dog'])->get_correlated_tags());
312         $this->assertCount(2, $correlatedtags);
313         $this->assertEquals('dogs', $correlatedtags[0]->rawname);
314         $this->assertEquals('puppy', $correlatedtags[1]->rawname);
316         // Function get_related_tags() will return both related and correlated tags.
317         $relatedtags = array_values(core_tag_tag::get($tags['cat'])->get_related_tags());
318         $this->assertCount(2, $relatedtags);
319         $this->assertEquals('kitten', $relatedtags[0]->rawname);
320         $this->assertEquals('cats', $relatedtags[1]->rawname);
322         // Also test get_correlated_tags().
323         $correlatedtags = array_values(core_tag_tag::get($tags['cat'])->get_correlated_tags(true));
324         $this->assertCount(3, $correlatedtags); // This will return all existing instances but they all point to the same tag.
325         $this->assertEquals('cats', $correlatedtags[0]->rawname);
326         $this->assertEquals('cats', $correlatedtags[1]->rawname);
327         $this->assertEquals('cats', $correlatedtags[2]->rawname);
329         $correlatedtags = array_values(core_tag_tag::get($tags['cat'])->get_correlated_tags());
330         $this->assertCount(1, $correlatedtags); // Duplicates are filtered out here.
331         $this->assertEquals('cats', $correlatedtags[0]->rawname);
333         $correlatedtags = array_values(core_tag_tag::get($tags['dog'])->get_correlated_tags(true));
334         $this->assertCount(6, $correlatedtags); // 2 tags times 3 instances.
336         $correlatedtags = array_values(core_tag_tag::get($tags['dog'])->get_correlated_tags());
337         $this->assertCount(2, $correlatedtags);
338         $this->assertEquals('dogs', $correlatedtags[0]->rawname);
339         $this->assertEquals('puppy', $correlatedtags[1]->rawname);
341         $relatedtags = array_values(core_tag_tag::get($tags['cat'])->get_related_tags());
342         $this->assertCount(2, $relatedtags);
343         $this->assertEquals('kitten', $relatedtags[0]->rawname);
344         $this->assertEquals('cats', $relatedtags[1]->rawname);
345         // End of testing deprecated methods.
347         // If we then manually set 'cat' and 'cats' as related, get_related_tags() will filter out duplicates.
348         core_tag_tag::get($tags['cat'])->set_related_tags(array('kitten', 'cats'));
350         $relatedtags = array_values(core_tag_tag::get($tags['cat'])->get_related_tags());
351         $this->assertCount(2, $relatedtags);
352         $this->assertEquals('kitten', $relatedtags[0]->rawname);
353         $this->assertEquals('cats', $relatedtags[1]->rawname);
355         // Make sure core_tag_tag::get_item_tags(), core_tag_tag::get_correlated_tags() return the same set of fields.
356         $relatedtags = core_tag_tag::get_item_tags('core', 'tag', $tags['cat']);
357         $relatedtag = reset($relatedtags);
358         $correlatedtags = core_tag_tag::get($tags['cat'])->get_correlated_tags();
359         $correlatedtag = reset($correlatedtags);
360         $this->assertEquals(array_keys((array)$relatedtag->to_object()), array_keys((array)$correlatedtag->to_object()));
362         $relatedtags = core_tag_tag::get_item_tags(null, 'tag', $tags['cat']);
363         $relatedtag = reset($relatedtags);
364         $correlatedtags = core_tag_tag::get($tags['cat'])->get_correlated_tags();
365         $correlatedtag = reset($correlatedtags);
366         $this->assertEquals(array_keys((array)$relatedtag), array_keys((array)$correlatedtag));
367     }
369     /**
370      * Test for function cleanup() that is part of tag cron
371      */
372     public function test_cleanup() {
373         global $DB;
374         $task = new \core\task\tag_cron_task();
376         $user = $this->getDataGenerator()->create_user();
377         $defaultcoll = core_tag_collection::get_default();
379         // Setting tags will create non-standard tags 'cat', 'dog' and 'fish'.
380         core_tag_tag::set_item_tags('core', 'user', $user->id, context_user::instance($user->id), array('cat', 'dog', 'fish'));
382         $this->assertTrue($DB->record_exists('tag', array('name' => 'cat')));
383         $this->assertTrue($DB->record_exists('tag', array('name' => 'dog')));
384         $this->assertTrue($DB->record_exists('tag', array('name' => 'fish')));
386         // Make tag 'dog' standard.
387         $dogtag = core_tag_tag::get_by_name($defaultcoll, 'dog', '*');
388         $fishtag = core_tag_tag::get_by_name($defaultcoll, 'fish');
389         $dogtag->update(array('isstandard' => 1));
391         // Manually remove the instances pointing on tags 'dog' and 'fish'.
392         $DB->execute('DELETE FROM {tag_instance} WHERE tagid in (?,?)', array($dogtag->id, $fishtag->id));
394         $task->cleanup();
396         // Tag 'cat' is still present because it's used. Tag 'dog' is present because it's standard.
397         // Tag 'fish' was removed because it is not standard and it is no longer used by anybody.
398         $this->assertTrue($DB->record_exists('tag', array('name' => 'cat')));
399         $this->assertTrue($DB->record_exists('tag', array('name' => 'dog')));
400         $this->assertFalse($DB->record_exists('tag', array('name' => 'fish')));
402         // Delete user without using API function.
403         $DB->update_record('user', array('id' => $user->id, 'deleted' => 1));
405         $task->cleanup();
407         // Tag 'cat' was now deleted too.
408         $this->assertFalse($DB->record_exists('tag', array('name' => 'cat')));
410         // Assign tag to non-existing record. Make sure tag was created in the DB.
411         core_tag_tag::set_item_tags('core', 'course', 1231231, context_system::instance(), array('bird'));
412         $this->assertTrue($DB->record_exists('tag', array('name' => 'bird')));
414         $task->cleanup();
416         // Tag 'bird' was now deleted because the related record does not exist in the DB.
417         $this->assertFalse($DB->record_exists('tag', array('name' => 'bird')));
419         // Now we have a tag instance pointing on 'sometag' tag.
420         $user = $this->getDataGenerator()->create_user();
421         core_tag_tag::set_item_tags('core', 'user', $user->id, context_user::instance($user->id), array('sometag'));
422         $sometag = core_tag_tag::get_by_name($defaultcoll, 'sometag');
424         $this->assertTrue($DB->record_exists('tag_instance', array('tagid' => $sometag->id)));
426         // Some hacker removes the tag without using API.
427         $DB->delete_records('tag', array('id' => $sometag->id));
429         $task->cleanup();
431         // The tag instances were also removed.
432         $this->assertFalse($DB->record_exists('tag_instance', array('tagid' => $sometag->id)));
433     }
435     public function test_guess_tag() {
436         global $DB;
437         $user = $this->getDataGenerator()->create_user();
438         $this->setUser($user);
439         $tag1 = $this->getDataGenerator()->create_tag(array('name' => 'Cat'));
440         $tc = core_tag_collection::create((object)array('name' => 'tagcoll'));
441         $tag2 = $this->getDataGenerator()->create_tag(array('name' => 'Cat', 'tagcollid' => $tc->id));
442         $this->assertEquals(2, count($DB->get_records('tag')));
443         $this->assertEquals(2, count(core_tag_tag::guess_by_name('Cat')));
444         $this->assertEquals(core_tag_collection::get_default(), core_tag_tag::get_by_name(0, 'Cat')->tagcollid);
445     }
447     public function test_instances() {
448         global $DB;
449         $user = $this->getDataGenerator()->create_user();
450         $this->setUser($user);
452         // Create a course to tag.
453         $course = $this->getDataGenerator()->create_course();
454         $context = context_course::instance($course->id);
456         $initialtagscount = $DB->count_records('tag');
458         core_tag_tag::set_item_tags('core', 'course', $course->id, $context, array('Tag 1', 'Tag 2'));
459         $tags = core_tag_tag::get_item_tags('core', 'course', $course->id);
460         $tagssimple = array_values($tags);
461         $this->assertEquals(2, count($tags));
462         $this->assertEquals('Tag 1', $tagssimple[0]->rawname);
463         $this->assertEquals('Tag 2', $tagssimple[1]->rawname);
464         $this->assertEquals($initialtagscount + 2, $DB->count_records('tag'));
466         core_tag_tag::set_item_tags('core', 'course', $course->id, $context, array('Tag 3', 'Tag 2', 'Tag 1'));
467         $tags = core_tag_tag::get_item_tags('core', 'course', $course->id);
468         $tagssimple = array_values($tags);
469         $this->assertEquals(3, count($tags));
470         $this->assertEquals('Tag 3', $tagssimple[0]->rawname);
471         $this->assertEquals('Tag 2', $tagssimple[1]->rawname);
472         $this->assertEquals('Tag 1', $tagssimple[2]->rawname);
473         $this->assertEquals($initialtagscount + 3, $DB->count_records('tag'));
475         core_tag_tag::set_item_tags('core', 'course', $course->id, $context, array('Tag 3'));
476         $tags = core_tag_tag::get_item_tags('core', 'course', $course->id);
477         $tagssimple = array_values($tags);
478         $this->assertEquals(1, count($tags));
479         $this->assertEquals('Tag 3', $tagssimple[0]->rawname);
481         // Make sure the unused tags were removed from tag table.
482         $this->assertEquals($initialtagscount + 1, $DB->count_records('tag'));
483     }
485     public function test_related_tags() {
486         global $DB;
487         $user = $this->getDataGenerator()->create_user();
488         $this->setUser($user);
489         $tagcollid = core_tag_collection::get_default();
490         $tag = $this->getDataGenerator()->create_tag(array('$tagcollid' => $tagcollid, 'rawname' => 'My tag'));
491         $tag = core_tag_tag::get($tag->id, '*');
493         $tag->set_related_tags(array('Synonym 1', 'Synonym 2'));
494         $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $tag->id));
495         $this->assertEquals(2, count($relatedtags));
496         $this->assertEquals('Synonym 1', $relatedtags[0]->rawname);
497         $this->assertEquals('Synonym 2', $relatedtags[1]->rawname);
499         $t1 = core_tag_tag::get_by_name($tagcollid, 'Synonym 1', '*');
500         $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $t1->id));
501         $this->assertEquals(1, count($relatedtags));
502         $this->assertEquals('My tag', $relatedtags[0]->rawname);
504         $t2 = core_tag_tag::get_by_name($tagcollid, 'Synonym 2', '*');
505         $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $t2->id));
506         $this->assertEquals(1, count($relatedtags));
507         $this->assertEquals('My tag', $relatedtags[0]->rawname);
509         $tag->set_related_tags(array('Synonym 3', 'Synonym 2', 'Synonym 1'));
510         $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $tag->id));
511         $this->assertEquals(3, count($relatedtags));
512         $this->assertEquals('Synonym 1', $relatedtags[0]->rawname);
513         $this->assertEquals('Synonym 2', $relatedtags[1]->rawname);
514         $this->assertEquals('Synonym 3', $relatedtags[2]->rawname);
516         $t3 = core_tag_tag::get_by_name($tagcollid, 'Synonym 3', '*');
517         $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $t3->id));
518         $this->assertEquals(1, count($relatedtags));
519         $this->assertEquals('My tag', $relatedtags[0]->rawname);
521         $tag->set_related_tags(array('Synonym 3', 'Synonym 2'));
522         $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $tag->id));
523         $this->assertEquals(2, count($relatedtags));
524         $this->assertEquals('Synonym 2', $relatedtags[0]->rawname);
525         $this->assertEquals('Synonym 3', $relatedtags[1]->rawname);
527         // Assert "Synonym 1" no longer links but is still present (will be removed by cron).
528         $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $t1->id));
529         $this->assertEquals(0, count($relatedtags));
530     }
532     /**
533      * Very basic test for create/move/update/delete actions, without any itemtype movements.
534      */
535     public function test_tag_coll_basic() {
536         global $DB;
538         // Make sure there is one and only one tag coll that is marked as default.
539         $tagcolls = core_tag_collection::get_collections();
540         $this->assertEquals(1, count($DB->get_records('tag_coll', array('isdefault' => 1))));
541         $defaulttagcoll = core_tag_collection::get_default();
543         // Create a new tag coll to store user tags and something else.
544         $data = (object)array('name' => 'new tag coll');
545         $tagcollid1 = core_tag_collection::create($data)->id;
546         $tagcolls = core_tag_collection::get_collections();
547         $this->assertEquals('new tag coll', $tagcolls[$tagcollid1]->name);
549         // Create a new tag coll to store post tags.
550         $data = (object)array('name' => 'posts');
551         $tagcollid2 = core_tag_collection::create($data)->id;
552         $tagcolls = core_tag_collection::get_collections();
553         $this->assertEquals('posts', $tagcolls[$tagcollid2]->name);
554         $this->assertEquals($tagcolls[$tagcollid1]->sortorder + 1,
555             $tagcolls[$tagcollid2]->sortorder);
557         // Illegal tag colls sortorder changing.
558         $this->assertFalse(core_tag_collection::change_sortorder($tagcolls[$defaulttagcoll], 1));
559         $this->assertFalse(core_tag_collection::change_sortorder($tagcolls[$defaulttagcoll], -1));
560         $this->assertFalse(core_tag_collection::change_sortorder($tagcolls[$tagcollid2], 1));
562         // Move the very last tag coll one position up.
563         $this->assertTrue(core_tag_collection::change_sortorder($tagcolls[$tagcollid2], -1));
564         $tagcolls = core_tag_collection::get_collections();
565         $this->assertEquals($tagcolls[$tagcollid2]->sortorder + 1,
566             $tagcolls[$tagcollid1]->sortorder);
568         // Move the second last tag coll one position down.
569         $this->assertTrue(core_tag_collection::change_sortorder($tagcolls[$tagcollid2], 1));
570         $tagcolls = core_tag_collection::get_collections();
571         $this->assertEquals($tagcolls[$tagcollid1]->sortorder + 1,
572             $tagcolls[$tagcollid2]->sortorder);
574         // Edit tag coll.
575         $this->assertTrue(core_tag_collection::update($tagcolls[$tagcollid2],
576             (object)array('name' => 'posts2')));
577         $tagcolls = core_tag_collection::get_collections();
578         $this->assertEquals('posts2', $tagcolls[$tagcollid2]->name);
580         // Delete tag coll.
581         $count = $DB->count_records('tag_coll');
582         $this->assertFalse(core_tag_collection::delete($tagcolls[$defaulttagcoll]));
583         $this->assertTrue(core_tag_collection::delete($tagcolls[$tagcollid1]));
584         $this->assertEquals($count - 1, $DB->count_records('tag_coll'));
585     }
587     /**
588      * Prepares environment for test_move_tags_* tests
589      */
590     protected function prepare_move_tags() {
591         global $CFG;
592         require_once($CFG->dirroot.'/blog/locallib.php');
593         $this->setUser($this->getDataGenerator()->create_user());
595         $collid1 = core_tag_collection::get_default();
596         $collid2 = core_tag_collection::create(array('name' => 'newcoll'))->id;
597         $user1 = $this->getDataGenerator()->create_user();
598         $user2 = $this->getDataGenerator()->create_user();
599         $blogpost = new blog_entry(null, array('subject' => 'test'), null);
600         $states = blog_entry::get_applicable_publish_states();
601         $blogpost->publishstate = reset($states);
602         $blogpost->add();
604         core_tag_tag::set_item_tags('core', 'user', $user1->id, context_user::instance($user1->id),
605                 array('Tag1', 'Tag2'));
606         core_tag_tag::set_item_tags('core', 'user', $user2->id, context_user::instance($user2->id),
607                 array('Tag2', 'Tag3'));
608         $this->getDataGenerator()->create_tag(array('rawname' => 'Tag4',
609             'tagcollid' => $collid1, 'isstandard' => 1));
610         $this->getDataGenerator()->create_tag(array('rawname' => 'Tag5',
611             'tagcollid' => $collid2, 'isstandard' => 1));
613         return array($collid1, $collid2, $user1, $user2, $blogpost);
614     }
616     public function test_move_tags_simple() {
617         global $DB;
618         list($collid1, $collid2, $user1, $user2, $blogpost) = $this->prepare_move_tags();
620         // Move 'user' area from collection 1 to collection 2, make sure tags were moved completely.
621         $tagarea = $DB->get_record('tag_area', array('itemtype' => 'user', 'component' => 'core'));
622         core_tag_area::update($tagarea, array('tagcollid' => $collid2));
624         $tagsaftermove = $DB->get_records('tag');
625         foreach ($tagsaftermove as $tag) {
626             // Confirm that the time modified has not been unset.
627             $this->assertNotEmpty($tag->timemodified);
628         }
630         $this->assertEquals(array('Tag4'),
631                 $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid1)));
632         $this->assertEquals(array('Tag1', 'Tag2', 'Tag3', 'Tag5'),
633                 $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid2)));
634         $this->assertEquals(array('Tag1', 'Tag2'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user1->id)));
635         $this->assertEquals(array('Tag2', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user2->id)));
636     }
638     public function test_move_tags_split_tag() {
639         global $DB;
640         list($collid1, $collid2, $user1, $user2, $blogpost) = $this->prepare_move_tags();
642         core_tag_tag::set_item_tags('core', 'post', $blogpost->id, context_system::instance(),
643                 array('Tag1', 'Tag3'));
645         // Move 'user' area from collection 1 to collection 2, make sure tag Tag2 was moved and tags Tag1 and Tag3 were duplicated.
646         $tagareauser = $DB->get_record('tag_area', array('itemtype' => 'user', 'component' => 'core'));
647         core_tag_area::update($tagareauser, array('tagcollid' => $collid2));
649         $tagsaftermove = $DB->get_records('tag');
650         foreach ($tagsaftermove as $tag) {
651             // Confirm that the time modified has not been unset.
652             $this->assertNotEmpty($tag->timemodified);
653         }
655         $this->assertEquals(array('Tag1', 'Tag3', 'Tag4'),
656                 $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid1)));
657         $this->assertEquals(array('Tag1', 'Tag2', 'Tag3', 'Tag5'),
658                 $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid2)));
659         $this->assertEquals(array('Tag1', 'Tag2'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user1->id)));
660         $this->assertEquals(array('Tag2', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user2->id)));
661         $this->assertEquals(array('Tag1', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'post', $blogpost->id)));
662     }
664     public function test_move_tags_merge_tag() {
665         global $DB;
666         list($collid1, $collid2, $user1, $user2, $blogpost) = $this->prepare_move_tags();
668         // Set collection for 'post' tag area to be collection 2 and add some tags there.
669         $tagareablog = $DB->get_record('tag_area', array('itemtype' => 'post', 'component' => 'core'));
670         core_tag_area::update($tagareablog, array('tagcollid' => $collid2));
672         core_tag_tag::set_item_tags('core', 'post', $blogpost->id, context_system::instance(),
673                 array('TAG1', 'Tag3'));
675         // Move 'user' area from collection 1 to collection 2,
676         // make sure tag Tag2 was moved and tags Tag1 and Tag3 were merged into existing.
677         $tagareauser = $DB->get_record('tag_area', array('itemtype' => 'user', 'component' => 'core'));
678         core_tag_area::update($tagareauser, array('tagcollid' => $collid2));
680         $tagsaftermove = $DB->get_records('tag');
681         foreach ($tagsaftermove as $tag) {
682             // Confirm that the time modified has not been unset.
683             $this->assertNotEmpty($tag->timemodified);
684         }
686         $this->assertEquals(array('Tag4'),
687                 $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid1)));
688         $this->assertEquals(array('TAG1', 'Tag2', 'Tag3', 'Tag5'),
689                 $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid2)));
690         $this->assertEquals(array('TAG1', 'Tag2'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user1->id)));
691         $this->assertEquals(array('Tag2', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user2->id)));
692         $this->assertEquals(array('TAG1', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'post', $blogpost->id)));
693     }
695     public function test_move_tags_with_related() {
696         global $DB;
697         list($collid1, $collid2, $user1, $user2, $blogpost) = $this->prepare_move_tags();
699         // Set Tag1 to be related to Tag2 and Tag4 (in collection 1).
700         core_tag_tag::get_by_name($collid1, 'Tag1')->set_related_tags(array('Tag2', 'Tag4'));
702         // Set collection for 'post' tag area to be collection 2 and add some tags there.
703         $tagareablog = $DB->get_record('tag_area', array('itemtype' => 'post', 'component' => 'core'));
704         core_tag_area::update($tagareablog, array('tagcollid' => $collid2));
706         core_tag_tag::set_item_tags('core', 'post', $blogpost->id, context_system::instance(),
707                 array('TAG1', 'Tag3'));
709         // Move 'user' area from collection 1 to collection 2, make sure tags were moved completely.
710         $tagarea = $DB->get_record('tag_area', array('itemtype' => 'user', 'component' => 'core'));
711         core_tag_area::update($tagarea, array('tagcollid' => $collid2));
713         $tagsaftermove = $DB->get_records('tag');
714         foreach ($tagsaftermove as $tag) {
715             // Confirm that the time modified has not been unset.
716             $this->assertNotEmpty($tag->timemodified);
717         }
719         $this->assertEquals(array('Tag1', 'Tag2', 'Tag4'),
720                 $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid1)));
721         $this->assertEquals(array('TAG1', 'Tag2', 'Tag3', 'Tag4', 'Tag5'),
722                 $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid2)));
723         $this->assertEquals(array('TAG1', 'Tag2'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user1->id)));
724         $this->assertEquals(array('Tag2', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user2->id)));
726         $tag11 = core_tag_tag::get_by_name($collid1, 'Tag1');
727         $related11 = core_tag_tag::get($tag11->id)->get_manual_related_tags();
728         $related11 = array_map('core_tag_tag::make_display_name', $related11);
729         sort($related11); // Order of related tags may be random.
730         $this->assertEquals('Tag2, Tag4', join(', ', $related11));
732         $tag21 = core_tag_tag::get_by_name($collid2, 'TAG1');
733         $related21 = core_tag_tag::get($tag21->id)->get_manual_related_tags();
734         $related21 = array_map('core_tag_tag::make_display_name', $related21);
735         sort($related21); // Order of related tags may be random.
736         $this->assertEquals('Tag2, Tag4', join(', ', $related21));
737     }
739     public function test_move_tags_corrupted() {
740         global $DB;
741         list($collid1, $collid2, $user1, $user2, $blogpost) = $this->prepare_move_tags();
742         $collid3 = core_tag_collection::create(array('name' => 'weirdcoll'))->id;
744         // We already have Tag1 in coll1, now let's create it in coll3.
745         $extratag1 = $this->getDataGenerator()->create_tag(array('rawname' => 'Tag1',
746             'tagcollid' => $collid3, 'isstandard' => 1));
748         // Artificially add 'Tag1' from coll3 to user2.
749         $DB->insert_record('tag_instance', array('tagid' => $extratag1->id, 'itemtype' => 'user',
750             'component' => 'core', 'itemid' => $user2->id, 'ordering' => 3));
752         // Now we have corrupted data: both users are tagged with 'Tag1', however these are two tags in different collections.
753         $user1tags = array_values(core_tag_tag::get_item_tags('core', 'user', $user1->id));
754         $user2tags = array_values(core_tag_tag::get_item_tags('core', 'user', $user2->id));
755         $this->assertEquals('Tag1', $user1tags[0]->rawname);
756         $this->assertEquals('Tag1', $user2tags[2]->rawname);
757         $this->assertNotEquals($user1tags[0]->tagcollid, $user2tags[2]->tagcollid);
759         // Move user interests tag area into coll2.
760         $tagarea = $DB->get_record('tag_area', array('itemtype' => 'user', 'component' => 'core'));
761         core_tag_area::update($tagarea, array('tagcollid' => $collid2));
763         $tagsaftermove = $DB->get_records('tag');
764         foreach ($tagsaftermove as $tag) {
765             // Confirm that the time modified has not been unset.
766             $this->assertNotEmpty($tag->timemodified);
767         }
769         // Now all tags are correctly moved to the new collection and both tags 'Tag1' were merged.
770         $user1tags = array_values(core_tag_tag::get_item_tags('core', 'user', $user1->id));
771         $user2tags = array_values(core_tag_tag::get_item_tags('core', 'user', $user2->id));
772         $this->assertEquals('Tag1', $user1tags[0]->rawname);
773         $this->assertEquals('Tag1', $user2tags[2]->rawname);
774         $this->assertEquals($collid2, $user1tags[0]->tagcollid);
775         $this->assertEquals($collid2, $user2tags[2]->tagcollid);
776     }
778     /**
779      * Tests that tag_normalize function throws an exception.
780      * This function was deprecated in 3.1
781      */
782     public function test_normalize() {
783         $this->expectException('coding_exception');
784         $this->expectExceptionMessage('tag_normalize() can not be used anymore. Please use ' .
785             'core_tag_tag::normalize().');
786         tag_normalize();
787     }
789     /**
790      * Test functions core_tag_tag::create_if_missing() and core_tag_tag::get_by_name_bulk().
791      */
792     public function test_create_get() {
793         $tagset = array('Cat', ' Dog  ', '<Mouse', '<>', 'mouse', 'Dog');
795         $collid = core_tag_collection::get_default();
796         $tags = core_tag_tag::create_if_missing($collid, $tagset);
797         $this->assertEquals(array('cat', 'dog', 'mouse'), array_keys($tags));
798         $this->assertEquals('Dog', $tags['dog']->rawname);
799         $this->assertEquals('mouse', $tags['mouse']->rawname); // Case of the last tag wins.
801         $tags2 = core_tag_tag::create_if_missing($collid, array('CAT', 'Elephant'));
802         $this->assertEquals(array('cat', 'elephant'), array_keys($tags2));
803         $this->assertEquals('Cat', $tags2['cat']->rawname);
804         $this->assertEquals('Elephant', $tags2['elephant']->rawname);
805         $this->assertEquals($tags['cat']->id, $tags2['cat']->id); // Tag 'cat' already existed and was not created again.
807         $tags3 = core_tag_tag::get_by_name_bulk($collid, $tagset);
808         $this->assertEquals(array('cat', 'dog', 'mouse'), array_keys($tags3));
809         $this->assertEquals('Dog', $tags3['dog']->rawname);
810         $this->assertEquals('mouse', $tags3['mouse']->rawname);
812     }
814     /**
815      * Testing function core_tag_tag::combine_tags()
816      */
817     public function test_combine_tags() {
818         $initialtags = array(
819             array('Cat', 'Dog'),
820             array('Dog', 'Cat'),
821             array('Cats', 'Hippo'),
822             array('Hippo', 'Cats'),
823             array('Cat', 'Mouse', 'Kitten'),
824             array('Cats', 'Mouse', 'Kitten'),
825             array('Kitten', 'Mouse', 'Cat'),
826             array('Kitten', 'Mouse', 'Cats'),
827             array('Cats', 'Mouse', 'Kitten'),
828             array('Mouse', 'Hippo')
829         );
831         $finaltags = array(
832             array('Cat', 'Dog'),
833             array('Dog', 'Cat'),
834             array('Cat', 'Hippo'),
835             array('Hippo', 'Cat'),
836             array('Cat', 'Mouse'),
837             array('Cat', 'Mouse'),
838             array('Mouse', 'Cat'),
839             array('Mouse', 'Cat'),
840             array('Cat', 'Mouse'),
841             array('Mouse', 'Hippo')
842         );
844         $collid = core_tag_collection::get_default();
845         $context = context_system::instance();
846         foreach ($initialtags as $id => $taglist) {
847             core_tag_tag::set_item_tags('core', 'course', $id + 10, $context, $initialtags[$id]);
848         }
850         core_tag_tag::get_by_name($collid, 'Cats', '*')->update(array('isstandard' => 1));
852         // Combine tags 'Cats' and 'Kitten' into 'Cat'.
853         $cat = core_tag_tag::get_by_name($collid, 'Cat', '*');
854         $cats = core_tag_tag::get_by_name($collid, 'Cats', '*');
855         $kitten = core_tag_tag::get_by_name($collid, 'Kitten', '*');
856         $cat->combine_tags(array($cats, $kitten));
858         foreach ($finaltags as $id => $taglist) {
859             $this->assertEquals($taglist,
860                 array_values(core_tag_tag::get_item_tags_array('core', 'course', $id + 10)),
861                     'Original array ('.join(', ', $initialtags[$id]).')');
862         }
864         // Ensure combined tags are deleted and 'Cat' is now official (because 'Cats' was official).
865         $this->assertEmpty(core_tag_tag::get_by_name($collid, 'Cats'));
866         $this->assertEmpty(core_tag_tag::get_by_name($collid, 'Kitten'));
867         $cattag = core_tag_tag::get_by_name($collid, 'Cat', '*');
868         $this->assertEquals(1, $cattag->isstandard);
869     }
871     /**
872      * Testing function core_tag_tag::combine_tags() when related tags are present.
873      */
874     public function test_combine_tags_with_related() {
875         $collid = core_tag_collection::get_default();
876         $context = context_system::instance();
877         core_tag_tag::set_item_tags('core', 'course', 10, $context, array('Cat', 'Cats', 'Dog'));
878         core_tag_tag::get_by_name($collid, 'Cat', '*')->set_related_tags(array('Kitty'));
879         core_tag_tag::get_by_name($collid, 'Cats', '*')->set_related_tags(array('Cat', 'Kitten', 'Kitty'));
881         // Combine tags 'Cats' into 'Cat'.
882         $cat = core_tag_tag::get_by_name($collid, 'Cat', '*');
883         $cats = core_tag_tag::get_by_name($collid, 'Cats', '*');
884         $cat->combine_tags(array($cats));
886         // Ensure 'Cat' is now related to 'Kitten' and 'Kitty' (order of related tags may be random).
887         $relatedtags = array_map(function($t) {return $t->rawname;}, $cat->get_manual_related_tags());
888         sort($relatedtags);
889         $this->assertEquals(array('Kitten', 'Kitty'), array_values($relatedtags));
890     }
892     /**
893      * Testing function core_tag_tag::combine_tags() when correlated tags are present.
894      */
895     public function test_combine_tags_with_correlated() {
896         $task = new \core\task\tag_cron_task();
898         $tags = $this->prepare_correlated();
900         $task->compute_correlations();
901         // Now 'cat' is correlated with 'cats'.
902         // Also 'dog', 'dogs' and 'puppy' are correlated.
903         // There is a manual relation between 'cat' and 'kitten'.
904         // See function test_correlations() for assertions.
906         // Combine tags 'dog' and 'kitten' into 'cat' and make sure that cat is now correlated with dogs and puppy.
907         $tags['cat']->combine_tags(array($tags['dog'], $tags['kitten']));
909         $correlatedtags = $this->get_correlated_tags_names($tags['cat']);
910         $this->assertEquals(['cats', 'dogs', 'puppy'], $correlatedtags);
912         $correlatedtags = $this->get_correlated_tags_names($tags['dogs']);
913         $this->assertEquals(['cat', 'puppy'], $correlatedtags);
915         $correlatedtags = $this->get_correlated_tags_names($tags['puppy']);
916         $this->assertEquals(['cat', 'dogs'], $correlatedtags);
918         // Add tag that does not have any correlations.
919         $user7 = $this->getDataGenerator()->create_user();
920         core_tag_tag::set_item_tags('core', 'user', $user7->id, context_user::instance($user7->id), array('hippo'));
921         $tags['hippo'] = core_tag_tag::get_by_name(core_tag_collection::get_default(), 'hippo', '*');
923         // Combine tag 'cat' into 'hippo'. Now 'hippo' should have the same correlations 'cat' used to have and also
924         // tags 'dogs' and 'puppy' should have 'hippo' in correlations.
925         $tags['hippo']->combine_tags(array($tags['cat']));
927         $correlatedtags = $this->get_correlated_tags_names($tags['hippo']);
928         $this->assertEquals(['cats', 'dogs', 'puppy'], $correlatedtags);
930         $correlatedtags = $this->get_correlated_tags_names($tags['dogs']);
931         $this->assertEquals(['hippo', 'puppy'], $correlatedtags);
933         $correlatedtags = $this->get_correlated_tags_names($tags['puppy']);
934         $this->assertEquals(['dogs', 'hippo'], $correlatedtags);
935     }
937     /**
938      * get_tags_by_area_in_contexts should return an empty array if there
939      * are no tag instances for the area in the given context.
940      */
941     public function test_get_tags_by_area_in_contexts_empty() {
942         $tagnames = ['foo'];
943         $collid = core_tag_collection::get_default();
944         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
945         $user = $this->getDataGenerator()->create_user();
946         $context = context_user::instance($user->id);
947         $component = 'core';
948         $itemtype = 'user';
950         $result = core_tag_tag::get_tags_by_area_in_contexts($component, $itemtype, [$context]);
951         $this->assertEmpty($result);
952     }
954     /**
955      * get_tags_by_area_in_contexts should return an array of tags that
956      * have instances in the given context even when there is only a single
957      * instance.
958      */
959     public function test_get_tags_by_area_in_contexts_single_tag_one_context() {
960         $tagnames = ['foo'];
961         $collid = core_tag_collection::get_default();
962         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
963         $user = $this->getDataGenerator()->create_user();
964         $context = context_user::instance($user->id);
965         $component = 'core';
966         $itemtype = 'user';
967         core_tag_tag::set_item_tags($component, $itemtype, $user->id, $context, $tagnames);
969         $result = core_tag_tag::get_tags_by_area_in_contexts($component, $itemtype, [$context]);
970         $expected = array_map(function($t) {
971             return $t->id;
972         }, $tags);
973         $actual = array_map(function($t) {
974             return $t->id;
975         }, $result);
977         sort($expected);
978         sort($actual);
980         $this->assertEquals($expected, $actual);
981     }
983     /**
984      * get_tags_by_area_in_contexts should return all tags in an array
985      * that have tag instances in for the area in the given context and
986      * should ignore all tags that don't have an instance.
987      */
988     public function test_get_tags_by_area_in_contexts_multiple_tags_one_context() {
989         $tagnames = ['foo', 'bar', 'baz'];
990         $collid = core_tag_collection::get_default();
991         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
992         $user = $this->getDataGenerator()->create_user();
993         $context = context_user::instance($user->id);
994         $component = 'core';
995         $itemtype = 'user';
996         core_tag_tag::set_item_tags($component, $itemtype, $user->id, $context, array_slice($tagnames, 0, 2));
998         $result = core_tag_tag::get_tags_by_area_in_contexts($component, $itemtype, [$context]);
999         $expected = ['foo', 'bar'];
1000         $actual = array_map(function($t) {
1001             return $t->name;
1002         }, $result);
1004         sort($expected);
1005         sort($actual);
1007         $this->assertEquals($expected, $actual);
1008     }
1010     /**
1011      * get_tags_by_area_in_contexts should return the unique set of
1012      * tags for a area in the given contexts. Multiple tag instances of
1013      * the same tag don't result in duplicates in the result set.
1014      *
1015      * Tags with tag instances in the same area with in difference contexts
1016      * should be ignored.
1017      */
1018     public function test_get_tags_by_area_in_contexts_multiple_tags_multiple_contexts() {
1019         $tagnames = ['foo', 'bar', 'baz', 'bop', 'bam', 'bip'];
1020         $collid = core_tag_collection::get_default();
1021         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1022         $user1 = $this->getDataGenerator()->create_user();
1023         $user2 = $this->getDataGenerator()->create_user();
1024         $user3 = $this->getDataGenerator()->create_user();
1025         $context1 = context_user::instance($user1->id);
1026         $context2 = context_user::instance($user2->id);
1027         $context3 = context_user::instance($user3->id);
1028         $component = 'core';
1029         $itemtype = 'user';
1031         // User 1 tags: 'foo', 'bar'.
1032         core_tag_tag::set_item_tags($component, $itemtype, $user1->id, $context1, array_slice($tagnames, 0, 2));
1033         // User 2 tags: 'bar', 'baz'.
1034         core_tag_tag::set_item_tags($component, $itemtype, $user2->id, $context2, array_slice($tagnames, 1, 2));
1035         // User 3 tags: 'bop', 'bam'.
1036         core_tag_tag::set_item_tags($component, $itemtype, $user3->id, $context3, array_slice($tagnames, 3, 2));
1038         $result = core_tag_tag::get_tags_by_area_in_contexts($component, $itemtype, [$context1, $context2]);
1039         // Both User 1 and 2 have tagged using 'bar' but we don't
1040         // expect duplicate tags in the result since they are the same
1041         // tag.
1042         //
1043         // User 3 has tagged 'bop' and 'bam' but we aren't searching in
1044         // that context so they shouldn't be in the results.
1045         $expected = ['foo', 'bar', 'baz'];
1046         $actual = array_map(function($t) {
1047             return $t->name;
1048         }, $result);
1050         sort($expected);
1051         sort($actual);
1053         $this->assertEquals($expected, $actual);
1054     }
1056     /**
1057      * get_items_tags should return an empty array if the tag area is disabled.
1058      */
1059     public function test_get_items_tags_disabled_component() {
1060         global $CFG;
1062         $user1 = $this->getDataGenerator()->create_user();
1063         $context1 = context_user::instance($user1->id);
1064         $component = 'core';
1065         $itemtype = 'user';
1066         $itemids = [$user1->id];
1068         // User 1 tags: 'foo', 'bar'.
1069         core_tag_tag::set_item_tags($component, $itemtype, $user1->id, $context1, ['foo']);
1070         // This mimics disabling tags for a component.
1071         $CFG->usetags = false;
1072         $result = core_tag_tag::get_items_tags($component, $itemtype, $itemids);
1073         $this->assertEmpty($result);
1074     }
1076     /**
1077      * get_items_tags should return an empty array if the tag item ids list
1078      * is empty.
1079      */
1080     public function test_get_items_tags_empty_itemids() {
1081         $user1 = $this->getDataGenerator()->create_user();
1082         $context1 = context_user::instance($user1->id);
1083         $component = 'core';
1084         $itemtype = 'user';
1086         // User 1 tags: 'foo', 'bar'.
1087         core_tag_tag::set_item_tags($component, $itemtype, $user1->id, $context1, ['foo']);
1088         $result = core_tag_tag::get_items_tags($component, $itemtype, []);
1089         $this->assertEmpty($result);
1090     }
1092     /**
1093      * get_items_tags should return an array indexed by the item ids with empty
1094      * arrays as the values when the component or itemtype is unknown.
1095      */
1096     public function test_get_items_tags_unknown_component_itemtype() {
1097         $itemids = [1, 2, 3];
1098         $result = core_tag_tag::get_items_tags('someunknowncomponent', 'user', $itemids);
1099         foreach ($itemids as $itemid) {
1100             // Unknown component should return an array indexed by the item ids
1101             // with empty arrays as the values.
1102             $this->assertEmpty($result[$itemid]);
1103         }
1105         $result = core_tag_tag::get_items_tags('core', 'someunknownitemtype', $itemids);
1106         foreach ($itemids as $itemid) {
1107             // Unknown item type should return an array indexed by the item ids
1108             // with empty arrays as the values.
1109             $this->assertEmpty($result[$itemid]);
1110         }
1111     }
1113     /**
1114      * get_items_tags should return an array indexed by the item ids with empty
1115      * arrays as the values for any item ids that don't have tag instances.
1116      *
1117      * Data setup:
1118      * Users: 1, 2, 3
1119      * Tags: user 1 = ['foo', 'bar']
1120      *       user 2 = ['baz', 'bop']
1121      *       user 3 = []
1122      *
1123      * Expected result:
1124      * [
1125      *      1 => [
1126      *          1 => 'foo',
1127      *          2 => 'bar'
1128      *      ],
1129      *      2 => [
1130      *          3 => 'baz',
1131      *          4 => 'bop'
1132      *      ],
1133      *      3 => []
1134      * ]
1135      */
1136     public function test_get_items_tags_missing_itemids() {
1137         $user1 = $this->getDataGenerator()->create_user();
1138         $user2 = $this->getDataGenerator()->create_user();
1139         $user3 = $this->getDataGenerator()->create_user();
1140         $context1 = context_user::instance($user1->id);
1141         $context2 = context_user::instance($user2->id);
1142         $component = 'core';
1143         $itemtype = 'user';
1144         $itemids = [$user1->id, $user2->id, $user3->id];
1145         $expecteduser1tagnames = ['foo', 'bar'];
1146         $expecteduser2tagnames = ['baz', 'bop'];
1147         $expecteduser3tagnames = [];
1149         // User 1 tags: 'foo', 'bar'.
1150         core_tag_tag::set_item_tags($component, $itemtype, $user1->id, $context1, $expecteduser1tagnames);
1151         // User 2 tags: 'bar', 'baz'.
1152         core_tag_tag::set_item_tags($component, $itemtype, $user2->id, $context2, $expecteduser2tagnames);
1154         $result = core_tag_tag::get_items_tags($component, $itemtype, $itemids);
1155         $actualuser1tagnames = array_map(function($taginstance) {
1156             return $taginstance->name;
1157         }, $result[$user1->id]);
1158         $actualuser2tagnames = array_map(function($taginstance) {
1159             return $taginstance->name;
1160         }, $result[$user2->id]);
1161         $actualuser3tagnames = $result[$user3->id];
1163         sort($expecteduser1tagnames);
1164         sort($expecteduser2tagnames);
1165         sort($actualuser1tagnames);
1166         sort($actualuser2tagnames);
1168         $this->assertEquals($expecteduser1tagnames, $actualuser1tagnames);
1169         $this->assertEquals($expecteduser2tagnames, $actualuser2tagnames);
1170         $this->assertEquals($expecteduser3tagnames, $actualuser3tagnames);
1171     }
1173     /**
1174      * set_item_tags should remove any tags that aren't in the given list and should
1175      * add any instances that are missing.
1176      */
1177     public function test_set_item_tags_no_multiple_context_add_remove_instances() {
1178         $tagnames = ['foo', 'bar', 'baz', 'bop'];
1179         $collid = core_tag_collection::get_default();
1180         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1181         $user1 = $this->getDataGenerator()->create_user();
1182         $context = context_user::instance($user1->id);
1183         $component = 'core';
1184         $itemtype = 'user';
1185         $itemid = 1;
1186         $tagareas = core_tag_area::get_areas();
1187         $tagarea = $tagareas[$itemtype][$component];
1188         $newtagnames = ['bar', 'baz', 'bop'];
1190         // Make sure the tag area doesn't allow multiple contexts.
1191         core_tag_area::update($tagarea, ['multiplecontexts' => false]);
1193         // Create tag instances in separate contexts.
1194         $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1195         $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context);
1197         core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context, $newtagnames);
1199         $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1200         $actualtagnames = array_map(function($record) {
1201             return $record->name;
1202         }, $result);
1204         sort($newtagnames);
1205         sort($actualtagnames);
1207         // The list of tags should match the $newtagnames which means 'foo'
1208         // should have been removed while 'baz' and 'bop' were added. 'bar'
1209         // should remain as it was in the new list of tags.
1210         $this->assertEquals($newtagnames, $actualtagnames);
1211     }
1213     /**
1214      * set_item_tags should set all of the tag instance context ids to the given
1215      * context if the tag area for the items doesn't allow multiple contexts for
1216      * the tag instances.
1217      */
1218     public function test_set_item_tags_no_multiple_context_updates_context_of_instances() {
1219         $tagnames = ['foo', 'bar'];
1220         $collid = core_tag_collection::get_default();
1221         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1222         $user1 = $this->getDataGenerator()->create_user();
1223         $user2 = $this->getDataGenerator()->create_user();
1224         $context1 = context_user::instance($user1->id);
1225         $context2 = context_user::instance($user2->id);
1226         $component = 'core';
1227         $itemtype = 'user';
1228         $itemid = 1;
1229         $tagareas = core_tag_area::get_areas();
1230         $tagarea = $tagareas[$itemtype][$component];
1232         // Make sure the tag area doesn't allow multiple contexts.
1233         core_tag_area::update($tagarea, ['multiplecontexts' => false]);
1235         // Create tag instances in separate contexts.
1236         $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1237         $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context2);
1239         core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context1, $tagnames);
1241         $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1242         $this->assertCount(count($tagnames), $result);
1244         foreach ($result as $tag) {
1245             // The core user tag area doesn't allow multiple contexts for tag instances
1246             // so set_item_tags should have set all of the tag instance context ids
1247             // to match $context1.
1248             $this->assertEquals($context1->id, $tag->taginstancecontextid);
1249         }
1250     }
1252     /**
1253      * set_item_tags should delete all of the tag instances that don't match
1254      * the new set of tags, regardless of the context that the tag instance
1255      * is in.
1256      */
1257     public function test_set_item_tags_no_multiple_contex_deletes_old_instancest() {
1258         $tagnames = ['foo', 'bar', 'baz', 'bop'];
1259         $collid = core_tag_collection::get_default();
1260         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1261         $user1 = $this->getDataGenerator()->create_user();
1262         $user2 = $this->getDataGenerator()->create_user();
1263         $context1 = context_user::instance($user1->id);
1264         $context2 = context_user::instance($user2->id);
1265         $component = 'core';
1266         $itemtype = 'user';
1267         $itemid = 1;
1268         $expectedtagnames = ['foo', 'baz'];
1269         $tagareas = core_tag_area::get_areas();
1270         $tagarea = $tagareas[$itemtype][$component];
1272         // Make sure the tag area doesn't allow multiple contexts.
1273         core_tag_area::update($tagarea, ['multiplecontexts' => false]);
1275         // Create tag instances in separate contexts.
1276         $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1277         $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context1);
1278         $this->add_tag_instance($tags['baz'], $component, $itemtype, $itemid, $context2);
1279         $this->add_tag_instance($tags['bop'], $component, $itemtype, $itemid, $context2);
1281         core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context1, $expectedtagnames);
1283         $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1284         $actualtagnames = array_map(function($record) {
1285             return $record->name;
1286         }, $result);
1288         sort($expectedtagnames);
1289         sort($actualtagnames);
1291         // The list of tags should match the $expectedtagnames.
1292         $this->assertEquals($expectedtagnames, $actualtagnames);
1294         foreach ($result as $tag) {
1295             // The core user tag area doesn't allow multiple contexts for tag instances
1296             // so set_item_tags should have set all of the tag instance context ids
1297             // to match $context1.
1298             $this->assertEquals($context1->id, $tag->taginstancecontextid);
1299         }
1300     }
1302     /**
1303      * set_item_tags should not change tag instances in a different context to the one
1304      * it's opertating on if the tag area allows instances from multiple contexts.
1305      */
1306     public function test_set_item_tags_allow_multiple_context_doesnt_update_context() {
1307         global $DB;
1308         $tagnames = ['foo', 'bar', 'bop'];
1309         $collid = core_tag_collection::get_default();
1310         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1311         $user1 = $this->getDataGenerator()->create_user();
1312         $user2 = $this->getDataGenerator()->create_user();
1313         $context1 = context_user::instance($user1->id);
1314         $context2 = context_user::instance($user2->id);
1315         $component = 'core';
1316         $itemtype = 'user';
1317         $itemid = 1;
1318         $tagareas = core_tag_area::get_areas();
1319         $tagarea = $tagareas[$itemtype][$component];
1321         // Make sure the tag area allows multiple contexts.
1322         core_tag_area::update($tagarea, ['multiplecontexts' => true]);
1324         // Create tag instances in separate contexts.
1325         $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1326         $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context2);
1328         // Set the list of tags for $context1. This includes a tag that already exists
1329         // in that context and a new tag. There is another tag, 'bar', that exists in a
1330         // different context ($context2) that should be ignored.
1331         core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context1, ['foo', 'bop']);
1333         $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1334         $actualtagnames = array_map(function($record) {
1335             return $record->name;
1336         }, $result);
1338         sort($tagnames);
1339         sort($actualtagnames);
1340         // The list of tags should match the $tagnames.
1341         $this->assertEquals($tagnames, $actualtagnames);
1343         foreach ($result as $tag) {
1344             if ($tag->name == 'bar') {
1345                 // The tag instance for 'bar' should have been left untouched
1346                 // because it was in a different context.
1347                 $this->assertEquals($context2->id, $tag->taginstancecontextid);
1348             } else {
1349                 $this->assertEquals($context1->id, $tag->taginstancecontextid);
1350             }
1351         }
1352     }
1354     /**
1355      * set_item_tags should delete all of the tag instances that don't match
1356      * the new set of tags only in the same context if the tag area allows
1357      * multiple contexts.
1358      */
1359     public function test_set_item_tags_allow_multiple_context_deletes_instances_in_same_context() {
1360         $tagnames = ['foo', 'bar', 'baz', 'bop'];
1361         $collid = core_tag_collection::get_default();
1362         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1363         $user1 = $this->getDataGenerator()->create_user();
1364         $user2 = $this->getDataGenerator()->create_user();
1365         $context1 = context_user::instance($user1->id);
1366         $context2 = context_user::instance($user2->id);
1367         $component = 'core';
1368         $itemtype = 'user';
1369         $itemid = 1;
1370         $expectedtagnames = ['foo', 'bar', 'bop'];
1371         $tagareas = core_tag_area::get_areas();
1372         $tagarea = $tagareas[$itemtype][$component];
1374         // Make sure the tag area allows multiple contexts.
1375         core_tag_area::update($tagarea, ['multiplecontexts' => true]);
1377         // Create tag instances in separate contexts.
1378         $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1379         $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context1);
1380         $this->add_tag_instance($tags['baz'], $component, $itemtype, $itemid, $context1);
1381         $this->add_tag_instance($tags['bop'], $component, $itemtype, $itemid, $context2);
1383         core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context1, ['foo', 'bar']);
1385         $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1386         $actualtagnames = array_map(function($record) {
1387             return $record->name;
1388         }, $result);
1390         sort($expectedtagnames);
1391         sort($actualtagnames);
1393         // The list of tags should match the $expectedtagnames, which includes the
1394         // tag 'bop' because it was in a different context to the one being set
1395         // even though it wasn't in the new set of tags.
1396         $this->assertEquals($expectedtagnames, $actualtagnames);
1397     }
1399     /**
1400      * set_item_tags should allow multiple instances of the same tag in different
1401      * contexts if the tag area allows multiple contexts.
1402      */
1403     public function test_set_item_tags_allow_multiple_context_same_tag_multiple_contexts() {
1404         $tagnames = ['foo'];
1405         $collid = core_tag_collection::get_default();
1406         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1407         $user1 = $this->getDataGenerator()->create_user();
1408         $user2 = $this->getDataGenerator()->create_user();
1409         $context1 = context_user::instance($user1->id);
1410         $context2 = context_user::instance($user2->id);
1411         $component = 'core';
1412         $itemtype = 'user';
1413         $itemid = 1;
1414         $expectedtagnames = ['foo', 'bar', 'bop'];
1415         $tagareas = core_tag_area::get_areas();
1416         $tagarea = $tagareas[$itemtype][$component];
1418         // Make sure the tag area allows multiple contexts.
1419         core_tag_area::update($tagarea, ['multiplecontexts' => true]);
1421         // Create first instance of 'foo' in $context1.
1422         $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1424         core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context2, ['foo']);
1426         $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1427         $tagsbycontext = array_reduce($result, function($carry, $tag) {
1428             $contextid = $tag->taginstancecontextid;
1429             if (isset($carry[$contextid])) {
1430                 $carry[$contextid][] = $tag;
1431             } else {
1432                 $carry[$contextid] = [$tag];
1433             }
1434             return $carry;
1435         }, []);
1437         // The result should be two tag instances of 'foo' in each of the
1438         // two contexts, $context1 and $context2.
1439         $this->assertCount(1, $tagsbycontext[$context1->id]);
1440         $this->assertCount(1, $tagsbycontext[$context2->id]);
1441         $this->assertEquals('foo', $tagsbycontext[$context1->id][0]->name);
1442         $this->assertEquals('foo', $tagsbycontext[$context2->id][0]->name);
1443     }
1445     /**
1446      * delete_instances_as_record with an empty set of instances should do nothing.
1447      */
1448     public function test_delete_instances_as_record_empty_set() {
1449         $user = $this->getDataGenerator()->create_user();
1450         $context = context_user::instance($user->id);
1451         $component = 'core';
1452         $itemtype = 'user';
1453         $itemid = 1;
1455         core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context, ['foo']);
1456         // This shouldn't error.
1457         core_tag_tag::delete_instances_as_record([]);
1459         $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1460         // We should still have one tag.
1461         $this->assertCount(1, $tags);
1462     }
1464     /**
1465      * delete_instances_as_record with an instance that doesn't exist should do
1466      * nothing.
1467      */
1468     public function test_delete_instances_as_record_missing_set() {
1469         $tagnames = ['foo'];
1470         $collid = core_tag_collection::get_default();
1471         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1472         $user = $this->getDataGenerator()->create_user();
1473         $context = context_user::instance($user->id);
1474         $component = 'core';
1475         $itemtype = 'user';
1476         $itemid = 1;
1478         $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1479         $taginstance->id++;
1481         // Delete an instance that doesn't exist should do nothing.
1482         core_tag_tag::delete_instances_as_record([$taginstance]);
1484         $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1485         // We should still have one tag.
1486         $this->assertCount(1, $tags);
1487     }
1489     /**
1490      * delete_instances_as_record with a list of all tag instances should
1491      * leave no tags left.
1492      */
1493     public function test_delete_instances_as_record_whole_set() {
1494         $tagnames = ['foo'];
1495         $collid = core_tag_collection::get_default();
1496         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1497         $user = $this->getDataGenerator()->create_user();
1498         $context = context_user::instance($user->id);
1499         $component = 'core';
1500         $itemtype = 'user';
1501         $itemid = 1;
1503         $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1505         core_tag_tag::delete_instances_as_record([$taginstance]);
1507         $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1508         // There should be no tags left.
1509         $this->assertEmpty($tags);
1510     }
1512     /**
1513      * delete_instances_as_record with a list of only some tag instances should
1514      * delete only the given tag instances and leave other tag instances.
1515      */
1516     public function test_delete_instances_as_record_partial_set() {
1517         $tagnames = ['foo', 'bar'];
1518         $collid = core_tag_collection::get_default();
1519         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1520         $user = $this->getDataGenerator()->create_user();
1521         $context = context_user::instance($user->id);
1522         $component = 'core';
1523         $itemtype = 'user';
1524         $itemid = 1;
1526         $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1527         $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context);
1529         core_tag_tag::delete_instances_as_record([$taginstance]);
1531         $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1532         // We should be left with a single tag, 'bar'.
1533         $this->assertCount(1, $tags);
1534         $tag = array_shift($tags);
1535         $this->assertEquals('bar', $tag->name);
1536     }
1538     /**
1539      * delete_instances_by_id with an empty set of ids should do nothing.
1540      */
1541     public function test_delete_instances_by_id_empty_set() {
1542         $user = $this->getDataGenerator()->create_user();
1543         $context = context_user::instance($user->id);
1544         $component = 'core';
1545         $itemtype = 'user';
1546         $itemid = 1;
1548         core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context, ['foo']);
1549         // This shouldn't error.
1550         core_tag_tag::delete_instances_by_id([]);
1552         $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1553         // We should still have one tag.
1554         $this->assertCount(1, $tags);
1555     }
1557     /**
1558      * delete_instances_by_id with an id that doesn't exist should do
1559      * nothing.
1560      */
1561     public function test_delete_instances_by_id_missing_set() {
1562         $tagnames = ['foo'];
1563         $collid = core_tag_collection::get_default();
1564         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1565         $user = $this->getDataGenerator()->create_user();
1566         $context = context_user::instance($user->id);
1567         $component = 'core';
1568         $itemtype = 'user';
1569         $itemid = 1;
1571         $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1573         // Delete an instance that doesn't exist should do nothing.
1574         core_tag_tag::delete_instances_by_id([$taginstance->id + 1]);
1576         $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1577         // We should still have one tag.
1578         $this->assertCount(1, $tags);
1579     }
1581     /**
1582      * delete_instances_by_id with a list of all tag instance ids should
1583      * leave no tags left.
1584      */
1585     public function test_delete_instances_by_id_whole_set() {
1586         $tagnames = ['foo'];
1587         $collid = core_tag_collection::get_default();
1588         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1589         $user = $this->getDataGenerator()->create_user();
1590         $context = context_user::instance($user->id);
1591         $component = 'core';
1592         $itemtype = 'user';
1593         $itemid = 1;
1595         $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1597         core_tag_tag::delete_instances_by_id([$taginstance->id]);
1599         $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1600         // There should be no tags left.
1601         $this->assertEmpty($tags);
1602     }
1604     /**
1605      * delete_instances_by_id with a list of only some tag instance ids should
1606      * delete only the given tag instance ids and leave other tag instances.
1607      */
1608     public function test_delete_instances_by_id_partial_set() {
1609         $tagnames = ['foo', 'bar'];
1610         $collid = core_tag_collection::get_default();
1611         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1612         $user = $this->getDataGenerator()->create_user();
1613         $context = context_user::instance($user->id);
1614         $component = 'core';
1615         $itemtype = 'user';
1616         $itemid = 1;
1618         $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1619         $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context);
1621         core_tag_tag::delete_instances_by_id([$taginstance->id]);
1623         $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1624         // We should be left with a single tag, 'bar'.
1625         $this->assertCount(1, $tags);
1626         $tag = array_shift($tags);
1627         $this->assertEquals('bar', $tag->name);
1628     }
1630     /**
1631      * delete_instances should delete all tag instances for a component if given
1632      * only the component as a parameter.
1633      */
1634     public function test_delete_instances_with_component() {
1635         global $DB;
1637         $tagnames = ['foo', 'bar'];
1638         $collid = core_tag_collection::get_default();
1639         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1640         $user = $this->getDataGenerator()->create_user();
1641         $context = context_user::instance($user->id);
1642         $component = 'core';
1643         $itemtype1 = 'user';
1644         $itemtype2 = 'course';
1645         $itemid = 1;
1647         // Add 2 tag instances in the same $component but with different item types.
1648         $this->add_tag_instance($tags['foo'], $component, $itemtype1, $itemid, $context);
1649         $this->add_tag_instance($tags['bar'], $component, $itemtype2, $itemid, $context);
1651         // Delete all tag instances for the component.
1652         core_tag_tag::delete_instances($component);
1654         $taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance} WHERE component = ?', [$component]);
1655         // Both tag instances from the $component should have been deleted even though
1656         // they are in different item types.
1657         $this->assertEmpty($taginstances);
1658     }
1660     /**
1661      * delete_instances should delete all tag instances for a component if given
1662      * only the component as a parameter.
1663      */
1664     public function test_delete_instances_with_component_and_itemtype() {
1665         global $DB;
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         $itemtype1 = 'user';
1674         $itemtype2 = 'course';
1675         $itemid = 1;
1677         // Add 2 tag instances in the same $component but with different item types.
1678         $this->add_tag_instance($tags['foo'], $component, $itemtype1, $itemid, $context);
1679         $this->add_tag_instance($tags['bar'], $component, $itemtype2, $itemid, $context);
1681         // Delete all tag instances for the component and itemtype.
1682         core_tag_tag::delete_instances($component, $itemtype1);
1684         $taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance} WHERE component = ?', [$component]);
1685         // Only the tag instances for $itemtype1 should have been deleted. We
1686         // should still be left with the instance for 'bar'.
1687         $this->assertCount(1, $taginstances);
1688         $taginstance = array_shift($taginstances);
1689         $this->assertEquals($itemtype2, $taginstance->itemtype);
1690         $this->assertEquals($tags['bar']->id, $taginstance->tagid);
1691     }
1693     /**
1694      * delete_instances should delete all tag instances for a component in a context
1695      * if given both the component and context id as parameters.
1696      */
1697     public function test_delete_instances_with_component_and_context() {
1698         global $DB;
1700         $tagnames = ['foo', 'bar', 'baz'];
1701         $collid = core_tag_collection::get_default();
1702         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1703         $user1 = $this->getDataGenerator()->create_user();
1704         $user2 = $this->getDataGenerator()->create_user();
1705         $context1 = context_user::instance($user1->id);
1706         $context2 = context_user::instance($user2->id);
1707         $component = 'core';
1708         $itemtype1 = 'user';
1709         $itemtype2 = 'course';
1710         $itemid = 1;
1712         // Add 3 tag instances in the same $component but with different contexts.
1713         $this->add_tag_instance($tags['foo'], $component, $itemtype1, $itemid, $context1);
1714         $this->add_tag_instance($tags['bar'], $component, $itemtype2, $itemid, $context1);
1715         $this->add_tag_instance($tags['baz'], $component, $itemtype2, $itemid, $context2);
1717         // Delete all tag instances for the component and context.
1718         core_tag_tag::delete_instances($component, null, $context1->id);
1720         $taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance} WHERE component = ?', [$component]);
1721         // Only the tag instances for $context1 should have been deleted. We
1722         // should still be left with the instance for 'baz'.
1723         $this->assertCount(1, $taginstances);
1724         $taginstance = array_shift($taginstances);
1725         $this->assertEquals($context2->id, $taginstance->contextid);
1726         $this->assertEquals($tags['baz']->id, $taginstance->tagid);
1727     }
1729     /**
1730      * delete_instances should delete all tag instances for a component, item type
1731      * and context if given the component, itemtype, and context id as parameters.
1732      */
1733     public function test_delete_instances_with_component_and_itemtype_and_context() {
1734         global $DB;
1736         $tagnames = ['foo', 'bar', 'baz'];
1737         $collid = core_tag_collection::get_default();
1738         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1739         $user1 = $this->getDataGenerator()->create_user();
1740         $user2 = $this->getDataGenerator()->create_user();
1741         $context1 = context_user::instance($user1->id);
1742         $context2 = context_user::instance($user2->id);
1743         $component = 'core';
1744         $itemtype1 = 'user';
1745         $itemtype2 = 'course';
1746         $itemid = 1;
1748         // Add 3 tag instances in the same $component but with different contexts.
1749         $this->add_tag_instance($tags['foo'], $component, $itemtype1, $itemid, $context1);
1750         $this->add_tag_instance($tags['bar'], $component, $itemtype2, $itemid, $context1);
1751         $this->add_tag_instance($tags['baz'], $component, $itemtype2, $itemid, $context2);
1753         // Delete all tag instances for the component and context.
1754         core_tag_tag::delete_instances($component, $itemtype2, $context1->id);
1756         $taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance} WHERE component = ?', [$component]);
1757         // Only the tag instances for $itemtype2 in $context1 should have been
1758         // deleted. We should still be left with the instance for 'foo' and 'baz'.
1759         $this->assertCount(2, $taginstances);
1760         $fooinstances = array_filter($taginstances, function($instance) use ($tags) {
1761             return $instance->tagid == $tags['foo']->id;
1762         });
1763         $fooinstance = array_shift($fooinstances);
1764         $bazinstances = array_filter($taginstances, function($instance) use ($tags) {
1765             return $instance->tagid == $tags['baz']->id;
1766         });
1767         $bazinstance = array_shift($bazinstances);
1768         $this->assertNotEmpty($fooinstance);
1769         $this->assertNotEmpty($bazinstance);
1770         $this->assertEquals($context1->id, $fooinstance->contextid);
1771         $this->assertEquals($context2->id, $bazinstance->contextid);
1772     }
1774     /**
1775      * change_instances_context should not change any existing instance contexts
1776      * if not given any instance ids.
1777      */
1778     public function test_change_instances_context_empty_set() {
1779         global $DB;
1781         $tagnames = ['foo'];
1782         $collid = core_tag_collection::get_default();
1783         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1784         $user1 = $this->getDataGenerator()->create_user();
1785         $user2 = $this->getDataGenerator()->create_user();
1786         $context1 = context_user::instance($user1->id);
1787         $context2 = context_user::instance($user2->id);
1788         $component = 'core';
1789         $itemtype = 'user';
1790         $itemid = 1;
1792         $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1794         core_tag_tag::change_instances_context([], $context2);
1796         $taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance}');
1797         // The existing tag instance should not have changed.
1798         $this->assertCount(1, $taginstances);
1799         $taginstance = array_shift($taginstances);
1800         $this->assertEquals($context1->id, $taginstance->contextid);
1801     }
1803     /**
1804      * change_instances_context should only change the context of the given ids.
1805      */
1806     public function test_change_instances_context_partial_set() {
1807         global $DB;
1809         $tagnames = ['foo', 'bar'];
1810         $collid = core_tag_collection::get_default();
1811         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1812         $user1 = $this->getDataGenerator()->create_user();
1813         $user2 = $this->getDataGenerator()->create_user();
1814         $context1 = context_user::instance($user1->id);
1815         $context2 = context_user::instance($user2->id);
1816         $component = 'core';
1817         $itemtype = 'user';
1818         $itemid = 1;
1820         $fooinstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1821         $barinstance = $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context1);
1823         core_tag_tag::change_instances_context([$fooinstance->id], $context2);
1825         // Reload the record.
1826         $fooinstance = $DB->get_record('tag_instance', ['id' => $fooinstance->id]);
1827         $barinstance = $DB->get_record('tag_instance', ['id' => $barinstance->id]);
1828         // Tag 'foo' context should be updated.
1829         $this->assertEquals($context2->id, $fooinstance->contextid);
1830         // Tag 'bar' context should not be changed.
1831         $this->assertEquals($context1->id, $barinstance->contextid);
1832     }
1834     /**
1835      * change_instances_context should change multiple items from multiple contexts.
1836      */
1837     public function test_change_instances_context_multiple_contexts() {
1838         global $DB;
1840         $tagnames = ['foo', 'bar'];
1841         $collid = core_tag_collection::get_default();
1842         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1843         $user1 = $this->getDataGenerator()->create_user();
1844         $user2 = $this->getDataGenerator()->create_user();
1845         $user3 = $this->getDataGenerator()->create_user();
1846         $context1 = context_user::instance($user1->id);
1847         $context2 = context_user::instance($user2->id);
1848         $context3 = context_user::instance($user3->id);
1849         $component = 'core';
1850         $itemtype = 'user';
1851         $itemid = 1;
1853         // Two instances in different contexts.
1854         $fooinstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1855         $barinstance = $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context2);
1857         core_tag_tag::change_instances_context([$fooinstance->id, $barinstance->id], $context3);
1859         // Reload the record.
1860         $fooinstance = $DB->get_record('tag_instance', ['id' => $fooinstance->id]);
1861         $barinstance = $DB->get_record('tag_instance', ['id' => $barinstance->id]);
1862         // Tag 'foo' context should be updated.
1863         $this->assertEquals($context3->id, $fooinstance->contextid);
1864         // Tag 'bar' context should be updated.
1865         $this->assertEquals($context3->id, $barinstance->contextid);
1866         // There shouldn't be any tag instances left in $context1.
1867         $context1records = $DB->get_records('tag_instance', ['contextid' => $context1->id]);
1868         $this->assertEmpty($context1records);
1869         // There shouldn't be any tag instances left in $context2.
1870         $context2records = $DB->get_records('tag_instance', ['contextid' => $context2->id]);
1871         $this->assertEmpty($context2records);
1872     }
1874     /**
1875      * change_instances_context moving an instance from one context into a context
1876      * that already has an instance of that tag should throw an exception.
1877      */
1878     public function test_change_instances_context_conflicting_instances() {
1879         global $DB;
1881         $tagnames = ['foo'];
1882         $collid = core_tag_collection::get_default();
1883         $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1884         $user1 = $this->getDataGenerator()->create_user();
1885         $user2 = $this->getDataGenerator()->create_user();
1886         $context1 = context_user::instance($user1->id);
1887         $context2 = context_user::instance($user2->id);
1888         $component = 'core';
1889         $itemtype = 'user';
1890         $itemid = 1;
1892         // Two instances of 'foo' in different contexts.
1893         $fooinstance1 = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1894         $fooinstance2 = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context2);
1896         // There is already an instance of 'foo' in $context2 so the code
1897         // should throw an exception when we try to move another instance there.
1898         $this->expectException('Exception');
1899         core_tag_tag::change_instances_context([$fooinstance1->id], $context2);
1900     }
1902     /**
1903      * Help method to return sorted array of names of correlated tags to use for assertions
1904      * @param core_tag $tag
1905      * @return string
1906      */
1907     protected function get_correlated_tags_names($tag) {
1908         $rv = array_map(function($t) {
1909             return $t->rawname;
1910         }, $tag->get_correlated_tags());
1911         sort($rv);
1912         return array_values($rv);
1913     }
1915     /**
1916      * Add a tag instance.
1917      *
1918      * @param core_tag_tag $tag
1919      * @param string $component
1920      * @param string $itemtype
1921      * @param int $itemid
1922      * @param context $context
1923      * @return stdClass
1924      */
1925     protected function add_tag_instance(core_tag_tag $tag, $component, $itemtype, $itemid, $context) {
1926         global $DB;
1927         $record = (array) $tag->to_object();
1928         $record['tagid'] = $record['id'];
1929         $record['component'] = $component;
1930         $record['itemtype'] = $itemtype;
1931         $record['itemid'] = $itemid;
1932         $record['contextid'] = $context->id;
1933         $record['tiuserid'] = 0;
1934         $record['ordering'] = 0;
1935         $record['timecreated'] = time();
1936         $record['id'] = $DB->insert_record('tag_instance', $record);
1937         return (object) $record;
1938     }