Merge branch 'MDL-61363-int-master' of git://github.com/ryanwyllie/moodle

This commit is contained in:
Jake Dallimore
2018-03-07 10:55:01 +08:00
12 changed files with 2290 additions and 93 deletions
+1
View File
@@ -391,6 +391,7 @@ $string['questionbehavioursdisabledexplained'] = 'Enter a comma separated list o
$string['questionbehavioursorder'] = 'Question behaviours order';
$string['questionbehavioursorderexplained'] = 'Enter a comma separated list of behaviours in the order you want them to appear in dropdown menu';
$string['questionidmismatch'] = 'Question ids mismatch';
$string['questionformtagheader'] = '{$a} tags';
$string['questionname'] = 'Question name';
$string['questionnamecopy'] = '{$a} (copy)';
$string['questionpreviewdefaults'] = 'Question preview defaults';
Regular → Executable
+3 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20171222" COMMENT="XMLDB file for core Moodle tables"
<XMLDB PATH="lib/db" VERSION="20180222" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
@@ -2019,6 +2019,7 @@
<FIELD NAME="callback" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="callbackfile" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="showstandard" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="multiplecontexts" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether the tag area allows tag instances to be created in multiple contexts."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
@@ -2081,7 +2082,7 @@
<KEY NAME="contextid" TYPE="foreign" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="taggeditem" UNIQUE="true" FIELDS="component, itemtype, itemid, tiuserid, tagid"/>
<INDEX NAME="taggeditem" UNIQUE="true" FIELDS="component, itemtype, itemid, contextid, tiuserid, tagid"/>
<INDEX NAME="taglookup" UNIQUE="false" FIELDS="itemtype, component, tagid, contextid"/>
</INDEXES>
</TABLE>
+1
View File
@@ -69,6 +69,7 @@ $tagareas = array(
array(
'itemtype' => 'question', // Questions.
'component' => 'core_question',
'multiplecontexts' => true,
),
array(
'itemtype' => 'post', // Blog posts.
+39 -1
View File
@@ -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;
}
+236 -16
View File
@@ -537,6 +537,120 @@ function question_delete_activity($cm, $feedback=true) {
return true;
}
/**
* This function will handle moving all tag instances to a new context for a
* given list of questions.
*
* Questions can be tagged in up to two contexts:
* 1.) The context the question exists in.
* 2.) The course context (if the question context is a higher context.
* E.g. course category context or system context.
*
* This means a question that exists in a higher context (e.g. course cat or
* system context) may have multiple groups of tags in any number of child
* course contexts.
*
* Questions in the course category context can be move "down" a context level
* into one of their child course contexts or activity contexts which affects the
* availability of that question in other courses / activities.
*
* In this case it makes the questions no longer available in the other course or
* activity contexts so we need to make sure that the tag instances in those other
* contexts are removed.
*
* @param stdClass[] $questions The list of question being moved (must include
* the id and contextid)
* @param context $newcontext The Moodle context the questions are being moved to
*/
function question_move_question_tags_to_new_context(array $questions, context $newcontext) {
// If the questions are moving to a new course/activity context then we need to
// find any existing tag instances from any unavailable course contexts and
// delete them because they will no longer be applicable (we don't support
// tagging questions across courses).
$instancestodelete = [];
$instancesfornewcontext = [];
$newcontextparentids = $newcontext->get_parent_context_ids();
$questionids = array_map(function($question) {
return $question->id;
}, $questions);
$questionstagobjects = core_tag_tag::get_items_tags('core_question', 'question', $questionids);
foreach ($questions as $question) {
$tagobjects = $questionstagobjects[$question->id];
foreach ($tagobjects as $tagobject) {
$tagid = $tagobject->taginstanceid;
$tagcontextid = $tagobject->taginstancecontextid;
$istaginnewcontext = $tagcontextid == $newcontext->id;
$istaginquestioncontext = $tagcontextid == $question->contextid;
if ($istaginnewcontext) {
// This tag instance is already in the correct context so we can
// ignore it.
continue;
}
if ($istaginquestioncontext) {
// This tag instance is in the question context so it needs to be
// updated.
$instancesfornewcontext[] = $tagid;
continue;
}
// These tag instances are in neither the new context nor the
// question context so we need to determine what to do based on
// the context they are in and the new question context.
$tagcontext = context::instance_by_id($tagcontextid);
$tagcoursecontext = $tagcontext->get_course_context(false);
// The tag is in a course context if get_course_context() returns
// itself.
$istaginstancecontextcourse = !empty($tagcoursecontext)
&& $tagcontext->id == $tagcoursecontext->id;
if ($istaginstancecontextcourse) {
// If the tag instance is in a course context we need to add some
// special handling.
$tagcontextparentids = $tagcontext->get_parent_context_ids();
$isnewcontextaparent = in_array($newcontext->id, $tagcontextparentids);
$isnewcontextachild = in_array($tagcontext->id, $newcontextparentids);
if ($isnewcontextaparent) {
// If the tag instance is a course context tag and the new
// context is still a parent context to the tag context then
// we can leave this tag where it is.
continue;
} else if ($isnewcontextachild) {
// If the new context is a child context (e.g. activity) of this
// tag instance then we should move all of this tag instance
// down into the activity context along with the question.
$instancesfornewcontext[] = $tagid;
} else {
// If the tag is in a course context that is no longer a parent
// or child of the new context then this tag instance should be
// removed.
$instancestodelete[] = $tagid;
}
} else {
// This is a catch all for any tag instances not in the question
// context or a course context. These tag instances should be
// updated to the new context id. This will clean up old invalid
// data.
$instancesfornewcontext[] = $tagid;
}
}
}
if (!empty($instancestodelete)) {
// Delete any course context tags that may no longer be valid.
core_tag_tag::delete_instances_by_id($instancestodelete);
}
if (!empty($instancesfornewcontext)) {
// Update the tag instances to the new context id.
core_tag_tag::change_instances_context($instancesfornewcontext, $newcontext);
}
}
/**
* This function should be considered private to the question bank, it is called from
* question/editlib.php question/contextmoveq.php and a few similar places to to the
@@ -573,8 +687,8 @@ function question_move_questions_to_category($questionids, $newcategoryid) {
$DB->set_field_select('question', 'category', $newcategoryid,
"parent $questionidcondition", $params);
// Update the contextid for any tag instances that may exist for these questions.
core_tag_tag::change_items_context('core_question', 'question', $questionids, $newcontextid);
$newcontext = context::instance_by_id($newcontextid);
question_move_question_tags_to_new_context($questions, $newcontext);
// TODO Deal with datasets.
@@ -597,6 +711,7 @@ function question_move_questions_to_category($questionids, $newcategoryid) {
function question_move_category_to_context($categoryid, $oldcontextid, $newcontextid) {
global $DB;
$questions = [];
$questionids = $DB->get_records_menu('question',
array('category' => $categoryid), '', 'id,qtype');
foreach ($questionids as $questionid => $qtype) {
@@ -604,10 +719,15 @@ function question_move_category_to_context($categoryid, $oldcontextid, $newconte
$questionid, $oldcontextid, $newcontextid);
// Purge this question from the cache.
question_bank::notify_question_edited($questionid);
$questions[] = (object) [
'id' => $questionid,
'contextid' => $oldcontextid
];
}
core_tag_tag::change_items_context('core_question', 'question',
array_keys($questionids), $newcontextid);
$newcontext = context::instance_by_id($newcontextid);
question_move_question_tags_to_new_context($questions, $newcontext);
$subcatids = $DB->get_records_menu('question_categories',
array('parent' => $categoryid), '', 'id,1');
@@ -770,9 +890,11 @@ function question_load_questions($questionids, $extrafields = '', $join = '') {
* Private function to factor common code out of get_question_options().
*
* @param object $question the question to tidy.
* @param boolean $loadtags load the question tags from the tags table. Optional, default false.
* @param stdClass $category The question_categories record for the given $question.
* @param stdClass[]|null $tagobjects The tags for the given $question.
* @param stdClass[]|null $filtercourses The courses to filter the course tags by.
*/
function _tidy_question($question, $loadtags = false) {
function _tidy_question($question, $category, array $tagobjects = null, array $filtercourses = null) {
global $CFG;
// Load question-type specific fields.
@@ -790,8 +912,76 @@ function _tidy_question($question, $loadtags = false) {
unset($question->_partiallyloaded);
}
if ($loadtags && core_tag_tag::is_enabled('core_question', 'question')) {
$question->tags = core_tag_tag::get_item_tags_array('core_question', 'question', $question->id);
$question->categoryobject = $category;
if (!is_null($tagobjects)) {
$categorycontext = context::instance_by_id($category->contextid);
// Questions can have two sets of tag instances. One set at the
// course context level and another at the context the question
// belongs to (e.g. course category, system etc).
$question->coursetagobjects = [];
$question->coursetags = [];
$question->tagobjects = [];
$question->tags = [];
$taginstanceidstonormalise = [];
$filtercoursecontextids = [];
$hasfiltercourses = !empty($filtercourses);
if ($hasfiltercourses) {
// If we're being asked to filter the course tags by a set of courses
// then get the context ids to filter below.
$filtercoursecontextids = array_map(function($course) {
$coursecontext = context_course::instance($course->id);
return $coursecontext->id;
}, $filtercourses);
}
foreach ($tagobjects as $tagobject) {
$tagcontextid = $tagobject->taginstancecontextid;
$tagcontext = context::instance_by_id($tagcontextid);
$tagcoursecontext = $tagcontext->get_course_context(false);
// This is a course tag if the tag context is a course context which
// doesn't match the question's context. Any tag in the question context
// is not considered a course tag, it belongs to the question.
$iscoursetag = $tagcoursecontext
&& $tagcontext->id == $tagcoursecontext->id
&& $tagcontext->id != $categorycontext->id;
if ($iscoursetag) {
// Any tag instance in a course context level is considered a course tag.
if (!$hasfiltercourses || in_array($tagcontextid, $filtercoursecontextids)) {
// Add the tag to the list of course tags if we aren't being
// asked to filter or if this tag is in the list of courses
// we're being asked to filter by.
$question->coursetagobjects[] = $tagobject;
$question->coursetags[$tagobject->id] = $tagobject->get_display_name();
}
} else {
// All non course context level tag instances or tags in the question
// context belong to the context that the question was created in.
$question->tagobjects[] = $tagobject;
$question->tags[$tagobject->id] = $tagobject->get_display_name();
// Due to legacy tag implementations that don't force the recording
// of a context id, some tag instances may have context ids that don't
// match either a course context or the question context. In this case
// we should take the opportunity to fix up the data and set the correct
// context id.
if ($tagcontext->id != $categorycontext->id) {
$taginstanceidstonormalise[] = $tagobject->taginstanceid;
// Update the object properties to reflect the DB update that will
// happen below.
$tagobject->taginstancecontextid = $categorycontext->id;
}
}
}
if (!empty($taginstanceidstonormalise)) {
// If we found any tag instances with incorrect context id data then we can
// correct those values now by setting them to the question context id.
core_tag_tag::change_instances_context($taginstanceidstonormalise, $categorycontext);
}
}
}
@@ -804,17 +994,47 @@ function _tidy_question($question, $loadtags = false) {
*
* @param mixed $questions Either an array of question objects to be updated
* or just a single question object
* @param boolean $loadtags load the question tags from the tags table. Optional, default false.
* @param bool $loadtags load the question tags from the tags table. Optional, default false.
* @param stdClass[] $filtercourses The courses to filter the course tags by.
* @return bool Indicates success or failure.
*/
function get_question_options(&$questions, $loadtags = false) {
if (is_array($questions)) { // deal with an array of questions
foreach ($questions as $i => $notused) {
_tidy_question($questions[$i], $loadtags);
}
} else { // deal with single question
_tidy_question($questions, $loadtags);
function get_question_options(&$questions, $loadtags = false, $filtercourses = null) {
global $DB;
$questionlist = is_array($questions) ? $questions : [$questions];
$categoryids = [];
$questionids = [];
if (empty($questionlist)) {
return true;
}
foreach ($questionlist as $question) {
$questionids[] = $question->id;
if (!in_array($question->category, $categoryids)) {
$categoryids[] = $question->category;
}
}
$categories = $DB->get_records_list('question_categories', 'id', $categoryids);
if ($loadtags && core_tag_tag::is_enabled('core_question', 'question')) {
$tagobjectsbyquestion = core_tag_tag::get_items_tags('core_question', 'question', $questionids);
} else {
$tagobjectsbyquestion = null;
}
foreach ($questionlist as $question) {
if (is_null($tagobjectsbyquestion)) {
$tagobjects = null;
} else {
$tagobjects = $tagobjectsbyquestion[$question->id];
}
_tidy_question($question, $categories[$question->category], $tagobjects, $filtercourses);
}
return true;
}
+855 -7
View File
@@ -22,6 +22,8 @@
* @copyright 2006 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core_tag\output\tag;
defined('MOODLE_INTERNAL') || die();
@@ -74,7 +76,10 @@ class core_questionlib_testcase extends advanced_testcase {
$category = $this->getDataGenerator()->create_category();
// Create course.
$course = $this->getDataGenerator()->create_course(array('numsections' => 5));
$course = $this->getDataGenerator()->create_course(array(
'numsections' => 5,
'category' => $category->id
));
$options = array(
'course' => $course->id,
@@ -86,12 +91,22 @@ class core_questionlib_testcase extends advanced_testcase {
$qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
if ('course' == $type) {
$context = context_course::instance($course->id);
} else if ('category' == $type) {
$context = context_coursecat::instance($category->id);
} else {
$context = context_module::instance($quiz->cmid);
switch ($type) {
case 'course':
$context = context_course::instance($course->id);
break;
case 'category':
$context = context_coursecat::instance($category->id);
break;
case 'system':
$context = context_system::instance();
break;
default:
$context = context_module::instance($quiz->cmid);
break;
}
$qcat = $qgen->create_question_category(array('contextid' => $context->id));
@@ -456,4 +471,837 @@ class core_questionlib_testcase extends advanced_testcase {
$this->assertFalse($DB->record_exists('question', ['id' => $q2c->id]));
$this->assertTrue($DB->record_exists('question', ['id' => $q2d->id]));
}
/**
* get_question_options should add the category object to the given question.
*/
public function test_get_question_options_includes_category_object_single_question() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
$question = array_shift($questions);
get_question_options($question);
$this->assertEquals($qcat, $question->categoryobject);
}
/**
* get_question_options should add the category object to all of the questions in
* the given list.
*/
public function test_get_question_options_includes_category_object_multiple_questions() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
get_question_options($questions);
foreach ($questions as $question) {
$this->assertEquals($qcat, $question->categoryobject);
}
}
/**
* get_question_options includes the tags for all questions in the list.
*/
public function test_get_question_options_includes_question_tags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
$question1 = $questions[0];
$question2 = $questions[1];
$qcontext = context::instance_by_id($qcat->contextid);
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['baz', 'bop']);
get_question_options($questions, true);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
$expectedtags = [];
$actualtags = $question->tags;
foreach ($tags as $tag) {
$expectedtags[$tag->id] = $tag->get_display_name();
}
// The question should have a tags property populated with each tag id
// and display name as a key vale pair.
$this->assertEquals($expectedtags, $actualtags);
$actualtagobjects = $question->tagobjects;
sort($tags);
sort($actualtagobjects);
// The question should have a full set of each tag object.
$this->assertEquals($tags, $actualtagobjects);
// The question should not have any course tags.
$this->assertEmpty($question->coursetagobjects);
}
}
/**
* get_question_options includes the course tags for all questions in the list.
*/
public function test_get_question_options_includes_course_tags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
$question1 = $questions[0];
$question2 = $questions[1];
$coursecontext = context_course::instance($course->id);
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo', 'bar']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['baz', 'bop']);
get_question_options($questions, true);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
$expectedcoursetags = [];
$actualcoursetags = $question->coursetags;
foreach ($tags as $tag) {
$expectedcoursetags[$tag->id] = $tag->get_display_name();
}
// The question should have a coursetags property populated with each tag id
// and display name as a key vale pair.
$this->assertEquals($expectedcoursetags, $actualcoursetags);
$actualcoursetagobjects = $question->coursetagobjects;
sort($tags);
sort($actualcoursetagobjects);
// The question should have a full set of the course tag objects.
$this->assertEquals($tags, $actualcoursetagobjects);
// The question should not have any other tags.
$this->assertEmpty($question->tagobjects);
$this->assertEmpty($question->tags);
}
}
/**
* get_question_options only categorises a tag as a course tag if it is in a
* course context that is different from the question context.
*/
public function test_get_question_options_course_tags_in_course_question_context() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
$question1 = $questions[0];
$question2 = $questions[1];
$coursecontext = context_course::instance($course->id);
// Create course level tags in the course context that matches the question
// course context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo', 'bar']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['baz', 'bop']);
get_question_options($questions, true);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
$actualtagobjects = $question->tagobjects;
sort($tags);
sort($actualtagobjects);
// The tags should not be considered course tags because they are in
// the same context as the question. That makes them question tags.
$this->assertEmpty($question->coursetagobjects);
// The course context tags should be returned in the regular tag object
// list.
$this->assertEquals($tags, $actualtagobjects);
}
}
/**
* get_question_options includes the tags and course tags for all questions in the list
* if each question has course and question level tags.
*/
public function test_get_question_options_includes_question_and_course_tags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
$question1 = $questions[0];
$question2 = $questions[1];
$qcontext = context::instance_by_id($qcat->contextid);
$coursecontext = context_course::instance($course->id);
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['cfoo', 'cbar']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['baz', 'bop']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['cbaz', 'cbop']);
get_question_options($questions, true);
foreach ($questions as $question) {
$alltags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
$tags = array_filter($alltags, function($tag) use ($qcontext) {
return $tag->taginstancecontextid == $qcontext->id;
});
$coursetags = array_filter($alltags, function($tag) use ($coursecontext) {
return $tag->taginstancecontextid == $coursecontext->id;
});
$expectedtags = [];
$actualtags = $question->tags;
foreach ($tags as $tag) {
$expectedtags[$tag->id] = $tag->get_display_name();
}
// The question should have a tags property populated with each tag id
// and display name as a key vale pair.
$this->assertEquals($expectedtags, $actualtags);
$actualtagobjects = $question->tagobjects;
sort($tags);
sort($actualtagobjects);
// The question should have a full set of each tag object.
$this->assertEquals($tags, $actualtagobjects);
$actualcoursetagobjects = $question->coursetagobjects;
sort($coursetags);
sort($actualcoursetagobjects);
// The question should have a full set of course tag objects.
$this->assertEquals($coursetags, $actualcoursetagobjects);
}
}
/**
* get_question_options should update the context id to the question category
* context id for any non-course context tag that isn't in the question category
* context.
*/
public function test_get_question_options_normalises_question_tags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
$question1 = $questions[0];
$question2 = $questions[1];
$qcontext = context::instance_by_id($qcat->contextid);
$systemcontext = context_system::instance();
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['baz', 'bop']);
$q1tags = core_tag_tag::get_item_tags('core_question', 'question', $question1->id);
$q2tags = core_tag_tag::get_item_tags('core_question', 'question', $question2->id);
$q1tag = array_shift($q1tags);
$q2tag = array_shift($q2tags);
// Change two of the tag instances to be a different (non-course) context to the
// question tag context. These tags should then be normalised back to the question
// tag context.
core_tag_tag::change_instances_context([$q1tag->taginstanceid, $q2tag->taginstanceid], $systemcontext);
get_question_options($questions, true);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
// The database should have been updated with the correct context id.
foreach ($tags as $tag) {
$this->assertEquals($qcontext->id, $tag->taginstancecontextid);
}
// The tag objects on the question should have been updated with the
// correct context id.
foreach ($question->tagobjects as $tag) {
$this->assertEquals($qcontext->id, $tag->taginstancecontextid);
}
}
}
/**
* get_question_options if the question is a course level question then tags
* in that context should not be consdered course tags, they are question tags.
*/
public function test_get_question_options_includes_course_context_question_tags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
$question1 = $questions[0];
$question2 = $questions[1];
$coursecontext = context_course::instance($course->id);
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo', 'bar']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['baz', 'bop']);
get_question_options($questions, true);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
// Tags in a course context that matches the question context should
// not be considered course tags.
$this->assertEmpty($question->coursetagobjects);
$this->assertEmpty($question->coursetags);
$actualtagobjects = $question->tagobjects;
sort($tags);
sort($actualtagobjects);
// The tags should be considered question tags not course tags.
$this->assertEquals($tags, $actualtagobjects);
}
}
/**
* get_question_options should return tags from all course contexts by default.
*/
public function test_get_question_options_includes_multiple_courses_tags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
$question1 = $questions[0];
$question2 = $questions[1];
$coursecontext = context_course::instance($course->id);
// Create a sibling course.
$siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
$siblingcoursecontext = context_course::instance($siblingcourse->id);
// Create course tags.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['c1']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['c1']);
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['c2']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['c2']);
get_question_options($questions, true);
foreach ($questions as $question) {
$this->assertCount(2, $question->coursetagobjects);
foreach ($question->coursetagobjects as $tag) {
if ($tag->name == 'c1') {
$this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
} else {
$this->assertEquals($siblingcoursecontext->id, $tag->taginstancecontextid);
}
}
}
}
/**
* get_question_options should filter the course tags by the given list of courses.
*/
public function test_get_question_options_includes_filter_course_tags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
$question1 = $questions[0];
$question2 = $questions[1];
$coursecontext = context_course::instance($course->id);
// Create a sibling course.
$siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
$siblingcoursecontext = context_course::instance($siblingcourse->id);
// Create course tags.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['foo']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['bar']);
// Create sibling course tags. These should be filtered out.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['filtered1']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['filtered2']);
// Ask to only receive course tags from $course (ignoring $siblingcourse tags).
get_question_options($questions, true, [$course]);
foreach ($questions as $question) {
foreach ($question->coursetagobjects as $tag) {
// We should only be seeing course tags from $course. The tags from
// $siblingcourse should have been filtered out.
$this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
}
}
}
/**
* question_move_question_tags_to_new_context should update all of the
* question tags contexts when they are moving down (from system to course
* category context).
*/
public function test_question_move_question_tags_to_new_context_system_to_course_cat_qtags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('system');
$question1 = $questions[0];
$question2 = $questions[1];
$qcontext = context::instance_by_id($qcat->contextid);
$newcontext = context_coursecat::instance($category->id);
foreach ($questions as $question) {
$question->contextid = $qcat->contextid;
}
// Create tags in the system context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo', 'bar']);
question_move_question_tags_to_new_context($questions, $newcontext);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
// All of the tags should have their context id set to the new context.
foreach ($tags as $tag) {
$this->assertEquals($newcontext->id, $tag->taginstancecontextid);
}
}
}
/**
* question_move_question_tags_to_new_context should update all of the question tags
* contexts when they are moving down (from system to course category context)
* but leave any tags in the course context where they are.
*/
public function test_question_move_question_tags_to_new_context_system_to_course_cat_qtags_and_course_tags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('system');
$question1 = $questions[0];
$question2 = $questions[1];
$qcontext = context::instance_by_id($qcat->contextid);
$coursecontext = context_course::instance($course->id);
$newcontext = context_coursecat::instance($category->id);
foreach ($questions as $question) {
$question->contextid = $qcat->contextid;
}
// Create tags in the system context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
// Create tags in the course context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
question_move_question_tags_to_new_context($questions, $newcontext);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
foreach ($tags as $tag) {
if ($tag->name == 'ctag') {
// Course tags should remain in the course context.
$this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
} else {
// Other tags should be updated.
$this->assertEquals($newcontext->id, $tag->taginstancecontextid);
}
}
}
}
/**
* question_move_question_tags_to_new_context should update all of the question
* contexts tags when they are moving up (from course category to system context).
*/
public function test_question_move_question_tags_to_new_context_course_cat_to_system_qtags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
$question1 = $questions[0];
$question2 = $questions[1];
$qcontext = context::instance_by_id($qcat->contextid);
$newcontext = context_system::instance();
foreach ($questions as $question) {
$question->contextid = $qcat->contextid;
}
// Create tags in the course category context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo', 'bar']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo', 'bar']);
question_move_question_tags_to_new_context($questions, $newcontext);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
// All of the tags should have their context id set to the new context.
foreach ($tags as $tag) {
$this->assertEquals($newcontext->id, $tag->taginstancecontextid);
}
}
}
/**
* question_move_question_tags_to_new_context should update all of the question
* tags contexts when they are moving up (from course category context to system
* context) but leave any tags in the course context where they are.
*/
public function test_question_move_question_tags_to_new_context_course_cat_to_system_qtags_and_course_tags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
$question1 = $questions[0];
$question2 = $questions[1];
$qcontext = context::instance_by_id($qcat->contextid);
$coursecontext = context_course::instance($course->id);
$newcontext = context_system::instance();
foreach ($questions as $question) {
$question->contextid = $qcat->contextid;
}
// Create tags in the system context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
// Create tags in the course context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
question_move_question_tags_to_new_context($questions, $newcontext);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
foreach ($tags as $tag) {
if ($tag->name == 'ctag') {
// Course tags should remain in the course context.
$this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
} else {
// Other tags should be updated.
$this->assertEquals($newcontext->id, $tag->taginstancecontextid);
}
}
}
}
/**
* question_move_question_tags_to_new_context should merge all tags into the course
* context when moving down from course category context into course context.
*/
public function test_question_move_question_tags_to_new_context_course_cat_to_coures_qtags_and_course_tags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
$question1 = $questions[0];
$question2 = $questions[1];
$qcontext = context::instance_by_id($qcat->contextid);
$coursecontext = context_course::instance($course->id);
$newcontext = $coursecontext;
foreach ($questions as $question) {
$question->contextid = $qcat->contextid;
}
// Create tags in the system context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
// Create tags in the course context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
question_move_question_tags_to_new_context($questions, $newcontext);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
// Each question should have 2 tags.
$this->assertCount(2, $tags);
foreach ($tags as $tag) {
// All tags should be updated to the course context and merged in.
$this->assertEquals($newcontext->id, $tag->taginstancecontextid);
}
}
}
/**
* question_move_question_tags_to_new_context should delete all of the tag
* instances from sibling courses when moving the context of a question down
* from a course category into a course context because the other courses will
* no longer have access to the question.
*/
public function test_question_move_question_tags_to_new_context_remove_other_course_tags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
// Create a sibling course.
$siblingcourse = $this->getDataGenerator()->create_course(['category' => $course->category]);
$question1 = $questions[0];
$question2 = $questions[1];
$qcontext = context::instance_by_id($qcat->contextid);
$coursecontext = context_course::instance($course->id);
$siblingcoursecontext = context_course::instance($siblingcourse->id);
$newcontext = $coursecontext;
foreach ($questions as $question) {
$question->contextid = $qcat->contextid;
}
// Create tags in the system context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
// Create tags in the target course context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
// Create tags in the sibling course context. These should be deleted as
// part of the move.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $siblingcoursecontext, ['stag']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $siblingcoursecontext, ['stag']);
question_move_question_tags_to_new_context($questions, $newcontext);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
// Each question should have 2 tags, 'foo' and 'ctag'.
$this->assertCount(2, $tags);
foreach ($tags as $tag) {
$tagname = $tag->name;
// The 'stag' should have been deleted because it's in a sibling
// course context.
$this->assertContains($tagname, ['foo', 'ctag']);
// All tags should be in the course context now.
$this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
}
}
}
/**
* question_move_question_tags_to_new_context should update all of the question
* tags to be the course category context when moving the tags from a course
* context to a course category context.
*/
public function test_question_move_question_tags_to_new_context_course_to_course_cat() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
$question1 = $questions[0];
$question2 = $questions[1];
$qcontext = context::instance_by_id($qcat->contextid);
// Moving up into the course category context.
$newcontext = context_coursecat::instance($category->id);
foreach ($questions as $question) {
$question->contextid = $qcat->contextid;
}
// Create tags in the course context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
question_move_question_tags_to_new_context($questions, $newcontext);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
// All of the tags should have their context id set to the new context.
foreach ($tags as $tag) {
$this->assertEquals($newcontext->id, $tag->taginstancecontextid);
}
}
}
/**
* question_move_question_tags_to_new_context should update all of the
* question tags contexts when they are moving down (from system to course
* category context).
*/
public function test_question_move_question_tags_to_new_context_orphaned_tag_contexts() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('system');
$question1 = $questions[0];
$question2 = $questions[1];
$othercategory = $this->getDataGenerator()->create_category();
$qcontext = context::instance_by_id($qcat->contextid);
$newcontext = context_coursecat::instance($category->id);
$othercategorycontext = context_coursecat::instance($othercategory->id);
foreach ($questions as $question) {
$question->contextid = $qcat->contextid;
}
// Create tags in the system context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
// Create tags in the other course category context. These should be
// update to the next context id because they represent erroneous data
// from a time before context id was mandatory in the tag API.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $othercategorycontext, ['bar']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $othercategorycontext, ['bar']);
question_move_question_tags_to_new_context($questions, $newcontext);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
// Each question should have two tags, 'foo' and 'bar'.
$this->assertCount(2, $tags);
// All of the tags should have their context id set to the new context
// (course category context).
foreach ($tags as $tag) {
$this->assertEquals($newcontext->id, $tag->taginstancecontextid);
}
}
}
/**
* When moving from a course category context down into an activity context
* all question context tags and course tags (where the course is a parent of
* the activity) should move into the new context.
*/
public function test_question_move_question_tags_to_new_context_course_cat_to_activity_qtags_and_course_tags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
$question1 = $questions[0];
$question2 = $questions[1];
$qcontext = context::instance_by_id($qcat->contextid);
$coursecontext = context_course::instance($course->id);
$newcontext = context_module::instance($quiz->cmid);
foreach ($questions as $question) {
$question->contextid = $qcat->contextid;
}
// Create tags in the course category context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
// Move the questions to the activity context which is a child context of
// $coursecontext.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
question_move_question_tags_to_new_context($questions, $newcontext);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
// Each question should have 2 tags.
$this->assertCount(2, $tags);
foreach ($tags as $tag) {
$this->assertEquals($newcontext->id, $tag->taginstancecontextid);
}
}
}
/**
* When moving from a course category context down into an activity context
* all question context tags and course tags (where the course is a parent of
* the activity) should move into the new context. Tags in course contexts
* that are not a parent of the activity context should be deleted.
*/
public function test_question_move_question_tags_to_new_context_course_cat_to_activity_orphaned_tags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('category');
$question1 = $questions[0];
$question2 = $questions[1];
$qcontext = context::instance_by_id($qcat->contextid);
$coursecontext = context_course::instance($course->id);
$newcontext = context_module::instance($quiz->cmid);
$othercourse = $this->getDataGenerator()->create_course();
$othercoursecontext = context_course::instance($othercourse->id);
foreach ($questions as $question) {
$question->contextid = $qcat->contextid;
}
// Create tags in the course category context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
// Create tags in the course context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag']);
// Create tags in the other course context. These should be deleted.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $othercoursecontext, ['delete']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $othercoursecontext, ['delete']);
// Move the questions to the activity context which is a child context of
// $coursecontext.
question_move_question_tags_to_new_context($questions, $newcontext);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
// Each question should have 2 tags.
$this->assertCount(2, $tags);
foreach ($tags as $tag) {
// Make sure we don't have any 'delete' tags.
$this->assertContains($tag->name, ['foo', 'ctag']);
$this->assertEquals($newcontext->id, $tag->taginstancecontextid);
}
}
}
/**
* When moving from a course context down into an activity context all of the
* course tags should move into the activity context.
*/
public function test_question_move_question_tags_to_new_context_course_to_activity_qtags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('course');
$question1 = $questions[0];
$question2 = $questions[1];
$qcontext = context::instance_by_id($qcat->contextid);
$newcontext = context_module::instance($quiz->cmid);
foreach ($questions as $question) {
$question->contextid = $qcat->contextid;
}
// Create tags in the course context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
question_move_question_tags_to_new_context($questions, $newcontext);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
foreach ($tags as $tag) {
$this->assertEquals($newcontext->id, $tag->taginstancecontextid);
}
}
}
/**
* When moving from a course context down into an activity context all of the
* course tags should move into the activity context.
*/
public function test_question_move_question_tags_to_new_context_activity_to_course_qtags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
$question1 = $questions[0];
$question2 = $questions[1];
$qcontext = context::instance_by_id($qcat->contextid);
$newcontext = context_course::instance($course->id);
foreach ($questions as $question) {
$question->contextid = $qcat->contextid;
}
// Create tags in the activity context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
question_move_question_tags_to_new_context($questions, $newcontext);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
foreach ($tags as $tag) {
$this->assertEquals($newcontext->id, $tag->taginstancecontextid);
}
}
}
/**
* question_move_question_tags_to_new_context should update all of the
* question tags contexts when they are moving down (from system to course
* category context).
*
* Course tags within the new category context should remain while any course
* tags in course contexts that can no longer access the question should be
* deleted.
*/
public function test_question_move_question_tags_to_new_context_system_to_course_cat_with_orphaned_tags() {
list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions('system');
$question1 = $questions[0];
$question2 = $questions[1];
$othercategory = $this->getDataGenerator()->create_category();
$othercourse = $this->getDataGenerator()->create_course(['category' => $othercategory->id]);
$qcontext = context::instance_by_id($qcat->contextid);
$newcontext = context_coursecat::instance($category->id);
$othercategorycontext = context_coursecat::instance($othercategory->id);
$coursecontext = context_course::instance($course->id);
$othercoursecontext = context_course::instance($othercourse->id);
foreach ($questions as $question) {
$question->contextid = $qcat->contextid;
}
// Create tags in the system context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['foo']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['foo']);
// Create tags in the child course context of the new context.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['bar']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['bar']);
// Create tags in the other course context. These should be deleted when
// the question moves to the new course category context because this
// course belongs to a different category, which means it will no longer
// have access to the question.
core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $othercoursecontext, ['delete']);
core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $othercoursecontext, ['delete']);
question_move_question_tags_to_new_context($questions, $newcontext);
foreach ($questions as $question) {
$tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
// Each question should have two tags, 'foo' and 'bar'.
$this->assertCount(2, $tags);
// All of the tags should have their context id set to the new context
// (course category context).
foreach ($tags as $tag) {
$this->assertContains($tag->name, ['foo', 'bar']);
if ($tag->name == 'foo') {
$this->assertEquals($newcontext->id, $tag->taginstancecontextid);
} else {
$this->assertEquals($coursecontext->id, $tag->taginstancecontextid);
}
}
}
}
}
+20 -5
View File
@@ -120,7 +120,10 @@ if ($id) {
if (!$question = $DB->get_record('question', array('id' => $id))) {
print_error('questiondoesnotexist', 'question', $returnurl);
}
get_question_options($question, true);
// We can use $COURSE here because it's been initialised as part of the
// require_login above. Passing it as the third parameter tells the function
// to filter the course tags by that course.
get_question_options($question, true, [$COURSE]);
} else if ($categoryid && $qtype) { // only for creating new questions
$question = new stdClass();
@@ -146,9 +149,13 @@ if ($id) {
$qtypeobj = question_bank::get_qtype($question->qtype);
// Validate the question category.
if (!$category = $DB->get_record('question_categories', array('id' => $question->category))) {
print_error('categorydoesnotexist', 'question', $returnurl);
if (isset($question->categoryobject)) {
$category = $question->categoryobject;
} else {
// Validate the question category.
if (!$category = $DB->get_record('question_categories', array('id' => $question->category))) {
print_error('categorydoesnotexist', 'question', $returnurl);
}
}
// Check permissions
@@ -261,10 +268,18 @@ if ($mform->is_cancelled()) {
print_error('nopermissions', '', '', 'edit');
}
}
$question = $qtypeobj->save_question($question, $fromform);
if (isset($fromform->tags)) {
// If we have any question context level tags then set those tags now.
core_tag_tag::set_item_tags('core_question', 'question', $question->id,
context::instance_by_id($contextid), $fromform->tags);
context::instance_by_id($contextid), $fromform->tags, 0);
}
if (isset($fromform->coursetags)) {
// If we have and course context level tags then set those now.
core_tag_tag::set_item_tags('core_question', 'question', $question->id,
context_course::instance($fromform->courseid), $fromform->coursetags, 0);
}
// Purge this question from the cache.
+48 -7
View File
@@ -202,13 +202,7 @@ abstract class question_edit_form extends question_wizard_form {
$this->definition_inner($mform);
if (core_tag_tag::is_enabled('core_question', 'question')) {
$mform->addElement('header', 'tagsheader', get_string('tags'));
$mform->addElement('tags', 'tags', get_string('tags'),
array('itemtype' => 'question', 'component' => 'core_question'));
if (!question_has_capability_on($this->question, 'tag')) {
$mform->hardFreeze('tags');
}
$this->add_tag_fields($mform);
}
if (!empty($this->question->id)) {
@@ -309,6 +303,53 @@ abstract class question_edit_form extends question_wizard_form {
return $repeated;
}
/**
* Add the tag and course tag fields to the mform.
*
* If the form is being built in a course context then add the field
* for course tags.
*
* If the question category doesn't belong to a course context or we
* aren't editing in a course context then add the tags element to allow
* tags to be added to the question category context.
*
* @param object $mform The form being built
*/
protected function add_tag_fields($mform) {
$hastagcapability = question_has_capability_on($this->question, 'tag');
// Is the question category in a course context?
$qcontext = $this->categorycontext;
$qcoursecontext = $qcontext->get_course_context(false);
$iscourseoractivityquestion = !empty($qcoursecontext);
// Is the current context we're editing in a course context?
$editingcontext = $this->contexts->lowest();
$editingcoursecontext = $editingcontext->get_course_context(false);
$iseditingcontextcourseoractivity = !empty($editingcoursecontext);
$mform->addElement('header', 'tagsheader', get_string('tags'));
$mform->addElement('tags', 'tags', get_string('tags'),
array('itemtype' => 'question', 'component' => 'core_question'));
if (!$hastagcapability) {
$mform->hardFreeze('tags');
}
if ($iseditingcontextcourseoractivity && !$iscourseoractivityquestion) {
// If the question is being edited in a course or activity context
// and the question isn't a course or activity level question then
// allow course tags to be added to the course.
$coursetagheader = get_string('questionformtagheader', 'core_question',
$editingcoursecontext->get_context_name(true));
$mform->addElement('header', 'coursetagsheader', $coursetagheader);
$mform->addElement('tags', 'coursetags', get_string('tags'),
array('itemtype' => 'question', 'component' => 'core_question'));
if (!$hastagcapability) {
$mform->hardFreeze('coursetags');
}
}
}
/**
* Add a set of form fields, obtained from get_per_answer_fields, to the form,
* one for each existing answer, with some blanks for some new ones.
+33 -2
View File
@@ -122,6 +122,32 @@ class core_tag_area {
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 @@ class core_tag_area {
'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 @@ class core_tag_area {
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 @@ class core_tag_area {
if (!isset($record->callbackfile)) {
$record->callbackfile = null;
}
if (!isset($record->multiplecontexts)) {
$record->multiplecontexts = false;
}
$itemtypes[$record->itemtype . ':' . $record->component] = $record;
}
}
+182 -52
View File
@@ -499,6 +499,64 @@ class core_tag_tag {
$this->delete_instance_as_record($taginstance, true);
}
/**
* Bulk delete all tag instances.
*
* @param stdClass[] $taginstances A list of tag_instance records to delete. Each
* record must also contain the name and rawname
* columns from the related tag record.
*/
public static function delete_instances_as_record(array $taginstances) {
global $DB;
if (empty($taginstances)) {
return;
}
$taginstanceids = array_map(function($taginstance) {
return $taginstance->id;
}, $taginstances);
// Now remove all the tag instances.
$DB->delete_records_list('tag_instance', 'id', $taginstanceids);
// 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();
}
}
/**
* Bulk delete all tag instances by tag id.
*
* @param int[] $taginstanceids List of tag instance ids to be deleted.
*/
public static function delete_instances_by_id(array $taginstanceids) {
global $DB;
if (empty($taginstanceids)) {
return;
}
list($idsql, $params) = $DB->get_in_or_equal($taginstanceids);
$sql = "SELECT ti.*, t.name, t.rawname, t.isstandard
FROM {tag_instance} ti
JOIN {tag} t
ON ti.tagid = t.id
WHERE ti.id {$idsql}";
if ($taginstances = $DB->get_records_sql($sql, $params)) {
static::delete_instances_as_record($taginstances);
}
}
/**
* Bulk delete all tag instances for a component or tag area
*
@@ -523,22 +581,9 @@ class core_tag_tag {
$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();
}
if ($taginstances = $DB->get_records_sql($sql, $params)) {
static::delete_instances_as_record($taginstances);
}
}
@@ -557,7 +602,7 @@ class core_tag_tag {
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;
@@ -592,6 +637,62 @@ class core_tag_tag {
$DB->update_record('tag_instance', $data);
}
/**
* Get the array of core_tag_tag objects associated with a list of items.
*
* 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[] $itemids
* @param int $standardonly wether to return only standard tags or any
* @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_items_tags($component, $itemtype, $itemids, $standardonly = self::BOTH_STANDARD_AND_NOT,
$tiuserid = 0) {
global $DB;
if (static::is_enabled($component, $itemtype) === false) {
// Tagging area is properly defined but not enabled - return empty array.
return array();
}
if (empty($itemids)) {
return array();
}
$standardonly = (int)$standardonly; // In case somebody passed bool.
list($idsql, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
// 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, ti.itemid
FROM {tag_instance} ti
JOIN {tag} tg ON tg.id = ti.tagid
WHERE ti.itemtype = :itemtype AND ti.itemid $idsql ".
($component ? "AND ti.component = :component " : "").
($tiuserid ? "AND ti.tiuserid = :tiuserid " : "").
(($standardonly == self::STANDARD_ONLY) ? "AND tg.isstandard = 1 " : "").
(($standardonly == self::NOT_STANDARD_ONLY) ? "AND tg.isstandard = 0 " : "").
"ORDER BY ti.ordering ASC, ti.id";
$params['itemtype'] = $itemtype;
$params['component'] = $component;
$params['tiuserid'] = $tiuserid;
$records = $DB->get_records_sql($sql, $params);
$result = array();
foreach ($itemids as $itemid) {
$result[$itemid] = [];
}
foreach ($records as $id => $record) {
$result[$record->itemid][$id] = new static($record);
}
return $result;
}
/**
* Get the array of core_tag_tag objects associated with an item (instances).
*
@@ -607,39 +708,8 @@ class core_tag_tag {
*/
public static function get_item_tags($component, $itemtype, $itemid, $standardonly = self::BOTH_STANDARD_AND_NOT,
$tiuserid = 0) {
global $DB;
if (static::is_enabled($component, $itemtype) === false) {
// Tagging area is properly defined but not enabled - return empty array.
return array();
}
$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 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
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 " : "").
(($standardonly == self::STANDARD_ONLY) ? "AND tg.isstandard = 1 " : "").
(($standardonly == self::NOT_STANDARD_ONLY) ? "AND tg.isstandard = 0 " : "").
"ORDER BY ti.ordering ASC, ti.id";
$params = array();
$params['itemtype'] = $itemtype;
$params['itemid'] = $itemid;
$params['component'] = $component;
$params['tiuserid'] = $tiuserid;
$records = $DB->get_records_sql($sql, $params);
$result = array();
foreach ($records as $id => $record) {
$result[$id] = new static($record);
}
return $result;
$tagobjects = static::get_items_tags($component, $itemtype, [$itemid], $standardonly, $tiuserid);
return empty($tagobjects) ? [] : $tagobjects[$itemid];
}
/**
@@ -703,12 +773,35 @@ class core_tag_tag {
$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);
@@ -716,11 +809,30 @@ class core_tag_tag {
}
}
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);
}
@@ -888,10 +1000,28 @@ class core_tag_tag {
if ($newcontext instanceof context) {
$newcontext = $newcontext->id;
}
$DB->set_field_select('tag_instance', 'contextid', $newcontext,
'component = :component AND itemtype = :itemtype AND itemid ' . $sql, $params);
}
/**
* Moves all of the specified tag instances into a new context.
*
* @param array $taginstanceids The list of tag instance ids that should be moved
* @param context $newcontext The context to move the tag instances into
*/
public static function change_instances_context(array $taginstanceids, context $newcontext) {
global $DB;
if (empty($taginstanceids)) {
return;
}
list($sql, $params) = $DB->get_in_or_equal($taginstanceids);
$DB->set_field_select('tag_instance', 'contextid', $newcontext->id, "id {$sql}", $params);
}
/**
* Updates the information about the tag
*
@@ -1113,7 +1243,7 @@ class core_tag_tag {
// 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.isstandard, tg.name, tg.rawname, tg.flag,
tg.tagcollid, ti.ordering, ti.contextid AS taginstancecontextid
tg.tagcollid, ti.ordering, ti.contextid AS taginstancecontextid, ti.itemid
FROM {tag} tg
INNER JOIN {tag_instance} ti ON tg.id = ti.tagid
WHERE tg.id $query AND tg.id <> ? AND tg.tagcollid = ?
+871
View File
@@ -1172,6 +1172,852 @@ class core_tag_taglib_testcase extends advanced_testcase {
$this->assertEquals($expected, $actual);
}
/**
* get_items_tags should return an empty array if the tag area is disabled.
*/
public function test_get_items_tags_disabled_component() {
global $CFG;
$user1 = $this->getDataGenerator()->create_user();
$context1 = context_user::instance($user1->id);
$component = 'core';
$itemtype = 'user';
$itemids = [$user1->id];
// User 1 tags: 'foo', 'bar'.
core_tag_tag::set_item_tags($component, $itemtype, $user1->id, $context1, ['foo']);
// This mimics disabling tags for a component.
$CFG->usetags = false;
$result = core_tag_tag::get_items_tags($component, $itemtype, $itemids);
$this->assertEmpty($result);
}
/**
* get_items_tags should return an empty array if the tag item ids list
* is empty.
*/
public function test_get_items_tags_empty_itemids() {
$user1 = $this->getDataGenerator()->create_user();
$context1 = context_user::instance($user1->id);
$component = 'core';
$itemtype = 'user';
// User 1 tags: 'foo', 'bar'.
core_tag_tag::set_item_tags($component, $itemtype, $user1->id, $context1, ['foo']);
$result = core_tag_tag::get_items_tags($component, $itemtype, []);
$this->assertEmpty($result);
}
/**
* get_items_tags should return an array indexed by the item ids with empty
* arrays as the values when the component or itemtype is unknown.
*/
public function test_get_items_tags_unknown_component_itemtype() {
$itemids = [1, 2, 3];
$result = core_tag_tag::get_items_tags('someunknowncomponent', 'user', $itemids);
foreach ($itemids as $itemid) {
// Unknown component should return an array indexed by the item ids
// with empty arrays as the values.
$this->assertEmpty($result[$itemid]);
}
$result = core_tag_tag::get_items_tags('core', 'someunknownitemtype', $itemids);
foreach ($itemids as $itemid) {
// Unknown item type should return an array indexed by the item ids
// with empty arrays as the values.
$this->assertEmpty($result[$itemid]);
}
}
/**
* get_items_tags should return an array indexed by the item ids with empty
* arrays as the values for any item ids that don't have tag instances.
*
* Data setup:
* Users: 1, 2, 3
* Tags: user 1 = ['foo', 'bar']
* user 2 = ['baz', 'bop']
* user 3 = []
*
* Expected result:
* [
* 1 => [
* 1 => 'foo',
* 2 => 'bar'
* ],
* 2 => [
* 3 => 'baz',
* 4 => 'bop'
* ],
* 3 => []
* ]
*/
public function test_get_items_tags_missing_itemids() {
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user();
$context1 = context_user::instance($user1->id);
$context2 = context_user::instance($user2->id);
$component = 'core';
$itemtype = 'user';
$itemids = [$user1->id, $user2->id, $user3->id];
$expecteduser1tagnames = ['foo', 'bar'];
$expecteduser2tagnames = ['baz', 'bop'];
$expecteduser3tagnames = [];
// User 1 tags: 'foo', 'bar'.
core_tag_tag::set_item_tags($component, $itemtype, $user1->id, $context1, $expecteduser1tagnames);
// User 2 tags: 'bar', 'baz'.
core_tag_tag::set_item_tags($component, $itemtype, $user2->id, $context2, $expecteduser2tagnames);
$result = core_tag_tag::get_items_tags($component, $itemtype, $itemids);
$actualuser1tagnames = array_map(function($taginstance) {
return $taginstance->name;
}, $result[$user1->id]);
$actualuser2tagnames = array_map(function($taginstance) {
return $taginstance->name;
}, $result[$user2->id]);
$actualuser3tagnames = $result[$user3->id];
sort($expecteduser1tagnames);
sort($expecteduser2tagnames);
sort($actualuser1tagnames);
sort($actualuser2tagnames);
$this->assertEquals($expecteduser1tagnames, $actualuser1tagnames);
$this->assertEquals($expecteduser2tagnames, $actualuser2tagnames);
$this->assertEquals($expecteduser3tagnames, $actualuser3tagnames);
}
/**
* set_item_tags should remove any tags that aren't in the given list and should
* add any instances that are missing.
*/
public function test_set_item_tags_no_multiple_context_add_remove_instances() {
$tagnames = ['foo', 'bar', 'baz', 'bop'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user1 = $this->getDataGenerator()->create_user();
$context = context_user::instance($user1->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
$tagareas = core_tag_area::get_areas();
$tagarea = $tagareas[$itemtype][$component];
$newtagnames = ['bar', 'baz', 'bop'];
// Make sure the tag area doesn't allow multiple contexts.
core_tag_area::update($tagarea, ['multiplecontexts' => false]);
// Create tag instances in separate contexts.
$this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
$this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context);
core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context, $newtagnames);
$result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
$actualtagnames = array_map(function($record) {
return $record->name;
}, $result);
sort($newtagnames);
sort($actualtagnames);
// The list of tags should match the $newtagnames which means 'foo'
// should have been removed while 'baz' and 'bop' were added. 'bar'
// should remain as it was in the new list of tags.
$this->assertEquals($newtagnames, $actualtagnames);
}
/**
* set_item_tags should set all of the tag instance context ids to the given
* context if the tag area for the items doesn't allow multiple contexts for
* the tag instances.
*/
public function test_set_item_tags_no_multiple_context_updates_context_of_instances() {
$tagnames = ['foo', 'bar'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$context1 = context_user::instance($user1->id);
$context2 = context_user::instance($user2->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
$tagareas = core_tag_area::get_areas();
$tagarea = $tagareas[$itemtype][$component];
// Make sure the tag area doesn't allow multiple contexts.
core_tag_area::update($tagarea, ['multiplecontexts' => false]);
// Create tag instances in separate contexts.
$this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
$this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context2);
core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context1, $tagnames);
$result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
$this->assertCount(count($tagnames), $result);
foreach ($result as $tag) {
// The core user tag area doesn't allow multiple contexts for tag instances
// so set_item_tags should have set all of the tag instance context ids
// to match $context1.
$this->assertEquals($context1->id, $tag->taginstancecontextid);
}
}
/**
* set_item_tags should delete all of the tag instances that don't match
* the new set of tags, regardless of the context that the tag instance
* is in.
*/
public function test_set_item_tags_no_multiple_contex_deletes_old_instancest() {
$tagnames = ['foo', 'bar', 'baz', 'bop'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$context1 = context_user::instance($user1->id);
$context2 = context_user::instance($user2->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
$expectedtagnames = ['foo', 'baz'];
$tagareas = core_tag_area::get_areas();
$tagarea = $tagareas[$itemtype][$component];
// Make sure the tag area doesn't allow multiple contexts.
core_tag_area::update($tagarea, ['multiplecontexts' => false]);
// Create tag instances in separate contexts.
$this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
$this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context1);
$this->add_tag_instance($tags['baz'], $component, $itemtype, $itemid, $context2);
$this->add_tag_instance($tags['bop'], $component, $itemtype, $itemid, $context2);
core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context1, $expectedtagnames);
$result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
$actualtagnames = array_map(function($record) {
return $record->name;
}, $result);
sort($expectedtagnames);
sort($actualtagnames);
// The list of tags should match the $expectedtagnames.
$this->assertEquals($expectedtagnames, $actualtagnames);
foreach ($result as $tag) {
// The core user tag area doesn't allow multiple contexts for tag instances
// so set_item_tags should have set all of the tag instance context ids
// to match $context1.
$this->assertEquals($context1->id, $tag->taginstancecontextid);
}
}
/**
* set_item_tags should not change tag instances in a different context to the one
* it's opertating on if the tag area allows instances from multiple contexts.
*/
public function test_set_item_tags_allow_multiple_context_doesnt_update_context() {
global $DB;
$tagnames = ['foo', 'bar', 'bop'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$context1 = context_user::instance($user1->id);
$context2 = context_user::instance($user2->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
$tagareas = core_tag_area::get_areas();
$tagarea = $tagareas[$itemtype][$component];
// Make sure the tag area allows multiple contexts.
core_tag_area::update($tagarea, ['multiplecontexts' => true]);
// Create tag instances in separate contexts.
$this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
$this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context2);
// Set the list of tags for $context1. This includes a tag that already exists
// in that context and a new tag. There is another tag, 'bar', that exists in a
// different context ($context2) that should be ignored.
core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context1, ['foo', 'bop']);
$result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
$actualtagnames = array_map(function($record) {
return $record->name;
}, $result);
sort($tagnames);
sort($actualtagnames);
// The list of tags should match the $tagnames.
$this->assertEquals($tagnames, $actualtagnames);
foreach ($result as $tag) {
if ($tag->name == 'bar') {
// The tag instance for 'bar' should have been left untouched
// because it was in a different context.
$this->assertEquals($context2->id, $tag->taginstancecontextid);
} else {
$this->assertEquals($context1->id, $tag->taginstancecontextid);
}
}
}
/**
* set_item_tags should delete all of the tag instances that don't match
* the new set of tags only in the same context if the tag area allows
* multiple contexts.
*/
public function test_set_item_tags_allow_multiple_context_deletes_instances_in_same_context() {
$tagnames = ['foo', 'bar', 'baz', 'bop'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$context1 = context_user::instance($user1->id);
$context2 = context_user::instance($user2->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
$expectedtagnames = ['foo', 'bar', 'bop'];
$tagareas = core_tag_area::get_areas();
$tagarea = $tagareas[$itemtype][$component];
// Make sure the tag area allows multiple contexts.
core_tag_area::update($tagarea, ['multiplecontexts' => true]);
// Create tag instances in separate contexts.
$this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
$this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context1);
$this->add_tag_instance($tags['baz'], $component, $itemtype, $itemid, $context1);
$this->add_tag_instance($tags['bop'], $component, $itemtype, $itemid, $context2);
core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context1, ['foo', 'bar']);
$result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
$actualtagnames = array_map(function($record) {
return $record->name;
}, $result);
sort($expectedtagnames);
sort($actualtagnames);
// The list of tags should match the $expectedtagnames, which includes the
// tag 'bop' because it was in a different context to the one being set
// even though it wasn't in the new set of tags.
$this->assertEquals($expectedtagnames, $actualtagnames);
}
/**
* set_item_tags should allow multiple instances of the same tag in different
* contexts if the tag area allows multiple contexts.
*/
public function test_set_item_tags_allow_multiple_context_same_tag_multiple_contexts() {
$tagnames = ['foo'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$context1 = context_user::instance($user1->id);
$context2 = context_user::instance($user2->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
$expectedtagnames = ['foo', 'bar', 'bop'];
$tagareas = core_tag_area::get_areas();
$tagarea = $tagareas[$itemtype][$component];
// Make sure the tag area allows multiple contexts.
core_tag_area::update($tagarea, ['multiplecontexts' => true]);
// Create first instance of 'foo' in $context1.
$this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context2, ['foo']);
$result = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
$tagsbycontext = array_reduce($result, function($carry, $tag) {
$contextid = $tag->taginstancecontextid;
if (isset($carry[$contextid])) {
$carry[$contextid][] = $tag;
} else {
$carry[$contextid] = [$tag];
}
return $carry;
}, []);
// The result should be two tag instances of 'foo' in each of the
// two contexts, $context1 and $context2.
$this->assertCount(1, $tagsbycontext[$context1->id]);
$this->assertCount(1, $tagsbycontext[$context2->id]);
$this->assertEquals('foo', $tagsbycontext[$context1->id][0]->name);
$this->assertEquals('foo', $tagsbycontext[$context2->id][0]->name);
}
/**
* delete_instances_as_record with an empty set of instances should do nothing.
*/
public function test_delete_instances_as_record_empty_set() {
$user = $this->getDataGenerator()->create_user();
$context = context_user::instance($user->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context, ['foo']);
// This shouldn't error.
core_tag_tag::delete_instances_as_record([]);
$tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
// We should still have one tag.
$this->assertCount(1, $tags);
}
/**
* delete_instances_as_record with an instance that doesn't exist should do
* nothing.
*/
public function test_delete_instances_as_record_missing_set() {
$tagnames = ['foo'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user = $this->getDataGenerator()->create_user();
$context = context_user::instance($user->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
$taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
$taginstance->id++;
// Delete an instance that doesn't exist should do nothing.
core_tag_tag::delete_instances_as_record([$taginstance]);
$tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
// We should still have one tag.
$this->assertCount(1, $tags);
}
/**
* delete_instances_as_record with a list of all tag instances should
* leave no tags left.
*/
public function test_delete_instances_as_record_whole_set() {
$tagnames = ['foo'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user = $this->getDataGenerator()->create_user();
$context = context_user::instance($user->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
$taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
core_tag_tag::delete_instances_as_record([$taginstance]);
$tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
// There should be no tags left.
$this->assertEmpty($tags);
}
/**
* delete_instances_as_record with a list of only some tag instances should
* delete only the given tag instances and leave other tag instances.
*/
public function test_delete_instances_as_record_partial_set() {
$tagnames = ['foo', 'bar'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user = $this->getDataGenerator()->create_user();
$context = context_user::instance($user->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
$taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
$this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context);
core_tag_tag::delete_instances_as_record([$taginstance]);
$tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
// We should be left with a single tag, 'bar'.
$this->assertCount(1, $tags);
$tag = array_shift($tags);
$this->assertEquals('bar', $tag->name);
}
/**
* delete_instances_by_id with an empty set of ids should do nothing.
*/
public function test_delete_instances_by_id_empty_set() {
$user = $this->getDataGenerator()->create_user();
$context = context_user::instance($user->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context, ['foo']);
// This shouldn't error.
core_tag_tag::delete_instances_by_id([]);
$tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
// We should still have one tag.
$this->assertCount(1, $tags);
}
/**
* delete_instances_by_id with an id that doesn't exist should do
* nothing.
*/
public function test_delete_instances_by_id_missing_set() {
$tagnames = ['foo'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user = $this->getDataGenerator()->create_user();
$context = context_user::instance($user->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
$taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
// Delete an instance that doesn't exist should do nothing.
core_tag_tag::delete_instances_by_id([$taginstance->id + 1]);
$tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
// We should still have one tag.
$this->assertCount(1, $tags);
}
/**
* delete_instances_by_id with a list of all tag instance ids should
* leave no tags left.
*/
public function test_delete_instances_by_id_whole_set() {
$tagnames = ['foo'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user = $this->getDataGenerator()->create_user();
$context = context_user::instance($user->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
$taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
core_tag_tag::delete_instances_by_id([$taginstance->id]);
$tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
// There should be no tags left.
$this->assertEmpty($tags);
}
/**
* delete_instances_by_id with a list of only some tag instance ids should
* delete only the given tag instance ids and leave other tag instances.
*/
public function test_delete_instances_by_id_partial_set() {
$tagnames = ['foo', 'bar'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user = $this->getDataGenerator()->create_user();
$context = context_user::instance($user->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
$taginstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context);
$this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context);
core_tag_tag::delete_instances_by_id([$taginstance->id]);
$tags = core_tag_tag::get_item_tags($component, $itemtype, $itemid);
// We should be left with a single tag, 'bar'.
$this->assertCount(1, $tags);
$tag = array_shift($tags);
$this->assertEquals('bar', $tag->name);
}
/**
* delete_instances should delete all tag instances for a component if given
* only the component as a parameter.
*/
public function test_delete_instances_with_component() {
global $DB;
$tagnames = ['foo', 'bar'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user = $this->getDataGenerator()->create_user();
$context = context_user::instance($user->id);
$component = 'core';
$itemtype1 = 'user';
$itemtype2 = 'course';
$itemid = 1;
// Add 2 tag instances in the same $component but with different item types.
$this->add_tag_instance($tags['foo'], $component, $itemtype1, $itemid, $context);
$this->add_tag_instance($tags['bar'], $component, $itemtype2, $itemid, $context);
// Delete all tag instances for the component.
core_tag_tag::delete_instances($component);
$taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance} WHERE component = ?', [$component]);
// Both tag instances from the $component should have been deleted even though
// they are in different item types.
$this->assertEmpty($taginstances);
}
/**
* delete_instances should delete all tag instances for a component if given
* only the component as a parameter.
*/
public function test_delete_instances_with_component_and_itemtype() {
global $DB;
$tagnames = ['foo', 'bar'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user = $this->getDataGenerator()->create_user();
$context = context_user::instance($user->id);
$component = 'core';
$itemtype1 = 'user';
$itemtype2 = 'course';
$itemid = 1;
// Add 2 tag instances in the same $component but with different item types.
$this->add_tag_instance($tags['foo'], $component, $itemtype1, $itemid, $context);
$this->add_tag_instance($tags['bar'], $component, $itemtype2, $itemid, $context);
// Delete all tag instances for the component and itemtype.
core_tag_tag::delete_instances($component, $itemtype1);
$taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance} WHERE component = ?', [$component]);
// Only the tag instances for $itemtype1 should have been deleted. We
// should still be left with the instance for 'bar'.
$this->assertCount(1, $taginstances);
$taginstance = array_shift($taginstances);
$this->assertEquals($itemtype2, $taginstance->itemtype);
$this->assertEquals($tags['bar']->id, $taginstance->tagid);
}
/**
* delete_instances should delete all tag instances for a component in a context
* if given both the component and context id as parameters.
*/
public function test_delete_instances_with_component_and_context() {
global $DB;
$tagnames = ['foo', 'bar', 'baz'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$context1 = context_user::instance($user1->id);
$context2 = context_user::instance($user2->id);
$component = 'core';
$itemtype1 = 'user';
$itemtype2 = 'course';
$itemid = 1;
// Add 3 tag instances in the same $component but with different contexts.
$this->add_tag_instance($tags['foo'], $component, $itemtype1, $itemid, $context1);
$this->add_tag_instance($tags['bar'], $component, $itemtype2, $itemid, $context1);
$this->add_tag_instance($tags['baz'], $component, $itemtype2, $itemid, $context2);
// Delete all tag instances for the component and context.
core_tag_tag::delete_instances($component, null, $context1->id);
$taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance} WHERE component = ?', [$component]);
// Only the tag instances for $context1 should have been deleted. We
// should still be left with the instance for 'baz'.
$this->assertCount(1, $taginstances);
$taginstance = array_shift($taginstances);
$this->assertEquals($context2->id, $taginstance->contextid);
$this->assertEquals($tags['baz']->id, $taginstance->tagid);
}
/**
* delete_instances should delete all tag instances for a component, item type
* and context if given the component, itemtype, and context id as parameters.
*/
public function test_delete_instances_with_component_and_itemtype_and_context() {
global $DB;
$tagnames = ['foo', 'bar', 'baz'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$context1 = context_user::instance($user1->id);
$context2 = context_user::instance($user2->id);
$component = 'core';
$itemtype1 = 'user';
$itemtype2 = 'course';
$itemid = 1;
// Add 3 tag instances in the same $component but with different contexts.
$this->add_tag_instance($tags['foo'], $component, $itemtype1, $itemid, $context1);
$this->add_tag_instance($tags['bar'], $component, $itemtype2, $itemid, $context1);
$this->add_tag_instance($tags['baz'], $component, $itemtype2, $itemid, $context2);
// Delete all tag instances for the component and context.
core_tag_tag::delete_instances($component, $itemtype2, $context1->id);
$taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance} WHERE component = ?', [$component]);
// Only the tag instances for $itemtype2 in $context1 should have been
// deleted. We should still be left with the instance for 'foo' and 'baz'.
$this->assertCount(2, $taginstances);
$fooinstances = array_filter($taginstances, function($instance) use ($tags) {
return $instance->tagid == $tags['foo']->id;
});
$fooinstance = array_shift($fooinstances);
$bazinstances = array_filter($taginstances, function($instance) use ($tags) {
return $instance->tagid == $tags['baz']->id;
});
$bazinstance = array_shift($bazinstances);
$this->assertNotEmpty($fooinstance);
$this->assertNotEmpty($bazinstance);
$this->assertEquals($context1->id, $fooinstance->contextid);
$this->assertEquals($context2->id, $bazinstance->contextid);
}
/**
* change_instances_context should not change any existing instance contexts
* if not given any instance ids.
*/
public function test_change_instances_context_empty_set() {
global $DB;
$tagnames = ['foo'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$context1 = context_user::instance($user1->id);
$context2 = context_user::instance($user2->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
$this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
core_tag_tag::change_instances_context([], $context2);
$taginstances = $DB->get_records_sql('SELECT * FROM {tag_instance}');
// The existing tag instance should not have changed.
$this->assertCount(1, $taginstances);
$taginstance = array_shift($taginstances);
$this->assertEquals($context1->id, $taginstance->contextid);
}
/**
* change_instances_context should only change the context of the given ids.
*/
public function test_change_instances_context_partial_set() {
global $DB;
$tagnames = ['foo', 'bar'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$context1 = context_user::instance($user1->id);
$context2 = context_user::instance($user2->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
$fooinstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
$barinstance = $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context1);
core_tag_tag::change_instances_context([$fooinstance->id], $context2);
// Reload the record.
$fooinstance = $DB->get_record('tag_instance', ['id' => $fooinstance->id]);
$barinstance = $DB->get_record('tag_instance', ['id' => $barinstance->id]);
// Tag 'foo' context should be updated.
$this->assertEquals($context2->id, $fooinstance->contextid);
// Tag 'bar' context should not be changed.
$this->assertEquals($context1->id, $barinstance->contextid);
}
/**
* change_instances_context should change multiple items from multiple contexts.
*/
public function test_change_instances_context_multiple_contexts() {
global $DB;
$tagnames = ['foo', 'bar'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user();
$context1 = context_user::instance($user1->id);
$context2 = context_user::instance($user2->id);
$context3 = context_user::instance($user3->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
// Two instances in different contexts.
$fooinstance = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
$barinstance = $this->add_tag_instance($tags['bar'], $component, $itemtype, $itemid, $context2);
core_tag_tag::change_instances_context([$fooinstance->id, $barinstance->id], $context3);
// Reload the record.
$fooinstance = $DB->get_record('tag_instance', ['id' => $fooinstance->id]);
$barinstance = $DB->get_record('tag_instance', ['id' => $barinstance->id]);
// Tag 'foo' context should be updated.
$this->assertEquals($context3->id, $fooinstance->contextid);
// Tag 'bar' context should be updated.
$this->assertEquals($context3->id, $barinstance->contextid);
// There shouldn't be any tag instances left in $context1.
$context1records = $DB->get_records('tag_instance', ['contextid' => $context1->id]);
$this->assertEmpty($context1records);
// There shouldn't be any tag instances left in $context2.
$context2records = $DB->get_records('tag_instance', ['contextid' => $context2->id]);
$this->assertEmpty($context2records);
}
/**
* change_instances_context moving an instance from one context into a context
* that already has an instance of that tag should throw an exception.
*/
public function test_change_instances_context_conflicting_instances() {
global $DB;
$tagnames = ['foo'];
$collid = core_tag_collection::get_default();
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$context1 = context_user::instance($user1->id);
$context2 = context_user::instance($user2->id);
$component = 'core';
$itemtype = 'user';
$itemid = 1;
// Two instances of 'foo' in different contexts.
$fooinstance1 = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context1);
$fooinstance2 = $this->add_tag_instance($tags['foo'], $component, $itemtype, $itemid, $context2);
// There is already an instance of 'foo' in $context2 so the code
// should throw an exception when we try to move another instance there.
$this->expectException('Exception');
core_tag_tag::change_instances_context([$fooinstance1->id], $context2);
}
/**
* Help method to return sorted array of names of correlated tags to use for assertions
* @param core_tag $tag
@@ -1184,4 +2030,29 @@ class core_tag_taglib_testcase extends advanced_testcase {
sort($rv);
return array_values($rv);
}
/**
* Add a tag instance.
*
* @param core_tag_tag $tag
* @param string $component
* @param string $itemtype
* @param int $itemid
* @param context $context
* @return stdClass
*/
protected function add_tag_instance(core_tag_tag $tag, $component, $itemtype, $itemid, $context) {
global $DB;
$record = (array) $tag->to_object();
$record['tagid'] = $record['id'];
$record['component'] = $component;
$record['itemtype'] = $itemtype;
$record['itemid'] = $itemid;
$record['contextid'] = $context->id;
$record['tiuserid'] = 0;
$record['ordering'] = 0;
$record['timecreated'] = time();
$record['id'] = $DB->insert_record('tag_instance', $record);
return (object) $record;
}
}
+1 -1
View File
@@ -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.