Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace core_tag;
18
 
19
use core_tag_area;
20
use core_tag_collection;
21
use core_tag_tag;
1441 ariadna 22
use core_tag;
1 efrain 23
 
24
/**
25
 * Tag related unit tests.
26
 *
27
 * @package core_tag
28
 * @category test
29
 * @copyright 2014 Mark Nelson <markn@moodle.com>
30
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1441 ariadna 31
 * @covers \core_tag_tag
1 efrain 32
 */
1441 ariadna 33
final class taglib_test extends \advanced_testcase {
1 efrain 34
 
35
    /**
36
     * Test set up.
37
     *
38
     * This is executed before running any test in this file.
39
     */
40
    public function setUp(): void {
1441 ariadna 41
        parent::setUp();
1 efrain 42
        $this->resetAfterTest();
43
    }
44
 
45
    /**
46
     * Test the core_tag_tag::add_item_tag() and core_tag_tag::remove_item_tag() functions.
47
     */
11 efrain 48
    public function test_add_remove_item_tag(): void {
1 efrain 49
        global $DB;
50
 
51
        // Create a course to tag.
52
        $course = $this->getDataGenerator()->create_course();
53
 
54
        // Create the tag and tag instance we are going to delete.
55
        core_tag_tag::add_item_tag('core', 'course', $course->id, \context_course::instance($course->id), 'A random tag');
56
 
57
        $this->assertEquals(1, $DB->count_records('tag'));
58
        $this->assertEquals(1, $DB->count_records('tag_instance'));
59
 
60
        // Call the tag_set_delete function.
61
        core_tag_tag::remove_item_tag('core', 'course', $course->id, 'A random tag');
62
 
63
        // Now check that there are no tags or tag instances.
64
        $this->assertEquals(0, $DB->count_records('tag'));
65
        $this->assertEquals(0, $DB->count_records('tag_instance'));
66
    }
67
 
68
    /**
69
     * Test add_item_tag function correctly calculates the ordering for a new tag.
70
     */
11 efrain 71
    public function test_add_tag_ordering_calculation(): void {
1 efrain 72
        global $DB;
73
 
74
        $user1 = $this->getDataGenerator()->create_user();
75
        $course1 = $this->getDataGenerator()->create_course();
76
        $course2 = $this->getDataGenerator()->create_course();
77
        $book1 = $this->getDataGenerator()->create_module('book', array('course' => $course1->id));
78
        $now = time();
79
        $chapter1id = $DB->insert_record('book_chapters', (object) [
80
            'bookid' => $book1->id,
81
            'hidden' => 0,
82
            'timecreated' => $now,
83
            'timemodified' => $now,
84
            'importsrc' => '',
85
            'content' => '',
86
            'contentformat' => FORMAT_HTML,
87
        ]);
88
 
89
        // Create a tag (ordering should start at 1).
90
        $ti1 = core_tag_tag::add_item_tag('core', 'course', $course1->id,
91
            \context_course::instance($course1->id), 'A random tag for course 1');
92
        $this->assertEquals(1, $DB->get_field('tag_instance', 'ordering', ['id' => $ti1]));
93
 
94
        // Create another tag with a common component, itemtype and itemid (should increase the ordering by 1).
95
        $ti2 = core_tag_tag::add_item_tag('core', 'course', $course1->id,
96
            \context_course::instance($course1->id), 'Another random tag for course 1');
97
        $this->assertEquals(2, $DB->get_field('tag_instance', 'ordering', ['id' => $ti2]));
98
 
99
        // Create a new tag with the same component and itemtype, but different itemid (should start counting from 1 again).
100
        $ti3 = core_tag_tag::add_item_tag('core', 'course', $course2->id,
101
            \context_course::instance($course2->id), 'A random tag for course 2');
102
        $this->assertEquals(1, $DB->get_field('tag_instance', 'ordering', ['id' => $ti3]));
103
 
104
        // Create a new tag with a different itemtype (should start counting from 1 again).
105
        $ti4 = core_tag_tag::add_item_tag('core', 'user', $user1->id,
106
            \context_user::instance($user1->id), 'A random tag for user 1');
107
        $this->assertEquals(1, $DB->get_field('tag_instance', 'ordering', ['id' => $ti4]));
108
 
109
        // Create a new tag with a different component (should start counting from 1 again).
110
        $ti5 = core_tag_tag::add_item_tag('mod_book', 'book_chapters', $chapter1id,
111
            \context_module::instance($book1->cmid), 'A random tag for a book chapter');
112
        $this->assertEquals(1, $DB->get_field('tag_instance', 'ordering', ['id' => $ti5]));
113
    }
114
 
115
    /**
116
     * Test the tag cleanup function used by the cron.
117
     */
11 efrain 118
    public function test_tag_cleanup(): void {
1 efrain 119
        global $DB;
120
 
121
        $task = new \core\task\tag_cron_task();
122
 
123
        // Create some users.
124
        $users = array();
125
        for ($i = 0; $i < 10; $i++) {
126
            $users[] = $this->getDataGenerator()->create_user();
127
        }
128
 
129
        // Create a course to tag.
130
        $course = $this->getDataGenerator()->create_course();
131
        $context = \context_course::instance($course->id);
132
 
133
        // Test clean up instances with tags that no longer exist.
134
        $tags = array();
135
        $tagnames = array();
136
        for ($i = 0; $i < 10; $i++) {
137
            $tags[] = $tag = $this->getDataGenerator()->create_tag(array('userid' => $users[0]->id));
138
            $tagnames[] = $tag->rawname;
139
        }
140
        // Create instances with the tags.
141
        core_tag_tag::set_item_tags('core', 'course', $course->id, $context, $tagnames);
142
        // We should now have ten tag instances.
143
        $coursetaginstances = $DB->count_records('tag_instance', array('itemtype' => 'course'));
144
        $this->assertEquals(10, $coursetaginstances);
145
 
146
        // Delete four tags
147
        // Manual delete of tags is done as the function will remove the instances as well.
148
        $DB->delete_records('tag', array('id' => $tags[6]->id));
149
        $DB->delete_records('tag', array('id' => $tags[7]->id));
150
        $DB->delete_records('tag', array('id' => $tags[8]->id));
151
        $DB->delete_records('tag', array('id' => $tags[9]->id));
152
 
153
        // Clean up the tags.
154
        $task->cleanup();
155
        // Check that we now only have six tag_instance records left.
156
        $coursetaginstances = $DB->count_records('tag_instance', array('itemtype' => 'course'));
157
        $this->assertEquals(6, $coursetaginstances);
158
 
159
        // Test clean up with users that have been deleted.
160
        // Create a tag for this course.
161
        foreach ($users as $user) {
162
            $context = \context_user::instance($user->id);
163
            core_tag_tag::set_item_tags('core', 'user', $user->id, $context, array($tags[0]->rawname));
164
        }
165
        $usertags = $DB->count_records('tag_instance', array('itemtype' => 'user'));
166
        $this->assertCount($usertags, $users);
167
        // Remove three students.
168
        // Using the proper function to delete the user will also remove the tags.
169
        $DB->update_record('user', array('id' => $users[4]->id, 'deleted' => 1));
170
        $DB->update_record('user', array('id' => $users[5]->id, 'deleted' => 1));
171
        $DB->update_record('user', array('id' => $users[6]->id, 'deleted' => 1));
172
 
173
        // Clean up the tags.
174
        $task->cleanup();
175
        $usertags = $DB->count_records('tag_instance', array('itemtype' => 'user'));
176
        $usercount = $DB->count_records('user', array('deleted' => 0));
177
        // Remove admin and guest from the count.
178
        $this->assertEquals($usertags, ($usercount - 2));
179
 
180
        // Test clean up where a course has been removed.
181
        // Delete the course. This also needs to be this way otherwise the tags are removed by using the proper function.
182
        $DB->delete_records('course', array('id' => $course->id));
183
        $task->cleanup();
184
        $coursetags = $DB->count_records('tag_instance', array('itemtype' => 'course'));
185
        $this->assertEquals(0, $coursetags);
186
 
187
        // Test clean up where a post has been removed.
188
        // Create default post.
189
        $post = new \stdClass();
190
        $post->userid = $users[1]->id;
191
        $post->content = 'test post content text';
192
        $post->id = $DB->insert_record('post', $post);
193
        $context = \context_system::instance();
194
        core_tag_tag::set_item_tags('core', 'post', $post->id, $context, array($tags[0]->rawname));
195
 
196
        // Add another one with a fake post id to be removed.
197
        core_tag_tag::set_item_tags('core', 'post', 15, $context, array($tags[0]->rawname));
198
        // Check that there are two tag instances.
199
        $posttags = $DB->count_records('tag_instance', array('itemtype' => 'post'));
200
        $this->assertEquals(2, $posttags);
201
        // Clean up the tags.
202
        $task->cleanup();
203
        // We should only have one entry left now.
204
        $posttags = $DB->count_records('tag_instance', array('itemtype' => 'post'));
205
        $this->assertEquals(1, $posttags);
206
    }
207
 
208
    /**
209
     * Test deleting a group of tag instances.
210
     */
11 efrain 211
    public function test_tag_bulk_delete_instances(): void {
1 efrain 212
        global $DB;
213
        $task = new \core\task\tag_cron_task();
214
 
215
        // Setup.
216
        $user = $this->getDataGenerator()->create_user();
217
        $course = $this->getDataGenerator()->create_course();
218
        $context = \context_course::instance($course->id);
219
 
220
        // Create some tag instances.
221
        for ($i = 0; $i < 10; $i++) {
222
            $tag = $this->getDataGenerator()->create_tag(array('userid' => $user->id));
223
            core_tag_tag::add_item_tag('core', 'course', $course->id, $context, $tag->rawname);
224
        }
225
        // Get tag instances. tag name and rawname are required for the event fired in this function.
226
        $sql = "SELECT ti.*, t.name, t.rawname
227
                  FROM {tag_instance} ti
228
                  JOIN {tag} t ON t.id = ti.tagid";
229
        $taginstances = $DB->get_records_sql($sql);
230
        $this->assertCount(10, $taginstances);
231
        // Run the function.
232
        $task->bulk_delete_instances($taginstances);
233
        // Make sure they are gone.
234
        $instancecount = $DB->count_records('tag_instance');
235
        $this->assertEquals(0, $instancecount);
236
    }
237
 
238
    /**
239
     * Test that setting a list of tags for "tag" item type throws exception if userid specified
240
     */
241
    public function test_set_item_tags_with_invalid_userid(): void {
242
        $user = $this->getDataGenerator()->create_user();
243
 
244
        $this->expectException(\coding_exception::class);
245
        $this->expectExceptionMessage('Related tags can not have tag instance userid');
246
        core_tag_tag::set_item_tags('core', 'tag', 1, \context_system::instance(), ['all', 'night', 'long'], $user->id);
247
    }
248
 
249
    /**
250
     * Prepares environment for testing tag correlations
251
     * @return core_tag_tag[] list of used tags
252
     */
253
    protected function prepare_correlated() {
254
        global $DB;
255
 
256
        $user = $this->getDataGenerator()->create_user();
257
        $this->setUser($user);
258
 
259
        $user1 = $this->getDataGenerator()->create_user();
260
        $user2 = $this->getDataGenerator()->create_user();
261
        $user3 = $this->getDataGenerator()->create_user();
262
        $user4 = $this->getDataGenerator()->create_user();
263
        $user5 = $this->getDataGenerator()->create_user();
264
        $user6 = $this->getDataGenerator()->create_user();
265
 
266
        // Several records have both 'cat' and 'cats' tags attached to them.
267
        // This will make those tags automatically correlated.
268
        // Same with 'dog', 'dogs' and 'puppy.
269
        core_tag_tag::set_item_tags('core', 'user', $user1->id, \context_user::instance($user1->id), array('cat', 'cats'));
270
        core_tag_tag::set_item_tags('core', 'user', $user2->id, \context_user::instance($user2->id), array('cat', 'cats', 'kitten'));
271
        core_tag_tag::set_item_tags('core', 'user', $user3->id, \context_user::instance($user3->id), array('cat', 'cats'));
272
        core_tag_tag::set_item_tags('core', 'user', $user4->id, \context_user::instance($user4->id), array('dog', 'dogs', 'puppy'));
273
        core_tag_tag::set_item_tags('core', 'user', $user5->id, \context_user::instance($user5->id), array('dog', 'dogs', 'puppy'));
274
        core_tag_tag::set_item_tags('core', 'user', $user6->id, \context_user::instance($user6->id), array('dog', 'dogs', 'puppy'));
275
        $tags = core_tag_tag::get_by_name_bulk(core_tag_collection::get_default(),
276
            array('cat', 'cats', 'dog', 'dogs', 'kitten', 'puppy'), '*');
277
 
278
        // Add manual relation between tags 'cat' and 'kitten'.
279
        core_tag_tag::get($tags['cat']->id)->set_related_tags(array('kitten'));
280
 
281
        return $tags;
282
    }
283
 
284
    /**
285
     * Test for function compute_correlations() that is part of tag cron
286
     */
11 efrain 287
    public function test_correlations(): void {
1 efrain 288
        global $DB;
289
        $task = new \core\task\tag_cron_task();
290
 
291
        $tags = array_map(function ($t) {
292
            return $t->id;
293
        }, $this->prepare_correlated());
294
 
295
        $task->compute_correlations();
296
 
297
        $this->assertEquals($tags['cats'],
298
            $DB->get_field_select('tag_correlation', 'correlatedtags',
299
                'tagid = ?', array($tags['cat'])));
300
        $this->assertEquals($tags['cat'],
301
            $DB->get_field_select('tag_correlation', 'correlatedtags',
302
                'tagid = ?', array($tags['cats'])));
303
        $this->assertEquals($tags['dogs'] . ',' . $tags['puppy'],
304
            $DB->get_field_select('tag_correlation', 'correlatedtags',
305
                'tagid = ?', array($tags['dog'])));
306
        $this->assertEquals($tags['dog'] . ',' . $tags['puppy'],
307
            $DB->get_field_select('tag_correlation', 'correlatedtags',
308
                'tagid = ?', array($tags['dogs'])));
309
        $this->assertEquals($tags['dog'] . ',' . $tags['dogs'],
310
            $DB->get_field_select('tag_correlation', 'correlatedtags',
311
                'tagid = ?', array($tags['puppy'])));
312
 
313
        // Make sure get_correlated_tags() returns 'cats' as the only correlated tag to the 'cat'.
314
        $correlatedtags = array_values(core_tag_tag::get($tags['cat'])->get_correlated_tags(true));
315
        $this->assertCount(3, $correlatedtags); // This will return all existing instances but they all point to the same tag.
316
        $this->assertEquals('cats', $correlatedtags[0]->rawname);
317
        $this->assertEquals('cats', $correlatedtags[1]->rawname);
318
        $this->assertEquals('cats', $correlatedtags[2]->rawname);
319
 
320
        $correlatedtags = array_values(core_tag_tag::get($tags['cat'])->get_correlated_tags());
321
        $this->assertCount(1, $correlatedtags); // Duplicates are filtered out here.
322
        $this->assertEquals('cats', $correlatedtags[0]->rawname);
323
 
324
        // Make sure get_correlated_tags() returns 'dogs' and 'puppy' as the correlated tags to the 'dog'.
325
        $correlatedtags = core_tag_tag::get($tags['dog'])->get_correlated_tags(true);
326
        $this->assertCount(6, $correlatedtags); // 2 tags times 3 instances.
327
 
328
        $correlatedtags = array_values(core_tag_tag::get($tags['dog'])->get_correlated_tags());
329
        $this->assertCount(2, $correlatedtags);
330
        $this->assertEquals('dogs', $correlatedtags[0]->rawname);
331
        $this->assertEquals('puppy', $correlatedtags[1]->rawname);
332
 
333
        // Function get_related_tags() will return both related and correlated tags.
334
        $relatedtags = array_values(core_tag_tag::get($tags['cat'])->get_related_tags());
335
        $this->assertCount(2, $relatedtags);
336
        $this->assertEquals('kitten', $relatedtags[0]->rawname);
337
        $this->assertEquals('cats', $relatedtags[1]->rawname);
338
 
339
        // Also test get_correlated_tags().
340
        $correlatedtags = array_values(core_tag_tag::get($tags['cat'])->get_correlated_tags(true));
341
        $this->assertCount(3, $correlatedtags); // This will return all existing instances but they all point to the same tag.
342
        $this->assertEquals('cats', $correlatedtags[0]->rawname);
343
        $this->assertEquals('cats', $correlatedtags[1]->rawname);
344
        $this->assertEquals('cats', $correlatedtags[2]->rawname);
345
 
346
        $correlatedtags = array_values(core_tag_tag::get($tags['cat'])->get_correlated_tags());
347
        $this->assertCount(1, $correlatedtags); // Duplicates are filtered out here.
348
        $this->assertEquals('cats', $correlatedtags[0]->rawname);
349
 
350
        $correlatedtags = array_values(core_tag_tag::get($tags['dog'])->get_correlated_tags(true));
351
        $this->assertCount(6, $correlatedtags); // 2 tags times 3 instances.
352
 
353
        $correlatedtags = array_values(core_tag_tag::get($tags['dog'])->get_correlated_tags());
354
        $this->assertCount(2, $correlatedtags);
355
        $this->assertEquals('dogs', $correlatedtags[0]->rawname);
356
        $this->assertEquals('puppy', $correlatedtags[1]->rawname);
357
 
358
        $relatedtags = array_values(core_tag_tag::get($tags['cat'])->get_related_tags());
359
        $this->assertCount(2, $relatedtags);
360
        $this->assertEquals('kitten', $relatedtags[0]->rawname);
361
        $this->assertEquals('cats', $relatedtags[1]->rawname);
362
        // End of testing deprecated methods.
363
 
364
        // If we then manually set 'cat' and 'cats' as related, get_related_tags() will filter out duplicates.
365
        core_tag_tag::get($tags['cat'])->set_related_tags(array('kitten', 'cats'));
366
 
367
        $relatedtags = array_values(core_tag_tag::get($tags['cat'])->get_related_tags());
368
        $this->assertCount(2, $relatedtags);
369
        $this->assertEquals('kitten', $relatedtags[0]->rawname);
370
        $this->assertEquals('cats', $relatedtags[1]->rawname);
371
 
372
        // Make sure core_tag_tag::get_item_tags(), core_tag_tag::get_correlated_tags() return the same set of fields.
373
        $relatedtags = core_tag_tag::get_item_tags('core', 'tag', $tags['cat']);
374
        $relatedtag = reset($relatedtags);
375
        $correlatedtags = core_tag_tag::get($tags['cat'])->get_correlated_tags();
376
        $correlatedtag = reset($correlatedtags);
377
        $this->assertEquals(array_keys((array)$relatedtag->to_object()), array_keys((array)$correlatedtag->to_object()));
378
 
379
        $relatedtags = core_tag_tag::get_item_tags(null, 'tag', $tags['cat']);
380
        $relatedtag = reset($relatedtags);
381
        $correlatedtags = core_tag_tag::get($tags['cat'])->get_correlated_tags();
382
        $correlatedtag = reset($correlatedtags);
383
        $this->assertEquals(array_keys((array)$relatedtag), array_keys((array)$correlatedtag));
384
    }
385
 
386
    /**
387
     * Test for function cleanup() that is part of tag cron
388
     */
11 efrain 389
    public function test_cleanup(): void {
1 efrain 390
        global $DB;
391
        $task = new \core\task\tag_cron_task();
392
 
393
        $user = $this->getDataGenerator()->create_user();
394
        $defaultcoll = core_tag_collection::get_default();
395
 
396
        // Setting tags will create non-standard tags 'cat', 'dog' and 'fish'.
397
        core_tag_tag::set_item_tags('core', 'user', $user->id, \context_user::instance($user->id), array('cat', 'dog', 'fish'));
398
 
399
        $this->assertTrue($DB->record_exists('tag', array('name' => 'cat')));
400
        $this->assertTrue($DB->record_exists('tag', array('name' => 'dog')));
401
        $this->assertTrue($DB->record_exists('tag', array('name' => 'fish')));
402
 
403
        // Make tag 'dog' standard.
404
        $dogtag = core_tag_tag::get_by_name($defaultcoll, 'dog', '*');
405
        $fishtag = core_tag_tag::get_by_name($defaultcoll, 'fish');
406
        $dogtag->update(array('isstandard' => 1));
407
 
408
        // Manually remove the instances pointing on tags 'dog' and 'fish'.
409
        $DB->execute('DELETE FROM {tag_instance} WHERE tagid in (?,?)', array($dogtag->id, $fishtag->id));
410
 
411
        $task->cleanup();
412
 
413
        // Tag 'cat' is still present because it's used. Tag 'dog' is present because it's standard.
414
        // Tag 'fish' was removed because it is not standard and it is no longer used by anybody.
415
        $this->assertTrue($DB->record_exists('tag', array('name' => 'cat')));
416
        $this->assertTrue($DB->record_exists('tag', array('name' => 'dog')));
417
        $this->assertFalse($DB->record_exists('tag', array('name' => 'fish')));
418
 
419
        // Delete user without using API function.
420
        $DB->update_record('user', array('id' => $user->id, 'deleted' => 1));
421
 
422
        $task->cleanup();
423
 
424
        // Tag 'cat' was now deleted too.
425
        $this->assertFalse($DB->record_exists('tag', array('name' => 'cat')));
426
 
427
        // Assign tag to non-existing record. Make sure tag was created in the DB.
428
        core_tag_tag::set_item_tags('core', 'course', 1231231, \context_system::instance(), array('bird'));
429
        $this->assertTrue($DB->record_exists('tag', array('name' => 'bird')));
430
 
431
        $task->cleanup();
432
 
433
        // Tag 'bird' was now deleted because the related record does not exist in the DB.
434
        $this->assertFalse($DB->record_exists('tag', array('name' => 'bird')));
435
 
436
        // Now we have a tag instance pointing on 'sometag' tag.
437
        $user = $this->getDataGenerator()->create_user();
438
        core_tag_tag::set_item_tags('core', 'user', $user->id, \context_user::instance($user->id), array('sometag'));
439
        $sometag = core_tag_tag::get_by_name($defaultcoll, 'sometag');
440
 
441
        $this->assertTrue($DB->record_exists('tag_instance', array('tagid' => $sometag->id)));
442
 
443
        // Some hacker removes the tag without using API.
444
        $DB->delete_records('tag', array('id' => $sometag->id));
445
 
446
        $task->cleanup();
447
 
448
        // The tag instances were also removed.
449
        $this->assertFalse($DB->record_exists('tag_instance', array('tagid' => $sometag->id)));
450
    }
451
 
11 efrain 452
    public function test_guess_tag(): void {
1 efrain 453
        global $DB;
454
        $user = $this->getDataGenerator()->create_user();
455
        $this->setUser($user);
456
        $tag1 = $this->getDataGenerator()->create_tag(array('name' => 'Cat'));
457
        $tc = core_tag_collection::create((object)array('name' => 'tagcoll'));
458
        $tag2 = $this->getDataGenerator()->create_tag(array('name' => 'Cat', 'tagcollid' => $tc->id));
459
        $this->assertEquals(2, count($DB->get_records('tag')));
460
        $this->assertEquals(2, count(core_tag_tag::guess_by_name('Cat')));
461
        $this->assertEquals(core_tag_collection::get_default(), core_tag_tag::get_by_name(0, 'Cat')->tagcollid);
462
    }
463
 
11 efrain 464
    public function test_instances(): void {
1 efrain 465
        global $DB;
466
        $user = $this->getDataGenerator()->create_user();
467
        $this->setUser($user);
468
 
469
        // Create a course to tag.
470
        $course = $this->getDataGenerator()->create_course();
471
        $context = \context_course::instance($course->id);
472
 
473
        $initialtagscount = $DB->count_records('tag');
474
 
475
        core_tag_tag::set_item_tags('core', 'course', $course->id, $context, array('Tag 1', 'Tag 2'));
476
        $tags = core_tag_tag::get_item_tags('core', 'course', $course->id);
477
        $tagssimple = array_values($tags);
478
        $this->assertEquals(2, count($tags));
479
        $this->assertEquals('Tag 1', $tagssimple[0]->rawname);
480
        $this->assertEquals('Tag 2', $tagssimple[1]->rawname);
481
        $this->assertEquals($initialtagscount + 2, $DB->count_records('tag'));
482
 
483
        core_tag_tag::set_item_tags('core', 'course', $course->id, $context, array('Tag 3', 'Tag 2', 'Tag 1'));
484
        $tags = core_tag_tag::get_item_tags('core', 'course', $course->id);
485
        $tagssimple = array_values($tags);
486
        $this->assertEquals(3, count($tags));
487
        $this->assertEquals('Tag 3', $tagssimple[0]->rawname);
488
        $this->assertEquals('Tag 2', $tagssimple[1]->rawname);
489
        $this->assertEquals('Tag 1', $tagssimple[2]->rawname);
490
        $this->assertEquals($initialtagscount + 3, $DB->count_records('tag'));
491
 
492
        core_tag_tag::set_item_tags('core', 'course', $course->id, $context, array('Tag 3'));
493
        $tags = core_tag_tag::get_item_tags('core', 'course', $course->id);
494
        $tagssimple = array_values($tags);
495
        $this->assertEquals(1, count($tags));
496
        $this->assertEquals('Tag 3', $tagssimple[0]->rawname);
497
 
498
        // Make sure the unused tags were removed from tag table.
499
        $this->assertEquals($initialtagscount + 1, $DB->count_records('tag'));
500
    }
501
 
11 efrain 502
    public function test_related_tags(): void {
1 efrain 503
        global $DB;
504
        $user = $this->getDataGenerator()->create_user();
505
        $this->setUser($user);
506
        $tagcollid = core_tag_collection::get_default();
507
        $tag = $this->getDataGenerator()->create_tag(array('$tagcollid' => $tagcollid, 'rawname' => 'My tag'));
508
        $tag = core_tag_tag::get($tag->id, '*');
509
 
510
        $tag->set_related_tags(array('Synonym 1', 'Synonym 2'));
511
        $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $tag->id));
