Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

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