MDL-50851 core_tag: introduce tag collections
authorMarina Glancy <marina@moodle.com>
Fri, 2 Oct 2015 15:13:44 +0000 (23:13 +0800)
committerMarina Glancy <marina@moodle.com>
Sun, 10 Jan 2016 07:25:43 +0000 (15:25 +0800)
70 files changed:
course/lib.php
lang/en/cache.php
lang/en/deprecated.txt
lang/en/question.php
lang/en/tag.php
lib/adminlib.php
lib/amd/build/tag.min.js
lib/amd/src/tag.js
lib/blocklib.php
lib/classes/event/tag_added.php
lib/classes/event/tag_collection_created.php [new file with mode: 0644]
lib/classes/event/tag_collection_deleted.php [new file with mode: 0644]
lib/classes/event/tag_collection_updated.php [new file with mode: 0644]
lib/classes/event/tag_created.php
lib/classes/event/tag_removed.php
lib/classes/task/tag_cron_task.php
lib/db/caches.php
lib/db/install.xml
lib/db/services.php
lib/db/tag.php [new file with mode: 0644]
lib/db/tasks.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/form/tags.php
lib/outputrenderers.php
lib/testing/generator/data_generator.php
lib/testing/tests/generator_test.php
lib/upgrade.txt
lib/upgradelib.php
mod/wiki/tests/behat/edit_tags.feature
tag/classes/area.php [new file with mode: 0644]
tag/classes/areas_table.php [new file with mode: 0644]
tag/classes/collection.php [new file with mode: 0644]
tag/classes/collection_form.php [new file with mode: 0644]
tag/classes/collections_table.php [new file with mode: 0644]
tag/classes/external.php
tag/classes/manage_table.php
tag/classes/output/tag.php
tag/classes/output/tagcloud.php [new file with mode: 0644]
tag/classes/output/tagfeed.php [new file with mode: 0644]
tag/classes/output/tagindex.php [new file with mode: 0644]
tag/classes/output/taglist.php [new file with mode: 0644]
tag/classes/renderer.php [new file with mode: 0644]
tag/classes/tag.php [new file with mode: 0644]
tag/edit.php
tag/edit_form.php
tag/index.php
tag/lib.php
tag/locallib.php
tag/manage.php
tag/search.php
tag/templates/index.mustache [new file with mode: 0644]
tag/templates/tagcloud.mustache [new file with mode: 0644]
tag/templates/tagfeed.mustache [new file with mode: 0644]
tag/templates/taglist.mustache [new file with mode: 0644]
tag/tests/behat/collections.feature [new file with mode: 0644]
tag/tests/behat/delete_tag.feature
tag/tests/behat/edit_tag.feature
tag/tests/behat/flag_tags.feature
tag/tests/behat/official_tags.feature
tag/tests/behat/tagindex.feature [new file with mode: 0644]
tag/tests/events_test.php
tag/tests/external_test.php
tag/tests/taglib_test.php
tag/upgrade.txt
theme/base/style/core.css
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/style/moodle.css
theme/canvas/style/core.css
version.php