512
        $this->assertEquals(2, count($relatedtags));
513
        $this->assertEquals('Synonym 1', $relatedtags[0]->rawname);
514
        $this->assertEquals('Synonym 2', $relatedtags[1]->rawname);
515
 
516
        $t1 = core_tag_tag::get_by_name($tagcollid, 'Synonym 1', '*');
517
        $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $t1->id));
518
        $this->assertEquals(1, count($relatedtags));
519
        $this->assertEquals('My tag', $relatedtags[0]->rawname);
520
 
521
        $t2 = core_tag_tag::get_by_name($tagcollid, 'Synonym 2', '*');
522
        $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $t2->id));
523
        $this->assertEquals(1, count($relatedtags));
524
        $this->assertEquals('My tag', $relatedtags[0]->rawname);
525
 
526
        $tag->set_related_tags(array('Synonym 3', 'Synonym 2', 'Synonym 1'));
527
        $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $tag->id));
528
        $this->assertEquals(3, count($relatedtags));
529
        $this->assertEquals('Synonym 1', $relatedtags[0]->rawname);
530
        $this->assertEquals('Synonym 2', $relatedtags[1]->rawname);
531
        $this->assertEquals('Synonym 3', $relatedtags[2]->rawname);
532
 
533
        $t3 = core_tag_tag::get_by_name($tagcollid, 'Synonym 3', '*');
