From 5436b0ed021c697de09b7b593ae64438d7974090 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Fri, 23 Feb 2018 10:47:14 +0800 Subject: [PATCH] MDL-61363 tag: allow tagging in different context to item --- lib/db/install.xml | 5 +++-- lib/db/tag.php | 1 + lib/db/upgrade.php | 40 +++++++++++++++++++++++++++++++++++- tag/classes/area.php | 35 ++++++++++++++++++++++++++++++-- tag/classes/tag.php | 48 +++++++++++++++++++++++++++++++++++++++++--- version.php | 2 +- 6 files changed, 122 insertions(+), 9 deletions(-) mode change 100644 => 100755 lib/db/install.xml diff --git a/lib/db/install.xml b/lib/db/install.xml old mode 100644 new mode 100755 index 593ff37a451d8..97df4857b3804 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -2019,6 +2019,7 @@ + @@ -2081,7 +2082,7 @@ - + diff --git a/lib/db/tag.php b/lib/db/tag.php index 2dc6b314ba3ce..ed5cf19b95d5b 100644 --- a/lib/db/tag.php +++ b/lib/db/tag.php @@ -69,6 +69,7 @@ array( 'itemtype' => 'question', // Questions. 'component' => 'core_question', + 'multiplecontexts' => true, ), array( 'itemtype' => 'post', // Blog posts. diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 7e68ae117c862..78ebbee0dd06c 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1974,7 +1974,6 @@ function xmldb_main_upgrade($oldversion) { } if ($oldversion < 2018022800.01) { - // Fix old block configurations that use the deprecated (and now removed) object class. upgrade_fix_block_instance_configuration(); @@ -1982,5 +1981,44 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2018022800.01); } + if ($oldversion < 2018022800.02) { + // Define index taggeditem (unique) to be dropped form tag_instance. + $table = new xmldb_table('tag_instance'); + $index = new xmldb_index('taggeditem', XMLDB_INDEX_UNIQUE, array('component', + 'itemtype', 'itemid', 'tiuserid', 'tagid')); + + // Conditionally launch drop index taggeditem. + if ($dbman->index_exists($table, $index)) { + $dbman->drop_index($table, $index); + } + + $index = new xmldb_index('taggeditem', XMLDB_INDEX_UNIQUE, array('component', + 'itemtype', 'itemid', 'contextid', '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, 2018022800.02); + } + + if ($oldversion < 2018022800.03) { + + // Define field multiplecontexts to be added to tag_area. + $table = new xmldb_table('tag_area'); + $field = new xmldb_field('multiplecontexts', XMLDB_TYPE_INTEGER, '1', null, + XMLDB_NOTNULL, null, '0', 'showstandard'); + + // Conditionally launch add field multiplecontexts. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2018022800.03); + } + return true; } diff --git a/tag/classes/area.php b/tag/classes/area.php index a51b82541d8bd..9e79fac970fcf 100644 --- a/tag/classes/area.php +++ b/tag/classes/area.php @@ -122,6 +122,32 @@ public static function is_enabled($component, $itemtype) { return null; } + /** + * Checks if the tag area allows items to be tagged in multiple different contexts. + * + * If true then it indicates that not all tag instance contexts must match the + * context of the item they are tagging. If false then all tag instance should + * match the context of the item they are tagging. + * + * Example use case for multi-context tagging: + * A question that exists in a course category context may be used by multiple + * child courses. The question tag area can allow tag instances to be created in + * multiple contexts which allows the tag API to tag the question at the course + * category context and then seperately in each of the child course contexts. + * + * @param string $component component responsible for tagging + * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc. + * @return bool + */ + public static function allows_tagging_in_multiple_contexts($component, $itemtype) { + $itemtypes = self::get_areas(); + if (isset($itemtypes[$itemtype][$component])) { + $config = $itemtypes[$itemtype][$component]; + return isset($config->multiplecontexts) ? $config->multiplecontexts : false; + } + return false; + } + /** * Returns the id of the tag collection that should be used for storing tags of this itemtype * @@ -217,7 +243,8 @@ protected static function create($record) { 'tagcollid' => $record->tagcollid, 'callback' => $record->callback, 'callbackfile' => $record->callbackfile, - 'showstandard' => isset($record->showstandard) ? $record->showstandard : core_tag_tag::BOTH_STANDARD_AND_NOT)); + 'showstandard' => isset($record->showstandard) ? $record->showstandard : core_tag_tag::BOTH_STANDARD_AND_NOT, + 'multiplecontexts' => isset($record->multiplecontexts) ? $record->multiplecontexts : 0)); // Reset cache. cache::make('core', 'tags')->delete('tag_area'); @@ -233,7 +260,8 @@ public static function update($existing, $data) { global $DB; $data = array_intersect_key((array)$data, array('enabled' => 1, 'tagcollid' => 1, - 'callback' => 1, 'callbackfile' => 1, 'showstandard' => 1)); + 'callback' => 1, 'callbackfile' => 1, 'showstandard' => 1, + 'multiplecontexts' => 1)); foreach ($data as $key => $value) { if ($existing->$key == $value) { unset($data[$key]); @@ -310,6 +338,9 @@ public static function reset_definitions_for_component($componentname) { if (!isset($record->callbackfile)) { $record->callbackfile = null; } + if (!isset($record->multiplecontexts)) { + $record->multiplecontexts = false; + } $itemtypes[$record->itemtype . ':' . $record->component] = $record; } } diff --git a/tag/classes/tag.php b/tag/classes/tag.php index 0af1a629b06fd..0c9331610588e 100644 --- a/tag/classes/tag.php +++ b/tag/classes/tag.php @@ -602,7 +602,7 @@ protected function add_instance($component, $itemtype, $itemid, context $context global $DB; $this->ensure_fields_exist(array('name', 'rawname'), 'add_instance'); - $taginstance = new StdClass; + $taginstance = new stdClass; $taginstance->tagid = $this->id; $taginstance->component = $component ? $component : ''; $taginstance->itemid = $itemid; @@ -773,12 +773,35 @@ public static function set_item_tags($component, $itemtype, $itemid, context $co $tagobjects = array(); } + $allowmultiplecontexts = core_tag_area::allows_tagging_in_multiple_contexts($component, $itemtype); $currenttags = static::get_item_tags($component, $itemtype, $itemid, self::BOTH_STANDARD_AND_NOT, $tiuserid); + $taginstanceidstomovecontext = []; // 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)) { + $hasbeenrequested = array_key_exists($currenttag->name, $tagobjects); + $issamecontext = $currenttag->taginstancecontextid == $context->id; + + if ($allowmultiplecontexts) { + // If the tag area allows multiple contexts then we should only be + // managing tags in the given $context. All other tags can be ignored. + $shoulddelete = $issamecontext && !$hasbeenrequested; + } else { + // If the tag area only allows tag instances in a single context then + // all tags that aren't in the requested tags should be deleted, regardless + // of their context, if they are not part of the new set of tags. + $shoulddelete = !$hasbeenrequested; + // If the tag instance isn't in the correct context (legacy data) + // then we should take this opportunity to update it with the correct + // context id. + if (!$shoulddelete && !$issamecontext) { + $currenttag->taginstancecontextid = $context->id; + $taginstanceidstomovecontext[] = $currenttag->taginstanceid; + } + } + + if ($shoulddelete) { $taginstance = (object)array('id' => $currenttag->taginstanceid, 'itemtype' => $itemtype, 'itemid' => $itemid, 'contextid' => $currenttag->taginstancecontextid, 'tiuserid' => $tiuserid); @@ -786,11 +809,30 @@ public static function set_item_tags($component, $itemtype, $itemid, context $co } } + if (!empty($taginstanceidstomovecontext)) { + static::change_instances_context($taginstanceidstomovecontext, $context); + } + $ordering = -1; foreach ($tagobjects as $name => $tag) { $ordering++; foreach ($currenttags as $currenttag) { - if (strval($currenttag->name) === strval($name)) { + $namesmatch = strval($currenttag->name) === strval($name); + + if ($allowmultiplecontexts) { + // If the tag area allows multiple contexts then we should only + // skip adding a new instance if the existing one is in the correct + // context. + $contextsmatch = $currenttag->taginstancecontextid == $context->id; + $shouldskipinstance = $namesmatch && $contextsmatch; + } else { + // The existing behaviour for single context tag areas is to + // skip adding a new instance regardless of whether the existing + // instance is in the same context as the provided $context. + $shouldskipinstance = $namesmatch; + } + + if ($shouldskipinstance) { if ($currenttag->ordering != $ordering) { $currenttag->update_instance_ordering($currenttag->taginstanceid, $ordering); } diff --git a/version.php b/version.php index 641e28593ec05..e5fdaa815ab8d 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2018022800.01; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2018022800.03; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.