index 21c4481..5412f24 100644 (file)
@@ -1648,7 +1648,6 @@ function course_delete_module($cmid) {
     require_once($CFG->libdir.'/questionlib.php');
     require_once($CFG->dirroot.'/blog/lib.php');
     require_once($CFG->dirroot.'/calendar/lib.php');
-    require_once($CFG->dirroot.'/tag/lib.php');
 
     // Get the course module.
     if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) {
@@ -1717,7 +1716,7 @@ function course_delete_module($cmid) {
                                                             'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY));
 
     // Delete all tag instances associated with the instance of this module.
-    tag_delete_instances('mod_' . $modulename, $modcontext->id);
+    core_tag::delete_instances('mod_' . $modulename, null, $modcontext->id);
 
     // Delete the context.
     context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
index a44742e..3a2c783 100644 (file)
@@ -60,6 +60,7 @@ $string['cachedef_plugin_manager'] = 'Plugin info manager';
 $string['cachedef_questiondata'] = 'Question definitions';
 $string['cachedef_repositories'] = 'Repositories instances data';
 $string['cachedef_string'] = 'Language string cache';
+$string['cachedef_tags'] = 'Tags collections and areas';
 $string['cachedef_userselections'] = 'Data used to persist user selections throughout Moodle';
 $string['cachedef_yuimodules'] = 'YUI Module definitions';
 $string['cachelock_file_default'] = 'Default file locking';
index ab59bdb..366e26b 100644 (file)
@@ -20,3 +20,4 @@ updated,core_tag
 withselectedtags,core_tag
 tag:create,core_role
 categoriesanditems,core_grades
+taggedwith,core_tag
index 2ce0f2c..c4ea1b4 100644 (file)
@@ -421,6 +421,7 @@ $string['submissionoutofsequencefriendlymessage'] = "You have entered data outsi
 $string['submit'] = 'Submit';
 $string['submitandfinish'] = 'Submit and finish';
 $string['submitted'] = 'Submit: {$a}';
+$string['tagarea_question'] = 'Questions';
 $string['technicalinfo'] = 'Technical information';
 $string['technicalinfo_help'] = 'This technical information is probably only useful for developers working on new question types. It may also be helpful when trying to diagnose problems with questions.';
 $string['technicalinfominfraction'] = 'Minimum fraction: {$a}';
index 6065fb4..e20a603 100644 (file)
 
 $string['added'] = 'Official tag(s) added';
 $string['addotags'] = 'Add official tags';
+$string['addtagcoll'] = 'Add tag collection';
 $string['addtagtomyinterests'] = 'Add "{$a}" to my interests';
 $string['alltagpages'] = 'All tag pages';
+$string['backtoallitems'] = 'Back to all items tagged with "{$a}"';
+$string['changessaved'] = 'Changes saved';
+$string['changetagcoll'] = 'Change tag collection of area {$a}';
+$string['collnameexplained'] = 'Leave the field empty to use the default value: {$a}';
+$string['component'] = 'Component';
 $string['confirmdeletetag'] = 'Are you sure you want to delete this tag?';
 $string['confirmdeletetags'] = 'Are you sure you want to delete selected tags?';
 $string['count'] = 'Count';
 $string['coursetags'] = 'Course tags';
+$string['defautltagcoll'] = 'Default collection';
 $string['delete'] = 'Delete';
 $string['deleteselected'] = 'Delete selected';
 $string['deleted'] = 'Tag(s) deleted';
@@ -38,15 +45,20 @@ $string['description'] = 'Description';
 $string['editname'] = 'Edit tag name';
 $string['edittag'] = 'Edit this tag';
 $string['entertags'] = 'Enter tags...';
+$string['edittagcoll'] = 'Edit tag collection {$a}';
 $string['errortagfrontpage'] = 'Tagging the site main page is not allowed';
 $string['errorupdatingrecord'] = 'Error updating tag record';
 $string['eventtagadded'] = 'Tag added to an item';
+$string['eventtagcolldeleted'] = 'Tag collection deleted';
+$string['eventtagcollcreated'] = 'Tag collection created';
+$string['eventtagcollupdated'] = 'Tag collection updatedhp ';
 $string['eventtagcreated'] = 'Tag created';
 $string['eventtagdeleted'] = 'Tag deleted';
 $string['eventtagflagged'] = 'Tag flagged';
 $string['eventtagremoved'] = 'Tag removed from an item';
 $string['eventtagunflagged'] = 'Tag unflagged';
 $string['eventtagupdated'] = 'Tag updated';
+$string['exclusivemode'] = 'Show only tagged {$a->tagarea}';
 $string['flag'] = 'Flag';
 $string['flagged'] = 'Tag flagged';
 $string['flagasinappropriate'] = 'Flag as inappropriate';
@@ -54,17 +66,25 @@ $string['helprelatedtags'] = 'Comma separated related tags';
 $string['changename'] = 'Change tag name';
 $string['changetype'] = 'Change tag type';
 $string['id'] = 'id';
+$string['inalltagcoll'] = 'Everywhere';
+$string['itemstaggedwith'] = '{$a->tagarea} tagged with "{$a->tag}"';
+$string['lesstags'] = 'less...';
 $string['manageofficialtags'] = 'Manage official tags';
 $string['managetags'] = 'Manage tags';
+$string['managetagcolls'] = 'Manage tag collections';
+$string['moretags'] = 'more...';
 $string['name'] = 'Tag name';
 $string['namesalreadybeeingused'] = 'Tag names already being used';
 $string['newnamefor'] = 'New name for tag {$a}';
+$string['nextpage'] = 'More';
+$string['notagsfound'] = 'No tags matching "{$a}" found';
 $string['noresultsfor'] = 'No results for "{$a}"';
 $string['nothingtoupdate'] = 'Nothing to update';
 $string['officialtag'] = 'Official';
 $string['otags'] = 'Official tags';
 $string['othertags'] = 'Other tags';
 $string['owner'] = 'Owner';
+$string['prevpage'] = 'Back';
 $string['ptags'] = 'User defined tags (Comma separated)';
 $string['relatedblogs'] = 'Most recent blog entries';
 $string['relatedtags'] = 'Related tags';
@@ -75,16 +95,29 @@ $string['responsiblewillbenotified'] = 'The person responsible will be notified'
 $string['rssdesc'] = 'This RSS feed was automatically generated by Moodle and contains user generated tags for courses.';
 $string['rsstitle'] = 'Course tags RSS feed for user: {$a}';
 $string['search'] = 'Search';
+$string['searchable'] = 'Searchable';
+$string['searchable_help'] = 'Tags in this tag collection can be searched for on "Search tags" page. If unchecked, tags can still be accessed by clicking on them or via different search interfaces.';
 $string['searchresultsfor'] = 'Search results for "{$a}"';
 $string['searchtags'] = 'Search tags';
-$string['seeallblogs'] = 'See all blog entries tagged with "{$a}"...';
+$string['seeallblogs'] = 'See all blog entries tagged with "{$a}"';
 $string['select'] = 'Select';
+$string['selectcoll'] = 'Select tag collection';
 $string['selecttag'] = 'Select tag {$a}';
 $string['settypedefault'] = 'Remove from official tags';
 $string['settypeofficial'] = 'Make official';
+$string['showingfirsttags'] = 'Showing {$a} most popular tags';
+$string['suredeletecoll'] = 'Are you sure you want to delete tag collection "{$a}"?';
 $string['tag'] = 'Tag';
+$string['tagarea_blog_external'] = 'External blog posts';
+$string['tagarea_post'] = 'Blog posts';
+$string['tagarea_user'] = 'User interests';
+$string['tagarea_course'] = 'Courses';
+$string['tagareaenabled'] = 'Enabled';
+$string['tagareaname'] = 'Name';
+$string['tagareas'] = 'Tag areas';
+$string['tagcollection'] = 'Tag collection';
+$string['tagcollections'] = 'Tag collections';
 $string['tagdescription'] = 'Tag description';
-$string['taggedwith'] = 'tagged with "{$a}"';
 $string['tags'] = 'Tags';
 $string['tagsaredisabled'] = 'Tags are disabled';
 $string['tagtype'] = 'Tag type';
@@ -107,3 +140,7 @@ $string['tagtype_official'] = 'Official';
 $string['thistaghasnodesc'] = 'This tag currently has no description.';
 $string['updated'] = 'Updated';
 $string['withselectedtags'] = 'With selected tags...';
+
+// Deprecated since 3.1 .
+
+$string['taggedwith'] = 'tagged with "{$a}"';
index 81c95d0..cc90580 100644 (file)
@@ -166,9 +166,8 @@ function uninstall_plugin($type, $name) {
 
     echo $OUTPUT->heading($pluginname);
 
-    // Delete all tag instances associated with this plugin.
-    require_once($CFG->dirroot . '/tag/lib.php');
-    tag_delete_instances($component);
+    // Delete all tag areas, collections and instances associated with this plugin.
+    core_tag_area::uninstall($component);
 
     // Custom plugin uninstall.
     $plugindirectory = core_component::get_plugin_directory($type, $name);
index 8d684ae..7a4328f 100644 (file)
Binary files a/lib/amd/build/tag.min.js and b/lib/amd/build/tag.min.js differ
index 0edf6ef..01ef814 100644 (file)
@@ -27,9 +27,37 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
     return /** @alias module:core/tag */ {
 
         /**
-         * Initialises handlers for AJAX methods.
+         * Initialises tag index page.
          *
-         * @method init
+         * @method init_tagindex_page
+         */
+        init_tagindex_page: function() {
+            // Click handler for changing tag type.
+            $('body').delegate('.tagarea[data-ta] a[data-quickload=1]', 'click', function(e) {
+                e.preventDefault();
+                var target = $( this ),
+                    query = target.context.search.replace(/^\?/, ''),
+                    tagarea = target.closest('.tagarea[data-ta]'),
+                    args = query.split('&').reduce(function(s,c){var t=c.split('=');s[t[0]]=decodeURIComponent(t[1]);return s;},{});
+
+                var promises = ajax.call([{
+                    methodname: 'core_tag_get_tagindex',
+                    args: { tagindex: args }
+                }], true);
+
+                $.when.apply($, promises)
+                    .done( function(data) {
+                        templates.render('core_tag/index', data).done(function(html) {
+                            tagarea.replaceWith(html);
+                        });
+                    });
+            });
+        },
+
+        /**
+         * Initialises tag management page.
+         *
+         * @method init_manage_page
          */
         init_manage_page: function() {
 
index 56208b6..b045eb2 100644 (file)
@@ -1497,10 +1497,10 @@ class block_manager {
             if ($bits[0] == 'tag' && !empty($this->page->subpage)) {
                 // better navbar for tag pages
                 $editpage->navbar->add(get_string('tags'), new moodle_url('/tag/'));
-                $tag = tag_get('id', $this->page->subpage, '*');
+                $tag = core_tag_tag::get($this->page->subpage);
                 // tag search page doesn't have subpageid
                 if ($tag) {
-                    $editpage->navbar->add($tag->name, new moodle_url('/tag/index.php', array('id'=>$tag->id)));
+                    $editpage->navbar->add($tag->get_display_name(), $tag->get_view_url());
                 }
             }
             $editpage->navbar->add($block->get_title());
index 6316cc3..fecf439 100644 (file)
@@ -74,6 +74,34 @@ class tag_added extends base {
             s($this->other['itemtype']) . "' with id '{$this->other['itemid']}'.";
     }
 
+    /**
+     * Creates an event from taginstance object
+     *
+     * @since Moodle 3.1
+     * @param stdClass $taginstance
+     * @param string $tagname
+     * @param string $tagrawname
+     * @param bool $addsnapshot trust that $taginstance has all necessary fields and add it as a record snapshot
+     * @return tag_added
+     */
+    public static function create_from_tag_instance($taginstance, $tagname, $tagrawname, $addsnapshot = false) {
+        $event = self::create(array(
+            'objectid' => $taginstance->id,
+            'contextid' => $taginstance->contextid,
+            'other' => array(
+                'tagid' => $taginstance->tagid,
+                'tagname' => $tagname,
+                'tagrawname' => $tagrawname,
+                'itemid' => $taginstance->itemid,
+                'itemtype' => $taginstance->itemtype
+            )
+        ));
+        if ($addsnapshot) {
+            $event->add_record_snapshot('tag_instance', $taginstance);
+        }
+        return $event;
+    }
+
     /**
      * Return legacy data for add_to_log().
      *
diff --git a/lib/classes/event/tag_collection_created.php b/lib/classes/event/tag_collection_created.php
new file mode 100644 (file)
index 0000000..57573d0
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tag collection created event.
+ *
+ * @package    core
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tag collection created event class.
+ *
+ * @package    core
+ * @since      Moodle 3.0
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tag_collection_created extends base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tag_coll';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Utility method to create new event.
+     *
+     * @param object $tagcoll
+     * @return user_graded
+     */
+    public static function create_from_record($tagcoll) {
+        $event = self::create(array(
+            'objectid' => $tagcoll->id,
+            'context' => \context_system::instance(),
+        ));
+        $event->add_record_snapshot('tag_coll', $tagcoll);
+        return $event;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventtagcollcreated', 'core_tag');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' created the tag collection with id '$this->objectid'";
+    }
+}
diff --git a/lib/classes/event/tag_collection_deleted.php b/lib/classes/event/tag_collection_deleted.php
new file mode 100644 (file)
index 0000000..f9b332f
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tag collection deleted event.
+ *
+ * @package    core
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tag collection deleted event class.
+ *
+ * @package    core
+ * @since      Moodle 3.0
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tag_collection_deleted extends base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tag_coll';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Utility method to create new event.
+     *
+     * @param object $tagcoll
+     * @return user_graded
+     */
+    public static function create_from_record($tagcoll) {
+        $event = self::create(array(
+            'objectid' => $tagcoll->id,
+            'context' => \context_system::instance(),
+        ));
+        $event->add_record_snapshot('tag_coll', $tagcoll);
+        return $event;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventtagcolldeleted', 'core_tag');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' deleted the tag collection with id '$this->objectid'";
+    }
+}
diff --git a/lib/classes/event/tag_collection_updated.php b/lib/classes/event/tag_collection_updated.php
new file mode 100644 (file)
index 0000000..0417b0f
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tag collection updated event.
+ *
+ * @package    core
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tag collection updated event class.
+ *
+ * @package    core
+ * @since      Moodle 3.0
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tag_collection_updated extends base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tag_coll';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Utility method to create new event.
+     *
+     * @param object $tagcoll
+     * @return user_graded
+     */
+    public static function create_from_record($tagcoll) {
+        $event = self::create(array(
+            'objectid' => $tagcoll->id,
+            'context' => \context_system::instance(),
+        ));
+        $event->add_record_snapshot('tag_coll', $tagcoll);
+        return $event;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventtagcollupdated', 'core_tag');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' updated the tag collection with id '$this->objectid'";
+    }
+}
index e239b39..9573a49 100644 (file)
@@ -52,6 +52,26 @@ class tag_created extends base {
         $this->data['edulevel'] = self::LEVEL_OTHER;
     }
 
+    /**
+     * Creates an event from tag object
+     *
+     * @since Moodle 3.1
+     * @param \core_tag_tag|\stdClass $tag
+     * @return tag_created
+     */
+    public static function create_from_tag($tag) {
+        $event = self::create(array(
+            'objectid' => $tag->id,
+            'relateduserid' => $tag->userid,
+            'context' => \context_system::instance(),
+            'other' => array(
+                'name' => $tag->name,
+                'rawname' => $tag->rawname
+            )
+        ));
+        return $event;
+    }
+
     /**
      * Returns localised general event name.
      *
index 04558ac..faa3610 100644 (file)
@@ -74,6 +74,34 @@ class tag_removed extends base {
             s($this->other['itemtype']) . "' with id '{$this->other['itemid']}'.";
     }
 
+    /**
+     * Creates an event from taginstance object
+     *
+     * @since Moodle 3.1
+     * @param stdClass $taginstance
+     * @param string $tagname
+     * @param string $tagrawname
+     * @param bool $addsnapshot trust that $taginstance has all necessary fields and add it as a record snapshot
+     * @return tag_removed
+     */
+    public static function create_from_tag_instance($taginstance, $tagname, $tagrawname, $addsnapshot = false) {
+        $event = self::create(array(
+            'objectid' => $taginstance->id,
+            'contextid' => $taginstance->contextid,
+            'other' => array(
+                'tagid' => $taginstance->tagid,
+                'tagname' => $tagname,
+                'tagrawname' => $tagrawname,
+                'itemid' => $taginstance->itemid,
+                'itemtype' => $taginstance->itemtype
+            )
+        ));
+        if ($addsnapshot) {
+            $event->add_record_snapshot('tag_instance', $taginstance);
+        }
+        return $event;
+    }
+
     /**
      * Custom validation.
      *
index cdedf29..721edfb 100644 (file)
@@ -23,6 +23,8 @@
  */
 namespace core\task;
 
+use core_tag_collection, core_tag_tag, core_tag_area, stdClass;
+
 /**
  * Simple task to run the tag cron.
  */
@@ -45,9 +47,224 @@ class tag_cron_task extends scheduled_task {
         global $CFG;
 
         if (!empty($CFG->usetags)) {
-            require_once($CFG->dirroot.'/tag/lib.php');
-            tag_cron();
+            $this->compute_correlations();
+            $this->cleanup();
+        }
+    }
+
+    /**
+     * Calculates and stores the correlated tags of all tags.
+     *
+     * The correlations are stored in the 'tag_correlation' table.
+     *
+     * Two tags are correlated if they appear together a lot. Ex.: Users tagged with "computers"
+     * will probably also be tagged with "algorithms".
+     *
+     * The rationale for the 'tag_correlation' table is performance. It works as a cache
+     * for a potentially heavy load query done at the 'tag_instance' table. So, the
+     * 'tag_correlation' table stores redundant information derived from the 'tag_instance' table.
+     *
+     * @param int $mincorrelation Only tags with more than $mincorrelation correlations will be identified.
+     */
+    public function compute_correlations($mincorrelation = 2) {
+        global $DB;
+
+        // This mighty one line query fetches a row from the database for every
+        // individual tag correlation. We then need to process the rows collecting
+        // the correlations for each tag id.
+        // The fields used by this query are as follows:
+        //   tagid         : This is the tag id, there should be at least $mincorrelation
+        //                   rows for each tag id.
+        //   correlation   : This is the tag id that correlates to the above tagid field.
+        //   correlationid : This is the id of the row in the tag_correlation table that
+        //                   relates to the tagid field and will be NULL if there are no
+        //                   existing correlations.
+        $sql = 'SELECT pairs.tagid, pairs.correlation, pairs.ocurrences, co.id AS correlationid
+                  FROM (
+                           SELECT ta.tagid, tb.tagid AS correlation, COUNT(*) AS ocurrences
+                             FROM {tag_instance} ta
+                             JOIN {tag} tga ON ta.tagid = tga.id
+                             JOIN {tag_instance} tb ON (ta.itemtype = tb.itemtype AND ta.component = tb.component
+                                AND ta.itemid = tb.itemid AND ta.tagid <> tb.tagid)
+                             JOIN {tag} tgb ON tb.tagid = tgb.id AND tgb.tagcollid = tga.tagcollid
+                         GROUP BY ta.tagid, tb.tagid
+                           HAVING COUNT(*) > :mincorrelation
+                       ) pairs
+             LEFT JOIN {tag_correlation} co ON co.tagid = pairs.tagid
+              ORDER BY pairs.tagid ASC, pairs.ocurrences DESC, pairs.correlation ASC';
+        $rs = $DB->get_recordset_sql($sql, array('mincorrelation' => $mincorrelation));
+
+        // Set up an empty tag correlation object.
+        $tagcorrelation = new stdClass;
+        $tagcorrelation->id = null;
+        $tagcorrelation->tagid = null;
+        $tagcorrelation->correlatedtags = array();
+
+        // We store each correlation id in this array so we can remove any correlations
+        // that no longer exist.
+        $correlations = array();
+
+        // Iterate each row of the result set and build them into tag correlations.
+        // We add all of a tag's correlations to $tagcorrelation->correlatedtags[]
+        // then save the $tagcorrelation object.
+        foreach ($rs as $row) {
+            if ($row->tagid != $tagcorrelation->tagid) {
+                // The tag id has changed so we have all of the correlations for this tag.
+                $tagcorrelationid = $this->process_computed_correlation($tagcorrelation);
+                if ($tagcorrelationid) {
+                    $correlations[] = $tagcorrelationid;
+                }
+                // Now we reset the tag correlation object so we can reuse it and set it
+                // up for the current record.
+                $tagcorrelation = new stdClass;
+                $tagcorrelation->id = $row->correlationid;
+                $tagcorrelation->tagid = $row->tagid;
+                $tagcorrelation->correlatedtags = array();
+            }
+            // Save the correlation on the tag correlation object.
+            $tagcorrelation->correlatedtags[] = $row->correlation;
+        }
+        // Update the current correlation after the last record.
+        $tagcorrelationid = $this->process_computed_correlation($tagcorrelation);
+        if ($tagcorrelationid) {
+            $correlations[] = $tagcorrelationid;
+        }
+
+        // Close the recordset.
+        $rs->close();
+
+        // Remove any correlations that weren't just identified.
+        if (empty($correlations)) {
+            // There are no tag correlations.
+            $DB->delete_records('tag_correlation');
+        } else {
+            list($sql, $params) = $DB->get_in_or_equal($correlations,
+                    SQL_PARAMS_NAMED, 'param0000', false);
+            $DB->delete_records_select('tag_correlation', 'id '.$sql, $params);
+        }
+    }
+
+    /**
+     * Clean up the tag tables, making sure all tagged object still exists.
+     *
+     * This method is called from cron.
+     *
+     * This should normally not be necessary, but in case related tags are not deleted
+     * when the tagged record is removed, this should be done once in a while, perhaps
+     * on an occasional cron run.  On a site with lots of tags, this could become an
+     * expensive function to call.
+     */
+    public function cleanup() {
+        global $DB;
+
+        // Get ids to delete from instances where the tag has been deleted. This should never happen apparently.
+        $sql = "SELECT ti.id
+                  FROM {tag_instance} ti
+             LEFT JOIN {tag} t ON t.id = ti.tagid
+                 WHERE t.id IS null";
+        $tagids = $DB->get_records_sql($sql);
+        $tagarray = array();
+        foreach ($tagids as $tagid) {
+            $tagarray[] = $tagid->id;
         }
+
+        // Next get ids from instances that have an owner that has been deleted.
+        $sql = "SELECT ti.id
+                  FROM {tag_instance} ti, {user} u
+                 WHERE ti.itemid = u.id
+                   AND ti.itemtype = 'user'
+                   AND ti.component = 'core'
+                   AND u.deleted = 1";
+        $tagids = $DB->get_records_sql($sql);
+        foreach ($tagids as $tagid) {
+            $tagarray[] = $tagid->id;
+        }
+
+        // Get the other itemtypes.
+        $sql = "SELECT DISTINCT component, itemtype
+                  FROM {tag_instance}
+                 WHERE itemtype <> 'user' or component <> 'core'";
+        $tagareas = $DB->get_records_sql($sql);
+        foreach ($tagareas as $tagarea) {
+            $sql = 'SELECT ti.id
+                      FROM {tag_instance} ti
+                 LEFT JOIN {' . $tagarea->itemtype . '} it ON it.id = ti.itemid
+                     WHERE it.id IS null
+                     AND ti.itemtype = ? AND ti.component = ?';
+            $tagids = $DB->get_records_sql($sql, array($tagarea->itemtype, $tagarea->component));
+            foreach ($tagids as $tagid) {
+                $tagarray[] = $tagid->id;
+            }
+        }
+
+        // Get instances for each of the ids to be deleted.
+        if (count($tagarray) > 0) {
+            list($sqlin, $params) = $DB->get_in_or_equal($tagarray);
+            $sql = "SELECT ti.*, COALESCE(t.name, 'deleted') AS name, COALESCE(t.rawname, 'deleted') AS rawname
+                      FROM {tag_instance} ti
+                 LEFT JOIN {tag} t ON t.id = ti.tagid
+                     WHERE ti.id $sqlin";
+            $instances = $DB->get_records_sql($sql, $params);
+            $this->bulk_delete_instances($instances);
+        }
+
+        core_tag_collection::cleanup_unused_tags();
+    }
+
+    /**
+     * This function processes a tag correlation and makes changes in the database as required.
+     *
+     * The tag correlation object needs have both a tagid property and a correlatedtags property that is an array.
+     *
+     * @param   stdClass $tagcorrelation
+     * @return  int/bool The id of the tag correlation that was just processed or false.
+     */
+    public function process_computed_correlation(stdClass $tagcorrelation) {
+        global $DB;
+
+        // You must provide a tagid and correlatedtags must be set and be an array.
+        if (empty($tagcorrelation->tagid) || !isset($tagcorrelation->correlatedtags) ||
+                !is_array($tagcorrelation->correlatedtags)) {
+            return false;
+        }
+
+        $tagcorrelation->correlatedtags = join(',', $tagcorrelation->correlatedtags);
+        if (!empty($tagcorrelation->id)) {
+            // The tag correlation already exists so update it.
+            $DB->update_record('tag_correlation', $tagcorrelation);
+        } else {
+            // This is a new correlation to insert.
+            $tagcorrelation->id = $DB->insert_record('tag_correlation', $tagcorrelation);
+        }
+        return $tagcorrelation->id;
     }
 
+    /**
+     * This function will delete numerous tag instances efficiently.
+     * This removes tag instances only. It doesn't check to see if it is the last use of a tag.
+     *
+     * @param array $instances An array of tag instance objects with the addition of the tagname and tagrawname
+     *        (used for recording a delete event).
+     */
+    public function bulk_delete_instances($instances) {
+        global $DB;
+
+        $instanceids = array();
+        foreach ($instances as $instance) {
+            $instanceids[] = $instance->id;
+        }
+
+        // This is a multi db compatible method of creating the correct sql when using the 'IN' value.
+        // $insql is the sql statement, $params are the id numbers.
+        list($insql, $params) = $DB->get_in_or_equal($instanceids);
+        $sql = 'id ' . $insql;
+        $DB->delete_records_select('tag_instance', $sql, $params);
+
+        // Now go through and record each tag individually with the event system.
+        foreach ($instances as $instance) {
+            // Trigger tag removed event (i.e. The tag instance has been removed).
+            \core\event\tag_removed::create_from_tag_instance($instance, $instance->name,
+                    $instance->rawname, true)->trigger();
+        }
+    }
 }
index 7d90e55..dd8dc96 100644 (file)
@@ -252,6 +252,12 @@ $definitions = array(
         'simpledata' => true,
         'staticacceleration' => true,
         'staticaccelerationsize' => 5
+    ),
+
+    // Caches data about tag collections and areas.
+    'tags' => array(
+        'mode' => cache_store::MODE_REQUEST,
+        'simplekeys' => true,
     )
 
 );
index de8750a..3c9e420 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20150922" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20160111" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <KEY NAME="importer" TYPE="foreign" FIELDS="importer" REFTABLE="user" REFFIELDS="id" COMMENT="user who is importing"/>
       </KEYS>
     </TABLE>
+    <TABLE NAME="tag_coll" COMMENT="Defines different set of tags">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="isdefault" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="sortorder" TYPE="int" LENGTH="5" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="searchable" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Whether the tag collection is searchable"/>
+        <FIELD NAME="customurl" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Custom URL for the tag page instead of /tag/index.php"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+      </KEYS>
+    </TABLE>
+    <TABLE NAME="tag_area" COMMENT="Defines various tag areas, one area is identified by component and itemtype">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="itemtype" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="enabled" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
+        <FIELD NAME="tagcollid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="callback" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="callbackfile" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="tagcollid" TYPE="foreign" FIELDS="tagcollid" REFTABLE="tag_coll" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="compitemtype" UNIQUE="true" FIELDS="component, itemtype"/>
+      </INDEXES>
+    </TABLE>
     <TABLE NAME="tag" COMMENT="Tag table - this generic table will replace the old &quot;tags&quot; table.">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="tagcollid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="rawname" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="The raw, unnormalised name for the tag as entered by users"/>
         <FIELD NAME="tagtype" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
         <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+        <KEY NAME="tagcollid" TYPE="foreign" FIELDS="tagcollid" REFTABLE="tag_coll" REFFIELDS="id"/>
       </KEYS>
       <INDEXES>
-        <INDEX NAME="name" UNIQUE="true" FIELDS="name" COMMENT="tag names are unique"/>
+        <INDEX NAME="tagcollname" UNIQUE="true" FIELDS="tagcollid, name"/>
+        <INDEX NAME="tagcolltype" UNIQUE="false" FIELDS="tagcollid, tagtype"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="tag_correlation" COMMENT="The rationale for the 'tag_correlation' table is performance.   It works as a cache for a potentially heavy load query done at the 'tag_instance' table.   So, the 'tag_correlation' table stores redundant information derived from the 'tag_instance' table">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="tagid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" COMMENT="Defines the Moodle component which the tag was added to"/>
-        <FIELD NAME="itemtype" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false" COMMENT="Defines the Moodle component which the tag was added to"/>
+        <FIELD NAME="itemtype" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="itemid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The context id of the item that was tagged"/>
         <FIELD NAME="tiuserid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <KEY NAME="contextid" TYPE="foreign" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/>
       </KEYS>
       <INDEXES>
-        <INDEX NAME="itemtype-itemid-tagid-tiuserid" UNIQUE="true" FIELDS="itemtype, itemid, tagid, tiuserid"/>
+        <INDEX NAME="taggeditem" UNIQUE="true" FIELDS="component, itemtype, itemid, tiuserid, tagid"/>
+        <INDEX NAME="taglookup" UNIQUE="false" FIELDS="itemtype, component, tagid, contextid"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="groups" COMMENT="Each record represents a group.">
index d9867da..bc3230f 100644 (file)
@@ -1148,6 +1148,14 @@ $functions = array(
         'ajax'        => true
     ),
 
+    'core_tag_get_tagindex' => array(
+        'classname'   => 'core_tag_external',
+        'methodname'  => 'get_tagindex',
+        'description' => 'Gets tag index page for one tag and one tag area',
+        'type'        => 'read',
+        'ajax'        => true
+    ),
+
 );
 
 $services = array(
diff --git a/lib/db/tag.php b/lib/db/tag.php
new file mode 100644 (file)
index 0000000..2923bb5
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tag area definitions
+ *
+ * File db/tag.php lists all available tag areas in core or a plugin.
+ *
+ * Each tag area may have the following attributes:
+ *   - itemtype (required) - what is tagged. Must be name of the existing DB table
+ *   - component - component responsible for tagging, if the tag area is inside a
+ *     plugin the component must be the full frankenstyle name of the plugin
+ *   - collection - name of the custom tag collection that will be used to store
+ *     tags in this area. If specified aministrator will be able to neither add
+ *     any other tag areas to this collection nor move this tag area elsewhere
+ *   - searchable (only if collection is specified) - wether the tag collection
+ *     should be searchable on /tag/search.php
+ *   - customurl (only if collection is specified) - custom url to use instead of
+ *     /tag/search.php to display information about one tag
+ *   - callback - name of the function that returns items tagged with this tag,
+ *     see core_tag_tag::get_tag_index() and existing callbacks for more details,
+ *     callback should return instance of core_tag\output\tagindex
+ *   - callbackfile - file where callback is located (if not an autoloaded location)
+ *
+ * Language file must contain the human-readable names of the tag areas and
+ * collections (either in plugin language file or in component language file or
+ * lang/en/tag.php in case of core):
+ * - for item type "user":
+ *     $string['tagarea_user'] = 'Users';
+ * - for tag collection "mycollection":
+ *     $string['tagcollection_mycollection'] = 'My tag collection';
+ *
+ * @package   core
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$tagareas = array(
+    array(
+        'itemtype' => 'user', // Users.
+        'component' => 'core',
+        'callback' => 'user_get_tagged_users',
+        'callbackfile' => '/user/lib.php',
+    ),
+    array(
+        'itemtype' => 'course', // Courses.
+        'component' => 'core',
+        'callback' => 'course_get_tagged_courses',
+        'callbackfile' => '/course/lib.php',
+    ),
+    array(
+        'itemtype' => 'question', // Questions.
+        'component' => 'core_question',
+    ),
+    array(
+        'itemtype' => 'post', // Blog posts.
+        'component' => 'core',
+        'callback' => 'blog_get_tagged_posts',
+        'callbackfile' => '/blog/lib.php',
+    ),
+    array(
+        'itemtype' => 'blog_external', // External blogs.
+        'component' => 'core',
+    ),
+);
index 9723d33..41991f1 100644 (file)
@@ -71,8 +71,8 @@ $tasks = array(
     array(
         'classname' => 'core\task\tag_cron_task',
         'blocking' => 0,
-        'minute' => '20',
-        'hour' => '*',
+        'minute' => 'R',
+        'hour' => '3',
         'day' => '*',
         'dayofweek' => '*',
         'month' => '*'
index 40b21c7..cb48385 100644 (file)
@@ -4609,5 +4609,191 @@ function xmldb_main_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2016011100.00) {
+
+        // This is a big upgrade script. We create new table tag_coll and the field
+        // tag.tagcollid pointing to it.
+
+        // Define table tag_coll to be created.
+        $table = new xmldb_table('tag_coll');
+
+        // Adding fields to table tagcloud.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
+        $table->add_field('isdefault', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0');
+        $table->add_field('component', XMLDB_TYPE_CHAR, '100', null, null, null, null);
+        $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '5', null, XMLDB_NOTNULL, null, '0');
+        $table->add_field('searchable', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1');
+        $table->add_field('customurl', XMLDB_TYPE_CHAR, '255', null, null, null, null);
+
+        // Adding keys to table tagcloud.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+
+        // Conditionally launch create table for tagcloud.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Table {tag}.
+        // Define index name (unique) to be dropped form tag - we will replace it with index on (tagcollid,name) later.
+        $table = new xmldb_table('tag');
+        $index = new xmldb_index('name', XMLDB_INDEX_UNIQUE, array('name'));
+
+        // Conditionally launch drop index name.
+        if ($dbman->index_exists($table, $index)) {
+            $dbman->drop_index($table, $index);
+        }
+
+        // Define field tagcollid to be added to tag, we create it as null first and will change to notnull later.
+        $table = new xmldb_table('tag');
+        $field = new xmldb_field('tagcollid', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'userid');
+
+        // Conditionally launch add field tagcloudid.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2016011100.00);
+    }
+
+    if ($oldversion < 2016011100.02) {
+        // Create a default tag collection if not exists and update the field tag.tagcollid to point to it.
+        if (!$tcid = $DB->get_field_sql('SELECT id FROM {tag_coll} ORDER BY isdefault DESC, sortorder, id', null,
+                IGNORE_MULTIPLE)) {
+            $tcid = $DB->insert_record('tag_coll', array('isdefault' => 1, 'sortorder' => 0));
+        }
+        $DB->execute('UPDATE {tag} SET tagcollid = ? WHERE tagcollid IS NULL', array($tcid));
+
+        // Define index tagcollname (unique) to be added to tag.
+        $table = new xmldb_table('tag');
+        $index = new xmldb_index('tagcollname', XMLDB_INDEX_UNIQUE, array('tagcollid', 'name'));
+        $field = new xmldb_field('tagcollid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'userid');
+
+        // Conditionally launch add index tagcollname.
+        if (!$dbman->index_exists($table, $index)) {
+            // Launch change of nullability for field tagcollid.
+            $dbman->change_field_notnull($table, $field);
+            $dbman->add_index($table, $index);
+        }
+
+        // Define key tagcollid (foreign) to be added to tag.
+        $table = new xmldb_table('tag');
+        $key = new xmldb_key('tagcollid', XMLDB_KEY_FOREIGN, array('tagcollid'), 'tag_coll', array('id'));
+
+        // Launch add key tagcloudid.
+        $dbman->add_key($table, $key);
+
+        // Define index tagcolltype (not unique) to be added to tag.
+        $table = new xmldb_table('tag');
+        $index = new xmldb_index('tagcolltype', XMLDB_INDEX_NOTUNIQUE, array('tagcollid', 'tagtype'));
+
+        // Conditionally launch add index tagcolltype.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2016011100.02);
+    }
+
+    if ($oldversion < 2016011100.03) {
+
+        // Define table tag_area to be created.
+        $table = new xmldb_table('tag_area');
+
+        // Adding fields to table tag_area.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('component', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('itemtype', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('enabled', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1');
+        $table->add_field('tagcollid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('callback', XMLDB_TYPE_CHAR, '100', null, null, null, null);
+        $table->add_field('callbackfile', XMLDB_TYPE_CHAR, '100', null, null, null, null);
+
+        // Adding keys to table tag_area.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('tagcollid', XMLDB_KEY_FOREIGN, array('tagcollid'), 'tag_coll', array('id'));
+
+        // Adding indexes to table tag_area.
+        $table->add_index('compitemtype', XMLDB_INDEX_UNIQUE, array('component', 'itemtype'));
+
+        // Conditionally launch create table for tag_area.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2016011100.03);
+    }
+
+    if ($oldversion < 2016011100.12) {
+
+        // Define index itemtype-itemid-tagid-tiuserid (unique) to be dropped form tag_instance.
+        $table = new xmldb_table('tag_instance');
+        $index = new xmldb_index('itemtype-itemid-tagid-tiuserid', XMLDB_INDEX_UNIQUE,
+                array('itemtype', 'itemid', 'tagid', 'tiuserid'));
+
+        // Conditionally launch drop index itemtype-itemid-tagid-tiuserid.
+        if ($dbman->index_exists($table, $index)) {
+            $dbman->drop_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2016011100.12);
+    }
+
+    if ($oldversion < 2016011100.13) {
+
+        $DB->execute("UPDATE {tag_instance} SET component = ? WHERE component IS NULL", array(''));
+
+        // Changing nullability of field component on table tag_instance to not null.
+        $table = new xmldb_table('tag_instance');
+        $field = new xmldb_field('component', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, 'tagid');
+
+        // Launch change of nullability for field component.
+        $dbman->change_field_notnull($table, $field);
+
+        // Changing type of field itemtype on table tag_instance to char.
+        $table = new xmldb_table('tag_instance');
+        $field = new xmldb_field('itemtype', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, 'component');
+
+        // Launch change of type for field itemtype.
+        $dbman->change_field_type($table, $field);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2016011100.13);
+    }
+
+    if ($oldversion < 2016011100.14) {
+
+        // Define index taggeditem (unique) to be added to tag_instance.
+        $table = new xmldb_table('tag_instance');
+        $index = new xmldb_index('taggeditem', XMLDB_INDEX_UNIQUE, array('component', 'itemtype', 'itemid', 'tiuserid', 'tagid'));
+
+        // Conditionally launch add index taggeditem.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2016011100.14);
+    }
+
+    if ($oldversion < 2016011100.15) {
+
+        // Define index taglookup (not unique) to be added to tag_instance.
+        $table = new xmldb_table('tag_instance');
+        $index = new xmldb_index('taglookup', XMLDB_INDEX_NOTUNIQUE, array('itemtype', 'component', 'tagid', 'contextid'));
+
+        // Conditionally launch add index taglookup.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2016011100.15);
+    }
+
     return true;
 }
index cd38aa4..f20c2d1 100644 (file)
@@ -2617,30 +2617,17 @@ function coursetag_store_keywords($tags, $courseid, $userid=0, $tagtype='officia
     debugging('Function coursetag_store_keywords() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
 
     global $CFG;
-    require_once $CFG->dirroot.'/tag/lib.php';
 
     if (is_array($tags) and !empty($tags)) {
+        if ($tagtype === 'official') {
+            $tagcoll = core_tag_area::get_collection('core', 'course');
+            // We don't normally need to create tags, they are created automatically when added to items. but we do here because we want them to be official.
+            core_tag_tag::create_if_missing($tagcoll, $tags, true);
+        }
         foreach ($tags as $tag) {
             $tag = trim($tag);
             if (strlen($tag) > 0) {
-                //tag_set_add('course', $courseid, $tag, $userid); //deletes official tags
-
-                //add tag if does not exist
-                if (!$tagid = tag_get_id($tag)) {
-                    $tag_id_array = tag_add(array($tag), $tagtype);
-                    $tagid = $tag_id_array[core_text::strtolower($tag)];
-                }
-                //ordering
-                $ordering = 0;
-                if ($current_ids = tag_get_tags_ids('course', $courseid)) {
-                    end($current_ids);
-                    $ordering = key($current_ids) + 1;
-                }
-                //set type
-                tag_type_set($tagid, $tagtype);
-
-                //tag_instance entry
-                tag_assign('course', $courseid, $tagid, $ordering, $userid, 'core', context_course::instance($courseid)->id);
+                core_tag_tag::add_item_tag('core', 'course', $courseid, context_course::instance($courseid), $tag, $userid);
             }
         }
     }
@@ -2660,7 +2647,8 @@ function coursetag_store_keywords($tags, $courseid, $userid=0, $tagtype='officia
 function coursetag_delete_keyword($tagid, $userid, $courseid) {
     debugging('Function coursetag_delete_keyword() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
 
-    tag_delete_instance('course', $courseid, $tagid, $userid);
+    $tag = core_tag_tag::get($tagid);
+    core_tag_tag::remove_item_tag('core', 'course', $courseid, $tag->rawname, $userid);
 }
 
 /**
@@ -2709,21 +2697,428 @@ function coursetag_get_tagged_courses($tagid) {
  * @param   bool     $showfeedback if we should output a notification of the delete to the end user
  */
 function coursetag_delete_course_tags($courseid, $showfeedback=false) {
-    debugging('Function coursetag_delete_course_tags() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
+    debugging('Function coursetag_delete_course_tags() is deprecated. Use core_tag_tag::remove_all_item_tags().', DEBUG_DEVELOPER);
+
+    global $OUTPUT;
+    core_tag_tag::remove_all_item_tags('core', 'course', $courseid);
 
-    global $DB, $OUTPUT;
+    if ($showfeedback) {
+        echo $OUTPUT->notification(get_string('deletedcoursetags', 'tag'), 'notifysuccess');
+    }
+}
 
-    if ($taginstances = $DB->get_recordset_select('tag_instance', "itemtype = 'course' AND itemid = :courseid",
-        array('courseid' => $courseid), '', 'tagid, tiuserid')) {
+/**
+ * Set the type of a tag.  At this time (version 2.2) the possible values are 'default' or 'official'.  Official tags will be
+ * displayed separately "at tagging time" (while selecting the tags to apply to a record).
+ *
+ * @package  core_tag
+ * @deprecated since 3.1
+ * @param    string   $tagid tagid to modify
+ * @param    string   $type either 'default' or 'official'
+ * @return   bool     true on success, false otherwise
+ */
+function tag_type_set($tagid, $type) {
+    debugging('Function tag_type_set() is deprecated and can be replaced with use core_tag_tag::get($tagid)->update().', DEBUG_DEVELOPER);
+    if ($tag = core_tag_tag::get($tagid, '*')) {
+        return $tag->update(array('tagtype' => $type));
+    }
+    return false;
+}
 
-        foreach ($taginstances as $record) {
-            tag_delete_instance('course', $courseid, $record->tagid, $record->tiuserid);
+/**
+ * Set the description of a tag
+ *
+ * @package  core_tag
+ * @deprecated since 3.1
+ * @param    int      $tagid the id of the tag
+ * @param    string   $description the tag's description string to be set
+ * @param    int      $descriptionformat the moodle text format of the description
+ *                    {@link http://docs.moodle.org/dev/Text_formats_2.0#Database_structure}
+ * @return   bool     true on success, false otherwise
+ */
+function tag_description_set($tagid, $description, $descriptionformat) {
+    debugging('Function tag_type_set() is deprecated and can be replaced with core_tag_tag::get($tagid)->update().', DEBUG_DEVELOPER);
+    if ($tag = core_tag_tag::get($tagid, '*')) {
+        return $tag->update(array('description' => $description, 'descriptionformat' => $descriptionformat));
+    }
+    return false;
+}
+
+/**
+ * Get the array of db record of tags associated to a record (instances).
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param string $record_type the record type for which we want to get the tags
+ * @param int $record_id the record id for which we want to get the tags
+ * @param string $type the tag type (either 'default' or 'official'). By default, all tags are returned.
+ * @param int $userid (optional) only required for course tagging
+ * @return array the array of tags
+ */
+function tag_get_tags($record_type, $record_id, $type=null, $userid=0) {
+    debugging('Method tag_get_tags() is deprecated and replaced with core_tag_tag::get_item_tags(). ' .
+        'Component is now required when retrieving tag instances.', DEBUG_DEVELOPER);
+    $official = ($type === 'official' ? true : (!empty($type) ? false : null));
+    $tags = core_tag_tag::get_item_tags(null, $record_type, $record_id, $official, $userid);
+    $rv = array();
+    foreach ($tags as $id => $t) {
+        $rv[$id] = $t->to_object();
+    }
+    return $rv;
+}
+
+/**
+ * Get the array of tags display names, indexed by id.
+ *
+ * @package  core_tag
+ * @deprecated since 3.1
+ * @param    string $record_type the record type for which we want to get the tags
+ * @param    int    $record_id   the record id for which we want to get the tags
+ * @param    string $type        the tag type (either 'default' or 'official'). By default, all tags are returned.
+ * @return   array  the array of tags (with the value returned by core_tag_tag::make_display_name), indexed by id
+ */
+function tag_get_tags_array($record_type, $record_id, $type=null) {
+    debugging('Method tag_get_tags_array() is deprecated and replaced with core_tag_tag::get_item_tags_array(). ' .
+        'Component is now required when retrieving tag instances.', DEBUG_DEVELOPER);
+    $official = ($type === 'official' ? true : (!empty($type) ? false : null));
+    return core_tag_tag::get_item_tags_array('', $record_type, $record_id, $official);
+}
+
+/**
+ * Get a comma-separated string of tags associated to a record.
+ *
+ * Use {@link tag_get_tags()} to get the same information in an array.
+ *
+ * @package  core_tag
+ * @deprecated since 3.1
+ * @param    string   $record_type the record type for which we want to get the tags
+ * @param    int      $record_id   the record id for which we want to get the tags
+ * @param    int      $html        either TAG_RETURN_HTML or TAG_RETURN_TEXT, depending on the type of output desired
+ * @param    string   $type        either 'official' or 'default', if null, all tags are returned
+ * @return   string   the comma-separated list of tags.
+ */
+function tag_get_tags_csv($record_type, $record_id, $html=null, $type=null) {
+    global $CFG, $OUTPUT;
+    debugging('Method tag_get_tags_csv() is deprecated. Instead you should use either ' .
+            'core_tag_tag::get_item_tags_array() or $OUTPUT->tag_list(core_tag_tag::get_item_tags()). ' .
+        'Component is now required when retrieving tag instances.', DEBUG_DEVELOPER);
+    $official = ($type === 'official' ? true : (!empty($type) ? false : null));
+    if ($html != TAG_RETURN_TEXT) {
+        return $OUTPUT->tag_list(core_tag_tag::get_item_tags('', $record_type, $record_id, $official), '');
+    } else {
+        return join(', ', core_tag_tag::get_item_tags_array('', $record_type, $record_id, $official, 0, false));
+    }
+}
+
+/**
+ * Get an array of tag ids associated to a record.
+ *
+ * @package  core_tag
+ * @deprecated since 3.1
+ * @param    string    $record_type the record type for which we want to get the tags
+ * @param    int       $record_id the record id for which we want to get the tags
+ * @return   array     tag ids, indexed and sorted by 'ordering'
+ */
+function tag_get_tags_ids($record_type, $record_id) {
+    debugging('Method tag_get_tags_ids() is deprecated. Please consider using core_tag_tag::get_item_tags() or similar methods.', DEBUG_DEVELOPER);
+    $tag_ids = array();
+    $tagobjects = core_tag_tag::get_item_tags(null, $record_type, $record_id);
+    foreach ($tagobjects as $tagobject) {
+        $tag = $tagobject->to_object();
+        if ( array_key_exists($tag->ordering, $tag_ids) ) {
+            $tag->ordering++;
         }
-        $taginstances->close();
+        $tag_ids[$tag->ordering] = $tag->id;
     }
+    ksort($tag_ids);
+    return $tag_ids;
+}
 
-    if ($showfeedback) {
-        echo $OUTPUT->notification(get_string('deletedcoursetags', 'tag'), 'notifysuccess');
+/**
+ * Returns the database ID of a set of tags.
+ *
+ * @deprecated since 3.1
+ * @param    mixed $tags one tag, or array of tags, to look for.
+ * @param    bool  $return_value specify the type of the returned value. Either TAG_RETURN_OBJECT, or TAG_RETURN_ARRAY (default).
+ *                               If TAG_RETURN_ARRAY is specified, an array will be returned even if only one tag was passed in $tags.
+ * @return   mixed tag-indexed array of ids (or objects, if second parameter is TAG_RETURN_OBJECT), or only an int, if only one tag
+ *                 is given *and* the second parameter is null. No value for a key means the tag wasn't found.
+ */
+function tag_get_id($tags, $return_value = null) {
+    global $CFG, $DB;
+    debugging('Method tag_get_id() is deprecated and can be replaced with core_tag_tag::get_by_name() or core_tag_tag::get_by_name_bulk(). ' .
+        'You need to specify tag collection when retrieving tag by name', DEBUG_DEVELOPER);
+
+    if (!is_array($tags)) {
+        if(is_null($return_value) || $return_value == TAG_RETURN_OBJECT) {
+            if ($tagobject = core_tag_tag::get_by_name(core_tag_collection::get_default(), $tags)) {
+                return $tagobject->id;
+            } else {
+                return 0;
+            }
+        }
+        $tags = array($tags);
+    }
+
+    $records = core_tag_tag::get_by_name_bulk(core_tag_collection::get_default(), $tags,
+        $return_value == TAG_RETURN_OBJECT ? '*' : 'id, name');
+    foreach ($records as $name => $record) {
+        if ($return_value != TAG_RETURN_OBJECT) {
+            $records[$name] = $record->id ? $record->id : null;
+        } else {
+            $records[$name] = $record->to_object();
+        }
+    }
+    return $records;
+}
+
+/**
+ * Change the "value" of a tag, and update the associated 'name'.
+ *
+ * @package  core_tag
+ * @deprecated since 3.1
+ * @param    int      $tagid  the id of the tag to modify
+ * @param    string   $newrawname the new rawname
+ * @return   bool     true on success, false otherwise
+ */
+function tag_rename($tagid, $newrawname) {
+    debugging('Function tag_rename() is deprecated and may be replaced with core_tag_tag::get($tagid)->update().', DEBUG_DEVELOPER);
+    if ($tag = core_tag_tag::get($tagid, '*')) {
+        return $tag->update(array('rawname' => $newrawname));
+    }
+    return false;
+}
+
+/**
+ * Delete one instance of a tag.  If the last instance was deleted, it will also delete the tag, unless its type is 'official'.
+ *
+ * @package  core_tag
+ * @deprecated since 3.1
+ * @param    string $record_type the type of the record for which to remove the instance
+ * @param    int    $record_id   the id of the record for which to remove the instance
+ * @param    int    $tagid       the tagid that needs to be removed
+ * @param    int    $userid      (optional) the userid
+ * @return   bool   true on success, false otherwise
+ */
+function tag_delete_instance($record_type, $record_id, $tagid, $userid = null) {
+    debugging('Function tag_delete_instance() is deprecated and replaced with core_tag_tag::remove_item_tag() instead. ' .
+        'Component is required for retrieving instances', DEBUG_DEVELOPER);
+    $tag = core_tag_tag::get($tagid);
+    core_tag_tag::remove_item_tag('', $record_type, $record_id, $tag->rawname, $userid);
+}
+
+/**
+ * Find all records tagged with a tag of a given type ('post', 'user', etc.)
+ *
+ * @package  core_tag
+ * @category tag
+ * @param    string   $tag       tag to look for
+ * @param    string   $type      type to restrict search to.  If null, every matching record will be returned
+ * @param    int      $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point.
+ * @param    int      $limitnum  (optional, required if $limitfrom is set) return a subset comprising this many records.
+ * @return   array of matching objects, indexed by record id, from the table containing the type requested
+ */
+function tag_find_records($tag, $type, $limitfrom='', $limitnum='') {
+    debugging('Function tag_find_records() is deprecated and replaced with core_tag_tag::get_by_name()->get_tagged_items(). '.
+        'You need to specify tag collection when retrieving tag by name', DEBUG_DEVELOPER);
+
+    if (!$tag || !$type) {
+        return array();
+    }
+
+    $tagobject = core_tag_tag::get_by_name(core_tag_area::get_collection('', $type), $tag);
+    return $tagobject->get_tagged_items('', $type, $limitfrom, $limitnum);
+}
+
+/**
+ * Adds one or more tag in the database.  This function should not be called directly : you should
+ * use tag_set.
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param   mixed    $tags     one tag, or an array of tags, to be created
+ * @param   string   $type     type of tag to be created ("default" is the default value and "official" is the only other supported
+ *                             value at this time). An official tag is kept even if there are no records tagged with it.
+ * @return array     $tags ids indexed by their lowercase normalized names. Any boolean false in the array indicates an error while
+ *                             adding the tag.
+ */
+function tag_add($tags, $type="default") {
+    debugging('Function tag_add() is deprecated. You can use core_tag_tag::create_if_missing(), however it should not be necessary ' .
+        'since tags are created automatically when assigned to items', DEBUG_DEVELOPER);
+    if (!is_array($tags)) {
+        $tags = array($tags);
+    }
+    $objects = core_tag_tag::create_if_missing(core_tag_collection::get_default(), $tags, $type === 'official');
+
+    // New function returns the tags in different format, for BC we keep the format that this function used to have.
+    $rv = array();
+    foreach ($objects as $name => $tagobject) {
+        if (isset($tagobject->id)) {
+            $rv[$tagobject->name] = $tagobject->id;
+        } else {
+            $rv[$name] = false;
+        }
+    }
+    return $rv;
+}
+
+/**
+ * Assigns a tag to a record; if the record already exists, the time and ordering will be updated.
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param string $record_type the type of the record that will be tagged
+ * @param int $record_id the id of the record that will be tagged
+ * @param string $tagid the tag id to set on the record.
+ * @param int $ordering the order of the instance for this record
+ * @param int $userid (optional) only required for course tagging
+ * @param string|null $component the component that was tagged
+ * @param int|null $contextid the context id of where this tag was assigned
+ * @return bool true on success, false otherwise
+ */
+function tag_assign($record_type, $record_id, $tagid, $ordering, $userid = 0, $component = null, $contextid = null) {
+    global $DB;
+    $message = 'Function tag_assign() is deprecated. Use core_tag_tag::set_item_tags() or core_tag_tag::add_item_tag() instead. ' .
+        'Tag instance ordering should not be set manually';
+    if ($component === null || $contextid === null) {
+        $message .= '. You should specify the component and contextid of the item being tagged in your call to tag_assign.';
+    }
+    debugging($message, DEBUG_DEVELOPER);
+
+    if ($contextid) {
+        $context = context::instance_by_id($contextid);
+    } else {
+        $context = context_system::instance();
+    }
+
+    // Get the tag.
+    $tag = $DB->get_record('tag', array('id' => $tagid), 'name, rawname', MUST_EXIST);
+
+    $taginstanceid = core_tag_tag::add_item_tag($component, $record_type, $record_id, $context, $tag->rawname, $userid);
+
+    // Alter the "ordering" of tag_instance. This should never be done manually and only remains here for the backward compatibility.
+    $taginstance = new stdClass();
+    $taginstance->id = $taginstanceid;
+    $taginstance->ordering     = $ordering;
+    $taginstance->timemodified = time();
+
+    $DB->update_record('tag_instance', $taginstance);
+
+    return true;
+}
+
+/**
+ * Count how many records are tagged with a specific tag.
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param   string   $record_type record to look for ('post', 'user', etc.)
+ * @param   int      $tagid       is a single tag id
+ * @return  int      number of mathing tags.
+ */
+function tag_record_count($record_type, $tagid) {
+    debugging('Method tag_record_count() is deprecated and replaced with core_tag_tag::get($tagid)->count_tagged_items(). '.
+        'Component is now required when retrieving tag instances.', DEBUG_DEVELOPER);
+    return core_tag_tag::get($tagid)->count_tagged_items('', $record_type);
+}
+
+/**
+ * Determine if a record is tagged with a specific tag
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param   string   $record_type the record type to look for
+ * @param   int      $record_id   the record id to look for
+ * @param   string   $tag         a tag name
+ * @return  bool/int true if it is tagged, 0 (false) otherwise
+ */
+function tag_record_tagged_with($record_type, $record_id, $tag) {
+    debugging('Method tag_record_tagged_with() is deprecated and replaced with core_tag_tag::get($tagid)->is_item_tagged_with(). '.
+        'Component is now required when retrieving tag instances.', DEBUG_DEVELOPER);
+    return core_tag_tag::is_item_tagged_with('', $record_type, $record_id, $tag);
+}
+
+/**
+ * Flag a tag as inappropriate.
+ *
+ * @deprecated since 3.1
+ * @param int|array $tagids a single tagid, or an array of tagids
+ */
+function tag_set_flag($tagids) {
+    debugging('Function tag_set_flag() is deprecated and replaced with core_tag_tag::get($tagid)->flag().', DEBUG_DEVELOPER);
+    $tagids = (array) $tagids;
+    foreach ($tagids as $tagid) {
+        if ($tag = core_tag_tag::get($tagid, '*')) {
+            $tag->flag();
+        }
+    }
+}
+
+/**
+ * Remove the inappropriate flag on a tag.
+ *
+ * @deprecated since 3.1
+ * @param int|array $tagids a single tagid, or an array of tagids
+ */
+function tag_unset_flag($tagids) {
+    debugging('Function tag_unset_flag() is deprecated and replaced with core_tag_tag::get($tagid)->reset_flag().', DEBUG_DEVELOPER);
+    $tagids = (array) $tagids;
+    foreach ($tagids as $tagid) {
+        if ($tag = core_tag_tag::get($tagid, '*')) {
+            $tag->reset_flag();
+        }
+    }
+}
+
+/**
+ * Prints or returns a HTML tag cloud with varying classes styles depending on the popularity and type of each tag.
+ *
+ * @deprecated since 3.1
+ *
+ * @param    array     $tagset Array of tags to display
+ * @param    int       $nr_of_tags Limit for the number of tags to return/display, used if $tagset is null
+ * @param    bool      $return     if true the function will return the generated tag cloud instead of displaying it.
+ * @param    string    $sort (optional) selected sorting, default is alpha sort (name) also timemodified or popularity
+ * @return string|null a HTML string or null if this function does the output
+ */
+function tag_print_cloud($tagset=null, $nr_of_tags=150, $return=false, $sort='') {
+    global $OUTPUT;
+
+    debugging('Function tag_print_cloud() is deprecated and replaced with function core_tag_collection::get_tag_cloud(), '
+            . 'templateable core_tag\output\tagcloud and template core_tag/tagcloud.', DEBUG_DEVELOPER);
+
+    // Set up sort global - used to pass sort type into core_tag_collection::cloud_sort through usort() avoiding multiple sort functions.
+    if ($sort == 'popularity') {
+        $sort = 'count';
+    } else if ($sort == 'date') {
+        $sort = 'timemodified';
+    } else {
+        $sort = 'name';
+    }
+
+    if (is_null($tagset)) {
+        // No tag set received, so fetch tags from database.
+        // Always add query by tagcollid even when it's not known to make use of the table index.
+        $tagcloud = core_tag_collection::get_tag_cloud(0, '', $nr_of_tags, $sort);
+    } else {
+        $tagsincloud = $tagset;
+
+        $etags = array();
+        foreach ($tagsincloud as $tag) {
+            $etags[] = $tag;
+        }
+
+        core_tag_collection::$cloudsortfield = $sort;
+        usort($tagsincloud, "core_tag_collection::cloud_sort");
+
+        $tagcloud = new \core_tag\output\tagcloud($tagsincloud);
+    }
+
+    $output = $OUTPUT->render_from_template('core_tag/tagcloud', $tagcloud->export_for_template($OUTPUT));
+    if ($return) {
+        return $output;
+    } else {
+        echo $output;
     }
 }
 
@@ -2744,3 +3139,838 @@ function tag_autocomplete($text) {
                                    FROM {tag} tg
                                   WHERE tg.name LIKE ?", array(core_text::strtolower($text)."%"));
 }
+
+/**
+ * Prints a box with the description of a tag and its related tags
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param   stdClass    $tag_object
+ * @param   bool        $return     if true the function will return the generated tag cloud instead of displaying it.
+ * @return  string/null a HTML box showing a description of the tag object and it's relationsips or null if output is done directly
+ *                      in the function.
+ */
+function tag_print_description_box($tag_object, $return=false) {
+    global $USER, $CFG, $OUTPUT;
+    require_once($CFG->libdir.'/filelib.php');
+
+    debugging('Function tag_print_description_box() is deprecated without replacement. ' .
+            'See core_tag_renderer for similar code.', DEBUG_DEVELOPER);
+
+    $relatedtags = array();
+    if ($tag = core_tag_tag::get($tag_object->id)) {
+        $relatedtags = $tag->get_related_tags();
+    }
+
+    $content = !empty($tag_object->description);
+    $output = '';
+
+    if ($content) {
+        $output .= $OUTPUT->box_start('generalbox tag-description');
+    }
+
+    if (!empty($tag_object->description)) {
+        $options = new stdClass();
+        $options->para = false;
+        $options->overflowdiv = true;
+        $tag_object->description = file_rewrite_pluginfile_urls($tag_object->description, 'pluginfile.php', context_system::instance()->id, 'tag', 'description', $tag_object->id);
+        $output .= format_text($tag_object->description, $tag_object->descriptionformat, $options);
+    }
+
+    if ($content) {
+        $output .= $OUTPUT->box_end();
+    }
+
+    if ($relatedtags) {
+        $output .= $OUTPUT->tag_list($relatedtags, get_string('relatedtags', 'tag'), 'tag-relatedtags');
+    }
+
+    if ($return) {
+        return $output;
+    } else {
+        echo $output;
+    }
+}
+
+/**
+ * Prints a box that contains the management links of a tag
+ *
+ * @deprecated since 3.1
+ * @param  core_tag_tag|stdClass    $tag_object
+ * @param  bool        $return     if true the function will return the generated tag cloud instead of displaying it.
+ * @return string|null a HTML string or null if this function does the output
+ */
+function tag_print_management_box($tag_object, $return=false) {
+    global $USER, $CFG, $OUTPUT;
+
+    debugging('Function tag_print_description_box() is deprecated without replacement. ' .
+            'See core_tag_renderer for similar code.', DEBUG_DEVELOPER);
+
+    $tagname  = core_tag_tag::make_display_name($tag_object);
+    $output = '';
+
+    if (!isguestuser()) {
+        $output .= $OUTPUT->box_start('box','tag-management-box');
+        $systemcontext   = context_system::instance();
+        $links = array();
+
+        // Add a link for users to add/remove this from their interests
+        if (core_tag_tag::is_enabled('core', 'user') && core_tag_area::get_collection('core', 'user') == $tag_object->tagcollid) {
+            if (core_tag_tag::is_item_tagged_with('core', 'user', $USER->id, $tag_object->name)) {
+                $links[] = '<a href="'. $CFG->wwwroot .'/tag/user.php?action=removeinterest&amp;sesskey='. sesskey() .
+                        '&amp;tag='. rawurlencode($tag_object->name) .'">'.
+                        get_string('removetagfrommyinterests', 'tag', $tagname) .'</a>';
+            } else {
+                $links[] = '<a href="'. $CFG->wwwroot .'/tag/user.php?action=addinterest&amp;sesskey='. sesskey() .
+                        '&amp;tag='. rawurlencode($tag_object->name) .'">'.
+                        get_string('addtagtomyinterests', 'tag', $tagname) .'</a>';
+            }
+        }
+
+        // Flag as inappropriate link.  Only people with moodle/tag:flag capability.
+        if (has_capability('moodle/tag:flag', $systemcontext)) {
+            $links[] = '<a href="'. $CFG->wwwroot .'/tag/user.php?action=flaginappropriate&amp;sesskey='.
+                    sesskey() . '&amp;id='. $tag_object->id . '">'. get_string('flagasinappropriate',
+                            'tag', rawurlencode($tagname)) .'</a>';
+        }
+
+        // Edit tag: Only people with moodle/tag:edit capability who either have it as an interest or can manage tags
+        if (has_capability('moodle/tag:edit', $systemcontext) ||
+            has_capability('moodle/tag:manage', $systemcontext)) {
+            $links[] = '<a href="' . $CFG->wwwroot . '/tag/edit.php?id=' . $tag_object->id . '">' .
+                    get_string('edittag', 'tag') . '</a>';
+        }
+
+        $output .= implode(' | ', $links);
+        $output .= $OUTPUT->box_end();
+    }
+
+    if ($return) {
+        return $output;
+    } else {
+        echo $output;
+    }
+}
+
+/**
+ * Prints the tag search box
+ *
+ * @deprecated since 3.1
+ * @param  bool        $return if true return html string
+ * @return string|null a HTML string or null if this function does the output
+ */
+function tag_print_search_box($return=false) {
+    global $CFG, $OUTPUT;
+
+    debugging('Function tag_print_search_box() is deprecated without replacement. ' .
+            'See core_tag_renderer for similar code.', DEBUG_DEVELOPER);
+
+    $query = optional_param('query', '', PARAM_RAW);
+
+    $output = $OUTPUT->box_start('','tag-search-box');
+    $output .= '<form action="'.$CFG->wwwroot.'/tag/search.php" style="display:inline">';
+    $output .= '<div>';
+    $output .= '<label class="accesshide" for="searchform_search">'.get_string('searchtags', 'tag').'</label>';
+    $output .= '<input id="searchform_search" name="query" type="text" size="40" value="'.s($query).'" />';
+    $output .= '<button id="searchform_button" type="submit">'. get_string('search', 'tag') .'</button><br />';
+    $output .= '</div>';
+    $output .= '</form>';
+    $output .= $OUTPUT->box_end();
+
+    if ($return) {
+        return $output;
+    }
+    else {
+        echo $output;
+    }
+}
+
+/**
+ * Prints the tag search results
+ *
+ * @deprecated since 3.1
+ * @param string       $query text that tag names will be matched against
+ * @param int          $page current page
+ * @param int          $perpage nr of users displayed per page
+ * @param bool         $return if true return html string
+ * @return string|null a HTML string or null if this function does the output
+ */
+function tag_print_search_results($query,  $page, $perpage, $return=false) {
+    global $CFG, $USER, $OUTPUT;
+
+    debugging('Function tag_print_search_results() is deprecated without replacement. ' .
+            'In /tag/search.php the search results are printed using the core_tag/tagcloud template.', DEBUG_DEVELOPER);
+
+    $query = clean_param($query, PARAM_TAG);
+
+    $count = count(tag_find_tags($query, false));
+    $tags = array();
+
+    if ( $found_tags = tag_find_tags($query, true,  $page * $perpage, $perpage) ) {
+        $tags = array_values($found_tags);
+    }
+
+    $baseurl = $CFG->wwwroot.'/tag/search.php?query='. rawurlencode($query);
+    $output = '';
+
+    // link "Add $query to my interests"
+    $addtaglink = '';
+    if (core_tag_tag::is_enabled('core', 'user') && !core_tag_tag::is_item_tagged_with('core', 'user', $USER->id, $query)) {
+        $addtaglink = html_writer::link(new moodle_url('/tag/user.php', array('action' => 'addinterest', 'sesskey' => sesskey(),
+            'tag' => $query)), get_string('addtagtomyinterests', 'tag', s($query)));
+    }
+
+    if ( !empty($tags) ) { // there are results to display!!
+        $output .= $OUTPUT->heading(get_string('searchresultsfor', 'tag', htmlspecialchars($query)) ." : {$count}", 3, 'main');
+
+        //print a link "Add $query to my interests"
+        if (!empty($addtaglink)) {
+            $output .= $OUTPUT->box($addtaglink, 'box', 'tag-management-box');
+        }
+
+        $nr_of_lis_per_ul = 6;
+        $nr_of_uls = ceil( sizeof($tags) / $nr_of_lis_per_ul );
+
+        $output .= '<ul id="tag-search-results">';
+        for($i = 0; $i < $nr_of_uls; $i++) {
+            foreach (array_slice($tags, $i * $nr_of_lis_per_ul, $nr_of_lis_per_ul) as $tag) {
+                $output .= '<li>';
+                $tag_link = html_writer::link(core_tag_tag::make_url($tag->tagcollid, $tag->rawname),
+                    core_tag_tag::make_display_name($tag));
+                $output .= $tag_link;
+                $output .= '</li>';
+            }
+        }
+        $output .= '</ul>';
+        $output .= '<div>&nbsp;</div>'; // <-- small layout hack in order to look good in Firefox
+
+        $output .= $OUTPUT->paging_bar($count, $page, $perpage, $baseurl);
+    }
+    else { //no results were found!!
+        $output .= $OUTPUT->heading(get_string('noresultsfor', 'tag', htmlspecialchars($query)), 3, 'main');
+
+        //print a link "Add $query to my interests"
+        if (!empty($addtaglink)) {
+            $output .= $OUTPUT->box($addtaglink, 'box', 'tag-management-box');
+        }
+    }
+
+    if ($return) {
+        return $output;
+    }
+    else {
+        echo $output;
+    }
+}
+
+/**
+ * Prints a table of the users tagged with the tag passed as argument
+ *
+ * @deprecated since 3.1
+ * @param  stdClass    $tagobject the tag we wish to return data for
+ * @param  int         $limitfrom (optional, required if $limitnum is set) prints users starting at this point.
+ * @param  int         $limitnum (optional, required if $limitfrom is set) prints this many users.
+ * @param  bool        $return if true return html string
+ * @return string|null a HTML string or null if this function does the output
+ */
+function tag_print_tagged_users_table($tagobject, $limitfrom='', $limitnum='', $return=false) {
+
+    debugging('Function tag_print_tagged_users_table() is deprecated without replacement. ' .
+            'See core_user_renderer for similar code.', DEBUG_DEVELOPER);
+
+    //List of users with this tag
+    $tagobject = core_tag_tag::get($tagobject->id);
+    $userlist = $tagobject->get_tagged_items('core', 'user', $limitfrom, $limitnum);
+
+    $output = tag_print_user_list($userlist, true);
+
+    if ($return) {
+        return $output;
+    }
+    else {
+        echo $output;
+    }
+}
+
+/**
+ * Prints an individual user box
+ *
+ * @deprecated since 3.1
+ * @param user_object  $user  (contains the following fields: id, firstname, lastname and picture)
+ * @param bool         $return if true return html string
+ * @return string|null a HTML string or null if this function does the output
+ */
+function tag_print_user_box($user, $return=false) {
+    global $CFG, $OUTPUT;
+
+    debugging('Function tag_print_user_box() is deprecated without replacement. ' .
+            'See core_user_renderer for similar code.', DEBUG_DEVELOPER);
+
+    $usercontext = context_user::instance($user->id);
+    $profilelink = '';
+
+    if ($usercontext and (has_capability('moodle/user:viewdetails', $usercontext) || has_coursecontact_role($user->id))) {
+        $profilelink = $CFG->wwwroot .'/user/view.php?id='. $user->id;
+    }
+
+    $output = $OUTPUT->box_start('user-box', 'user'. $user->id);
+    $fullname = fullname($user);
+    $alt = '';
+
+    if (!empty($profilelink)) {
+        $output .= '<a href="'. $profilelink .'">';
+        $alt = $fullname;
+    }
+
+    $output .= $OUTPUT->user_picture($user, array('size'=>100));
+    $output .= '<br />';
+
+    if (!empty($profilelink)) {
+        $output .= '</a>';
+    }
+
+    //truncate name if it's too big
+    if (core_text::strlen($fullname) > 26) {
+        $fullname = core_text::substr($fullname, 0, 26) .'...';
+    }
+
+    $output .= '<strong>'. $fullname .'</strong>';
+    $output .= $OUTPUT->box_end();
+
+    if ($return) {
+        return $output;
+    }
+    else {
+        echo $output;
+    }
+}
+
+/**
+ * Prints a list of users
+ *
+ * @deprecated since 3.1
+ * @param  array       $userlist an array of user objects
+ * @param  bool        $return if true return html string, otherwise output the result
+ * @return string|null a HTML string or null if this function does the output
+ */
+function tag_print_user_list($userlist, $return=false) {
+
+    debugging('Function tag_print_user_list() is deprecated without replacement. ' .
+            'See core_user_renderer for similar code.', DEBUG_DEVELOPER);
+
+    $output = '<div><ul class="inline-list">';
+
+    foreach ($userlist as $user){
+        $output .= '<li>'. tag_print_user_box($user, true) ."</li>\n";
+    }
+    $output .= "</ul></div>\n";
+
+    if ($return) {
+        return $output;
+    }
+    else {
+        echo $output;
+    }
+}
+
+/**
+ * Function that returns the name that should be displayed for a specific tag
+ *
+ * @package  core_tag
+ * @category tag
+ * @deprecated since 3.1
+ * @param    stdClass|core_tag_tag   $tagobject a line out of tag table, as returned by the adobd functions
+ * @param    int      $html TAG_RETURN_HTML (default) will return htmlspecialchars encoded string, TAG_RETURN_TEXT will not encode.
+ * @return   string
+ */
+function tag_display_name($tagobject, $html=TAG_RETURN_HTML) {
+    debugging('Function tag_display_name() is deprecated. Use core_tag_tag::make_display_name().', DEBUG_DEVELOPER);
+    if (!isset($tagobject->name)) {
+        return '';
+    }
+    return core_tag_tag::make_display_name($tagobject, $html != TAG_RETURN_TEXT);
+}
+
+/**
+ * Function that normalizes a list of tag names.
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param   array/string $rawtags array of tags, or a single tag.
+ * @param   int          $case    case to use for returned value (default: lower case). Either TAG_CASE_LOWER (default) or TAG_CASE_ORIGINAL
+ * @return  array        lowercased normalized tags, indexed by the normalized tag, in the same order as the original array.
+ *                       (Eg: 'Banana' => 'banana').
+ */
+function tag_normalize($rawtags, $case = TAG_CASE_LOWER) {
+    debugging('Function tag_normalize() is deprecated. Use core_tag_tag::normalize().', DEBUG_DEVELOPER);
+
+    if ( !is_array($rawtags) ) {
+        $rawtags = array($rawtags);
+    }
+
+    return core_tag_tag::normalize($rawtags, $case == TAG_CASE_LOWER);
+}
+
+/**
+ * Get a comma-separated list of tags related to another tag.
+ *
+ * @package  core_tag
+ * @deprecated since 3.1
+ * @param    array    $related_tags the array returned by tag_get_related_tags
+ * @param    int      $html    either TAG_RETURN_HTML (default) or TAG_RETURN_TEXT : return html links, or just text.
+ * @return   string   comma-separated list
+ */
+function tag_get_related_tags_csv($related_tags, $html=TAG_RETURN_HTML) {
+    global $OUTPUT;
+    debugging('Method tag_get_related_tags_csv() is deprecated. Consider '
+            . 'looping through array or using $OUTPUT->tag_list(core_tag_tag::get_item_tags())',
+        DEBUG_DEVELOPER);
+    if ($html != TAG_RETURN_TEXT) {
+        return $OUTPUT->tag_list($related_tags, '');
+    }
+
+    $tagsnames = array();
+    foreach ($related_tags as $tag) {
+        $tagsnames[] = core_tag_tag::make_display_name($tag, false);
+    }
+    return implode(', ', $tagsnames);
+}
+
+/**
+ * Used to require that the return value from a function is an array.
+ * Only used in the deprecated function {@link tag_get_id()}
+ * @deprecated since 3.1
+ */
+define('TAG_RETURN_ARRAY', 0);
+/**
+ * Used to require that the return value from a function is an object.
+ * Only used in the deprecated function {@link tag_get_id()}
+ * @deprecated since 3.1
+ */
+define('TAG_RETURN_OBJECT', 1);
+/**
+ * Use to specify that HTML free text is expected to be returned from a function.
+ * Only used in deprecated functions {@link tag_get_tags_csv()}, {@link tag_display_name()},
+ * {@link tag_get_related_tags_csv()}
+ * @deprecated since 3.1
+ */
+define('TAG_RETURN_TEXT', 2);
+/**
+ * Use to specify that encoded HTML is expected to be returned from a function.
+ * Only used in deprecated functions {@link tag_get_tags_csv()}, {@link tag_display_name()},
+ * {@link tag_get_related_tags_csv()}
+ * @deprecated since 3.1
+ */
+define('TAG_RETURN_HTML', 3);
+
+/**
+ * Used to specify that we wish a lowercased string to be returned
+ * Only used in deprecated function {@link tag_normalize()}
+ * @deprecated since 3.1
+ */
+define('TAG_CASE_LOWER', 0);
+/**
+ * Used to specify that we do not wish the case of the returned string to change
+ * Only used in deprecated function {@link tag_normalize()}
+ * @deprecated since 3.1
+ */
+define('TAG_CASE_ORIGINAL', 1);
+
+/**
+ * Used to specify that we want all related tags returned, no matter how they are related.
+ * Only used in deprecated function {@link tag_get_related_tags()}
+ * @deprecated since 3.1
+ */
+define('TAG_RELATED_ALL', 0);
+/**
+ * Used to specify that we only want back tags that were manually related.
+ * Only used in deprecated function {@link tag_get_related_tags()}
+ * @deprecated since 3.1
+ */
+define('TAG_RELATED_MANUAL', 1);
+/**
+ * Used to specify that we only want back tags where the relationship was automatically correlated.
+ * Only used in deprecated function {@link tag_get_related_tags()}
+ * @deprecated since 3.1
+ */
+define('TAG_RELATED_CORRELATED', 2);
+
+/**
+ * Set the tags assigned to a record.  This overwrites the current tags.
+ *
+ * This function is meant to be fed the string coming up from the user interface, which contains all tags assigned to a record.
+ *
+ * Due to API change $component and $contextid are now required. Instead of
+ * calling  this function you can use {@link core_tag_tag::set_item_tags()} or
+ * {@link core_tag_tag::set_related_tags()}
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, 'tag' for tags, etc.)
+ * @param int $itemid the id of the record to tag
+ * @param array $tags the array of tags to set on the record. If given an empty array, all tags will be removed.
+ * @param string|null $component the component that was tagged
+ * @param int|null $contextid the context id of where this tag was assigned
+ * @return bool|null
+ */
+function tag_set($itemtype, $itemid, $tags, $component = null, $contextid = null) {
+    debugging('Function tag_set() is deprecated. Use ' .
+        ' core_tag_tag::set_item_tags() instead', DEBUG_DEVELOPER);
+
+    if ($itemtype === 'tag') {
+        return core_tag_tag::get($itemid, '*', MUST_EXIST)->set_related_tags($tags);
+    } else {
+        $context = $contextid ? context::instance_by_id($contextid) : context_system::instance();
+        return core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context, $tags);
+    }
+}
+
+/**
+ * Adds a tag to a record, without overwriting the current tags.
+ *
+ * This function remains here for backward compatiblity. It is recommended to use
+ * {@link core_tag_tag::add_item_tag()} or {@link core_tag_tag::add_related_tags()} instead
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.)
+ * @param int $itemid the id of the record to tag
+ * @param string $tag the tag to add
+ * @param string|null $component the component that was tagged
+ * @param int|null $contextid the context id of where this tag was assigned
+ * @return bool|null
+ */
+function tag_set_add($itemtype, $itemid, $tag, $component = null, $contextid = null) {
+    debugging('Function tag_set_add() is deprecated. Use ' .
+        ' core_tag_tag::add_item_tag() instead', DEBUG_DEVELOPER);
+
+    if ($itemtype === 'tag') {
+        return core_tag_tag::get($itemid, '*', MUST_EXIST)->add_related_tags(array($tag));
+    } else {
+        $context = $contextid ? context::instance_by_id($contextid) : context_system::instance();
+        return core_tag_tag::add_item_tag($component, $itemtype, $itemid, $context, $tag);
+    }
+}
+
+/**
+ * Removes a tag from a record, without overwriting other current tags.
+ *
+ * This function remains here for backward compatiblity. It is recommended to use
+ * {@link core_tag_tag::remove_item_tag()} instead
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.)
+ * @param int $itemid the id of the record to tag
+ * @param string $tag the tag to delete
+ * @param string|null $component the component that was tagged
+ * @param int|null $contextid the context id of where this tag was assigned
+ * @return bool|null
+ */
+function tag_set_delete($itemtype, $itemid, $tag, $component = null, $contextid = null) {
+    debugging('Function tag_set_delete() is deprecated. Use ' .
+        ' core_tag_tag::remove_item_tag() instead', DEBUG_DEVELOPER);
+    return core_tag_tag::remove_item_tag($component, $itemtype, $itemid, $tag);
+}
+
+/**
+ * Simple function to just return a single tag object when you know the name or something
+ *
+ * See also {@link core_tag_tag::get()} and {@link core_tag_tag::get_by_name()}
+ *
+ * @package  core_tag
+ * @deprecated since 3.1
+ * @param    string $field        which field do we use to identify the tag: id, name or rawname
+ * @param    string $value        the required value of the aforementioned field
+ * @param    string $returnfields which fields do we want returned. This is a comma seperated string containing any combination of
+ *                                'id', 'name', 'rawname' or '*' to include all fields.
+ * @return   mixed  tag object
+ */
+function tag_get($field, $value, $returnfields='id, name, rawname, tagcollid') {
+    global $DB;
+    debugging('Function tag_get() is deprecated. Use ' .
+        ' core_tag_tag::get() or core_tag_tag::get_by_name()',
+        DEBUG_DEVELOPER);
+    if ($field === 'id') {
+        $tag = core_tag_tag::get((int)$value, $returnfields);
+    } else if ($field === 'name') {
+        $tag = core_tag_tag::get_by_name(0, $value, $returnfields);
+    } else {
+        $params = array($field => $value);
+        return $DB->get_record('tag', $params, $returnfields);
+    }
+    if ($tag) {
+        return $tag->to_object();
+    }
+    return null;
+}
+
+/**
+ * Returns tags related to a tag
+ *
+ * Related tags of a tag come from two sources:
+ *   - manually added related tags, which are tag_instance entries for that tag
+ *   - correlated tags, which are calculated
+ *
+ * @package  core_tag
+ * @deprecated since 3.1
+ * @param    string   $tagid          is a single **normalized** tag name or the id of a tag
+ * @param    int      $type           the function will return either manually (TAG_RELATED_MANUAL) related tags or correlated
+ *                                    (TAG_RELATED_CORRELATED) tags. Default is TAG_RELATED_ALL, which returns everything.
+ * @param    int      $limitnum       (optional) return a subset comprising this many records, the default is 10
+ * @return   array    an array of tag objects
+ */
+function tag_get_related_tags($tagid, $type=TAG_RELATED_ALL, $limitnum=10) {
+    debugging('Method tag_get_related_tags() is deprecated, '
+        . 'use core_tag_tag::get_correlated_tags(), core_tag_tag::get_related_tags() or '
+        . 'core_tag_tag::get_manual_related_tags()', DEBUG_DEVELOPER);
+    $result = array();
+    if ($tag = core_tag_tag::get($tagid)) {
+        if ($type == TAG_RELATED_CORRELATED) {
+            $tags = $tag->get_correlated_tags();
+        } else if ($type == TAG_RELATED_MANUAL) {
+            $tags = $tag->get_manual_related_tags();
+        } else {
+            $tags = $tag->get_related_tags();
+        }
+        $tags = array_slice($tags, 0, $limitnum);
+        foreach ($tags as $id => $tag) {
+            $result[$id] = $tag->to_object();
+        }
+    }
+    return $result;
+}
+
+/**
+ * Delete one or more tag, and all their instances if there are any left.
+ *
+ * @package  core_tag
+ * @deprecated since 3.1
+ * @param    mixed    $tagids one tagid (int), or one array of tagids to delete
+ * @return   bool     true on success, false otherwise
+ */
+function tag_delete($tagids) {
+    debugging('Method tag_delete() is deprecated, use core_tag_tag::delete_tags()',
+        DEBUG_DEVELOPER);
+    return core_tag_tag::delete_tags($tagids);
+}
+
+/**
+ * Deletes all the tag instances given a component and an optional contextid.
+ *
+ * @deprecated since 3.1
+ * @param string $component
+ * @param int $contextid if null, then we delete all tag instances for the $component
+ */
+function tag_delete_instances($component, $contextid = null) {
+    debugging('Method tag_delete() is deprecated, use core_tag_tag::delete_instances()',
+        DEBUG_DEVELOPER);
+    core_tag_tag::delete_instances($component, null, $contextid);
+}
+
+/**
+ * Clean up the tag tables, making sure all tagged object still exists.
+ *
+ * This should normally not be necessary, but in case related tags are not deleted when the tagged record is removed, this should be
+ * done once in a while, perhaps on an occasional cron run.  On a site with lots of tags, this could become an expensive function to
+ * call: don't run at peak time.
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ */
+function tag_cleanup() {
+    debugging('Method tag_cleanup() is deprecated, use \core\task\tag_cron_task::cleanup()',
+        DEBUG_DEVELOPER);
+
+    $task = new \core\task\tag_cron_task();
+    return $task->cleanup();
+}
+
+/**
+ * This function will delete numerous tag instances efficiently.
+ * This removes tag instances only. It doesn't check to see if it is the last use of a tag.
+ *
+ * @deprecated since 3.1
+ * @param array $instances An array of tag instance objects with the addition of the tagname and tagrawname
+ *        (used for recording a delete event).
+ */
+function tag_bulk_delete_instances($instances) {
+    debugging('Method tag_bulk_delete_instances() is deprecated, '
+        . 'use \core\task\tag_cron_task::bulk_delete_instances()',
+        DEBUG_DEVELOPER);
+
+    $task = new \core\task\tag_cron_task();
+    return $task->bulk_delete_instances($instances);
+}
+
+/**
+ * Calculates and stores the correlated tags of all tags. The correlations are stored in the 'tag_correlation' table.
+ *
+ * Two tags are correlated if they appear together a lot. Ex.: Users tagged with "computers" will probably also be tagged with "algorithms".
+ *
+ * The rationale for the 'tag_correlation' table is performance. It works as a cache for a potentially heavy load query done at the
+ * 'tag_instance' table. So, the 'tag_correlation' table stores redundant information derived from the 'tag_instance' table.
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param   int      $mincorrelation Only tags with more than $mincorrelation correlations will be identified.
+ */
+function tag_compute_correlations($mincorrelation = 2) {
+    debugging('Method tag_compute_correlations() is deprecated, '
+        . 'use \core\task\tag_cron_task::compute_correlations()',
+        DEBUG_DEVELOPER);
+
+    $task = new \core\task\tag_cron_task();
+    return $task->compute_correlations($mincorrelation);
+}
+
+/**
+ * This function processes a tag correlation and makes changes in the database as required.
+ *
+ * The tag correlation object needs have both a tagid property and a correlatedtags property that is an array.
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param   stdClass $tagcorrelation
+ * @return  int/bool The id of the tag correlation that was just processed or false.
+ */
+function tag_process_computed_correlation(stdClass $tagcorrelation) {
+    debugging('Method tag_process_computed_correlation() is deprecated, '
+        . 'use \core\task\tag_cron_task::process_computed_correlation()',
+        DEBUG_DEVELOPER);
+
+    $task = new \core\task\tag_cron_task();
+    return $task->process_computed_correlation($tagcorrelation);
+}
+
+/**
+ * Tasks that should be performed at cron time
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ */
+function tag_cron() {
+    debugging('Method tag_cron() is deprecated, use \core\task\tag_cron_task::execute()',
+        DEBUG_DEVELOPER);
+
+    $task = new \core\task\tag_cron_task();
+    $task->execute();
+}
+
+/**
+ * Search for tags with names that match some text
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param   string        $text      escaped string that the tag names will be matched against
+ * @param   bool          $ordered   If true, tags are ordered by their popularity. If false, no ordering.
+ * @param   int/string    $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point.
+ * @param   int/string    $limitnum  (optional, required if $limitfrom is set) return a subset comprising this many records.
+ * @param   int           $tagcollid
+ * @return  array/boolean an array of objects, or false if no records were found or an error occured.
+ */
+function tag_find_tags($text, $ordered=true, $limitfrom='', $limitnum='', $tagcollid = null) {
+    debugging('Method tag_find_tags() is deprecated without replacement', DEBUG_DEVELOPER);
+    global $DB;
+
+    $text = core_text::strtolower(clean_param($text, PARAM_TAG));
+
+    list($sql, $params) = $DB->get_in_or_equal($tagcollid ? array($tagcollid) :
+        array_keys(core_tag_collection::get_collections(true)));
+    array_unshift($params, "%{$text}%");
+
+    if ($ordered) {
+        $query = "SELECT tg.id, tg.name, tg.rawname, tg.tagcollid, COUNT(ti.id) AS count
+                    FROM {tag} tg LEFT JOIN {tag_instance} ti ON tg.id = ti.tagid
+                   WHERE tg.name LIKE ? AND tg.tagcollid $sql
+                GROUP BY tg.id, tg.name, tg.rawname
+                ORDER BY count DESC";
+    } else {
+        $query = "SELECT tg.id, tg.name, tg.rawname, tg.tagcollid
+                    FROM {tag} tg
+                   WHERE tg.name LIKE ? AND tg.tagcollid $sql";
+    }
+    return $DB->get_records_sql($query, $params, $limitfrom , $limitnum);
+}
+
+/**
+ * Get the name of a tag
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param   mixed    $tagids the id of the tag, or an array of ids
+ * @return  mixed    string name of one tag, or id-indexed array of strings
+ */
+function tag_get_name($tagids) {
+    debugging('Method tag_get_name() is deprecated without replacement', DEBUG_DEVELOPER);
+    global $DB;
+
+    if (!is_array($tagids)) {
+        if ($tag = $DB->get_record('tag', array('id'=>$tagids))) {
+            return $tag->name;
+        }
+        return false;
+    }
+
+    $tag_names = array();
+    foreach($DB->get_records_list('tag', 'id', $tagids) as $tag) {
+        $tag_names[$tag->id] = $tag->name;
+    }
+
+    return $tag_names;
+}
+
+/**
+ * Returns the correlated tags of a tag, retrieved from the tag_correlation table. Make sure cron runs, otherwise the table will be
+ * empty and this function won't return anything.
+ *
+ * Correlated tags are calculated in cron based on existing tag instances.
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param   int      $tagid   is a single tag id
+ * @param   int      $notused  this argument is no longer used
+ * @return  array    an array of tag objects or an empty if no correlated tags are found
+ */
+function tag_get_correlated($tagid, $notused = null) {
+    debugging('Method tag_get_correlated() is deprecated, '
+        . 'use core_tag_tag::get_correlated_tags()', DEBUG_DEVELOPER);
+    $result = array();
+    if ($tag = core_tag_tag::get($tagid)) {
+        $tags = $tag->get_correlated_tags(true);
+        // Convert to objects for backward-compatibility.
+        foreach ($tags as $id => $tag) {
+            $result[$id] = $tag->to_object();
+        }
+    }
+    return $result;
+}
+
+/**
+ * This function is used by print_tag_cloud, to usort() the tags in the cloud. See php.net/usort for the parameters documentation.
+ * This was originally in blocks/blog_tags/block_blog_tags.php, named blog_tags_sort().
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param   string $a Tag name to compare against $b
+ * @param   string $b Tag name to compare against $a
+ * @return  int    The result of the comparison/validation 1, 0 or -1
+ */
+function tag_cloud_sort($a, $b) {
+    debugging('Method tag_cloud_sort() is deprecated, similar method can be found in core_tag_collection::cloud_sort()', DEBUG_DEVELOPER);
+    global $CFG;
+
+    if (empty($CFG->tagsort)) {
+        $tagsort = 'name'; // by default, sort by name
+    } else {
+        $tagsort = $CFG->tagsort;
+    }
+
+    if (is_numeric($a->$tagsort)) {
+        return ($a->$tagsort == $b->$tagsort) ? 0 : ($a->$tagsort > $b->$tagsort) ? 1 : -1;
+    } elseif (is_string($a->$tagsort)) {
+        return strcmp($a->$tagsort, $b->$tagsort);
+    } else {
+        return 0;
+    }
+}
index a472767..096bbd9 100644 (file)
@@ -63,6 +63,12 @@ class MoodleQuickForm_tags extends MoodleQuickForm_autocomplete {
      */
     protected $showingofficial = false;
 
+    /**
+     * Options passed when creating an element.
+     * @var array
+     */
+    protected $tagsoptions = array();
+
     /**
      * Constructor
      *
@@ -72,27 +78,64 @@ class MoodleQuickForm_tags extends MoodleQuickForm_autocomplete {
      * @param mixed $attributes Either a typical HTML attribute string or an associative array.
      */
     public function __construct($elementName = null, $elementLabel = null, $options = array(), $attributes = null) {
-        if (!isset($options['display'])) {
-            $options['display'] = self::DEFAULTUI;
+        $validoptions = array();
+
+        if (!empty($options)) {
+            // Only execute it when the element was created and $options has values set by user.
+            // In onQuickFormEvent() we make sure that $options is not empty even if developer left it empty.
+            if (empty($options['display'])) {
+                $options['display'] = self::DEFAULTUI;
+            }
+            $this->tagsoptions = $options;
+
+            $this->showingofficial = $options['display'] != self::NOOFFICIAL;
+
+            if ($this->showingofficial) {
+                $validoptions = $this->load_official_tags();
+            }
+            // Option 'tags' allows us to type new tags.
+            if ($options['display'] == self::ONLYOFFICIAL) {
+                $attributes['tags'] = false;
+            } else {
+                $attributes['tags'] = true;
+            }
+            $attributes['multiple'] = 'multiple';
+            $attributes['placeholder'] = get_string('entertags', 'tag');
+            $attributes['showsuggestions'] = $this->showingofficial;
         }
 
-        $this->showingofficial = $options['display'] != MoodleQuickForm_tags::NOOFFICIAL;
+        parent::__construct($elementName, $elementLabel, $validoptions, $attributes);
+    }
 
-        $validoptions = array();
-        if ($this->showingofficial) {
-            $validoptions = $this->load_official_tags();
-        }
-        // 'tags' option allows us to type new tags.
-        if ($options['display'] == MoodleQuickForm_tags::ONLYOFFICIAL) {
-            $attributes['tags'] = false;
-        } else {
-            $attributes['tags'] = true;
+    /**
+     * Called by HTML_QuickForm whenever form event is made on this element
+     *
+     * @param string $event Name of event
+     * @param mixed $arg event arguments
+     * @param object $caller calling object
+     * @return bool
+     */
+    public function onQuickFormEvent($event, $arg, &$caller) {
+        if ($event === 'createElement') {
+            $arg[2] += array('itemtype' => '', 'component' => '');
         }
-        $attributes['multiple'] = 'multiple';
-        $attributes['placeholder'] = get_string('entertags', 'tag');
-        $attributes['showsuggestions'] = $this->showingofficial;
+        return parent::onQuickFormEvent($event, $arg, $caller);
+    }
 
-        parent::__construct($elementName, $elementLabel, $validoptions, $attributes);
+    /**
+     * Checks if tagging is enabled for this itemtype
+     *
+     * @return boolean
+     */
+    protected function is_tagging_enabled() {
+        if (!empty($this->tagsoptions['itemtype']) && !empty($this->tagsoptions['component'])) {
+            $enabled = core_tag_tag::is_enabled($this->tagsoptions['component'], $this->tagsoptions['itemtype']);
+            if ($enabled === false) {
+                return false;
+            }
+        }
+        // Backward compatibility with code developed before Moodle 3.0 where itemtype/component were not specified.
+        return true;
     }
 
     /**
@@ -105,21 +148,41 @@ class MoodleQuickForm_tags extends MoodleQuickForm_autocomplete {
         self::__construct($elementName, $elementLabel, $options, $attributes);
     }
 
+    /**
+     * Finds the tag collection to use for official tag selector
+     *
+     * @return int
+     */
+    protected function get_tag_collection() {
+        if (empty($this->tagsoptions['tagcollid']) && (empty($this->tagsoptions['itemtype']) ||
+                empty($this->tagsoptions['component']))) {
+            debugging('You need to specify \'itemtype\' and \'component\' of the tagged '
+                    . 'area in the tags form element options',
+                    DEBUG_DEVELOPER);
+        }
+        if (!empty($this->tagsoptions['tagcollid'])) {
+            return $this->tagsoptions['tagcollid'];
+        }
+        if ($this->tagsoptions['itemtype']) {
+            $this->tagsoptions['tagcollid'] = core_tag_area::get_collection($this->tagsoptions['component'],
+                    $this->tagsoptions['itemtype']);
+        } else {
+            $this->tagsoptions['tagcollid'] = core_tag_collection::get_default();
+        }
+        return $this->tagsoptions['tagcollid'];
+    }
+
     /**
      * Returns HTML for select form element.
      *
      * @return string
      */
     function toHtml(){
-        global $CFG, $OUTPUT;
-
-        if (empty($CFG->usetags)) {
-            debugging('A tags formslib field has been created even thought $CFG->usetags is false.', DEBUG_DEVELOPER);
-        }
+        global $OUTPUT;
 
         $managelink = '';
         if (has_capability('moodle/tag:manage', context_system::instance()) && $this->showingofficial) {
-            $url = $CFG->wwwroot .'/tag/manage.php';
+            $url = new moodle_url('/tag/manage.php', array('tc' => $this->get_tag_collection()));
             $managelink = ' ' . $OUTPUT->action_link($url, get_string('manageofficialtags', 'tag'));
         }
 
@@ -127,21 +190,47 @@ class MoodleQuickForm_tags extends MoodleQuickForm_autocomplete {
     }
 
     /**
-     * Internal function to load official tags
+     * Accepts a renderer
      *
-     * @access protected
+     * @param HTML_QuickForm_Renderer $renderer An HTML_QuickForm_Renderer object
+     * @param bool $required Whether a group is required
+     * @param string $error An error message associated with a group
+     */
+    public function accept(&$renderer, $required = false, $error = null) {
+        if ($this->is_tagging_enabled()) {
+            $renderer->renderElement($this, $required, $error);
+        } else {
+            $renderer->renderHidden($this);
+        }
+    }
+
+    /**
+     * Internal function to load official tags
      */
     protected function load_official_tags() {
         global $CFG, $DB;
-
+        if (!$this->is_tagging_enabled()) {
+            return array();
+        }
         $namefield = empty($CFG->keeptagnamecase) ? 'name' : 'rawname';
-        $records = $DB->get_records('tag', array('tagtype' => 'official'), $namefield, 'id,' . $namefield);
-        $tags = array();
+        $tags = $DB->get_records_menu('tag',
+            array('tagtype' => 'official', 'tagcollid' => $this->get_tag_collection()),
+            $namefield, 'id,' . $namefield);
+        return array_combine($tags, $tags);
+    }
 
-        foreach ($records as $record) {
-            $tags[$record->$namefield] = $record->$namefield;
+    /**
+     * Returns a 'safe' element's value
+     *
+     * @param  array  $submitValues array of submitted values to search
+     * @param  bool   $assoc        whether to return the value as associative array
+     * @return mixed
+     */
+    public function exportValue(&$submitValues, $assoc = false) {
+        if (!$this->is_tagging_enabled()) {
+            return $assoc ? array($this->getName() => array()) : array();
         }
-        return $tags;
-    }
 
+        return parent::exportValue($submitValues, $assoc);
+    }
 }
index 35fba72..858061e 100644 (file)
@@ -4103,6 +4103,23 @@ EOD;
         $html .= html_writer::end_tag('header');
         return $html;
     }
+
+    /**
+     * Displays the list of tags associated with an entry
+     *
+     * @param array $tags list of instances of core_tag or stdClass
+     * @param string $label label to display in front, by default 'Tags' (get_string('tags')), set to null
+     *               to use default, set to '' (empty string) to omit the label completely
+     * @param string $classes additional classes for the enclosing div element
+     * @param int $limit limit the number of tags to display, if size of $tags is more than this limit the "more" link
+     *               will be appended to the end, JS will toggle the rest of the tags
+     * @param context $pagecontext specify if needed to overwrite the current page context for the view tag link
+     * @return string
+     */
+    public function tag_list($tags, $label = null, $classes = '', $limit = 10, $pagecontext = null) {
+        $list = new \core_tag\output\taglist($tags, $label, $classes, $limit, $pagecontext);
+        return $this->render_from_template('core_tag/taglist', $list->export_for_template($this));
+    }
 }
 
 /**
index 7dfe2a4..756b15c 100644 (file)
@@ -847,6 +847,10 @@ EOD;
             $record['tagtype'] = 'default';
         }
 
+        if (!isset($record['tagcollid'])) {
+            $record['tagcollid'] = core_tag_collection::get_default();
+        }
+
         if (!isset($record['description'])) {
             $record['description'] = 'Tag description';
         }
index 32cb7f3..42e07fd 100644 (file)
@@ -174,7 +174,7 @@ class core_test_generator_testcase extends advanced_testcase {
         $this->assertEquals($course->id, $section->course);
 
         $course = $generator->create_course(array('tags' => 'Cat, Dog'));
-        $this->assertEquals('Cat, Dog', tag_get_tags_csv('course', $course->id, TAG_RETURN_TEXT));
+        $this->assertEquals(array('Cat', 'Dog'), array_values(core_tag_tag::get_item_tags_array('core', 'course', $course->id)));
 
         $scale = $generator->create_scale();
         $this->assertNotEmpty($scale);
index c7d74ef..02437f8 100644 (file)
@@ -31,8 +31,9 @@ information provided here is intended especially for developers.
     custom user filters. Similar deprecations in existing user_filter_* classes.
   * table_default_export_format_parent::table_default_export_format_parent() is
     deprecated, use parent::__construct() in extending classes.
-  * groups_delete_group_members() $showfeedback parameter has been removed and is no longer
-    respected. Users of this function should output their own feedback if required.
+* groups_delete_group_members() $showfeedback parameter has been removed and is no longer
+  respected. Users of this function should output their own feedback if required.
+* Number of changes to Tags API, see tag/upgrade.txt for more details
 
 === 3.0 ===
 
index c44cee9..1771153 100644 (file)
@@ -494,6 +494,7 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) {
                         message_update_processors($plug);
                     }
                     upgrade_plugin_mnet_functions($component);
+                    core_tag_area::reset_definitions_for_component($component);
                     $endcallback($component, true, $verbose);
                 }
             }
@@ -532,6 +533,7 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) {
                 message_update_processors($plug);
             }
             upgrade_plugin_mnet_functions($component);
+            core_tag_area::reset_definitions_for_component($component);
             $endcallback($component, true, $verbose);
 
         } else if ($installedversion < $plugin->version) { // upgrade
@@ -566,6 +568,7 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) {
                 message_update_processors($plug);
             }
             upgrade_plugin_mnet_functions($component);
+            core_tag_area::reset_definitions_for_component($component);
             $endcallback($component, false, $verbose);
 
         } else if ($installedversion > $plugin->version) {
@@ -669,6 +672,7 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) {
                     message_update_providers($component);
                     \core\message\inbound\manager::update_handlers_for_component($component);
                     upgrade_plugin_mnet_functions($component);
+                    core_tag_area::reset_definitions_for_component($component);
                     $endcallback($component, true, $verbose);
                 }
             }
@@ -703,6 +707,7 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) {
             message_update_providers($component);
             \core\message\inbound\manager::update_handlers_for_component($component);
             upgrade_plugin_mnet_functions($component);
+            core_tag_area::reset_definitions_for_component($component);
 
             $endcallback($component, true, $verbose);
 
@@ -739,6 +744,7 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) {
             message_update_providers($component);
             \core\message\inbound\manager::update_handlers_for_component($component);
             upgrade_plugin_mnet_functions($component);
+            core_tag_area::reset_definitions_for_component($component);
 
             $endcallback($component, false, $verbose);
 
@@ -860,6 +866,7 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) {
                     message_update_providers($component);
                     \core\message\inbound\manager::update_handlers_for_component($component);
                     upgrade_plugin_mnet_functions($component);
+                    core_tag_area::reset_definitions_for_component($component);
                     $endcallback($component, true, $verbose);
                 }
             }
@@ -899,6 +906,7 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) {
             \core\task\manager::reset_scheduled_tasks_for_component($component);
             message_update_providers($component);
             \core\message\inbound\manager::update_handlers_for_component($component);
+            core_tag_area::reset_definitions_for_component($component);
             upgrade_plugin_mnet_functions($component);
 
             $endcallback($component, true, $verbose);
@@ -935,6 +943,7 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) {
             message_update_providers($component);
             \core\message\inbound\manager::update_handlers_for_component($component);
             upgrade_plugin_mnet_functions($component);
+            core_tag_area::reset_definitions_for_component($component);
 
             $endcallback($component, false, $verbose);
 
@@ -1536,6 +1545,7 @@ function install_core($version, $verbose) {
         \core\task\manager::reset_scheduled_tasks_for_component('moodle');
         message_update_providers('moodle');
         \core\message\inbound\manager::update_handlers_for_component('moodle');
+        core_tag_area::reset_definitions_for_component('moodle');
 
         // Write default settings unconditionally
         admin_apply_default_settings(NULL, true);
@@ -1603,6 +1613,7 @@ function upgrade_core($version, $verbose) {
         \core\task\manager::reset_scheduled_tasks_for_component('moodle');
         message_update_providers('moodle');
         \core\message\inbound\manager::update_handlers_for_component('moodle');
+        core_tag_area::reset_definitions_for_component('moodle');
         // Update core definitions.
         cache_helper::update_definitions(true);
 
index dc957e2..5899b8c 100644 (file)
@@ -49,6 +49,7 @@ Feature: Edited wiki pages handle tags correctly
     And I expand "Site administration" node
     And I expand "Appearance" node
     And I follow "Manage tags"
+    And I follow "Default collection"
     And I set the field "otagsadd" to "OT1, OT2, OT3"
     And I press "Add official tags"
     And I log out
diff --git a/tag/classes/area.php b/tag/classes/area.php
new file mode 100644 (file)
index 0000000..49ed67e
--- /dev/null
@@ -0,0 +1,450 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class core_tag_area for managing tag areas
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class to manage tag areas
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_tag_area {
+
+    /**
+     * Returns the list of areas indexed by itemtype and component
+     *
+     * @param int $tagcollid return only areas in this tag collection
+     * @param bool $enabledonly return only enabled tag areas
+     * @return array itemtype=>component=>tagarea object
+     */
+    public static function get_areas($tagcollid = null, $enabledonly = false) {
+        global $DB;
+        $cache = cache::make('core', 'tags');
+        if (($itemtypes = $cache->get('tag_area')) === false) {
+            $colls = core_tag_collection::get_collections();
+            $defaultcoll = reset($colls);
+            $itemtypes = array();
+            $areas = $DB->get_records('tag_area', array(), 'component,itemtype');
+            foreach ($areas as $area) {
+                if ($colls[$area->tagcollid]->component) {
+                    $area->locked = true;
+                }
+                $itemtypes[$area->itemtype][$area->component] = $area;
+            }
+            $cache->set('tag_area', $itemtypes);
+        }
+        if ($tagcollid || $enabledonly) {
+            $rv = array();
+            foreach ($itemtypes as $itemtype => $it) {
+                foreach ($it as $component => $v) {
+                    if (($v->tagcollid == $tagcollid || !$tagcollid) && (!$enabledonly || $v->enabled)) {
+                        $rv[$itemtype][$component] = $v;
+                    }
+                }
+            }
+            return $rv;
+        }
+        return $itemtypes;
+    }
+
+    /**
+     * Retrieves info about one tag area
+     *
+     * @param int $tagareaid
+     * @return stdClass
+     */
+    public static function get_by_id($tagareaid) {
+        $tagareas = self::get_areas();
+        foreach ($tagareas as $itemtype => $it) {
+            foreach ($it as $component => $v) {
+                if ($v->id == $tagareaid) {
+                    return $v;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the display name for this area
+     *
+     * @param string $component
+     * @param string $itemtype
+     * @return lang_string
+     */
+    public static function display_name($component, $itemtype) {
+        $identifier = 'tagarea_' . clean_param($itemtype, PARAM_STRINGID);
+        if ($component === 'core') {
+            $component = 'tag';
+        }
+        return new lang_string($identifier, $component);
+    }
+
+    /**
+     * Returns whether the tag area is enabled
+     *
+     * @param string $component component responsible for tagging
+     * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc.
+     * @return bool|null
+     */
+    public static function is_enabled($component, $itemtype) {
+        global $CFG;
+        if (empty($CFG->usetags)) {
+            return false;
+        }
+        $itemtypes = self::get_areas();
+        if (isset($itemtypes[$itemtype][$component])) {
+            return $itemtypes[$itemtype][$component]->enabled ? true : false;
+        }
+        return null;
+    }
+
+    /**
+     * Returns the id of the tag collection that should be used for storing tags of this itemtype
+     *
+     * @param string $component component responsible for tagging
+     * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc.
+     * @return int
+     */
+    public static function get_collection($component, $itemtype) {
+        $itemtypes = self::get_areas();
+        if (array_key_exists($itemtype, $itemtypes)) {
+            if (!array_key_exists($component, $itemtypes[$itemtype])) {
+                $component = key($itemtypes[$itemtype]);
+            }
+            return $itemtypes[$itemtype][$component]->tagcollid;
+        }
+        return core_tag_collection::get_default();
+    }
+
+    /**
+     * Returns all tag areas and collections that are currently cached in DB for this component
+     *
+     * @param string $componentname
+     * @return array first element is the list of areas and the second list of collections
+     */
+    protected static function get_definitions_for_component($componentname) {
+        global $DB;
+        list($a, $b) = core_component::normalize_component($componentname);
+        $component = $b ? ($a . '_' . $b) : $a;
+        $sql = 'component = :component';
+        $params = array('component' => $component);
+        if ($component === 'core') {
+            $sql .= ' OR component LIKE :coreprefix';
+            $params['coreprefix'] = 'core_%';
+        }
+        $fields = $DB->sql_concat_join("':'", array('itemtype', 'component'));
+        $existingareas = $DB->get_records_sql(
+                "SELECT $fields AS returnkey, a.* FROM {tag_area} a WHERE $sql", $params);
+        $fields = $DB->sql_concat_join("':'", array('name', 'component'));
+        $existingcolls = $DB->get_records_sql(
+                "SELECT $fields AS returnkey, t.* FROM {tag_coll} t WHERE $sql", $params);
+        return array($existingareas, $existingcolls);
+
+    }
+
+    /**
+     * Completely delete a tag area and all instances inside it
+     *
+     * @param stdClass $record
+     */
+    protected static function delete($record) {
+        global $DB;
+
+        core_tag_tag::delete_instances($record->component, $record->itemtype);
+
+        $DB->delete_records('tag_area',
+                array('itemtype' => $record->itemtype,
+                    'component' => $record->component));
+
+        // Reset cache.
+        cache::make('core', 'tags')->delete('tag_area');
+    }
+
+    /**
+     * Create a new tag area
+     *
+     * @param stdClass $record
+     */
+    protected static function create($record) {
+        global $DB;
+        if (empty($record->tagcollid)) {
+            $record->tagcollid = core_tag_collection::get_default();
+        }
+        $DB->insert_record('tag_area', array('component' => $record->component,
+            'itemtype' => $record->itemtype,
+            'tagcollid' => $record->tagcollid,
+            'callback' => $record->callback,
+            'callbackfile' => $record->callbackfile));
+
+        // Reset cache.
+        cache::make('core', 'tags')->delete('tag_area');
+    }
+
+    /**
+     * Update the tag area
+     *
+     * @param stdClass $existing current record from DB table tag_area
+     * @param array|stdClass $data fields that need updating
+     */
+    public static function update($existing, $data) {
+        global $DB;
+        $data = array_intersect_key((array)$data,
+                array('enabled' => 1, 'tagcollid' => 1,
+                    'callback' => 1, 'callbackfile' => 1));
+        foreach ($data as $key => $value) {
+            if ($existing->$key == $value) {
+                unset($data[$key]);
+            }
+        }
+        if (!$data) {
+            return;
+        }
+
+        if (!empty($data['tagcollid'])) {
+            self::move_tags($existing->component, $existing->itemtype, $data['tagcollid']);
+        }
+
+        $data['id'] = $existing->id;
+        $DB->update_record('tag_area', $data);
+
+        // Reset cache.
+        cache::make('core', 'tags')->delete('tag_area');
+    }
+
+    /**
+     * Update the database to contain a list of tagged areas for a component.
+     * The list of tagged areas is read from [plugindir]/db/tag.php
+     *
+     * @param string $componentname - The frankenstyle component name.
+     */
+    public static function reset_definitions_for_component($componentname) {
+        global $DB;
+        $dir = core_component::get_component_directory($componentname);
+        $file = $dir . '/db/tag.php';
+        $tagareas = null;
+        if (file_exists($file)) {
+            require_once($file);
+        }
+
+        list($a, $b) = core_component::normalize_component($componentname);
+        $component = $b ? ($a . '_' . $b) : $a;
+
+        list($existingareas, $existingcolls) = self::get_definitions_for_component($componentname);
+
+        $itemtypes = array();
+        $collections = array();
+        $needcleanup = false;
+        if ($tagareas) {
+            foreach ($tagareas as $tagarea) {
+                $record = (object)$tagarea;
+                if ($component !== 'core' || empty($record->component)) {
+                    if (isset($record->component) && $record->component !== $component) {
+                        debugging("Item type {$record->itemtype} has illegal component {$record->component}", DEBUG_DEVELOPER);
+                    }
+                    $record->component = $component;
+                }
+                unset($record->tagcollid);
+                if (!empty($record->collection)) {
+                    // Create collection if it does not exist, or update 'searchable' and/or 'customurl' if needed.
+                    $key = $record->collection . ':' . $record->component;
+                    $collectiondata = array_intersect_key((array)$record,
+                            array('component' => 1, 'searchable' => 1, 'customurl' => 1));
+                    $collectiondata['name'] = $record->collection;
+                    if (!array_key_exists($key, $existingcolls)) {
+                        $existingcolls[$key] = core_tag_collection::create($collectiondata);
+                    } else {
+                        core_tag_collection::update($existingcolls[$key], $collectiondata);
+                    }
+                    $record->tagcollid = $existingcolls[$key]->id;
+                    $collections[$key] = $existingcolls[$key];
+                    unset($record->collection);
+                }
+                unset($record->searchable);
+                unset($record->customurl);
+                if (!isset($record->callback)) {
+                    $record->callback = null;
+                }
+                if (!isset($record->callbackfile)) {
+                    $record->callbackfile = null;
+                }
+                $itemtypes[$record->itemtype . ':' . $record->component] = $record;
+            }
+        }
+        $todeletearea = array_diff_key($existingareas, $itemtypes);
+        $todeletecoll = array_diff_key($existingcolls, $collections);
+
+        // Delete tag areas that are no longer needed.
+        foreach ($todeletearea as $key => $record) {
+            self::delete($record);
+        }
+
+        // Update tag areas if changed.
+        $toupdatearea = array_intersect_key($existingareas, $itemtypes);
+        foreach ($toupdatearea as $key => $tagarea) {
+            if (!isset($itemtypes[$key]->tagcollid)) {
+                foreach ($todeletecoll as $tagcoll) {
+                    if ($tagcoll->id == $tagarea->tagcollid) {
+                        $itemtypes[$key]->tagcollid = core_tag_collection::get_default();
+                    }
+                }
+            }
+            self::update($tagarea, $itemtypes[$key]);
+        }
+
+        // Create new tag areas.
+        $toaddarea = array_diff_key($itemtypes, $existingareas);
+        foreach ($toaddarea as $record) {
+            self::create($record);
+        }
+
+        // Delete tag collections that are no longer needed.
+        foreach ($todeletecoll as $key => $tagcoll) {
+            core_tag_collection::delete($tagcoll);
+        }
+    }
+
+    /**
+     * Deletes all tag areas, collections and instances associated with the plugin.
+     *
+     * @param string $pluginname
+     */
+    public static function uninstall($pluginname) {
+        global $DB;
+
+        list($a, $b) = core_component::normalize_component($pluginname);
+        if (empty($b) || $a === 'core') {
+            throw new coding_exception('Core component can not be uninstalled');
+        }
+        $component = $a . '_' . $b;
+
+        core_tag_tag::delete_instances($component);
+
+        $DB->delete_records('tag_area', array('component' => $component));
+        $DB->delete_records('tag_coll', array('component' => $component));
+        cache::make('core', 'tags')->delete_many(array('tag_area', 'tag_coll'));
+    }
+
+    /**
+     * Moves existing tags associated with an item type to another tag collection
+     *
+     * @param string $component
+     * @param string $itemtype
+     * @param int $tagcollid
+     */
+    public static function move_tags($component, $itemtype, $tagcollid) {
+        global $DB;
+        $params = array('itemtype1' => $itemtype, 'component1' => $component,
+            'itemtype2' => $itemtype, 'component2' => $component,
+            'tagcollid1' => $tagcollid, 'tagcollid2' => $tagcollid);
+
+        // Find all collections that need to be cleaned later.
+        $sql = "SELECT DISTINCT t.tagcollid " .
+            "FROM {tag_instance} ti " .
+            "JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 " .
+            "WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 ";
+        $cleanupcollections = $DB->get_fieldset_sql($sql, $params);
+
+        // Find all tags that are related to the tags being moved and make sure they are present in the target tagcoll.
+        $sql = "SELECT DISTINCT r.name, r.rawname, r.description, r.descriptionformat, ".
+                "    r.userid, r.tagtype, r.flag ".
+                "FROM {tag_instance} ti ". // Instances that need moving.
+                "JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 ". // Tags that need moving.
+                "JOIN {tag_instance} tr ON tr.itemtype = 'tag' and tr.component = 'core' AND tr.itemid = t.id ".
+                "JOIN {tag} r ON r.id = tr.tagid ". // Tags related to the tags that need moving.
+                "LEFT JOIN {tag} re ON re.name = r.name AND re.tagcollid = :tagcollid2 ". // Existing tags in the target tagcoll with the same name as related tags.
+                "WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 ".
+                "    AND re.id IS NULL"; // We need related tags that ARE NOT present in the target tagcoll.
+        $result = $DB->get_records_sql($sql, $params);
+        foreach ($result as $tag) {
+            $tag->tagcollid = $tagcollid;
+            $tag->id = $DB->insert_record('tag', $tag);
+            \core\event\tag_created::create_from_tag($tag);
+        }
+
+        // Find all tags that need moving and have related tags, remember their related tags.
+        $sql = "SELECT t.name AS tagname, r.rawname AS relatedtag ".
+                "FROM {tag_instance} ti ". // Instances that need moving.
+                "JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 ". // Tags that need moving.
+                "JOIN {tag_instance} tr ON t.id = tr.tagid AND tr.itemtype = 'tag' and tr.component = 'core' ".
+                "JOIN {tag} r ON r.id = tr.itemid ". // Tags related to the tags that need moving.
+                "WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 ".
+                "ORDER BY t.id, tr.ordering ";
+        $relatedtags = array();
+        $result = $DB->get_recordset_sql($sql, $params);
+        foreach ($result as $record) {
+            $relatedtags[$record->tagname][] = $record->relatedtag;
+        }
+        $result->close();
+
+        // Find all tags that are used for this itemtype/component and are not present in the target tag collection.
+        $sql = "SELECT DISTINCT t.id, t.name, t.rawname, t.description, t.descriptionformat,
+                    t.userid, t.tagtype, t.flag
+                FROM {tag_instance} ti
+                JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1
+                LEFT JOIN {tag} tt ON tt.name = t.name AND tt.tagcollid = :tagcollid2
+                WHERE ti.itemtype = :itemtype2 AND ti.component = :component2
+                    AND tt.id IS NULL";
+        $todelete = array();
+        $result = $DB->get_records_sql($sql, $params);
+        foreach ($result as $tag) {
+            $originaltagid = $tag->id;
+            unset($tag->id);
+            $tag->tagcollid = $tagcollid;
+            $tag->id = $DB->insert_record('tag', $tag);
+            \core\event\tag_created::create_from_tag($tag);
+            $DB->execute("UPDATE {tag_instance} SET tagid = ? WHERE tagid = ? AND itemtype = ? AND component = ?",
+                    array($tag->id, $originaltagid, $itemtype, $component));
+        }
+
+        // Find all tags that are used for this itemtype/component and are already present in the target tag collection.
+        $sql = "SELECT DISTINCT t.id, tt.id AS targettagid
+                FROM {tag_instance} ti
+                JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1
+                JOIN {tag} tt ON tt.name = t.name AND tt.tagcollid = :tagcollid2
+                WHERE ti.itemtype = :itemtype2 AND ti.component = :component2";
+        $result = $DB->get_records_sql($sql, $params);
+        foreach ($result as $tag) {
+            $DB->execute("UPDATE {tag_instance} SET tagid = ? WHERE tagid = ? AND itemtype = ? AND component = ?",
+                    array($tag->targettagid, $tag->id, $itemtype, $component));
+        }
+
+        // Add related tags to the moved tags.
+        if ($relatedtags) {
+            $tags = core_tag_tag::get_by_name_bulk($tagcollid, array_keys($relatedtags));
+            foreach ($tags as $tag) {
+                $tag->add_related_tags($relatedtags[$tag->name]);
+            }
+        }
+
+        if ($cleanupcollections) {
+            core_tag_collection::cleanup_unused_tags($cleanupcollections);
+        }
+
+        // Reset caches.
+        cache::make('core', 'tags')->delete('tag_area');
+    }
+}
diff --git a/tag/classes/areas_table.php b/tag/classes/areas_table.php
new file mode 100644 (file)
index 0000000..495f011
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains class core_tag_areas_table
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Table with the list of available tag areas for "Manage tags" page.
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_tag_areas_table extends html_table {
+
+    /**
+     * Constructor
+     *
+     * @param string|moodle_url $pageurl
+     */
+    public function __construct($pageurl) {
+        global $OUTPUT;
+        parent::__construct();
+
+        $this->attributes['class'] = 'generaltable tag-areas-table';
+
+        $this->head = array(
+            get_string('tagareaname', 'core_tag'),
+            get_string('component', 'tag'),
+            get_string('tagareaenabled', 'core_tag'),
+            get_string('tagcollection', 'tag'),
+        );
+
+        $this->data = array();
+        $this->rowclasses = array();
+
+        $tagareas = core_tag_area::get_areas();
+        $tagcollections = core_tag_collection::get_collections_menu(true);
+        $tagcollectionsall = core_tag_collection::get_collections_menu();
+
+        foreach ($tagareas as $itemtype => $it) {
+            foreach ($it as $component => $record) {
+                $areaname = core_tag_area::display_name($record->component, $record->itemtype);
+                $baseurl = new moodle_url($pageurl, array('ta' => $record->id, 'sesskey' => sesskey()));
+                if ($record->enabled) {
+                    $enableurl = new moodle_url($baseurl, array('action' => 'areadisable'));
+                    $enabled = html_writer::link($enableurl, $OUTPUT->pix_icon('i/hide', get_string('disable')));
+                } else {
+                    $enableurl = new moodle_url($baseurl, array('action' => 'areaenable'));
+                    $enabled = html_writer::link($enableurl, $OUTPUT->pix_icon('i/show', get_string('enable')));
+                }
+
+                if ($record->enabled && empty($record->locked) && count($tagcollections) > 1) {
+                    $changecollurl = new moodle_url($baseurl, array('action' => 'areasetcoll'));
+
+                    $select = new single_select($changecollurl, 'areacollid', $tagcollections, $record->tagcollid, null);
+                    $select->set_label(get_string('changetagcoll', 'core_tag', $areaname), array('class' => 'accesshide'));
+                    $collectionselect = $OUTPUT->render($select);
+                } else {
+                    $collectionselect = $tagcollectionsall[$record->tagcollid];
+                }
+                $this->data[] = array(
+                    $areaname,
+                    ($record->component === 'core' || preg_match('/^core_/', $record->component)) ?
+                        get_string('coresystem') : get_string('pluginname', $record->component),
+                    $enabled,
+                    $collectionselect
+                );
+                $this->rowclasses[] = $record->enabled ? '' : 'dimmed_text';
+            }
+        }
+
+    }
+
+}
diff --git a/tag/classes/collection.php b/tag/classes/collection.php
new file mode 100644 (file)
index 0000000..c8be5d2
--- /dev/null
@@ -0,0 +1,419 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class to manage tag collections
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class to manage tag collections
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_tag_collection {
+
+    /** @var string used for function cloud_sort() */
+    public static $cloudsortfield = 'name';
+
+    /**
+     * Returns the list of tag collections defined in the system.
+     *
+     * @param bool $onlysearchable only return collections that can be searched.
+     * @return array array of objects where each object has properties: id, name, isdefault, itemtypes, sortorder
+     */
+    public static function get_collections($onlysearchable = false) {
+        global $DB;
+        $cache = cache::make('core', 'tags');
+        if (($tagcolls = $cache->get('tag_coll')) === false) {
+            // Retrieve records from DB and create a default one if it is not present.
+            $tagcolls = $DB->get_records('tag_coll', null, 'isdefault DESC, sortorder, id');
+            if (empty($tagcolls)) {
+                // When this method is called for the first time it automatically creates the default tag collection.
+                $DB->insert_record('tag_coll', array('isdefault' => 1, 'sortorder' => 0));
+                $tagcolls = $DB->get_records('tag_coll');
+            } else {
+                // Make sure sortorder is correct.
+                $idx = 0;
+                foreach ($tagcolls as $id => $tagcoll) {
+                    if ($tagcoll->sortorder != $idx) {
+                        $DB->update_record('tag_coll', array('sortorder' => $idx, 'id' => $id));
+                        $tagcolls[$id]->sortorder = $idx;
+                    }
+                    $idx++;
+                }
+            }
+            $cache->set('tag_coll', $tagcolls);
+        }
+        if ($onlysearchable) {
+            $rv = array();
+            foreach ($tagcolls as $id => $tagcoll) {
+                if ($tagcoll->searchable) {
+                    $rv[$id] = $tagcoll;
+                }
+            }
+            return $rv;
+        }
+        return $tagcolls;
+    }
+
+    /**
+     * Returns the tag collection object
+     *
+     * @param int $tagcollid
+     * @return stdClass
+     */
+    public static function get_by_id($tagcollid) {
+        $tagcolls = self::get_collections();
+        if (array_key_exists($tagcollid, $tagcolls)) {
+            return $tagcolls[$tagcollid];
+        }
+        return null;
+    }
+
+    /**
+     * Returns the list of existing tag collections as id=>name
+     *
+     * @param bool $unlockedonly
+     * @param bool $onlysearchable
+     * @param string $selectalllabel
+     * @return array
+     */
+    public static function get_collections_menu($unlockedonly = false, $onlysearchable = false,
+            $selectalllabel = null) {
+        $tagcolls = self::get_collections($onlysearchable);
+        $options = array();
+        foreach ($tagcolls as $id => $tagcoll) {
+            if (!$unlockedonly || empty($tagcoll->component)) {
+                $options[$id] = self::display_name($tagcoll);
+            }
+        }
+        if (count($options) > 1 && $selectalllabel) {
+            $options = array(0 => $selectalllabel) + $options;
+        }
+        return $options;
+    }
+
+    /**
+     * Returns id of the default tag collection
+     *
+     * @return int
+     */
+    public static function get_default() {
+        $collections = self::get_collections();
+        $keys = array_keys($collections);
+        return $keys[0];
+    }
+
+    /**
+     * Returns formatted name of the tag collection
+     *
+     * @param stdClass $record record from DB table tag_coll
+     * @return string
+     */
+    public static function display_name($record) {
+        $syscontext = context_system::instance();
+        if (!empty($record->component)) {
+            $identifier = 'tagcollection_' .
+                    clean_param($record->name, PARAM_STRINGID);
+            $component = $record->component;
+            if ($component === 'core') {
+                $component = 'tag';
+            }
+            return get_string($identifier, $component);
+        }
+        if (!empty($record->name)) {
+            return format_string($record->name, true, $syscontext);
+        } else if ($record->isdefault) {
+            return get_string('defautltagcoll', 'tag');
+        } else {
+            return $record->id;
+        }
+    }
+
+    /**
+     * Returns all tag areas in the given tag collection
+     *
+     * @param int $tagcollid
+     * @return array
+     */
+    public static function get_areas($tagcollid) {
+        $allitemtypes = core_tag_area::get_areas($tagcollid, true);
+        $itemtypes = array();
+        foreach ($allitemtypes as $itemtype => $it) {
+            foreach ($it as $component => $v) {
+                $itemtypes[$v->id] = $v;
+            }
+        }
+        return $itemtypes;
+    }
+
+    /**
+     * Returns the list of names of areas (enabled only) that are in this collection.
+     *
+     * @param int $tagcollid
+     * @return array
+     */
+    public static function get_areas_names($tagcollid) {
+        $allitemtypes = core_tag_area::get_areas($tagcollid, true);
+        $itemtypes = array();
+        foreach ($allitemtypes as $itemtype => $it) {
+            foreach ($it as $component => $v) {
+                $itemtypes[] = core_tag_area::display_name($component, $itemtype);
+            }
+        }
+        return $itemtypes;
+    }
+
+    /**
+     * Creates a new tag collection
+     *
+     * @param stdClass $data data from form core_tag_collection_form
+     * @return int|false id of created tag collection or false if failed
+     */
+    public static function create($data) {
+        global $DB;
+        $data = (object)$data;
+        $tagcolls = self::get_collections();
+        $tagcoll = (object)array(
+            'name' => $data->name,
+            'isdefault' => 0,
+            'component' => !empty($data->component) ? $data->component : null,
+            'sortorder' => count($tagcolls),
+            'searchable' => isset($data->searchable) ? (int)(bool)$data->searchable : 1,
+            'customurl' => !empty($data->customurl) ? $data->customurl : null,
+        );
+        $tagcoll->id = $DB->insert_record('tag_coll', $tagcoll);
+
+        // Reset cache.
+        cache::make('core', 'tags')->delete('tag_coll');
+
+        \core\event\tag_collection_created::create_from_record($tagcoll)->trigger();
+        return $tagcoll;
+    }
+
+    /**
+     * Updates the tag collection information
+     *
+     * @param stdClass $tagcoll existing record in DB table tag_coll
+     * @param stdClass $data data from form core_tag_collection_form
+     * @return bool wether the record was updated
+     */
+    public static function update($tagcoll, $data) {
+        global $DB;
+        $defaulttagcollid = self::get_default();
+        $allowedfields = array('name', 'searchable', 'customurl');
+        if ($tagcoll->id == $defaulttagcollid) {
+            $allowedfields = array('name');
+        }
+
+        $updatedata = array();
+        $data = (array)$data;
+        foreach ($allowedfields as $key) {
+            if (array_key_exists($key, $data) && $data[$key] !== $tagcoll->$key) {
+                $updatedata[$key] = $data[$key];
+            }
+        }
+
+        if (!$updatedata) {
+            // Nothing to update.
+            return false;
+        }
+
+        if (isset($updatedata['searchable'])) {
+            $updatedata['searchable'] = (int)(bool)$updatedata['searchable'];
+        }
+        foreach ($updatedata as $key => $value) {
+            $tagcoll->$key = $value;
+        }
+        $updatedata['id'] = $tagcoll->id;
+        $DB->update_record('tag_coll', $updatedata);
+
+        // Reset cache.
+        cache::make('core', 'tags')->delete('tag_coll');
+
+        \core\event\tag_collection_updated::create_from_record($tagcoll)->trigger();
+
+        return true;
+    }
+
+    /**
+     * Deletes a custom tag collection
+     *
+     * @param stdClass $tagcoll existing record in DB table tag_coll
+     * @return bool wether the tag collection was deleted
+     */
+    public static function delete($tagcoll) {
+        global $DB, $CFG;
+
+        $defaulttagcollid = self::get_default();
+        if ($tagcoll->id == $defaulttagcollid) {
+            return false;
+        }
+
+        // Move all tags from this tag collection to the default one.
+        $allitemtypes = core_tag_area::get_areas($tagcoll->id);
+        foreach ($allitemtypes as $it) {
+            foreach ($it as $v) {
+                core_tag_area::update($v, array('tagcollid' => $defaulttagcollid));
+            }
+        }
+
+        // Delete tags from this tag_coll.
+        core_tag_tag::delete_tags($DB->get_fieldset_select('tag', 'id', 'tagcollid = ?', array($tagcoll->id)));
+
+        // Delete the tag collection.
+        $DB->delete_records('tag_coll', array('id' => $tagcoll->id));
+
+        // Reset cache.
+        cache::make('core', 'tags')->delete('tag_coll');
+
+        \core\event\tag_collection_deleted::create_from_record($tagcoll)->trigger();
+
+        return true;
+    }
+
+    /**
+     * Moves the tag collection in the list one position up or down
+     *
+     * @param stdClass $tagcoll existing record in DB table tag_coll
+     * @param int $direction move direction: +1 or -1
+     * @return bool
+     */
+    public static function change_sortorder($tagcoll, $direction) {
+        global $DB;
+        if ($direction != -1 && $direction != 1) {
+            throw coding_exception('Second argument in tag_coll_change_sortorder() can be only 1 or -1');
+        }
+        $tagcolls = self::get_collections();
+        $keys = array_keys($tagcolls);
+        $idx = array_search($tagcoll->id, $keys);
+        if ($idx === false || $idx == 0 || $idx + $direction < 1 || $idx + $direction >= count($tagcolls)) {
+            return false;
+        }
+        $otherid = $keys[$idx + $direction];
+        $DB->update_record('tag_coll', array('id' => $tagcoll->id, 'sortorder' => $idx + $direction));
+        $DB->update_record('tag_coll', array('id' => $otherid, 'sortorder' => $idx));
+        // Reset cache.
+        cache::make('core', 'tags')->delete('tag_coll');
+        return true;
+    }
+
+    /**
+     * Permanently deletes all non-official tags that no longer have any instances pointing to them
+     *
+     * @param array $collections optional list of tag collections ids to cleanup
+     */
+    public static function cleanup_unused_tags($collections = null) {
+        global $DB, $CFG;
+
+        $params = array();
+        $sql = "SELECT tg.id FROM {tag} tg LEFT OUTER JOIN {tag_instance} ti ON ti.tagid = tg.id
+                WHERE ti.id IS NULL AND tg.tagtype = 'default'";
+        if ($collections) {
+            list($sqlcoll, $params) = $DB->get_in_or_equal($collections);
+            $sql .= " AND tg.tagcollid " . $sqlcoll;
+        }
+        if ($unusedtags = $DB->get_fieldset_sql($sql, $params)) {
+            core_tag_tag::delete_tags($unusedtags);
+        }
+    }
+
+    /**
+     * Returns the list of tags with number of items tagged
+     *
+     * @param int $tagcollid
+     * @param string $tagtype possible values 'official', 'default' or empty for any tag type
+     * @param int $limit maximum number of tags to retrieve, tags are sorted by the instance count
+     *            descending here regardless of $sort parameter
+     * @param string $sort sort order for display, default 'name' - tags will be sorted after they are retrieved
+     * @param string $search search string
+     * @param int $fromctx context id where this tag cloud is displayed
+     * @param int $ctx only retrieve tag instances in this context
+     * @param int $rec retrieve tag instances in the $ctx context and it's children (default 1)
+     * @return \core_tag\output\tagcloud
+     */
+    public static function get_tag_cloud($tagcollid, $tagtype = '', $limit = 150, $sort = 'name',
+            $search = '', $fromctx = 0, $ctx = 0, $rec = 1) {
+        global $DB;
+
+        $fromclause = 'FROM {tag_instance} ti JOIN {tag} tg ON tg.id = ti.tagid';
+        $whereclause = 'WHERE ti.itemtype <> \'tag\'';
+        list($sql, $params) = $DB->get_in_or_equal($tagcollid ? array($tagcollid) :
+            array_keys(self::get_collections(true)));
+        $whereclause .= ' AND tg.tagcollid ' . $sql;
+        if (!empty($tagtype)) {
+            $whereclause .= ' AND tg.tagtype = ?';
+            $params[] = $tagtype;
+        }
+        $context = $ctx ? context::instance_by_id($ctx) : context_system::instance();
+        if ($rec && $context->contextlevel != CONTEXT_SYSTEM) {
+            $fromclause .= ' JOIN {context} ctx ON ctx.id = ti.contextid ';
+            $whereclause .= ' AND ctx.path LIKE ?';
+            $params[] = $context->path . '%';
+        } else if (!$rec) {
+            $whereclause .= ' AND ti.contextid = ?';
+            $params[] = $context->id;
+        }
+        if (strval($search) !== '') {
+            $whereclause .= ' AND tg.name LIKE ?';
+            $params[] = '%' . core_text::strtolower($search) . '%';
+        }
+        $tagsincloud = $DB->get_records_sql(
+                "SELECT tg.id, tg.rawname, tg.name, tg.tagtype, COUNT(ti.id) AS count, tg.flag, tg.tagcollid
+                $fromclause
+                $whereclause
+                GROUP BY tg.id, tg.rawname, tg.name, tg.flag, tg.tagtype, tg.tagcollid
+                ORDER BY count DESC, tg.name ASC",
+            $params, 0, $limit);
+
+        $tagscount = count($tagsincloud);
+        if ($tagscount == $limit) {
+            $tagscount = $DB->get_field_sql("SELECT COUNT(DISTINCT tg.id) $fromclause $whereclause", $params);
+        }
+
+        self::$cloudsortfield = $sort;
+        usort($tagsincloud, "self::cloud_sort");
+
+        return new core_tag\output\tagcloud($tagsincloud, $tagscount, $fromctx, $ctx, $rec);
+    }
+
+    /**
+     * This function is used to sort the tags in the cloud.
+     *
+     * @param   string $a Tag name to compare against $b
+     * @param   string $b Tag name to compare against $a
+     * @return  int    The result of the comparison/validation 1, 0 or -1
+     */
+    public static function cloud_sort($a, $b) {
+        $tagsort = self::$cloudsortfield ?: 'name';
+
+        if (is_numeric($a->$tagsort)) {
+            return ($a->$tagsort == $b->$tagsort) ? 0 : ($a->$tagsort > $b->$tagsort) ? 1 : -1;
+        } else if (is_string($a->$tagsort)) {
+            return strcmp($a->$tagsort, $b->$tagsort);
+        } else {
+            return 0;
+        }
+    }
+}
\ No newline at end of file
diff --git a/tag/classes/collection_form.php b/tag/classes/collection_form.php
new file mode 100644 (file)
index 0000000..04a95b7
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains class core_tag_collection_form
+ *
+ * @package   core
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/formslib.php');
+
+/**
+ * Form for editing tag collection
+ *
+ * @package   core
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_tag_collection_form extends moodleform {
+
+    /**
+     * Form definition
+     */
+    public function definition() {
+        $data = fullclone($this->_customdata);
+        if (isset($data->id)) {
+            $data->tc = $data->id;
+            $data->action = 'colledit';
+        } else {
+            $data = new stdClass();
+            $data->action = 'colladd';
+            $data->isdefault = false;
+        }
+
+        $mform = $this->_form;
+        $mform->addElement('hidden', 'tc');
+        $mform->setType('tc', PARAM_INT);
+        $mform->addElement('hidden', 'action');
+        $mform->setType('action', PARAM_ALPHA);
+
+        $mform->addElement('text', 'name', get_string('name'));
+        $mform->setType('name', PARAM_NOTAGS);
+        $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
+        if (empty($data->isdefault)) {
+            $mform->addRule('name', get_string('required'), 'required', null, 'client');
+        } else {
+            $mform->addElement('static', 'collnameexplained', '', get_string('collnameexplained', 'tag',
+                    get_string('defautltagcoll', 'tag')));
+        }
+
+        $mform->addElement('advcheckbox', 'searchable', get_string('searchable', 'tag'));
+        $mform->addHelpButton('searchable', 'searchable', 'tag');
+        $mform->setDefault('searchable', 1);
+        if (!empty($data->isdefault)) {
+            $mform->freeze('searchable');
+        }
+
+        $this->add_action_buttons();
+
+        $this->set_data($data);
+    }
+}
diff --git a/tag/classes/collections_table.php b/tag/classes/collections_table.php
new file mode 100644 (file)
index 0000000..28ae760
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains class core_tag_collections_table
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Table with the list of tag collections for "Manage tags" page.
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_tag_collections_table extends html_table {
+
+    /**
+     * Constructor
+     * @param string|moodle_url $pageurl
+     */
+    public function __construct($pageurl) {
+        global $OUTPUT;
+        parent::__construct();
+
+        $this->attributes['class'] = 'generaltable tag-collections-table';
+
+        $this->head = array(
+            get_string('name'),
+            get_string('component', 'tag'),
+            get_string('tagareas', 'tag'),
+            get_string('searchable', 'tag') . $OUTPUT->help_icon('searchable', 'tag'),
+            ''
+        );
+
+        $this->data = array();
+
+        $tagcolls = core_tag_collection::get_collections();
+        $idx = 0;
+        foreach ($tagcolls as $tagcoll) {
+            $actions = '';
+            $name = core_tag_collection::display_name($tagcoll);
+            $url = new moodle_url($pageurl, array('sesskey' => sesskey(), 'tc' => $tagcoll->id));
+            if (!$tagcoll->isdefault) {
+                // Move up.
+                if ($idx > 1) {
+                    $url->param('action', 'collmoveup');
+                    $actions .= $OUTPUT->action_icon($url, new pix_icon('t/up', get_string('moveup')));
+                }
+                // Move down.
+                if ($idx < count($tagcolls) - 1) {
+                    $url->param('action', 'collmovedown');
+                    $actions .= $OUTPUT->action_icon($url, new pix_icon('t/down', get_string('movedown')));
+                }
+            }
+            if (empty($tagcoll->component)) {
+                // Edit.
+                $url->param('action', 'colledit');
+                $actions .= $OUTPUT->action_icon($url, new pix_icon('t/edit', get_string('edittagcoll', 'tag', $name)));
+            }
+            if (!$tagcoll->isdefault && empty($tagcoll->component)) {
+                // Delete.
+                $url->param('action', 'colldelete');
+                $actions .= $OUTPUT->action_icon($url, new pix_icon('t/delete', get_string('delete')));
+            }
+            $manageurl = new moodle_url('/tag/manage.php', array('tc' => $tagcoll->id));
+            $component = '';
+            if ($tagcoll->component) {
+                $component = ($tagcoll->component === 'core' || preg_match('/^core_/', $tagcoll->component)) ?
+                    get_string('coresystem') : get_string('pluginname', $tagcoll->component);
+            }
+            $this->data[] = array(
+                html_writer::link($manageurl, $name),
+                $component,
+                join(', ', core_tag_collection::get_areas_names($tagcoll->id)),
+                $tagcoll->searchable ? get_string('yes') : '-',
+                $actions);
+            $idx++;
+        }
+
+    }
+}
\ No newline at end of file
index 2bb564f..324ecad 100644 (file)
@@ -66,7 +66,6 @@ class core_tag_external extends external_api {
      */
     public static function update_tags($tags) {
         global $CFG, $PAGE, $DB;
-        require_once($CFG->dirroot.'/tag/lib.php');
 
         // Validate and normalize parameters.
         $tags = self::validate_parameters(self::update_tags_parameters(), array('tags' => $tags));
@@ -87,8 +86,6 @@ class core_tag_external extends external_api {
                 $tag['rawname'] = clean_param($tag['rawname'], PARAM_TAG);
                 if (empty($tag['rawname'])) {
                     unset($tag['rawname']);
-                } else {
-                    $tag['name'] = core_text::strtolower($tag['rawname']);
                 }
             }
             if (!$canmanage) {
@@ -109,7 +106,7 @@ class core_tag_external extends external_api {
                 );
                 continue;
             }
-            if (!$tagobject = $DB->get_record('tag', array('id' => $tag['id']))) {
+            if (!$tagobject = core_tag_tag::get($tag['id'], '*')) {
                 $warnings[] = array(
                     'item' => $tag['id'],
                     'warningcode' => 'tagnotfound',
@@ -118,7 +115,7 @@ class core_tag_external extends external_api {
                 continue;
             }
             // First check if new tag name is allowed.
-            if (!empty($tag['name']) && ($existing = $DB->get_record('tag', array('name' => $tag['name']), 'id'))) {
+            if (!empty($tag['rawname']) && ($existing = core_tag_tag::get_by_name($tagobject->tagcollid, $tag['rawname']))) {
                 if ($existing->id != $tag['id']) {
                     $warnings[] = array(
                         'item' => $tag['id'],
@@ -132,23 +129,18 @@ class core_tag_external extends external_api {
                 $tag['tagtype'] = $tag['official'] ? 'official' : 'default';
                 unset($tag['official']);
             }
-            $tag['timemodified'] = time();
-            $DB->update_record('tag', $tag);
-
-            foreach ($tag as $key => $value) {
-                $tagobject->$key = $value;
+            if (isset($tag['flag'])) {
+                if ($tag['flag']) {
+                    $tagobject->flag();
+                } else {
+                    $tagobject->reset_flag();
+                }
+                unset($tag['flag']);
+            }
+            unset($tag['id']);
+            if (count($tag)) {
+                $tagobject->update($tag);
             }
-
-            $event = \core\event\tag_updated::create(array(
-                'objectid' => $tagobject->id,
-                'relateduserid' => $tagobject->userid,
-                'context' => context_system::instance(),
-                'other' => array(
-                    'name' => $tagobject->name,
-                    'rawname' => $tagobject->rawname
-                )
-            ));
-            $event->trigger();
         }
         return array('warnings' => $warnings);
     }
@@ -192,7 +184,6 @@ class core_tag_external extends external_api {
      */
     public static function get_tags($tags) {
         global $CFG, $PAGE, $DB;
-        require_once($CFG->dirroot.'/tag/lib.php');
 
         // Validate and normalize parameters.
         $tags = self::validate_parameters(self::get_tags_parameters(), array('tags' => $tags));
@@ -248,6 +239,7 @@ class core_tag_external extends external_api {
                 'tags' => new external_multiple_structure( new external_single_structure(
                     array(
                         'id' => new external_value(PARAM_INT, 'tag id'),
+                        'tagcollid' => new external_value(PARAM_INT, 'tag collection id'),
                         'name' => new external_value(PARAM_TAG, 'name'),
                         'rawname' => new external_value(PARAM_RAW, 'tag raw name (may contain capital letters)'),
                         'description' => new external_value(PARAM_RAW, 'tag description'),
@@ -263,4 +255,81 @@ class core_tag_external extends external_api {
             )
         );
     }
+
+    /**
+     * Parameters for function get_tagindex()
+     *
+     * @return external_function_parameters
+     */
+    public static function get_tagindex_parameters() {
+        return new external_function_parameters(
+            array(
+                'tagindex' => new external_single_structure(array(
+                    'tag' => new external_value(PARAM_TAG, 'tag name'),
+                    'tc' => new external_value(PARAM_INT, 'tag collection id'),
+                    'ta' => new external_value(PARAM_INT, 'tag area id'),
+                    'excl' => new external_value(PARAM_BOOL, 'exlusive mode for this tag area', VALUE_OPTIONAL, 0),
+                    'from' => new external_value(PARAM_INT, 'context id where the link was displayed', VALUE_OPTIONAL, 0),
+                    'ctx' => new external_value(PARAM_INT, 'context id where to search for items', VALUE_OPTIONAL, 0),
+                    'rec' => new external_value(PARAM_INT, 'search in the context recursive', VALUE_OPTIONAL, 1),
+                    'page' => new external_value(PARAM_INT, 'page number (0-based)', VALUE_OPTIONAL, 0),
+                ), 'parameters')
+            )
+        );
+    }
+
+    /**
+     * Get tags by their ids
+     *
+     * @param array $params
+     */
+    public static function get_tagindex($params) {
+        global $PAGE;
+        // Validate and normalize parameters.
+        $tagindex = self::validate_parameters(
+                self::get_tagindex_parameters(), array('tagindex' => $params));
+        $params = $tagindex['tagindex'] + array(
+            'excl' => 0,
+            'from' => 0,
+            'ctx' => 0,
+            'rec' => 1,
+            'page' => 0
+        );
+
+        // Login to the course / module if applicable.
+        $context = $params['ctx'] ? context::instance_by_id($params['ctx']) : context_system::instance();
+        require_login(null, false, null, false, true);
+        self::validate_context($context);
+
+        $tag = core_tag_tag::get_by_name($params['tc'], $params['tag'], '*', MUST_EXIST);
+        $tagareas = core_tag_collection::get_areas($params['tc']);
+        $tagindex = $tag->get_tag_index($tagareas[$params['ta']], $params['excl'], $params['from'],
+                $params['ctx'], $params['rec'], $params['page']);
+        $renderer = $PAGE->get_renderer('core');
+        return $tagindex->export_for_template($renderer);
+    }
+
+    /**
+     * Return structure for get_tag()
+     *
+     * @return external_description
+     */
+    public static function get_tagindex_returns() {
+        return new external_single_structure(
+            array(
+                'tagid' => new external_value(PARAM_INT, 'tag id'),
+                'ta' => new external_value(PARAM_INT, 'tag area id'),
+                'component' => new external_value(PARAM_COMPONENT, 'component'),
+                'itemtype' => new external_value(PARAM_NOTAGS, 'itemtype'),
+                'nextpageurl' => new external_value(PARAM_URL, 'URL for the next page', VALUE_OPTIONAL),
+                'prevpageurl' => new external_value(PARAM_URL, 'URL for the next page', VALUE_OPTIONAL),
+                'exclusiveurl' => new external_value(PARAM_URL, 'URL for exclusive link', VALUE_OPTIONAL),
+                'exclusivetext' => new external_value(PARAM_TEXT, 'text for exclusive link', VALUE_OPTIONAL),
+                'title' => new external_value(PARAM_RAW, 'title'),
+                'content' => new external_value(PARAM_RAW, 'title'),
+                'hascontent' => new external_value(PARAM_INT, 'whether the content is present'),
+                'anchor' => new external_value(PARAM_TEXT, 'name of anchor', VALUE_OPTIONAL),
+            ), 'tag index'
+        );
+    }
 }
index 61d3b19..48af902 100644 (file)
@@ -38,16 +38,24 @@ class core_tag_manage_table extends table_sql {
     /** @var int stores the total number of found tags */
     public $totalcount = null;
 
+    /** @var int */
+    protected $tagcollid;
+
     /**
      * Constructor
+     *
+     * @param int $tagcollid
      */
-    public function __construct() {
+    public function __construct($tagcollid) {
         global $USER, $CFG, $PAGE;
         parent::__construct('tag-management-list-'.$USER->id);
 
+        $this->tagcollid = $tagcollid;
+
         $perpage = optional_param('perpage', DEFAULT_PAGE_SIZE, PARAM_INT);
         $page = optional_param('page', 0, PARAM_INT);
-        $baseurl = new moodle_url('/tag/manage.php', array('perpage' => $perpage, 'page' => $page));
+        $baseurl = new moodle_url('/tag/manage.php', array('tc' => $tagcollid,
+            'perpage' => $perpage, 'page' => $page));
 
         $tablecolumns = array('select', 'name', 'fullname', 'count', 'flag', 'timemodified', 'tagtype', 'controls');
         $tableheaders = array(get_string('select', 'tag'),
@@ -80,8 +88,10 @@ class core_tag_manage_table extends table_sql {
         $this->set_attribute('id', 'tag-management-list');
         $this->set_attribute('class', 'admintable generaltable tag-management-table');
 
-        $totalcount = "SELECT COUNT(id) FROM {tag}";
-        $params = array();
+        $totalcount = "SELECT COUNT(id)
+            FROM {tag}
+            WHERE tagcollid = :tagcollid";
+        $params = array('tagcollid' => $this->tagcollid);
 
         $this->set_count_sql($totalcount, $params);
 
@@ -135,13 +145,13 @@ class core_tag_manage_table extends table_sql {
         $sql = "
             SELECT tg.id, tg.name, tg.rawname, tg.tagtype, tg.flag, tg.timemodified,
                        u.id AS owner, $allusernames,
-                       COUNT(ti.id) AS count
+                       COUNT(ti.id) AS count, tg.tagcollid
             FROM {tag} tg
             LEFT JOIN {tag_instance} ti ON ti.tagid = tg.id
             LEFT JOIN {user} u ON u.id = tg.userid
-                       WHERE 1 = 1 $where
+                       WHERE tagcollid = :tagcollid $where
             GROUP BY tg.id, tg.name, tg.rawname, tg.tagtype, tg.flag, tg.timemodified,
-                       u.id, $allusernames
+                       u.id, $allusernames, tg.tagcollid
             ORDER BY $sort";
 
         if (!$this->is_downloading()) {
index 74aa47d..dd8559f 100644 (file)
@@ -39,15 +39,19 @@ use moodle_url;
  */
 class tag implements renderable, templatable {
 
-    /** @var stdClass */
+    /** @var \core_tag_tag|stdClass */
     protected $record;
 
     /**
      * Constructor
      *
-     * @param stdClass $tag
+     * @param \core_tag_tag|stdClass $tag
      */
     public function __construct($tag) {
+        if ($tag instanceof \core_tag_tag) {
+            $this->record = $tag;
+            return;
+        }
         $tag = (array)$tag +
             array(
                 'name' => '',
@@ -56,7 +60,8 @@ class tag implements renderable, templatable {
                 'descriptionformat' => FORMAT_HTML,
                 'flag' => 0,
                 'tagtype' => 'default',
-                'id' => 0
+                'id' => 0,
+                'tagcollid' => 0,
             );
         $this->record = (object)$tag;
     }
@@ -73,6 +78,7 @@ class tag implements renderable, templatable {
 
         $r = new stdClass();
         $r->id = (int)$this->record->id;
+        $r->tagcollid = clean_param($this->record->tagcollid, PARAM_INT);
         $r->rawname = clean_param($this->record->rawname, PARAM_TAG);
         $r->name = clean_param($this->record->name, PARAM_TAG);
         $format = clean_param($this->record->descriptionformat, PARAM_INT);
@@ -85,7 +91,7 @@ class tag implements renderable, templatable {
             $r->official = ($this->record->tagtype === 'official') ? 1 : 0;
         }
 
-        $url = new moodle_url('/tag/index.php', array('id' => $this->record->id));
+        $url = \core_tag_tag::make_url($r->tagcollid, $r->rawname);
         $r->viewurl = $url->out(false);
 
         $manageurl = new moodle_url('/tag/manage.php', array('sesskey' => sesskey(),
diff --git a/tag/classes/output/tagcloud.php b/tag/classes/output/tagcloud.php
new file mode 100644 (file)
index 0000000..85d61ce
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains class core_tag\output\tagindex
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_tag\output;
+
+use renderable;
+use templatable;
+use renderer_base;
+use stdClass;
+use moodle_url;
+use core_tag_tag;
+
+/**
+ * Class to display a tag cloud - set of tags where each has a weight.
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tagcloud implements templatable {
+
+    /** @var array */
+    protected $tagset;
+
+    /** @var int */
+    protected $totalcount;
+
+    /**
+     * Constructor
+     *
+     * @param array $tagset array of core_tag or stdClass elements, each of them must have attributes:
+     *              name, rawname, tagcollid
+     *              preferrably also have attributes:
+     *              tagtype, count, flag
+     * @param int $totalcount total count of tags (for example to indicate that there are more tags than the count of tagset)
+     *            leave 0 if count of tagset is the actual count of tags
+     * @param int $fromctx context id where this tag cloud is displayed
+     * @param int $ctx context id for tag view link
+     * @param int $rec recursive argument for tag view link
+     */
+    public function __construct($tagset, $totalcount = 0, $fromctx = 0, $ctx = 0, $rec = 1) {
+        $canmanagetags = has_capability('moodle/tag:manage', \context_system::instance());
+
+        $maxcount = 1;
+        foreach ($tagset as $tag) {
+            if (isset($tag->count) && $tag->count > $maxcount) {
+                $maxcount = $tag->count;
+            }
+        }
+
+        $this->tagset = array();
+        foreach ($tagset as $idx => $tag) {
+            $this->tagset[$idx] = new stdClass();
+
+            $this->tagset[$idx]->name = core_tag_tag::make_display_name($tag, false);
+
+            if ($canmanagetags && !empty($tag->flag)) {
+                $this->tagset[$idx]->flag = 1;
+            }
+
+            $viewurl = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, 0, $fromctx, $ctx, $rec);
+            $this->tagset[$idx]->viewurl = $viewurl->out(false);
+
+            if (!empty($tag->tagtype)) {
+                $this->tagset[$idx]->tagtype = $tag->tagtype;
+            }
+
+            if (!empty($tag->count)) {
+                $this->tagset[$idx]->count = $tag->count;
+                $this->tagset[$idx]->size = (int)($tag->count / $maxcount * 20);
+            }
+        }
+
+        $this->totalcount = $totalcount ? $totalcount : count($this->tagset);
+    }
+
+    /**
+     * Returns number of tags in the cloud
+     * @return int
+     */
+    public function get_count() {
+        return count($this->tagset);
+    }
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(renderer_base $output) {
+        $cnt = count($this->tagset);
+        return (object)array(
+            'tags' => $this->tagset,
+            'tagscount' => $cnt,
+            'totalcount' => $this->totalcount,
+            'overflow' => ($this->totalcount > $cnt) ? 1 : 0,
+        );
+    }
+}
diff --git a/tag/classes/output/tagfeed.php b/tag/classes/output/tagfeed.php
new file mode 100644 (file)
index 0000000..4c79e40
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains class core_tag\output\tagfeed
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_tag\output;
+
+use templatable;
+use renderer_base;
+use stdClass;
+
+/**
+ * Class to display feed of tagged items
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tagfeed implements templatable {
+
+    /** @var array */
+    protected $items;
+
+    /**
+     * Constructor
+     *
+     * Usually the most convenient way is to call constructor without arguments and
+     * add items later using add() method.
+     *
+     * @param array $items
+     */
+    public function __construct($items = array()) {
+        $this->items = array();
+        if ($items) {
+            foreach ($items as $item) {
+                $item = (array)$item + array('img' => '', 'heading' => '', 'details' => '');
+                $this->add($item['img'], $item['heading'], $item['details']);
+            }
+        }
+    }
+
+    /**
+     * Adds one item to the tagfeed
+     *
+     * @param string $img HTML code representing image (or image wrapped in a link), note that
+     *               core_tag/tagfeed template expects image to be 35x35 px
+     * @param string $heading HTML for item heading
+     * @param string $details HTML for item details (keep short)
+     */
+    public function add($img, $heading, $details = '') {
+        $this->items[] = array('img' => $img, 'heading' => $heading, 'details' => $details);
+    }
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(renderer_base $output) {
+        return array('items' => $this->items);
+    }
+}
diff --git a/tag/classes/output/tagindex.php b/tag/classes/output/tagindex.php
new file mode 100644 (file)
index 0000000..f888d6f
--- /dev/null
@@ -0,0 +1,161 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains class core_tag\output\tagindex
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_tag\output;
+
+use renderable;
+use templatable;
+use renderer_base;
+use stdClass;
+use moodle_url;
+use core_tag_tag;
+
+/**
+ * Class to display items tagged with a specific tag
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tagindex implements templatable {
+
+    /** @var core_tag_tag|stdClass */
+    protected $tag;
+
+    /** @var stdClass */
+    protected $tagarea;
+
+    /** @var stdClass */
+    protected $record;
+
+    /**
+     * Constructor
+     *
+     * @param core_tag_tag|stdClass $tag
+     * @param string $component
+     * @param string $itemtype
+     * @param string $content
+     * @param bool $exclusivemode
+     * @param int $fromctx context id where the link was displayed, may be used by callbacks
+     *            to display items in the same context first
+     * @param int $ctx context id where we need to search for items
+     * @param int $rec search items in sub contexts as well
+     * @param int $page
+     * @param bool $totalpages
+     */
+    public function __construct($tag, $component, $itemtype, $content,
+            $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0, $totalpages = 1) {
+        $this->record = new stdClass();
+        $this->tag = $tag;
+
+        $tagareas = \core_tag_area::get_areas();
+        if (!isset($tagareas[$itemtype][$component])) {
+            throw new \coding_exception('Tag area for component '.$component.' and itemtype '.$itemtype.' is not defined');
+        }
+        $this->tagarea = $tagareas[$itemtype][$component];
+        $this->record->tagid = $tag->id;
+        $this->record->ta = $this->tagarea->id;
+        $this->record->itemtype = $itemtype;
+        $this->record->component = $component;
+
+        $a = (object)array(
+            'tagarea' => \core_tag_area::display_name($component, $itemtype),
+            'tag' => \core_tag_tag::make_display_name($tag)
+        );
+        if ($exclusivemode) {
+            $this->record->title = get_string('itemstaggedwith', 'tag', $a);
+        } else {
+            $this->record->title = (string)$a->tagarea;
+        }
+        $this->record->content = $content;
+
+        $this->record->nextpageurl = null;
+        $this->record->prevpageurl = null;
+        $this->record->exclusiveurl = null;
+
+        $url = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, $exclusivemode, $fromctx, $ctx, $rec);
+        $urlparams = array('ta' => $this->tagarea->id);
+        if ($totalpages > $page + 1) {
+            $this->record->nextpageurl = new moodle_url($url, $urlparams + array('page' => $page + 1));
+        }
+        if ($page > 0) {
+            $this->record->prevpageurl = new moodle_url($url, $urlparams + array('page' => $page - 1));
+        }
+        if (!$exclusivemode && ($totalpages > 1 || $page)) {
+            $this->record->exclusiveurl = new moodle_url($url, $urlparams + array('excl' => 1));
+        }
+        $this->record->exclusivetext = get_string('exclusivemode', 'tag', $a);
+        $this->record->hascontent = ($totalpages > 1 || $page || $content);
+        $this->record->anchor = $component . '_' . $itemtype;
+    }
+
+    /**
+     * Magic setter
+     *
+     * @param string $name
+     * @param mixed $value
+     */
+    public function __set($name, $value) {
+        $this->record->$name = $value;
+    }
+
+    /**
+     * Magic getter
+     *
+     * @param string $name
+     * @return mixed
+     */
+    public function __get($name) {
+        return $this->record->$name;
+    }
+
+    /**
+     * Magic isset
+     *
+     * @param string $name
+     * @return bool
+     */
+    public function __isset($name) {
+        return isset($this->record->$name);
+    }
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(renderer_base $output) {
+        if ($this->record->nextpageurl && $this->record->nextpageurl instanceof moodle_url) {
+            $this->record->nextpageurl = $this->record->nextpageurl->out(false);
+        }
+        if ($this->record->prevpageurl && $this->record->prevpageurl instanceof moodle_url) {
+            $this->record->prevpageurl = $this->record->prevpageurl->out(false);
+        }
+        if ($this->record->exclusiveurl && $this->record->exclusiveurl instanceof moodle_url) {
+            $this->record->exclusiveurl = $this->record->exclusiveurl->out(false);
+        }
+        return $this->record;
+    }
+}
diff --git a/tag/classes/output/taglist.php b/tag/classes/output/taglist.php
new file mode 100644 (file)
index 0000000..d0ad198
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains class core_tag\output\taglist
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_tag\output;
+
+use templatable;
+use renderer_base;
+use stdClass;
+use core_tag_tag;
+use context;
+
+/**
+ * Class to preapare a list of tags for display, usually the list of tags some entry is tagged with.
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class taglist implements templatable {
+
+    /** @var array */
+    protected $tags;
+
+    /** @var string */
+    protected $label;
+
+    /** @var string */
+    protected $classes;
+
+    /** @var int */
+    protected $limit;
+
+    /**
+     * Constructor
+     *
+     * @param array $tags list of instances of \core_tag_tag or \stdClass
+     * @param string $label label to display in front, by default 'Tags' (get_string('tags')), set to null
+     *               to use default, set to '' (empty string) to omit the label completely
+     * @param string $classes additional classes for the enclosing div element
+     * @param int $limit limit the number of tags to display, if size of $tags is more than this limit the "more" link
+     *               will be appended to the end, JS will toggle the rest of the tags
+     * @param context $pagecontext specify if needed to overwrite the current page context for the view tag link
+     */
+    public function __construct($tags, $label = null, $classes = '', $limit = 10, $pagecontext = null) {
+        global $PAGE;
+        $canmanagetags = has_capability('moodle/tag:manage', \context_system::instance());
+
+        $this->label = ($label === null) ? get_string('tags') : $label;
+        $this->classes = $classes;
+        $fromctx = $pagecontext ? $pagecontext->id :
+                (($PAGE->context->contextlevel == CONTEXT_SYSTEM) ? 0 : $PAGE->context->id);
+
+        $this->tags = array();
+        foreach ($tags as $idx => $tag) {
+            $this->tags[$idx] = new stdClass();
+
+            $this->tags[$idx]->name = core_tag_tag::make_display_name($tag, false);
+
+            if ($canmanagetags && !empty($tag->flag)) {
+                $this->tags[$idx]->flag = 1;
+            }
+
+            $viewurl = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, 0, $fromctx);
+            $this->tags[$idx]->viewurl = $viewurl->out(false);
+
+            if (!empty($tag->tagtype)) {
+                $this->tags[$idx]->tagtype = $tag->tagtype;
+            }
+
+            if ($limit && count($this->tags) > $limit) {
+                $this->tags[$idx]->overlimit = 1;
+            }
+        }
+        $this->limit = $limit;
+    }
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(renderer_base $output) {
+        $cnt = count($this->tags);
+        return (object)array(
+            'tags' => array_values($this->tags),
+            'label' => $this->label,
+            'tagscount' => $cnt,
+            'overflow' => ($this->limit && $cnt > $this->limit) ? 1 : 0,
+            'classes' => $this->classes,
+        );
+    }
+}
diff --git a/tag/classes/renderer.php b/tag/classes/renderer.php
new file mode 100644 (file)
index 0000000..3949c5e
--- /dev/null
@@ -0,0 +1,180 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains class core_tag_renderer
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class core_tag_renderer
+ *
+ * @package   core_tag
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_tag_renderer extends plugin_renderer_base {
+
+    /**
+     * Renders the tag search page
+     *
+     * @param string $query
+     * @param int $tagcollid
+     * @return string
+     */
+    public function tag_search_page($query = '', $tagcollid = 0) {
+        $rv = $this->output->heading(get_string('searchtags', 'tag'), 2);
+
+        $searchbox = $this->search_form($query, $tagcollid);
+        $rv .= html_writer::div($searchbox, '', array('id' => 'tag-search-box'));
+
+        $tagcloud = core_tag_collection::get_tag_cloud($tagcollid, '', 150, 'name', $query);
+        $searchresults = '';
+        if ($tagcloud->get_count()) {
+            $searchresults = $this->output->render_from_template('core_tag/tagcloud',
+                    $tagcloud->export_for_template($this->output));
+            $rv .= html_writer::div($searchresults, '', array('id' => 'tag-search-results'));
+        } else if (strval($query) !== '') {
+            $rv .= '<div class="tag-search-empty">' . get_string('notagsfound', 'tag', s($query)) . '</div>';
+        }
+
+        return $rv;
+    }
+
+    /**
+     * Renders the tag index page
+     *
+     * @param core_tag_tag $tag
+     * @param \core_tag\output\tagindex[] $entities
+     * @param int $tagareaid
+     * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
+     *             are displayed on the page and the per-page limit may be bigger
+     * @param int $fromctx context id where the link was displayed, may be used by callbacks
+     *            to display items in the same context first
+     * @param int $ctx context id where to search for records
+     * @param bool $rec search in subcontexts as well
+     * @param int $page 0-based number of page being displayed
+     * @return string
+     */
+    public function tag_index_page($tag, $entities, $tagareaid, $exclusivemode, $fromctx, $ctx, $rec, $page) {
+        global $CFG, $OUTPUT;
+        $this->page->requires->js_call_amd('core/tag', 'init_tagindex_page');
+
+        $tagname = $tag->get_display_name();
+        $systemcontext = context_system::instance();
+
+        if ($tag->flag > 0 && has_capability('moodle/tag:manage', $systemcontext)) {
+            $tagname = '<span class="flagged-tag">' . $tagname . '</span>';
+        }
+
+        $rv = '';
+        $rv .= $this->output->heading($tagname, 2);
+
+        $rv .= $this->tag_links($tag);
+
+        if ($desciption = $tag->get_formatted_description()) {
+            $rv .= $this->output->box($desciption, 'generalbox tag-description');
+        }
+
+        $relatedtagslimit = 10;
+        $relatedtags = $tag->get_related_tags();
+        $taglist = new \core_tag\output\taglist($relatedtags, get_string('relatedtags', 'tag'),
+                'tag-relatedtags', $relatedtagslimit);
+        $rv .= $OUTPUT->render_from_template('core_tag/taglist', $taglist->export_for_template($OUTPUT));
+
+        // Display quick menu of the item types (if more than one item type found).
+        $entitylinks = array();
+        foreach ($entities as $entity) {
+            if (!empty($entity->hascontent)) {
+                $entitylinks[] = '<li><a href="#'.$entity->anchor.'">' .
+                        core_tag_area::display_name($entity->component, $entity->itemtype) . '</a></li>';
+            }
+        }
+
+        if (count($entitylinks) > 1) {
+            $rv .= '<div class="tag-index-toc"><ul class="inline-list">' . join('', $entitylinks) . '</ul></div>';
+        } else if (!$entitylinks) {
+            $rv .= '<div class="tag-noresults">' . get_string('noresultsfor', 'tag', $tagname) . '</div>';
+        }
+
+        // Display entities tagged with the tag.
+        $content = '';
+        foreach ($entities as $entity) {
+            if (!empty($entity->hascontent)) {
+                $content .= $this->output->render_from_template('core_tag/index', $entity->export_for_template($this->output));
+            }
+        }
+
+        if ($exclusivemode) {
+            $rv .= $content;
+        } else if ($content) {
+            $rv .= html_writer::div($content, 'tag-index-items');
+        }
+
+        // Display back link if we are browsing one tag area.
+        if ($tagareaid) {
+            $url = $tag->get_view_url(0, $fromctx, $ctx, $rec);
+            $rv .= '<div class="tag-backtoallitems">' .
+                    html_writer::link($url, get_string('backtoallitems', 'tag', $tag->get_display_name())) .
+                    '</div>';
+        }
+
+        return $rv;
+    }
+
+    /**
+     * Prints a box that contains the management links of a tag
+     *
+     * @param core_tag_tag $tag
+     * @return string
+     */
+    protected function tag_links($tag) {
+        if ($links = $tag->get_links()) {
+            $content = '<ul class="inline-list"><li>' . implode('</li> <li>', $links) . '</li></ul>';
+            return html_writer::div($content, 'tag-management-box');
+        }
+        return '';
+    }
+
+    /**
+     * Prints the tag search box
+     *
+     * @param string $query last search string
+     * @param int $tagcollid last selected tag collection id
+     * @return string
+     */
+    protected function search_form($query = '', $tagcollid = 0) {
+        $searchurl = new moodle_url('/tag/search.php');
+        $output = '<form action="' . $searchurl . '">';
+        $output .= '<label class="accesshide" for="searchform_query">' . get_string('searchtags', 'tag') . '</label>';
+        $output .= '<input id="searchform_query" name="query" type="text" size="40" value="' . s($query) . '" />';
+        $tagcolls = core_tag_collection::get_collections_menu(false, true, get_string('inalltagcoll', 'tag'));
+        if (count($tagcolls) > 1) {
+            $output .= '<label class="accesshide" for="searchform_tc">' . get_string('selectcoll', 'tag') . '</label>';
+            $output .= html_writer::select($tagcolls, 'tc', $tagcollid, null, array('id' => 'searchform_tc'));
+        }
+        $output .= '<input name="go" type="submit" size="40" value="' . s(get_string('search', 'tag')) . '" />';
+        $output .= '</form>';
+
+        return $output;
+    }
+
+}
diff --git a/tag/classes/tag.php b/tag/classes/tag.php
new file mode 100644 (file)
index 0000000..a7a62c7
--- /dev/null
@@ -0,0 +1,1414 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains class core_tag_tag
+ *
+ * @package   core_tag
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Represents one tag and also contains lots of useful tag-related methods as static functions.
+ *
+ * Tags can be added to any database records.
+ * $itemtype refers to the DB table name
+ * $itemid refers to id field in this DB table
+ * $component is the component that is responsible for the tag instance
+ * $context is the affected context
+ *
+ * BASIC INSTRUCTIONS :
+ *  - to "tag a blog post" (for example):
+ *        core_tag_tag::set_item_tags('post', 'core', $blogpost->id, $context, $arrayoftags);
+ *
+ *  - to "remove all the tags on a blog post":
+ *        core_tag_tag::remove_all_item_tags('post', 'core', $blogpost->id);
+ *
+ * set_item_tags() will create tags that do not exist yet.
+ *
+ * @property-read int $id
+ * @property-read string $name
+ * @property-read string $rawname
+ * @property-read int $tagcollid
+ * @property-read int $userid
+ * @property-read string $tagtype "official" or "default"
+ * @property-read string $description
+ * @property-read int $descriptionformat
+ * @property-read int $flag 0 if not flagged or positive integer if flagged
+ * @property-read int $timemodified
+ *
+ * @package   core_tag
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_tag_tag {
+
+    /** @var stdClass data about the tag */
+    protected $record = null;
+
+    /**
+     * Constructor. Use functions get(), get_by_name(), etc.
+     *
+     * @param stdClass $record
+     */
+    protected function __construct($record) {
+        if (empty($record->id)) {
+            throw new coding_exeption("Record must contain at least field 'id'");
+        }
+        $this->record = $record;
+    }
+
+    /**
+     * Magic getter
+     *
+     * @param string $name
+     * @return mixed
+     */
+    public function __get($name) {
+        return $this->record->$name;
+    }
+
+    /**
+     * Magic isset method
+     *
+     * @param string $name
+     * @return bool
+     */
+    public function __isset($name) {
+        return isset($this->record->$name);
+    }
+
+    /**
+     * Converts to object
+     *
+     * @return stdClass
+     */
+    public function to_object() {
+        return fullclone($this->record);
+    }
+
+    /**
+     * Returns tag name ready to be displayed
+     *
+     * @param bool $ashtml (default true) if true will return htmlspecialchars encoded string
+     * @return string
+     */
+    public function get_display_name($ashtml = true) {
+        return static::make_display_name($this->record, $ashtml);
+    }
+
+    /**
+     * Prepares tag name ready to be displayed
+     *
+     * @param stdClass|core_tag_tag $tag record from db table tag, must contain properties name and rawname
+     * @param bool $ashtml (default true) if true will return htmlspecialchars encoded string
+     * @return string
+     */
+    public static function make_display_name($tag, $ashtml = true) {
+        global $CFG;
+
+        if (empty($CFG->keeptagnamecase)) {
+            // This is the normalized tag name.
+            $tagname = core_text::strtotitle($tag->name);
+        } else {
+            // Original casing of the tag name.
+            $tagname = $tag->rawname;
+        }
+
+        // Clean up a bit just in case the rules change again.
+        $tagname = clean_param($tagname, PARAM_TAG);
+
+        return $ashtml ? htmlspecialchars($tagname) : $tagname;
+    }
+
+    /**
+     * Adds one or more tag in the database.  This function should not be called directly : you should
+     * use tag_set.
+     *
+     * @param   int      $tagcollid
+     * @param   string|array $tags     one tag, or an array of tags, to be created
+     * @param   bool     $isofficial type of tag to be created. An official tag is kept even if there are no records tagged with it.
+     * @return  array    tag objects indexed by their lowercase normalized names. Any boolean false in the array
+     *                             indicates an error while adding the tag.
+     */
+    protected static function add($tagcollid, $tags, $isofficial = false) {
+        global $USER, $DB;
+
+        $tagobject = new stdClass();
+        $tagobject->tagtype      = $isofficial ? 'official' : 'default';
+        $tagobject->userid       = $USER->id;
+        $tagobject->timemodified = time();
+        $tagobject->tagcollid    = $tagcollid;
+
+        $rv = array();
+        foreach ($tags as $veryrawname) {
+            $rawname = clean_param($veryrawname, PARAM_TAG);
+            if (!$rawname) {
+                $rv[$rawname] = false;
+            } else {
+                $obj = (object)(array)$tagobject;
+                $obj->rawname = $rawname;
+                $obj->name    = core_text::strtolower($rawname);
+                $obj->id      = $DB->insert_record('tag', $obj);
+                $rv[$obj->name] = new static($obj);
+
+                \core\event\tag_created::create_from_tag($rv[$obj->name])->trigger();
+            }
+        }
+
+        return $rv;
+    }
+
+    /**
+     * Simple function to just return a single tag object by its id
+     *
+     * @param    int    $id
+     * @param    string $returnfields which fields do we want returned from table {tag}.
+     *                        Default value is 'id,name,rawname,tagcollid',
+     *                        specify '*' to include all fields.
+     * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
+     *                        IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended);
+     *                        MUST_EXIST means throw exception if no record or multiple records found
+     * @return   core_tag_tag|false  tag object
+     */
+    public static function get($id, $returnfields = 'id, name, rawname, tagcollid', $strictness = IGNORE_MISSING) {
+        global $DB;
+        $record = $DB->get_record('tag', array('id' => $id), $returnfields, $strictness);
+        if ($record) {
+            return new static($record);
+        }
+        return false;
+    }
+
+    /**
+     * Simple function to just return a single tag object by tagcollid and name
+     *
+     * @param int $tagcollid tag collection to use,
+     *        if 0 is given we will try to guess the tag collection and return the first match
+     * @param string $name tag name
+     * @param string $returnfields which fields do we want returned. This is a comma separated string
+     *         containing any combination of 'id', 'name', 'rawname', 'tagcollid' or '*' to include all fields.
+     * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
+     *                        IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended);
+     *                        MUST_EXIST means throw exception if no record or multiple records found
+     * @return core_tag_tag|false tag object
+     */
+    public static function get_by_name($tagcollid, $name, $returnfields='id, name, rawname, tagcollid',
+                        $strictness = IGNORE_MISSING) {
+        global $DB;
+        if ($tagcollid == 0) {
+            $tags = static::guess_by_name($name, $returnfields);
+            if ($tags) {
+                $tag = reset($tags);
+                return $tag;
+            } else if ($strictness == MUST_EXIST) {
+                throw new dml_missing_record_exception('tag', 'name=?', array($name));
+            }
+            return false;
+        }
+        $name = core_text::strtolower($name);   // To cope with input that might just be wrong case.
+        $params = array('name' => $name, 'tagcollid' => $tagcollid);
+        $record = $DB->get_record('tag', $params, $returnfields, $strictness);
+        if ($record) {
+            return new static($record);
+        }
+        return false;
+    }
+
+    /**
+     * Looking in all tag collections for the tag with the given name
+     *
+     * @param string $name tag name
+     * @param string $returnfields
+     * @return array array of core_tag_tag instances
+     */
+    public static function guess_by_name($name, $returnfields='id, name, rawname, tagcollid') {
+        global $DB;
+        if (empty($name)) {
+            return array();
+        }
+        $tagcolls = core_tag_collection::get_collections();
+        list($sql, $params) = $DB->get_in_or_equal(array_keys($tagcolls), SQL_PARAMS_NAMED);
+        $params['name'] = core_text::strtolower($name);
+        $tags = $DB->get_records_select('tag', 'name = :name AND tagcollid ' . $sql, $params, '', $returnfields);
+        if (count($tags) > 1) {
+            // Sort in the same order as tag collections.
+            uasort($tags, create_function('$a,$b', '$tagcolls = core_tag_collection::get_collections(); ' .
+                'return $tagcolls[$a->tagcollid]->sortorder < $tagcolls[$b->tagcollid]->sortorder ? -1 : 1;'));
+        }
+        $rv = array();
+        foreach ($tags as $id => $tag) {
+            $rv[$id] = new static($tag);
+        }
+        return $rv;
+    }
+
+    /**
+     * Returns the list of tag objects by tag collection id and the list of tag names
+     *
+     * @param    int   $tagcollid
+     * @param    array $tags array of tags to look for
+     * @param    string $returnfields list of DB fields to return, must contain 'id', 'name' and 'rawname'
+     * @return   array tag-indexed array of objects. No value for a key means the tag wasn't found.
+     */
+    public static function get_by_name_bulk($tagcollid, $tags, $returnfields = 'id, name, rawname, tagcollid') {
+        global $DB;
+
+        if (empty($tags)) {
+            return array();
+        }
+
+        $cleantags = self::normalize(self::normalize($tags, false)); // Format: rawname => normalised name.
+
+        list($namesql, $params) = $DB->get_in_or_equal(array_values($cleantags));
+        array_unshift($params, $tagcollid);
+
+        $recordset = $DB->get_recordset_sql("SELECT $returnfields FROM {tag} WHERE tagcollid = ? AND name $namesql", $params);
+
+        $result = array_fill_keys($cleantags, null);
+        foreach ($recordset as $record) {
+            $result[$record->name] = new static($record);
+        }
+        $recordset->close();
+        return $result;
+    }
+
+
+    /**
+     * Function that normalizes a list of tag names.
+     *
+     * @param   array        $rawtags array of tags
+     * @param   bool         $tolowercase convert to lower case?
+     * @return  array        lowercased normalized tags, indexed by the normalized tag, in the same order as the original array.
+     *                       (Eg: 'Banana' => 'banana').
+     */
+    public static function normalize($rawtags, $tolowercase = true) {
+        $result = array();
+        foreach ($rawtags as $rawtag) {
+            $rawtag = trim($rawtag);
+            if (strval($rawtag) !== '') {
+                $clean = clean_param($rawtag, PARAM_TAG);
+                if ($tolowercase) {
+                    $result[$rawtag] = core_text::strtolower($clean);
+                } else {
+                    $result[$rawtag] = $clean;
+                }
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Retrieves tags and/or creates them if do not exist yet
+     *
+     * @param int $tagcollid
+     * @param array $tags array of raw tag names, do not have to be normalised
+     * @param bool $createasofficial
+     * @return core_tag_tag[] array of tag objects indexed with lowercase normalised tag name
+     */
+    public static function create_if_missing($tagcollid, $tags, $createasofficial = false) {
+        $cleantags = self::normalize(array_filter(self::normalize($tags, false))); // Array rawname => normalised name .
+
+        $result = static::get_by_name_bulk($tagcollid, $tags, '*');
+        $existing = array_filter($result);
+        $missing = array_diff_key(array_flip($cleantags), $existing); // Array normalised name => rawname.
+        if ($missing) {
+            $newtags = static::add($tagcollid, array_values($missing), $createasofficial);
+            foreach ($newtags as $tag) {
+                $result[$tag->name] = $tag;
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Creates a URL to view a tag
+     *
+     * @param int $tagcollid
+     * @param string $name
+     * @param int $exclusivemode
+     * @param int $fromctx context id where this tag cloud is displayed
+     * @param int $ctx context id for tag view link
+     * @param int $rec recursive argument for tag view link
+     * @return \moodle_url
+     */
+    public static function make_url($tagcollid, $name, $exclusivemode = 0, $fromctx = 0, $ctx = 0, $rec = 1) {
+        $coll = core_tag_collection::get_by_id($tagcollid);
+        if (!empty($coll->customurl)) {
+            $url = '/' . ltrim(trim($coll->customurl), '/');
+        } else {
+            $url = '/tag/index.php';
+        }
+        $params = array('tc' => $tagcollid, 'tag' => $name);
+        if ($exclusivemode) {
+            $params['excl'] = 1;
+        }
+        if ($fromctx) {
+            $params['from'] = $fromctx;
+        }
+        if ($ctx) {
+            $params['ctx'] = $ctx;
+        }
+        if (!$rec) {
+            $params['rec'] = 0;
+        }
+        return new moodle_url($url, $params);
+    }
+
+    /**
+     * Returns URL to view the tag
+     *
+     * @param int $exclusivemode
+     * @param int $fromctx context id where this tag cloud is displayed
+     * @param int $ctx context id for tag view link
+     * @param int $rec recursive argument for tag view link
+     * @return \moodle_url
+     */
+    public function get_view_url($exclusivemode = 0, $fromctx = 0, $ctx = 0, $rec = 1) {
+        return static::make_url($this->record->tagcollid, $this->record->rawname,
+            $exclusivemode, $fromctx, $ctx, $rec);
+    }
+
+    /**
+     * Validates that the required fields were retrieved and retrieves them if missing
+     *
+     * @param array $list array of the fields that need to be validated
+     * @param string $caller name of the function that requested it, for the debugging message
+     */
+    protected function ensure_fields_exist($list, $caller) {
+        global $DB;
+        $missing = array_diff($list, array_keys((array)$this->record));
+        if ($missing) {
+            debugging('core_tag_tag::' . $caller . '() must be called on fully retrieved tag object. Missing fields: '.
+                    join(', ', $missing), DEBUG_DEVELOPER);
+            $this->record = $DB->get_record('tag', array('id' => $this->record->id), '*', MUST_EXIST);
+        }
+    }
+
+    /**
+     * Deletes the tag instance given the record from tag_instance DB table
+     *
+     * @param stdClass $taginstance
+     * @param bool $fullobject whether $taginstance contains all fields from DB table tag_instance
+     *          (in this case it is safe to add a record snapshot to the event)
+     * @return bool
+     */
+    protected function delete_instance_as_record($taginstance, $fullobject = false) {
+        global $DB;
+
+        $this->ensure_fields_exist(array('name', 'rawname', 'tagtype'), 'delete_instance_as_record');
+
+        $DB->delete_records('tag_instance', array('id' => $taginstance->id));
+
+        // We can not fire an event with 'null' as the contextid.
+        if (is_null($taginstance->contextid)) {
+            $taginstance->contextid = context_system::instance()->id;
+        }
+
+        // Trigger tag removed event.
+        $taginstance->tagid = $this->id;
+        \core\event\tag_removed::create_from_tag_instance($taginstance, $this->name, $this->rawname, $fullobject)->trigger();
+
+        // If there are no other instances of the tag then consider deleting the tag as well.
+        if ($this->tagtype === 'default') {
+            if (!$DB->record_exists('tag_instance', array('tagid' => $this->id))) {
+                self::delete_tags($this->id);
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Delete one instance of a tag.  If the last instance was deleted, it will also delete the tag, unless its type is 'official'.
+     *
+     * @param    string $component component responsible for tagging. For BC it can be empty but in this case the
+     *                  query will be slow because DB index will not be used.
+     * @param    string $itemtype the type of the record for which to remove the instance
+     * @param    int    $itemid   the id of the record for which to remove the instance
+     * @param    int    $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)
+     */
+    protected function delete_instance($component, $itemtype, $itemid, $tiuserid = 0) {
+        global $DB;
+        $params = array('tagid' => $this->id,
+                'itemtype' => $itemtype, 'itemid' => $itemid);
+        if ($tiuserid) {
+            $params['tiuserid'] = $tiuserid;
+        }
+        if ($component) {
+            $params['component'] = $component;
+        }
+
+        $taginstance = $DB->get_record('tag_instance', $params);
+        if (!$taginstance) {
+            return;
+        }
+        $this->delete_instance_as_record($taginstance, true);
+    }
+
+    /**
+     * Bulk delete all tag instances for a component or tag area
+     *
+     * @param string $component
+     * @param string $itemtype (optional)
+     * @param int $contextid (optional)
+     */
+    public static function delete_instances($component, $itemtype = null, $contextid = null) {
+        global $DB;
+
+        $sql = "SELECT ti.*, t.name, t.rawname, t.tagtype
+                  FROM {tag_instance} ti
+                  JOIN {tag} t
+                    ON ti.tagid = t.id
+                 WHERE ti.component = :component";
+        $params = array('component' => $component);
+        if (!is_null($contextid)) {
+            $sql .= " AND ti.contextid = :contextid";
+            $params['contextid'] = $contextid;
+        }
+        if (!is_null($itemtype)) {
+            $sql .= " AND ti.itemtype = :itemtype";
+            $params['itemtype'] = $itemtype;
+        }
+        if ($taginstances = $DB->get_records_sql($sql, $params)) {
+            // Now remove all the tag instances.
+            $DB->delete_records('tag_instance', $params);
+            // Save the system context in case the 'contextid' column in the 'tag_instance' table is null.
+            $syscontextid = context_system::instance()->id;
+            // Loop through the tag instances and fire an 'tag_removed' event.
+            foreach ($taginstances as $taginstance) {
+                // We can not fire an event with 'null' as the contextid.
+                if (is_null($taginstance->contextid)) {
+                    $taginstance->contextid = $syscontextid;
+                }
+
+                // Trigger tag removed event.
+                \core\event\tag_removed::create_from_tag_instance($taginstance, $taginstance->name,
+                        $taginstance->rawname, true)->trigger();
+            }
+        }
+    }
+
+    /**
+     * Adds a tag instance
+     *
+     * @param string $component
+     * @param string $itemtype
+     * @param string $itemid
+     * @param context $context
+     * @param int $ordering
+     * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)
+     * @return int id of tag_instance
+     */
+    protected function add_instance($component, $itemtype, $itemid, context $context, $ordering, $tiuserid = 0) {
+        global $DB;
+        $this->ensure_fields_exist(array('name', 'rawname'), 'add_instance');
+
+        $taginstance = new StdClass;
+        $taginstance->tagid        = $this->id;
+        $taginstance->component    = $component ? $component : '';
+        $taginstance->itemid       = $itemid;
+        $taginstance->itemtype     = $itemtype;
+        $taginstance->contextid    = $context->id;
+        $taginstance->ordering     = $ordering;
+        $taginstance->timecreated  = time();
+        $taginstance->timemodified = $taginstance->timecreated;
+        $taginstance->tiuserid     = $tiuserid;
+
+        $taginstance->id = $DB->insert_record('tag_instance', $taginstance);
+
+        // Trigger tag added event.
+        \core\event\tag_added::create_from_tag_instance($taginstance, $this->name, $this->rawname, true)->trigger();
+
+        return $taginstance->id;
+    }
+
+    /**
+     * Updates the ordering on tag instance
+     *
+     * @param int $instanceid
+     * @param int $ordering
+     */
+    protected function update_instance_ordering($instanceid, $ordering) {
+        global $DB;
+        $data = new stdClass();
+        $data->id = $instanceid;
+        $data->ordering = $ordering;
+        $data->timemodified = time();
+
+        $DB->update_record('tag_instance', $data);
+    }
+
+    /**
+     * Get the array of core_tag_tag objects associated with an item (instances).
+     *
+     * Use {@link core_tag_tag::get_item_tags_array()} if you wish to get the same data as simple array.
+     *
+     * @param string $component component responsible for tagging. For BC it can be empty but in this case the
+     *               query will be slow because DB index will not be used.
+     * @param string $itemtype type of the tagged item
+     * @param int $itemid
+     * @param null|bool $official - true - official only, false - non official only, null - any (default)
+     * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging
+     * @return core_tag_tag[] each object contains additional fields taginstanceid, taginstancecontextid and ordering
+     */
+    public static function get_item_tags($component, $itemtype, $itemid, $official = null, $tiuserid = 0) {
+        global $DB;
+
+        if (static::is_enabled($component, $itemtype) === false) {
+            // Tagging area is properly defined but not enabled - return empty array.
+            return array();
+        }
+
+        // Note: if the fields in this query are changed, you need to do the same changes in tag_get_correlated().
+        $sql = "SELECT ti.id AS taginstanceid, tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag,
+                    tg.tagcollid, ti.ordering, ti.contextid AS taginstancecontextid
+                  FROM {tag_instance} ti
+                  JOIN {tag} tg ON tg.id = ti.tagid
+                  WHERE ti.itemtype = :itemtype AND ti.itemid = :itemid ".
+                ($component ? "AND ti.component = :component " : "").
+                ($tiuserid ? "AND ti.tiuserid = :tiuserid " : "").
+                (($official === true) ? "AND tg.tagtype = :official " : "").
+                (($official === false) ? "AND tg.tagtype <> :official " : "").
+               "ORDER BY ti.ordering ASC, ti.id";
+
+        $params = array();
+        $params['itemtype'] = $itemtype;
+        $params['itemid'] = $itemid;
+        $params['component'] = $component;
+        $params['official'] = 'official';
+        $params['tiuserid'] = $tiuserid;
+
+        $records = $DB->get_records_sql($sql, $params);
+        $result = array();
+        foreach ($records as $id => $record) {
+            $result[$id] = new static($record);
+        }
+        return $result;
+    }
+
+    /**
+     * Returns the list of display names of the tags that are associated with an item
+     *
+     * This method is usually used to prefill the form data for the 'tags' form element
+     *
+     * @param string $component component responsible for tagging. For BC it can be empty but in this case the
+     *               query will be slow because DB index will not be used.
+     * @param string $itemtype type of the tagged item
+     * @param int $itemid
+     * @param null|bool $official - true - official only, false - non official only, null - any (default)
+     * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging
+     * @param bool $ashtml (default true) if true will return htmlspecialchars encoded tag names
+     * @return string[] array of tags display names
+     */
+    public static function get_item_tags_array($component, $itemtype, $itemid, $official = null, $tiuserid = 0, $ashtml = true) {
+        $tags = array();
+        foreach (static::get_item_tags($component, $itemtype, $itemid, $official, $tiuserid) as $tag) {
+            $tags[$tag->id] = $tag->get_display_name($ashtml);
+        }
+        return $tags;
+    }
+
+    /**
+     * Sets the list of tag instances for one item (table record).
+     *
+     * Extra exsisting instances are removed, new ones are added. New tags are created if needed.
+     *
+     * This method can not be used for setting tags relations, please use set_related_tags()
+     *
+     * @param string $component component responsible for tagging
+     * @param string $itemtype type of the tagged item
+     * @param int $itemid
+     * @param context $context
+     * @param array $tagnames
+     * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)
+     */
+    public static function set_item_tags($component, $itemtype, $itemid, context $context, $tagnames, $tiuserid = 0) {
+        if ($itemtype === 'tag') {
+            if ($tiuserid) {
+                throw new coding_exeption('Related tags can not have tag instance userid');
+            }
+            debugging('You can not use set_item_tags() for tagging a tag, please use set_related_tags()', DEBUG_DEVELOPER);
+            static::get($itemid, '*', MUST_EXIST)->set_related_tags($tagnames);
+            return;
+        }
+
+        if ($tagnames !== null && static::is_enabled($component, $itemtype) === false) {
+            // Tagging area is properly defined but not enabled - do nothing.
+            // Unless we are deleting the item tags ($tagnames === null), in which case proceed with deleting.
+            return;
+        }
+
+        // Apply clean_param() to all tags.
+        if ($tagnames) {
+            $tagcollid = core_tag_area::get_collection($component, $itemtype);
+            $tagobjects = static::create_if_missing($tagcollid, $tagnames);
+        } else {
+            $tagobjects = array();
+        }
+
+        $currenttags = static::get_item_tags($component, $itemtype, $itemid, null, $tiuserid);
+
+        // For data coherence reasons, it's better to remove deleted tags
+        // before adding new data: ordering could be duplicated.
+        foreach ($currenttags as $currenttag) {
+            if (!array_key_exists($currenttag->name, $tagobjects)) {
+                $taginstance = (object)array('id' => $currenttag->taginstanceid,
+                    'itemtype' => $itemtype, 'itemid' => $itemid,
+                    'contextid' => $currenttag->taginstancecontextid, 'tiuserid' => $tiuserid);
+                $currenttag->delete_instance_as_record($taginstance, false);
+            }
+        }
+
+        $ordering = -1;
+        foreach ($tagobjects as $name => $tag) {
+            $ordering++;
+            foreach ($currenttags as $currenttag) {
+                if (strval($currenttag->name) === strval($name)) {
+                    if ($currenttag->ordering != $ordering) {
+                        $currenttag->update_instance_ordering($currenttag->taginstanceid, $ordering);
+                    }
+                    continue 2;
+                }
+            }
+            $tag->add_instance($component, $itemtype, $itemid, $context, $ordering, $tiuserid);
+        }
+    }
+
+    /**
+     * Removes all tags from an item.
+     *
+     * All tags will be removed even if tagging is disabled in this area. This is
+     * usually called when the item itself has been deleted.
+     *
+     * @param string $component component responsible for tagging
+     * @param string $itemtype type of the tagged item
+     * @param int $itemid
+     * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)
+     */
+    public static function remove_all_item_tags($component, $itemtype, $itemid, $tiuserid = 0) {
+        $context = context_system::instance(); // Context will not be used.
+        static::set_item_tags($component, $itemtype, $itemid, $context, null, $tiuserid);
+    }
+
+    /**
+     * Adds a tag to an item, without overwriting the current tags.
+     *
+     * If the tag has already been added to the record, no changes are made.
+     *
+     * @param string $component the component that was tagged
+     * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.)
+     * @param int $itemid the id of the record to tag
+     * @param context $context the context of where this tag was assigned
+     * @param string $tagname the tag to add
+     * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)
+     * @return int id of tag_instance that was either created or already existed or null if tagging is not enabled
+     */
+    public static function add_item_tag($component, $itemtype, $itemid, context $context, $tagname, $tiuserid = 0) {
+        global $DB;
+
+        if (static::is_enabled($component, $itemtype) === false) {
+            // Tagging area is properly defined but not enabled - do nothing.
+            return null;
+        }
+
+        $rawname = clean_param($tagname, PARAM_TAG);
+        $normalisedname = core_text::strtolower($rawname);
+        $tagcollid = core_tag_area::get_collection($component, $itemtype);
+
+        $usersql = $tiuserid ? " AND ti.tiuserid = :tiuserid " : "";
+        $sql = 'SELECT t.*, ti.id AS taginstanceid
+                FROM {tag} t
+                LEFT JOIN {tag_instance} ti ON ti.tagid = t.id AND ti.itemtype = :itemtype '.
+                $usersql .
+                'AND ti.itemid = :itemid AND ti.component = :component
+                WHERE t.name = :name AND t.tagcollid = :tagcollid';
+        $params = array('name' => $normalisedname, 'tagcollid' => $tagcollid, 'itemtype' => $itemtype,
+            'itemid' => $itemid, 'component' => $component, 'tiuserid' => $tiuserid);
+        $record = $DB->get_record_sql($sql, $params);
+        if ($record) {
+            if ($record->taginstanceid) {
+                // Tag was already added to the item, nothing to do here.
+                return $record->taginstanceid;
+            }
+            $tag = new static($record);
+        } else {
+            // The tag does not exist yet, create it.
+            $tags = static::add($tagcollid, array($tagname));
+            $tag = reset($tags);
+        }
+
+        $ordering = $DB->get_field_sql('SELECT MAX(ordering) FROM {tag_instance} ti
+                WHERE ti.itemtype = :itemtype AND ti.itemid = itemid AND
+                ti.component = :component' . $usersql, $params);
+
+        return $tag->add_instance($component, $itemtype, $itemid, $context, $ordering + 1, $tiuserid);
+    }
+
+    /**
+     * Removes the tag from an item without changing the other tags
+     *
+     * @param string $component the component that was tagged
+     * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.)
+     * @param int $itemid the id of the record to tag
+     * @param string $tagname the tag to remove
+     * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)
+     */
+    public static function remove_item_tag($component, $itemtype, $itemid, $tagname, $tiuserid = 0) {
+        global $DB;
+
+        if (static::is_enabled($component, $itemtype) === false) {
+            // Tagging area is properly defined but not enabled - do nothing.
+            return array();
+        }
+
+        $rawname = clean_param($tagname, PARAM_TAG);
+        $normalisedname = core_text::strtolower($rawname);
+
+        $usersql = $tiuserid ? " AND tiuserid = :tiuserid " : "";
+        $componentsql = $component ? " AND ti.component = :component " : "";
+        $sql = 'SELECT t.*, ti.id AS taginstanceid, ti.contextid AS taginstancecontextid, ti.ordering
+                FROM {tag} t JOIN {tag_instance} ti ON ti.tagid = t.id ' . $usersql . '
+                WHERE t.name = :name AND ti.itemtype = :itemtype
+                AND ti.itemid = :itemid ' . $componentsql;
+        $params = array('name' => $normalisedname,
+            'itemtype' => $itemtype, 'itemid' => $itemid, 'component' => $component,
+            'tiuserid' => $tiuserid);
+        if ($record = $DB->get_record_sql($sql, $params)) {
+            $taginstance = (object)array('id' => $record->taginstanceid,
+                'itemtype' => $itemtype, 'itemid' => $itemid,
+                'contextid' => $record->taginstancecontextid, 'tiuserid' => $tiuserid);
+            $tag = new static($record);
+            $tag->delete_instance_as_record($taginstance, false);
+            $sql = "UPDATE {tag_instance} ti SET ordering = ordering - 1
+                    WHERE ti.itemtype = :itemtype
+                AND ti.itemid = :itemid $componentsql $usersql
+                AND ti.ordering > :ordering";
+            $params['ordering'] = $record->ordering;
+            $DB->execute($sql, $params);
+        }
+    }
+
+    /**
+     * Allows to move all tag instances from one context to another
+     *
+     * @param string $component the component that was tagged
+     * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.)
+     * @param context $oldcontext
+     * @param context $newcontext
+     */
+    public static function move_context($component, $itemtype, $oldcontext, $newcontext) {
+        global $DB;
+        if ($oldcontext instanceof context) {
+            $oldcontext = $oldcontext->id;
+        }
+        if ($newcontext instanceof context) {
+            $newcontext = $newcontext->id;
+        }
+        $DB->set_field('tag_instance', 'contextid', $newcontext,
+                array('component' => $component, 'itemtype' => $itemtype, 'contextid' => $oldcontext));
+    }
+
+    /**
+     * Moves all tags of the specified items to the new context
+     *
+     * @param string $component the component that was tagged
+     * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.)
+     * @param array $itemids
+     * @param context|int $newcontext target context to move tags to
+     */
+    public static function change_items_context($component, $itemtype, $itemids, $newcontext) {
+        global $DB;
+        if (empty($itemids)) {
+            return;
+        }
+        if (!is_array($itemids)) {
+            $itemids = array($itemids);
+        }
+        list($sql, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
+        $params['component'] = $component;
+        $params['itemtype'] = $itemtype;
+        if ($newcontext instanceof context) {
+            $newcontext = $newcontext->id;
+        }
+        $DB->set_field_select('tag_instance', 'contextid', $newcontext,
+            'component = :component AND itemtype = :itemtype AND itemid ' . $sql, $params);
+    }
+
+    /**
+     * Updates the information about the tag
+     *
+     * @param array|stdClass $data data to update, may contain: tagtype, description, descriptionformat, rawname
+     * @return bool whether the tag was updated. False may be returned if: all new values match the existing,
+     *         or an invalid tagtype was supplied, or it was attempted to rename the tag to the name that is already used.
+     */
+    public function update($data) {
+        global $DB, $COURSE;
+
+        $allowedfields = array('tagtype', 'description', 'descriptionformat', 'rawname');
+
+        $data = (array)$data;
+        if ($extrafields = array_diff(array_keys($data), $allowedfields)) {
+            debugging('The field(s) '.join(', ', $extrafields).' will be ignored when updating the tag',
+                    DEBUG_DEVELOPER);
+        }
+        $data = array_intersect_key($data, array_fill_keys($allowedfields, 1));
+        $this->ensure_fields_exist(array_merge(array('tagcollid', 'userid', 'name', 'rawname'), array_keys($data)), 'update');
+
+        // Validate the tag name.
+        if (array_key_exists('rawname', $data)) {
+            $data['rawname'] = clean_param($data['rawname'], PARAM_TAG);
+            $name = core_text::strtolower($data['rawname']);
+
+            if (!$name) {
+                unset($data['rawname']);
+            } else if ($existing = static::get_by_name($this->tagcollid, $name, 'id')) {
+                // Prevent the rename if a tag with that name already exists.
+                if ($existing->id != $this->id) {
+                    debugging('New tag name already exists, you should check it before calling core_tag_tag::update()',
+                            DEBUG_DEVELOPER);
+                    unset($data['rawname']);
+                }
+            }
+            if (isset($data['rawname'])) {
+                $data['name'] = $name;
+            }
+        }
+
+        // Validate the tag type.
+        if (array_key_exists('tagtype', $data) && $data['tagtype'] !== 'official' && $data['tagtype'] !== 'default') {
+            debugging('Unrecognised tag type "'.$data['tagtype'].'" ignored when updating the tag', DEBUG_DEVELOPER);
+            unset($data['tagtype']);
+        }
+
+        // Find only the attributes that need to be changed.
+        $originalname = $this->name;
+        foreach ($data as $key => $value) {
+            if ($this->record->$key !== $value) {
+                $this->record->$key = $value;
+            } else {
+                unset($data[$key]);
+            }
+        }
+        if (empty($data)) {
+            return false;
+        }
+
+        $data['id'] = $this->id;
+        $data['timemodified'] = time();
+        $DB->update_record('tag', $data);
+
+        $event = \core\event\tag_updated::create(array(
+            'objectid' => $this->id,
+            'relateduserid' => $this->userid,
+            'context' => context_system::instance(),
+            'other' => array(
+                'name' => $this->name,
+                'rawname' => $this->rawname
+            )
+        ));
+        if (isset($data['rawname'])) {
+            $event->set_legacy_logdata(array($COURSE->id, 'tag', 'update', 'index.php?id='. $this->id,
+                $originalname . '->'. $this->name));
+        }
+        $event->trigger();
+        return true;
+    }
+
+    /**
+     * Flag a tag as inappropriate
+     */
+    public function flag() {
+        global $DB;
+
+        $this->ensure_fields_exist(array('name', 'userid', 'rawname', 'flag'), 'flag');
+
+        // Update all the tags to flagged.
+        $this->timemodified = time();
+        $this->flag++;
+        $DB->update_record('tag', array('timemodified' => $this->timemodified,
+            'flag' => $this->flag, 'id' => $this->id));
+
+        $event = \core\event\tag_flagged::create(array(
+            'objectid' => $this->id,
+            'relateduserid' => $this->userid,
+            'context' => context_system::instance(),
+            'other' => array(
+                'name' => $this->name,
+                'rawname' => $this->rawname
+            )
+
+        ));
+        $event->trigger();
+    }
+
+    /**
+     * Remove the inappropriate flag on a tag.
+     */
+    public function reset_flag() {
+        global $DB;
+
+        $this->ensure_fields_exist(array('name', 'userid', 'rawname', 'flag'), 'flag');
+
+        if (!$this->flag) {
+            // Nothing to do.
+            return false;
+        }
+
+        $this->timemodified = time();
+        $this->flag = 0;
+        $DB->update_record('tag', array('timemodified' => $this->timemodified,
+            'flag' => 0, 'id' => $this->id));
+
+        $event = \core\event\tag_unflagged::create(array(
+            'objectid' => $this->id,
+            'relateduserid' => $this->userid,
+            'context' => context_system::instance(),
+            'other' => array(
+                'name' => $this->name,
+                'rawname' => $this->rawname
+            )
+        ));
+        $event->trigger();
+    }
+
+    /**
+     * Sets the list of tags related to this one.
+     *
+     * Tag relations are recorded by two instances linking two tags to each other.
+     * For tag relations ordering is not used and may be random.
+     *
+     * @param array $tagnames
+     */
+    public function set_related_tags($tagnames) {
+        $context = context_system::instance();
+        $tagobjects = $tagnames ? static::create_if_missing($this->tagcollid, $tagnames) : array();
+        unset($tagobjects[$this->name]); // Never link to itself.
+
+        $currenttags = static::get_item_tags('core', 'tag', $this->id);
+
+        // For data coherence reasons, it's better to remove deleted tags
+        // before adding new data: ordering could be duplicated.
+        foreach ($currenttags as $currenttag) {
+            if (!array_key_exists($currenttag->name, $tagobjects)) {
+                $taginstance = (object)array('id' => $currenttag->taginstanceid,
+                    'itemtype' => 'tag', 'itemid' => $this->id,
+                    'contextid' => $context->id);
+                $currenttag->delete_instance_as_record($taginstance, false);
+                $this->delete_instance('core', 'tag', $currenttag->id);
+            }
+        }
+
+        foreach ($tagobjects as $name => $tag) {
+            foreach ($currenttags as $currenttag) {
+                if ($currenttag->name === $name) {
+                    continue 2;
+                }
+            }
+            $this->add_instance('core', 'tag', $tag->id, $context, 0);
+            $tag->add_instance('core', 'tag', $this->id, $context, 0);
+            $currenttags[] = $tag;
+        }
+    }
+
+    /**
+     * Adds to the list of related tags without removing existing
+     *
+     * Tag relations are recorded by two instances linking two tags to each other.
+     * For tag relations ordering is not used and may be random.
+     *
+     * @param array $tagnames
+     */
+    public function add_related_tags($tagnames) {
+        $context = context_system::instance();
+        $tagobjects = static::create_if_missing($this->tagcollid, $tagnames);
+
+        $currenttags = static::get_item_tags('core', 'tag', $this->id);
+
+        foreach ($tagobjects as $name => $tag) {
+            foreach ($currenttags as $currenttag) {
+                if ($currenttag->name === $name) {
+                    continue 2;
+                }
+            }
+            $this->add_instance('core', 'tag', $tag->id, $context, 0);
+            $tag->add_instance('core', 'tag', $this->id, $context, 0);
+            $currenttags[] = $tag;
+        }
+    }
+
+    /**
+     * Returns the correlated tags of a tag, retrieved from the tag_correlation table.
+     *
+     * Correlated tags are calculated in cron based on existing tag instances.
+     *
+     * @param bool $keepduplicates if true, will return one record for each existing
+     *      tag instance which may result in duplicates of the actual tags
+     * @return core_tag_tag[] an array of tag objects
+     */
+    public function get_correlated_tags($keepduplicates = false) {
+        global $DB;
+
+        $correlated = $DB->get_field('tag_correlation', 'correlatedtags', array('tagid' => $this->id));
+
+        if (!$correlated) {
+            return array();
+        }
+        $correlated = preg_split('/\s*,\s*/', trim($correlated), -1, PREG_SPLIT_NO_EMPTY);
+        list($query, $params) = $DB->get_in_or_equal($correlated);
+
+        // This is (and has to) return the same fields as the query in core_tag_tag::get_item_tags().
+        $sql = "SELECT ti.id AS taginstanceid, tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag,
+                tg.tagcollid, ti.ordering, ti.contextid AS taginstancecontextid
+              FROM {tag} tg
+        INNER JOIN {tag_instance} ti ON tg.id = ti.tagid
+             WHERE tg.id $query
+          ORDER BY ti.ordering ASC, ti.id";
+        $records = $DB->get_records_sql($sql, $params);
+        $seen = array();
+        $result = array();
+        foreach ($records as $id => $record) {
+            if (!$keepduplicates && !empty($seen[$record->id])) {
+                continue;
+            }
+            $result[$id] = new static($record);
+            $seen[$record->id] = true;
+        }
+        return $result;
+    }
+
+    /**
+     * Returns tags that this tag was manually set as related to
+     *
+     * @return core_tag_tag[]
+     */
+    public function get_manual_related_tags() {
+        return self::get_item_tags('core', 'tag', $this->id);
+    }
+
+    /**
+     * Returns tags related to a tag
+     *
+     * Related tags of a tag come from two sources:
+     *   - manually added related tags, which are tag_instance entries for that tag
+     *   - correlated tags, which are calculated
+     *
+     * @return core_tag_tag[] an array of tag objects
+     */
+    public function get_related_tags() {
+        $manual = $this->get_manual_related_tags();
+        $automatic = $this->get_correlated_tags();
+        $relatedtags = array_merge($manual, $automatic);
+
+        // Remove duplicated tags (multiple instances of the same tag).
+        $seen = array();
+        foreach ($relatedtags as $instance => $tag) {
+            if (isset($seen[$tag->id])) {
+                unset($relatedtags[$instance]);
+            } else {
+                $seen[$tag->id] = 1;
+            }
+        }
+
+        return $relatedtags;
+    }
+
+    /**
+     * Find all items tagged with a tag of a given type ('post', 'user', etc.)
+     *
+     * @param    string   $component component responsible for tagging. For BC it can be empty but in this case the
+     *                    query will be slow because DB index will not be used.
+     * @param    string   $itemtype  type to restrict search to
+     * @param    int      $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point.
+     * @param    int      $limitnum  (optional, required if $limitfrom is set) return a subset comprising this many records.
+     * @param    string   $subquery additional query to be appended to WHERE clause, refer to the itemtable as 'it'
+     * @param    array    $params additional parameters for the DB query
+     * @return   array of matching objects, indexed by record id, from the table containing the type requested
+     */
+    public function get_tagged_items($component, $itemtype, $limitfrom = '', $limitnum = '', $subquery = '', $params = array()) {
+        global $DB;
+
+        if (empty($itemtype) || !$DB->get_manager()->table_exists($itemtype)) {
+            return array();
+        }
+        $params = $params ? $params : array();
+
+        $query = "SELECT it.*
+                    FROM {".$itemtype."} it INNER JOIN {tag_instance} tt ON it.id = tt.itemid
+                   WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid";
+        $params['itemtype'] = $itemtype;
+        $params['tagid'] = $this->id;
+        if ($component) {
+            $query .= ' AND tt.component = :component';
+            $params['component'] = $component;
+        }
+        if ($subquery) {
+            $query .= ' AND ' . $subquery;
+        }
+        $query .= ' ORDER BY it.id';
+
+        return $DB->get_records_sql($query, $params, $limitfrom, $limitnum);
+    }
+
+    /**
+     * Count how many items are tagged with a specific tag.
+     *
+     * @param    string   $component component responsible for tagging. For BC it can be empty but in this case the
+     *                    query will be slow because DB index will not be used.
+     * @param    string   $itemtype  type to restrict search to
+     * @param    string   $subquery additional query to be appended to WHERE clause, refer to the itemtable as 'it'
+     * @param    array    $params additional parameters for the DB query
+     * @return   int      number of mathing tags.
+     */
+    public function count_tagged_items($component, $itemtype, $subquery = '', $params = array()) {
+        global $DB;
+
+        if (empty($itemtype) || !$DB->get_manager()->table_exists($itemtype)) {
+            return 0;
+        }
+        $params = $params ? $params : array();
+
+        $query = "SELECT COUNT(it.id)
+                    FROM {".$itemtype."} it INNER JOIN {tag_instance} tt ON it.id = tt.itemid
+                   WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid";
+        $params['itemtype'] = $itemtype;
+        $params['tagid'] = $this->id;
+        if ($component) {
+            $query .= ' AND tt.component = :component';
+            $params['component'] = $component;
+        }
+        if ($subquery) {
+            $query .= ' AND ' . $subquery;
+        }
+
+        return $DB->get_field_sql($query, $params);
+    }
+
+    /**
+     * Determine if an item is tagged with a specific tag
+     *
+     * Note that this is a static method and not a method of core_tag object because the tag might not exist yet,
+     * for example user searches for "php" and we offer him to add "php" to his interests.
+     *
+     * @param   string   $component component responsible for tagging. For BC it can be empty but in this case the
+     *                   query will be slow because DB index will not be used.
+     * @param   string   $itemtype    the record type to look for
+     * @param   int      $itemid      the record id to look for
+     * @param   string   $tagname     a tag name
+     * @return  int                   1 if it is tagged, 0 otherwise
+     */
+    public static function is_item_tagged_with($component, $itemtype, $itemid, $tagname) {
+        global $DB;
+        $tagcollid = core_tag_area::get_collection($component, $itemtype);
+        $query = 'SELECT 1 FROM {tag} t
+                    JOIN {tag_instance} ti ON ti.tagid = t.id
+                    WHERE t.name = ? AND t.tagcollid = ? AND ti.itemtype = ? AND ti.itemid = ?';
+        $cleanname = core_text::strtolower(clean_param($tagname, PARAM_TAG));
+        $params = array($cleanname, $tagcollid, $itemtype, $itemid);
+        if ($component) {
+            $query .= ' AND ti.component = ?';
+            $params[] = $component;
+        }
+        return $DB->record_exists_sql($query, $params) ? 1 : 0;
+    }
+
+    /**
+     * Returns whether the tag area is enabled
+     *
+     * @param string $component component responsible for tagging
+     * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc.
+     * @return bool|null
+     */
+    public static function is_enabled($component, $itemtype) {
+        return core_tag_area::is_enabled($component, $itemtype);
+    }
+
+    /**
+     * Retrieves contents of tag area for the tag/index.php page
+     *
+     * @param stdClass $tagarea
+     * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
+     *             are displayed on the page and the per-page limit may be bigger
+     * @param int $fromctx context id where the link was displayed, may be used by callbacks
+     *            to display items in the same context first
+     * @param int $ctx context id where to search for records
+     * @param bool $rec search in subcontexts as well
+     * @param int $page 0-based number of page being displayed
+     * @return \core_tag\output\tagindex
+     */
+    public function get_tag_index($tagarea, $exclusivemode, $fromctx, $ctx, $rec, $page = 0) {
+        global $CFG;
+        if (!empty($tagarea->callback)) {
+            if (!empty($tagarea->callbackfile)) {
+                require_once($CFG->dirroot . '/' . ltrim($tagarea->callbackfile, '/'));
+            }
+            $callback = $tagarea->callback;
+            return $callback($this, $exclusivemode, $fromctx, $ctx, $rec, $page);
+        }
+        return null;
+    }
+
+    /**
+     * Returns formatted description of the tag
+     *
+     * @param array $options
+     * @return string
+     */
+    public function get_formatted_description($options = array()) {
+        $options = empty($options) ? array() : (array)$options;
+        $options += array('para' => false, 'overflowdiv' => true);
+        $description = file_rewrite_pluginfile_urls($this->description, 'pluginfile.php',
+                context_system::instance()->id, 'tag', 'description', $this->id);
+        return format_text($description, $this->descriptionformat, $options);
+    }
+
+    /**
+     * Returns the list of tag links available for the current user (edit, flag, etc.)
+     *
+     * @return array
+     */
+    public function get_links() {
+        global $USER;
+        $links = array();
+
+        if (!isloggedin() || isguestuser()) {
+            return $links;
+        }
+
+        $tagname = $this->get_display_name();
+        $systemcontext = context_system::instance();
+
+        // Add a link for users to add/remove this from their interests.
+        if (static::is_enabled('core', 'user') && core_tag_area::get_collection('core', 'user') == $this->tagcollid) {
+            if (static::is_item_tagged_with('core', 'user', $USER->id, $this->name)) {
+                $url = new moodle_url('/tag/user.php', array('action' => 'removeinterest',
+                    'sesskey' => sesskey(), 'tag' => $this->rawname));
+                $links[] = html_writer::link($url, get_string('removetagfrommyinterests', 'tag', $tagname),
+                        array('class' => 'removefrommyinterests'));
+            } else {
+                $url = new moodle_url('/tag/user.php', array('action' => 'addinterest',
+                    'sesskey' => sesskey(), 'tag' => $this->rawname));
+                $links[] = html_writer::link($url, get_string('addtagtomyinterests', 'tag', $tagname),
+                        array('class' => 'addtomyinterests'));
+            }
+        }
+
+        // Flag as inappropriate link.  Only people with moodle/tag:flag capability.
+        if (has_capability('moodle/tag:flag', $systemcontext)) {
+            $url = new moodle_url('/tag/user.php', array('action' => 'flaginappropriate',
+                'sesskey' => sesskey(), 'id' => $this->id));
+            $links[] = html_writer::link($url, get_string('flagasinappropriate', 'tag', $tagname),
+                        array('class' => 'flagasinappropriate'));
+        }
+
+        // Edit tag: Only people with moodle/tag:edit capability who either have it as an interest or can manage tags.
+        if (has_capability('moodle/tag:edit', $systemcontext) ||
+                has_capability('moodle/tag:manage', $systemcontext)) {
+            $url = new moodle_url('/tag/edit.php', array('id' => $this->id));
+            $links[] = html_writer::link($url, get_string('edittag', 'tag'),
+                        array('class' => 'edittag'));
+        }
+
+        return $links;
+    }
+
+    /**
+     * Delete one or more tag, and all their instances if there are any left.
+     *
+     * @param    int|array    $tagids one tagid (int), or one array of tagids to delete
+     * @return   bool     true on success, false otherwise
+     */
+    public static function delete_tags($tagids) {
+        global $DB;
+
+        if (!is_array($tagids)) {
+            $tagids = array($tagids);
+        }
+        if (empty($tagids)) {
+            return;
+        }
+
+        // Use the tagids to create a select statement to be used later.
+        list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids);
+
+        // Store the tags and tag instances we are going to delete.
+        $tags = $DB->get_records_select('tag', 'id ' . $tagsql, $tagparams);
+        $taginstances = $DB->get_records_select('tag_instance', 'tagid ' . $tagsql, $tagparams);
+
+        // Delete all the tag instances.
+        $select = 'WHERE tagid ' . $tagsql;
+        $sql = "DELETE FROM {tag_instance} $select";
+        $DB->execute($sql, $tagparams);
+
+        // Delete all the tag correlations.
+        $sql = "DELETE FROM {tag_correlation} $select";
+        $DB->execute($sql, $tagparams);
+
+        // Delete all the tags.
+        $select = 'WHERE id ' . $tagsql;
+        $sql = "DELETE FROM {tag} $select";
+        $DB->execute($sql, $tagparams);
+
+        // Fire an event that these items were untagged.
+        if ($taginstances) {
+            // Save the system context in case the 'contextid' column in the 'tag_instance' table is null.
+            $syscontextid = context_system::instance()->id;
+            // Loop through the tag instances and fire a 'tag_removed'' event.
+            foreach ($taginstances as $taginstance) {
+                // We can not fire an event with 'null' as the contextid.
+                if (is_null($taginstance->contextid)) {
+                    $taginstance->contextid = $syscontextid;
+                }
+
+                // Trigger tag removed event.
+                \core\event\tag_removed::create_from_tag_instance($taginstance,
+                    $tags[$taginstance->tagid]->name, $tags[$taginstance->tagid]->rawname,
+                    true)->trigger();
+            }
+        }
+
+        // Fire an event that these tags were deleted.
+        if ($tags) {
+            $context = context_system::instance();
+            foreach ($tags as $tag) {
+                // Delete all files associated with this tag.
+                $fs = get_file_storage();
+                $files = $fs->get_area_files($context->id, 'tag', 'description', $tag->id);
+                foreach ($files as $file) {
+                    $file->delete();
+                }
+
+                // Trigger an event for deleting this tag.
+                $event = \core\event\tag_deleted::create(array(
+                    'objectid' => $tag->id,
+                    'relateduserid' => $tag->userid,
+                    'context' => $context,
+                    'other' => array(
+                        'name' => $tag->name,
+                        'rawname' => $tag->rawname
+                    )
+                ));
+                $event->add_record_snapshot('tag', $tag);
+                $event->trigger();
+            }
+        }
+
+        return true;
+    }
+}
index 16bf9e7..a308757 100644 (file)
@@ -26,8 +26,8 @@ require_once('../config.php');
 require_once('lib.php');
 require_once('edit_form.php');
 
-$tag_id = optional_param('id', 0, PARAM_INT);
-$tag_name = optional_param('tag', '', PARAM_TAG);
+$tagid = optional_param('id', 0, PARAM_INT);
+$tagname = optional_param('tag', '', PARAM_TAG);
 $returnurl = optional_param('returnurl', '', PARAM_LOCALURL);
 
 require_login();
@@ -40,34 +40,50 @@ if (empty($CFG->usetags)) {
 $systemcontext   = context_system::instance();
 require_capability('moodle/tag:edit', $systemcontext);
 
-if ($tag_name) {
-    $tag = tag_get('name', $tag_name, '*');
-} else if ($tag_id) {
-    $tag = tag_get('id', $tag_id, '*');
+if ($tagname) {
+    $tagcollid = optional_param('tc', 0, PARAM_INT);
+    if (!$tagcollid) {
+        // Tag name specified but tag collection was not. Try to guess it.
+        $tags = core_tag_tag::guess_by_name($tagname, '*');
+        if (count($tags) > 1) {
+            // This tag was found in more than one collection, redirect to search.
+            redirect(new moodle_url('/tag/search.php', array('tag' => $tagname)));
+        } else if (count($tags) == 1) {
+            $tag = reset($tags);
+        }
+    } else {
+        if (!$tag = core_tag_tag::get_by_name($tagcollid, $tagname, '*')) {
+            redirect(new moodle_url('/tag/search.php', array('tagcollid' => $tagcollid)));
+        }
+    }
+} else if ($tagid) {
+    $tag = core_tag_tag::get($tagid, '*');
 }
 
 if (empty($tag)) {
-    redirect($CFG->wwwroot.'/tag/search.php');
+    redirect(new moodle_url('/tag/search.php'));
 }
 
-$PAGE->set_url('/tag/index.php', array('id' => $tag->id));
+$PAGE->set_url($tag->get_view_url());
 $PAGE->set_subpage($tag->id);
 $PAGE->set_context($systemcontext);
 $PAGE->set_blocks_editing_capability('moodle/tag:editblocks');
 $PAGE->set_pagelayout('standard');
 
-$tagname = tag_display_name($tag);
+$tagname = $tag->get_display_name();
+$tagcollid = $tag->tagcollid;
 
 // set the relatedtags field of the $tag object that will be passed to the form
-$tag->relatedtags = tag_get_tags_array('tag', $tag->id);
+$data = $tag->to_object();
+$data->relatedtags = core_tag_tag::get_item_tags_array('core', 'tag', $tag->id);
 
 $options = new stdClass();
 $options->smiley = false;
 $options->filter = false;
 
 // convert and remove any XSS
-$tag->description       = format_text($tag->description, $tag->descriptionformat, $options);
-$tag->descriptionformat = FORMAT_HTML;
+$data->description       = format_text($tag->description, $tag->descriptionformat, $options);
+$data->descriptionformat = FORMAT_HTML;
 
 $errorstring = '';
 
@@ -78,76 +94,37 @@ $editoroptions = array(
     'context'   => $systemcontext,
     'subdirs'   => file_area_contains_subdirs($systemcontext, 'tag', 'description', $tag->id),
 );
-$tag = file_prepare_standard_editor($tag, 'description', $editoroptions, $systemcontext, 'tag', 'description', $tag->id);
+$data = file_prepare_standard_editor($data, 'description', $editoroptions, $systemcontext, 'tag', 'description', $data->id);
 
-$tagform = new tag_edit_form(null, compact('editoroptions'));
-if ( $tag->tagtype == 'official' ) {
-    $tag->tagtype = '1';
-} else {
-    $tag->tagtype = '0';
-}
+$tagform = new tag_edit_form(null, array('editoroptions' => $editoroptions, 'tag' => $tag));
+$data->tagtype = ($data->tagtype === 'official') ? '1' : '0';
+$data->returnurl = $returnurl;
 
-$tag->returnurl = $returnurl;
-$tagform->set_data($tag);
+$tagform->set_data($data);
 
-// If new data has been sent, update the tag record
 if ($tagform->is_cancelled()) {
-    redirect($returnurl ? new moodle_url($returnurl) :
-        new moodle_url('/tag/index.php', array('tag' => $tag->name)));
+    redirect($returnurl ? new moodle_url($returnurl) : $tag->get_view_url());
 } else if ($tagnew = $tagform->get_data()) {
+    // If new data has been sent, update the tag record.
+    $updatedata = array();
 
     if (has_capability('moodle/tag:manage', $systemcontext)) {
-        if (($tag->tagtype != 'default') && (!isset($tagnew->tagtype) || ($tagnew->tagtype != '1'))) {
-            tag_type_set($tag->id, 'default');
-
-        } elseif (($tag->tagtype != 'official') && ($tagnew->tagtype == '1')) {
-            tag_type_set($tag->id, 'official');
-        }
+        $updatedata['tagtype'] = empty($tagnew->tagtype) ? 'default' : 'official';
+        $updatedata['rawname'] = $tagnew->rawname;
     }
 
-    if (!has_capability('moodle/tag:manage', $systemcontext)) {
-        unset($tagnew->name);
-        unset($tagnew->rawname);
-
-    } else {  // They might be trying to change the rawname, make sure it's a change that doesn't affect name
-        $norm = tag_normalize($tagnew->rawname, TAG_CASE_LOWER);
-        $tagnew->name = array_shift($norm);
+    $tagnew = file_postupdate_standard_editor($tagnew, 'description', $editoroptions,
+            $systemcontext, 'tag', 'description', $tag->id);
+    $updatedata['description'] = $tagnew->description;
+    $updatedata['descriptionformat'] = $tagnew->descriptionformat;
 
-        if ($tag->rawname !== $tagnew->rawname) {  // The name has changed, let's make sure it's not another existing tag
-            if (($id = tag_get_id($tagnew->name)) && $id != $tag->id) { // Something exists already, so flag an error.
-                $errorstring = s($tagnew->rawname).': '.get_string('namesalreadybeeingused', 'tag');
-            }
-        }
-    }
+    // Update name, description and official type.
+    $tag->update($updatedata);
 
-    if (empty($errorstring)) {    // All is OK, let's save it
+    // Updated related tags.
+    $tag->set_related_tags($tagnew->relatedtags);
 
-        $tagnew = file_postupdate_standard_editor($tagnew, 'description', $editoroptions, $systemcontext, 'tag', 'description', $tag->id);
-
-        if ($tag->description != $tagnew->description) {
-            tag_description_set($tag_id, $tagnew->description, $tagnew->descriptionformat);
-        }
-
-        $tagnew->timemodified = time();
-
-        if (has_capability('moodle/tag:manage', $systemcontext)) {
-            // Check if we need to rename the tag.
-            if (isset($tagnew->name) && ($tag->rawname != $tagnew->rawname)) {
-                // Rename the tag.
-                if (!tag_rename($tag->id, $tagnew->rawname)) {
-                    print_error('errorupdatingrecord', 'tag');
-                }
-            }
-        }
-
-        //updated related tags
-        tag_set('tag', $tagnew->id, $tagnew->relatedtags, 'core', $systemcontext->id);
-        //print_object($tagnew); die();
-
-        $tagname = isset($tagnew->rawname) ? $tagnew->rawname : $tag->rawname;
-        redirect($returnurl ? new moodle_url($returnurl) :
-            new moodle_url('/tag/index.php', array('tag' => $tagname)));
-    }
+    redirect($returnurl ? new moodle_url($returnurl) : $tag->get_view_url());
 }
 
 navigation_node::override_active_url(new moodle_url('/tag/search.php'));
index c2fcdc3..ef2fc8a 100644 (file)
@@ -68,10 +68,36 @@ class tag_edit_form extends moodleform {
            $mform->addElement('checkbox', 'tagtype', get_string('officialtag', 'tag'));
         }
 
-        $mform->addElement('tags', 'relatedtags', get_string('relatedtags','tag'));
+        $mform->addElement('tags', 'relatedtags', get_string('relatedtags', 'tag'),
+                array('tagcollid' => $this->_customdata['tag']->tagcollid));
 
         $this->add_action_buttons(true, get_string('updatetag', 'tag'));
 
     }
 
+    /**
+     * Custom form validation
+     *
+     * @param array $data
+     * @param array $files
+     * @return array
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+
+        if (isset($data['rawname'])) {
+            $newname = core_text::strtolower($data['rawname']);
+            $tag = $this->_customdata['tag'];
+            if ($tag->name != $newname) {
+                // The name has changed, let's make sure it's not another existing tag.
+                if (core_tag_tag::get_by_name($tag->tagcollid, $newname)) {
+                    // Something exists already, so flag an error.
+                    $errors['rawname'] = get_string('namesalreadybeeingused', 'tag');
+                }
+            }
+        }
+
+        return $errors;
+    }
+
 }
index 8934d87..b037316 100644 (file)
  */
 
 require_once('../config.php');
-require_once('lib.php');
-require_once('locallib.php');
-require_once($CFG->dirroot.'/lib/weblib.php');
-require_once($CFG->dirroot.'/blog/lib.php');
+require_once($CFG->dirroot . '/lib/weblib.php');
+require_once($CFG->dirroot . '/blog/lib.php');
 
 require_login();
 
@@ -36,166 +34,93 @@ if (empty($CFG->usetags)) {
 
 $tagid       = optional_param('id', 0, PARAM_INT); // tag id
 $tagname     = optional_param('tag', '', PARAM_TAG); // tag
+$tagareaid   = optional_param('ta', 0, PARAM_INT); // Tag area id.
+$exclusivemode = optional_param('excl', 0, PARAM_BOOL); // Exclusive mode (show entities in one tag area only).
+$page        = optional_param('page', 0, PARAM_INT); // Page to display.
+$fromctx     = optional_param('from', null, PARAM_INT);
+$ctx         = optional_param('ctx', null, PARAM_INT);
+$rec         = optional_param('rec', 1, PARAM_INT);
 
 $edit        = optional_param('edit', -1, PARAM_BOOL);
-$userpage    = optional_param('userpage', 0, PARAM_INT); // which page to show
-$perpage     = optional_param('perpage', 24, PARAM_INT);
 
 $systemcontext   = context_system::instance();
 
 if ($tagname) {
-    $tag = tag_get('name', $tagname, '*');
+    $tagcollid = optional_param('tc', 0, PARAM_INT);
+    if (!$tagcollid) {
+        // Tag name specified but tag collection was not. Try to guess it.
+        $tags = core_tag_tag::guess_by_name($tagname, '*');
+        if (count($tags) > 1) {
+            // This tag was found in more than one collection, redirect to search.
+            redirect(new moodle_url('/tag/search.php', array('query' => $tagname)));
+        } else if (count($tags) == 1) {
+            $tag = reset($tags);
+        }
+    } else {
+        if (!$tag = core_tag_tag::get_by_name($tagcollid, $tagname, '*')) {
+            redirect(new moodle_url('/tag/search.php', array('tc' => $tagcollid, 'query' => $tagname)));
+        }
+    }
 } else if ($tagid) {
-    $tag = tag_get('id', $tagid, '*');
+    $tag = core_tag_tag::get($tagid, '*');
 }
 unset($tagid);
 if (empty($tag)) {
-    redirect($CFG->wwwroot.'/tag/search.php');
+    redirect(new moodle_url('/tag/search.php'));
+}
+
+if ($ctx && ($context = context::instance_by_id($ctx, IGNORE_MISSING)) && $context->contextlevel >= CONTEXT_COURSE) {
+    list($context, $course, $cm) = get_context_info_array($context->id);
+    require_login($course, false, $cm, false, true);
+} else {
+    $PAGE->set_context($systemcontext);
 }
 
-$PAGE->set_url('/tag/index.php', array('id' => $tag->id));
+$tagcollid = $tag->tagcollid;
+
+$PAGE->set_url($tag->get_view_url($exclusivemode, $fromctx, $ctx, $rec));
 $PAGE->set_subpage($tag->id);
-$PAGE->set_context($systemcontext);
 $tagnode = $PAGE->navigation->find('tags', null);
 $tagnode->make_active();
 $PAGE->set_pagelayout('standard');
 $PAGE->set_blocks_editing_capability('moodle/tag:editblocks');
 
-if (($edit != -1) and $PAGE->user_allowed_editing()) {
-    $USER->editing = $edit;
+$buttons = '';
+if (has_capability('moodle/tag:manage', context_system::instance())) {
+    $buttons .= $OUTPUT->single_button(new moodle_url('/tag/manage.php'),
+            get_string('managetags', 'tag'), 'GET');
 }
-
-$tagname = tag_display_name($tag);
-$title = get_string('tag', 'tag') .' - '. $tagname;
-
-$button = '';
-if ($PAGE->user_allowed_editing() ) {
-    $button = $OUTPUT->edit_button(new moodle_url("$CFG->wwwroot/tag/index.php", array('id' => $tag->id)));
+if ($PAGE->user_allowed_editing()) {
+    if ($edit != -1) {
+        $USER->editing = $edit;
+    }
+    $buttons .= $OUTPUT->edit_button(clone($PAGE->url));
 }
 
 $PAGE->navbar->add($tagname);
-$PAGE->set_title($title);
+$PAGE->set_title(get_string('tag', 'tag') .' - '. $tag->get_display_name());
 $PAGE->set_heading($COURSE->fullname);
-$PAGE->set_button($button);
-$courserenderer = $PAGE->get_renderer('core', 'course');
-echo $OUTPUT->header();
+$PAGE->set_button($buttons);
 
-// Manage all tags links
-if (has_capability('moodle/tag:manage', $systemcontext)) {
-    echo '<div class="managelink"><a href="'. $CFG->wwwroot .'/tag/manage.php">'. get_string('managetags', 'tag') .'</a></div>' ;
+// Find all areas in this collection and their items tagged with this tag.
+$tagareas = core_tag_collection::get_areas($tagcollid);
+if ($tagareaid) {
+    $tagareas = array_intersect_key($tagareas, array($tagareaid => 1));
 }
-
-$tagname  = tag_display_name($tag);
-
-if ($tag->flag > 0 && has_capability('moodle/tag:manage', $systemcontext)) {
-    $tagname =  '<span class="flagged-tag">' . $tagname . '</span>';
-}
-
-echo $OUTPUT->heading($tagname, 2);
-tag_print_management_box($tag);
-tag_print_description_box($tag);
-// Check what type of results are avaialable
-$courses = $courserenderer->tagged_courses($tag->id);
-
-if (!empty($CFG->enableblogs) && has_capability('moodle/blog:view', $systemcontext)) {
-    require_once($CFG->dirroot.'/blog/lib.php');
-    require_once($CFG->dirroot.'/blog/locallib.php');
-
-    $bloglisting = new blog_listing(array('tag' => $tag->id));
-    $limit = 10;
-    $start = 0;
-    $blogs = $bloglisting->get_entries($start, $limit);
-}
-$usercount = tag_record_count('user', $tag->id);
-
-// Only include <a href />'s to those anchors that actually will be shown
-$relatedpageslink = "";
-$countanchors = 0;
-if (!empty($courses)) {
-    $relatedpageslink = '<a href="#course">'.get_string('courses').'</a>';
-    $countanchors++;
+if (!$tagareaid && count($tagareas) == 1) {
+    // Automatically set "exclusive" mode for tag collection with one tag area only.
+    $exclusivemode = 1;
 }
-if (!empty($blogs)) {
-    if ($countanchors > 0) {
-        $relatedpageslink .= ' | ';
-    }
-    $relatedpageslink .= '<a href="#blog">'.get_string('relatedblogs', 'tag').'</a>';
-    $countanchors++;
-}
-if ($usercount > 0) {
-    if ($countanchors > 0) {
-        $relatedpageslink .= ' | ';
-    }
-    $relatedpageslink .= '<a href="#user">'.get_string('users').'</a>';
-    $countanchors++;
+$entities = array();
+foreach ($tagareas as $ta) {
+    $entities[] = $tag->get_tag_index($ta, $exclusivemode, $fromctx, $ctx, $rec, $page);
 }
-// If only one anchor is present, no <a href /> is needed
-if ($countanchors == 0) {
-    echo '<div class="relatedpages"><p>'.get_string('noresultsfor', 'tag', $tagname).'</p></div>';
-} elseif ($countanchors > 1) {
-    echo '<div class="relatedpages"><p>'.$relatedpageslink.'</p></div>';
-}
-
-// Display courses tagged with the tag
-if (!empty($courses)) {
-
-    echo $OUTPUT->box_start('generalbox', 'tag-blogs'); //could use an id separate from tag-blogs, but would have to copy the css style to make it look the same
-
-    echo "<a name='course'></a>";
-    echo $courses;
-
-    echo $OUTPUT->box_end();
-}
-
-// Print up to 10 previous blogs entries
-
-if (!empty($blogs)) {
-    echo $OUTPUT->box_start('generalbox', 'tag-blogs');
-    $heading = get_string('relatedblogs', 'tag', $tagname). ' ' . get_string('taggedwith', 'tag', $tagname);
-    echo "<a name='blog'></a>";
-    echo $OUTPUT->heading($heading, 3);
+$entities = array_filter($entities);
 
-    echo '<ul id="tagblogentries">';
-    foreach ($blogs as $blog) {
-        if ($blog->publishstate == 'draft') {
-            $class = 'class="dimmed"';
-        } else {
-            $class = '';
-        }
-        echo '<li '.$class.'>';
-        echo '<a '.$class.' href="'.$CFG->wwwroot.'/blog/index.php?entryid='.$blog->id.'">';
-        echo format_string($blog->subject);
-        echo '</a>';
-        echo ' - ';
-        echo '<a '.$class.' href="'.$CFG->wwwroot.'/user/view.php?id='.$blog->userid.'">';
-        echo fullname($blog);
-        echo '</a>';
-        echo ', '. userdate($blog->lastmodified);
-        echo '</li>';
-    }
-    echo '</ul>';
-
-    $allblogsurl = new moodle_url('/blog/index.php', array('tagid' => $tag->id));
-    echo '<p class="moreblogs"><a href="'.$allblogsurl->out().'">'.get_string('seeallblogs', 'tag', $tagname).'</a></p>';
-
-    echo $OUTPUT->box_end();
-}
-
-if ($usercount > 0) {
-
-    //user table box
-    echo $OUTPUT->box_start('generalbox', 'tag-user-table');
-
-    $heading = get_string('users'). ' ' . get_string('taggedwith', 'tag', $tagname) . ': ' . $usercount;
-    echo "<a name='user'></a>";
-    echo $OUTPUT->heading($heading, 3);
-
-    $baseurl = new moodle_url('/tag/index.php', array('id' => $tag->id));
-    $pagingbar = new paging_bar($usercount, $userpage, $perpage, $baseurl);
-    $pagingbar->pagevar = 'userpage';
-    echo $OUTPUT->render($pagingbar);
-    tag_print_tagged_users_table($tag, $userpage * $perpage, $perpage);
-    echo $OUTPUT->box_end();
-}
+$tagrenderer = $PAGE->get_renderer('core', 'tag');
+$pagecontents = $tagrenderer->tag_index_page($tag, array_filter($entities), $tagareaid,
+        $exclusivemode, $fromctx, $ctx, $rec, $page);
 
+echo $OUTPUT->header();
+echo $pagecontents;
 echo $OUTPUT->footer();
index 15dc38e..c399650 100644 (file)
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
-
 /**
- * Moodle tag library
- *
- * Tag strings : you can use any character in tags, except the comma (which is the separator) and
- * the '\' (backslash).  Note that many spaces (or other blank characters) will get "compressed"
- * into one. A tag string is always a rawurlencode'd string. This is the same behavior as
- * http://del.icio.us.
- *
- * A "record" is a php array (note that an object will work too) that contains the following
- * variables :
- *  - type: The database table containing the record that we are tagging (eg: for a blog, this is
- *          the table named 'post', and for a user it is the table name 'user')
- *  - id:   The id of the record
+ * Functions for component core_tag
  *
- * BASIC INSTRUCTIONS :
- *  - to "tag a blog post" (for example):
- *        tag_set('post', $blog_post->id, $array_of_tags, 'core', $thecontext);
- *
- *  - to "remove all the tags on a blog post":
- *        tag_set('post', $blog_post->id, array(), 'core', $thecontext);
- *
- * Tag set will create tags that need to be created.
+ * To set or get item tags refer to the class {@link core_tag_tag}
  *
  * @package    core_tag
- * @category   tag
- * @todo       MDL-31090 turn this into a full-fledged categorization system. This could start by
- *             modifying (removing, probably) the 'tag type' to use another table describing the
- *             relationship between tags (parents, sibling, etc.), which could then be merged with
- *             the 'course categorization' system.
- * @see        http://www.php.net/manual/en/function.urlencode.php
  * @copyright  2007 Luiz Cruz <luiz.laydner@gmail.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-/**
- * Used to require that the return value from a function is an array.
- * @see tag_set()
- */
-define('TAG_RETURN_ARRAY', 0);
-/**
- * Used to require that the return value from a function is an object.
- * @see tag_set()
- */
-define('TAG_RETURN_OBJECT', 1);
-/**
- * Use to specify that HTML free text is expected to be returned from a function.
- * @see tag_display_name()
- */
-define('TAG_RETURN_TEXT', 2);
-/**
- * Use to specify that encoded HTML is expected to be returned from a function.
- * @see tag_display_name()
- */
-define('TAG_RETURN_HTML', 3);
-
-/**
- * Used to specify that we wish a lowercased string to be returned
- * @see tag_normal()
- */
-define('TAG_CASE_LOWER', 0);
-/**
- * Used to specify that we do not wish the case of the returned string to change
- * @see tag_normal()
- */
-define('TAG_CASE_ORIGINAL', 1);
-
-/**
- * Used to specify that we want all related tags returned, no matter how they are related.
- * @see tag_get_related_tags()
- */
-define('TAG_RELATED_ALL', 0);
-/**
- * Used to specify that we only want back tags that were manually related.
- * @see tag_get_related_tags()
- */
-define('TAG_RELATED_MANUAL', 1);
-/**
- * Used to specify that we only want back tags where the relationship was automatically correlated.
- * @see tag_get_related_tags()
- */
-define('TAG_RELATED_CORRELATED', 2);
-
-///////////////////////////////////////////////////////
-/////////////////// PUBLIC TAG API ////////////////////
-
-/// Functions for settings tags  //////////////////////
-
-/**
- * Set the tags assigned to a record.  This overwrites the current tags.
- *
- * This function is meant to be fed the string coming up from the user interface, which contains all tags assigned to a record.
- *
- * @package core_tag
- * @category tag
- * @access public
- * @param string $record_type the type of record to tag ('post' for blogs, 'user' for users, 'tag' for tags, etc.)
- * @param int $record_id the id of the record to tag
- * @param array $tags the array of tags to set on the record. If given an empty array, all tags will be removed.
- * @param string|null $component the component that was tagged
- * @param int|null $contextid the context id of where this tag was assigned
- * @return bool|null
- */
-function tag_set($record_type, $record_id, $tags, $component = null, $contextid = null) {
-
-    static $in_recursion_semaphore = false; // this is to prevent loops when tagging a tag
-
-    if ( $record_type == 'tag' && !$in_recursion_semaphore) {
-        $current_tagged_tag_name = tag_get_name($record_id);
-    }
-
-    $tags_ids = tag_get_id($tags, TAG_RETURN_ARRAY); // force an array, even if we only have one tag.
-    $cleaned_tags = tag_normalize($tags);
-    //echo 'tags-in-tag_set'; var_dump($tags); var_dump($tags_ids); var_dump($cleaned_tags);
-
-    $current_ids = tag_get_tags_ids($record_type, $record_id);
-    //var_dump($current_ids);
-
-    // for data coherence reasons, it's better to remove deleted tags
-    // before adding new data: ordering could be duplicated.
-    foreach($current_ids as $current_id) {
-        if (!in_array($current_id, $tags_ids)) {
-            tag_delete_instance($record_type, $record_id, $current_id);
-            if ( $record_type == 'tag' && !$in_recursion_semaphore) {
-                // if we are removing a tag-on-a-tag (manually related tag),
-                // we need to remove the opposite relationship as well.
-                tag_delete_instance('tag', $current_id, $record_id);
-            }
-        }
-    }
-
-    if (empty($tags)) {
-        return true;
-    }
-
-    foreach($tags as $ordering => $tag) {
-        $tag = trim($tag);
-        if (!$tag) {
-            continue;
-        }
-
-        $clean_tag = $cleaned_tags[$tag];
-        $tag_current_id = $tags_ids[$clean_tag];
-
-        if ( is_null($tag_current_id) ) {
-            // create new tags
-            //echo "call to add tag $tag\n";
-            $new_tag = tag_add($tag);
-            $tag_current_id = $new_tag[$clean_tag];
-        }
-
-        tag_assign($record_type, $record_id, $tag_current_id, $ordering, 0, $component, $contextid);
-
-        // if we are tagging a tag (adding a manually-assigned related tag), we
-        // need to create the opposite relationship as well.
-        if ( $record_type == 'tag' && !$in_recursion_semaphore) {
-            $in_recursion_semaphore = true;
-            tag_set_add('tag', $tag_current_id, $current_tagged_tag_name, $component, $contextid);
-            $in_recursion_semaphore = false;
-        }
-    }
-}
-
-/**
- * Adds a tag to a record, without overwriting the current tags.
- *
- * @package core_tag
- * @category tag
- * @access public
- * @param string $record_type the type of record to tag ('post' for blogs, 'user' for users, etc.)
- * @param int $record_id the id of the record to tag
- * @param string $tag the tag to add
- * @param string|null $component the component that was tagged
- * @param int|null $contextid the context id of where this tag was assigned
- * @return bool|null
- */
-function tag_set_add($record_type, $record_id, $tag, $component = null, $contextid = null) {
-
-    $new_tags = array();
-    foreach( tag_get_tags($record_type, $record_id) as $current_tag ) {
-        $new_tags[] = $current_tag->rawname;
-    }
-    $new_tags[] = $tag;
-
-    return tag_set($record_type, $record_id, $new_tags, $component, $contextid);
-}
-
-/**
- * Removes a tag from a record, without overwriting other current tags.
- *
- * @package core_tag
- * @category tag
- * @access public
- * @param string $record_type the type of record to tag ('post' for blogs, 'user' for users, etc.)
- * @param int $record_id the id of the record to tag
- * @param string $tag the tag to delete
- * @param string|null $component the component that was tagged
- * @param int|null $contextid the context id of where this tag was assigned
- * @return bool|null
- */
-function tag_set_delete($record_type, $record_id, $tag, $component = null, $contextid = null) {
-
-    $new_tags = array();
-    foreach( tag_get_tags($record_type, $record_id) as $current_tag ) {
-        if ($current_tag->name != $tag) {  // Keep all tags but the one specified
-            $new_tags[] = $current_tag->name;
-        }
-    }
-
-    return tag_set($record_type, $record_id, $new_tags, $component, $contextid);
-}
-
-/**
- * Set the type of a tag.  At this time (version 2.2) the possible values are 'default' or 'official'.  Official tags will be
- * displayed separately "at tagging time" (while selecting the tags to apply to a record).
- *
- * @package  core_tag
- * @category tag
- * @access   public
- * @param    string   $tagid tagid to modify
- * @param    string   $type either 'default' or 'official'
- * @return   bool     true on success, false otherwise
- */
-function tag_type_set($tagid, $type) {
-    global $DB;
-
-    if ($tag = $DB->get_record('tag', array('id' => $tagid), 'id, userid, name, rawname')) {
-        $tag->tagtype = $type;
-        $tag->timemodified = time();
-        $DB->update_record('tag', $tag);
-
-        $event = \core\event\tag_updated::create(array(
-            'objectid' => $tag->id,
-            'relateduserid' => $tag->userid,
-            'context' => context_system::instance(),
-            'other' => array(
-                'name' => $tag->name,
-                'rawname' => $tag->rawname
-            )
-        ));
-        $event->trigger();
-
-        return true;
-    }
-    return false;
-}
-
-/**
- * Set the description of a tag
- *
- * @package  core_tag
- * @category tag
- * @access   public
- * @param    int      $tagid the id of the tag
- * @param    string   $description the tag's description string to be set
- * @param    int      $descriptionformat the moodle text format of the description
- *                    {@link http://docs.moodle.org/dev/Text_formats_2.0#Database_structure}
- * @return   bool     true on success, false otherwise
- */
-function tag_description_set($tagid, $description, $descriptionformat) {
-    global $DB;
-
-    if ($tag = $DB->get_record('tag', array('id' => $tagid), 'id, userid, name, rawname')) {
-        $tag->description = $description;
-        $tag->descriptionformat = $descriptionformat;
-        $tag->timemodified = time();
-        $DB->update_record('tag', $tag);
-
-        $event = \core\event\tag_updated::create(array(
-            'objectid' => $tag->id,
-            'relateduserid' => $tag->userid,
-            'context' => context_system::instance(),
-            'other' => array(
-                'name' => $tag->name,
-                'rawname' => $tag->rawname
-            )
-        ));
-        $event->trigger();
-
-        return true;
-    }
-
-    return false;
-}
-
-
-
-
-
-
-/// Functions for getting information about tags //////
-
-/**
- * Simple function to just return a single tag object when you know the name or something
- *
- * @package  core_tag
- * @category tag
- * @access   public
- * @param    string $field        which field do we use to identify the tag: id, name or rawname
- * @param    string $value        the required value of the aforementioned field
- * @param    string $returnfields which fields do we want returned. This is a comma seperated string containing any combination of
- *                                'id', 'name', 'rawname' or '*' to include all fields.
- * @return   mixed  tag object
- */
-function tag_get($field, $value, $returnfields='id, name, rawname') {
-    global $DB;
-
-    if ($field == 'name') {
-        $value = core_text::strtolower($value);   // To cope with input that might just be wrong case
-    }
-    return $DB->get_record('tag', array($field=>$value), $returnfields);
-}
-
-
-/**
- * Get the array of db record of tags associated to a record (instances).  Use {@see tag_get_tags_csv()} if you wish to get the same
- * data in a comma-separated string, for instances such as needing to simply display a list of tags to the end user. This should
- * really be called tag_get_tag_instances().
- *
- * @package core_tag
- * @category tag
- * @access public
- * @param string $record_type the record type for which we want to get the tags
- * @param int $record_id the record id for which we want to get the tags
- * @param string $type the tag type (either 'default' or 'official'). By default, all tags are returned.
- * @param int $userid (optional) only required for course tagging
- * @return array the array of tags
- */
-function tag_get_tags($record_type, $record_id, $type=null, $userid=0) {
-    global $CFG, $DB;
-
-    $params = array();
-
-    if ($type) {
-        $sql_type = "AND tg.tagtype = :type";
-        $params['type'] = $type;
-    } else {
-        $sql_type = '';
-    }
-
-   $u = null;
-    if ($userid) {
-        $u =  "AND ti.tiuserid = :userid ";
-        $params['userid'] = $userid;
-    }
-
-    $sql = "SELECT ti.id AS taginstanceid, tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering
-              FROM {tag_instance} ti
-              JOIN {tag} tg ON tg.id = ti.tagid
-              WHERE ti.itemtype = :recordtype AND ti.itemid = :recordid $u $sql_type
-           ORDER BY ti.ordering ASC";
-    $params['recordtype'] = $record_type;
-    $params['recordid']   = $record_id;
-
-    // if the fields in this query are changed, you need to do the same changes in tag_get_correlated_tags
-    return $DB->get_records_sql($sql, $params);
-    // This version of the query, reversing the ON clause, "correctly" returns
-    // a row with NULL values for instances that are still in the DB even though
-    // the tag has been deleted.  This shouldn't happen, but if it did, using
-    // this query could help "clean it up".  This causes bugs at this time.
-    //$tags = $DB->get_records_sql("SELECT ti.tagid, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering ".
-    //    "FROM {tag_instance} ti LEFT JOIN {tag} tg ON ti.tagid = tg.id ".
-    //    "WHERE ti.itemtype = '{$record_type}' AND ti.itemid = '{$record_id}' {$type} ".
-    //    "ORDER BY ti.ordering ASC");
-}
-
-/**
- * Get the array of tags display names, indexed by id.
- *
- * @package  core_tag
- * @category tag
- * @access   public
- * @param    string $record_type the record type for which we want to get the tags
- * @param    int    $record_id   the record id for which we want to get the tags
- * @param    string $type        the tag type (either 'default' or 'official'). By default, all tags are returned.
- * @return   array  the array of tags (with the value returned by tag_display_name), indexed by id
- */
-function tag_get_tags_array($record_type, $record_id, $type=null) {
-    $tags = array();
-    foreach(tag_get_tags($record_type, $record_id, $type) as $tag) {
-        $tags[$tag->id] = tag_display_name($tag);
-    }
-    return $tags;
-}
-
-/**
- * Get a comma-separated string of tags associated to a record.  Use {@see tag_get_tags()} to get the same information in an array.
- *
- * @package  core_tag
- * @category tag
- * @access   public
- * @param    string   $record_type the record type for which we want to get the tags
- * @param    int      $record_id   the record id for which we want to get the tags
- * @param    int      $html        either TAG_RETURN_HTML or TAG_RETURN_TEXT, depending on the type of output desired
- * @param    string   $type        either 'official' or 'default', if null, all tags are returned
- * @return   string   the comma-separated list of tags.
- */
-function tag_get_tags_csv($record_type, $record_id, $html=TAG_RETURN_HTML, $type=null) {
-    global $CFG;
-
-    $tags_names = array();
-    foreach(tag_get_tags($record_type, $record_id, $type) as $tag) {
-        if ($html == TAG_RETURN_TEXT) {
-            $tags_names[] = tag_display_name($tag, TAG_RETURN_TEXT);
-        } else { // TAG_RETURN_HTML
-            $tags_names[] = '<a href="'. $CFG->wwwroot .'/tag/index.php?tag='. rawurlencode($tag->name) .'">'. tag_display_name($tag) .'</a>';
-        }
-    }
-    return implode(', ', $tags_names);
-}
-
-/**
- * Get an array of tag ids associated to a record.
- *
- * @package  core_tag
- * @category tag
- * @access   public
- * @todo     MDL-31150 Update ordering property
- * @param    string    $record_type the record type for which we want to get the tags
- * @param    int       $record_id the record id for which we want to get the tags
- * @return   array     tag ids, indexed and sorted by 'ordering'
- */
-function tag_get_tags_ids($record_type, $record_id) {
-    $tag_ids = array();
-    foreach (tag_get_tags($record_type, $record_id) as $tag) {
-        if ( array_key_exists($tag->ordering, $tag_ids) ) {
-            // until we can add a unique constraint, in table tag_instance,
-            // on (itemtype, itemid, ordering), this is needed to prevent a bug
-            // TODO MDL-31150 modify database in 2.0
-            $tag->ordering++;
-        }
-        $tag_ids[$tag->ordering] = $tag->id;
-    }
-    ksort($tag_ids);
-    return $tag_ids;
-}
-
-/**
- * Returns the database ID of a set of tags.
- *
- * @package  core_tag
- * @category tag
- * @access   public
- * @todo     MDL-31152 Test the commented MDL-31152 todo in this function to see if it helps performance
- *                     without breaking anything.
- * @param    mixed $tags one tag, or array of tags, to look for.
- * @param    bool  $return_value specify the type of the returned value. Either TAG_RETURN_OBJECT, or TAG_RETURN_ARRAY (default).
- *                               If TAG_RETURN_ARRAY is specified, an array will be returned even if only one tag was passed in $tags.
- * @return   mixed tag-indexed array of ids (or objects, if second parameter is TAG_RETURN_OBJECT), or only an int, if only one tag
- *                 is given *and* the second parameter is null. No value for a key means the tag wasn't found.
- */
-function tag_get_id($tags, $return_value=null) {
-    global $CFG, $DB;
-
-    static $tag_id_cache = array();
-
-    $return_an_int = false;
-    if (!is_array($tags)) {
-        if(is_null($return_value) || $return_value == TAG_RETURN_OBJECT) {
-            $return_an_int = true;
-        }
-        $tags = array($tags);
-    }
-
-    $result = array();
-
-    //TODO MDL-31152 test this and see if it helps performance without breaking anything
-    //foreach($tags as $key => $tag) {
-    //    $clean_tag = core_text::strtolower($tag);
-    //    if ( array_key_exists($clean_tag), $tag_id_cache) ) {
-    //        $result[$clean_tag] = $tag_id_cache[$clean_tag];
-    //        $tags[$key] = ''; // prevent further processing for this one.
-    //    }
-    //}
-
-    $tags = array_values(tag_normalize($tags));
-    foreach($tags as $key => $tag) {
-        $tags[$key] = core_text::strtolower($tag);
-        $result[core_text::strtolower($tag)] = null; // key must exists : no value for a key means the tag wasn't found.
-    }
-
-    if (empty($tags)) {
-        return array();
-    }
-
-    list($tag_string, $params) = $DB->get_in_or_equal($tags);
-
-    $rs = $DB->get_recordset_sql("SELECT * FROM {tag} WHERE name $tag_string ORDER BY name", $params);
-    foreach ($rs as $record) {
-        if ($return_value == TAG_RETURN_OBJECT) {
-            $result[$record->name] = $record;
-        } else { // TAG_RETURN_ARRAY
-            $result[$record->name] = $record->id;
-        }
-    }
-    $rs->close();
-
-    if ($return_an_int) {
-        return array_pop($result);
-    }
-
-    return $result;
-}
-
-
-/**
- * Returns tags related to a tag
- *
- * Related tags of a tag come from two sources:
- *   - manually added related tags, which are tag_instance entries for that tag
- *   - correlated tags, which are calculated
- *
- * @package  core_tag
- * @category tag
- * @access   public
- * @param    string   $tagid          is a single **normalized** tag name or the id of a tag
- * @param    int      $type           the function will return either manually (TAG_RELATED_MANUAL) related tags or correlated
- *                                    (TAG_RELATED_CORRELATED) tags. Default is TAG_RELATED_ALL, which returns everything.
- * @param    int      $limitnum       (optional) return a subset comprising this many records, the default is 10
- * @return   array    an array of tag objects
- */
-function tag_get_related_tags($tagid, $type=TAG_RELATED_ALL, $limitnum=10) {
-
-    $related_tags = array();
-
-    if ( $type == TAG_RELATED_ALL || $type == TAG_RELATED_MANUAL) {
-        //gets the manually added related tags
-        $related_tags = tag_get_tags('tag', $tagid);
-    }
-
-    if ( $type == TAG_RELATED_ALL || $type == TAG_RELATED_CORRELATED ) {
-        //gets the correlated tags
-        $automatic_related_tags = tag_get_correlated($tagid);
-        $related_tags = array_merge($related_tags, $automatic_related_tags);
-    }
-
-    // Remove duplicated tags (multiple instances of the same tag).
-    $seen = array();
-    foreach ($related_tags as $instance => $tag) {
-        if (isset($seen[$tag->id])) {
-            unset($related_tags[$instance]);
-        } else {
-            $seen[$tag->id] = 1;
-        }
-    }
-
-    return array_slice($related_tags, 0 , $limitnum);
-}
-
-/**
- * Get a comma-separated list of tags related to another tag.
- *
- * @package  core_tag
- * @category tag
- * @access   public
- * @param    array    $related_tags the array returned by tag_get_related_tags
- * @param    int      $html    either TAG_RETURN_HTML (default) or TAG_RETURN_TEXT : return html links, or just text.
- * @return   string   comma-separated list
- */
-function tag_get_related_tags_csv($related_tags, $html=TAG_RETURN_HTML) {
-    global $CFG;
-
-    $tags_names = array();
-    foreach($related_tags as $tag) {
-        if ( $html == TAG_RETURN_TEXT) {
-            $tags_names[] = tag_display_name($tag, TAG_RETURN_TEXT);
-        } else {
-            // TAG_RETURN_HTML
-            $tags_names[] = '<a href="'. $CFG->wwwroot .'/tag/index.php?tag='. rawurlencode($tag->name) .'">'. tag_display_name($tag) .'</a>';
-        }
-    }
-
-    return implode(', ', $tags_names);
-}
-
-/**
- * Change the "value" of a tag, and update the associated 'name'.
- *
- * @package  core_tag
- * @category tag
- * @access   public
- * @param    int      $tagid  the id of the tag to modify
- * @param    string   $newrawname the new rawname
- * @return   bool     true on success, false otherwise
- */
-function tag_rename($tagid, $newrawname) {
-    global $COURSE, $DB;
-
-    $norm = tag_normalize($newrawname, TAG_CASE_ORIGINAL);
-    if (! $newrawname_clean = array_shift($norm) ) {
-        return false;
-    }
-
-    if (! $newname_clean = core_text::strtolower($newrawname_clean)) {
-        return false;
-    }
-
-    // Prevent the rename if a tag with that name already exists
-    if ($existing = tag_get('name', $newname_clean, 'id, name, rawname')) {
-        if ($existing->id != $tagid) {  // Another tag already exists with this name
-            return false;
-        }
-    }
-
-    if ($tag = tag_get('id', $tagid, 'id, userid, name, rawname')) {
-        // Store the name before we change it.
-        $oldname = $tag->name;
-
-        $tag->rawname = $newrawname_clean;
-        $tag->name = $newname_clean;
-        $tag->timemodified = time();
-        $DB->update_record('tag', $tag);
-
-        $event = \core\event\tag_updated::create(array(
-            'objectid' => $tag->id,
-            'relateduserid' => $tag->userid,
-            'context' => context_system::instance(),
-            'other' => array(
-                'name' => $newname_clean,
-                'rawname' => $newrawname_clean
-            )
-        ));
-        $event->set_legacy_logdata(array($COURSE->id, 'tag', 'update', 'index.php?id='. $tag->id, $oldname . '->'. $tag->name));
-        $event->trigger();
-
-        return true;
-    }
-    return false;
-}
-
-
-/**
- * Delete one or more tag, and all their instances if there are any left.
- *
- * @package  core_tag
- * @category tag
- * @access   public
- * @param    mixed    $tagids one tagid (int), or one array of tagids to delete
- * @return   bool     true on success, false otherwise
- */
-function tag_delete($tagids) {
-    global $DB;
-
-    if (!is_array($tagids)) {
-        $tagids = array($tagids);
-    }
-
-    // Use the tagids to create a select statement to be used later.
-    list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids);
-
-    // Store the tags and tag instances we are going to delete.
-    $tags = $DB->get_records_select('tag', 'id ' . $tagsql, $tagparams);
-    $taginstances = $DB->get_records_select('tag_instance', 'tagid ' . $tagsql, $tagparams);
-
-    // Delete all the tag instances.
-    $select = 'WHERE tagid ' . $tagsql;
-    $sql = "DELETE FROM {tag_instance} $select";
-    $DB->execute($sql, $tagparams);
-
-    // Delete all the tag correlations.
-    $sql = "DELETE FROM {tag_correlation} $select";
-    $DB->execute($sql, $tagparams);
-
-    // Delete all the tags.
-    $select = 'WHERE id ' . $tagsql;
-    $sql = "DELETE FROM {tag} $select";
-    $DB->execute($sql, $tagparams);
-
-    // Fire an event that these items were untagged.
-    if ($taginstances) {
-        // Save the system context in case the 'contextid' column in the 'tag_instance' table is null.
-        $syscontextid = context_system::instance()->id;
-        // Loop through the tag instances and fire a 'tag_removed'' event.
-        foreach ($taginstances as $taginstance) {
-            // We can not fire an event with 'null' as the contextid.
-            if (is_null($taginstance->contextid)) {
-                $taginstance->contextid = $syscontextid;
-            }
-
-            // Trigger tag removed event.
-            $event = \core\event\tag_removed::create(array(
-                'objectid' => $taginstance->id,
-                'contextid' => $taginstance->contextid,
-                'other' => array(
-                    'tagid' => $taginstance->tagid,
-                    'tagname' => $tags[$taginstance->tagid]->name,
-                    'tagrawname' => $tags[$taginstance->tagid]->rawname,
-                    'itemid' => $taginstance->itemid,
-                    'itemtype' => $taginstance->itemtype
-                )
-            ));
-            $event->add_record_snapshot('tag_instance', $taginstance);
-            $event->trigger();
-        }
-    }
-
-    // Fire an event that these tags were deleted.
-    if ($tags) {
-        $context = context_system::instance();
-        foreach ($tags as $tag) {
-            // Delete all files associated with this tag
-            $fs = get_file_storage();
-            $files = $fs->get_area_files($context->id, 'tag', 'description', $tag->id);
-            foreach ($files as $file) {
-                $file->delete();
-            }
-
-            // Trigger an event for deleting this tag.
-            $event = \core\event\tag_deleted::create(array(
-                'objectid' => $tag->id,
-                'relateduserid' => $tag->userid,
-                'context' => $context,
-                'other' => array(
-                    'name' => $tag->name,
-                    'rawname' => $tag->rawname
-                )
-            ));
-            $event->add_record_snapshot('tag', $tag);
-            $event->trigger();
-        }
-    }
-
-    return true;
-}
-
-/**
- * Deletes all the tag instances given a component and an optional contextid.
- *
- * @param string $component
- * @param int $contextid if null, then we delete all tag instances for the $component
- */
-function tag_delete_instances($component, $contextid = null) {
-    global $DB;
-
-    $sql = "SELECT ti.*, t.name, t.rawname
-              FROM {tag_instance} ti
-              JOIN {tag} t
-                ON ti.tagid = t.id ";
-    if (is_null($contextid)) {
-        $params = array('component' => $component);
-        $sql .= "WHERE ti.component = :component";
-    } else {
-        $params = array('component' => $component, 'contextid' => $contextid);
-        $sql .= "WHERE ti.component = :component
-                   AND ti.contextid = :contextid";
-    }
-    if ($taginstances = $DB->get_records_sql($sql, $params)) {
-        // Now remove all the tag instances.
-        $DB->delete_records('tag_instance',$params);
-        // Save the system context in case the 'contextid' column in the 'tag_instance' table is null.
-        $syscontextid = context_system::instance()->id;
-        // Loop through the tag instances and fire an 'tag_removed' event.
-        foreach ($taginstances as $taginstance) {
-            // We can not fire an event with 'null' as the contextid.
-            if (is_null($taginstance->contextid)) {
-                $taginstance->contextid = $syscontextid;
-            }
-
-            // Trigger tag removed event.
-            $event = \core\event\tag_removed::create(array(
-                'objectid' => $taginstance->id,
-                'contextid' => $taginsta