534
        $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $t3->id));
535
        $this->assertEquals(1, count($relatedtags));
536
        $this->assertEquals('My tag', $relatedtags[0]->rawname);
537
 
538
        $tag->set_related_tags(array('Synonym 3', 'Synonym 2'));
539
        $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $tag->id));
540
        $this->assertEquals(2, count($relatedtags));
541
        $this->assertEquals('Synonym 2', $relatedtags[0]->rawname);
542
        $this->assertEquals('Synonym 3', $relatedtags[1]->rawname);
543
 
544
        // Assert "Synonym 1" no longer links but is still present (will be removed by cron).
545
        $relatedtags = array_values(core_tag_tag::get_item_tags('core', 'tag', $t1->id));
546
        $this->assertEquals(0, count($relatedtags));
547
    }
548
 
549
    /**
550
     * Very basic test for create/move/update/delete actions, without any itemtype movements.
551
     */
11 efrain 552
    public function test_tag_coll_basic(): void {
1 efrain 553
        global $DB;
554
 
555
        // Make sure there is one and only one tag coll that is marked as default.
556
        $tagcolls = core_tag_collection::get_collections();
557
        $this->assertEquals(1, count($DB->get_records('tag_coll', array('isdefault' => 1))));
558
        $defaulttagcoll = core_tag_collection::get_default();
559
 
560
        // Create a new tag coll to store user tags and something else.
561
        $data = (object)array('name' => 'new tag coll');
562
        $tagcollid1 = core_tag_collection::create($data)->id;
563
        $tagcolls = core_tag_collection::get_collections();
564
        $this->assertEquals('new tag coll', $tagcolls[$tagcollid1]->name);
565
 
566
        // Create a new tag coll to store post tags.
567
        $data = (object)array('name' => 'posts');
568
        $tagcollid2 = core_tag_collection::create($data)->id;
569
        $tagcolls = core_tag_collection::get_collections();
570
        $this->assertEquals('posts', $tagcolls[$tagcollid2]->name);
571
        $this->assertEquals($tagcolls[$tagcollid1]->sortorder + 1,
572
            $tagcolls[$tagcollid2]->sortorder);
573
 
574
        // Illegal tag colls sortorder changing.
575
        $this->assertFalse(core_tag_collection::change_sortorder($tagcolls[$defaulttagcoll], 1));
576
        $this->assertFalse(core_tag_collection::change_sortorder($tagcolls[$defaulttagcoll], -1));
577
        $this->assertFalse(core_tag_collection::change_sortorder($tagcolls[$tagcollid2], 1));
578
 
579
        // Move the very last tag coll one position up.
580
        $this->assertTrue(core_tag_collection::change_sortorder($tagcolls[$tagcollid2], -1));
581
        $tagcolls = core_tag_collection::get_collections();
582
        $this->assertEquals($tagcolls[$tagcollid2]->sortorder + 1,
583
            $tagcolls[$tagcollid1]->sortorder);
584
 
585
        // Move the second last tag coll one position down.
586
        $this->assertTrue(core_tag_collection::change_sortorder($tagcolls[$tagcollid2], 1));
587
        $tagcolls = core_tag_collection::get_collections();
588
        $this->assertEquals($tagcolls[$tagcollid1]->sortorder + 1,
589
            $tagcolls[$tagcollid2]->sortorder);
590
 
591
        // Edit tag coll.
592
        $this->assertTrue(core_tag_collection::update($tagcolls[$tagcollid2],
593
            (object)array('name' => 'posts2')));
594
        $tagcolls = core_tag_collection::get_collections();
595
        $this->assertEquals('posts2', $tagcolls[$tagcollid2]->name);
596
 
597
        // Delete tag coll.
598
        $count = $DB->count_records('tag_coll');
599
        $this->assertFalse(core_tag_collection::delete($tagcolls[$defaulttagcoll]));
600
        $this->assertTrue(core_tag_collection::delete($tagcolls[$tagcollid1]));
601
        $this->assertEquals($count - 1, $DB->count_records('tag_coll'));
602
    }
603
 
604
    /**
605
     * Prepares environment for test_move_tags_* tests
606
     */
607
    protected function prepare_move_tags() {
608
        global $CFG;
609
        require_once($CFG->dirroot.'/blog/locallib.php');
610
        $this->setUser($this->getDataGenerator()->create_user());
611
 
612
        $collid1 = core_tag_collection::get_default();
613
        $collid2 = core_tag_collection::create(array('name' => 'newcoll'))->id;
614
        $user1 = $this->getDataGenerator()->create_user();
615
        $user2 = $this->getDataGenerator()->create_user();
616
        $blogpost = new \blog_entry(null, array('subject' => 'test'), null);
617
        $states = \blog_entry::get_applicable_publish_states();
618
        $blogpost->publishstate = reset($states);
619
        $blogpost->add();
620
 
621
        core_tag_tag::set_item_tags('core', 'user', $user1->id, \context_user::instance($user1->id),
622
                array('Tag1', 'Tag2'));
623
        core_tag_tag::set_item_tags('core', 'user', $user2->id, \context_user::instance($user2->id),
624
                array('Tag2', 'Tag3'));
625
        $this->getDataGenerator()->create_tag(array('rawname' => 'Tag4',
626
            'tagcollid' => $collid1, 'isstandard' => 1));
627
        $this->getDataGenerator()->create_tag(array('rawname' => 'Tag5',
628
            'tagcollid' => $collid2, 'isstandard' => 1));
629
 
630
        return array($collid1, $collid2, $user1, $user2, $blogpost);
631
    }
632
 
11 efrain 633
    public function test_move_tags_simple(): void {
1 efrain 634
        global $DB;
635
        list($collid1, $collid2, $user1, $user2, $blogpost) = $this->prepare_move_tags();
636
 
637
        // Move 'user' area from collection 1 to collection 2, make sure tags were moved completely.
638
        $tagarea = $DB->get_record('tag_area', array('itemtype' => 'user', 'component' => 'core'));
639
        core_tag_area::update($tagarea, array('tagcollid' => $collid2));
640
 
641
        $tagsaftermove = $DB->get_records('tag');
642
        foreach ($tagsaftermove as $tag) {
643
            // Confirm that the time modified has not been unset.
644
            $this->assertNotEmpty($tag->timemodified);
645
        }
646
 
647
        $this->assertEquals(array('Tag4'),
648
                $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid1)));
649
        $this->assertEquals(array('Tag1', 'Tag2', 'Tag3', 'Tag5'),
650
                $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid2)));
651
        $this->assertEquals(array('Tag1', 'Tag2'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user1->id)));
652
        $this->assertEquals(array('Tag2', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user2->id)));
653
    }
654
 
11 efrain 655
    public function test_move_tags_split_tag(): void {
1 efrain 656
        global $DB;
657
        list($collid1, $collid2, $user1, $user2, $blogpost) = $this->prepare_move_tags();
658
 
659
        core_tag_tag::set_item_tags('core', 'post', $blogpost->id, \context_system::instance(),
660
                array('Tag1', 'Tag3'));
661
 
662
        // Move 'user' area from collection 1 to collection 2, make sure tag Tag2 was moved and tags Tag1 and Tag3 were duplicated.
663
        $tagareauser = $DB->get_record('tag_area', array('itemtype' => 'user', 'component' => 'core'));
664
        core_tag_area::update($tagareauser, array('tagcollid' => $collid2));
665
 
666
        $tagsaftermove = $DB->get_records('tag');
667
        foreach ($tagsaftermove as $tag) {
668
            // Confirm that the time modified has not been unset.
669
            $this->assertNotEmpty($tag->timemodified);
670
        }
671
 
672
        $this->assertEquals(array('Tag1', 'Tag3', 'Tag4'),
673
                $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid1)));
674
        $this->assertEquals(array('Tag1', 'Tag2', 'Tag3', 'Tag5'),
675
                $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid2)));
676
        $this->assertEquals(array('Tag1', 'Tag2'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user1->id)));
677
        $this->assertEquals(array('Tag2', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user2->id)));
678
        $this->assertEquals(array('Tag1', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'post', $blogpost->id)));
679
    }
680
 
11 efrain 681
    public function test_move_tags_merge_tag(): void {
1 efrain 682
        global $DB;
683
        list($collid1, $collid2, $user1, $user2, $blogpost) = $this->prepare_move_tags();
684
 
685
        // Set collection for 'post' tag area to be collection 2 and add some tags there.
686
        $tagareablog = $DB->get_record('tag_area', array('itemtype' => 'post', 'component' => 'core'));
687
        core_tag_area::update($tagareablog, array('tagcollid' => $collid2));
688
 
689
        core_tag_tag::set_item_tags('core', 'post', $blogpost->id, \context_system::instance(),
690
                array('TAG1', 'Tag3'));
691
 
692
        // Move 'user' area from collection 1 to collection 2,
693
        // make sure tag Tag2 was moved and tags Tag1 and Tag3 were merged into existing.
694
        $tagareauser = $DB->get_record('tag_area', array('itemtype' => 'user', 'component' => 'core'));
695
        core_tag_area::update($tagareauser, array('tagcollid' => $collid2));
696
 
697
        $tagsaftermove = $DB->get_records('tag');
698
        foreach ($tagsaftermove as $tag) {
699
            // Confirm that the time modified has not been unset.
700
            $this->assertNotEmpty($tag->timemodified);
701
        }
702
 
703
        $this->assertEquals(array('Tag4'),
704
                $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid1)));
705
        $this->assertEquals(array('TAG1', 'Tag2', 'Tag3', 'Tag5'),
706
                $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid2)));
707
        $this->assertEquals(array('TAG1', 'Tag2'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user1->id)));
708
        $this->assertEquals(array('Tag2', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user2->id)));
709
        $this->assertEquals(array('TAG1', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'post', $blogpost->id)));
710
    }
711
 
