$string['helprelatedtags'] = 'Comma separated related tags';
$string['changename'] = 'Change tag name';
$string['changetype'] = 'Change tag type';
+$string['combined'] = 'Tags are combined';
+$string['combineselected'] = 'Combine selected';
$string['id'] = 'id';
$string['inalltagcoll'] = 'Everywhere';
$string['itemstaggedwith'] = '{$a->tagarea} tagged with "{$a->tag}"';
$string['moretags'] = 'more...';
$string['name'] = 'Tag name';
$string['namesalreadybeeingused'] = 'Tag names already being used';
+$string['nameuseddocombine'] = 'This tag name is already used, do you want to combine these tags?';
$string['newcollnamefor'] = 'New name for tag collection {$a}';
$string['newnamefor'] = 'New name for tag {$a}';
$string['nextpage'] = 'More';
$string['seeallblogs'] = 'See all blog entries tagged with "{$a}"';
$string['select'] = 'Select';
$string['selectcoll'] = 'Select tag collection';
+$string['selectmaintag'] = 'Select the tag that will be used after combining';
+$string['selectmultipletags'] = 'Please select more than one tag';
$string['selecttag'] = 'Select tag {$a}';
$string['settypedefault'] = 'Remove from standard tags';
$string['settypestandard'] = 'Make standard';
if (!cnt) {
return false;
}
+ var tempElement = $("<input type='hidden'/>").attr('name', this.name);
e.preventDefault();
str.get_strings([
{key : 'delete'},
{key : 'no'},
]).done(function(s) {
notification.confirm(s[0], s[1], s[2], s[3], function() {
+ tempElement.appendTo(form);
form.submit();
});
}
);
});
+
+ // Confirmation for bulk tag combine button.
+ $("#tag-management-combine").click(function(e){
+ e.preventDefault();
+ var form = $(this).closest('form').get(0),
+ tags = $(form).find("input[type=checkbox]:checked");
+ if (tags.length <= 1) {
+ str.get_strings([
+ {key : 'combineselected', component : 'tag'},
+ {key : 'selectmultipletags', component : 'tag'},
+ {key : 'ok'},
+ ]).done(function(s) {
+ notification.alert(s[0], s[1], s[2]);
+ }
+ );
+ return false;
+ }
+ var tempElement = $("<input type='hidden'/>").attr('name', this.name);
+ str.get_strings([
+ {key : 'combineselected', component : 'tag'},
+ {key : 'selectmaintag', component : 'tag'},
+ {key : 'continue'},
+ {key : 'cancel'},
+ ]).done(function(s) {
+ var el = $('<div><form id="combinetags_form" class="form-inline">'+
+ '<p class="description"></p><p class="options"></p>' +
+ '<p class="mdl-align"><input type="submit" id="combinetags_submit"/>'+
+ '<input type="button" id="combinetags_cancel"/></p>' +
+ '</form></div>');
+ el.find('.description').html(s[1]);
+ el.find('#combinetags_submit').attr('value', s[2]);
+ el.find('#combinetags_cancel').attr('value', s[3]);
+ var fldset = el.find('.options');
+ tags.each(function() {
+ var tagid = $(this).val(),
+ tagname = $('.inplaceeditable[data-itemtype=tagname][data-itemid='+tagid+']').attr('data-value');
+ fldset.append($('<input type="radio" name="maintag" id="combinetags_maintag_'+tagid+'" value="'+tagid+
+ '"/><label for="combinetags_maintag_'+tagid+'">'+tagname+'</label><br>'));
+ });
+ var panel = new M.core.dialogue ({
+ draggable: true,
+ modal: true,
+ closeButton: true,
+ headerContent: s[0],
+ bodyContent: el.html()
+ });
+ panel.show();
+ $('#combinetags_form input[type=radio]').first().focus().prop('checked', true);
+ $('#combinetags_form #combinetags_cancel').on('click', function() {
+ panel.destroy();
+ });
+ $('#combinetags_form').on('submit', function() {
+ tempElement.appendTo(form);
+ var maintag = $('input[name=maintag]:checked', '#combinetags_form').val();
+ $("<input type='hidden'/>").attr('name', 'maintag').attr('value', maintag).appendTo(form);
+ form.submit();
+ return false;
+ });
+ });
+ });
+
+ // When user changes tag name to some name that already exists suggest to combine the tags.
+ $('body').on('updatefailed', '[data-inplaceeditable][data-itemtype=tagname]', function(e) {
+ var exception = e.exception; // The exception object returned by the callback.
+ var newvalue = e.newvalue; // The value that user tried to udpated the element to.
+ var tagid = $(e.target).attr('data-itemid');
+ if (exception.errorcode === 'namesalreadybeeingused') {
+ e.preventDefault(); // This will prevent default error dialogue.
+ str.get_strings([
+ {key : 'nameuseddocombine', component : 'tag'},
+ {key : 'yes'},
+ {key : 'cancel'},
+ ]).done(function(s) {
+ notification.confirm(e.message, s[0], s[1], s[2], function() {
+ window.location.href = window.location.href + "&newname=" + encodeURIComponent(newvalue) +
+ "&tagid=" + encodeURIComponent(tagid) +
+ '&action=renamecombine&sesskey=' + M.cfg.sesskey;
+ });
+ });
+ }
+ });
},
/**
return false;
}
+ /**
+ * Simple function to just return a single tag object by its id
+ *
+ * @param int[] $ids
+ * @param string $returnfields which fields do we want returned from table {tag}.
+ * Default value is 'id,name,rawname,tagcollid',
+ * specify '*' to include all fields.
+ * @return core_tag_tag[] array of retrieved tags
+ */
+ public static function get_bulk($ids, $returnfields = 'id, name, rawname, tagcollid') {
+ global $DB;
+ $result = array();
+ if (empty($ids)) {
+ return $result;
+ }
+ list($sql, $params) = $DB->get_in_or_equal($ids);
+ $records = $DB->get_records_select('tag', 'id '.$sql, $params, '', $returnfields);
+ foreach ($records as $record) {
+ $result[$record->id] = new static($record);
+ }
+ return $result;
+ }
+
/**
* Simple function to just return a single tag object by tagcollid and name
*
$standardonly = (int)$standardonly; // In case somebody passed bool.
- // Note: if the fields in this query are changed, you need to do the same changes in tag_get_correlated().
+ // Note: if the fields in this query are changed, you need to do the same changes in core_tag_tag::get_correlated_tags().
$sql = "SELECT ti.id AS taginstanceid, tg.id, tg.isstandard, tg.name, tg.rawname, tg.flag,
tg.tagcollid, ti.ordering, ti.contextid AS taginstancecontextid
FROM {tag_instance} ti
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
+ WHERE tg.id $query AND tg.id <> ? AND tg.tagcollid = ?
ORDER BY ti.ordering ASC, ti.id";
+ $params[] = $this->id;
+ $params[] = $this->tagcollid;
$records = $DB->get_records_sql($sql, $params);
$seen = array();
$result = array();
return true;
}
+
+ /**
+ * Combine together correlated tags of several tags
+ *
+ * This is a help method for method combine_tags()
+ *
+ * @param core_tag_tag[] $tags
+ */
+ protected function combine_correlated_tags($tags) {
+ global $DB;
+ $ids = array_map(function($t) {
+ return $t->id;
+ }, $tags);
+
+ // Retrieve the correlated tags of this tag and correlated tags of all tags to be merged in one query
+ // but store them separately. Calculate the list of correlated tags that need to be added to the current.
+ list($sql, $params) = $DB->get_in_or_equal($ids);
+ $params[] = $this->id;
+ $records = $DB->get_records_select('tag_correlation', 'tagid '.$sql.' OR tagid = ?',
+ $params, '', 'tagid, id, correlatedtags');
+ $correlated = array();
+ $mycorrelated = array();
+ foreach ($records as $record) {
+ $taglist = preg_split('/\s*,\s*/', trim($record->correlatedtags), -1, PREG_SPLIT_NO_EMPTY);
+ if ($record->tagid == $this->id) {
+ $mycorrelated = $taglist;
+ } else {
+ $correlated = array_merge($correlated, $taglist);
+ }
+ }
+ array_unique($correlated);
+ // Strip out from $correlated the ids of the tags that are already in $mycorrelated
+ // or are one of the tags that are going to be combined.
+ $correlated = array_diff($correlated, [$this->id], $ids, $mycorrelated);
+
+ if (empty($correlated)) {
+ // Nothing to do, ignore situation when current tag is correlated to one of the merged tags - they will
+ // be deleted later and get_tag_correlation() will not return them. Next cron will clean everything up.
+ return;
+ }
+
+ // Update correlated tags of this tag.
+ $newcorrelatedlist = join(',', array_merge($mycorrelated, $correlated));
+ if (isset($records[$this->id])) {
+ $DB->update_record('tag_correlation', array('id' => $records[$this->id]->id, 'correlatedtags' => $newcorrelatedlist));
+ } else {
+ $DB->insert_record('tag_correlation', array('tagid' => $this->id, 'correlatedtags' => $newcorrelatedlist));
+ }
+
+ // Add this tag to the list of correlated tags of each tag in $correlated.
+ list($sql, $params) = $DB->get_in_or_equal($correlated);
+ $records = $DB->get_records_select('tag_correlation', 'tagid '.$sql, $params, '', 'tagid, id, correlatedtags');
+ foreach ($correlated as $tagid) {
+ if (isset($records[$tagid])) {
+ $newcorrelatedlist = $records[$tagid]->correlatedtags . ',' . $this->id;
+ $DB->update_record('tag_correlation', array('id' => $records[$tagid]->id, 'correlatedtags' => $newcorrelatedlist));
+ } else {
+ $DB->insert_record('tag_correlation', array('tagid' => $tagid, 'correlatedtags' => '' . $this->id));
+ }
+ }
+ }
+
+ /**
+ * Combines several other tags into this one
+ *
+ * Combining rules:
+ * - current tag becomes the "main" one, all instances
+ * pointing to other tags are changed to point to it.
+ * - if any of the tags is standard, the "main" tag becomes standard too
+ * - all tags except for the current ("main") are deleted, even when they are standard
+ *
+ * @param core_tag_tag[] $tags tags to combine into this one
+ */
+ public function combine_tags($tags) {
+ global $DB;
+
+ $this->ensure_fields_exist(array('id', 'tagcollid', 'isstandard', 'name', 'rawname'), 'combine_tags');
+
+ // Retrieve all tag objects, find if there are any standard tags in the set.
+ $isstandard = false;
+ $tagstocombine = array();
+ $ids = array();
+ $relatedtags = $this->get_manual_related_tags();
+ foreach ($tags as $tag) {
+ $tag->ensure_fields_exist(array('id', 'tagcollid', 'isstandard', 'tagcollid', 'name', 'rawname'), 'combine_tags');
+ if ($tag && $tag->id != $this->id && $tag->tagcollid == $this->tagcollid) {
+ $isstandard = $isstandard || $tag->isstandard;
+ $tagstocombine[$tag->name] = $tag;
+ $ids[] = $tag->id;
+ $relatedtags = array_merge($relatedtags, $tag->get_manual_related_tags());
+ }
+ }
+
+ if (empty($tagstocombine)) {
+ // Nothing to do.
+ return;
+ }
+
+ // Combine all manually set related tags, exclude itself all the tags it is about to be combined with.
+ if ($relatedtags) {
+ $relatedtags = array_map(function($t) {
+ return $t->name;
+ }, $relatedtags);
+ array_unique($relatedtags);
+ $relatedtags = array_diff($relatedtags, [$this->name], array_keys($tagstocombine));
+ }
+ $this->set_related_tags($relatedtags);
+
+ // Combine all correlated tags, exclude itself all the tags it is about to be combined with.
+ $this->combine_correlated_tags($tagstocombine);
+
+ // If any of the duplicate tags are standard, mark this one as standard too.
+ if ($isstandard && !$this->isstandard) {
+ $this->update(array('isstandard' => 1));
+ }
+
+ // Go through all instances of each tag that needs to be combined and make them point to this tag instead.
+ // We go though the list one by one because otherwise looking-for-duplicates logic would be too complicated.
+ foreach ($tagstocombine as $tag) {
+ $params = array('tagid' => $tag->id, 'mainid' => $this->id);
+ $mainsql = 'SELECT ti.*, t.name, t.rawname, tim.id AS alreadyhasmaintag '
+ . 'FROM {tag_instance} ti '
+ . 'LEFT JOIN {tag} t ON t.id = ti.tagid '
+ . 'LEFT JOIN {tag_instance} tim ON ti.component = tim.component AND '
+ . ' ti.itemtype = tim.itemtype AND ti.itemid = tim.itemid AND '
+ . ' ti.tiuserid = tim.tiuserid AND tim.tagid = :mainid '
+ . 'WHERE ti.tagid = :tagid';
+
+ $records = $DB->get_records_sql($mainsql, $params);
+ foreach ($records as $record) {
+ if ($record->alreadyhasmaintag) {
+ // Item is tagged with both main tag and the duplicate tag.
+ // Remove instance pointing to the duplicate tag.
+ $tag->delete_instance_as_record($record, false);
+ $sql = "UPDATE {tag_instance} ti SET ordering = ordering - 1
+ WHERE ti.itemtype = :itemtype
+ AND ti.itemid = :itemid AND ti.component = :component AND ti.tiuserid = :tiuserid
+ AND ti.ordering > :ordering";
+ $DB->execute($sql, (array)$record);
+ } else {
+ // Item is tagged only with duplicate tag but not the main tag.
+ // Replace tagid in the instance pointing to the duplicate tag with this tag.
+ $DB->update_record('tag_instance', array('id' => $record->id, 'tagid' => $this->id));
+ \core\event\tag_removed::create_from_tag_instance($record, $record->name, $record->rawname)->trigger();
+ $record->tagid = $this->id;
+ \core\event\tag_added::create_from_tag_instance($record, $this->name, $this->rawname)->trigger();
+ }
+ }
+ }
+
+ // Finally delete all tags that we combined into the current one.
+ self::delete_tags($ids);
+ }
}
if ($page > 0) {
$params['page'] = $page;
}
+if ($tagcollid) {
+ $params['tc'] = $tagcollid;
+}
admin_externalpage_setup('managetags', '', $params, '', array('pagelayout' => 'report'));
$tagarea = core_tag_area::get_by_id($tagareaid);
$manageurl = new moodle_url('/tag/manage.php');
if ($tagcoll) {
- // We are inside a tag collection - add it to the page url and the breadcrumb.
- $PAGE->set_url(new moodle_url($PAGE->url, array('tc' => $tagcoll->id)));
+ // We are inside a tag collection - add it to the breadcrumb.
$PAGE->navbar->add(core_tag_collection::display_name($tagcoll),
new moodle_url($manageurl, array('tc' => $tagcoll->id)));
}
break;
case 'delete':
- require_sesskey();
- if (!$tagschecked && $tagid) {
- $tagschecked = array($tagid);
+ if ($tagid) {
+ require_sesskey();
+ core_tag_tag::delete_tags(array($tagid));
+ \core\notification::success(get_string('deleted', 'core_tag'));
}
- core_tag_tag::delete_tags($tagschecked);
- if ($tagschecked) {
- redirect($PAGE->url, get_string('deleted', 'core_tag'), null, \core\output\notification::NOTIFY_SUCCESS);
- } else {
+ redirect($PAGE->url);
+ break;
+
+ case 'bulk':
+ if (optional_param('bulkdelete', null, PARAM_RAW) !== null) {
+ if ($tagschecked) {
+ require_sesskey();
+ core_tag_tag::delete_tags($tagschecked);
+ \core\notification::success(get_string('deleted', 'core_tag'));
+ }
redirect($PAGE->url);
+ } else if (optional_param('bulkcombine', null, PARAM_RAW) !== null) {
+ $tags = core_tag_tag::get_bulk($tagschecked, '*');
+ if (count($tags) > 1) {
+ require_sesskey();
+ if (($maintag = optional_param('maintag', 0, PARAM_INT)) && array_key_exists($maintag, $tags)) {
+ $tag = $tags[$maintag];
+ } else {
+ $tag = array_shift($tags);
+ }
+ $tag->combine_tags($tags);
+ \core\notification::success(get_string('combined', 'core_tag'));
+ }
+ redirect($PAGE->url);
+ }
+ break;
+
+ case 'renamecombine':
+ // Allows to rename the tag and if the tag with the new name already exists these tags will be combined.
+ if ($tagid && ($newname = required_param('newname', PARAM_TAG))) {
+ require_sesskey();
+ $tag = core_tag_tag::get($tagid, '*', MUST_EXIST);
+ $targettag = core_tag_tag::get_by_name($tag->tagcollid, $newname, '*');
+ if ($targettag) {
+ $targettag->combine_tags(array($tag));
+ \core\notification::success(get_string('combined', 'core_tag'));
+ } else {
+ $tag->update(array('rawname' => $newname));
+ \core\notification::success(get_string('changessaved', 'core_tag'));
+ }
}
+ redirect($PAGE->url);
break;
case 'addstandardtag':
echo '<form class="tag-management-form" method="post" action="'.$CFG->wwwroot.'/tag/manage.php">';
echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'tc', 'value' => $tagcollid));
echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
-echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'action', 'value' => 'delete'));
+echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'action', 'value' => 'bulk'));
echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'perpage', 'value' => $perpage));
echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'page', 'value' => $page));
echo $table->out($perpage, true);
if ($table->rawdata) {
echo html_writer::start_tag('p');
echo html_writer::tag('button', get_string('deleteselected', 'tag'),
- array('id' => 'tag-management-delete', 'type' => 'submit', 'class' => 'tagdeleteselected'));
+ array('id' => 'tag-management-delete', 'type' => 'submit', 'class' => 'tagdeleteselected', 'name' => 'bulkdelete'));
+ echo html_writer::tag('button', get_string('combineselected', 'tag'),
+ array('id' => 'tag-management-combine', 'type' => 'submit', 'class' => 'tagcombineselected', 'name' => 'bulkcombine'));
echo html_writer::end_tag('p');
}
echo '</form>';
And I click on "Edit tag name" "link" in the "Turtle" "table_row"
And I set the field "New name for tag Turtle" to "DOG"
And I press key "13" in the field "New name for tag Turtle"
- And I should see "Tag names already being used"
- And I press "Close"
+ And I should see "This tag name is already used, do you want to combine these tags?"
+ And I press "Cancel"
And "New name for tag" "field" should not exist
And I should see "Turtle"
And I should see "Dog"
And I should see "Turtle"
And I should not see "Penguin"
And I log out
+
+ @javascript
+ Scenario: Combining tags when renaming
+ When I log in as "manager1"
+ And I navigate to "Manage tags" node in "Site administration > Appearance"
+ And I follow "Default collection"
+ And I click on "Edit tag name" "link" in the "Turtle" "table_row"
+ And I set the field "New name for tag Turtle" to "DOG"
+ And I press key "13" in the field "New name for tag Turtle"
+ And I should see "This tag name is already used, do you want to combine these tags?"
+ And I press "Yes"
+ Then I should not see "Turtle"
+ And I should not see "DOG"
+ And I should see "Dog"
+ And I log out
+
+ @javascript
+ Scenario: Combining multiple tags
+ When I log in as "manager1"
+ And I navigate to "Manage tags" node in "Site administration > Appearance"
+ And I follow "Default collection"
+ And I set the following fields to these values:
+ | Select tag Dog | 1 |
+ | Select tag Neverusedtag | 1 |
+ | Select tag Turtle | 1 |
+ And I press "Combine selected"
+ And I should see "Select the tag that will be used after combining"
+ And I click on "//form[@id='combinetags_form']//input[@type='radio'][3]" "xpath_element"
+ And I press "Continue"
+ Then I should see "Tags are combined"
+ And I should not see "Dog"
+ And I should not see "Neverusedtag"
+ And I should see "Turtle"
+ # Even though Turtle was not standard but at least one of combined tags was (Neverusedtag). Now Turtle is also standard.
+ And "Remove from standard tags" "link" should exist in the "Turtle" "table_row"
+ And I log out
}
/**
- * Test for function tag_compute_correlations() that is part of tag cron
+ * Prepares environment for testing tag correlations
+ * @return core_tag_tag[] list of used tags
*/
- public function test_correlations() {
+ protected function prepare_correlated() {
global $DB;
- $task = new \core\task\tag_cron_task();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
core_tag_tag::set_item_tags('core', 'user', $user6->id, context_user::instance($user6->id), array('dog', 'dogs', 'puppy'));
$tags = core_tag_tag::get_by_name_bulk(core_tag_collection::get_default(),
- array('cat', 'cats', 'dog', 'dogs', 'kitten', 'puppy'));
- $tags = array_map(function ($t) {
- return $t->id;
- }, $tags);
+ array('cat', 'cats', 'dog', 'dogs', 'kitten', 'puppy'), '*');
// Add manual relation between tags 'cat' and 'kitten'.
- core_tag_tag::get($tags['cat'])->set_related_tags(array('kitten'));
+ core_tag_tag::get($tags['cat']->id)->set_related_tags(array('kitten'));
+
+ return $tags;
+ }
+
+ /**
+ * Test for function tag_compute_correlations() that is part of tag cron
+ */
+ public function test_correlations() {
+ global $DB;
+ $task = new \core\task\tag_cron_task();
+
+ $tags = array_map(function ($t) {
+ return $t->id;
+ }, $this->prepare_correlated());
$task->compute_correlations();
$this->assertEquals('mouse', $tags3['mouse']->rawname);
}
+
+ /**
+ * Testing function core_tag_tag::combine_tags()
+ */
+ public function test_combine_tags() {
+ $initialtags = array(
+ array('Cat', 'Dog'),
+ array('Dog', 'Cat'),
+ array('Cats', 'Hippo'),
+ array('Hippo', 'Cats'),
+ array('Cat', 'Mouse', 'Kitten'),
+ array('Cats', 'Mouse', 'Kitten'),
+ array('Kitten', 'Mouse', 'Cat'),
+ array('Kitten', 'Mouse', 'Cats'),
+ array('Cats', 'Mouse', 'Kitten'),
+ array('Mouse', 'Hippo')
+ );
+
+ $finaltags = array(
+ array('Cat', 'Dog'),
+ array('Dog', 'Cat'),
+ array('Cat', 'Hippo'),
+ array('Hippo', 'Cat'),
+ array('Cat', 'Mouse'),
+ array('Cat', 'Mouse'),
+ array('Mouse', 'Cat'),
+ array('Mouse', 'Cat'),
+ array('Cat', 'Mouse'),
+ array('Mouse', 'Hippo')
+ );
+
+ $collid = core_tag_collection::get_default();
+ $context = context_system::instance();
+ foreach ($initialtags as $id => $taglist) {
+ core_tag_tag::set_item_tags('core', 'course', $id + 10, $context, $initialtags[$id]);
+ }
+
+ core_tag_tag::get_by_name($collid, 'Cats', '*')->update(array('isstandard' => 1));
+
+ // Combine tags 'Cats' and 'Kitten' into 'Cat'.
+ $cat = core_tag_tag::get_by_name($collid, 'Cat', '*');
+ $cats = core_tag_tag::get_by_name($collid, 'Cats', '*');
+ $kitten = core_tag_tag::get_by_name($collid, 'Kitten', '*');
+ $cat->combine_tags(array($cats, $kitten));
+
+ foreach ($finaltags as $id => $taglist) {
+ $this->assertEquals($taglist,
+ array_values(core_tag_tag::get_item_tags_array('core', 'course', $id + 10)),
+ 'Original array ('.join(', ', $initialtags[$id]).')');
+ }
+
+ // Ensure combined tags are deleted and 'Cat' is now official (because 'Cats' was official).
+ $this->assertEmpty(core_tag_tag::get_by_name($collid, 'Cats'));
+ $this->assertEmpty(core_tag_tag::get_by_name($collid, 'Kitten'));
+ $cattag = core_tag_tag::get_by_name($collid, 'Cat', '*');
+ $this->assertEquals(1, $cattag->isstandard);
+ }
+
+ /**
+ * Testing function core_tag_tag::combine_tags() when related tags are present.
+ */
+ public function test_combine_tags_with_related() {
+ $collid = core_tag_collection::get_default();
+ $context = context_system::instance();
+ core_tag_tag::set_item_tags('core', 'course', 10, $context, array('Cat', 'Cats', 'Dog'));
+ core_tag_tag::get_by_name($collid, 'Cat', '*')->set_related_tags(array('Kitty'));
+ core_tag_tag::get_by_name($collid, 'Cats', '*')->set_related_tags(array('Cat', 'Kitten', 'Kitty'));
+
+ // Combine tags 'Cats' into 'Cat'.
+ $cat = core_tag_tag::get_by_name($collid, 'Cat', '*');
+ $cats = core_tag_tag::get_by_name($collid, 'Cats', '*');
+ $cat->combine_tags(array($cats));
+
+ // Ensure 'Cat' is now related to 'Kitten' and 'Kitty' (order of related tags may be random).
+ $relatedtags = array_map(function($t) {return $t->rawname;}, $cat->get_manual_related_tags());
+ sort($relatedtags);
+ $this->assertEquals(array('Kitten', 'Kitty'), array_values($relatedtags));
+ }
+
+ /**
+ * Testing function core_tag_tag::combine_tags() when correlated tags are present.
+ */
+ public function test_combine_tags_with_correlated() {
+ $task = new \core\task\tag_cron_task();
+
+ $tags = $this->prepare_correlated();
+
+ $task->compute_correlations();
+ // Now 'cat' is correlated with 'cats'.
+ // Also 'dog', 'dogs' and 'puppy' are correlated.
+ // There is a manual relation between 'cat' and 'kitten'.
+ // See function test_correlations() for assertions.
+
+ // Combine tags 'dog' and 'kitten' into 'cat' and make sure that cat is now correlated with dogs and puppy.
+ $tags['cat']->combine_tags(array($tags['dog'], $tags['kitten']));
+
+ $correlatedtags = $this->get_correlated_tags_names($tags['cat']);
+ $this->assertEquals(['cats', 'dogs', 'puppy'], $correlatedtags);
+
+ $correlatedtags = $this->get_correlated_tags_names($tags['dogs']);
+ $this->assertEquals(['cat', 'puppy'], $correlatedtags);
+
+ $correlatedtags = $this->get_correlated_tags_names($tags['puppy']);
+ $this->assertEquals(['cat', 'dogs'], $correlatedtags);
+
+ // Add tag that does not have any correlations.
+ $user7 = $this->getDataGenerator()->create_user();
+ core_tag_tag::set_item_tags('core', 'user', $user7->id, context_user::instance($user7->id), array('hippo'));
+ $tags['hippo'] = core_tag_tag::get_by_name(core_tag_collection::get_default(), 'hippo', '*');
+
+ // Combine tag 'cat' into 'hippo'. Now 'hippo' should have the same correlations 'cat' used to have and also
+ // tags 'dogs' and 'puppy' should have 'hippo' in correlations.
+ $tags['hippo']->combine_tags(array($tags['cat']));
+
+ $correlatedtags = $this->get_correlated_tags_names($tags['hippo']);
+ $this->assertEquals(['cats', 'dogs', 'puppy'], $correlatedtags);
+
+ $correlatedtags = $this->get_correlated_tags_names($tags['dogs']);
+ $this->assertEquals(['hippo', 'puppy'], $correlatedtags);
+
+ $correlatedtags = $this->get_correlated_tags_names($tags['puppy']);
+ $this->assertEquals(['dogs', 'hippo'], $correlatedtags);
+ }
+
+ /**
+ * Help method to return sorted array of names of correlated tags to use for assertions
+ * @param core_tag $tag
+ * @return string
+ */
+ protected function get_correlated_tags_names($tag) {
+ $rv = array_map(function($t) {
+ return $t->rawname;
+ }, $tag->get_correlated_tags());
+ sort($rv);
+ return array_values($rv);
+ }
}