11 efrain 712
    public function test_move_tags_with_related(): void {
1 efrain 713
        global $DB;
714
        list($collid1, $collid2, $user1, $user2, $blogpost) = $this->prepare_move_tags();
715
 
716
        // Set Tag1 to be related to Tag2 and Tag4 (in collection 1).
717
        core_tag_tag::get_by_name($collid1, 'Tag1')->set_related_tags(array('Tag2', 'Tag4'));
718
 
719
        // Set collection for 'post' tag area to be collection 2 and add some tags there.
720
        $tagareablog = $DB->get_record('tag_area', array('itemtype' => 'post', 'component' => 'core'));
721
        core_tag_area::update($tagareablog, array('tagcollid' => $collid2));
722
 
723
        core_tag_tag::set_item_tags('core', 'post', $blogpost->id, \context_system::instance(),
724
                array('TAG1', 'Tag3'));
725
 
726
        // Move 'user' area from collection 1 to collection 2, make sure tags were moved completely.
727
        $tagarea = $DB->get_record('tag_area', array('itemtype' => 'user', 'component' => 'core'));
728
        core_tag_area::update($tagarea, array('tagcollid' => $collid2));
729
 
730
        $tagsaftermove = $DB->get_records('tag');
731
        foreach ($tagsaftermove as $tag) {
732
            // Confirm that the time modified has not been unset.
733
            $this->assertNotEmpty($tag->timemodified);
734
        }
735
 
736
        $this->assertEquals(array('Tag1', 'Tag2', 'Tag4'),
737
                $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid1)));
738
        $this->assertEquals(array('TAG1', 'Tag2', 'Tag3', 'Tag4', 'Tag5'),
739
                $DB->get_fieldset_select('tag', 'rawname', 'tagcollid = ? ORDER BY name', array($collid2)));
740
        $this->assertEquals(array('TAG1', 'Tag2'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user1->id)));
741
        $this->assertEquals(array('Tag2', 'Tag3'), array_values(core_tag_tag::get_item_tags_array('core', 'user', $user2->id)));
742
 
743
        $tag11 = core_tag_tag::get_by_name($collid1, 'Tag1');
744
        $related11 = core_tag_tag::get($tag11->id)->get_manual_related_tags();
745
        $related11 = array_map('core_tag_tag::make_display_name', $related11);
746
        sort($related11); // Order of related tags may be random.
747
        $this->assertEquals('Tag2, Tag4', join(', ', $related11));
748
 
749
        $tag21 = core_tag_tag::get_by_name($collid2, 'TAG1');
750
        $related21 = core_tag_tag::get($tag21->id)->get_manual_related_tags();
751
        $related21 = array_map('core_tag_tag::make_display_name', $related21);
752
        sort($related21); // Order of related tags may be random.
753
        $this->assertEquals('Tag2, Tag4', join(', ', $related21));
754
    }
755
 
11 efrain 756
    public function test_move_tags_corrupted(): void {
1 efrain 757
        global $DB;
758
        list($collid1, $collid2, $user1, $user2, $blogpost) = $this->prepare_move_tags();
759
        $collid3 = core_tag_collection::create(array('name' => 'weirdcoll'))->id;
760
 
761
        // We already have Tag1 in coll1, now let's create it in coll3.
762
        $extratag1 = $this->getDataGenerator()->create_tag(array('rawname' => 'Tag1',
763
            'tagcollid' => $collid3, 'isstandard' => 1));
764
 
765
        // Artificially add 'Tag1' from coll3 to user2.
766
        $DB->insert_record('tag_instance', array('tagid' => $extratag1->id, 'itemtype' => 'user',
767
            'component' => 'core', 'itemid' => $user2->id, 'ordering' => 3));
768
 
769
        // Now we have corrupted data: both users are tagged with 'Tag1', however these are two tags in different collections.
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->assertNotEquals($user1tags[0]->tagcollid, $user2tags[2]->tagcollid);
775
 
776
        // Move user interests tag area into coll2.
777
        $tagarea = $DB->get_record('tag_area', array('itemtype' => 'user', 'component' => 'core'));
778
        core_tag_area::update($tagarea, array('tagcollid' => $collid2));
779
 
780
        $tagsaftermove = $DB->get_records('tag');
781
        foreach ($tagsaftermove as $tag) {
782
            // Confirm that the time modified has not been unset.
783
            $this->assertNotEmpty($tag->timemodified);
784
        }
785
 
786
        // Now all tags are correctly moved to the new collection and both tags 'Tag1' were merged.
787
        $user1tags = array_values(core_tag_tag::get_item_tags('core', 'user', $user1->id));
788
        $user2tags = array_values(core_tag_tag::get_item_tags('core', 'user', $user2->id));
789
        $this->assertEquals('Tag1', $user1tags[0]->rawname);
790
        $this->assertEquals('Tag1', $user2tags[2]->rawname);
791
        $this->assertEquals($collid2, $user1tags[0]->tagcollid);
792
        $this->assertEquals($collid2, $user2tags[2]->tagcollid);
793
    }
794
 
795
    /**
796
     * Test functions core_tag_tag::create_if_missing() and core_tag_tag::get_by_name_bulk().
797
     */
11 efrain 798
    public function test_create_get(): void {
1 efrain 799
        $tagset = array('Cat', ' Dog  ', '<Mouse', '<>', 'mouse', 'Dog');
800
 
801
        $collid = core_tag_collection::get_default();
802
        $tags = core_tag_tag::create_if_missing($collid, $tagset);
803
        $this->assertEquals(array('cat', 'dog', 'mouse'), array_keys($tags));
804
        $this->assertEquals('Dog', $tags['dog']->rawname);
805
        $this->assertEquals('mouse', $tags['mouse']->rawname); // Case of the last tag wins.
806
 
807
        $tags2 = core_tag_tag::create_if_missing($collid, array('CAT', 'Elephant'));
808
        $this->assertEquals(array('cat', 'elephant'), array_keys($tags2));
809
        $this->assertEquals('Cat', $tags2['cat']->rawname);
810
        $this->assertEquals('Elephant', $tags2['elephant']->rawname);
811
        $this->assertEquals($tags['cat']->id, $tags2['cat']->id); // Tag 'cat' already existed and was not created again.
812
 
813
        $tags3 = core_tag_tag::get_by_name_bulk($collid, $tagset);
814
        $this->assertEquals(array('cat', 'dog', 'mouse'), array_keys($tags3));
815
        $this->assertEquals('Dog', $tags3['dog']->rawname);
816
        $this->assertEquals('mouse', $tags3['mouse']->rawname);
817
 
818
    }
819
 
820
    /**
821
     * Testing function core_tag_tag::combine_tags()
822
     */
11 efrain 823
    public function test_combine_tags(): void {
1 efrain 824
        $initialtags = array(
825
            array('Cat', 'Dog'),
826
            array('Dog', 'Cat'),
827
            array('Cats', 'Hippo'),
828
            array('Hippo', 'Cats'),
829
            array('Cat', 'Mouse', 'Kitten'),
830
            array('Cats', 'Mouse', 'Kitten'),
831
            array('Kitten', 'Mouse', 'Cat'),
832
            array('Kitten', 'Mouse', 'Cats'),
833
            array('Cats', 'Mouse', 'Kitten'),
834
            array('Mouse', 'Hippo')
835
        );
836
 
837
        $finaltags = array(
838
            array('Cat', 'Dog'),
839
            array('Dog', 'Cat'),
840
            array('Cat', 'Hippo'),
841
            array('Hippo', 'Cat'),
842
            array('Cat', 'Mouse'),
843
            array('Cat', 'Mouse'),
844
            array('Mouse', 'Cat'),
845
            array('Mouse', 'Cat'),
846
            array('Cat', 'Mouse'),
847
            array('Mouse', 'Hippo')
848
        );
849
 
850
        $collid = core_tag_collection::get_default();
851
        $context = \context_system::instance();
852
        foreach ($initialtags as $id => $taglist) {
853
            core_tag_tag::set_item_tags('core', 'course', $id + 10, $context, $initialtags[$id]);
854
        }
855
 
856
        core_tag_tag::get_by_name($collid, 'Cats', '*')->update(array('isstandard' => 1));
857
 
858
        // Combine tags 'Cats' and 'Kitten' into 'Cat'.
859
        $cat = core_tag_tag::get_by_name($collid, 'Cat', '*');
860
        $cats = core_tag_tag::get_by_name($collid, 'Cats', '*');
861
        $kitten = core_tag_tag::get_by_name($collid, 'Kitten', '*');
862
        $cat->combine_tags(array($cats, $kitten));
863
 
864
        foreach ($finaltags as $id => $taglist) {
865
            $this->assertEquals($taglist,
866
                array_values(core_tag_tag::get_item_tags_array('core', 'course', $id + 10)),
867
                    'Original array ('.join(', ', $initialtags[$id]).')');
868
        }
869
 
870
        // Ensure combined tags are deleted and 'Cat' is now official (because 'Cats' was official).
871
        $this->assertEmpty(core_tag_tag::get_by_name($collid, 'Cats'));
872
        $this->assertEmpty(core_tag_tag::get_by_name($collid, 'Kitten'));
873
        $cattag = core_tag_tag::get_by_name($collid, 'Cat', '*');
874
        $this->assertEquals(1, $cattag->isstandard);
875
    }
876
 
877
    /**
878
     * Testing function core_tag_tag::combine_tags() when related tags are present.
879
     */
11 efrain 880
    public function test_combine_tags_with_related(): void {
1 efrain 881
        $collid = core_tag_collection::get_default();
882
        $context = \context_system::instance();
883
        core_tag_tag::set_item_tags('core', 'course', 10, $context, array('Cat', 'Cats', 'Dog'));
884
        core_tag_tag::get_by_name($collid, 'Cat', '*')->set_related_tags(array('Kitty'));
885
        core_tag_tag::get_by_name($collid, 'Cats', '*')->set_related_tags(array('Cat', 'Kitten', 'Kitty'));
886
 
887
        // Combine tags 'Cats' into 'Cat'.
888
        $cat = core_tag_tag::get_by_name($collid, 'Cat', '*');
889
        $cats = core_tag_tag::get_by_name($collid, 'Cats', '*');
890
        $cat->combine_tags(array($cats));
891
 
892
        // Ensure 'Cat' is now related to 'Kitten' and 'Kitty' (order of related tags may be random).
893
        $relatedtags = array_map(function($t) {return $t->rawname;}, $cat->get_manual_related_tags());
894
        sort($relatedtags);
895
        $this->assertEquals(array('Kitten', 'Kitty'), array_values($relatedtags));
896
    }
897
 
898
    /**
899
     * Testing function core_tag_tag::combine_tags() when correlated tags are present.
900
     */
11 efrain 901
    public function test_combine_tags_with_correlated(): void {
1 efrain 902
        $task = new \core\task\tag_cron_task();
903
 
904
        $tags = $this->prepare_correlated();
905
 
906
        $task->compute_correlations();
907
        // Now 'cat' is correlated with 'cats'.
908
        // Also 'dog', 'dogs' and 'puppy' are correlated.
909
        // There is a manual relation between 'cat' and 'kitten'.
910
        // See function test_correlations() for assertions.
911
 
912
        // Combine tags 'dog' and 'kitten' into 'cat' and make sure that cat is now correlated with dogs and puppy.
913
        $tags['cat']->combine_tags(array($tags['dog'], $tags['kitten']));
914
 
915
        $correlatedtags = $this->get_correlated_tags_names($tags['cat']);
916
        $this->assertEquals(['cats', 'dogs', 'puppy'], $correlatedtags);
917
 
918
        $correlatedtags = $this->get_correlated_tags_names($tags['dogs']);
919
        $this->assertEquals(['cat', 'puppy'], $correlatedtags);
920
 
921
        $correlatedtags = $this->get_correlated_tags_names($tags['puppy']);
922
        $this->assertEquals(['cat', 'dogs'], $correlatedtags);
923
 
924
        // Add tag that does not have any correlations.
925
        $user7 = $this->getDataGenerator()->create_user();
926
        core_tag_tag::set_item_tags('core', 'user', $user7->id, \context_user::instance($user7->id), array('hippo'));
927
        $tags['hippo'] = core_tag_tag::get_by_name(core_tag_collection::get_default(), 'hippo', '*');
928
 
929
        // Combine tag 'cat' into 'hippo'. Now 'hippo' should have the same correlations 'cat' used to have and also
930
        // tags 'dogs' and 'puppy' should have 'hippo' in correlations.
931
        $tags['hippo']->combine_tags(array($tags['cat']));
932
 
933
        $correlatedtags = $this->get_correlated_tags_names($tags['hippo']);
934
        $this->assertEquals(['cats', 'dogs', 'puppy'], $correlatedtags);
935
 
936
        $correlatedtags = $this->get_correlated_tags_names($tags['dogs']);
937
        $this->assertEquals(['hippo', 'puppy'], $correlatedtags);
938
 
939
        $correlatedtags = $this->get_correlated_tags_names($tags['puppy']);
940
        $this->assertEquals(['dogs', 'hippo'], $correlatedtags);
941
    }
942
 
943
    /**
944
     * get_tags_by_area_in_contexts should return an empty array if there
945
     * are no tag instances for the area in the given context.
946
     */
11 efrain 947
    public function test_get_tags_by_area_in_contexts_empty(): void {
1 efrain 948
        $tagnames = ['foo'];
949
        $collid = core_tag_collection::get_default();
950
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
951
        $user = $this->getDataGenerator()->create_user();
952
        $context = \context_user::instance($user->id);
953
        $component = 'core';
954
        $itemtype = 'user';
955
 
956
        $result = core_tag_tag::get_tags_by_area_in_contexts($component, $itemtype, [$context]);
957
        $this->assertEmpty($result);
958
    }
959
 
960
    /**
961
     * get_tags_by_area_in_contexts should return an array of tags that
962
     * have instances in the given context even when there is only a single
963
     * instance.
964
     */
11 efrain 965
    public function test_get_tags_by_area_in_contexts_single_tag_one_context(): void {
1 efrain 966
        $tagnames = ['foo'];
967
        $collid = core_tag_collection::get_default();
968
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
969
        $user = $this->getDataGenerator()->create_user();
970
        $context = \context_user::instance($user->id);
971
        $component = 'core';
972
        $itemtype = 'user';
973
        core_tag_tag::set_item_tags($component, $itemtype, $user->id, $context, $tagnames);
974
 
975
        $result = core_tag_tag::get_tags_by_area_in_contexts($component, $itemtype, [$context]);
976
        $expected = array_map(function($t) {
977
            return $t->id;
978
        }, $tags);
979
        $actual = array_map(function($t) {
980
            return $t->id;
981
        }, $result);
982
 
983
        sort($expected);
984
        sort($actual);
985
 
986
        $this->assertEquals($expected, $actual);
987
    }
988
 
989
    /**
990
     * get_tags_by_area_in_contexts should return all tags in an array
991
     * that have tag instances in for the area in the given context and
992
     * should ignore all tags that don't have an instance.
993
     */
11 efrain 994
    public function test_get_tags_by_area_in_contexts_multiple_tags_one_context(): void {
1 efrain 995
        $tagnames = ['foo', 'bar', 'baz'];
996
        $collid = core_tag_collection::get_default();
997
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
998
        $user = $this->getDataGenerator()->create_user();
999
        $context = \context_user::instance($user->id);
1000
        $component = 'core';
1001
        $itemtype = 'user';
1002
        core_tag_tag::set_item_tags($component, $itemtype, $user->id, $context, array_slice($tagnames, 0, 2));
1003
 
1004
        $result = core_tag_tag::get_tags_by_area_in_contexts($component, $itemtype, [$context]);
1005
        $expected = ['foo', 'bar'];
1006
        $actual = array_map(function($t) {
1007
            return $t->name;
1008
        }, $result);
1009
 
1010
        sort($expected);
1011
        sort($actual);
1012
 
1013
        $this->assertEquals($expected, $actual);
1014
    }
1015
 
1016
    /**
1017
     * get_tags_by_area_in_contexts should return the unique set of
1018
     * tags for a area in the given contexts. Multiple tag instances of
1019
     * the same tag don't result in duplicates in the result set.
1020
     *
1021
     * Tags with tag instances in the same area with in difference contexts
1022
     * should be ignored.
1023
     */
11 efrain 1024
    public function test_get_tags_by_area_in_contexts_multiple_tags_multiple_contexts(): void {
1 efrain 1025
        $tagnames = ['foo', 'bar', 'baz', 'bop', 'bam', 'bip'];
1026
        $collid = core_tag_collection::get_default();
1027
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1028
        $user1 = $this->getDataGenerator()->create_user();
1029
        $user2 = $this->getDataGenerator()->create_user();
1030
        $user3 = $this->getDataGenerator()->create_user();
1031
        $context1 = \context_user::instance($user1->id);
1032
        $context2 = \context_user::instance($user2->id);
1033
        $context3 = \context_user::instance($user3->id);
1034
        $component = 'core';
1035
        $itemtype = 'user';
1036
 
1037
        // User 1 tags: 'foo', 'bar'.
1038
        core_tag_tag::set_item_tags($component, $itemtype, $user1->id, $context1, array_slice($tagnames, 0, 2));
1039
        // User 2 tags: 'bar', 'baz'.
1040
        core_tag_tag::set_item_tags($component, $itemtype, $user2->id, $context2, array_slice($tagnames, 1, 2));
1041
        // User 3 tags: 'bop', 'bam'.
1042
        core_tag_tag::set_item_tags($component, $itemtype, $user3->id, $context3, array_slice($tagnames, 3, 2));
1043
 
1044
        $result = core_tag_tag::get_tags_by_area_in_contexts($component, $itemtype, [$context1, $context2]);
1045
        // Both User 1 and 2 have tagged using 'bar' but we don't
1046
        // expect duplicate tags in the result since they are the same
1047
        // tag.
1048
        //
1049
        // User 3 has tagged 'bop' and 'bam' but we aren't searching in
1050
        // that context so they shouldn't be in the results.
1051
        $expected = ['foo', 'bar', 'baz'];
1052
        $actual = array_map(function($t) {
1053
            return $t->name;
1054
        }, $result);
1055
 
1056
        sort($expected);
1057
        sort($actual);
1058
 
1059
        $this->assertEquals($expected, $actual);
1060
    }
1061
 
1062
    /**
1063
     * get_items_tags should return an empty array if the tag area is disabled.
1064
     */
11 efrain 1065
    public function test_get_items_tags_disabled_component(): void {
1 efrain 1066
        global $CFG;
1067
 
1068
        $user1 = $this->getDataGenerator()->create_user();
1069
        $context1 = \context_user::instance($user1->id);
1070
        $component = 'core';
1071
        $itemtype = 'user';
1072
        $itemids = [$user1->id];
1073
 
1074
        // User 1 tags: 'foo', 'bar'.
1075
        core_tag_tag::set_item_tags($component, $itemtype, $user1->id, $context1, ['foo']);
1076
        // This mimics disabling tags for a component.
1077
        $CFG->usetags = false;
1078
        $result = core_tag_tag::get_items_tags($component, $itemtype, $itemids);
1079
        $this->assertEmpty($result);
1080
    }
1081
 
1082
    /**
1083
     * get_items_tags should return an empty array if the tag item ids list
1084
     * is empty.
1085
     */
11 efrain 1086
    public function test_get_items_tags_empty_itemids(): void {
1 efrain 1087
        $user1 = $this->getDataGenerator()->create_user();
1088
        $context1 = \context_user::instance($user1->id);
1089
        $component = 'core';
1090
        $itemtype = 'user';
1091
 
1092
        // User 1 tags: 'foo', 'bar'.
1093
        core_tag_tag::set_item_tags($component, $itemtype, $user1->id, $context1, ['foo']);
1094
        $result = core_tag_tag::get_items_tags($component, $itemtype, []);
1095
        $this->assertEmpty($result);
1096
    }
1097
 
1098
    /**
1099
     * get_items_tags should return an array indexed by the item ids with empty
1100
     * arrays as the values when the component or itemtype is unknown.
1101
     */
11 efrain 1102
    public function test_get_items_tags_unknown_component_itemtype(): void {
1 efrain 1103
        $itemids = [1, 2, 3];
1104
        $result = core_tag_tag::get_items_tags('someunknowncomponent', 'user', $itemids);
1105
        foreach ($itemids as $itemid) {
1106
            // Unknown component should return an array indexed by the item ids
1107
            // with empty arrays as the values.
1108
            $this->assertEmpty($result[$itemid]);
1109
        }
1110
 
1111
        $result = core_tag_tag::get_items_tags('core', 'someunknownitemtype', $itemids);
1112
        foreach ($itemids as $itemid) {
1113
            // Unknown item type should return an array indexed by the item ids
1114
            // with empty arrays as the values.
1115
            $this->assertEmpty($result[$itemid]);
1116
        }
1117
    }
1118
 
1119
    /**
1120
     * get_items_tags should return an array indexed by the item ids with empty
1121
     * arrays as the values for any item ids that don't have tag instances.
1122
     *
1123
     * Data setup:
1124
     * Users: 1, 2, 3
1125
     * Tags: user 1 = ['foo', 'bar']
1126
     *       user 2 = ['baz', 'bop']
1127
     *       user 3 = []
1128
     *
1129
     * Expected result:
1130
     * [
1131
     *      1 => [
1132
     *          1 => 'foo',
1133
     *          2 => 'bar'
1134
     *      ],
1135
     *      2 => [
1136
     *          3 => 'baz',
1137
     *          4 => 'bop'
1138
     *      ],
1139
     *      3 => []
1140
     * ]
1141
     */
11 efrain 1142
    public function test_get_items_tags_missing_itemids(): void {
1 efrain 1143
        $user1 = $this->getDataGenerator()->create_user();
1144
        $user2 = $this->getDataGenerator()->create_user();
1145
        $user3 = $this->getDataGenerator()->create_user();
1146
        $context1 = \context_user::instance($user1->id);
1147
        $context2 = \context_user::instance($user2->id);
1148
        $component = 'core';
1149
        $itemtype = 'user';
1150
        $itemids = [$user1->id, $user2->id, $user3->id];
1151
        $expecteduser1tagnames = ['foo', 'bar'];
1152
        $expecteduser2tagnames = ['baz', 'bop'];
1153
        $expecteduser3tagnames = [];
1154
 
1155
        // User 1 tags: 'foo', 'bar'.
1156
        core_tag_tag::set_item_tags($component, $itemtype, $user1->id, $context1, $expecteduser1tagnames);
1157
        // User 2 tags: 'bar', 'baz'.
1158
        core_tag_tag::set_item_tags($component, $itemtype, $user2->id, $context2, $expecteduser2tagnames);
1159
 
1160
        $result = core_tag_tag::get_items_tags($component, $itemtype, $itemids);
1161
        $actualuser1tagnames = array_map(function($taginstance) {
1162
            return $taginstance->name;
1163
        }, $result[$user1->id]);
1164
        $actualuser2tagnames = array_map(function($taginstance) {
1165
            return $taginstance->name;
1166
        }, $result[$user2->id]);
1167
        $actualuser3tagnames = $result[$user3->id];
1168
 
1169
        sort($expecteduser1tagnames);
1170
        sort($expecteduser2tagnames);
1171
        sort($actualuser1tagnames);
1172
        sort($actualuser2tagnames);
1173
 
1174
        $this->assertEquals($expecteduser1tagnames, $actualuser1tagnames);
1175
        $this->assertEquals($expecteduser2tagnames, $actualuser2tagnames);
1176
        $this->assertEquals($expecteduser3tagnames, $actualuser3tagnames);
1177
    }
1178
 
1179
    /**
1180
     * set_item_tags should remove any tags that aren't in the given list and should
1181
     * add any instances that are missing.
1182
     */
11 efrain 1183
    public function test_set_item_tags_no_multiple_context_add_remove_instances(): void {
1 efrain 1184
        $tagnames = ['foo', 'bar', 'baz', 'bop'];
1185
        $collid = core_tag_collection::get_default();
1186
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1187
        $user1 = $this->getDataGenerator()->create_user();
1188
        $context = \context_user::instance($user1->id);
1189
        $component = 'core';
1190
        $itemtype = 'user';
1191
        $itemid = 1;
1192
        $tagareas = core_tag_area::get_areas();
1193
        $tagarea = $tagareas[$itemtype][$component];
1194
        $newtagnames = ['bar', 'baz', 'bop'];
1195
 
1196
        // Make sure the tag area doesn't allow multiple contexts.
1197
        core_tag_area::update($tagarea, ['multiplecontexts' => false]);
1198
 
1199
        // Create tag instances in separate contexts.
1200
        $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1201
        $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context);
1202
 
1203
        core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context, $newtagnames);
1204
 
1205
        $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1206
        $actualtagnames = array_map(function($record) {
1207
            return $record->name;
1208
        }, $result);
1209
 
1210
        sort($newtagnames);
1211
        sort($actualtagnames);
1212
 
1213
        // The list of tags should match the $newtagnames which means 'foo'
1214
        // should have been removed while 'baz' and 'bop' were added. 'bar'
1215
        // should remain as it was in the new list of tags.
1216
        $this->assertEquals($newtagnames, $actualtagnames);
1217
    }
1218
 
1219
    /**
1220
     * set_item_tags should set all of the tag instance context ids to the given
1221
     * context if the tag area for the items doesn't allow multiple contexts for
1222
     * the tag instances.
1223
     */
11 efrain 1224
    public function test_set_item_tags_no_multiple_context_updates_context_of_instances(): void {
1 efrain 1225
        $tagnames = ['foo', 'bar'];
1226
        $collid = core_tag_collection::get_default();
1227
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1228
        $user1 = $this->getDataGenerator()->create_user();
1229
        $user2 = $this->getDataGenerator()->create_user();
1230
        $context1 = \context_user::instance($user1->id);
1231
        $context2 = \context_user::instance($user2->id);
1232
        $component = 'core';
1233
        $itemtype = 'user';
1234
        $itemid = 1;
1235
        $tagareas = core_tag_area::get_areas();
1236
        $tagarea = $tagareas[$itemtype][$component];
1237
 
1238
        // Make sure the tag area doesn't allow multiple contexts.
1239
        core_tag_area::update($tagarea, ['multiplecontexts' => false]);
1240
 
1241
        // Create tag instances in separate contexts.
1242
        $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1243
        $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context2);
1244
 
1245
        core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context1, $tagnames);
1246
 
1247
        $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1248
        $this->assertCount(count($tagnames), $result);
1249
 
1250
        foreach ($result as $tag) {
1251
            // The core user tag area doesn't allow multiple contexts for tag instances
1252
            // so set_item_tags should have set all of the tag instance context ids
1253
            // to match $context1.
1254
            $this->assertEquals($context1->id, $tag->taginstancecontextid);
1255
        }
1256
    }
1257
 
1258
    /**
1259
     * set_item_tags should delete all of the tag instances that don't match
1260
     * the new set of tags, regardless of the context that the tag instance
1261
     * is in.
1262
     */
11 efrain 1263
    public function test_set_item_tags_no_multiple_contex_deletes_old_instancest(): void {
1 efrain 1264
        $tagnames = ['foo', 'bar', 'baz', 'bop'];
1265
        $collid = core_tag_collection::get_default();
1266
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1267
        $user1 = $this->getDataGenerator()->create_user();
1268
        $user2 = $this->getDataGenerator()->create_user();
1269
        $context1 = \context_user::instance($user1->id);
1270
        $context2 = \context_user::instance($user2->id);
1271
        $component = 'core';
1272
        $itemtype = 'user';
1273
        $itemid = 1;
1274
        $expectedtagnames = ['foo', 'baz'];
1275
        $tagareas = core_tag_area::get_areas();
1276
        $tagarea = $tagareas[$itemtype][$component];
1277
 
1278
        // Make sure the tag area doesn't allow multiple contexts.
1279
        core_tag_area::update($tagarea, ['multiplecontexts' => false]);
1280
 
1281
        // Create tag instances in separate contexts.
1282
        $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1283
        $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context1);
1284
        $this->add_tag_instance($tags['baz'], $component, $itemtype, $itemid, $context2);
1285
        $this->add_tag_instance($tags['bop'], $component, $itemtype, $itemid, $context2);
1286
 
1287
        core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context1, $expectedtagnames);
1288
 
1289
        $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1290
        $actualtagnames = array_map(function($record) {
1291
            return $record->name;
1292
        }, $result);
1293
 
1294
        sort($expectedtagnames);
1295
        sort($actualtagnames);
1296
 
1297
        // The list of tags should match the $expectedtagnames.
1298
        $this->assertEquals($expectedtagnames, $actualtagnames);
1299
 
1300
        foreach ($result as $tag) {
1301
            // The core user tag area doesn't allow multiple contexts for tag instances
1302
            // so set_item_tags should have set all of the tag instance context ids
1303
            // to match $context1.
1304
            $this->assertEquals($context1->id, $tag->taginstancecontextid);
1305
        }
1306
    }
1307
 
1308
    /**
1309
     * set_item_tags should not change tag instances in a different context to the one
1310
     * it's opertating on if the tag area allows instances from multiple contexts.
1311
     */
11 efrain 1312
    public function test_set_item_tags_allow_multiple_context_doesnt_update_context(): void {
1 efrain 1313
        global $DB;
1314
        $tagnames = ['foo', 'bar', 'bop'];
1315
        $collid = core_tag_collection::get_default();
1316
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1317
        $user1 = $this->getDataGenerator()->create_user();
1318
        $user2 = $this->getDataGenerator()->create_user();
1319
        $context1 = \context_user::instance($user1->id);
1320
        $context2 = \context_user::instance($user2->id);
1321
        $component = 'core';
1322
        $itemtype = 'user';
1323
        $itemid = 1;
1324
        $tagareas = core_tag_area::get_areas();
1325
        $tagarea = $tagareas[$itemtype][$component];
1326
 
1327
        // Make sure the tag area allows multiple contexts.
1328
        core_tag_area::update($tagarea, ['multiplecontexts' => true]);
1329
 
1330
        // Create tag instances in separate contexts.
1331
        $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1332
        $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context2);
1333
 
1334
        // Set the list of tags for $context1. This includes a tag that already exists
1335
        // in that context and a new tag. There is another tag, 'bar', that exists in a
1336
        // different context ($context2) that should be ignored.
1337
        core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context1, ['foo', 'bop']);
1338
 
1339
        $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1340
        $actualtagnames = array_map(function($record) {
1341
            return $record->name;
1342
        }, $result);
1343
 
1344
        sort($tagnames);
1345
        sort($actualtagnames);
1346
        // The list of tags should match the $tagnames.
1347
        $this->assertEquals($tagnames, $actualtagnames);
1348
 
1349
        foreach ($result as $tag) {
1350
            if ($tag->name == 'bar') {
1351
                // The tag instance for 'bar' should have been left untouched
1352
                // because it was in a different context.
1353
                $this->assertEquals($context2->id, $tag->taginstancecontextid);
1354
            } else {
1355
                $this->assertEquals($context1->id, $tag->taginstancecontextid);
1356
            }
1357
        }
1358
    }
1359
 
1360
    /**
1361
     * set_item_tags should delete all of the tag instances that don't match
1362
     * the new set of tags only in the same context if the tag area allows
1363
     * multiple contexts.
1364
     */
11 efrain 1365
    public function test_set_item_tags_allow_multiple_context_deletes_instances_in_same_context(): void {
1 efrain 1366
        $tagnames = ['foo', 'bar', 'baz', 'bop'];
1367
        $collid = core_tag_collection::get_default();
1368
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1369
        $user1 = $this->getDataGenerator()->create_user();
1370
        $user2 = $this->getDataGenerator()->create_user();
1371
        $context1 = \context_user::instance($user1->id);
1372
        $context2 = \context_user::instance($user2->id);
1373
        $component = 'core';
1374
        $itemtype = 'user';
1375
        $itemid = 1;
1376
        $expectedtagnames = ['foo', 'bar', 'bop'];
1377
        $tagareas = core_tag_area::get_areas();
1378
        $tagarea = $tagareas[$itemtype][$component];
1379
 
1380
        // Make sure the tag area allows multiple contexts.
1381
        core_tag_area::update($tagarea, ['multiplecontexts' => true]);
1382
 
1383
        // Create tag instances in separate contexts.
1384
        $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1385
        $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context1);
1386
        $this->add_tag_instance($tags['baz'], $component, $itemtype, $itemid, $context1);
1387
        $this->add_tag_instance($tags['bop'], $component, $itemtype, $itemid, $context2);
1388
 
1389
        core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context1, ['foo', 'bar']);
1390
 
1391
        $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1392
        $actualtagnames = array_map(function($record) {
1393
            return $record->name;
1394
        }, $result);
1395
 
1396
        sort($expectedtagnames);
1397
        sort($actualtagnames);
1398
 
1399
        // The list of tags should match the $expectedtagnames, which includes the
1400
        // tag 'bop' because it was in a different context to the one being set
1401
        // even though it wasn't in the new set of tags.
1402
        $this->assertEquals($expectedtagnames, $actualtagnames);
1403
    }
1404
 
1405
    /**
1406
     * set_item_tags should allow multiple instances of the same tag in different
1407
     * contexts if the tag area allows multiple contexts.
1408
     */
11 efrain 1409
    public function test_set_item_tags_allow_multiple_context_same_tag_multiple_contexts(): void {
1 efrain 1410
        $tagnames = ['foo'];
1411
        $collid = core_tag_collection::get_default();
1412
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1413
        $user1 = $this->getDataGenerator()->create_user();
1414
        $user2 = $this->getDataGenerator()->create_user();
1415
        $context1 = \context_user::instance($user1->id);
1416
        $context2 = \context_user::instance($user2->id);
1417
        $component = 'core';
1418
        $itemtype = 'user';
1419
        $itemid = 1;
1420
        $expectedtagnames = ['foo', 'bar', 'bop'];
1421
        $tagareas = core_tag_area::get_areas();
1422
        $tagarea = $tagareas[$itemtype][$component];
1423
 
1424
        // Make sure the tag area allows multiple contexts.
1425
        core_tag_area::update($tagarea, ['multiplecontexts' => true]);
1426
 
1427
        // Create first instance of 'foo' in $context1.
1428
        $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1429
 
1430
        core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context2, ['foo']);
1431
 
1432
        $result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1433
        $tagsbycontext = array_reduce($result, function($carry, $tag) {
1434
            $contextid = $tag->taginstancecontextid;
1435
            if (isset($carry[$contextid])) {
1436
                $carry[$contextid][] = $tag;
1437
            } else {
1438
                $carry[$contextid] = [$tag];
1439
            }
1440
            return $carry;
1441
        }, []);
1442
 
1443
        // The result should be two tag instances of 'foo' in each of the
1444
        // two contexts, $context1 and $context2.
1445
        $this->assertCount(1, $tagsbycontext[$context1->id]);
1446
        $this->assertCount(1, $tagsbycontext[$context2->id]);
1447
        $this->assertEquals('foo', $tagsbycontext[$context1->id][0]->name);
1448
        $this->assertEquals('foo', $tagsbycontext[$context2->id][0]->name);
1449
    }
1450
 
1451
    /**
1452
     * delete_instances_as_record with an empty set of instances should do nothing.
1453
     */
11 efrain 1454
    public function test_delete_instances_as_record_empty_set(): void {
1 efrain 1455
        $user = $this->getDataGenerator()->create_user();
1456
        $context = \context_user::instance($user->id);
1457
        $component = 'core';
1458
        $itemtype = 'user';
1459
        $itemid = 1;
1460
 
1461
        core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context, ['foo']);
1462
        // This shouldn't error.
1463
        core_tag_tag::delete_instances_as_record([]);
1464
 
1465
        $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1466
        // We should still have one tag.
1467
        $this->assertCount(1, $tags);
1468
    }
1469
 
1470
    /**
1471
     * delete_instances_as_record with an instance that doesn't exist should do
1472
     * nothing.
1473
     */
11 efrain 1474
    public function test_delete_instances_as_record_missing_set(): void {
1 efrain 1475
        $tagnames = ['foo'];
1476
        $collid = core_tag_collection::get_default();
1477
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1478
        $user = $this->getDataGenerator()->create_user();
1479
        $context = \context_user::instance($user->id);
1480
        $component = 'core';
1481
        $itemtype = 'user';
1482
        $itemid = 1;
1483
 
1484
        $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1485
        $taginstance->id++;
1486
 
1487
        // Delete an instance that doesn't exist should do nothing.
1488
        core_tag_tag::delete_instances_as_record([$taginstance]);
1489
 
1490
        $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1491
        // We should still have one tag.
1492
        $this->assertCount(1, $tags);
1493
    }
1494
 
1495
    /**
1496
     * delete_instances_as_record with a list of all tag instances should
1497
     * leave no tags left.
1498
     */
11 efrain 1499
    public function test_delete_instances_as_record_whole_set(): void {
1 efrain 1500
        $tagnames = ['foo'];
1501
        $collid = core_tag_collection::get_default();
1502
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1503
        $user = $this->getDataGenerator()->create_user();
1504
        $context = \context_user::instance($user->id);
1505
        $component = 'core';
1506
        $itemtype = 'user';
1507
        $itemid = 1;
1508
 
1509
        $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1510
 
1511
        core_tag_tag::delete_instances_as_record([$taginstance]);
1512
 
1513
        $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1514
        // There should be no tags left.
1515
        $this->assertEmpty($tags);
1516
    }
1517
 
1518
    /**
1519
     * delete_instances_as_record with a list of only some tag instances should
1520
     * delete only the given tag instances and leave other tag instances.
1521
     */
11 efrain 1522
    public function test_delete_instances_as_record_partial_set(): void {
1 efrain 1523
        $tagnames = ['foo', 'bar'];
1524
        $collid = core_tag_collection::get_default();
1525
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1526
        $user = $this->getDataGenerator()->create_user();
1527
        $context = \context_user::instance($user->id);
1528
        $component = 'core';
1529
        $itemtype = 'user';
1530
        $itemid = 1;
1531
 
1532
        $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1533
        $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context);
1534
 
1535
        core_tag_tag::delete_instances_as_record([$taginstance]);
1536
 
1537
        $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1538
        // We should be left with a single tag, 'bar'.
1539
        $this->assertCount(1, $tags);
1540
        $tag = array_shift($tags);
1541
        $this->assertEquals('bar', $tag->name);
1542
    }
1543
 
1544
    /**
1545
     * delete_instances_by_id with an empty set of ids should do nothing.
1546
     */
11 efrain 1547
    public function test_delete_instances_by_id_empty_set(): void {
1 efrain 1548
        $user = $this->getDataGenerator()->create_user();
1549
        $context = \context_user::instance($user->id);
1550
        $component = 'core';
1551
        $itemtype = 'user';
1552
        $itemid = 1;
1553
 
1554
        core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context, ['foo']);
1555
        // This shouldn't error.
1556
        core_tag_tag::delete_instances_by_id([]);
1557
 
1558
        $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1559
        // We should still have one tag.
1560
        $this->assertCount(1, $tags);
1561
    }
1562
 
1563
    /**
1564
     * delete_instances_by_id with an id that doesn't exist should do
1565
     * nothing.
1566
     */
11 efrain 1567
    public function test_delete_instances_by_id_missing_set(): void {
1 efrain 1568
        $tagnames = ['foo'];
1569
        $collid = core_tag_collection::get_default();
1570
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1571
        $user = $this->getDataGenerator()->create_user();
1572
        $context = \context_user::instance($user->id);
1573
        $component = 'core';
1574
        $itemtype = 'user';
1575
        $itemid = 1;
1576
 
1577
        $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1578
 
1579
        // Delete an instance that doesn't exist should do nothing.
1580
        core_tag_tag::delete_instances_by_id([$taginstance->id + 1]);
1581
 
1582
        $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1583
        // We should still have one tag.
1584
        $this->assertCount(1, $tags);
1585
    }
1586
 
1587
    /**
1588
     * delete_instances_by_id with a list of all tag instance ids should
1589
     * leave no tags left.
1590
     */
11 efrain 1591
    public function test_delete_instances_by_id_whole_set(): void {
1 efrain 1592
        $tagnames = ['foo'];
1593
        $collid = core_tag_collection::get_default();
1594
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1595
        $user = $this->getDataGenerator()->create_user();
1596
        $context = \context_user::instance($user->id);
1597
        $component = 'core';
1598
        $itemtype = 'user';
1599
        $itemid = 1;
1600
 
1601
        $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1602
 
1603
        core_tag_tag::delete_instances_by_id([$taginstance->id]);
1604
 
1605
        $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1606
        // There should be no tags left.
1607
        $this->assertEmpty($tags);
1608
    }
1609
 
1610
    /**
1611
     * delete_instances_by_id with a list of only some tag instance ids should
1612
     * delete only the given tag instance ids and leave other tag instances.
1613
     */
11 efrain 1614
    public function test_delete_instances_by_id_partial_set(): void {
1 efrain 1615
        $tagnames = ['foo', 'bar'];
1616
        $collid = core_tag_collection::get_default();
1617
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1618
        $user = $this->getDataGenerator()->create_user();
1619
        $context = \context_user::instance($user->id);
1620
        $component = 'core';
1621
        $itemtype = 'user';
1622
        $itemid = 1;
1623
 
1624
        $taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
1625
        $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context);
1626
 
1627
        core_tag_tag::delete_instances_by_id([$taginstance->id]);
1628
 
1629
        $tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
1630
        // We should be left with a single tag, 'bar'.
1631
        $this->assertCount(1, $tags);
1632
        $tag = array_shift($tags);
1633
        $this->assertEquals('bar', $tag->name);
1634
    }
1635
 
1636
    /**
1637
     * delete_instances should delete all tag instances for a component if given
1638
     * only the component as a parameter.
1639
     */
11 efrain 1640
    public function test_delete_instances_with_component(): void {
1 efrain 1641
        global $DB;
1642
 
1643
        $tagnames = ['foo', 'bar'];
1644
        $collid = core_tag_collection::get_default();
1645
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1646
        $user = $this->getDataGenerator()->create_user();
1647
        $context = \context_user::instance($user->id);
1648
        $component = 'core';
1649
        $itemtype1 = 'user';
1650
        $itemtype2 = 'course';
1651
        $itemid = 1;
1652
 
1653
        // Add 2 tag instances in the same $component but with different item types.
1654
        $this->add_tag_instance($tags['foo'], $component, $itemtype1, $itemid, $context);
1655
        $this->add_tag_instance($tags['bar'], $component, $itemtype2, $itemid, $context);
1656
 
1657
        // Delete all tag instances for the component.
1658
        core_tag_tag::delete_instances($component);
1659
 
1660
        $taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance} WHERE component = ?', [$component]);
1661
        // Both tag instances from the $component should have been deleted even though
1662
        // they are in different item types.
1663
        $this->assertEmpty($taginstances);
1664
    }
1665
 
1666
    /**
1667
     * delete_instances should delete all tag instances for a component if given
1668
     * only the component as a parameter.
1669
     */
11 efrain 1670
    public function test_delete_instances_with_component_and_itemtype(): void {
1 efrain 1671
        global $DB;
1672
 
1673
        $tagnames = ['foo', 'bar'];
1674
        $collid = core_tag_collection::get_default();
1675
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1676
        $user = $this->getDataGenerator()->create_user();
1677
        $context = \context_user::instance($user->id);
1678
        $component = 'core';
1679
        $itemtype1 = 'user';
1680
        $itemtype2 = 'course';
1681
        $itemid = 1;
1682
 
1683
        // Add 2 tag instances in the same $component but with different item types.
1684
        $this->add_tag_instance($tags['foo'], $component, $itemtype1, $itemid, $context);
1685
        $this->add_tag_instance($tags['bar'], $component, $itemtype2, $itemid, $context);
1686
 
1687
        // Delete all tag instances for the component and itemtype.
1688
        core_tag_tag::delete_instances($component, $itemtype1);
1689
 
1690
        $taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance} WHERE component = ?', [$component]);
1691
        // Only the tag instances for $itemtype1 should have been deleted. We
1692
        // should still be left with the instance for 'bar'.
1693
        $this->assertCount(1, $taginstances);
1694
        $taginstance = array_shift($taginstances);
1695
        $this->assertEquals($itemtype2, $taginstance->itemtype);
1696
        $this->assertEquals($tags['bar']->id, $taginstance->tagid);
1697
    }
1698
 
1699
    /**
1700
     * delete_instances should delete all tag instances for a component in a context
1701
     * if given both the component and context id as parameters.
1702
     */
11 efrain 1703
    public function test_delete_instances_with_component_and_context(): void {
1 efrain 1704
        global $DB;
1705
 
1706
        $tagnames = ['foo', 'bar', 'baz'];
1707
        $collid = core_tag_collection::get_default();
1708
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1709
        $user1 = $this->getDataGenerator()->create_user();
1710
        $user2 = $this->getDataGenerator()->create_user();
1711
        $context1 = \context_user::instance($user1->id);
1712
        $context2 = \context_user::instance($user2->id);
1713
        $component = 'core';
1714
        $itemtype1 = 'user';
1715
        $itemtype2 = 'course';
1716
        $itemid = 1;
1717
 
1718
        // Add 3 tag instances in the same $component but with different contexts.
1719
        $this->add_tag_instance($tags['foo'], $component, $itemtype1, $itemid, $context1);
1720
        $this->add_tag_instance($tags['bar'], $component, $itemtype2, $itemid, $context1);
1721
        $this->add_tag_instance($tags['baz'], $component, $itemtype2, $itemid, $context2);
1722
 
1723
        // Delete all tag instances for the component and context.
1724
        core_tag_tag::delete_instances($component, null, $context1->id);
1725
 
1726
        $taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance} WHERE component = ?', [$component]);
1727
        // Only the tag instances for $context1 should have been deleted. We
1728
        // should still be left with the instance for 'baz'.
1729
        $this->assertCount(1, $taginstances);
1730
        $taginstance = array_shift($taginstances);
1731
        $this->assertEquals($context2->id, $taginstance->contextid);
1732
        $this->assertEquals($tags['baz']->id, $taginstance->tagid);
1733
    }
1734
 
1735
    /**
1736
     * delete_instances should delete all tag instances for a component, item type
1737
     * and context if given the component, itemtype, and context id as parameters.
1738
     */
11 efrain 1739
    public function test_delete_instances_with_component_and_itemtype_and_context(): void {
1 efrain 1740
        global $DB;
1741
 
1742
        $tagnames = ['foo', 'bar', 'baz'];
1743
        $collid = core_tag_collection::get_default();
1744
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1745
        $user1 = $this->getDataGenerator()->create_user();
1746
        $user2 = $this->getDataGenerator()->create_user();
1747
        $context1 = \context_user::instance($user1->id);
1748
        $context2 = \context_user::instance($user2->id);
1749
        $component = 'core';
1750
        $itemtype1 = 'user';
1751
        $itemtype2 = 'course';
1752
        $itemid = 1;
1753
 
1754
        // Add 3 tag instances in the same $component but with different contexts.
1755
        $this->add_tag_instance($tags['foo'], $component, $itemtype1, $itemid, $context1);
1756
        $this->add_tag_instance($tags['bar'], $component, $itemtype2, $itemid, $context1);
1757
        $this->add_tag_instance($tags['baz'], $component, $itemtype2, $itemid, $context2);
1758
 
1759
        // Delete all tag instances for the component and context.
1760
        core_tag_tag::delete_instances($component, $itemtype2, $context1->id);
1761
 
1762
        $taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance} WHERE component = ?', [$component]);
1763
        // Only the tag instances for $itemtype2 in $context1 should have been
1764
        // deleted. We should still be left with the instance for 'foo' and 'baz'.
1765
        $this->assertCount(2, $taginstances);
1766
        $fooinstances = array_filter($taginstances, function($instance) use ($tags) {
1767
            return $instance->tagid == $tags['foo']->id;
1768
        });
1769
        $fooinstance = array_shift($fooinstances);
1770
        $bazinstances = array_filter($taginstances, function($instance) use ($tags) {
1771
            return $instance->tagid == $tags['baz']->id;
1772
        });
1773
        $bazinstance = array_shift($bazinstances);
1774
        $this->assertNotEmpty($fooinstance);
1775
        $this->assertNotEmpty($bazinstance);
1776
        $this->assertEquals($context1->id, $fooinstance->contextid);
1777
        $this->assertEquals($context2->id, $bazinstance->contextid);
1778
    }
1779
 
1780
    /**
1781
     * change_instances_context should not change any existing instance contexts
1782
     * if not given any instance ids.
1783
     */
11 efrain 1784
    public function test_change_instances_context_empty_set(): void {
1 efrain 1785
        global $DB;
1786
 
1787
        $tagnames = ['foo'];
1788
        $collid = core_tag_collection::get_default();
1789
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1790
        $user1 = $this->getDataGenerator()->create_user();
1791
        $user2 = $this->getDataGenerator()->create_user();
1792
        $context1 = \context_user::instance($user1->id);
1793
        $context2 = \context_user::instance($user2->id);
1794
        $component = 'core';
1795
        $itemtype = 'user';
1796
        $itemid = 1;
1797
 
1798
        $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1799
 
1800
        core_tag_tag::change_instances_context([], $context2);
1801
 
1802
        $taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance}');
1803
        // The existing tag instance should not have changed.
1804
        $this->assertCount(1, $taginstances);
1805
        $taginstance = array_shift($taginstances);
1806
        $this->assertEquals($context1->id, $taginstance->contextid);
1807
    }
1808
 
1809
    /**
1810
     * change_instances_context should only change the context of the given ids.
1811
     */
11 efrain 1812
    public function test_change_instances_context_partial_set(): void {
1 efrain 1813
        global $DB;
1814
 
1815
        $tagnames = ['foo', 'bar'];
1816
        $collid = core_tag_collection::get_default();
1817
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1818
        $user1 = $this->getDataGenerator()->create_user();
1819
        $user2 = $this->getDataGenerator()->create_user();
1820
        $context1 = \context_user::instance($user1->id);
1821
        $context2 = \context_user::instance($user2->id);
1822
        $component = 'core';
1823
        $itemtype = 'user';
1824
        $itemid = 1;
1825
 
1826
        $fooinstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1827
        $barinstance = $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context1);
1828
 
1829
        core_tag_tag::change_instances_context([$fooinstance->id], $context2);
1830
 
1831
        // Reload the record.
1832
        $fooinstance = $DB->get_record('tag_instance', ['id' => $fooinstance->id]);
1833
        $barinstance = $DB->get_record('tag_instance', ['id' => $barinstance->id]);
1834
        // Tag 'foo' context should be updated.
1835
        $this->assertEquals($context2->id, $fooinstance->contextid);
1836
        // Tag 'bar' context should not be changed.
1837
        $this->assertEquals($context1->id, $barinstance->contextid);
1838
    }
1839
 
1840
    /**
1841
     * change_instances_context should change multiple items from multiple contexts.
1842
     */
11 efrain 1843
    public function test_change_instances_context_multiple_contexts(): void {
1 efrain 1844
        global $DB;
1845
 
1846
        $tagnames = ['foo', 'bar'];
1847
        $collid = core_tag_collection::get_default();
1848
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1849
        $user1 = $this->getDataGenerator()->create_user();
1850
        $user2 = $this->getDataGenerator()->create_user();
1851
        $user3 = $this->getDataGenerator()->create_user();
1852
        $context1 = \context_user::instance($user1->id);
1853
        $context2 = \context_user::instance($user2->id);
1854
        $context3 = \context_user::instance($user3->id);
1855
        $component = 'core';
1856
        $itemtype = 'user';
1857
        $itemid = 1;
1858
 
1859
        // Two instances in different contexts.
1860
        $fooinstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1861
        $barinstance = $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context2);
1862
 
1863
        core_tag_tag::change_instances_context([$fooinstance->id, $barinstance->id], $context3);
1864
 
1865
        // Reload the record.
1866
        $fooinstance = $DB->get_record('tag_instance', ['id' => $fooinstance->id]);
1867
        $barinstance = $DB->get_record('tag_instance', ['id' => $barinstance->id]);
1868
        // Tag 'foo' context should be updated.
1869
        $this->assertEquals($context3->id, $fooinstance->contextid);
1870
        // Tag 'bar' context should be updated.
1871
        $this->assertEquals($context3->id, $barinstance->contextid);
1872
        // There shouldn't be any tag instances left in $context1.
1873
        $context1records = $DB->get_records('tag_instance', ['contextid' => $context1->id]);
1874
        $this->assertEmpty($context1records);
1875
        // There shouldn't be any tag instances left in $context2.
1876
        $context2records = $DB->get_records('tag_instance', ['contextid' => $context2->id]);
1877
        $this->assertEmpty($context2records);
1878
    }
1879
 
1880
    /**
1881
     * change_instances_context moving an instance from one context into a context
1882
     * that already has an instance of that tag should throw an exception.
1883
     */
11 efrain 1884
    public function test_change_instances_context_conflicting_instances(): void {
1 efrain 1885
        global $DB;
1886
 
1887
        $tagnames = ['foo'];
1888
        $collid = core_tag_collection::get_default();
1889
        $tags = core_tag_tag::create_if_missing($collid, $tagnames);
1890
        $user1 = $this->getDataGenerator()->create_user();
1891
        $user2 = $this->getDataGenerator()->create_user();
1892
        $context1 = \context_user::instance($user1->id);
1893
        $context2 = \context_user::instance($user2->id);
1894
        $component = 'core';
1895
        $itemtype = 'user';
1896
        $itemid = 1;
1897
 
1898
        // Two instances of 'foo' in different contexts.
1899
        $fooinstance1 = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
1900
        $fooinstance2 = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context2);
1901
 
1902
        // There is already an instance of 'foo' in $context2 so the code
1903
        // should throw an exception when we try to move another instance there.
1904
        $this->expectException('Exception');
1905
        core_tag_tag::change_instances_context([$fooinstance1->id], $context2);
1906
    }
1907
 
1908
    /**
1441 ariadna 1909
     * Tests user pagination works correctly for filtered users.
1910
     */
1911
    public function test_user_get_tagged_users(): void {
1912
        global $DB;
1913
 
1914
        // Create some users.
1915
        $users = [];
1916
        for ($i = 0; $i < 11; $i++) {
1917
            $users[] = $this->getDataGenerator()->create_user();
1918
        }
1919
 
1920
        // Create the tag.
1921
        $tagcollid = core_tag_collection::get_default();
1922
        $tag = $this->getDataGenerator()->create_tag(['tagcollid' => $tagcollid, 'rawname' => 'bike']);
1923
 
1924
        // Add the tag to the users.
1925
        for ($i = 0; $i < count($users); $i++) {
1926
            core_tag_tag::add_item_tag('core', 'user', $users[$i]->id,
1927
                    \context_user::instance($users[$i]->id), 'bike');
1928
        }
1929
 
1930
        // The logged-in user.
1931
        $this->setUser($users[0]);
1932
 
1933
        // Get the tagged users.
1934
        $tag = core_tag_tag::get($tag->id, '*');
1935
        $taggedusers = user_get_tagged_users($tag);
1936
 
1937
        // Ensure it has content.
1938
        $this->assertEquals(1, $taggedusers->hascontent);
1939
 
1940
        // Ensure it should have 1 user and the "more" link is hidden (null).
1941
        $this->assertEquals(1, $this->count_html_elements($taggedusers->content, 'li'));
1942
        $this->assertNull($taggedusers->nextpageurl);
1943
 
1944
        // Test which users are visible to the logged-in user based on the course.
1945
        // Create a course to tag.
1946
        $course = $this->getDataGenerator()->create_course();
1947
        $studentrole = $DB->get_record('role', ['shortname' => 'student']);
1948
        for ($i = 0; $i < count($users); $i++) {
1949
            // Enrol only some users (0, 2, 4, 6, 8, 10).
1950
            if ($i % 2 === 0) {
1951
                $this->getDataGenerator()->enrol_user($users[$i]->id, $course->id, $studentrole->id);
1952
            }
1953
        }
1954
 
1955
        // First page should have 5 users and the "more" link is visible (not null).
1956
        $taggedusers = user_get_tagged_users(
1957
            tag: $tag,
1958
            page: 0,
1959
        );
1960
        $this->assertNotNull($taggedusers->nextpageurl);
1961
        $this->assertEquals(5, $this->count_html_elements($taggedusers->content, 'li'));
1962
 
1963
        // Second page should have 1 user and the "more" link is hidden (null).
1964
        $taggedusers = user_get_tagged_users(
1965
            tag: $tag,
1966
            page: 1,
1967
        );
1968
        $this->assertNull($taggedusers->nextpageurl);
1969
        $this->assertEquals(1, $this->count_html_elements($taggedusers->content, 'li'));
1970
    }
1971
 
1972
    /**
1973
     * Counts the number of specified HTML elements in a given HTML string.
1974
     *
1975
     * @param string $html The HTML string to be parsed.
1976
     * @param string $tagname The name of the HTML tag to count (e.g., 'li', 'div').
1977
     * @return int The number of elements with the specified tag name found in the HTML.
1978
     */
1979
    private function count_html_elements(string $html, string $tagname): int {
1980
        // Load the HTML into DOMDocument.
1981
        $dom = new \DOMDocument();
1982
        libxml_use_internal_errors(true); // Suppress warnings for invalid HTML.
1983
        $dom->loadHTML($html);
1984
        libxml_clear_errors();
1985
 
1986
        // Find all elements with the specified tag name.
1987
        $elements = $dom->getElementsByTagName($tagname);
1988
 
1989
        // Count the number of elements.
1990
        return $elements->length;
1991
    }
1992
 
1993
    /**
1 efrain 1994
     * Help method to return sorted array of names of correlated tags to use for assertions
1995
     * @param core_tag $tag
1996
     * @return string
1997
     */
1998
    protected function get_correlated_tags_names($tag) {
1999
        $rv = array_map(function($t) {
2000
            return $t->rawname;
2001
        }, $tag->get_correlated_tags());
2002
        sort($rv);
2003
        return array_values($rv);
2004
    }
2005
 
2006
    /**
2007
     * Add a tag instance.
2008
     *
2009
     * @param core_tag_tag $tag
2010
     * @param string $component
2011
     * @param string $itemtype
2012
     * @param int $itemid
2013
     * @param \context $context
2014
     * @return \stdClass
2015
     */
2016
    protected function add_tag_instance(core_tag_tag $tag, $component, $itemtype, $itemid, $context) {
2017
        global $DB;
2018
        $record = (array) $tag->to_object();
2019
        $record['tagid'] = $record['id'];
2020
        $record['component'] = $component;
2021
        $record['itemtype'] = $itemtype;
2022
        $record['itemid'] = $itemid;
2023
        $record['contextid'] = $context->id;
2024
        $record['tiuserid'] = 0;
2025
        $record['ordering'] = 0;
2026
        $record['timecreated'] = time();
2027
        $record['id'] = $DB->insert_record('tag_instance', $record);
2028
        return (object) $record;
2029
    }
1441 ariadna 2030
 
2031
    /**
2032
     * Checks the contents of the a tagcloud
2033
     *
2034
     * @param array $tags
2035
     * @param \core_tag\output\tagcloud $tagcloud
2036
     */
2037
    protected function assert_tag_cloud_contains_tags(array $tags, \core_tag\output\tagcloud $tagcloud) {
2038
        global $PAGE;
2039
        $renderer = $PAGE->get_renderer('core', 'tag');
2040
        $result = $tagcloud->export_for_template($renderer);
2041
        $result = json_decode(json_encode($result), true);
2042
        $this->assertEqualsCanonicalizing($tags, array_values(array_column($result['tags'], 'name')));
2043
    }
2044
 
2045
    public function test_get_tag_cloud(): void {
2046
        global $DB, $PAGE;
2047
        $this->resetAfterTest();
2048
 
2049
        // Create a course and a user with tags.
2050
        $this->getDataGenerator()->create_course(['tags' => 'cats,animals']);
2051
        $this->getDataGenerator()->create_user(['interests' => 'dogs,animals']);
2052
 
2053
        // Default tag cloud contains all three tags.
2054
        $tagcloud = core_tag_collection::get_tag_cloud(0);
2055
        $this->assert_tag_cloud_contains_tags(['animals', 'cats', 'dogs'], $tagcloud);
2056
 
2057
        // Create two new tag collections, move course tags to C1 and user tags to C2.
2058
        $c1 = core_tag_collection::create((object)['name' => 'C1', 'searchable' => 1]);
2059
        $c2 = core_tag_collection::create((object)['name' => 'C2', 'searchable' => 1]);
2060
        $tagareacourse = $DB->get_record('tag_area', ['component' => 'core', 'itemtype' => 'course'], '*', MUST_EXIST);
2061
        core_tag_area::update($tagareacourse, ['tagcollid' => $c1->id]);
2062
        $tagareauser = $DB->get_record('tag_area', ['component' => 'core', 'itemtype' => 'user'], '*', MUST_EXIST);
2063
        core_tag_area::update($tagareauser, ['tagcollid' => $c2->id]);
2064
 
2065
        // Tag cloud still has all tags and you can also search by a collection. Tag 'animals' now has two different view links.
2066
        $this->assert_tag_cloud_contains_tags(['animals', 'animals', 'cats', 'dogs'], core_tag_collection::get_tag_cloud(0));
2067
        $this->assert_tag_cloud_contains_tags(['animals', 'cats'], core_tag_collection::get_tag_cloud($c1->id));
2068
        $this->assert_tag_cloud_contains_tags(['animals', 'dogs'], core_tag_collection::get_tag_cloud($c2->id));
2069
 
2070
        // Make user interest tag area not searchable.
2071
        core_tag_collection::update($c2, ['searchable' => 0]);
2072
        // Check that the user interest tags do not appear in the tagclouds.
2073
        $this->assert_tag_cloud_contains_tags(['animals', 'cats'], core_tag_collection::get_tag_cloud(0));
2074
        $this->assert_tag_cloud_contains_tags(['animals', 'cats'], core_tag_collection::get_tag_cloud($c1->id));
2075
        $this->assert_tag_cloud_contains_tags([], core_tag_collection::get_tag_cloud($c2->id));
2076
    }
1 efrain 2077
}