diff --git a/admin/tool/uploadcourse/classes/course.php b/admin/tool/uploadcourse/classes/course.php index ff2283304d0..4486cdebb5c 100644 --- a/admin/tool/uploadcourse/classes/course.php +++ b/admin/tool/uploadcourse/classes/course.php @@ -659,6 +659,10 @@ class tool_uploadcourse_course { $this->data = $coursedata; $this->enrolmentdata = tool_uploadcourse_helper::get_enrolment_data($this->rawdata); + if (isset($this->rawdata['tags']) && strval($this->rawdata['tags']) !== '') { + $this->data['tags'] = preg_split('/\s*,\s*/', trim($this->rawdata['tags']), -1, PREG_SPLIT_NO_EMPTY); + } + // Restore data. // TODO Speed up things by not really extracting the backup just yet, but checking that // the backup file or shortname passed are valid. Extraction should happen in proceed(). diff --git a/admin/tool/uploadcourse/tests/course_test.php b/admin/tool/uploadcourse/tests/course_test.php index a173e721811..20bbaa352df 100644 --- a/admin/tool/uploadcourse/tests/course_test.php +++ b/admin/tool/uploadcourse/tests/course_test.php @@ -261,6 +261,7 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { 'groupmode' => '2', 'groupmodeforce' => '1', 'enablecompletion' => '1', + 'tags' => 'Cat, Dog', 'role_teacher' => 'Knight', 'role_manager' => 'Jedi', @@ -297,6 +298,7 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { $this->assertEquals($data['groupmode'], $course->groupmode); $this->assertEquals($data['groupmodeforce'], $course->groupmodeforce); $this->assertEquals($data['enablecompletion'], $course->enablecompletion); + $this->assertEquals($data['tags'], join(', ', core_tag_tag::get_item_tags_array('core', 'course', $course->id))); // Roles. $roleids = array(); diff --git a/admin/tool/uploaduser/index.php b/admin/tool/uploaduser/index.php index e687fe32214..2b3f43eaee0 100644 --- a/admin/tool/uploaduser/index.php +++ b/admin/tool/uploaduser/index.php @@ -96,6 +96,7 @@ $STD_FIELDS = array('id', 'username', 'email', 'suspended', // 1 means suspend user account, 0 means activate user account, nothing means keep as is for existing users 'deleted', // 1 means delete user 'mnethostid', // Can not be used for adding, updating or deleting of users - only for enrolments, groups, cohorts and suspending. + 'interests', ); // Include all name fields. $STD_FIELDS = array_merge($STD_FIELDS, get_all_user_name_fields()); @@ -836,6 +837,10 @@ if ($formdata = $mform2->is_cancelled()) { } } + // Update user interests. + if (isset($user->interests) && strval($user->interests) !== '') { + useredit_update_interests($user, preg_split('/\s*,\s*/', $user->interests, -1, PREG_SPLIT_NO_EMPTY)); + } // add to cohort first, it might trigger enrolments indirectly - do NOT create cohorts here! foreach ($filecolumns as $column) { diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index a41292dffdf..caf5f071b10 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -1778,22 +1778,8 @@ class restore_course_structure_step extends restore_structure_step { $data = (object)$data; - if (!empty($CFG->usetags)) { // if enabled in server - // TODO: This is highly inneficient. Each time we add one tag - // we fetch all the existing because tag_set() deletes them - // so everything must be reinserted on each call - $tags = array(); - $existingtags = tag_get_tags('course', $this->get_courseid()); - // Re-add all the existitng tags - foreach ($existingtags as $existingtag) { - $tags[] = $existingtag->rawname; - } - // Add the one being restored - $tags[] = $data->rawname; - // Send all the tags back to the course - tag_set('course', $this->get_courseid(), $tags, 'core', - context_course::instance($this->get_courseid())->id); - } + core_tag_tag::add_item_tag('core', 'course', $this->get_courseid(), + context_course::instance($this->get_courseid()), $data->rawname); } public function process_allowed_module($data) { @@ -4078,25 +4064,17 @@ class restore_create_categories_and_questions extends restore_structure_step { return; } - if (!empty($CFG->usetags)) { // if enabled in server - // TODO: This is highly inefficient. Each time we add one tag - // we fetch all the existing because tag_set() deletes them - // so everything must be reinserted on each call - $tags = array(); - $existingtags = tag_get_tags('question', $newquestion); - // Re-add all the existitng tags - foreach ($existingtags as $existingtag) { - $tags[] = $existingtag->rawname; - } - // Add the one being restored - $tags[] = $data->rawname; + if (core_tag_tag::is_enabled('core_question', 'question')) { + $tagname = $data->rawname; // Get the category, so we can then later get the context. $categoryid = $this->get_new_parentid('question_category'); if (empty($this->cachedcategory) || $this->cachedcategory->id != $categoryid) { $this->cachedcategory = $DB->get_record('question_categories', array('id' => $categoryid)); } - // Send all the tags back to the question - tag_set('question', $newquestion, $tags, 'core_question', $this->cachedcategory->contextid); + // Add the tag to the question. + core_tag_tag::add_item_tag('core_question', 'question', $newquestion, + context::instance_by_id($this->cachedcategory->contextid), + $tagname); } } diff --git a/backup/util/dbops/restore_dbops.class.php b/backup/util/dbops/restore_dbops.class.php index 430e8bb0e46..120ea6c7d4e 100644 --- a/backup/util/dbops/restore_dbops.class.php +++ b/backup/util/dbops/restore_dbops.class.php @@ -1209,16 +1209,14 @@ abstract class restore_dbops { } // Process tags - if (!empty($CFG->usetags) && isset($user->tags)) { // if enabled in server and present in backup + if (core_tag_tag::is_enabled('core', 'user') && isset($user->tags)) { // If enabled in server and present in backup. $tags = array(); foreach($user->tags['tag'] as $usertag) { $usertag = (object)$usertag; $tags[] = $usertag->rawname; } - if (empty($newuserctxid)) { - $newuserctxid = null; // Tag apis expect a null contextid not 0. - } - tag_set('user', $newuserid, $tags, 'core', $newuserctxid); + core_tag_tag::set_item_tags('core', 'user', $newuserid, + context_user::instance($newuserid), $tags); } // Process preferences diff --git a/blocks/blog_tags/block_blog_tags.php b/blocks/blog_tags/block_blog_tags.php index de32984c8ae..6edc7b5dff4 100644 --- a/blocks/blog_tags/block_blog_tags.php +++ b/blocks/blog_tags/block_blog_tags.php @@ -75,7 +75,7 @@ class block_blog_tags extends block_base { } return $this->content; - } else if (empty($CFG->usetags)) { + } else if (!core_tag_tag::is_enabled('core', 'post')) { $this->content = new stdClass(); $this->content->text = ''; if ($this->page->user_is_editing()) { @@ -126,6 +126,7 @@ class block_blog_tags extends block_base { WHERE t.id = ti.tagid AND p.id = ti.itemid $type AND ti.itemtype = 'post' + AND ti.component = 'core' AND ti.timemodified > $timewithin"; if ($context->contextlevel == CONTEXT_MODULE) { @@ -195,7 +196,9 @@ class block_blog_tags extends block_base { } $blogurl->param('tagid', $tag->id); - $link = html_writer::link($blogurl, tag_display_name($tag), array('class'=>$tag->class, 'title'=>get_string('numberofentries','blog',$tag->ct))); + $link = html_writer::link($blogurl, core_tag_tag::make_display_name($tag), + array('class' => $tag->class, + 'title' => get_string('numberofentries', 'blog', $tag->ct))); $this->content->text .= '
  • ' . $link . '
  • '; } $this->content->text .= "\n\n"; diff --git a/blocks/tag_flickr/block_tag_flickr.php b/blocks/tag_flickr/block_tag_flickr.php index 06f691d388d..22471f38eb1 100644 --- a/blocks/tag_flickr/block_tag_flickr.php +++ b/blocks/tag_flickr/block_tag_flickr.php @@ -47,7 +47,6 @@ class block_tag_flickr extends block_base { global $CFG, $USER; //note: do NOT include files at the top of this file - require_once($CFG->dirroot.'/tag/lib.php'); require_once($CFG->libdir . '/filelib.php'); if ($this->content !== NULL) { @@ -56,11 +55,12 @@ class block_tag_flickr extends block_base { $tagid = optional_param('id', 0, PARAM_INT); // tag id - for backware compatibility $tag = optional_param('tag', '', PARAM_TAG); // tag + $tc = optional_param('tc', 0, PARAM_INT); // Tag collection id. - if ($tag) { - $tagobject = tag_get('name', $tag); - } else if ($tagid) { - $tagobject = tag_get('id', $tagid); + if ($tagid) { + $tagobject = core_tag_tag::get($tagid); + } else if ($tag) { + $tagobject = core_tag_tag::get_by_name($tc, $tag); } if (empty($tagobject)) { @@ -73,7 +73,9 @@ class block_tag_flickr extends block_base { //include related tags in the photo query ? $tagscsv = $tagobject->name; if (!empty($this->config->includerelatedtags)) { - $tagscsv .= ',' . tag_get_related_tags_csv(tag_get_related_tags($tagobject->id), TAG_RETURN_TEXT); + foreach ($tagobject->get_related_tags() as $t) { + $tagscsv .= ',' . $t->get_display_name(false); + } } $tagscsv = urlencode($tagscsv); diff --git a/blocks/tag_flickr/edit_form.php b/blocks/tag_flickr/edit_form.php index e67e039a7a2..da0a23f5f8d 100644 --- a/blocks/tag_flickr/edit_form.php +++ b/blocks/tag_flickr/edit_form.php @@ -36,7 +36,7 @@ class block_tag_flickr_edit_form extends block_edit_form { $mform->setType('config_title', PARAM_TEXT); $mform->addElement('text', 'config_numberofphotos', get_string('numberofphotos', 'block_tag_flickr'), array('size' => 5)); - $mform->setType('config_numberofvideos', PARAM_INT); + $mform->setType('config_numberofphotos', PARAM_INT); $mform->addElement('selectyesno', 'config_includerelatedtags', get_string('includerelatedtags', 'block_tag_flickr')); $mform->setDefault('config_includerelatedtags', 0); diff --git a/blocks/tag_youtube/block_tag_youtube.php b/blocks/tag_youtube/block_tag_youtube.php index 7ab13be342c..9c79fe8d982 100644 --- a/blocks/tag_youtube/block_tag_youtube.php +++ b/blocks/tag_youtube/block_tag_youtube.php @@ -64,7 +64,6 @@ class block_tag_youtube extends block_base { global $CFG; //note: do NOT include files at the top of this file - require_once($CFG->dirroot.'/tag/lib.php'); require_once($CFG->libdir . '/filelib.php'); if ($this->content !== NULL) { @@ -132,11 +131,12 @@ class block_tag_youtube extends block_base { $tagid = optional_param('id', 0, PARAM_INT); // tag id - for backware compatibility $tag = optional_param('tag', '', PARAM_TAG); // tag + $tc = optional_param('tc', 0, PARAM_INT); // Tag collection id. - if ($tag) { - $tagobject = tag_get('name', $tag); - } else if ($tagid) { - $tagobject = tag_get('id', $tagid); + if ($tagid) { + $tagobject = core_tag_tag::get($tagid); + } else if ($tag) { + $tagobject = core_tag_tag::get_by_name($tc, $tag); } if (empty($tagobject)) { @@ -172,11 +172,12 @@ class block_tag_youtube extends block_base { $tagid = optional_param('id', 0, PARAM_INT); // tag id - for backware compatibility $tag = optional_param('tag', '', PARAM_TAG); // tag + $tc = optional_param('tc', 0, PARAM_INT); // Tag collection id. - if ($tag) { - $tagobject = tag_get('name', $tag); - } else if ($tagid) { - $tagobject = tag_get('id', $tagid); + if ($tagid) { + $tagobject = core_tag_tag::get($tagid); + } else if ($tag) { + $tagobject = core_tag_tag::get_by_name($tc, $tag); } if (empty($tagobject)) { diff --git a/blocks/tags/block_tags.php b/blocks/tags/block_tags.php index 329eb3f9cf8..31ced5814a5 100644 --- a/blocks/tags/block_tags.php +++ b/blocks/tags/block_tags.php @@ -70,6 +70,22 @@ class block_tags extends block_base { $this->config->numberoftags = 80; } + if (empty($this->config->tagtype)) { + $this->config->tagtype = ''; + } + + if (empty($this->config->ctx)) { + $this->config->ctx = 0; + } + + if (empty($this->config->rec)) { + $this->config->rec = 1; + } + + if (empty($this->config->tagcoll)) { + $this->config->tagcoll = 0; + } + if ($this->content !== NULL) { return $this->content; } @@ -85,9 +101,11 @@ class block_tags extends block_base { // Get a list of tags. - require_once($CFG->dirroot.'/tag/locallib.php'); - - $this->content->text = tag_print_cloud(null, $this->config->numberoftags, true); + $tagcloud = core_tag_collection::get_tag_cloud($this->config->tagcoll, + $this->config->tagtype, + $this->config->numberoftags, + 'name', '', $this->page->context->id, $this->config->ctx, $this->config->rec); + $this->content->text = $OUTPUT->render_from_template('core_tag/tagcloud', $tagcloud->export_for_template($OUTPUT)); return $this->content; } diff --git a/blocks/tags/edit_form.php b/blocks/tags/edit_form.php index e6c80f12a20..89742c59a55 100644 --- a/blocks/tags/edit_form.php +++ b/blocks/tags/edit_form.php @@ -30,6 +30,7 @@ */ class block_tags_edit_form extends block_edit_form { protected function specific_definition($mform) { + global $CFG; // Fields for editing HTML block title and contents. $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); @@ -37,11 +38,63 @@ class block_tags_edit_form extends block_edit_form { $mform->setType('config_title', PARAM_TEXT); $mform->setDefault('config_title', get_string('pluginname', 'block_tags')); + $this->add_collection_selector($mform); + $numberoftags = array(); for ($i = 1; $i <= 200; $i++) { $numberoftags[$i] = $i; } $mform->addElement('select', 'config_numberoftags', get_string('numberoftags', 'blog'), $numberoftags); $mform->setDefault('config_numberoftags', 80); + + $defaults = array( + 'official' => get_string('officialonly', 'block_tags'), + '' => get_string('anytype', 'block_tags')); + $mform->addElement('select', 'config_tagtype', get_string('defaultdisplay', 'block_tags'), $defaults); + $mform->setDefault('config_tagtype', ''); + + $defaults = array(0 => context_system::instance()->get_context_name()); + $parentcontext = context::instance_by_id($this->block->instance->parentcontextid); + if ($parentcontext->contextlevel > CONTEXT_COURSE) { + $coursecontext = $parentcontext->get_course_context(); + $defaults[$coursecontext->id] = $coursecontext->get_context_name(); + } + if ($parentcontext->contextlevel != CONTEXT_SYSTEM) { + $defaults[$parentcontext->id] = $parentcontext->get_context_name(); + } + $mform->addElement('select', 'config_ctx', get_string('taggeditemscontext', 'block_tags'), $defaults); + $mform->addHelpButton('config_ctx', 'taggeditemscontext', 'block_tags'); + $mform->setDefault('config_ctx', 0); + + $mform->addElement('advcheckbox', 'config_rec', get_string('recursivecontext', 'block_tags')); + $mform->addHelpButton('config_rec', 'recursivecontext', 'block_tags'); + $mform->setDefault('config_rec', 1); + } + + /** + * Add the tag collection selector + * + * @param object $mform the form being built. + */ + protected function add_collection_selector($mform) { + $tagcolls = core_tag_collection::get_collections_menu(false, false, get_string('anycollection', 'block_tags')); + if (count($tagcolls) <= 1) { + return; + } + + $tagcollssearchable = core_tag_collection::get_collections_menu(false, true); + $hasunsearchable = false; + foreach ($tagcolls as $id => $name) { + if ($id && !array_key_exists($id, $tagcollssearchable)) { + $hasunsearchable = true; + $tagcolls[$id] = $name . '*'; + } + } + + $mform->addElement('select', 'config_tagcoll', get_string('tagcollection', 'block_tags'), $tagcolls); + if ($hasunsearchable) { + $mform->addHelpButton('config_tagcoll', 'tagcollection', 'block_tags'); + } + $mform->setDefault('config_tagcoll', 0); } } diff --git a/blocks/tags/lang/en/block_tags.php b/blocks/tags/lang/en/block_tags.php index edf96699495..02dd7a1cb0a 100644 --- a/blocks/tags/lang/en/block_tags.php +++ b/blocks/tags/lang/en/block_tags.php @@ -22,14 +22,24 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['anycollection'] = 'Any'; +$string['anytype'] = 'All'; $string['configtitle'] = 'Block title'; $string['disabledtags'] = 'Tags are disabled'; $string['defaultdisplay'] = 'Tag type to display'; +$string['officialonly'] = 'Only official'; $string['pluginname'] = 'Tags'; +$string['recursivecontext'] = 'Include child contexts'; +$string['recursivecontext_help'] = 'If unchecked, tags of items in the context specified above will be displayed excluding underlying contexts, for example, you can search on course level only without searching inside course activities'; +$string['tagcollection'] = 'Tag collection'; +$string['tagcollection_help'] = 'Select tag collection to display tags from. If you choose "Any" ' + . 'the tags from all collections except for those marked with * will be displayed'; +$string['taggeditemscontext'] = 'Tagged items context'; +$string['taggeditemscontext_help'] = 'You can limit the tag cloud to the tags that are present in the current course category, course or module'; $string['tags:addinstance'] = 'Add a new tags block'; $string['tags:myaddinstance'] = 'Add a new tags block to Dashboard'; -// Deprecated since 3.0 +// Deprecated since 3.0. $string['add'] = 'Add'; $string['alltags'] = 'All tags:'; diff --git a/blocks/tags/tests/behat/tagcloud.feature b/blocks/tags/tests/behat/tagcloud.feature index ed80f7b03a4..7a254906516 100644 --- a/blocks/tags/tests/behat/tagcloud.feature +++ b/blocks/tags/tests/behat/tagcloud.feature @@ -45,6 +45,6 @@ Feature: Block tags displaying tag cloud And I should see "Cats" in the "Tags" "block" And I should not see "Neverusedtag" in the "Tags" "block" And I click on "Dogs" "link" in the "Tags" "block" - And I should see "Users tagged with \"Dogs\": 1" + And I should see "User interests" in the ".tag-index-items h3" "css_element" And I should see "Teacher 1" And I log out diff --git a/blog/edit.php b/blog/edit.php index 634c511377c..82480390c3c 100644 --- a/blog/edit.php +++ b/blog/edit.php @@ -24,9 +24,10 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(dirname(dirname(__FILE__)).'/config.php'); -require_once('lib.php'); -require_once('locallib.php'); -require_once($CFG->dirroot .'/comment/lib.php'); +require_once($CFG->dirroot . '/blog/lib.php'); +require_once($CFG->dirroot . '/blog/locallib.php'); +require_once($CFG->dirroot . '/comment/lib.php'); +require_once($CFG->dirroot . '/blog/edit_form.php'); $action = required_param('action', PARAM_ALPHA); $id = optional_param('entryid', 0, PARAM_INT); @@ -185,7 +186,6 @@ if (!empty($entry->id)) { } } -require_once('edit_form.php'); $summaryoptions = array('maxfiles' => 99, 'maxbytes' => $CFG->maxbytes, 'trusttext' => true, 'context' => $sitecontext, 'subdirs' => file_area_contains_subdirs($sitecontext, 'blog', 'post', $entry->id)); $attachmentoptions = array('subdirs' => false, 'maxfiles' => 99, 'maxbytes' => $CFG->maxbytes); @@ -206,9 +206,8 @@ $entry = file_prepare_standard_filemanager($entry, 'attachment', $entry->id); -if (!empty($CFG->usetags) && !empty($entry->id)) { - include_once($CFG->dirroot.'/tag/lib.php'); - $entry->tags = tag_get_tags_array('post', $entry->id); +if (!empty($entry->id)) { + $entry->tags = core_tag_tag::get_item_tags_array('core', 'post', $entry->id); } $entry->action = $action; @@ -271,7 +270,6 @@ switch ($action) { if (empty($entry->id)) { print_error('wrongentryid', 'blog'); } - $entry->tags = tag_get_tags_array('post', $entry->id); $strformheading = get_string('updateentrywithid', 'blog'); break; diff --git a/blog/edit_form.php b/blog/edit_form.php index 773f1533e11..7060c932b5a 100644 --- a/blog/edit_form.php +++ b/blog/edit_form.php @@ -65,10 +65,11 @@ class blog_edit_form extends moodleform { $mform->addHelpButton('publishstate', 'publishto', 'blog'); $mform->setDefault('publishstate', 0); - if (!empty($CFG->usetags)) { + if (core_tag_tag::is_enabled('core', 'post')) { $mform->addElement('header', 'tagshdr', get_string('tags', 'tag')); - $mform->addElement('tags', 'tags', get_string('tags')); } + $mform->addElement('tags', 'tags', get_string('tags'), + array('itemtype' => 'post', 'component' => 'core')); $allmodnames = array(); diff --git a/blog/external_blog_edit.php b/blog/external_blog_edit.php index 069ef9615f5..3f615bdd1b0 100644 --- a/blog/external_blog_edit.php +++ b/blog/external_blog_edit.php @@ -28,7 +28,6 @@ require_once('../config.php'); require_once('lib.php'); require_once('external_blog_edit_form.php'); require_once($CFG->libdir . '/simplepie/moodle_simplepie.php'); -require_once($CFG->dirroot.'/tag/lib.php'); require_login(); $context = context_system::instance(); @@ -58,6 +57,7 @@ if (!empty($id) && !$DB->record_exists('blog_external', array('id' => $id))) { print_error('wrongexternalid', 'blog'); } else if (!empty($id)) { $external = $DB->get_record('blog_external', array('id' => $id)); + $external->autotags = core_tag_tag::get_item_tags_array('core', 'blog_external', $id); } $strformheading = ($action == 'edit') ? get_string('editexternalblog', 'blog') : get_string('addnewexternalblog', 'blog'); @@ -84,12 +84,9 @@ if ($externalblogform->is_cancelled()) { $newexternal->timemodified = time(); $newexternal->id = $DB->insert_record('blog_external', $newexternal); + core_tag_tag::set_item_tags('core', 'blog_external', $newexternal->id, + context_user::instance($newexternal->userid), $data->autotags); blog_sync_external_entries($newexternal); - if ($CFG->usetags) { - $autotags = (!empty($data->autotags)) ? $data->autotags : null; - tag_set('blog_external', $newexternal->id, explode(',', $autotags), 'core', - context_user::instance($newexternal->userid)->id); - } break; @@ -107,11 +104,8 @@ if ($externalblogform->is_cancelled()) { $external->timemodified = time(); $DB->update_record('blog_external', $external); - if ($CFG->usetags) { - $autotags = (!empty($data->autotags)) ? $data->autotags : null; - tag_set('blog_external', $external->id, explode(',', $autotags), 'core', - context_user::instance($external->userid)->id); - } + core_tag_tag::set_item_tags('core', 'blog_external', $external->id, + context_user::instance($external->userid), $data->autotags); } else { print_error('wrongexternalid', 'blog'); } @@ -125,6 +119,9 @@ if ($externalblogform->is_cancelled()) { redirect($returnurl); } +navigation_node::override_active_url(new moodle_url('/blog/external_blogs.php')); +$PAGE->navbar->add(get_string('addnewexternalblog', 'blog')); + $PAGE->set_heading(fullname($USER)); $PAGE->set_title("$SITE->shortname: $strblogs: $strexternalblogs"); diff --git a/blog/external_blog_edit_form.php b/blog/external_blog_edit_form.php index fc28e9a406d..8de3bdcc58f 100644 --- a/blog/external_blog_edit_form.php +++ b/blog/external_blog_edit_form.php @@ -48,14 +48,14 @@ class blog_edit_external_form extends moodleform { $mform->addElement('textarea', 'description', get_string('description', 'blog'), array('cols' => 50, 'rows' => 7)); $mform->addHelpButton('description', 'description', 'blog'); - if (!empty($CFG->usetags)) { - $mform->addElement('text', 'filtertags', get_string('filtertags', 'blog'), array('size' => 50)); - $mform->setType('filtertags', PARAM_TAGLIST); - $mform->addHelpButton('filtertags', 'filtertags', 'blog'); - $mform->addElement('text', 'autotags', get_string('autotags', 'blog'), array('size' => 50)); - $mform->setType('autotags', PARAM_TAGLIST); - $mform->addHelpButton('autotags', 'autotags', 'blog'); - } + // To filter external blogs by their tags we do not need to check if tags in moodle are enabled. + $mform->addElement('text', 'filtertags', get_string('filtertags', 'blog'), array('size' => 50)); + $mform->setType('filtertags', PARAM_TAGLIST); + $mform->addHelpButton('filtertags', 'filtertags', 'blog'); + + $mform->addElement('tags', 'autotags', get_string('autotags', 'blog'), + array('itemtype' => 'blog_external', 'component' => 'core')); + $mform->addHelpButton('autotags', 'autotags', 'blog'); $this->add_action_buttons(); @@ -115,7 +115,6 @@ class blog_edit_external_form extends moodleform { } if ($id = $mform->getElementValue('id')) { - $mform->setDefault('autotags', implode(',', tag_get_tags_array('blog_external', $id))); $mform->freeze('url'); if ($mform->elementExists('filtertags')) { $mform->freeze('filtertags'); diff --git a/blog/index.php b/blog/index.php index bc3dd1ed06b..1b64b42fe56 100644 --- a/blog/index.php +++ b/blog/index.php @@ -24,7 +24,6 @@ require_once(dirname(dirname(__FILE__)).'/config.php'); require_once($CFG->dirroot .'/blog/lib.php'); require_once($CFG->dirroot .'/blog/locallib.php'); require_once($CFG->dirroot .'/course/lib.php'); -require_once($CFG->dirroot .'/tag/lib.php'); require_once($CFG->dirroot .'/comment/lib.php'); $id = optional_param('id', null, PARAM_INT); diff --git a/blog/lib.php b/blog/lib.php index 00a1ffa6bf5..06b4af808ae 100644 --- a/blog/lib.php +++ b/blog/lib.php @@ -29,7 +29,6 @@ defined('MOODLE_INTERNAL') || die(); * Library of functions and constants for blog */ require_once($CFG->dirroot .'/blog/rsslib.php'); -require_once($CFG->dirroot.'/tag/lib.php'); /** * User can edit a blog entry if this is their own blog entry and they have @@ -254,8 +253,8 @@ function blog_sync_external_entries($externalblog) { $id = $DB->insert_record('post', $newentry); // Set tags. - if ($tags = tag_get_tags_array('blog_external', $externalblog->id)) { - tag_set('post', $id, $tags, 'core', context_user::instance($externalblog->userid)->id); + if ($tags = core_tag_tag::get_item_tags_array('core', 'blog_external', $externalblog->id)) { + core_tag_tag::set_item_tags('core', 'post', $id, context_user::instance($externalblog->userid), $tags); } } else { $newentry->id = $postid; @@ -1046,3 +1045,105 @@ function core_blog_myprofile_navigation(core_user\output\myprofile\tree $tree, $ $tree->add_node($blognode); return true; } + +/** + * Returns posts tagged with a specified tag. + * + * @param core_tag_tag $tag + * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag + * are displayed on the page and the per-page limit may be bigger + * @param int $fromctx context id where the link was displayed, may be used by callbacks + * to display items in the same context first + * @param int $ctx context id where to search for records + * @param bool $rec search in subcontexts as well + * @param int $page 0-based number of page being displayed + * @return \core_tag\output\tagindex + */ +function blog_get_tagged_posts($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = true, $page = 0) { + global $CFG, $OUTPUT; + require_once($CFG->dirroot.'/user/lib.php'); + + $systemcontext = context_system::instance(); + $perpage = $exclusivemode ? 20 : 5; + $context = $ctx ? context::instance_by_id($ctx) : context_system::instance(); + + $content = ''; + if (empty($CFG->enableblogs) || !has_capability('moodle/blog:view', $systemcontext)) { + // Blogs are not enabled or are not visible to the current user. + $totalpages = 0; + } else if ($context->contextlevel != CONTEXT_SYSTEM && empty($CFG->useblogassociations)) { + // No blog entries can be associated to the non-system context. + $totalpages = 0; + } else if (!$rec && $context->contextlevel != CONTEXT_COURSE && $context->contextlevel != CONTEXT_MODULE) { + // No blog entries can be associated with category or block context. + $totalpages = 0; + } else { + require_once($CFG->dirroot.'/blog/locallib.php'); + + $filters = array('tag' => $tag->id); + if ($rec) { + if ($context->contextlevel != CONTEXT_SYSTEM) { + $filters['context'] = $context->id; + } + } else if ($context->contextlevel == CONTEXT_COURSE) { + $filters['course'] = $context->instanceid; + } else if ($context->contextlevel == CONTEXT_MODULE) { + $filters['module'] = $context->instanceid; + } + $bloglisting = new blog_listing($filters); + $blogs = $bloglisting->get_entries($page * $perpage, $perpage); + $totalcount = $bloglisting->count_entries(); + $totalpages = ceil($totalcount / $perpage); + if (!empty($blogs)) { + $tagfeed = new core_tag\output\tagfeed(); + foreach ($blogs as $blog) { + $user = fullclone($blog); + $user->id = $blog->userid; + $user->deleted = 0; + $img = $OUTPUT->user_picture($user, array('size' => 35)); + $subject = format_string($blog->subject); + + if ($blog->publishstate == 'draft') { + $class = 'dimmed'; + } else { + $class = ''; + } + + $url = new moodle_url('/blog/index.php', array('entryid' => $blog->id)); + $subject = html_writer::link($url, $subject, array('class' => $class)); + + $fullname = fullname($user); + if (user_can_view_profile($user)) { + $profilelink = new moodle_url('/user/view.php', array('id' => $blog->userid)); + $fullname = html_writer::link($profilelink, $fullname); + } + $details = $fullname . ', ' . userdate($blog->created); + + $tagfeed->add($img, $subject, $details); + } + + $items = $tagfeed->export_for_template($OUTPUT); + $content = $OUTPUT->render_from_template('core_tag/tagfeed', $items); + + $urlparams = array('tagid' => $tag->id); + if ($context->contextlevel == CONTEXT_COURSE) { + $urlparams['courseid'] = $context->instanceid; + } else if ($context->contextlevel == CONTEXT_MODULE) { + $urlparams['modid'] = $context->instanceid; + } + $allblogsurl = new moodle_url('/blog/index.php', $urlparams); + + $rv = new core_tag\output\tagindex($tag, 'core', 'post', + $content, + $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); + $rv->exclusiveurl = $allblogsurl; + return $rv; + } + } + + $rv = new core_tag\output\tagindex($tag, 'core', 'post', + $content, + $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); + $rv->exclusiveurl = null; + return $rv; +} diff --git a/blog/locallib.php b/blog/locallib.php index 7968483fb36..5e31f8e4401 100644 --- a/blog/locallib.php +++ b/blog/locallib.php @@ -256,14 +256,11 @@ class blog_entry implements renderable { // Insert the new blog entry. $this->id = $DB->insert_record('post', $this); - // Update tags. - $this->add_tags_info(); - if (!empty($CFG->useblogassociations)) { $this->add_associations(); } - tag_set('post', $this->id, $this->tags, 'core', context_user::instance($this->userid)->id); + core_tag_tag::set_item_tags('core', 'post', $this->id, context_user::instance($this->userid), $this->tags); // Trigger an event for the new entry. $event = \core\event\blog_entry_created::create(array( @@ -312,7 +309,7 @@ class blog_entry implements renderable { // Update record. $DB->update_record('post', $entry); - tag_set('post', $entry->id, $entry->tags, 'core', context_user::instance($this->userid)->id); + core_tag_tag::set_item_tags('core', 'post', $entry->id, context_user::instance($this->userid), $entry->tags); $event = \core\event\blog_entry_updated::create(array( 'objectid' => $entry->id, @@ -336,7 +333,7 @@ class blog_entry implements renderable { // Get record to pass onto the event. $record = $DB->get_record('post', array('id' => $this->id)); $DB->delete_records('post', array('id' => $this->id)); - tag_set('post', $this->id, array(), 'core', context_user::instance($this->userid)->id); + core_tag_tag::remove_all_item_tags('core', 'post', $this->id); $event = \core\event\blog_entry_deleted::create(array( 'objectid' => $this->id, @@ -424,26 +421,6 @@ class blog_entry implements renderable { $fs->delete_area_files(SYSCONTEXTID, 'blog', 'post', $this->id); } - /** - * function to attach tags into an entry - * @return void - */ - public function add_tags_info() { - - $tags = array(); - - if ($otags = optional_param('otags', '', PARAM_INT)) { - foreach ($otags as $tagid) { - // TODO : make this use the tag name in the form. - if ($tag = tag_get('id', $tagid)) { - $tags[] = $tag->name; - } - } - } - - tag_set('post', $this->id, $tags, 'core', context_user::instance($this->userid)->id); - } - /** * User can edit a blog entry if this is their own blog entry and they have * the capability moodle/blog:create, or if they have the capability @@ -570,7 +547,13 @@ class blog_listing { * Array of blog_entry objects. * @var array $entries */ - public $entries = array(); + public $entries = null; + + /** + * Caches the total number of the entries. + * @var int + */ + public $totalentries = null; /** * An array of blog_filter_* objects @@ -608,9 +591,12 @@ class blog_listing { public function get_entries($start=0, $limit=10) { global $DB; - if (empty($this->entries)) { + if ($this->entries === null) { if ($sqlarray = $this->get_entry_fetch_sql(false, 'created DESC')) { $this->entries = $DB->get_records_sql($sqlarray['sql'], $sqlarray['params'], $start, $limit); + if (!$start && count($this->entries) < $limit) { + $this->totalentries = count($this->entries); + } } else { return false; } @@ -619,6 +605,23 @@ class blog_listing { return $this->entries; } + /** + * Finds total number of blog entries + * + * @return int + */ + public function count_entries() { + global $DB; + if ($this->totalentries === null) { + if ($sqlarray = $this->get_entry_fetch_sql(true)) { + $this->totalentries = $DB->count_records_sql($sqlarray['sql'], $sqlarray['params']); + } else { + $this->totalentries = 0; + } + } + return $this->totalentries; + } + public function get_entry_fetch_sql($count=false, $sort='lastmodified DESC', $userid = false) { global $DB, $USER, $CFG; @@ -626,7 +629,7 @@ class blog_listing { $userid = $USER->id; } - $allnamefields = get_all_user_name_fields(true, 'u'); + $allnamefields = \user_picture::fields('u', null, 'useridalias'); // The query used to locate blog entries is complicated. It will be built from the following components: $requiredfields = "p.*, $allnamefields, u.email"; // The SELECT clause. $tables = array('p' => 'post', 'u' => 'user'); // Components of the FROM clause (table_id => table_name). @@ -707,13 +710,8 @@ class blog_listing { $morelink = '
      '; - if ($sqlarray = $this->get_entry_fetch_sql(true)) { - $totalentries = $DB->count_records_sql($sqlarray['sql'], $sqlarray['params']); - } else { - $totalentries = 0; - } - $entries = $this->get_entries($start, $limit); + $totalentries = $this->count_entries(); $pagingbar = new paging_bar($totalentries, $page, $limit, $this->get_baseurl()); $pagingbar->pagevar = 'blogpage'; $blogheaders = blog_get_headers(); @@ -909,13 +907,14 @@ class blog_filter_context extends blog_filter { $this->availabletypes = array('site' => get_string('site'), 'course' => get_string('course'), - 'module' => get_string('activity')); + 'module' => get_string('activity'), + 'context' => get_string('coresystem')); switch ($this->type) { case 'course': // Careful of site course! // Ignore course filter if blog associations are not enabled. if ($this->id != $SITE->id && !empty($CFG->useblogassociations)) { - $this->overrides = array('site'); + $this->overrides = array('site', 'context'); $context = context_course::instance($this->id); $this->tables['ba'] = 'blog_association'; $this->conditions[] = 'p.id = ba.blogid'; @@ -930,7 +929,7 @@ class blog_filter_context extends blog_filter { break; case 'module': if (!empty($CFG->useblogassociations)) { - $this->overrides = array('course', 'site'); + $this->overrides = array('course', 'site', 'context'); $context = context_module::instance($this->id); $this->tables['ba'] = 'blog_association'; @@ -939,6 +938,19 @@ class blog_filter_context extends blog_filter { $this->params = array($context->id); } break; + case 'context': + if ($id != context_system::instance()->id && !empty($CFG->useblogassociations)) { + $this->overrides = array('site'); + $context = context::instance_by_id($this->id); + $this->tables['ba'] = 'blog_association'; + $this->tables['ctx'] = 'context'; + $this->conditions[] = 'p.id = ba.blogid'; + $this->conditions[] = 'ctx.id = ba.contextid'; + $this->conditions[] = 'ctx.path LIKE ?'; + $this->params = array($context->path . '%'); + } + break; + } } } @@ -1012,6 +1024,7 @@ class blog_filter_tag extends blog_filter { $this->conditions = array('ti.tagid = t.id', "ti.itemtype = 'post'", + "ti.component = 'core'", 'ti.itemid = p.id', 't.id = ?'); $this->params = array($this->id); diff --git a/blog/renderer.php b/blog/renderer.php index 1045a898685..640686571c2 100644 --- a/blog/renderer.php +++ b/blog/renderer.php @@ -132,21 +132,7 @@ class core_blog_renderer extends plugin_renderer_base { } // Links to tags. - $officialtags = tag_get_tags_csv('post', $entry->id, TAG_RETURN_HTML, 'official'); - $defaulttags = tag_get_tags_csv('post', $entry->id, TAG_RETURN_HTML, 'default'); - - if (!empty($CFG->usetags) && ($officialtags || $defaulttags) ) { - $o .= $this->output->container_start('tags'); - - if ($officialtags) { - $o .= get_string('tags', 'tag') .': '. $this->output->container($officialtags, 'officialblogtags'); - if ($defaulttags) { - $o .= ', '; - } - } - $o .= $defaulttags; - $o .= $this->output->container_end(); - } + $o .= $this->output->tag_list(core_tag_tag::get_item_tags('core', 'post', $entry->id)); // Add associations. if (!empty($CFG->useblogassociations) && !empty($entry->renderable->blogassociations)) { diff --git a/blog/rsslib.php b/blog/rsslib.php index cad38d88d50..366c5cd5654 100644 --- a/blog/rsslib.php +++ b/blog/rsslib.php @@ -217,10 +217,8 @@ function blog_rss_get_feed($context, $args) { $summary = file_rewrite_pluginfile_urls($blogentry->summary, 'pluginfile.php', $sitecontext->id, 'blog', 'post', $blogentry->id); $item->description = format_text($summary, $blogentry->format); - if ( !empty($CFG->usetags) && ($blogtags = tag_get_tags_array('post', $blogentry->id)) ) { - if ($blogtags) { - $item->tags = $blogtags; - } + if ($blogtags = core_tag_tag::get_item_tags_array('core', 'post', $blogentry->id)) { + $item->tags = $blogtags; $item->tagscheme = $CFG->wwwroot . '/tag'; } $items[] = $item; diff --git a/blog/tests/lib_test.php b/blog/tests/lib_test.php index 1eb6583868f..81eac309c7e 100644 --- a/blog/tests/lib_test.php +++ b/blog/tests/lib_test.php @@ -65,18 +65,15 @@ class core_blog_lib_testcase extends advanced_testcase { )); // Create default tag. - $tag = new stdClass(); - $tag->userid = $user->id; - $tag->name = 'testtagname'; - $tag->rawname = 'Testtagname'; - $tag->tagtype = 'official'; - $tag->id = $DB->insert_record('tag', $tag); + $tag = $this->getDataGenerator()->create_tag(array('userid' => $user->id, + 'rawname' => 'Testtagname', 'tagtype' => 'official')); // Create default post. $post = new stdClass(); $post->userid = $user->id; $post->groupid = $group->id; $post->content = 'test post content text'; + $post->module = 'blog'; $post->id = $DB->insert_record('post', $post); // Grab important ids. @@ -544,5 +541,66 @@ class core_blog_lib_testcase extends advanced_testcase { $nodes->setAccessible(true); $this->assertArrayNotHasKey('blogs', $nodes->getValue($tree)); } + + public function test_blog_get_listing_course() { + $this->setAdminUser(); + $coursecontext = context_course::instance($this->courseid); + $anothercourse = $this->getDataGenerator()->create_course(); + + // Add blog associations with a course. + $blog = new blog_entry($this->postid); + $blog->add_association($coursecontext->id); + + // There is one entry associated with a course. + $bloglisting = new blog_listing(array('course' => $this->courseid)); + $this->assertCount(1, $bloglisting->get_entries()); + + // There is no entry associated with a wrong course. + $bloglisting = new blog_listing(array('course' => $anothercourse->id)); + $this->assertCount(0, $bloglisting->get_entries()); + + // There is no entry associated with a module. + $bloglisting = new blog_listing(array('module' => $this->cmid)); + $this->assertCount(0, $bloglisting->get_entries()); + + // There is one entry associated with a site (id is ignored). + $bloglisting = new blog_listing(array('site' => 12345)); + $this->assertCount(1, $bloglisting->get_entries()); + + // There is one entry associated with course context. + $bloglisting = new blog_listing(array('context' => $coursecontext->id)); + $this->assertCount(1, $bloglisting->get_entries()); + } + + public function test_blog_get_listing_module() { + $this->setAdminUser(); + $coursecontext = context_course::instance($this->courseid); + $contextmodule = context_module::instance($this->cmid); + $anothermodule = $this->getDataGenerator()->create_module('page', array('course' => $this->courseid)); + + // Add blog associations with a course. + $blog = new blog_entry($this->postid); + $blog->add_association($contextmodule->id); + + // There is no entry associated with a course. + $bloglisting = new blog_listing(array('course' => $this->courseid)); + $this->assertCount(0, $bloglisting->get_entries()); + + // There is one entry associated with a module. + $bloglisting = new blog_listing(array('module' => $this->cmid)); + $this->assertCount(1, $bloglisting->get_entries()); + + // There is no entry associated with a wrong module. + $bloglisting = new blog_listing(array('module' => $anothermodule->cmid)); + $this->assertCount(0, $bloglisting->get_entries()); + + // There is one entry associated with a site (id is ignored). + $bloglisting = new blog_listing(array('site' => 12345)); + $this->assertCount(1, $bloglisting->get_entries()); + + // There is one entry associated with course context (module is a subcontext of a course). + $bloglisting = new blog_listing(array('context' => $coursecontext->id)); + $this->assertCount(1, $bloglisting->get_entries()); + } } diff --git a/course/edit.php b/course/edit.php index 0189fae101b..d79c0d6ae6f 100644 --- a/course/edit.php +++ b/course/edit.php @@ -123,10 +123,7 @@ if (!empty($course)) { } // Populate course tags. - if (!empty($CFG->usetags)) { - include_once($CFG->dirroot.'/tag/lib.php'); - $course->tags = tag_get_tags_array('course', $course->id); - } + $course->tags = core_tag_tag::get_item_tags_array('core', 'course', $course->id); } else { // Editor should respect category context if course context is not set. diff --git a/course/edit_form.php b/course/edit_form.php index 94d260c2ab3..7c9f43edf99 100644 --- a/course/edit_form.php +++ b/course/edit_form.php @@ -304,11 +304,12 @@ class course_edit_form extends moodleform { } } - if (!empty($CFG->usetags) && + if (core_tag_tag::is_enabled('core', 'course') && ((empty($course->id) && guess_if_creator_will_have_course_capability('moodle/course:tag', $categorycontext)) || (!empty($course->id) && has_capability('moodle/course:tag', $coursecontext)))) { $mform->addElement('header', 'tagshdr', get_string('tags', 'tag')); - $mform->addElement('tags', 'tags', get_string('tags')); + $mform->addElement('tags', 'tags', get_string('tags'), + array('itemtype' => 'course', 'component' => 'core')); } // When two elements we need a group. diff --git a/course/lib.php b/course/lib.php index 21c44813573..0c2acd3601b 100644 --- a/course/lib.php +++ b/course/lib.php @@ -1648,7 +1648,6 @@ function course_delete_module($cmid) { require_once($CFG->libdir.'/questionlib.php'); require_once($CFG->dirroot.'/blog/lib.php'); require_once($CFG->dirroot.'/calendar/lib.php'); - require_once($CFG->dirroot.'/tag/lib.php'); // Get the course module. if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) { @@ -1717,7 +1716,7 @@ function course_delete_module($cmid) { 'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY)); // Delete all tag instances associated with the instance of this module. - tag_delete_instances('mod_' . $modulename, $modcontext->id); + core_tag_tag::delete_instances('mod_' . $modulename, null, $modcontext->id); // Delete the context. context_helper::delete_instance(CONTEXT_MODULE, $cm->id); @@ -2583,7 +2582,6 @@ function course_overviewfiles_options($course) { */ function create_course($data, $editoroptions = NULL) { global $DB, $CFG; - require_once($CFG->dirroot.'/tag/lib.php'); //check the categoryid - must be given for all new courses $category = $DB->get_record('course_categories', array('id'=>$data->category), '*', MUST_EXIST); @@ -2669,8 +2667,8 @@ function create_course($data, $editoroptions = NULL) { enrol_course_updated(true, $course, $data); // Update course tags. - if ($CFG->usetags && isset($data->tags)) { - tag_set('course', $course->id, $data->tags, 'core', context_course::instance($course->id)->id); + if (isset($data->tags)) { + core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags); } return $course; @@ -2688,7 +2686,6 @@ function create_course($data, $editoroptions = NULL) { */ function update_course($data, $editoroptions = NULL) { global $DB, $CFG; - require_once($CFG->dirroot.'/tag/lib.php'); $data->timemodified = time(); @@ -2776,8 +2773,8 @@ function update_course($data, $editoroptions = NULL) { enrol_course_updated(false, $course, $data); // Update course tags. - if ($CFG->usetags && isset($data->tags)) { - tag_set('course', $course->id, $data->tags, 'core', context_course::instance($course->id)->id); + if (isset($data->tags)) { + core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags); } // Trigger a course updated event. @@ -3829,3 +3826,36 @@ function course_view($context, $sectionnumber = 0) { $event = \core\event\course_viewed::create($eventdata); $event->trigger(); } + +/** + * Returns courses tagged with a specified tag. + * + * @param core_tag_tag $tag + * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag + * are displayed on the page and the per-page limit may be bigger + * @param int $fromctx context id where the link was displayed, may be used by callbacks + * to display items in the same context first + * @param int $ctx context id where to search for records + * @param bool $rec search in subcontexts as well + * @param int $page 0-based number of page being displayed + * @return \core_tag\output\tagindex + */ +function course_get_tagged_courses($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) { + global $CFG, $PAGE; + require_once($CFG->libdir . '/coursecatlib.php'); + + $perpage = $exclusivemode ? $CFG->coursesperpage : 5; + $displayoptions = array( + 'limit' => $perpage, + 'offset' => $page * $perpage, + 'viewmoreurl' => null, + ); + + $courserenderer = $PAGE->get_renderer('core', 'course'); + $totalcount = coursecat::search_courses_count(array('tagid' => $tag->id, 'ctx' => $ctx, 'rec' => $rec)); + $content = $courserenderer->tagged_courses($tag->id, $exclusivemode, $ctx, $rec, $displayoptions); + $totalpages = ceil($totalcount / $perpage); + + return new core_tag\output\tagindex($tag, 'core', 'course', $content, + $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); +} diff --git a/course/renderer.php b/course/renderer.php index ec26762998e..b71d4882d41 100644 --- a/course/renderer.php +++ b/course/renderer.php @@ -1896,29 +1896,52 @@ class core_course_renderer extends plugin_renderer_base { * Renders html to print list of courses tagged with particular tag * * @param int $tagid id of the tag + * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag + * are displayed on the page and the per-page limit may be bigger + * @param int $fromctx context id where the link was displayed, may be used by callbacks + * to display items in the same context first + * @param int $ctx context id where to search for records + * @param bool $rec search in subcontexts as well + * @param array $displayoptions * @return string empty string if no courses are marked with this tag or rendered list of courses */ - public function tagged_courses($tagid) { + public function tagged_courses($tagid, $exclusivemode = true, $ctx = 0, $rec = true, $displayoptions = null) { global $CFG; - require_once($CFG->libdir. '/coursecatlib.php'); - $displayoptions = array('limit' => $CFG->coursesperpage); - $displayoptions['viewmoreurl'] = new moodle_url('/course/search.php', - array('tagid' => $tagid, 'page' => 1, 'perpage' => $CFG->coursesperpage)); - $displayoptions['viewmoretext'] = new lang_string('findmorecourses'); + require_once($CFG->libdir . '/coursecatlib.php'); + if (empty($displayoptions)) { + $displayoptions = array(); + } + $showcategories = coursecat::count_all() > 1; + $displayoptions += array('limit' => $CFG->coursesperpage, 'offset' => 0); $chelper = new coursecat_helper(); - $searchcriteria = array('tagid' => $tagid); - $chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_EXPANDED_WITH_CAT)-> - set_search_criteria(array('tagid' => $tagid))-> + $searchcriteria = array('tagid' => $tagid, 'ctx' => $ctx, 'rec' => $rec); + $chelper->set_show_courses($showcategories ? self::COURSECAT_SHOW_COURSES_EXPANDED_WITH_CAT : + self::COURSECAT_SHOW_COURSES_EXPANDED)-> + set_search_criteria($searchcriteria)-> set_courses_display_options($displayoptions)-> set_attributes(array('class' => 'course-search-result course-search-result-tagid')); // (we set the same css class as in search results by tagid) - $courses = coursecat::search_courses($searchcriteria, $chelper->get_courses_display_options()); - $totalcount = coursecat::search_courses_count($searchcriteria); - $content = $this->coursecat_courses($chelper, $courses, $totalcount); - if ($totalcount) { - require_once $CFG->dirroot.'/tag/lib.php'; - $heading = get_string('courses') . ' ' . get_string('taggedwith', 'tag', tag_get_name($tagid)) .': '. $totalcount; - return $this->heading($heading, 3). $content; + if ($totalcount = coursecat::search_courses_count($searchcriteria)) { + $courses = coursecat::search_courses($searchcriteria, $chelper->get_courses_display_options()); + if ($exclusivemode) { + return $this->coursecat_courses($chelper, $courses, $totalcount); + } else { + $tagfeed = new core_tag\output\tagfeed(); + $img = $this->output->pix_icon('i/course', ''); + foreach ($courses as $course) { + $url = course_get_url($course); + $imgwithlink = html_writer::link($url, $img); + $coursename = html_writer::link($url, $course->get_formatted_name()); + $details = ''; + if ($showcategories && ($cat = coursecat::get($course->category, IGNORE_MISSING))) { + $details = get_string('category').': '. + html_writer::link(new moodle_url('/course/index.php', array('categoryid' => $cat->id)), + $cat->get_formatted_name(), array('class' => $cat->visible ? '' : 'dimmed')); + } + $tagfeed->add($imgwithlink, $coursename, $details); + } + return $this->output->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($this->output)); + } } return ''; } diff --git a/course/tags.php b/course/tags.php index 8e81fb1e79e..1c7ad66aedd 100644 --- a/course/tags.php +++ b/course/tags.php @@ -23,7 +23,6 @@ */ require_once("../config.php"); -require_once($CFG->dirroot . '/tag/lib.php'); require_once($CFG->dirroot . '/course/tags_form.php'); $id = required_param('id', PARAM_INT); // Course id. @@ -38,7 +37,7 @@ if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $co print_error('coursehidden', '', $CFG->wwwroot .'/'); } require_capability('moodle/course:tag', $context); -if (empty($CFG->usetags)) { +if (!core_tag_tag::is_enabled('core', 'course')) { print_error('tagsaredisabled', 'tag'); } @@ -49,14 +48,14 @@ $PAGE->set_title(get_string('coursetags', 'tag')); $PAGE->set_heading($course->fullname); $form = new coursetags_form(); -$data = array('id' => $course->id, 'tags' => tag_get_tags_array('course', $course->id)); +$data = array('id' => $course->id, 'tags' => core_tag_tag::get_item_tags_array('core', 'course', $course->id)); $form->set_data($data); $redirecturl = $returnurl ? new moodle_url($returnurl) : course_get_url($course); if ($form->is_cancelled()) { redirect($redirecturl); } else if ($data = $form->get_data()) { - tag_set('course', $course->id, $data->tags, 'core', context_course::instance($course->id)->id); + core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags); redirect($redirecturl); } diff --git a/course/tags_form.php b/course/tags_form.php index 08cacc02e4a..fc54c03f11e 100644 --- a/course/tags_form.php +++ b/course/tags_form.php @@ -41,7 +41,8 @@ class coursetags_form extends moodleform { public function definition() { $mform = $this->_form; - $mform->addElement('tags', 'tags', get_string('tags')); + $mform->addElement('tags', 'tags', get_string('tags'), + array('itemtype' => 'course', 'component' => 'core')); $mform->addElement('hidden', 'id', null); $mform->setType('id', PARAM_INT); diff --git a/course/tests/courselib_test.php b/course/tests/courselib_test.php index 421b1e4c2eb..de740656f85 100644 --- a/course/tests/courselib_test.php +++ b/course/tests/courselib_test.php @@ -29,7 +29,6 @@ global $CFG; require_once($CFG->dirroot . '/course/lib.php'); require_once($CFG->dirroot . '/course/tests/fixtures/course_capability_assignment.php'); require_once($CFG->dirroot . '/enrol/imsenterprise/tests/imsenterprise_test.php'); -require_once($CFG->dirroot . '/tag/lib.php'); class core_course_courselib_testcase extends advanced_testcase { @@ -1501,6 +1500,7 @@ class core_course_courselib_testcase extends advanced_testcase { */ public function test_course_delete_module($type, $options) { global $DB; + $this->resetAfterTest(true); $this->setAdminUser(); @@ -1521,7 +1521,7 @@ class core_course_courselib_testcase extends advanced_testcase { switch ($type) { case 'assign': // Add some tags to this assignment. - tag_set('assign', $module->id, array('Tag 1', 'Tag 2', 'Tag 3'), 'mod_assign', $modcontext->id); + core_tag_tag::set_item_tags('mod_assign', 'assign', $module->id, $modcontext, array('Tag 1', 'Tag 2', 'Tag 3')); // Confirm the tag instances were added. $criteria = array('component' => 'mod_assign', 'contextid' => $modcontext->id); diff --git a/course/tests/externallib_test.php b/course/tests/externallib_test.php index c5a6464edf3..10a84ba332d 100644 --- a/course/tests/externallib_test.php +++ b/course/tests/externallib_test.php @@ -605,8 +605,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase { */ public function test_search_courses () { - global $DB, $CFG; - require_once($CFG->dirroot . '/tag/lib.php'); + global $DB; $this->resetAfterTest(true); $this->setAdminUser(); @@ -636,8 +635,10 @@ class core_course_externallib_testcase extends externallib_advanced_testcase { // Enable coursetag option. set_config('block_tags_showcoursetags', true); // Add tag 'TAG-LABEL ON SECOND COURSE' to Course2. - tag_set('course', $course2->id, array('TAG-LABEL ON SECOND COURSE'), 'core', context_course::instance($course2->id)->id); - $taginstance = $DB->get_record('tag_instance', array('itemtype' => 'course', 'itemid' => $course2->id), '*', MUST_EXIST); + core_tag_tag::set_item_tags('core', 'course', $course2->id, context_course::instance($course2->id), + array('TAG-LABEL ON SECOND COURSE')); + $taginstance = $DB->get_record('tag_instance', + array('itemtype' => 'course', 'itemid' => $course2->id), '*', MUST_EXIST); // Search by tagid. $results = core_course_external::search_courses('tagid', $taginstance->tagid); $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results); diff --git a/lang/en/cache.php b/lang/en/cache.php index a44742e19fa..3a2c783c28e 100644 --- a/lang/en/cache.php +++ b/lang/en/cache.php @@ -60,6 +60,7 @@ $string['cachedef_plugin_manager'] = 'Plugin info manager'; $string['cachedef_questiondata'] = 'Question definitions'; $string['cachedef_repositories'] = 'Repositories instances data'; $string['cachedef_string'] = 'Language string cache'; +$string['cachedef_tags'] = 'Tags collections and areas'; $string['cachedef_userselections'] = 'Data used to persist user selections throughout Moodle'; $string['cachedef_yuimodules'] = 'YUI Module definitions'; $string['cachelock_file_default'] = 'Default file locking'; diff --git a/lang/en/deprecated.txt b/lang/en/deprecated.txt index ab59bdb7fc6..366e26bc5a2 100644 --- a/lang/en/deprecated.txt +++ b/lang/en/deprecated.txt @@ -20,3 +20,4 @@ updated,core_tag withselectedtags,core_tag tag:create,core_role categoriesanditems,core_grades +taggedwith,core_tag diff --git a/lang/en/question.php b/lang/en/question.php index 2ce0f2c175d..c4ea1b40553 100644 --- a/lang/en/question.php +++ b/lang/en/question.php @@ -421,6 +421,7 @@ $string['submissionoutofsequencefriendlymessage'] = "You have entered data outsi $string['submit'] = 'Submit'; $string['submitandfinish'] = 'Submit and finish'; $string['submitted'] = 'Submit: {$a}'; +$string['tagarea_question'] = 'Questions'; $string['technicalinfo'] = 'Technical information'; $string['technicalinfo_help'] = 'This technical information is probably only useful for developers working on new question types. It may also be helpful when trying to diagnose problems with questions.'; $string['technicalinfominfraction'] = 'Minimum fraction: {$a}'; diff --git a/lang/en/tag.php b/lang/en/tag.php index 6065fb41322..e20a6031c95 100644 --- a/lang/en/tag.php +++ b/lang/en/tag.php @@ -24,12 +24,19 @@ $string['added'] = 'Official tag(s) added'; $string['addotags'] = 'Add official tags'; +$string['addtagcoll'] = 'Add tag collection'; $string['addtagtomyinterests'] = 'Add "{$a}" to my interests'; $string['alltagpages'] = 'All tag pages'; +$string['backtoallitems'] = 'Back to all items tagged with "{$a}"'; +$string['changessaved'] = 'Changes saved'; +$string['changetagcoll'] = 'Change tag collection of area {$a}'; +$string['collnameexplained'] = 'Leave the field empty to use the default value: {$a}'; +$string['component'] = 'Component'; $string['confirmdeletetag'] = 'Are you sure you want to delete this tag?'; $string['confirmdeletetags'] = 'Are you sure you want to delete selected tags?'; $string['count'] = 'Count'; $string['coursetags'] = 'Course tags'; +$string['defautltagcoll'] = 'Default collection'; $string['delete'] = 'Delete'; $string['deleteselected'] = 'Delete selected'; $string['deleted'] = 'Tag(s) deleted'; @@ -38,15 +45,20 @@ $string['description'] = 'Description'; $string['editname'] = 'Edit tag name'; $string['edittag'] = 'Edit this tag'; $string['entertags'] = 'Enter tags...'; +$string['edittagcoll'] = 'Edit tag collection {$a}'; $string['errortagfrontpage'] = 'Tagging the site main page is not allowed'; $string['errorupdatingrecord'] = 'Error updating tag record'; $string['eventtagadded'] = 'Tag added to an item'; +$string['eventtagcolldeleted'] = 'Tag collection deleted'; +$string['eventtagcollcreated'] = 'Tag collection created'; +$string['eventtagcollupdated'] = 'Tag collection updatedhp '; $string['eventtagcreated'] = 'Tag created'; $string['eventtagdeleted'] = 'Tag deleted'; $string['eventtagflagged'] = 'Tag flagged'; $string['eventtagremoved'] = 'Tag removed from an item'; $string['eventtagunflagged'] = 'Tag unflagged'; $string['eventtagupdated'] = 'Tag updated'; +$string['exclusivemode'] = 'Show only tagged {$a->tagarea}'; $string['flag'] = 'Flag'; $string['flagged'] = 'Tag flagged'; $string['flagasinappropriate'] = 'Flag as inappropriate'; @@ -54,17 +66,25 @@ $string['helprelatedtags'] = 'Comma separated related tags'; $string['changename'] = 'Change tag name'; $string['changetype'] = 'Change tag type'; $string['id'] = 'id'; +$string['inalltagcoll'] = 'Everywhere'; +$string['itemstaggedwith'] = '{$a->tagarea} tagged with "{$a->tag}"'; +$string['lesstags'] = 'less...'; $string['manageofficialtags'] = 'Manage official tags'; $string['managetags'] = 'Manage tags'; +$string['managetagcolls'] = 'Manage tag collections'; +$string['moretags'] = 'more...'; $string['name'] = 'Tag name'; $string['namesalreadybeeingused'] = 'Tag names already being used'; $string['newnamefor'] = 'New name for tag {$a}'; +$string['nextpage'] = 'More'; +$string['notagsfound'] = 'No tags matching "{$a}" found'; $string['noresultsfor'] = 'No results for "{$a}"'; $string['nothingtoupdate'] = 'Nothing to update'; $string['officialtag'] = 'Official'; $string['otags'] = 'Official tags'; $string['othertags'] = 'Other tags'; $string['owner'] = 'Owner'; +$string['prevpage'] = 'Back'; $string['ptags'] = 'User defined tags (Comma separated)'; $string['relatedblogs'] = 'Most recent blog entries'; $string['relatedtags'] = 'Related tags'; @@ -75,16 +95,29 @@ $string['responsiblewillbenotified'] = 'The person responsible will be notified' $string['rssdesc'] = 'This RSS feed was automatically generated by Moodle and contains user generated tags for courses.'; $string['rsstitle'] = 'Course tags RSS feed for user: {$a}'; $string['search'] = 'Search'; +$string['searchable'] = 'Searchable'; +$string['searchable_help'] = 'Tags in this tag collection can be searched for on "Search tags" page. If unchecked, tags can still be accessed by clicking on them or via different search interfaces.'; $string['searchresultsfor'] = 'Search results for "{$a}"'; $string['searchtags'] = 'Search tags'; -$string['seeallblogs'] = 'See all blog entries tagged with "{$a}"...'; +$string['seeallblogs'] = 'See all blog entries tagged with "{$a}"'; $string['select'] = 'Select'; +$string['selectcoll'] = 'Select tag collection'; $string['selecttag'] = 'Select tag {$a}'; $string['settypedefault'] = 'Remove from official tags'; $string['settypeofficial'] = 'Make official'; +$string['showingfirsttags'] = 'Showing {$a} most popular tags'; +$string['suredeletecoll'] = 'Are you sure you want to delete tag collection "{$a}"?'; $string['tag'] = 'Tag'; +$string['tagarea_blog_external'] = 'External blog posts'; +$string['tagarea_post'] = 'Blog posts'; +$string['tagarea_user'] = 'User interests'; +$string['tagarea_course'] = 'Courses'; +$string['tagareaenabled'] = 'Enabled'; +$string['tagareaname'] = 'Name'; +$string['tagareas'] = 'Tag areas'; +$string['tagcollection'] = 'Tag collection'; +$string['tagcollections'] = 'Tag collections'; $string['tagdescription'] = 'Tag description'; -$string['taggedwith'] = 'tagged with "{$a}"'; $string['tags'] = 'Tags'; $string['tagsaredisabled'] = 'Tags are disabled'; $string['tagtype'] = 'Tag type'; @@ -107,3 +140,7 @@ $string['tagtype_official'] = 'Official'; $string['thistaghasnodesc'] = 'This tag currently has no description.'; $string['updated'] = 'Updated'; $string['withselectedtags'] = 'With selected tags...'; + +// Deprecated since 3.1 . + +$string['taggedwith'] = 'tagged with "{$a}"'; diff --git a/lib/adminlib.php b/lib/adminlib.php index 3bcb96658b6..c0ca283b2e9 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -166,9 +166,8 @@ function uninstall_plugin($type, $name) { echo $OUTPUT->heading($pluginname); - // Delete all tag instances associated with this plugin. - require_once($CFG->dirroot . '/tag/lib.php'); - tag_delete_instances($component); + // Delete all tag areas, collections and instances associated with this plugin. + core_tag_area::uninstall($component); // Custom plugin uninstall. $plugindirectory = core_component::get_plugin_directory($type, $name); diff --git a/lib/amd/build/tag.min.js b/lib/amd/build/tag.min.js index 8d684ae99fe..7a4328f304b 100644 --- a/lib/amd/build/tag.min.js +++ b/lib/amd/build/tag.min.js @@ -1 +1 @@ -define(["jquery","core/ajax","core/templates","core/notification","core/str","core/config"],function(a,b,c,d,e,f){return{init_manage_page:function(){var g=function(b){var c=b.closest("tr").get(0);if(c){var d=a(c).find("td.col-timemodified").get(0);e.get_string("now").done(function(b){a(d).html(b)})}};a(".tag-management-table").delegate(".tagtype","click",function(d){d.preventDefault();var e=a(this),f=e.attr("data-id"),h=e.attr("data-value"),i="1"===h?0:1,j=b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:f,official:i}]}},{methodname:"core_tag_get_tags",args:{tags:[{id:f}]}}],!0);a.when.apply(a,j).done(function(a,b){void 0===a.warnings[0]&&void 0!==b.tags[0]&&c.render("core_tag/tagtype",b.tags[0]).done(function(a){g(e);var b=e.parent();e.replaceWith(a),b.find(".tagtype").get(0).focus()})})}),a(".tag-management-table").delegate(".tagflag","click",function(d){d.preventDefault();var e=a(this),f=e.attr("data-id"),h=e.attr("data-value"),i="0"===h?1:0,j=b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:f,flag:i}]}},{methodname:"core_tag_get_tags",args:{tags:[{id:f}]}}],!0);a.when.apply(a,j).done(function(b,d){if(void 0===b.warnings[0]&&void 0!==d.tags[0]){var f=e.closest("tr").get(0);f&&(d.tags[0].flag?a(f).addClass("flagged-tag"):a(f).removeClass("flagged-tag")),c.render("core_tag/tagflag",d.tags[0]).done(function(a){g(e);var b=e.parent();e.replaceWith(a),b.find(".tagflag").get(0).focus()})}})}),a(".tag-management-table").delegate("a.tagdelete","click",function(b){b.preventDefault();var c=a(this).attr("href");e.get_strings([{key:"delete"},{key:"confirmdeletetag",component:"tag"},{key:"yes"},{key:"no"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],function(){window.location.href=c})})}),a("#tag-management-delete").click(function(b){var c=a(this).closest("form").get(0),f=a(c).find("input[type=checkbox]:checked").length;return f?(b.preventDefault(),void e.get_strings([{key:"delete"},{key:"confirmdeletetags",component:"tag"},{key:"yes"},{key:"no"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],function(){c.submit()})})):!1}),a(".tag-management-table").delegate(".tagnameedit","click keypress",function(h){if("keypress"!==h.type||13===h.keyCode){h.stopImmediatePropagation(),h.preventDefault();var i=a(this),j=a(i.closest("td").get(0)),k=a(j.find("input").get(0)),l=i.attr("data-id"),m=function(f,h){var i=b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:f,rawname:h}]}},{methodname:"core_tag_get_tags",args:{tags:[{id:f}]}}],!0);a.when.apply(a,i).done(function(b,f){void 0!==b.warnings[0]?e.get_string("error").done(function(a){d.alert(a,b.warnings[0].message)}):void 0!==f.tags[0]&&c.render("core_tag/tagname",f.tags[0]).done(function(b){g(j),j.html(b),a(j.find(".tagnameedit").get(0)).focus()})})},n=function(){a(".tag-management-table td.tageditingon").each(function(){var b=a(this),c=a(b.find("input").get(0));c.off(),b.removeClass("tageditingon"),c.val(b.attr("data-value"))})};n(),j.addClass("tageditingon"),j.attr("data-value",k.val()),k.select(),k.on("keypress focusout",function(a){f.behatsiterunning&&"focusout"===a.type||("keypress"===a.type&&13===a.keyCode&&(m(l,k.val()),n()),("keypress"===a.type&&27===a.keyCode||"focusout"===a.type)&&n())})}})}}}); \ No newline at end of file +define(["jquery","core/ajax","core/templates","core/notification","core/str","core/config"],function(a,b,c,d,e,f){return{init_tagindex_page:function(){a("body").delegate(".tagarea[data-ta] a[data-quickload=1]","click",function(d){d.preventDefault();var e=a(this),f=e.context.search.replace(/^\?/,""),g=e.closest(".tagarea[data-ta]"),h=f.split("&").reduce(function(a,b){var c=b.split("=");return a[c[0]]=decodeURIComponent(c[1]),a},{}),i=b.call([{methodname:"core_tag_get_tagindex",args:{tagindex:h}}],!0);a.when.apply(a,i).done(function(a){c.render("core_tag/index",a).done(function(a){g.replaceWith(a)})})})},init_manage_page:function(){var g=function(b){var c=b.closest("tr").get(0);if(c){var d=a(c).find("td.col-timemodified").get(0);e.get_string("now").done(function(b){a(d).html(b)})}};a(".tag-management-table").delegate(".tagtype","click",function(d){d.preventDefault();var e=a(this),f=e.attr("data-id"),h=e.attr("data-value"),i="1"===h?0:1,j=b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:f,official:i}]}},{methodname:"core_tag_get_tags",args:{tags:[{id:f}]}}],!0);a.when.apply(a,j).done(function(a,b){void 0===a.warnings[0]&&void 0!==b.tags[0]&&c.render("core_tag/tagtype",b.tags[0]).done(function(a){g(e);var b=e.parent();e.replaceWith(a),b.find(".tagtype").get(0).focus()})})}),a(".tag-management-table").delegate(".tagflag","click",function(d){d.preventDefault();var e=a(this),f=e.attr("data-id"),h=e.attr("data-value"),i="0"===h?1:0,j=b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:f,flag:i}]}},{methodname:"core_tag_get_tags",args:{tags:[{id:f}]}}],!0);a.when.apply(a,j).done(function(b,d){if(void 0===b.warnings[0]&&void 0!==d.tags[0]){var f=e.closest("tr").get(0);f&&(d.tags[0].flag?a(f).addClass("flagged-tag"):a(f).removeClass("flagged-tag")),c.render("core_tag/tagflag",d.tags[0]).done(function(a){g(e);var b=e.parent();e.replaceWith(a),b.find(".tagflag").get(0).focus()})}})}),a(".tag-management-table").delegate("a.tagdelete","click",function(b){b.preventDefault();var c=a(this).attr("href");e.get_strings([{key:"delete"},{key:"confirmdeletetag",component:"tag"},{key:"yes"},{key:"no"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],function(){window.location.href=c})})}),a("#tag-management-delete").click(function(b){var c=a(this).closest("form").get(0),f=a(c).find("input[type=checkbox]:checked").length;return f?(b.preventDefault(),void e.get_strings([{key:"delete"},{key:"confirmdeletetags",component:"tag"},{key:"yes"},{key:"no"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],function(){c.submit()})})):!1}),a(".tag-management-table").delegate(".tagnameedit","click keypress",function(h){if("keypress"!==h.type||13===h.keyCode){h.stopImmediatePropagation(),h.preventDefault();var i=a(this),j=a(i.closest("td").get(0)),k=a(j.find("input").get(0)),l=i.attr("data-id"),m=function(f,h){var i=b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:f,rawname:h}]}},{methodname:"core_tag_get_tags",args:{tags:[{id:f}]}}],!0);a.when.apply(a,i).done(function(b,f){void 0!==b.warnings[0]?e.get_string("error").done(function(a){d.alert(a,b.warnings[0].message)}):void 0!==f.tags[0]&&c.render("core_tag/tagname",f.tags[0]).done(function(b){g(j),j.html(b),a(j.find(".tagnameedit").get(0)).focus()})})},n=function(){a(".tag-management-table td.tageditingon").each(function(){var b=a(this),c=a(b.find("input").get(0));c.off(),b.removeClass("tageditingon"),c.val(b.attr("data-value"))})};n(),j.addClass("tageditingon"),j.attr("data-value",k.val()),k.select(),k.on("keypress focusout",function(a){f.behatsiterunning&&"focusout"===a.type||("keypress"===a.type&&13===a.keyCode&&(m(l,k.val()),n()),("keypress"===a.type&&27===a.keyCode||"focusout"===a.type)&&n())})}})}}}); \ No newline at end of file diff --git a/lib/amd/src/tag.js b/lib/amd/src/tag.js index 0edf6ef2aa4..01ef814a7ac 100644 --- a/lib/amd/src/tag.js +++ b/lib/amd/src/tag.js @@ -27,9 +27,37 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str' return /** @alias module:core/tag */ { /** - * Initialises handlers for AJAX methods. + * Initialises tag index page. * - * @method init + * @method init_tagindex_page + */ + init_tagindex_page: function() { + // Click handler for changing tag type. + $('body').delegate('.tagarea[data-ta] a[data-quickload=1]', 'click', function(e) { + e.preventDefault(); + var target = $( this ), + query = target.context.search.replace(/^\?/, ''), + tagarea = target.closest('.tagarea[data-ta]'), + args = query.split('&').reduce(function(s,c){var t=c.split('=');s[t[0]]=decodeURIComponent(t[1]);return s;},{}); + + var promises = ajax.call([{ + methodname: 'core_tag_get_tagindex', + args: { tagindex: args } + }], true); + + $.when.apply($, promises) + .done( function(data) { + templates.render('core_tag/index', data).done(function(html) { + tagarea.replaceWith(html); + }); + }); + }); + }, + + /** + * Initialises tag management page. + * + * @method init_manage_page */ init_manage_page: function() { diff --git a/lib/blocklib.php b/lib/blocklib.php index 56208b6fdc6..b045eb2e481 100644 --- a/lib/blocklib.php +++ b/lib/blocklib.php @@ -1497,10 +1497,10 @@ class block_manager { if ($bits[0] == 'tag' && !empty($this->page->subpage)) { // better navbar for tag pages $editpage->navbar->add(get_string('tags'), new moodle_url('/tag/')); - $tag = tag_get('id', $this->page->subpage, '*'); + $tag = core_tag_tag::get($this->page->subpage); // tag search page doesn't have subpageid if ($tag) { - $editpage->navbar->add($tag->name, new moodle_url('/tag/index.php', array('id'=>$tag->id))); + $editpage->navbar->add($tag->get_display_name(), $tag->get_view_url()); } } $editpage->navbar->add($block->get_title()); diff --git a/lib/classes/event/tag_added.php b/lib/classes/event/tag_added.php index 6316cc35012..fecf439bbd0 100644 --- a/lib/classes/event/tag_added.php +++ b/lib/classes/event/tag_added.php @@ -74,6 +74,34 @@ class tag_added extends base { s($this->other['itemtype']) . "' with id '{$this->other['itemid']}'."; } + /** + * Creates an event from taginstance object + * + * @since Moodle 3.1 + * @param stdClass $taginstance + * @param string $tagname + * @param string $tagrawname + * @param bool $addsnapshot trust that $taginstance has all necessary fields and add it as a record snapshot + * @return tag_added + */ + public static function create_from_tag_instance($taginstance, $tagname, $tagrawname, $addsnapshot = false) { + $event = self::create(array( + 'objectid' => $taginstance->id, + 'contextid' => $taginstance->contextid, + 'other' => array( + 'tagid' => $taginstance->tagid, + 'tagname' => $tagname, + 'tagrawname' => $tagrawname, + 'itemid' => $taginstance->itemid, + 'itemtype' => $taginstance->itemtype + ) + )); + if ($addsnapshot) { + $event->add_record_snapshot('tag_instance', $taginstance); + } + return $event; + } + /** * Return legacy data for add_to_log(). * diff --git a/lib/classes/event/tag_collection_created.php b/lib/classes/event/tag_collection_created.php new file mode 100644 index 00000000000..57573d00cc5 --- /dev/null +++ b/lib/classes/event/tag_collection_created.php @@ -0,0 +1,82 @@ +. + +/** + * Tag collection created event. + * + * @package core + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Tag collection created event class. + * + * @package core + * @since Moodle 3.0 + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tag_collection_created extends base { + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['objecttable'] = 'tag_coll'; + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Utility method to create new event. + * + * @param object $tagcoll + * @return user_graded + */ + public static function create_from_record($tagcoll) { + $event = self::create(array( + 'objectid' => $tagcoll->id, + 'context' => \context_system::instance(), + )); + $event->add_record_snapshot('tag_coll', $tagcoll); + return $event; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventtagcollcreated', 'core_tag'); + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' created the tag collection with id '$this->objectid'"; + } +} diff --git a/lib/classes/event/tag_collection_deleted.php b/lib/classes/event/tag_collection_deleted.php new file mode 100644 index 00000000000..f9b332faa39 --- /dev/null +++ b/lib/classes/event/tag_collection_deleted.php @@ -0,0 +1,82 @@ +. + +/** + * Tag collection deleted event. + * + * @package core + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Tag collection deleted event class. + * + * @package core + * @since Moodle 3.0 + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tag_collection_deleted extends base { + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['objecttable'] = 'tag_coll'; + $this->data['crud'] = 'd'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Utility method to create new event. + * + * @param object $tagcoll + * @return user_graded + */ + public static function create_from_record($tagcoll) { + $event = self::create(array( + 'objectid' => $tagcoll->id, + 'context' => \context_system::instance(), + )); + $event->add_record_snapshot('tag_coll', $tagcoll); + return $event; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventtagcolldeleted', 'core_tag'); + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' deleted the tag collection with id '$this->objectid'"; + } +} diff --git a/lib/classes/event/tag_collection_updated.php b/lib/classes/event/tag_collection_updated.php new file mode 100644 index 00000000000..0417b0f1a46 --- /dev/null +++ b/lib/classes/event/tag_collection_updated.php @@ -0,0 +1,82 @@ +. + +/** + * Tag collection updated event. + * + * @package core + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Tag collection updated event class. + * + * @package core + * @since Moodle 3.0 + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tag_collection_updated extends base { + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['objecttable'] = 'tag_coll'; + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Utility method to create new event. + * + * @param object $tagcoll + * @return user_graded + */ + public static function create_from_record($tagcoll) { + $event = self::create(array( + 'objectid' => $tagcoll->id, + 'context' => \context_system::instance(), + )); + $event->add_record_snapshot('tag_coll', $tagcoll); + return $event; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventtagcollupdated', 'core_tag'); + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' updated the tag collection with id '$this->objectid'"; + } +} diff --git a/lib/classes/event/tag_created.php b/lib/classes/event/tag_created.php index e239b392a74..9573a494f61 100644 --- a/lib/classes/event/tag_created.php +++ b/lib/classes/event/tag_created.php @@ -52,6 +52,26 @@ class tag_created extends base { $this->data['edulevel'] = self::LEVEL_OTHER; } + /** + * Creates an event from tag object + * + * @since Moodle 3.1 + * @param \core_tag_tag|\stdClass $tag + * @return tag_created + */ + public static function create_from_tag($tag) { + $event = self::create(array( + 'objectid' => $tag->id, + 'relateduserid' => $tag->userid, + 'context' => \context_system::instance(), + 'other' => array( + 'name' => $tag->name, + 'rawname' => $tag->rawname + ) + )); + return $event; + } + /** * Returns localised general event name. * diff --git a/lib/classes/event/tag_removed.php b/lib/classes/event/tag_removed.php index 04558aca24e..faa36109b6d 100644 --- a/lib/classes/event/tag_removed.php +++ b/lib/classes/event/tag_removed.php @@ -74,6 +74,34 @@ class tag_removed extends base { s($this->other['itemtype']) . "' with id '{$this->other['itemid']}'."; } + /** + * Creates an event from taginstance object + * + * @since Moodle 3.1 + * @param stdClass $taginstance + * @param string $tagname + * @param string $tagrawname + * @param bool $addsnapshot trust that $taginstance has all necessary fields and add it as a record snapshot + * @return tag_removed + */ + public static function create_from_tag_instance($taginstance, $tagname, $tagrawname, $addsnapshot = false) { + $event = self::create(array( + 'objectid' => $taginstance->id, + 'contextid' => $taginstance->contextid, + 'other' => array( + 'tagid' => $taginstance->tagid, + 'tagname' => $tagname, + 'tagrawname' => $tagrawname, + 'itemid' => $taginstance->itemid, + 'itemtype' => $taginstance->itemtype + ) + )); + if ($addsnapshot) { + $event->add_record_snapshot('tag_instance', $taginstance); + } + return $event; + } + /** * Custom validation. * diff --git a/lib/classes/task/tag_cron_task.php b/lib/classes/task/tag_cron_task.php index cdedf29f4a8..721edfbc95f 100644 --- a/lib/classes/task/tag_cron_task.php +++ b/lib/classes/task/tag_cron_task.php @@ -23,6 +23,8 @@ */ namespace core\task; +use core_tag_collection, core_tag_tag, core_tag_area, stdClass; + /** * Simple task to run the tag cron. */ @@ -45,9 +47,224 @@ class tag_cron_task extends scheduled_task { global $CFG; if (!empty($CFG->usetags)) { - require_once($CFG->dirroot.'/tag/lib.php'); - tag_cron(); + $this->compute_correlations(); + $this->cleanup(); } } + /** + * Calculates and stores the correlated tags of all tags. + * + * The correlations are stored in the 'tag_correlation' table. + * + * Two tags are correlated if they appear together a lot. Ex.: Users tagged with "computers" + * will probably also be tagged with "algorithms". + * + * The rationale for the 'tag_correlation' table is performance. It works as a cache + * for a potentially heavy load query done at the 'tag_instance' table. So, the + * 'tag_correlation' table stores redundant information derived from the 'tag_instance' table. + * + * @param int $mincorrelation Only tags with more than $mincorrelation correlations will be identified. + */ + public function compute_correlations($mincorrelation = 2) { + global $DB; + + // This mighty one line query fetches a row from the database for every + // individual tag correlation. We then need to process the rows collecting + // the correlations for each tag id. + // The fields used by this query are as follows: + // tagid : This is the tag id, there should be at least $mincorrelation + // rows for each tag id. + // correlation : This is the tag id that correlates to the above tagid field. + // correlationid : This is the id of the row in the tag_correlation table that + // relates to the tagid field and will be NULL if there are no + // existing correlations. + $sql = 'SELECT pairs.tagid, pairs.correlation, pairs.ocurrences, co.id AS correlationid + FROM ( + SELECT ta.tagid, tb.tagid AS correlation, COUNT(*) AS ocurrences + FROM {tag_instance} ta + JOIN {tag} tga ON ta.tagid = tga.id + JOIN {tag_instance} tb ON (ta.itemtype = tb.itemtype AND ta.component = tb.component + AND ta.itemid = tb.itemid AND ta.tagid <> tb.tagid) + JOIN {tag} tgb ON tb.tagid = tgb.id AND tgb.tagcollid = tga.tagcollid + GROUP BY ta.tagid, tb.tagid + HAVING COUNT(*) > :mincorrelation + ) pairs + LEFT JOIN {tag_correlation} co ON co.tagid = pairs.tagid + ORDER BY pairs.tagid ASC, pairs.ocurrences DESC, pairs.correlation ASC'; + $rs = $DB->get_recordset_sql($sql, array('mincorrelation' => $mincorrelation)); + + // Set up an empty tag correlation object. + $tagcorrelation = new stdClass; + $tagcorrelation->id = null; + $tagcorrelation->tagid = null; + $tagcorrelation->correlatedtags = array(); + + // We store each correlation id in this array so we can remove any correlations + // that no longer exist. + $correlations = array(); + + // Iterate each row of the result set and build them into tag correlations. + // We add all of a tag's correlations to $tagcorrelation->correlatedtags[] + // then save the $tagcorrelation object. + foreach ($rs as $row) { + if ($row->tagid != $tagcorrelation->tagid) { + // The tag id has changed so we have all of the correlations for this tag. + $tagcorrelationid = $this->process_computed_correlation($tagcorrelation); + if ($tagcorrelationid) { + $correlations[] = $tagcorrelationid; + } + // Now we reset the tag correlation object so we can reuse it and set it + // up for the current record. + $tagcorrelation = new stdClass; + $tagcorrelation->id = $row->correlationid; + $tagcorrelation->tagid = $row->tagid; + $tagcorrelation->correlatedtags = array(); + } + // Save the correlation on the tag correlation object. + $tagcorrelation->correlatedtags[] = $row->correlation; + } + // Update the current correlation after the last record. + $tagcorrelationid = $this->process_computed_correlation($tagcorrelation); + if ($tagcorrelationid) { + $correlations[] = $tagcorrelationid; + } + + // Close the recordset. + $rs->close(); + + // Remove any correlations that weren't just identified. + if (empty($correlations)) { + // There are no tag correlations. + $DB->delete_records('tag_correlation'); + } else { + list($sql, $params) = $DB->get_in_or_equal($correlations, + SQL_PARAMS_NAMED, 'param0000', false); + $DB->delete_records_select('tag_correlation', 'id '.$sql, $params); + } + } + + /** + * Clean up the tag tables, making sure all tagged object still exists. + * + * This method is called from cron. + * + * This should normally not be necessary, but in case related tags are not deleted + * when the tagged record is removed, this should be done once in a while, perhaps + * on an occasional cron run. On a site with lots of tags, this could become an + * expensive function to call. + */ + public function cleanup() { + global $DB; + + // Get ids to delete from instances where the tag has been deleted. This should never happen apparently. + $sql = "SELECT ti.id + FROM {tag_instance} ti + LEFT JOIN {tag} t ON t.id = ti.tagid + WHERE t.id IS null"; + $tagids = $DB->get_records_sql($sql); + $tagarray = array(); + foreach ($tagids as $tagid) { + $tagarray[] = $tagid->id; + } + + // Next get ids from instances that have an owner that has been deleted. + $sql = "SELECT ti.id + FROM {tag_instance} ti, {user} u + WHERE ti.itemid = u.id + AND ti.itemtype = 'user' + AND ti.component = 'core' + AND u.deleted = 1"; + $tagids = $DB->get_records_sql($sql); + foreach ($tagids as $tagid) { + $tagarray[] = $tagid->id; + } + + // Get the other itemtypes. + $sql = "SELECT DISTINCT component, itemtype + FROM {tag_instance} + WHERE itemtype <> 'user' or component <> 'core'"; + $tagareas = $DB->get_records_sql($sql); + foreach ($tagareas as $tagarea) { + $sql = 'SELECT ti.id + FROM {tag_instance} ti + LEFT JOIN {' . $tagarea->itemtype . '} it ON it.id = ti.itemid + WHERE it.id IS null + AND ti.itemtype = ? AND ti.component = ?'; + $tagids = $DB->get_records_sql($sql, array($tagarea->itemtype, $tagarea->component)); + foreach ($tagids as $tagid) { + $tagarray[] = $tagid->id; + } + } + + // Get instances for each of the ids to be deleted. + if (count($tagarray) > 0) { + list($sqlin, $params) = $DB->get_in_or_equal($tagarray); + $sql = "SELECT ti.*, COALESCE(t.name, 'deleted') AS name, COALESCE(t.rawname, 'deleted') AS rawname + FROM {tag_instance} ti + LEFT JOIN {tag} t ON t.id = ti.tagid + WHERE ti.id $sqlin"; + $instances = $DB->get_records_sql($sql, $params); + $this->bulk_delete_instances($instances); + } + + core_tag_collection::cleanup_unused_tags(); + } + + /** + * This function processes a tag correlation and makes changes in the database as required. + * + * The tag correlation object needs have both a tagid property and a correlatedtags property that is an array. + * + * @param stdClass $tagcorrelation + * @return int/bool The id of the tag correlation that was just processed or false. + */ + public function process_computed_correlation(stdClass $tagcorrelation) { + global $DB; + + // You must provide a tagid and correlatedtags must be set and be an array. + if (empty($tagcorrelation->tagid) || !isset($tagcorrelation->correlatedtags) || + !is_array($tagcorrelation->correlatedtags)) { + return false; + } + + $tagcorrelation->correlatedtags = join(',', $tagcorrelation->correlatedtags); + if (!empty($tagcorrelation->id)) { + // The tag correlation already exists so update it. + $DB->update_record('tag_correlation', $tagcorrelation); + } else { + // This is a new correlation to insert. + $tagcorrelation->id = $DB->insert_record('tag_correlation', $tagcorrelation); + } + return $tagcorrelation->id; + } + + /** + * This function will delete numerous tag instances efficiently. + * This removes tag instances only. It doesn't check to see if it is the last use of a tag. + * + * @param array $instances An array of tag instance objects with the addition of the tagname and tagrawname + * (used for recording a delete event). + */ + public function bulk_delete_instances($instances) { + global $DB; + + $instanceids = array(); + foreach ($instances as $instance) { + $instanceids[] = $instance->id; + } + + // This is a multi db compatible method of creating the correct sql when using the 'IN' value. + // $insql is the sql statement, $params are the id numbers. + list($insql, $params) = $DB->get_in_or_equal($instanceids); + $sql = 'id ' . $insql; + $DB->delete_records_select('tag_instance', $sql, $params); + + // Now go through and record each tag individually with the event system. + foreach ($instances as $instance) { + // Trigger tag removed event (i.e. The tag instance has been removed). + \core\event\tag_removed::create_from_tag_instance($instance, $instance->name, + $instance->rawname, true)->trigger(); + } + } } diff --git a/lib/coursecatlib.php b/lib/coursecatlib.php index df83db66717..fc8811f8b54 100644 --- a/lib/coursecatlib.php +++ b/lib/coursecatlib.php @@ -1339,8 +1339,27 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate { } else if (!empty($search['tagid'])) { // Search courses that are tagged with the specified tag. $where = "c.id IN (SELECT t.itemid ". - "FROM {tag_instance} t WHERE t.tagid = :tagid AND t.itemtype = :itemtype)"; - $params = array('tagid' => $search['tagid'], 'itemtype' => 'course'); + "FROM {tag_instance} t WHERE t.tagid = :tagid AND t.itemtype = :itemtype AND t.component = :component)"; + $params = array('tagid' => $search['tagid'], 'itemtype' => 'course', 'component' => 'core'); + if (!empty($search['ctx'])) { + $rec = isset($search['rec']) ? $search['rec'] : true; + $parentcontext = context::instance_by_id($search['ctx']); + if ($parentcontext->contextlevel == CONTEXT_SYSTEM && $rec) { + // Parent context is system context and recursive is set to yes. + // Nothing to filter - all courses fall into this condition. + } else if ($rec) { + // Filter all courses in the parent context at any level. + $where .= ' AND ctx.path LIKE :contextpath'; + $params['contextpath'] = $parentcontext->path . '%'; + } else if ($parentcontext->contextlevel == CONTEXT_COURSECAT) { + // All courses in the given course category. + $where .= ' AND c.category = :category'; + $params['category'] = $parentcontext->instanceid; + } else { + // No courses will satisfy the context criterion, do not bother searching. + $where = '1=0'; + } + } } else { debugging('No criteria is specified while searching courses', DEBUG_DEVELOPER); return array(); diff --git a/lib/db/caches.php b/lib/db/caches.php index 7d90e55efeb..dd8dc960aee 100644 --- a/lib/db/caches.php +++ b/lib/db/caches.php @@ -252,6 +252,12 @@ $definitions = array( 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 5 + ), + + // Caches data about tag collections and areas. + 'tags' => array( + 'mode' => cache_store::MODE_REQUEST, + 'simplekeys' => true, ) ); diff --git a/lib/db/install.xml b/lib/db/install.xml index de8750aa922..3c9e4204511 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -1958,10 +1958,43 @@ + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + +
    + @@ -1973,9 +2006,11 @@ + - + +
    @@ -1993,8 +2028,8 @@ - - + + @@ -2008,7 +2043,8 @@ - + +
    diff --git a/lib/db/services.php b/lib/db/services.php index d9867da6dce..bc3230f3f4d 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1148,6 +1148,14 @@ $functions = array( 'ajax' => true ), + 'core_tag_get_tagindex' => array( + 'classname' => 'core_tag_external', + 'methodname' => 'get_tagindex', + 'description' => 'Gets tag index page for one tag and one tag area', + 'type' => 'read', + 'ajax' => true + ), + ); $services = array( diff --git a/lib/db/tag.php b/lib/db/tag.php new file mode 100644 index 00000000000..2923bb56fb5 --- /dev/null +++ b/lib/db/tag.php @@ -0,0 +1,80 @@ +. + +/** + * Tag area definitions + * + * File db/tag.php lists all available tag areas in core or a plugin. + * + * Each tag area may have the following attributes: + * - itemtype (required) - what is tagged. Must be name of the existing DB table + * - component - component responsible for tagging, if the tag area is inside a + * plugin the component must be the full frankenstyle name of the plugin + * - collection - name of the custom tag collection that will be used to store + * tags in this area. If specified aministrator will be able to neither add + * any other tag areas to this collection nor move this tag area elsewhere + * - searchable (only if collection is specified) - wether the tag collection + * should be searchable on /tag/search.php + * - customurl (only if collection is specified) - custom url to use instead of + * /tag/search.php to display information about one tag + * - callback - name of the function that returns items tagged with this tag, + * see core_tag_tag::get_tag_index() and existing callbacks for more details, + * callback should return instance of core_tag\output\tagindex + * - callbackfile - file where callback is located (if not an autoloaded location) + * + * Language file must contain the human-readable names of the tag areas and + * collections (either in plugin language file or in component language file or + * lang/en/tag.php in case of core): + * - for item type "user": + * $string['tagarea_user'] = 'Users'; + * - for tag collection "mycollection": + * $string['tagcollection_mycollection'] = 'My tag collection'; + * + * @package core + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$tagareas = array( + array( + 'itemtype' => 'user', // Users. + 'component' => 'core', + 'callback' => 'user_get_tagged_users', + 'callbackfile' => '/user/lib.php', + ), + array( + 'itemtype' => 'course', // Courses. + 'component' => 'core', + 'callback' => 'course_get_tagged_courses', + 'callbackfile' => '/course/lib.php', + ), + array( + 'itemtype' => 'question', // Questions. + 'component' => 'core_question', + ), + array( + 'itemtype' => 'post', // Blog posts. + 'component' => 'core', + 'callback' => 'blog_get_tagged_posts', + 'callbackfile' => '/blog/lib.php', + ), + array( + 'itemtype' => 'blog_external', // External blogs. + 'component' => 'core', + ), +); diff --git a/lib/db/tasks.php b/lib/db/tasks.php index 9723d334beb..41991f15071 100644 --- a/lib/db/tasks.php +++ b/lib/db/tasks.php @@ -71,8 +71,8 @@ $tasks = array( array( 'classname' => 'core\task\tag_cron_task', 'blocking' => 0, - 'minute' => '20', - 'hour' => '*', + 'minute' => 'R', + 'hour' => '3', 'day' => '*', 'dayofweek' => '*', 'month' => '*' diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index c5d6eea37dc..7a306e99d81 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -4620,5 +4620,191 @@ function xmldb_main_upgrade($oldversion) { // Moodle v3.0.0 release upgrade line. // Put any upgrade step following this. + if ($oldversion < 2016011100.00) { + + // This is a big upgrade script. We create new table tag_coll and the field + // tag.tagcollid pointing to it. + + // Define table tag_coll to be created. + $table = new xmldb_table('tag_coll'); + + // Adding fields to table tagcloud. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null); + $table->add_field('isdefault', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('component', XMLDB_TYPE_CHAR, '100', null, null, null, null); + $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '5', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('searchable', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1'); + $table->add_field('customurl', XMLDB_TYPE_CHAR, '255', null, null, null, null); + + // Adding keys to table tagcloud. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Conditionally launch create table for tagcloud. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Table {tag}. + // Define index name (unique) to be dropped form tag - we will replace it with index on (tagcollid,name) later. + $table = new xmldb_table('tag'); + $index = new xmldb_index('name', XMLDB_INDEX_UNIQUE, array('name')); + + // Conditionally launch drop index name. + if ($dbman->index_exists($table, $index)) { + $dbman->drop_index($table, $index); + } + + // Define field tagcollid to be added to tag, we create it as null first and will change to notnull later. + $table = new xmldb_table('tag'); + $field = new xmldb_field('tagcollid', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'userid'); + + // Conditionally launch add field tagcloudid. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016011100.00); + } + + if ($oldversion < 2016011100.02) { + // Create a default tag collection if not exists and update the field tag.tagcollid to point to it. + if (!$tcid = $DB->get_field_sql('SELECT id FROM {tag_coll} ORDER BY isdefault DESC, sortorder, id', null, + IGNORE_MULTIPLE)) { + $tcid = $DB->insert_record('tag_coll', array('isdefault' => 1, 'sortorder' => 0)); + } + $DB->execute('UPDATE {tag} SET tagcollid = ? WHERE tagcollid IS NULL', array($tcid)); + + // Define index tagcollname (unique) to be added to tag. + $table = new xmldb_table('tag'); + $index = new xmldb_index('tagcollname', XMLDB_INDEX_UNIQUE, array('tagcollid', 'name')); + $field = new xmldb_field('tagcollid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'userid'); + + // Conditionally launch add index tagcollname. + if (!$dbman->index_exists($table, $index)) { + // Launch change of nullability for field tagcollid. + $dbman->change_field_notnull($table, $field); + $dbman->add_index($table, $index); + } + + // Define key tagcollid (foreign) to be added to tag. + $table = new xmldb_table('tag'); + $key = new xmldb_key('tagcollid', XMLDB_KEY_FOREIGN, array('tagcollid'), 'tag_coll', array('id')); + + // Launch add key tagcloudid. + $dbman->add_key($table, $key); + + // Define index tagcolltype (not unique) to be added to tag. + $table = new xmldb_table('tag'); + $index = new xmldb_index('tagcolltype', XMLDB_INDEX_NOTUNIQUE, array('tagcollid', 'tagtype')); + + // Conditionally launch add index tagcolltype. + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016011100.02); + } + + if ($oldversion < 2016011100.03) { + + // Define table tag_area to be created. + $table = new xmldb_table('tag_area'); + + // Adding fields to table tag_area. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('component', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null); + $table->add_field('itemtype', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null); + $table->add_field('enabled', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1'); + $table->add_field('tagcollid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('callback', XMLDB_TYPE_CHAR, '100', null, null, null, null); + $table->add_field('callbackfile', XMLDB_TYPE_CHAR, '100', null, null, null, null); + + // Adding keys to table tag_area. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('tagcollid', XMLDB_KEY_FOREIGN, array('tagcollid'), 'tag_coll', array('id')); + + // Adding indexes to table tag_area. + $table->add_index('compitemtype', XMLDB_INDEX_UNIQUE, array('component', 'itemtype')); + + // Conditionally launch create table for tag_area. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016011100.03); + } + + if ($oldversion < 2016011100.12) { + + // Define index itemtype-itemid-tagid-tiuserid (unique) to be dropped form tag_instance. + $table = new xmldb_table('tag_instance'); + $index = new xmldb_index('itemtype-itemid-tagid-tiuserid', XMLDB_INDEX_UNIQUE, + array('itemtype', 'itemid', 'tagid', 'tiuserid')); + + // Conditionally launch drop index itemtype-itemid-tagid-tiuserid. + if ($dbman->index_exists($table, $index)) { + $dbman->drop_index($table, $index); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016011100.12); + } + + if ($oldversion < 2016011100.13) { + + $DB->execute("UPDATE {tag_instance} SET component = ? WHERE component IS NULL", array('')); + + // Changing nullability of field component on table tag_instance to not null. + $table = new xmldb_table('tag_instance'); + $field = new xmldb_field('component', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, 'tagid'); + + // Launch change of nullability for field component. + $dbman->change_field_notnull($table, $field); + + // Changing type of field itemtype on table tag_instance to char. + $table = new xmldb_table('tag_instance'); + $field = new xmldb_field('itemtype', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, 'component'); + + // Launch change of type for field itemtype. + $dbman->change_field_type($table, $field); + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016011100.13); + } + + if ($oldversion < 2016011100.14) { + + // Define index taggeditem (unique) to be added to tag_instance. + $table = new xmldb_table('tag_instance'); + $index = new xmldb_index('taggeditem', XMLDB_INDEX_UNIQUE, array('component', 'itemtype', 'itemid', 'tiuserid', 'tagid')); + + // Conditionally launch add index taggeditem. + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016011100.14); + } + + if ($oldversion < 2016011100.15) { + + // Define index taglookup (not unique) to be added to tag_instance. + $table = new xmldb_table('tag_instance'); + $index = new xmldb_index('taglookup', XMLDB_INDEX_NOTUNIQUE, array('itemtype', 'component', 'tagid', 'contextid')); + + // Conditionally launch add index taglookup. + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016011100.15); + } + return true; } diff --git a/lib/deprecatedlib.php b/lib/deprecatedlib.php index cd38aa4d44f..f20c2d1349c 100644 --- a/lib/deprecatedlib.php +++ b/lib/deprecatedlib.php @@ -2617,30 +2617,17 @@ function coursetag_store_keywords($tags, $courseid, $userid=0, $tagtype='officia debugging('Function coursetag_store_keywords() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER); global $CFG; - require_once $CFG->dirroot.'/tag/lib.php'; if (is_array($tags) and !empty($tags)) { + if ($tagtype === 'official') { + $tagcoll = core_tag_area::get_collection('core', 'course'); + // We don't normally need to create tags, they are created automatically when added to items. but we do here because we want them to be official. + core_tag_tag::create_if_missing($tagcoll, $tags, true); + } foreach ($tags as $tag) { $tag = trim($tag); if (strlen($tag) > 0) { - //tag_set_add('course', $courseid, $tag, $userid); //deletes official tags - - //add tag if does not exist - if (!$tagid = tag_get_id($tag)) { - $tag_id_array = tag_add(array($tag), $tagtype); - $tagid = $tag_id_array[core_text::strtolower($tag)]; - } - //ordering - $ordering = 0; - if ($current_ids = tag_get_tags_ids('course', $courseid)) { - end($current_ids); - $ordering = key($current_ids) + 1; - } - //set type - tag_type_set($tagid, $tagtype); - - //tag_instance entry - tag_assign('course', $courseid, $tagid, $ordering, $userid, 'core', context_course::instance($courseid)->id); + core_tag_tag::add_item_tag('core', 'course', $courseid, context_course::instance($courseid), $tag, $userid); } } } @@ -2660,7 +2647,8 @@ function coursetag_store_keywords($tags, $courseid, $userid=0, $tagtype='officia function coursetag_delete_keyword($tagid, $userid, $courseid) { debugging('Function coursetag_delete_keyword() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER); - tag_delete_instance('course', $courseid, $tagid, $userid); + $tag = core_tag_tag::get($tagid); + core_tag_tag::remove_item_tag('core', 'course', $courseid, $tag->rawname, $userid); } /** @@ -2709,24 +2697,431 @@ function coursetag_get_tagged_courses($tagid) { * @param bool $showfeedback if we should output a notification of the delete to the end user */ function coursetag_delete_course_tags($courseid, $showfeedback=false) { - debugging('Function coursetag_delete_course_tags() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER); + debugging('Function coursetag_delete_course_tags() is deprecated. Use core_tag_tag::remove_all_item_tags().', DEBUG_DEVELOPER); - global $DB, $OUTPUT; - - if ($taginstances = $DB->get_recordset_select('tag_instance', "itemtype = 'course' AND itemid = :courseid", - array('courseid' => $courseid), '', 'tagid, tiuserid')) { - - foreach ($taginstances as $record) { - tag_delete_instance('course', $courseid, $record->tagid, $record->tiuserid); - } - $taginstances->close(); - } + global $OUTPUT; + core_tag_tag::remove_all_item_tags('core', 'course', $courseid); if ($showfeedback) { echo $OUTPUT->notification(get_string('deletedcoursetags', 'tag'), 'notifysuccess'); } } +/** + * Set the type of a tag. At this time (version 2.2) the possible values are 'default' or 'official'. Official tags will be + * displayed separately "at tagging time" (while selecting the tags to apply to a record). + * + * @package core_tag + * @deprecated since 3.1 + * @param string $tagid tagid to modify + * @param string $type either 'default' or 'official' + * @return bool true on success, false otherwise + */ +function tag_type_set($tagid, $type) { + debugging('Function tag_type_set() is deprecated and can be replaced with use core_tag_tag::get($tagid)->update().', DEBUG_DEVELOPER); + if ($tag = core_tag_tag::get($tagid, '*')) { + return $tag->update(array('tagtype' => $type)); + } + return false; +} + +/** + * Set the description of a tag + * + * @package core_tag + * @deprecated since 3.1 + * @param int $tagid the id of the tag + * @param string $description the tag's description string to be set + * @param int $descriptionformat the moodle text format of the description + * {@link http://docs.moodle.org/dev/Text_formats_2.0#Database_structure} + * @return bool true on success, false otherwise + */ +function tag_description_set($tagid, $description, $descriptionformat) { + debugging('Function tag_type_set() is deprecated and can be replaced with core_tag_tag::get($tagid)->update().', DEBUG_DEVELOPER); + if ($tag = core_tag_tag::get($tagid, '*')) { + return $tag->update(array('description' => $description, 'descriptionformat' => $descriptionformat)); + } + return false; +} + +/** + * Get the array of db record of tags associated to a record (instances). + * + * @package core_tag + * @deprecated since 3.1 + * @param string $record_type the record type for which we want to get the tags + * @param int $record_id the record id for which we want to get the tags + * @param string $type the tag type (either 'default' or 'official'). By default, all tags are returned. + * @param int $userid (optional) only required for course tagging + * @return array the array of tags + */ +function tag_get_tags($record_type, $record_id, $type=null, $userid=0) { + debugging('Method tag_get_tags() is deprecated and replaced with core_tag_tag::get_item_tags(). ' . + 'Component is now required when retrieving tag instances.', DEBUG_DEVELOPER); + $official = ($type === 'official' ? true : (!empty($type) ? false : null)); + $tags = core_tag_tag::get_item_tags(null, $record_type, $record_id, $official, $userid); + $rv = array(); + foreach ($tags as $id => $t) { + $rv[$id] = $t->to_object(); + } + return $rv; +} + +/** + * Get the array of tags display names, indexed by id. + * + * @package core_tag + * @deprecated since 3.1 + * @param string $record_type the record type for which we want to get the tags + * @param int $record_id the record id for which we want to get the tags + * @param string $type the tag type (either 'default' or 'official'). By default, all tags are returned. + * @return array the array of tags (with the value returned by core_tag_tag::make_display_name), indexed by id + */ +function tag_get_tags_array($record_type, $record_id, $type=null) { + debugging('Method tag_get_tags_array() is deprecated and replaced with core_tag_tag::get_item_tags_array(). ' . + 'Component is now required when retrieving tag instances.', DEBUG_DEVELOPER); + $official = ($type === 'official' ? true : (!empty($type) ? false : null)); + return core_tag_tag::get_item_tags_array('', $record_type, $record_id, $official); +} + +/** + * Get a comma-separated string of tags associated to a record. + * + * Use {@link tag_get_tags()} to get the same information in an array. + * + * @package core_tag + * @deprecated since 3.1 + * @param string $record_type the record type for which we want to get the tags + * @param int $record_id the record id for which we want to get the tags + * @param int $html either TAG_RETURN_HTML or TAG_RETURN_TEXT, depending on the type of output desired + * @param string $type either 'official' or 'default', if null, all tags are returned + * @return string the comma-separated list of tags. + */ +function tag_get_tags_csv($record_type, $record_id, $html=null, $type=null) { + global $CFG, $OUTPUT; + debugging('Method tag_get_tags_csv() is deprecated. Instead you should use either ' . + 'core_tag_tag::get_item_tags_array() or $OUTPUT->tag_list(core_tag_tag::get_item_tags()). ' . + 'Component is now required when retrieving tag instances.', DEBUG_DEVELOPER); + $official = ($type === 'official' ? true : (!empty($type) ? false : null)); + if ($html != TAG_RETURN_TEXT) { + return $OUTPUT->tag_list(core_tag_tag::get_item_tags('', $record_type, $record_id, $official), ''); + } else { + return join(', ', core_tag_tag::get_item_tags_array('', $record_type, $record_id, $official, 0, false)); + } +} + +/** + * Get an array of tag ids associated to a record. + * + * @package core_tag + * @deprecated since 3.1 + * @param string $record_type the record type for which we want to get the tags + * @param int $record_id the record id for which we want to get the tags + * @return array tag ids, indexed and sorted by 'ordering' + */ +function tag_get_tags_ids($record_type, $record_id) { + debugging('Method tag_get_tags_ids() is deprecated. Please consider using core_tag_tag::get_item_tags() or similar methods.', DEBUG_DEVELOPER); + $tag_ids = array(); + $tagobjects = core_tag_tag::get_item_tags(null, $record_type, $record_id); + foreach ($tagobjects as $tagobject) { + $tag = $tagobject->to_object(); + if ( array_key_exists($tag->ordering, $tag_ids) ) { + $tag->ordering++; + } + $tag_ids[$tag->ordering] = $tag->id; + } + ksort($tag_ids); + return $tag_ids; +} + +/** + * Returns the database ID of a set of tags. + * + * @deprecated since 3.1 + * @param mixed $tags one tag, or array of tags, to look for. + * @param bool $return_value specify the type of the returned value. Either TAG_RETURN_OBJECT, or TAG_RETURN_ARRAY (default). + * If TAG_RETURN_ARRAY is specified, an array will be returned even if only one tag was passed in $tags. + * @return mixed tag-indexed array of ids (or objects, if second parameter is TAG_RETURN_OBJECT), or only an int, if only one tag + * is given *and* the second parameter is null. No value for a key means the tag wasn't found. + */ +function tag_get_id($tags, $return_value = null) { + global $CFG, $DB; + debugging('Method tag_get_id() is deprecated and can be replaced with core_tag_tag::get_by_name() or core_tag_tag::get_by_name_bulk(). ' . + 'You need to specify tag collection when retrieving tag by name', DEBUG_DEVELOPER); + + if (!is_array($tags)) { + if(is_null($return_value) || $return_value == TAG_RETURN_OBJECT) { + if ($tagobject = core_tag_tag::get_by_name(core_tag_collection::get_default(), $tags)) { + return $tagobject->id; + } else { + return 0; + } + } + $tags = array($tags); + } + + $records = core_tag_tag::get_by_name_bulk(core_tag_collection::get_default(), $tags, + $return_value == TAG_RETURN_OBJECT ? '*' : 'id, name'); + foreach ($records as $name => $record) { + if ($return_value != TAG_RETURN_OBJECT) { + $records[$name] = $record->id ? $record->id : null; + } else { + $records[$name] = $record->to_object(); + } + } + return $records; +} + +/** + * Change the "value" of a tag, and update the associated 'name'. + * + * @package core_tag + * @deprecated since 3.1 + * @param int $tagid the id of the tag to modify + * @param string $newrawname the new rawname + * @return bool true on success, false otherwise + */ +function tag_rename($tagid, $newrawname) { + debugging('Function tag_rename() is deprecated and may be replaced with core_tag_tag::get($tagid)->update().', DEBUG_DEVELOPER); + if ($tag = core_tag_tag::get($tagid, '*')) { + return $tag->update(array('rawname' => $newrawname)); + } + return false; +} + +/** + * Delete one instance of a tag. If the last instance was deleted, it will also delete the tag, unless its type is 'official'. + * + * @package core_tag + * @deprecated since 3.1 + * @param string $record_type the type of the record for which to remove the instance + * @param int $record_id the id of the record for which to remove the instance + * @param int $tagid the tagid that needs to be removed + * @param int $userid (optional) the userid + * @return bool true on success, false otherwise + */ +function tag_delete_instance($record_type, $record_id, $tagid, $userid = null) { + debugging('Function tag_delete_instance() is deprecated and replaced with core_tag_tag::remove_item_tag() instead. ' . + 'Component is required for retrieving instances', DEBUG_DEVELOPER); + $tag = core_tag_tag::get($tagid); + core_tag_tag::remove_item_tag('', $record_type, $record_id, $tag->rawname, $userid); +} + +/** + * Find all records tagged with a tag of a given type ('post', 'user', etc.) + * + * @package core_tag + * @category tag + * @param string $tag tag to look for + * @param string $type type to restrict search to. If null, every matching record will be returned + * @param int $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point. + * @param int $limitnum (optional, required if $limitfrom is set) return a subset comprising this many records. + * @return array of matching objects, indexed by record id, from the table containing the type requested + */ +function tag_find_records($tag, $type, $limitfrom='', $limitnum='') { + debugging('Function tag_find_records() is deprecated and replaced with core_tag_tag::get_by_name()->get_tagged_items(). '. + 'You need to specify tag collection when retrieving tag by name', DEBUG_DEVELOPER); + + if (!$tag || !$type) { + return array(); + } + + $tagobject = core_tag_tag::get_by_name(core_tag_area::get_collection('', $type), $tag); + return $tagobject->get_tagged_items('', $type, $limitfrom, $limitnum); +} + +/** + * Adds one or more tag in the database. This function should not be called directly : you should + * use tag_set. + * + * @package core_tag + * @deprecated since 3.1 + * @param mixed $tags one tag, or an array of tags, to be created + * @param string $type type of tag to be created ("default" is the default value and "official" is the only other supported + * value at this time). An official tag is kept even if there are no records tagged with it. + * @return array $tags ids indexed by their lowercase normalized names. Any boolean false in the array indicates an error while + * adding the tag. + */ +function tag_add($tags, $type="default") { + debugging('Function tag_add() is deprecated. You can use core_tag_tag::create_if_missing(), however it should not be necessary ' . + 'since tags are created automatically when assigned to items', DEBUG_DEVELOPER); + if (!is_array($tags)) { + $tags = array($tags); + } + $objects = core_tag_tag::create_if_missing(core_tag_collection::get_default(), $tags, $type === 'official'); + + // New function returns the tags in different format, for BC we keep the format that this function used to have. + $rv = array(); + foreach ($objects as $name => $tagobject) { + if (isset($tagobject->id)) { + $rv[$tagobject->name] = $tagobject->id; + } else { + $rv[$name] = false; + } + } + return $rv; +} + +/** + * Assigns a tag to a record; if the record already exists, the time and ordering will be updated. + * + * @package core_tag + * @deprecated since 3.1 + * @param string $record_type the type of the record that will be tagged + * @param int $record_id the id of the record that will be tagged + * @param string $tagid the tag id to set on the record. + * @param int $ordering the order of the instance for this record + * @param int $userid (optional) only required for course tagging + * @param string|null $component the component that was tagged + * @param int|null $contextid the context id of where this tag was assigned + * @return bool true on success, false otherwise + */ +function tag_assign($record_type, $record_id, $tagid, $ordering, $userid = 0, $component = null, $contextid = null) { + global $DB; + $message = 'Function tag_assign() is deprecated. Use core_tag_tag::set_item_tags() or core_tag_tag::add_item_tag() instead. ' . + 'Tag instance ordering should not be set manually'; + if ($component === null || $contextid === null) { + $message .= '. You should specify the component and contextid of the item being tagged in your call to tag_assign.'; + } + debugging($message, DEBUG_DEVELOPER); + + if ($contextid) { + $context = context::instance_by_id($contextid); + } else { + $context = context_system::instance(); + } + + // Get the tag. + $tag = $DB->get_record('tag', array('id' => $tagid), 'name, rawname', MUST_EXIST); + + $taginstanceid = core_tag_tag::add_item_tag($component, $record_type, $record_id, $context, $tag->rawname, $userid); + + // Alter the "ordering" of tag_instance. This should never be done manually and only remains here for the backward compatibility. + $taginstance = new stdClass(); + $taginstance->id = $taginstanceid; + $taginstance->ordering = $ordering; + $taginstance->timemodified = time(); + + $DB->update_record('tag_instance', $taginstance); + + return true; +} + +/** + * Count how many records are tagged with a specific tag. + * + * @package core_tag + * @deprecated since 3.1 + * @param string $record_type record to look for ('post', 'user', etc.) + * @param int $tagid is a single tag id + * @return int number of mathing tags. + */ +function tag_record_count($record_type, $tagid) { + debugging('Method tag_record_count() is deprecated and replaced with core_tag_tag::get($tagid)->count_tagged_items(). '. + 'Component is now required when retrieving tag instances.', DEBUG_DEVELOPER); + return core_tag_tag::get($tagid)->count_tagged_items('', $record_type); +} + +/** + * Determine if a record is tagged with a specific tag + * + * @package core_tag + * @deprecated since 3.1 + * @param string $record_type the record type to look for + * @param int $record_id the record id to look for + * @param string $tag a tag name + * @return bool/int true if it is tagged, 0 (false) otherwise + */ +function tag_record_tagged_with($record_type, $record_id, $tag) { + debugging('Method tag_record_tagged_with() is deprecated and replaced with core_tag_tag::get($tagid)->is_item_tagged_with(). '. + 'Component is now required when retrieving tag instances.', DEBUG_DEVELOPER); + return core_tag_tag::is_item_tagged_with('', $record_type, $record_id, $tag); +} + +/** + * Flag a tag as inappropriate. + * + * @deprecated since 3.1 + * @param int|array $tagids a single tagid, or an array of tagids + */ +function tag_set_flag($tagids) { + debugging('Function tag_set_flag() is deprecated and replaced with core_tag_tag::get($tagid)->flag().', DEBUG_DEVELOPER); + $tagids = (array) $tagids; + foreach ($tagids as $tagid) { + if ($tag = core_tag_tag::get($tagid, '*')) { + $tag->flag(); + } + } +} + +/** + * Remove the inappropriate flag on a tag. + * + * @deprecated since 3.1 + * @param int|array $tagids a single tagid, or an array of tagids + */ +function tag_unset_flag($tagids) { + debugging('Function tag_unset_flag() is deprecated and replaced with core_tag_tag::get($tagid)->reset_flag().', DEBUG_DEVELOPER); + $tagids = (array) $tagids; + foreach ($tagids as $tagid) { + if ($tag = core_tag_tag::get($tagid, '*')) { + $tag->reset_flag(); + } + } +} + +/** + * Prints or returns a HTML tag cloud with varying classes styles depending on the popularity and type of each tag. + * + * @deprecated since 3.1 + * + * @param array $tagset Array of tags to display + * @param int $nr_of_tags Limit for the number of tags to return/display, used if $tagset is null + * @param bool $return if true the function will return the generated tag cloud instead of displaying it. + * @param string $sort (optional) selected sorting, default is alpha sort (name) also timemodified or popularity + * @return string|null a HTML string or null if this function does the output + */ +function tag_print_cloud($tagset=null, $nr_of_tags=150, $return=false, $sort='') { + global $OUTPUT; + + debugging('Function tag_print_cloud() is deprecated and replaced with function core_tag_collection::get_tag_cloud(), ' + . 'templateable core_tag\output\tagcloud and template core_tag/tagcloud.', DEBUG_DEVELOPER); + + // Set up sort global - used to pass sort type into core_tag_collection::cloud_sort through usort() avoiding multiple sort functions. + if ($sort == 'popularity') { + $sort = 'count'; + } else if ($sort == 'date') { + $sort = 'timemodified'; + } else { + $sort = 'name'; + } + + if (is_null($tagset)) { + // No tag set received, so fetch tags from database. + // Always add query by tagcollid even when it's not known to make use of the table index. + $tagcloud = core_tag_collection::get_tag_cloud(0, '', $nr_of_tags, $sort); + } else { + $tagsincloud = $tagset; + + $etags = array(); + foreach ($tagsincloud as $tag) { + $etags[] = $tag; + } + + core_tag_collection::$cloudsortfield = $sort; + usort($tagsincloud, "core_tag_collection::cloud_sort"); + + $tagcloud = new \core_tag\output\tagcloud($tagsincloud); + } + + $output = $OUTPUT->render_from_template('core_tag/tagcloud', $tagcloud->export_for_template($OUTPUT)); + if ($return) { + return $output; + } else { + echo $output; + } +} + /** * Function that returns tags that start with some text, for use by the autocomplete feature * @@ -2744,3 +3139,838 @@ function tag_autocomplete($text) { FROM {tag} tg WHERE tg.name LIKE ?", array(core_text::strtolower($text)."%")); } + +/** + * Prints a box with the description of a tag and its related tags + * + * @package core_tag + * @deprecated since 3.1 + * @param stdClass $tag_object + * @param bool $return if true the function will return the generated tag cloud instead of displaying it. + * @return string/null a HTML box showing a description of the tag object and it's relationsips or null if output is done directly + * in the function. + */ +function tag_print_description_box($tag_object, $return=false) { + global $USER, $CFG, $OUTPUT; + require_once($CFG->libdir.'/filelib.php'); + + debugging('Function tag_print_description_box() is deprecated without replacement. ' . + 'See core_tag_renderer for similar code.', DEBUG_DEVELOPER); + + $relatedtags = array(); + if ($tag = core_tag_tag::get($tag_object->id)) { + $relatedtags = $tag->get_related_tags(); + } + + $content = !empty($tag_object->description); + $output = ''; + + if ($content) { + $output .= $OUTPUT->box_start('generalbox tag-description'); + } + + if (!empty($tag_object->description)) { + $options = new stdClass(); + $options->para = false; + $options->overflowdiv = true; + $tag_object->description = file_rewrite_pluginfile_urls($tag_object->description, 'pluginfile.php', context_system::instance()->id, 'tag', 'description', $tag_object->id); + $output .= format_text($tag_object->description, $tag_object->descriptionformat, $options); + } + + if ($content) { + $output .= $OUTPUT->box_end(); + } + + if ($relatedtags) { + $output .= $OUTPUT->tag_list($relatedtags, get_string('relatedtags', 'tag'), 'tag-relatedtags'); + } + + if ($return) { + return $output; + } else { + echo $output; + } +} + +/** + * Prints a box that contains the management links of a tag + * + * @deprecated since 3.1 + * @param core_tag_tag|stdClass $tag_object + * @param bool $return if true the function will return the generated tag cloud instead of displaying it. + * @return string|null a HTML string or null if this function does the output + */ +function tag_print_management_box($tag_object, $return=false) { + global $USER, $CFG, $OUTPUT; + + debugging('Function tag_print_description_box() is deprecated without replacement. ' . + 'See core_tag_renderer for similar code.', DEBUG_DEVELOPER); + + $tagname = core_tag_tag::make_display_name($tag_object); + $output = ''; + + if (!isguestuser()) { + $output .= $OUTPUT->box_start('box','tag-management-box'); + $systemcontext = context_system::instance(); + $links = array(); + + // Add a link for users to add/remove this from their interests + if (core_tag_tag::is_enabled('core', 'user') && core_tag_area::get_collection('core', 'user') == $tag_object->tagcollid) { + if (core_tag_tag::is_item_tagged_with('core', 'user', $USER->id, $tag_object->name)) { + $links[] = ''. + get_string('removetagfrommyinterests', 'tag', $tagname) .''; + } else { + $links[] = ''. + get_string('addtagtomyinterests', 'tag', $tagname) .''; + } + } + + // Flag as inappropriate link. Only people with moodle/tag:flag capability. + if (has_capability('moodle/tag:flag', $systemcontext)) { + $links[] = ''. get_string('flagasinappropriate', + 'tag', rawurlencode($tagname)) .''; + } + + // Edit tag: Only people with moodle/tag:edit capability who either have it as an interest or can manage tags + if (has_capability('moodle/tag:edit', $systemcontext) || + has_capability('moodle/tag:manage', $systemcontext)) { + $links[] = '' . + get_string('edittag', 'tag') . ''; + } + + $output .= implode(' | ', $links); + $output .= $OUTPUT->box_end(); + } + + if ($return) { + return $output; + } else { + echo $output; + } +} + +/** + * Prints the tag search box + * + * @deprecated since 3.1 + * @param bool $return if true return html string + * @return string|null a HTML string or null if this function does the output + */ +function tag_print_search_box($return=false) { + global $CFG, $OUTPUT; + + debugging('Function tag_print_search_box() is deprecated without replacement. ' . + 'See core_tag_renderer for similar code.', DEBUG_DEVELOPER); + + $query = optional_param('query', '', PARAM_RAW); + + $output = $OUTPUT->box_start('','tag-search-box'); + $output .= ''; + $output .= '
    '; + $output .= ''; + $output .= ''; + $output .= '
    '; + $output .= '
    '; + $output .= ''; + $output .= $OUTPUT->box_end(); + + if ($return) { + return $output; + } + else { + echo $output; + } +} + +/** + * Prints the tag search results + * + * @deprecated since 3.1 + * @param string $query text that tag names will be matched against + * @param int $page current page + * @param int $perpage nr of users displayed per page + * @param bool $return if true return html string + * @return string|null a HTML string or null if this function does the output + */ +function tag_print_search_results($query, $page, $perpage, $return=false) { + global $CFG, $USER, $OUTPUT; + + debugging('Function tag_print_search_results() is deprecated without replacement. ' . + 'In /tag/search.php the search results are printed using the core_tag/tagcloud template.', DEBUG_DEVELOPER); + + $query = clean_param($query, PARAM_TAG); + + $count = count(tag_find_tags($query, false)); + $tags = array(); + + if ( $found_tags = tag_find_tags($query, true, $page * $perpage, $perpage) ) { + $tags = array_values($found_tags); + } + + $baseurl = $CFG->wwwroot.'/tag/search.php?query='. rawurlencode($query); + $output = ''; + + // link "Add $query to my interests" + $addtaglink = ''; + if (core_tag_tag::is_enabled('core', 'user') && !core_tag_tag::is_item_tagged_with('core', 'user', $USER->id, $query)) { + $addtaglink = html_writer::link(new moodle_url('/tag/user.php', array('action' => 'addinterest', 'sesskey' => sesskey(), + 'tag' => $query)), get_string('addtagtomyinterests', 'tag', s($query))); + } + + if ( !empty($tags) ) { // there are results to display!! + $output .= $OUTPUT->heading(get_string('searchresultsfor', 'tag', htmlspecialchars($query)) ." : {$count}", 3, 'main'); + + //print a link "Add $query to my interests" + if (!empty($addtaglink)) { + $output .= $OUTPUT->box($addtaglink, 'box', 'tag-management-box'); + } + + $nr_of_lis_per_ul = 6; + $nr_of_uls = ceil( sizeof($tags) / $nr_of_lis_per_ul ); + + $output .= ''; + $output .= '
     
    '; // <-- small layout hack in order to look good in Firefox + + $output .= $OUTPUT->paging_bar($count, $page, $perpage, $baseurl); + } + else { //no results were found!! + $output .= $OUTPUT->heading(get_string('noresultsfor', 'tag', htmlspecialchars($query)), 3, 'main'); + + //print a link "Add $query to my interests" + if (!empty($addtaglink)) { + $output .= $OUTPUT->box($addtaglink, 'box', 'tag-management-box'); + } + } + + if ($return) { + return $output; + } + else { + echo $output; + } +} + +/** + * Prints a table of the users tagged with the tag passed as argument + * + * @deprecated since 3.1 + * @param stdClass $tagobject the tag we wish to return data for + * @param int $limitfrom (optional, required if $limitnum is set) prints users starting at this point. + * @param int $limitnum (optional, required if $limitfrom is set) prints this many users. + * @param bool $return if true return html string + * @return string|null a HTML string or null if this function does the output + */ +function tag_print_tagged_users_table($tagobject, $limitfrom='', $limitnum='', $return=false) { + + debugging('Function tag_print_tagged_users_table() is deprecated without replacement. ' . + 'See core_user_renderer for similar code.', DEBUG_DEVELOPER); + + //List of users with this tag + $tagobject = core_tag_tag::get($tagobject->id); + $userlist = $tagobject->get_tagged_items('core', 'user', $limitfrom, $limitnum); + + $output = tag_print_user_list($userlist, true); + + if ($return) { + return $output; + } + else { + echo $output; + } +} + +/** + * Prints an individual user box + * + * @deprecated since 3.1 + * @param user_object $user (contains the following fields: id, firstname, lastname and picture) + * @param bool $return if true return html string + * @return string|null a HTML string or null if this function does the output + */ +function tag_print_user_box($user, $return=false) { + global $CFG, $OUTPUT; + + debugging('Function tag_print_user_box() is deprecated without replacement. ' . + 'See core_user_renderer for similar code.', DEBUG_DEVELOPER); + + $usercontext = context_user::instance($user->id); + $profilelink = ''; + + if ($usercontext and (has_capability('moodle/user:viewdetails', $usercontext) || has_coursecontact_role($user->id))) { + $profilelink = $CFG->wwwroot .'/user/view.php?id='. $user->id; + } + + $output = $OUTPUT->box_start('user-box', 'user'. $user->id); + $fullname = fullname($user); + $alt = ''; + + if (!empty($profilelink)) { + $output .= ''; + $alt = $fullname; + } + + $output .= $OUTPUT->user_picture($user, array('size'=>100)); + $output .= '
    '; + + if (!empty($profilelink)) { + $output .= '
    '; + } + + //truncate name if it's too big + if (core_text::strlen($fullname) > 26) { + $fullname = core_text::substr($fullname, 0, 26) .'...'; + } + + $output .= ''. $fullname .''; + $output .= $OUTPUT->box_end(); + + if ($return) { + return $output; + } + else { + echo $output; + } +} + +/** + * Prints a list of users + * + * @deprecated since 3.1 + * @param array $userlist an array of user objects + * @param bool $return if true return html string, otherwise output the result + * @return string|null a HTML string or null if this function does the output + */ +function tag_print_user_list($userlist, $return=false) { + + debugging('Function tag_print_user_list() is deprecated without replacement. ' . + 'See core_user_renderer for similar code.', DEBUG_DEVELOPER); + + $output = '
      '; + + foreach ($userlist as $user){ + $output .= '
    • '. tag_print_user_box($user, true) ."
    • \n"; + } + $output .= "
    \n"; + + if ($return) { + return $output; + } + else { + echo $output; + } +} + +/** + * Function that returns the name that should be displayed for a specific tag + * + * @package core_tag + * @category tag + * @deprecated since 3.1 + * @param stdClass|core_tag_tag $tagobject a line out of tag table, as returned by the adobd functions + * @param int $html TAG_RETURN_HTML (default) will return htmlspecialchars encoded string, TAG_RETURN_TEXT will not encode. + * @return string + */ +function tag_display_name($tagobject, $html=TAG_RETURN_HTML) { + debugging('Function tag_display_name() is deprecated. Use core_tag_tag::make_display_name().', DEBUG_DEVELOPER); + if (!isset($tagobject->name)) { + return ''; + } + return core_tag_tag::make_display_name($tagobject, $html != TAG_RETURN_TEXT); +} + +/** + * Function that normalizes a list of tag names. + * + * @package core_tag + * @deprecated since 3.1 + * @param array/string $rawtags array of tags, or a single tag. + * @param int $case case to use for returned value (default: lower case). Either TAG_CASE_LOWER (default) or TAG_CASE_ORIGINAL + * @return array lowercased normalized tags, indexed by the normalized tag, in the same order as the original array. + * (Eg: 'Banana' => 'banana'). + */ +function tag_normalize($rawtags, $case = TAG_CASE_LOWER) { + debugging('Function tag_normalize() is deprecated. Use core_tag_tag::normalize().', DEBUG_DEVELOPER); + + if ( !is_array($rawtags) ) { + $rawtags = array($rawtags); + } + + return core_tag_tag::normalize($rawtags, $case == TAG_CASE_LOWER); +} + +/** + * Get a comma-separated list of tags related to another tag. + * + * @package core_tag + * @deprecated since 3.1 + * @param array $related_tags the array returned by tag_get_related_tags + * @param int $html either TAG_RETURN_HTML (default) or TAG_RETURN_TEXT : return html links, or just text. + * @return string comma-separated list + */ +function tag_get_related_tags_csv($related_tags, $html=TAG_RETURN_HTML) { + global $OUTPUT; + debugging('Method tag_get_related_tags_csv() is deprecated. Consider ' + . 'looping through array or using $OUTPUT->tag_list(core_tag_tag::get_item_tags())', + DEBUG_DEVELOPER); + if ($html != TAG_RETURN_TEXT) { + return $OUTPUT->tag_list($related_tags, ''); + } + + $tagsnames = array(); + foreach ($related_tags as $tag) { + $tagsnames[] = core_tag_tag::make_display_name($tag, false); + } + return implode(', ', $tagsnames); +} + +/** + * Used to require that the return value from a function is an array. + * Only used in the deprecated function {@link tag_get_id()} + * @deprecated since 3.1 + */ +define('TAG_RETURN_ARRAY', 0); +/** + * Used to require that the return value from a function is an object. + * Only used in the deprecated function {@link tag_get_id()} + * @deprecated since 3.1 + */ +define('TAG_RETURN_OBJECT', 1); +/** + * Use to specify that HTML free text is expected to be returned from a function. + * Only used in deprecated functions {@link tag_get_tags_csv()}, {@link tag_display_name()}, + * {@link tag_get_related_tags_csv()} + * @deprecated since 3.1 + */ +define('TAG_RETURN_TEXT', 2); +/** + * Use to specify that encoded HTML is expected to be returned from a function. + * Only used in deprecated functions {@link tag_get_tags_csv()}, {@link tag_display_name()}, + * {@link tag_get_related_tags_csv()} + * @deprecated since 3.1 + */ +define('TAG_RETURN_HTML', 3); + +/** + * Used to specify that we wish a lowercased string to be returned + * Only used in deprecated function {@link tag_normalize()} + * @deprecated since 3.1 + */ +define('TAG_CASE_LOWER', 0); +/** + * Used to specify that we do not wish the case of the returned string to change + * Only used in deprecated function {@link tag_normalize()} + * @deprecated since 3.1 + */ +define('TAG_CASE_ORIGINAL', 1); + +/** + * Used to specify that we want all related tags returned, no matter how they are related. + * Only used in deprecated function {@link tag_get_related_tags()} + * @deprecated since 3.1 + */ +define('TAG_RELATED_ALL', 0); +/** + * Used to specify that we only want back tags that were manually related. + * Only used in deprecated function {@link tag_get_related_tags()} + * @deprecated since 3.1 + */ +define('TAG_RELATED_MANUAL', 1); +/** + * Used to specify that we only want back tags where the relationship was automatically correlated. + * Only used in deprecated function {@link tag_get_related_tags()} + * @deprecated since 3.1 + */ +define('TAG_RELATED_CORRELATED', 2); + +/** + * Set the tags assigned to a record. This overwrites the current tags. + * + * This function is meant to be fed the string coming up from the user interface, which contains all tags assigned to a record. + * + * Due to API change $component and $contextid are now required. Instead of + * calling this function you can use {@link core_tag_tag::set_item_tags()} or + * {@link core_tag_tag::set_related_tags()} + * + * @package core_tag + * @deprecated since 3.1 + * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, 'tag' for tags, etc.) + * @param int $itemid the id of the record to tag + * @param array $tags the array of tags to set on the record. If given an empty array, all tags will be removed. + * @param string|null $component the component that was tagged + * @param int|null $contextid the context id of where this tag was assigned + * @return bool|null + */ +function tag_set($itemtype, $itemid, $tags, $component = null, $contextid = null) { + debugging('Function tag_set() is deprecated. Use ' . + ' core_tag_tag::set_item_tags() instead', DEBUG_DEVELOPER); + + if ($itemtype === 'tag') { + return core_tag_tag::get($itemid, '*', MUST_EXIST)->set_related_tags($tags); + } else { + $context = $contextid ? context::instance_by_id($contextid) : context_system::instance(); + return core_tag_tag::set_item_tags($component, $itemtype, $itemid, $context, $tags); + } +} + +/** + * Adds a tag to a record, without overwriting the current tags. + * + * This function remains here for backward compatiblity. It is recommended to use + * {@link core_tag_tag::add_item_tag()} or {@link core_tag_tag::add_related_tags()} instead + * + * @package core_tag + * @deprecated since 3.1 + * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.) + * @param int $itemid the id of the record to tag + * @param string $tag the tag to add + * @param string|null $component the component that was tagged + * @param int|null $contextid the context id of where this tag was assigned + * @return bool|null + */ +function tag_set_add($itemtype, $itemid, $tag, $component = null, $contextid = null) { + debugging('Function tag_set_add() is deprecated. Use ' . + ' core_tag_tag::add_item_tag() instead', DEBUG_DEVELOPER); + + if ($itemtype === 'tag') { + return core_tag_tag::get($itemid, '*', MUST_EXIST)->add_related_tags(array($tag)); + } else { + $context = $contextid ? context::instance_by_id($contextid) : context_system::instance(); + return core_tag_tag::add_item_tag($component, $itemtype, $itemid, $context, $tag); + } +} + +/** + * Removes a tag from a record, without overwriting other current tags. + * + * This function remains here for backward compatiblity. It is recommended to use + * {@link core_tag_tag::remove_item_tag()} instead + * + * @package core_tag + * @deprecated since 3.1 + * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.) + * @param int $itemid the id of the record to tag + * @param string $tag the tag to delete + * @param string|null $component the component that was tagged + * @param int|null $contextid the context id of where this tag was assigned + * @return bool|null + */ +function tag_set_delete($itemtype, $itemid, $tag, $component = null, $contextid = null) { + debugging('Function tag_set_delete() is deprecated. Use ' . + ' core_tag_tag::remove_item_tag() instead', DEBUG_DEVELOPER); + return core_tag_tag::remove_item_tag($component, $itemtype, $itemid, $tag); +} + +/** + * Simple function to just return a single tag object when you know the name or something + * + * See also {@link core_tag_tag::get()} and {@link core_tag_tag::get_by_name()} + * + * @package core_tag + * @deprecated since 3.1 + * @param string $field which field do we use to identify the tag: id, name or rawname + * @param string $value the required value of the aforementioned field + * @param string $returnfields which fields do we want returned. This is a comma seperated string containing any combination of + * 'id', 'name', 'rawname' or '*' to include all fields. + * @return mixed tag object + */ +function tag_get($field, $value, $returnfields='id, name, rawname, tagcollid') { + global $DB; + debugging('Function tag_get() is deprecated. Use ' . + ' core_tag_tag::get() or core_tag_tag::get_by_name()', + DEBUG_DEVELOPER); + if ($field === 'id') { + $tag = core_tag_tag::get((int)$value, $returnfields); + } else if ($field === 'name') { + $tag = core_tag_tag::get_by_name(0, $value, $returnfields); + } else { + $params = array($field => $value); + return $DB->get_record('tag', $params, $returnfields); + } + if ($tag) { + return $tag->to_object(); + } + return null; +} + +/** + * Returns tags related to a tag + * + * Related tags of a tag come from two sources: + * - manually added related tags, which are tag_instance entries for that tag + * - correlated tags, which are calculated + * + * @package core_tag + * @deprecated since 3.1 + * @param string $tagid is a single **normalized** tag name or the id of a tag + * @param int $type the function will return either manually (TAG_RELATED_MANUAL) related tags or correlated + * (TAG_RELATED_CORRELATED) tags. Default is TAG_RELATED_ALL, which returns everything. + * @param int $limitnum (optional) return a subset comprising this many records, the default is 10 + * @return array an array of tag objects + */ +function tag_get_related_tags($tagid, $type=TAG_RELATED_ALL, $limitnum=10) { + debugging('Method tag_get_related_tags() is deprecated, ' + . 'use core_tag_tag::get_correlated_tags(), core_tag_tag::get_related_tags() or ' + . 'core_tag_tag::get_manual_related_tags()', DEBUG_DEVELOPER); + $result = array(); + if ($tag = core_tag_tag::get($tagid)) { + if ($type == TAG_RELATED_CORRELATED) { + $tags = $tag->get_correlated_tags(); + } else if ($type == TAG_RELATED_MANUAL) { + $tags = $tag->get_manual_related_tags(); + } else { + $tags = $tag->get_related_tags(); + } + $tags = array_slice($tags, 0, $limitnum); + foreach ($tags as $id => $tag) { + $result[$id] = $tag->to_object(); + } + } + return $result; +} + +/** + * Delete one or more tag, and all their instances if there are any left. + * + * @package core_tag + * @deprecated since 3.1 + * @param mixed $tagids one tagid (int), or one array of tagids to delete + * @return bool true on success, false otherwise + */ +function tag_delete($tagids) { + debugging('Method tag_delete() is deprecated, use core_tag_tag::delete_tags()', + DEBUG_DEVELOPER); + return core_tag_tag::delete_tags($tagids); +} + +/** + * Deletes all the tag instances given a component and an optional contextid. + * + * @deprecated since 3.1 + * @param string $component + * @param int $contextid if null, then we delete all tag instances for the $component + */ +function tag_delete_instances($component, $contextid = null) { + debugging('Method tag_delete() is deprecated, use core_tag_tag::delete_instances()', + DEBUG_DEVELOPER); + core_tag_tag::delete_instances($component, null, $contextid); +} + +/** + * Clean up the tag tables, making sure all tagged object still exists. + * + * This should normally not be necessary, but in case related tags are not deleted when the tagged record is removed, this should be + * done once in a while, perhaps on an occasional cron run. On a site with lots of tags, this could become an expensive function to + * call: don't run at peak time. + * + * @package core_tag + * @deprecated since 3.1 + */ +function tag_cleanup() { + debugging('Method tag_cleanup() is deprecated, use \core\task\tag_cron_task::cleanup()', + DEBUG_DEVELOPER); + + $task = new \core\task\tag_cron_task(); + return $task->cleanup(); +} + +/** + * This function will delete numerous tag instances efficiently. + * This removes tag instances only. It doesn't check to see if it is the last use of a tag. + * + * @deprecated since 3.1 + * @param array $instances An array of tag instance objects with the addition of the tagname and tagrawname + * (used for recording a delete event). + */ +function tag_bulk_delete_instances($instances) { + debugging('Method tag_bulk_delete_instances() is deprecated, ' + . 'use \core\task\tag_cron_task::bulk_delete_instances()', + DEBUG_DEVELOPER); + + $task = new \core\task\tag_cron_task(); + return $task->bulk_delete_instances($instances); +} + +/** + * Calculates and stores the correlated tags of all tags. The correlations are stored in the 'tag_correlation' table. + * + * Two tags are correlated if they appear together a lot. Ex.: Users tagged with "computers" will probably also be tagged with "algorithms". + * + * The rationale for the 'tag_correlation' table is performance. It works as a cache for a potentially heavy load query done at the + * 'tag_instance' table. So, the 'tag_correlation' table stores redundant information derived from the 'tag_instance' table. + * + * @package core_tag + * @deprecated since 3.1 + * @param int $mincorrelation Only tags with more than $mincorrelation correlations will be identified. + */ +function tag_compute_correlations($mincorrelation = 2) { + debugging('Method tag_compute_correlations() is deprecated, ' + . 'use \core\task\tag_cron_task::compute_correlations()', + DEBUG_DEVELOPER); + + $task = new \core\task\tag_cron_task(); + return $task->compute_correlations($mincorrelation); +} + +/** + * This function processes a tag correlation and makes changes in the database as required. + * + * The tag correlation object needs have both a tagid property and a correlatedtags property that is an array. + * + * @package core_tag + * @deprecated since 3.1 + * @param stdClass $tagcorrelation + * @return int/bool The id of the tag correlation that was just processed or false. + */ +function tag_process_computed_correlation(stdClass $tagcorrelation) { + debugging('Method tag_process_computed_correlation() is deprecated, ' + . 'use \core\task\tag_cron_task::process_computed_correlation()', + DEBUG_DEVELOPER); + + $task = new \core\task\tag_cron_task(); + return $task->process_computed_correlation($tagcorrelation); +} + +/** + * Tasks that should be performed at cron time + * + * @package core_tag + * @deprecated since 3.1 + */ +function tag_cron() { + debugging('Method tag_cron() is deprecated, use \core\task\tag_cron_task::execute()', + DEBUG_DEVELOPER); + + $task = new \core\task\tag_cron_task(); + $task->execute(); +} + +/** + * Search for tags with names that match some text + * + * @package core_tag + * @deprecated since 3.1 + * @param string $text escaped string that the tag names will be matched against + * @param bool $ordered If true, tags are ordered by their popularity. If false, no ordering. + * @param int/string $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point. + * @param int/string $limitnum (optional, required if $limitfrom is set) return a subset comprising this many records. + * @param int $tagcollid + * @return array/boolean an array of objects, or false if no records were found or an error occured. + */ +function tag_find_tags($text, $ordered=true, $limitfrom='', $limitnum='', $tagcollid = null) { + debugging('Method tag_find_tags() is deprecated without replacement', DEBUG_DEVELOPER); + global $DB; + + $text = core_text::strtolower(clean_param($text, PARAM_TAG)); + + list($sql, $params) = $DB->get_in_or_equal($tagcollid ? array($tagcollid) : + array_keys(core_tag_collection::get_collections(true))); + array_unshift($params, "%{$text}%"); + + if ($ordered) { + $query = "SELECT tg.id, tg.name, tg.rawname, tg.tagcollid, COUNT(ti.id) AS count + FROM {tag} tg LEFT JOIN {tag_instance} ti ON tg.id = ti.tagid + WHERE tg.name LIKE ? AND tg.tagcollid $sql + GROUP BY tg.id, tg.name, tg.rawname + ORDER BY count DESC"; + } else { + $query = "SELECT tg.id, tg.name, tg.rawname, tg.tagcollid + FROM {tag} tg + WHERE tg.name LIKE ? AND tg.tagcollid $sql"; + } + return $DB->get_records_sql($query, $params, $limitfrom , $limitnum); +} + +/** + * Get the name of a tag + * + * @package core_tag + * @deprecated since 3.1 + * @param mixed $tagids the id of the tag, or an array of ids + * @return mixed string name of one tag, or id-indexed array of strings + */ +function tag_get_name($tagids) { + debugging('Method tag_get_name() is deprecated without replacement', DEBUG_DEVELOPER); + global $DB; + + if (!is_array($tagids)) { + if ($tag = $DB->get_record('tag', array('id'=>$tagids))) { + return $tag->name; + } + return false; + } + + $tag_names = array(); + foreach($DB->get_records_list('tag', 'id', $tagids) as $tag) { + $tag_names[$tag->id] = $tag->name; + } + + return $tag_names; +} + +/** + * Returns the correlated tags of a tag, retrieved from the tag_correlation table. Make sure cron runs, otherwise the table will be + * empty and this function won't return anything. + * + * Correlated tags are calculated in cron based on existing tag instances. + * + * @package core_tag + * @deprecated since 3.1 + * @param int $tagid is a single tag id + * @param int $notused this argument is no longer used + * @return array an array of tag objects or an empty if no correlated tags are found + */ +function tag_get_correlated($tagid, $notused = null) { + debugging('Method tag_get_correlated() is deprecated, ' + . 'use core_tag_tag::get_correlated_tags()', DEBUG_DEVELOPER); + $result = array(); + if ($tag = core_tag_tag::get($tagid)) { + $tags = $tag->get_correlated_tags(true); + // Convert to objects for backward-compatibility. + foreach ($tags as $id => $tag) { + $result[$id] = $tag->to_object(); + } + } + return $result; +} + +/** + * This function is used by print_tag_cloud, to usort() the tags in the cloud. See php.net/usort for the parameters documentation. + * This was originally in blocks/blog_tags/block_blog_tags.php, named blog_tags_sort(). + * + * @package core_tag + * @deprecated since 3.1 + * @param string $a Tag name to compare against $b + * @param string $b Tag name to compare against $a + * @return int The result of the comparison/validation 1, 0 or -1 + */ +function tag_cloud_sort($a, $b) { + debugging('Method tag_cloud_sort() is deprecated, similar method can be found in core_tag_collection::cloud_sort()', DEBUG_DEVELOPER); + global $CFG; + + if (empty($CFG->tagsort)) { + $tagsort = 'name'; // by default, sort by name + } else { + $tagsort = $CFG->tagsort; + } + + if (is_numeric($a->$tagsort)) { + return ($a->$tagsort == $b->$tagsort) ? 0 : ($a->$tagsort > $b->$tagsort) ? 1 : -1; + } elseif (is_string($a->$tagsort)) { + return strcmp($a->$tagsort, $b->$tagsort); + } else { + return 0; + } +} diff --git a/lib/form/tags.php b/lib/form/tags.php index a4727672634..096bbd91955 100644 --- a/lib/form/tags.php +++ b/lib/form/tags.php @@ -63,6 +63,12 @@ class MoodleQuickForm_tags extends MoodleQuickForm_autocomplete { */ protected $showingofficial = false; + /** + * Options passed when creating an element. + * @var array + */ + protected $tagsoptions = array(); + /** * Constructor * @@ -72,29 +78,66 @@ class MoodleQuickForm_tags extends MoodleQuickForm_autocomplete { * @param mixed $attributes Either a typical HTML attribute string or an associative array. */ public function __construct($elementName = null, $elementLabel = null, $options = array(), $attributes = null) { - if (!isset($options['display'])) { - $options['display'] = self::DEFAULTUI; - } - - $this->showingofficial = $options['display'] != MoodleQuickForm_tags::NOOFFICIAL; - $validoptions = array(); - if ($this->showingofficial) { - $validoptions = $this->load_official_tags(); + + if (!empty($options)) { + // Only execute it when the element was created and $options has values set by user. + // In onQuickFormEvent() we make sure that $options is not empty even if developer left it empty. + if (empty($options['display'])) { + $options['display'] = self::DEFAULTUI; + } + $this->tagsoptions = $options; + + $this->showingofficial = $options['display'] != self::NOOFFICIAL; + + if ($this->showingofficial) { + $validoptions = $this->load_official_tags(); + } + // Option 'tags' allows us to type new tags. + if ($options['display'] == self::ONLYOFFICIAL) { + $attributes['tags'] = false; + } else { + $attributes['tags'] = true; + } + $attributes['multiple'] = 'multiple'; + $attributes['placeholder'] = get_string('entertags', 'tag'); + $attributes['showsuggestions'] = $this->showingofficial; } - // 'tags' option allows us to type new tags. - if ($options['display'] == MoodleQuickForm_tags::ONLYOFFICIAL) { - $attributes['tags'] = false; - } else { - $attributes['tags'] = true; - } - $attributes['multiple'] = 'multiple'; - $attributes['placeholder'] = get_string('entertags', 'tag'); - $attributes['showsuggestions'] = $this->showingofficial; parent::__construct($elementName, $elementLabel, $validoptions, $attributes); } + /** + * Called by HTML_QuickForm whenever form event is made on this element + * + * @param string $event Name of event + * @param mixed $arg event arguments + * @param object $caller calling object + * @return bool + */ + public function onQuickFormEvent($event, $arg, &$caller) { + if ($event === 'createElement') { + $arg[2] += array('itemtype' => '', 'component' => ''); + } + return parent::onQuickFormEvent($event, $arg, $caller); + } + + /** + * Checks if tagging is enabled for this itemtype + * + * @return boolean + */ + protected function is_tagging_enabled() { + if (!empty($this->tagsoptions['itemtype']) && !empty($this->tagsoptions['component'])) { + $enabled = core_tag_tag::is_enabled($this->tagsoptions['component'], $this->tagsoptions['itemtype']); + if ($enabled === false) { + return false; + } + } + // Backward compatibility with code developed before Moodle 3.0 where itemtype/component were not specified. + return true; + } + /** * Old syntax of class constructor. Deprecated in PHP7. * @@ -105,21 +148,41 @@ class MoodleQuickForm_tags extends MoodleQuickForm_autocomplete { self::__construct($elementName, $elementLabel, $options, $attributes); } + /** + * Finds the tag collection to use for official tag selector + * + * @return int + */ + protected function get_tag_collection() { + if (empty($this->tagsoptions['tagcollid']) && (empty($this->tagsoptions['itemtype']) || + empty($this->tagsoptions['component']))) { + debugging('You need to specify \'itemtype\' and \'component\' of the tagged ' + . 'area in the tags form element options', + DEBUG_DEVELOPER); + } + if (!empty($this->tagsoptions['tagcollid'])) { + return $this->tagsoptions['tagcollid']; + } + if ($this->tagsoptions['itemtype']) { + $this->tagsoptions['tagcollid'] = core_tag_area::get_collection($this->tagsoptions['component'], + $this->tagsoptions['itemtype']); + } else { + $this->tagsoptions['tagcollid'] = core_tag_collection::get_default(); + } + return $this->tagsoptions['tagcollid']; + } + /** * Returns HTML for select form element. * * @return string */ function toHtml(){ - global $CFG, $OUTPUT; - - if (empty($CFG->usetags)) { - debugging('A tags formslib field has been created even thought $CFG->usetags is false.', DEBUG_DEVELOPER); - } + global $OUTPUT; $managelink = ''; if (has_capability('moodle/tag:manage', context_system::instance()) && $this->showingofficial) { - $url = $CFG->wwwroot .'/tag/manage.php'; + $url = new moodle_url('/tag/manage.php', array('tc' => $this->get_tag_collection())); $managelink = ' ' . $OUTPUT->action_link($url, get_string('manageofficialtags', 'tag')); } @@ -127,21 +190,47 @@ class MoodleQuickForm_tags extends MoodleQuickForm_autocomplete { } /** - * Internal function to load official tags + * Accepts a renderer * - * @access protected + * @param HTML_QuickForm_Renderer $renderer An HTML_QuickForm_Renderer object + * @param bool $required Whether a group is required + * @param string $error An error message associated with a group + */ + public function accept(&$renderer, $required = false, $error = null) { + if ($this->is_tagging_enabled()) { + $renderer->renderElement($this, $required, $error); + } else { + $renderer->renderHidden($this); + } + } + + /** + * Internal function to load official tags */ protected function load_official_tags() { global $CFG, $DB; - - $namefield = empty($CFG->keeptagnamecase) ? 'name' : 'rawname'; - $records = $DB->get_records('tag', array('tagtype' => 'official'), $namefield, 'id,' . $namefield); - $tags = array(); - - foreach ($records as $record) { - $tags[$record->$namefield] = $record->$namefield; + if (!$this->is_tagging_enabled()) { + return array(); } - return $tags; + $namefield = empty($CFG->keeptagnamecase) ? 'name' : 'rawname'; + $tags = $DB->get_records_menu('tag', + array('tagtype' => 'official', 'tagcollid' => $this->get_tag_collection()), + $namefield, 'id,' . $namefield); + return array_combine($tags, $tags); } + /** + * Returns a 'safe' element's value + * + * @param array $submitValues array of submitted values to search + * @param bool $assoc whether to return the value as associative array + * @return mixed + */ + public function exportValue(&$submitValues, $assoc = false) { + if (!$this->is_tagging_enabled()) { + return $assoc ? array($this->getName() => array()) : array(); + } + + return parent::exportValue($submitValues, $assoc); + } } diff --git a/lib/moodlelib.php b/lib/moodlelib.php index e9598eb71ca..5638971b966 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -3901,7 +3901,6 @@ function delete_user(stdClass $user) { require_once($CFG->libdir.'/grouplib.php'); require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/message/lib.php'); - require_once($CFG->dirroot.'/tag/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); // Make sure nobody sends bogus record type as parameter. @@ -3944,7 +3943,7 @@ function delete_user(stdClass $user) { // TODO: remove from cohorts using standard API here. // Remove user tags. - tag_set('user', $user->id, array(), 'core', $usercontext->id); + core_tag_tag::remove_all_item_tags('core', 'user', $user->id); // Unconditionally unenrol from all courses. enrol_user_delete($user); @@ -4735,7 +4734,6 @@ function remove_course_contents($courseid, $showfeedback = true, array $options require_once($CFG->libdir.'/questionlib.php'); require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/group/lib.php'); - require_once($CFG->dirroot.'/tag/lib.php'); require_once($CFG->dirroot.'/comment/lib.php'); require_once($CFG->dirroot.'/rating/lib.php'); require_once($CFG->dirroot.'/notes/lib.php'); @@ -4910,7 +4908,7 @@ function remove_course_contents($courseid, $showfeedback = true, array $options $rm->delete_ratings($delopt); // Delete course tags. - tag_set('course', $course->id, array(), 'core', $coursecontext->id); + core_tag_tag::remove_all_item_tags('core', 'course', $course->id); // Delete calendar events. $DB->delete_records('event', array('courseid' => $course->id)); diff --git a/lib/myprofilelib.php b/lib/myprofilelib.php index 825753bab22..e397770ad57 100644 --- a/lib/myprofilelib.php +++ b/lib/myprofilelib.php @@ -23,7 +23,6 @@ */ defined('MOODLE_INTERNAL') || die(); -require_once($CFG->dirroot . '/tag/lib.php'); /** * Defines core nodes for my profile navigation tree. @@ -36,7 +35,7 @@ require_once($CFG->dirroot . '/tag/lib.php'); * @return bool */ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $iscurrentuser, $course) { - global $CFG, $USER, $DB, $PAGE; + global $CFG, $USER, $DB, $PAGE, $OUTPUT; $usercontext = context_user::instance($user->id, MUST_EXIST); $systemcontext = context_system::instance(); @@ -218,11 +217,10 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, } // Printing tagged interests. We want this only for full profile. - if (!empty($CFG->usetags) && empty($course)) { - if ($interests = tag_get_tags_csv('user', $user->id) ) { - $node = new core_user\output\myprofile\node('contact', 'interests', get_string('interests'), null, null, $interests); - $tree->add_node($node); - } + if (empty($course) && ($interests = core_tag_tag::get_item_tags('core', 'user', $user->id))) { + $node = new core_user\output\myprofile\node('contact', 'interests', get_string('interests'), null, null, + $OUTPUT->tag_list($interests, '')); + $tree->add_node($node); } if (!isset($hiddenfields['mycourses'])) { diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index 35fba720204..858061ea009 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -4103,6 +4103,23 @@ EOD; $html .= html_writer::end_tag('header'); return $html; } + + /** + * Displays the list of tags associated with an entry + * + * @param array $tags list of instances of core_tag or stdClass + * @param string $label label to display in front, by default 'Tags' (get_string('tags')), set to null + * to use default, set to '' (empty string) to omit the label completely + * @param string $classes additional classes for the enclosing div element + * @param int $limit limit the number of tags to display, if size of $tags is more than this limit the "more" link + * will be appended to the end, JS will toggle the rest of the tags + * @param context $pagecontext specify if needed to overwrite the current page context for the view tag link + * @return string + */ + public function tag_list($tags, $label = null, $classes = '', $limit = 10, $pagecontext = null) { + $list = new \core_tag\output\taglist($tags, $label, $classes, $limit, $pagecontext); + return $this->render_from_template('core_tag/taglist', $list->export_for_template($this)); + } } /** diff --git a/lib/questionlib.php b/lib/questionlib.php index 2a2d106f86e..a4fbf68ccde 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -334,7 +334,7 @@ function question_delete_question($questionid) { $questionid, $question->contextid); // Delete all tag instances. - $DB->delete_records('tag_instance', array('component' => 'core_question', 'itemid' => $question->id)); + core_tag_tag::remove_all_item_tags('core_question', 'question', $question->id); // Now recursively delete all child questions if ($children = $DB->get_records('question', @@ -435,8 +435,7 @@ function question_delete_course_category($category, $newcategory, $feedback=true } // Update the contextid for any tag instances for questions in the old context. - $DB->set_field('tag_instance', 'contextid', $newcontext->id, array('component' => 'core_question', - 'contextid' => $context->id)); + core_tag_tag::move_context('core_question', 'question', $context, $newcontext); $DB->set_field('question_categories', 'contextid', $newcontext->id, array('contextid' => $context->id)); @@ -544,8 +543,7 @@ function question_move_questions_to_category($questionids, $newcategoryid) { "parent $questionidcondition", $params); // Update the contextid for any tag instances that may exist for these questions. - $DB->set_field_select('tag_instance', 'contextid', $newcontextid, - "component = 'core_question' AND itemid $questionidcondition", $params); + core_tag_tag::change_items_context('core_question', 'question', $questionids, $newcontextid); // TODO Deal with datasets. @@ -577,12 +575,8 @@ function question_move_category_to_context($categoryid, $oldcontextid, $newconte question_bank::notify_question_edited($questionid); } - if ($questionids) { - // Update the contextid for any tag instances that may exist for these questions. - list($questionids, $params) = $DB->get_in_or_equal(array_keys($questionids)); - $DB->set_field_select('tag_instance', 'contextid', $newcontextid, - "component = 'core_question' AND itemid $questionids", $params); - } + core_tag_tag::change_items_context('core_question', 'question', + array_keys($questionids), $newcontextid); $subcatids = $DB->get_records_menu('question_categories', array('parent' => $categoryid), '', 'id,1'); @@ -765,9 +759,8 @@ function _tidy_question($question, $loadtags = false) { unset($question->_partiallyloaded); } - if ($loadtags && !empty($CFG->usetags)) { - require_once($CFG->dirroot . '/tag/lib.php'); - $question->tags = tag_get_tags_array('question', $question->id); + if ($loadtags && core_tag_tag::is_enabled('core_question', 'question')) { + $question->tags = core_tag_tag::get_item_tags_array('core_question', 'question', $question->id); } } diff --git a/lib/testing/generator/data_generator.php b/lib/testing/generator/data_generator.php index 7dfe2a44d50..756b15cf398 100644 --- a/lib/testing/generator/data_generator.php +++ b/lib/testing/generator/data_generator.php @@ -847,6 +847,10 @@ EOD; $record['tagtype'] = 'default'; } + if (!isset($record['tagcollid'])) { + $record['tagcollid'] = core_tag_collection::get_default(); + } + if (!isset($record['description'])) { $record['description'] = 'Tag description'; } diff --git a/lib/testing/tests/generator_test.php b/lib/testing/tests/generator_test.php index 32cb7f309d1..42e07fd2bf1 100644 --- a/lib/testing/tests/generator_test.php +++ b/lib/testing/tests/generator_test.php @@ -174,7 +174,7 @@ class core_test_generator_testcase extends advanced_testcase { $this->assertEquals($course->id, $section->course); $course = $generator->create_course(array('tags' => 'Cat, Dog')); - $this->assertEquals('Cat, Dog', tag_get_tags_csv('course', $course->id, TAG_RETURN_TEXT)); + $this->assertEquals(array('Cat', 'Dog'), array_values(core_tag_tag::get_item_tags_array('core', 'course', $course->id))); $scale = $generator->create_scale(); $this->assertNotEmpty($scale); diff --git a/lib/tests/questionlib_test.php b/lib/tests/questionlib_test.php index 6455616711f..16350107d16 100644 --- a/lib/tests/questionlib_test.php +++ b/lib/tests/questionlib_test.php @@ -29,7 +29,6 @@ global $CFG; require_once($CFG->libdir . '/questionlib.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); -require_once($CFG->dirroot . '/tag/lib.php'); // Get the necessary files to perform backup and restore. require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); @@ -164,21 +163,23 @@ class core_questionlib_testcase extends advanced_testcase { $coursecat2 = $this->getDataGenerator()->create_category(); // Create a couple of categories and questions. + $context1 = context_coursecat::instance($coursecat1->id); + $context2 = context_coursecat::instance($coursecat2->id); $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); $questioncat1 = $questiongenerator->create_question_category(array('contextid' => - context_coursecat::instance($coursecat1->id)->id)); + $context1->id)); $questioncat2 = $questiongenerator->create_question_category(array('contextid' => - context_coursecat::instance($coursecat2->id)->id)); + $context2->id)); $question1 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat1->id)); $question2 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat1->id)); $question3 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat2->id)); $question4 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat2->id)); // Now lets tag these questions. - tag_set('question', $question1->id, array('tag 1', 'tag 2'), 'core_question', $questioncat1->contextid); - tag_set('question', $question2->id, array('tag 3', 'tag 4'), 'core_question', $questioncat1->contextid); - tag_set('question', $question3->id, array('tag 5', 'tag 6'), 'core_question', $questioncat2->contextid); - tag_set('question', $question4->id, array('tag 7', 'tag 8'), 'core_question', $questioncat2->contextid); + core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $context1, array('tag 1', 'tag 2')); + core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $context1, array('tag 3', 'tag 4')); + core_tag_tag::set_item_tags('core_question', 'question', $question3->id, $context2, array('tag 5', 'tag 6')); + core_tag_tag::set_item_tags('core_question', 'question', $question4->id, $context2, array('tag 7', 'tag 8')); // Test moving the questions to another category. question_move_questions_to_category(array($question1->id, $question2->id), $questioncat2->id); @@ -224,14 +225,15 @@ class core_questionlib_testcase extends advanced_testcase { $course = $this->getDataGenerator()->create_course(); // Create some question categories and questions in this course. + $coursecontext = context_course::instance($course->id); $questioncat = $questiongenerator->create_question_category(array('contextid' => - context_course::instance($course->id)->id)); + $coursecontext->id)); $question1 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat->id)); $question2 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat->id)); // Add some tags to these questions. - tag_set('question', $question1->id, array('tag 1', 'tag 2'), 'core_question', $questioncat->contextid); - tag_set('question', $question2->id, array('tag 1', 'tag 2'), 'core_question', $questioncat->contextid); + core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, array('tag 1', 'tag 2')); + core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, array('tag 1', 'tag 2')); // Create a course that we are going to restore the other course to. $course2 = $this->getDataGenerator()->create_course(); diff --git a/lib/upgrade.txt b/lib/upgrade.txt index c7d74efca90..02437f8b7fe 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -31,8 +31,9 @@ information provided here is intended especially for developers. custom user filters. Similar deprecations in existing user_filter_* classes. * table_default_export_format_parent::table_default_export_format_parent() is deprecated, use parent::__construct() in extending classes. - * groups_delete_group_members() $showfeedback parameter has been removed and is no longer - respected. Users of this function should output their own feedback if required. +* groups_delete_group_members() $showfeedback parameter has been removed and is no longer + respected. Users of this function should output their own feedback if required. +* Number of changes to Tags API, see tag/upgrade.txt for more details === 3.0 === diff --git a/lib/upgradelib.php b/lib/upgradelib.php index c44cee9bd6f..1771153be90 100644 --- a/lib/upgradelib.php +++ b/lib/upgradelib.php @@ -494,6 +494,7 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) { message_update_processors($plug); } upgrade_plugin_mnet_functions($component); + core_tag_area::reset_definitions_for_component($component); $endcallback($component, true, $verbose); } } @@ -532,6 +533,7 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) { message_update_processors($plug); } upgrade_plugin_mnet_functions($component); + core_tag_area::reset_definitions_for_component($component); $endcallback($component, true, $verbose); } else if ($installedversion < $plugin->version) { // upgrade @@ -566,6 +568,7 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) { message_update_processors($plug); } upgrade_plugin_mnet_functions($component); + core_tag_area::reset_definitions_for_component($component); $endcallback($component, false, $verbose); } else if ($installedversion > $plugin->version) { @@ -669,6 +672,7 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) { message_update_providers($component); \core\message\inbound\manager::update_handlers_for_component($component); upgrade_plugin_mnet_functions($component); + core_tag_area::reset_definitions_for_component($component); $endcallback($component, true, $verbose); } } @@ -703,6 +707,7 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) { message_update_providers($component); \core\message\inbound\manager::update_handlers_for_component($component); upgrade_plugin_mnet_functions($component); + core_tag_area::reset_definitions_for_component($component); $endcallback($component, true, $verbose); @@ -739,6 +744,7 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) { message_update_providers($component); \core\message\inbound\manager::update_handlers_for_component($component); upgrade_plugin_mnet_functions($component); + core_tag_area::reset_definitions_for_component($component); $endcallback($component, false, $verbose); @@ -860,6 +866,7 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) { message_update_providers($component); \core\message\inbound\manager::update_handlers_for_component($component); upgrade_plugin_mnet_functions($component); + core_tag_area::reset_definitions_for_component($component); $endcallback($component, true, $verbose); } } @@ -899,6 +906,7 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) { \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); \core\message\inbound\manager::update_handlers_for_component($component); + core_tag_area::reset_definitions_for_component($component); upgrade_plugin_mnet_functions($component); $endcallback($component, true, $verbose); @@ -935,6 +943,7 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) { message_update_providers($component); \core\message\inbound\manager::update_handlers_for_component($component); upgrade_plugin_mnet_functions($component); + core_tag_area::reset_definitions_for_component($component); $endcallback($component, false, $verbose); @@ -1536,6 +1545,7 @@ function install_core($version, $verbose) { \core\task\manager::reset_scheduled_tasks_for_component('moodle'); message_update_providers('moodle'); \core\message\inbound\manager::update_handlers_for_component('moodle'); + core_tag_area::reset_definitions_for_component('moodle'); // Write default settings unconditionally admin_apply_default_settings(NULL, true); @@ -1603,6 +1613,7 @@ function upgrade_core($version, $verbose) { \core\task\manager::reset_scheduled_tasks_for_component('moodle'); message_update_providers('moodle'); \core\message\inbound\manager::update_handlers_for_component('moodle'); + core_tag_area::reset_definitions_for_component('moodle'); // Update core definitions. cache_helper::update_definitions(true); diff --git a/mod/wiki/backup/moodle2/backup_wiki_stepslib.php b/mod/wiki/backup/moodle2/backup_wiki_stepslib.php index 91192905142..b836df9e155 100644 --- a/mod/wiki/backup/moodle2/backup_wiki_stepslib.php +++ b/mod/wiki/backup/moodle2/backup_wiki_stepslib.php @@ -104,8 +104,10 @@ class backup_wiki_activity_structure_step extends backup_activity_structure_step FROM {tag} t JOIN {tag_instance} ti ON ti.tagid = t.id WHERE ti.itemtype = ? + AND ti.component = ? AND ti.itemid = ?', array( backup_helper::is_sqlparam('wiki_pages'), + backup_helper::is_sqlparam('mod_wiki'), backup::VAR_PARENTID)); } diff --git a/mod/wiki/backup/moodle2/restore_wiki_stepslib.php b/mod/wiki/backup/moodle2/restore_wiki_stepslib.php index bae98ddfcdb..9f43f23cbf8 100644 --- a/mod/wiki/backup/moodle2/restore_wiki_stepslib.php +++ b/mod/wiki/backup/moodle2/restore_wiki_stepslib.php @@ -159,7 +159,7 @@ class restore_wiki_activity_structure_step extends restore_activity_structure_st $data = (object)$data; $oldid = $data->id; - if (empty($CFG->usetags)) { // tags disabled in server, nothing to process + if (!core_tag_tag::is_enabled('mod_wiki', 'wiki_pages')) { // Tags disabled in server, nothing to process. return; } @@ -167,8 +167,8 @@ class restore_wiki_activity_structure_step extends restore_activity_structure_st $itemid = $this->get_new_parentid('wiki_page'); $wikiid = $this->get_new_parentid('wiki'); - $cm = get_coursemodule_from_instance('wiki', $wikiid); - tag_set_add('wiki_pages', $itemid, $tag, 'mod_wiki', context_module::instance($cm->id)->id); + $context = context_module::instance($this->task->get_moduleid()); + core_tag_tag::add_item_tag('mod_wiki', 'wiki_pages', $itemid, $context, $tag); } protected function after_execute() { diff --git a/mod/wiki/db/tag.php b/mod/wiki/db/tag.php new file mode 100644 index 00000000000..11a44c6f37c --- /dev/null +++ b/mod/wiki/db/tag.php @@ -0,0 +1,33 @@ +. + +/** + * Tag areas in component mod_wiki + * + * @package mod_wiki + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +$tagareas = array( + array( + 'itemtype' => 'wiki_pages', + 'component' => 'mod_wiki', + ), +); diff --git a/mod/wiki/edit_form.php b/mod/wiki/edit_form.php index 265a172fd55..81e7fcc88fe 100644 --- a/mod/wiki/edit_form.php +++ b/mod/wiki/edit_form.php @@ -100,11 +100,11 @@ class mod_wiki_edit_form extends moodleform { $mform->addElement('hidden', 'contentformat', $format); $mform->setType('contentformat', PARAM_ALPHANUMEXT); - if (!empty($CFG->usetags)) { + if (core_tag_tag::is_enabled('mod_wiki', 'wiki_pages')) { $mform->addElement('header', 'tagshdr', get_string('tags', 'tag')); - $mform->addElement('tags', 'tags', get_string('tags')); - $mform->setType('tags', PARAM_TEXT); } + $mform->addElement('tags', 'tags', get_string('tags'), + array('itemtype' => 'wiki_pages', 'component' => 'mod_wiki')); $buttongroup = array(); $buttongroup[] = $mform->createElement('submit', 'editoption', get_string('save', 'wiki'), array('id' => 'save')); diff --git a/mod/wiki/lang/en/wiki.php b/mod/wiki/lang/en/wiki.php index 881bcfd4fbb..58fa7e8f945 100644 --- a/mod/wiki/lang/en/wiki.php +++ b/mod/wiki/lang/en/wiki.php @@ -219,6 +219,7 @@ $string['searchterms'] = 'Search terms'; $string['searchwikis'] = 'Search wikis'; $string['special'] = 'Special'; $string['tableofcontents'] = 'Table of contents'; +$string['tagarea_wiki_pages'] = 'Wiki pages'; $string['tagsdeleted'] = 'Wiki tags have been deleted'; $string['tagtitle'] = 'See the "{$a}" tag'; $string['teacherrating'] = 'Teacher rating'; diff --git a/mod/wiki/lib.php b/mod/wiki/lib.php index 997475e64f0..a2def6c17db 100644 --- a/mod/wiki/lib.php +++ b/mod/wiki/lib.php @@ -141,10 +141,15 @@ function wiki_delete_instance($id) { return $result; } +/** + * Implements callback to reset course + * + * @param stdClass $data + * @return boolean|array + */ function wiki_reset_userdata($data) { global $CFG,$DB; require_once($CFG->dirroot . '/mod/wiki/pagelib.php'); - require_once($CFG->dirroot . '/tag/lib.php'); require_once($CFG->dirroot . "/mod/wiki/locallib.php"); $componentstr = get_string('modulenameplural', 'wiki'); @@ -154,10 +159,12 @@ function wiki_reset_userdata($data) { if (!$wikis = $DB->get_records('wiki', array('course' => $data->courseid))) { return false; } - $errors = false; - foreach ($wikis as $wiki) { + if (empty($data->reset_wiki_comments) && empty($data->reset_wiki_tags) && empty($data->reset_wiki_pages)) { + return $status; + } - if (!$cm = get_coursemodule_from_instance('wiki', $wiki->id)) { + foreach ($wikis as $wiki) { + if (!$cm = get_coursemodule_from_instance('wiki', $wiki->id, $data->courseid)) { continue; } $context = context_module::instance($cm->id); @@ -175,14 +182,7 @@ function wiki_reset_userdata($data) { if (empty($data->reset_wiki_pages)) { // Go through each page and delete the tags. foreach ($pages as $page) { - - $tags = tag_get_tags_array('wiki_pages', $page->id); - foreach ($tags as $tagid => $tagname) { - // Delete the related tag_instances related to the wiki page. - $errors = tag_delete_instance('wiki_pages', $page->id, $tagid); - $status[] = array('component' => $componentstr, 'item' => get_string('tagsdeleted', 'wiki'), - 'error' => $errors); - } + core_tag_tag::remove_all_item_tags('mod_wiki', 'wiki_pages', $page->id); } } else { // Otherwise we are removing pages and tags. @@ -196,17 +196,24 @@ function wiki_reset_userdata($data) { // Delete any attached files. $fs = get_file_storage(); $fs->delete_area_files($context->id, 'mod_wiki', 'attachments'); - - $status[] = array('component' => $componentstr, 'item' => get_string('deleteallpages', 'wiki'), - 'error' => $errors); } } + + if (!empty($data->reset_wiki_pages)) { + $status[] = array('component' => $componentstr, 'item' => get_string('deleteallpages', 'wiki'), + 'error' => false); + } + if (!empty($data->reset_wiki_tags)) { + $status[] = array('component' => $componentstr, 'item' => get_string('tagsdeleted', 'wiki'), 'error' => false); + } } // Remove all comments. if (!empty($data->reset_wiki_comments) || !empty($data->reset_wiki_pages)) { $DB->delete_records_select('comments', "contextid = ? AND commentarea='wiki_page'", array($context->id)); - $status[] = array('component' => $componentstr, 'item' => get_string('deleteallcomments'), 'error' => false); + if (!empty($data->reset_wiki_comments)) { + $status[] = array('component' => $componentstr, 'item' => get_string('deleteallcomments'), 'error' => false); + } } } return $status; diff --git a/mod/wiki/locallib.php b/mod/wiki/locallib.php index e2395888b4f..f19d4629b42 100644 --- a/mod/wiki/locallib.php +++ b/mod/wiki/locallib.php @@ -1163,8 +1163,6 @@ function wiki_delete_pages($context, $pageids = null, $subwikiid = null) { return; } - require_once($CFG->dirroot . '/tag/lib.php'); - /// Delete page and all it's relevent data foreach ($pageids as $pageid) { if (is_object($pageid)) { @@ -1178,10 +1176,7 @@ function wiki_delete_pages($context, $pageids = null, $subwikiid = null) { } //Delete page tags - $tags = tag_get_tags_array('wiki_pages', $pageid); - foreach ($tags as $tagid => $tagvalue) { - tag_delete_instance('wiki_pages', $pageid, $tagid); - } + core_tag_tag::remove_all_item_tags('mod_wiki', 'wiki_pages', $pageid); //Delete Synonym wiki_delete_synonym($subwikiid, $pageid); @@ -1386,18 +1381,8 @@ function wiki_print_page_content($page, $context, $subwikiid) { $html = format_text($html, FORMAT_MOODLE, array('overflowdiv'=>true, 'allowid'=>true)); echo $OUTPUT->box($html); - if (!empty($CFG->usetags)) { - $tags = tag_get_tags_array('wiki_pages', $page->id); - echo $OUTPUT->container_start('wiki-tags'); - echo ''.get_string('tags').': '; - $links = array(); - foreach ($tags as $tagid=>$tag) { - $url = new moodle_url('/tag/index.php', array('tag'=>$tag)); - $links[] = html_writer::link($url, $tag, array('title'=>get_string('tagtitle', 'wiki', $tag))); - } - echo join($links, ", "); - echo $OUTPUT->container_end(); - } + echo $OUTPUT->tag_list(core_tag_tag::get_item_tags('mod_wiki', 'wiki_pages', $page->id), + null, 'wiki-tags'); wiki_increment_pageviews($page); } diff --git a/mod/wiki/pagelib.php b/mod/wiki/pagelib.php index e6a840c099f..3c4ca767cb4 100644 --- a/mod/wiki/pagelib.php +++ b/mod/wiki/pagelib.php @@ -34,7 +34,6 @@ */ require_once($CFG->dirroot . '/mod/wiki/edit_form.php'); -require_once($CFG->dirroot . '/tag/lib.php'); /** * Class page_wiki contains the common code between all pages @@ -570,18 +569,9 @@ class page_wiki_edit extends page_wiki { $params['filearea'] = 'attachments'; } + $data->tags = core_tag_tag::get_item_tags_array('mod_wiki', 'wiki_pages', $this->page->id); + $form = new mod_wiki_edit_form($url, $params); - - if ($formdata = $form->get_data()) { - if (!empty($CFG->usetags)) { - $data->tags = $formdata->tags; - } - } else { - if (!empty($CFG->usetags)) { - $data->tags = tag_get_tags_array('wiki_pages', $this->page->id); - } - } - $form->set_data($data); $form->display(); } @@ -2060,9 +2050,7 @@ class page_wiki_save extends page_wiki_edit { } if ($save && $data) { - if (!empty($CFG->usetags)) { - tag_set('wiki_pages', $this->page->id, $data->tags, 'mod_wiki', $this->modcontext->id); - } + core_tag_tag::set_item_tags('mod_wiki', 'wiki_pages', $this->page->id, $this->modcontext, $data->tags); $message = '

    ' . get_string('saving', 'wiki') . '

    '; diff --git a/mod/wiki/styles.css b/mod/wiki/styles.css index d028184ba2f..45153b6e809 100644 --- a/mod/wiki/styles.css +++ b/mod/wiki/styles.css @@ -128,10 +128,6 @@ text-align: right; } -.wiki-tags span { - font-weight: bold; -} - .wiki_modifieduser p { line-height: 35px; } diff --git a/mod/wiki/tests/behat/edit_tags.feature b/mod/wiki/tests/behat/edit_tags.feature index dc957e27552..5899b8c4056 100644 --- a/mod/wiki/tests/behat/edit_tags.feature +++ b/mod/wiki/tests/behat/edit_tags.feature @@ -49,6 +49,7 @@ Feature: Edited wiki pages handle tags correctly And I expand "Site administration" node And I expand "Appearance" node And I follow "Manage tags" + And I follow "Default collection" And I set the field "otagsadd" to "OT1, OT2, OT3" And I press "Add official tags" And I log out diff --git a/mod/wiki/version.php b/mod/wiki/version.php index 38719eee369..d82e96b62f4 100644 --- a/mod/wiki/version.php +++ b/mod/wiki/version.php @@ -33,7 +33,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2015111602; // The current module version (Date: YYYYMMDDXX) +$plugin->version = 2016011100; // The current module version (Date: YYYYMMDDXX) $plugin->requires = 2015111000; // Requires this Moodle version $plugin->component = 'mod_wiki'; // Full name of the plugin (used for diagnostics) $plugin->cron = 0; diff --git a/question/format.php b/question/format.php index 2f3744f5e47..b2ddad210d7 100644 --- a/question/format.php +++ b/question/format.php @@ -425,9 +425,8 @@ class qformat_default { $result = question_bank::get_qtype($question->qtype)->save_question_options($question); - if (!empty($CFG->usetags) && isset($question->tags)) { - require_once($CFG->dirroot . '/tag/lib.php'); - tag_set('question', $question->id, $question->tags, 'core_question', $question->context->id); + if (isset($question->tags)) { + core_tag_tag::set_item_tags('core_question', 'question', $question->context, $question->id, $question->tags); } if (!empty($result->error)) { diff --git a/question/format/xml/format.php b/question/format/xml/format.php index 8bcada5abd7..efd209c6579 100644 --- a/question/format/xml/format.php +++ b/question/format/xml/format.php @@ -383,9 +383,9 @@ class qformat_xml extends qformat_default { public function import_question_tags($qo, $questionxml) { global $CFG; - if (!empty($CFG->usetags) && array_key_exists('tags', $questionxml['#']) + if (core_tag_tag::is_enabled('core_question', 'question') + && array_key_exists('tags', $questionxml['#']) && !empty($questionxml['#']['tags'][0]['#']['tag'])) { - require_once($CFG->dirroot.'/tag/lib.php'); $qo->tags = array(); foreach ($questionxml['#']['tags'][0]['#']['tag'] as $tagdata) { $qo->tags[] = $this->getpath($tagdata, array('#', 'text', 0, '#'), '', true); @@ -1467,16 +1467,13 @@ class qformat_xml extends qformat_default { $expout .= $this->write_hints($question); // Write the question tags. - if (!empty($CFG->usetags)) { - require_once($CFG->dirroot.'/tag/lib.php'); - $tags = tag_get_tags_array('question', $question->id); - if (!empty($tags)) { - $expout .= " \n"; - foreach ($tags as $tag) { - $expout .= " " . $this->writetext($tag, 0, true) . "\n"; - } - $expout .= " \n"; + $tags = core_tag_tag::get_item_tags_array('core_question', 'question', $question->id); + if (!empty($tags)) { + $expout .= " \n"; + foreach ($tags as $tag) { + $expout .= " " . $this->writetext($tag, 0, true) . "\n"; } + $expout .= " \n"; } // Close the question tag. diff --git a/question/format/xml/tests/xmlformat_test.php b/question/format/xml/tests/xmlformat_test.php index e7c180d7083..8ed027164fb 100644 --- a/question/format/xml/tests/xmlformat_test.php +++ b/question/format/xml/tests/xmlformat_test.php @@ -29,7 +29,6 @@ global $CFG; require_once($CFG->libdir . '/questionlib.php'); require_once($CFG->dirroot . '/question/format/xml/format.php'); require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); -require_once($CFG->dirroot . '/tag/lib.php'); /** diff --git a/question/question.php b/question/question.php index 0e75f4dc8bd..c47be702198 100644 --- a/question/question.php +++ b/question/question.php @@ -262,11 +262,9 @@ if ($mform->is_cancelled()) { } } $question = $qtypeobj->save_question($question, $fromform); - if (!empty($CFG->usetags) && isset($fromform->tags)) { - // A wizardpage from multipe pages questiontype like calculated may not - // allow editing the question tags, hence the isset($fromform->tags) test. - require_once($CFG->dirroot.'/tag/lib.php'); - tag_set('question', $question->id, $fromform->tags, 'core_question', $contextid); + if (isset($fromform->tags)) { + core_tag_tag::set_item_tags('core_question', 'question', $question->id, + context::instance_by_id($contextid), $fromform->tags); } // Purge this question from the cache. diff --git a/question/type/edit_question_form.php b/question/type/edit_question_form.php index d0e3d278d93..636c858a39e 100644 --- a/question/type/edit_question_form.php +++ b/question/type/edit_question_form.php @@ -201,10 +201,11 @@ abstract class question_edit_form extends question_wizard_form { // Any questiontype specific fields. $this->definition_inner($mform); - if (!empty($CFG->usetags)) { + if (core_tag_tag::is_enabled('core_question', 'question')) { $mform->addElement('header', 'tagsheader', get_string('tags')); - $mform->addElement('tags', 'tags', get_string('tags')); } + $mform->addElement('tags', 'tags', get_string('tags'), + array('itemtype' => 'question', 'component' => 'core_question')); if (!empty($this->question->id)) { $mform->addElement('header', 'createdmodifiedheader', diff --git a/tag/classes/area.php b/tag/classes/area.php new file mode 100644 index 00000000000..49ed67e2f92 --- /dev/null +++ b/tag/classes/area.php @@ -0,0 +1,450 @@ +. + +/** + * Class core_tag_area for managing tag areas + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class to manage tag areas + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_tag_area { + + /** + * Returns the list of areas indexed by itemtype and component + * + * @param int $tagcollid return only areas in this tag collection + * @param bool $enabledonly return only enabled tag areas + * @return array itemtype=>component=>tagarea object + */ + public static function get_areas($tagcollid = null, $enabledonly = false) { + global $DB; + $cache = cache::make('core', 'tags'); + if (($itemtypes = $cache->get('tag_area')) === false) { + $colls = core_tag_collection::get_collections(); + $defaultcoll = reset($colls); + $itemtypes = array(); + $areas = $DB->get_records('tag_area', array(), 'component,itemtype'); + foreach ($areas as $area) { + if ($colls[$area->tagcollid]->component) { + $area->locked = true; + } + $itemtypes[$area->itemtype][$area->component] = $area; + } + $cache->set('tag_area', $itemtypes); + } + if ($tagcollid || $enabledonly) { + $rv = array(); + foreach ($itemtypes as $itemtype => $it) { + foreach ($it as $component => $v) { + if (($v->tagcollid == $tagcollid || !$tagcollid) && (!$enabledonly || $v->enabled)) { + $rv[$itemtype][$component] = $v; + } + } + } + return $rv; + } + return $itemtypes; + } + + /** + * Retrieves info about one tag area + * + * @param int $tagareaid + * @return stdClass + */ + public static function get_by_id($tagareaid) { + $tagareas = self::get_areas(); + foreach ($tagareas as $itemtype => $it) { + foreach ($it as $component => $v) { + if ($v->id == $tagareaid) { + return $v; + } + } + } + return null; + } + + /** + * Returns the display name for this area + * + * @param string $component + * @param string $itemtype + * @return lang_string + */ + public static function display_name($component, $itemtype) { + $identifier = 'tagarea_' . clean_param($itemtype, PARAM_STRINGID); + if ($component === 'core') { + $component = 'tag'; + } + return new lang_string($identifier, $component); + } + + /** + * Returns whether the tag area is enabled + * + * @param string $component component responsible for tagging + * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc. + * @return bool|null + */ + public static function is_enabled($component, $itemtype) { + global $CFG; + if (empty($CFG->usetags)) { + return false; + } + $itemtypes = self::get_areas(); + if (isset($itemtypes[$itemtype][$component])) { + return $itemtypes[$itemtype][$component]->enabled ? true : false; + } + return null; + } + + /** + * Returns the id of the tag collection that should be used for storing tags of this itemtype + * + * @param string $component component responsible for tagging + * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc. + * @return int + */ + public static function get_collection($component, $itemtype) { + $itemtypes = self::get_areas(); + if (array_key_exists($itemtype, $itemtypes)) { + if (!array_key_exists($component, $itemtypes[$itemtype])) { + $component = key($itemtypes[$itemtype]); + } + return $itemtypes[$itemtype][$component]->tagcollid; + } + return core_tag_collection::get_default(); + } + + /** + * Returns all tag areas and collections that are currently cached in DB for this component + * + * @param string $componentname + * @return array first element is the list of areas and the second list of collections + */ + protected static function get_definitions_for_component($componentname) { + global $DB; + list($a, $b) = core_component::normalize_component($componentname); + $component = $b ? ($a . '_' . $b) : $a; + $sql = 'component = :component'; + $params = array('component' => $component); + if ($component === 'core') { + $sql .= ' OR component LIKE :coreprefix'; + $params['coreprefix'] = 'core_%'; + } + $fields = $DB->sql_concat_join("':'", array('itemtype', 'component')); + $existingareas = $DB->get_records_sql( + "SELECT $fields AS returnkey, a.* FROM {tag_area} a WHERE $sql", $params); + $fields = $DB->sql_concat_join("':'", array('name', 'component')); + $existingcolls = $DB->get_records_sql( + "SELECT $fields AS returnkey, t.* FROM {tag_coll} t WHERE $sql", $params); + return array($existingareas, $existingcolls); + + } + + /** + * Completely delete a tag area and all instances inside it + * + * @param stdClass $record + */ + protected static function delete($record) { + global $DB; + + core_tag_tag::delete_instances($record->component, $record->itemtype); + + $DB->delete_records('tag_area', + array('itemtype' => $record->itemtype, + 'component' => $record->component)); + + // Reset cache. + cache::make('core', 'tags')->delete('tag_area'); + } + + /** + * Create a new tag area + * + * @param stdClass $record + */ + protected static function create($record) { + global $DB; + if (empty($record->tagcollid)) { + $record->tagcollid = core_tag_collection::get_default(); + } + $DB->insert_record('tag_area', array('component' => $record->component, + 'itemtype' => $record->itemtype, + 'tagcollid' => $record->tagcollid, + 'callback' => $record->callback, + 'callbackfile' => $record->callbackfile)); + + // Reset cache. + cache::make('core', 'tags')->delete('tag_area'); + } + + /** + * Update the tag area + * + * @param stdClass $existing current record from DB table tag_area + * @param array|stdClass $data fields that need updating + */ + public static function update($existing, $data) { + global $DB; + $data = array_intersect_key((array)$data, + array('enabled' => 1, 'tagcollid' => 1, + 'callback' => 1, 'callbackfile' => 1)); + foreach ($data as $key => $value) { + if ($existing->$key == $value) { + unset($data[$key]); + } + } + if (!$data) { + return; + } + + if (!empty($data['tagcollid'])) { + self::move_tags($existing->component, $existing->itemtype, $data['tagcollid']); + } + + $data['id'] = $existing->id; + $DB->update_record('tag_area', $data); + + // Reset cache. + cache::make('core', 'tags')->delete('tag_area'); + } + + /** + * Update the database to contain a list of tagged areas for a component. + * The list of tagged areas is read from [plugindir]/db/tag.php + * + * @param string $componentname - The frankenstyle component name. + */ + public static function reset_definitions_for_component($componentname) { + global $DB; + $dir = core_component::get_component_directory($componentname); + $file = $dir . '/db/tag.php'; + $tagareas = null; + if (file_exists($file)) { + require_once($file); + } + + list($a, $b) = core_component::normalize_component($componentname); + $component = $b ? ($a . '_' . $b) : $a; + + list($existingareas, $existingcolls) = self::get_definitions_for_component($componentname); + + $itemtypes = array(); + $collections = array(); + $needcleanup = false; + if ($tagareas) { + foreach ($tagareas as $tagarea) { + $record = (object)$tagarea; + if ($component !== 'core' || empty($record->component)) { + if (isset($record->component) && $record->component !== $component) { + debugging("Item type {$record->itemtype} has illegal component {$record->component}", DEBUG_DEVELOPER); + } + $record->component = $component; + } + unset($record->tagcollid); + if (!empty($record->collection)) { + // Create collection if it does not exist, or update 'searchable' and/or 'customurl' if needed. + $key = $record->collection . ':' . $record->component; + $collectiondata = array_intersect_key((array)$record, + array('component' => 1, 'searchable' => 1, 'customurl' => 1)); + $collectiondata['name'] = $record->collection; + if (!array_key_exists($key, $existingcolls)) { + $existingcolls[$key] = core_tag_collection::create($collectiondata); + } else { + core_tag_collection::update($existingcolls[$key], $collectiondata); + } + $record->tagcollid = $existingcolls[$key]->id; + $collections[$key] = $existingcolls[$key]; + unset($record->collection); + } + unset($record->searchable); + unset($record->customurl); + if (!isset($record->callback)) { + $record->callback = null; + } + if (!isset($record->callbackfile)) { + $record->callbackfile = null; + } + $itemtypes[$record->itemtype . ':' . $record->component] = $record; + } + } + $todeletearea = array_diff_key($existingareas, $itemtypes); + $todeletecoll = array_diff_key($existingcolls, $collections); + + // Delete tag areas that are no longer needed. + foreach ($todeletearea as $key => $record) { + self::delete($record); + } + + // Update tag areas if changed. + $toupdatearea = array_intersect_key($existingareas, $itemtypes); + foreach ($toupdatearea as $key => $tagarea) { + if (!isset($itemtypes[$key]->tagcollid)) { + foreach ($todeletecoll as $tagcoll) { + if ($tagcoll->id == $tagarea->tagcollid) { + $itemtypes[$key]->tagcollid = core_tag_collection::get_default(); + } + } + } + self::update($tagarea, $itemtypes[$key]); + } + + // Create new tag areas. + $toaddarea = array_diff_key($itemtypes, $existingareas); + foreach ($toaddarea as $record) { + self::create($record); + } + + // Delete tag collections that are no longer needed. + foreach ($todeletecoll as $key => $tagcoll) { + core_tag_collection::delete($tagcoll); + } + } + + /** + * Deletes all tag areas, collections and instances associated with the plugin. + * + * @param string $pluginname + */ + public static function uninstall($pluginname) { + global $DB; + + list($a, $b) = core_component::normalize_component($pluginname); + if (empty($b) || $a === 'core') { + throw new coding_exception('Core component can not be uninstalled'); + } + $component = $a . '_' . $b; + + core_tag_tag::delete_instances($component); + + $DB->delete_records('tag_area', array('component' => $component)); + $DB->delete_records('tag_coll', array('component' => $component)); + cache::make('core', 'tags')->delete_many(array('tag_area', 'tag_coll')); + } + + /** + * Moves existing tags associated with an item type to another tag collection + * + * @param string $component + * @param string $itemtype + * @param int $tagcollid + */ + public static function move_tags($component, $itemtype, $tagcollid) { + global $DB; + $params = array('itemtype1' => $itemtype, 'component1' => $component, + 'itemtype2' => $itemtype, 'component2' => $component, + 'tagcollid1' => $tagcollid, 'tagcollid2' => $tagcollid); + + // Find all collections that need to be cleaned later. + $sql = "SELECT DISTINCT t.tagcollid " . + "FROM {tag_instance} ti " . + "JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 " . + "WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 "; + $cleanupcollections = $DB->get_fieldset_sql($sql, $params); + + // Find all tags that are related to the tags being moved and make sure they are present in the target tagcoll. + $sql = "SELECT DISTINCT r.name, r.rawname, r.description, r.descriptionformat, ". + " r.userid, r.tagtype, r.flag ". + "FROM {tag_instance} ti ". // Instances that need moving. + "JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 ". // Tags that need moving. + "JOIN {tag_instance} tr ON tr.itemtype = 'tag' and tr.component = 'core' AND tr.itemid = t.id ". + "JOIN {tag} r ON r.id = tr.tagid ". // Tags related to the tags that need moving. + "LEFT JOIN {tag} re ON re.name = r.name AND re.tagcollid = :tagcollid2 ". // Existing tags in the target tagcoll with the same name as related tags. + "WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 ". + " AND re.id IS NULL"; // We need related tags that ARE NOT present in the target tagcoll. + $result = $DB->get_records_sql($sql, $params); + foreach ($result as $tag) { + $tag->tagcollid = $tagcollid; + $tag->id = $DB->insert_record('tag', $tag); + \core\event\tag_created::create_from_tag($tag); + } + + // Find all tags that need moving and have related tags, remember their related tags. + $sql = "SELECT t.name AS tagname, r.rawname AS relatedtag ". + "FROM {tag_instance} ti ". // Instances that need moving. + "JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 ". // Tags that need moving. + "JOIN {tag_instance} tr ON t.id = tr.tagid AND tr.itemtype = 'tag' and tr.component = 'core' ". + "JOIN {tag} r ON r.id = tr.itemid ". // Tags related to the tags that need moving. + "WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 ". + "ORDER BY t.id, tr.ordering "; + $relatedtags = array(); + $result = $DB->get_recordset_sql($sql, $params); + foreach ($result as $record) { + $relatedtags[$record->tagname][] = $record->relatedtag; + } + $result->close(); + + // Find all tags that are used for this itemtype/component and are not present in the target tag collection. + $sql = "SELECT DISTINCT t.id, t.name, t.rawname, t.description, t.descriptionformat, + t.userid, t.tagtype, t.flag + FROM {tag_instance} ti + JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 + LEFT JOIN {tag} tt ON tt.name = t.name AND tt.tagcollid = :tagcollid2 + WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 + AND tt.id IS NULL"; + $todelete = array(); + $result = $DB->get_records_sql($sql, $params); + foreach ($result as $tag) { + $originaltagid = $tag->id; + unset($tag->id); + $tag->tagcollid = $tagcollid; + $tag->id = $DB->insert_record('tag', $tag); + \core\event\tag_created::create_from_tag($tag); + $DB->execute("UPDATE {tag_instance} SET tagid = ? WHERE tagid = ? AND itemtype = ? AND component = ?", + array($tag->id, $originaltagid, $itemtype, $component)); + } + + // Find all tags that are used for this itemtype/component and are already present in the target tag collection. + $sql = "SELECT DISTINCT t.id, tt.id AS targettagid + FROM {tag_instance} ti + JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 + JOIN {tag} tt ON tt.name = t.name AND tt.tagcollid = :tagcollid2 + WHERE ti.itemtype = :itemtype2 AND ti.component = :component2"; + $result = $DB->get_records_sql($sql, $params); + foreach ($result as $tag) { + $DB->execute("UPDATE {tag_instance} SET tagid = ? WHERE tagid = ? AND itemtype = ? AND component = ?", + array($tag->targettagid, $tag->id, $itemtype, $component)); + } + + // Add related tags to the moved tags. + if ($relatedtags) { + $tags = core_tag_tag::get_by_name_bulk($tagcollid, array_keys($relatedtags)); + foreach ($tags as $tag) { + $tag->add_related_tags($relatedtags[$tag->name]); + } + } + + if ($cleanupcollections) { + core_tag_collection::cleanup_unused_tags($cleanupcollections); + } + + // Reset caches. + cache::make('core', 'tags')->delete('tag_area'); + } +} diff --git a/tag/classes/areas_table.php b/tag/classes/areas_table.php new file mode 100644 index 00000000000..495f011d7ad --- /dev/null +++ b/tag/classes/areas_table.php @@ -0,0 +1,95 @@ +. + +/** + * Contains class core_tag_areas_table + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Table with the list of available tag areas for "Manage tags" page. + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_tag_areas_table extends html_table { + + /** + * Constructor + * + * @param string|moodle_url $pageurl + */ + public function __construct($pageurl) { + global $OUTPUT; + parent::__construct(); + + $this->attributes['class'] = 'generaltable tag-areas-table'; + + $this->head = array( + get_string('tagareaname', 'core_tag'), + get_string('component', 'tag'), + get_string('tagareaenabled', 'core_tag'), + get_string('tagcollection', 'tag'), + ); + + $this->data = array(); + $this->rowclasses = array(); + + $tagareas = core_tag_area::get_areas(); + $tagcollections = core_tag_collection::get_collections_menu(true); + $tagcollectionsall = core_tag_collection::get_collections_menu(); + + foreach ($tagareas as $itemtype => $it) { + foreach ($it as $component => $record) { + $areaname = core_tag_area::display_name($record->component, $record->itemtype); + $baseurl = new moodle_url($pageurl, array('ta' => $record->id, 'sesskey' => sesskey())); + if ($record->enabled) { + $enableurl = new moodle_url($baseurl, array('action' => 'areadisable')); + $enabled = html_writer::link($enableurl, $OUTPUT->pix_icon('i/hide', get_string('disable'))); + } else { + $enableurl = new moodle_url($baseurl, array('action' => 'areaenable')); + $enabled = html_writer::link($enableurl, $OUTPUT->pix_icon('i/show', get_string('enable'))); + } + + if ($record->enabled && empty($record->locked) && count($tagcollections) > 1) { + $changecollurl = new moodle_url($baseurl, array('action' => 'areasetcoll')); + + $select = new single_select($changecollurl, 'areacollid', $tagcollections, $record->tagcollid, null); + $select->set_label(get_string('changetagcoll', 'core_tag', $areaname), array('class' => 'accesshide')); + $collectionselect = $OUTPUT->render($select); + } else { + $collectionselect = $tagcollectionsall[$record->tagcollid]; + } + $this->data[] = array( + $areaname, + ($record->component === 'core' || preg_match('/^core_/', $record->component)) ? + get_string('coresystem') : get_string('pluginname', $record->component), + $enabled, + $collectionselect + ); + $this->rowclasses[] = $record->enabled ? '' : 'dimmed_text'; + } + } + + } + +} diff --git a/tag/classes/collection.php b/tag/classes/collection.php new file mode 100644 index 00000000000..c8be5d27501 --- /dev/null +++ b/tag/classes/collection.php @@ -0,0 +1,419 @@ +. + +/** + * Class to manage tag collections + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class to manage tag collections + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_tag_collection { + + /** @var string used for function cloud_sort() */ + public static $cloudsortfield = 'name'; + + /** + * Returns the list of tag collections defined in the system. + * + * @param bool $onlysearchable only return collections that can be searched. + * @return array array of objects where each object has properties: id, name, isdefault, itemtypes, sortorder + */ + public static function get_collections($onlysearchable = false) { + global $DB; + $cache = cache::make('core', 'tags'); + if (($tagcolls = $cache->get('tag_coll')) === false) { + // Retrieve records from DB and create a default one if it is not present. + $tagcolls = $DB->get_records('tag_coll', null, 'isdefault DESC, sortorder, id'); + if (empty($tagcolls)) { + // When this method is called for the first time it automatically creates the default tag collection. + $DB->insert_record('tag_coll', array('isdefault' => 1, 'sortorder' => 0)); + $tagcolls = $DB->get_records('tag_coll'); + } else { + // Make sure sortorder is correct. + $idx = 0; + foreach ($tagcolls as $id => $tagcoll) { + if ($tagcoll->sortorder != $idx) { + $DB->update_record('tag_coll', array('sortorder' => $idx, 'id' => $id)); + $tagcolls[$id]->sortorder = $idx; + } + $idx++; + } + } + $cache->set('tag_coll', $tagcolls); + } + if ($onlysearchable) { + $rv = array(); + foreach ($tagcolls as $id => $tagcoll) { + if ($tagcoll->searchable) { + $rv[$id] = $tagcoll; + } + } + return $rv; + } + return $tagcolls; + } + + /** + * Returns the tag collection object + * + * @param int $tagcollid + * @return stdClass + */ + public static function get_by_id($tagcollid) { + $tagcolls = self::get_collections(); + if (array_key_exists($tagcollid, $tagcolls)) { + return $tagcolls[$tagcollid]; + } + return null; + } + + /** + * Returns the list of existing tag collections as id=>name + * + * @param bool $unlockedonly + * @param bool $onlysearchable + * @param string $selectalllabel + * @return array + */ + public static function get_collections_menu($unlockedonly = false, $onlysearchable = false, + $selectalllabel = null) { + $tagcolls = self::get_collections($onlysearchable); + $options = array(); + foreach ($tagcolls as $id => $tagcoll) { + if (!$unlockedonly || empty($tagcoll->component)) { + $options[$id] = self::display_name($tagcoll); + } + } + if (count($options) > 1 && $selectalllabel) { + $options = array(0 => $selectalllabel) + $options; + } + return $options; + } + + /** + * Returns id of the default tag collection + * + * @return int + */ + public static function get_default() { + $collections = self::get_collections(); + $keys = array_keys($collections); + return $keys[0]; + } + + /** + * Returns formatted name of the tag collection + * + * @param stdClass $record record from DB table tag_coll + * @return string + */ + public static function display_name($record) { + $syscontext = context_system::instance(); + if (!empty($record->component)) { + $identifier = 'tagcollection_' . + clean_param($record->name, PARAM_STRINGID); + $component = $record->component; + if ($component === 'core') { + $component = 'tag'; + } + return get_string($identifier, $component); + } + if (!empty($record->name)) { + return format_string($record->name, true, $syscontext); + } else if ($record->isdefault) { + return get_string('defautltagcoll', 'tag'); + } else { + return $record->id; + } + } + + /** + * Returns all tag areas in the given tag collection + * + * @param int $tagcollid + * @return array + */ + public static function get_areas($tagcollid) { + $allitemtypes = core_tag_area::get_areas($tagcollid, true); + $itemtypes = array(); + foreach ($allitemtypes as $itemtype => $it) { + foreach ($it as $component => $v) { + $itemtypes[$v->id] = $v; + } + } + return $itemtypes; + } + + /** + * Returns the list of names of areas (enabled only) that are in this collection. + * + * @param int $tagcollid + * @return array + */ + public static function get_areas_names($tagcollid) { + $allitemtypes = core_tag_area::get_areas($tagcollid, true); + $itemtypes = array(); + foreach ($allitemtypes as $itemtype => $it) { + foreach ($it as $component => $v) { + $itemtypes[] = core_tag_area::display_name($component, $itemtype); + } + } + return $itemtypes; + } + + /** + * Creates a new tag collection + * + * @param stdClass $data data from form core_tag_collection_form + * @return int|false id of created tag collection or false if failed + */ + public static function create($data) { + global $DB; + $data = (object)$data; + $tagcolls = self::get_collections(); + $tagcoll = (object)array( + 'name' => $data->name, + 'isdefault' => 0, + 'component' => !empty($data->component) ? $data->component : null, + 'sortorder' => count($tagcolls), + 'searchable' => isset($data->searchable) ? (int)(bool)$data->searchable : 1, + 'customurl' => !empty($data->customurl) ? $data->customurl : null, + ); + $tagcoll->id = $DB->insert_record('tag_coll', $tagcoll); + + // Reset cache. + cache::make('core', 'tags')->delete('tag_coll'); + + \core\event\tag_collection_created::create_from_record($tagcoll)->trigger(); + return $tagcoll; + } + + /** + * Updates the tag collection information + * + * @param stdClass $tagcoll existing record in DB table tag_coll + * @param stdClass $data data from form core_tag_collection_form + * @return bool wether the record was updated + */ + public static function update($tagcoll, $data) { + global $DB; + $defaulttagcollid = self::get_default(); + $allowedfields = array('name', 'searchable', 'customurl'); + if ($tagcoll->id == $defaulttagcollid) { + $allowedfields = array('name'); + } + + $updatedata = array(); + $data = (array)$data; + foreach ($allowedfields as $key) { + if (array_key_exists($key, $data) && $data[$key] !== $tagcoll->$key) { + $updatedata[$key] = $data[$key]; + } + } + + if (!$updatedata) { + // Nothing to update. + return false; + } + + if (isset($updatedata['searchable'])) { + $updatedata['searchable'] = (int)(bool)$updatedata['searchable']; + } + foreach ($updatedata as $key => $value) { + $tagcoll->$key = $value; + } + $updatedata['id'] = $tagcoll->id; + $DB->update_record('tag_coll', $updatedata); + + // Reset cache. + cache::make('core', 'tags')->delete('tag_coll'); + + \core\event\tag_collection_updated::create_from_record($tagcoll)->trigger(); + + return true; + } + + /** + * Deletes a custom tag collection + * + * @param stdClass $tagcoll existing record in DB table tag_coll + * @return bool wether the tag collection was deleted + */ + public static function delete($tagcoll) { + global $DB, $CFG; + + $defaulttagcollid = self::get_default(); + if ($tagcoll->id == $defaulttagcollid) { + return false; + } + + // Move all tags from this tag collection to the default one. + $allitemtypes = core_tag_area::get_areas($tagcoll->id); + foreach ($allitemtypes as $it) { + foreach ($it as $v) { + core_tag_area::update($v, array('tagcollid' => $defaulttagcollid)); + } + } + + // Delete tags from this tag_coll. + core_tag_tag::delete_tags($DB->get_fieldset_select('tag', 'id', 'tagcollid = ?', array($tagcoll->id))); + + // Delete the tag collection. + $DB->delete_records('tag_coll', array('id' => $tagcoll->id)); + + // Reset cache. + cache::make('core', 'tags')->delete('tag_coll'); + + \core\event\tag_collection_deleted::create_from_record($tagcoll)->trigger(); + + return true; + } + + /** + * Moves the tag collection in the list one position up or down + * + * @param stdClass $tagcoll existing record in DB table tag_coll + * @param int $direction move direction: +1 or -1 + * @return bool + */ + public static function change_sortorder($tagcoll, $direction) { + global $DB; + if ($direction != -1 && $direction != 1) { + throw coding_exception('Second argument in tag_coll_change_sortorder() can be only 1 or -1'); + } + $tagcolls = self::get_collections(); + $keys = array_keys($tagcolls); + $idx = array_search($tagcoll->id, $keys); + if ($idx === false || $idx == 0 || $idx + $direction < 1 || $idx + $direction >= count($tagcolls)) { + return false; + } + $otherid = $keys[$idx + $direction]; + $DB->update_record('tag_coll', array('id' => $tagcoll->id, 'sortorder' => $idx + $direction)); + $DB->update_record('tag_coll', array('id' => $otherid, 'sortorder' => $idx)); + // Reset cache. + cache::make('core', 'tags')->delete('tag_coll'); + return true; + } + + /** + * Permanently deletes all non-official tags that no longer have any instances pointing to them + * + * @param array $collections optional list of tag collections ids to cleanup + */ + public static function cleanup_unused_tags($collections = null) { + global $DB, $CFG; + + $params = array(); + $sql = "SELECT tg.id FROM {tag} tg LEFT OUTER JOIN {tag_instance} ti ON ti.tagid = tg.id + WHERE ti.id IS NULL AND tg.tagtype = 'default'"; + if ($collections) { + list($sqlcoll, $params) = $DB->get_in_or_equal($collections); + $sql .= " AND tg.tagcollid " . $sqlcoll; + } + if ($unusedtags = $DB->get_fieldset_sql($sql, $params)) { + core_tag_tag::delete_tags($unusedtags); + } + } + + /** + * Returns the list of tags with number of items tagged + * + * @param int $tagcollid + * @param string $tagtype possible values 'official', 'default' or empty for any tag type + * @param int $limit maximum number of tags to retrieve, tags are sorted by the instance count + * descending here regardless of $sort parameter + * @param string $sort sort order for display, default 'name' - tags will be sorted after they are retrieved + * @param string $search search string + * @param int $fromctx context id where this tag cloud is displayed + * @param int $ctx only retrieve tag instances in this context + * @param int $rec retrieve tag instances in the $ctx context and it's children (default 1) + * @return \core_tag\output\tagcloud + */ + public static function get_tag_cloud($tagcollid, $tagtype = '', $limit = 150, $sort = 'name', + $search = '', $fromctx = 0, $ctx = 0, $rec = 1) { + global $DB; + + $fromclause = 'FROM {tag_instance} ti JOIN {tag} tg ON tg.id = ti.tagid'; + $whereclause = 'WHERE ti.itemtype <> \'tag\''; + list($sql, $params) = $DB->get_in_or_equal($tagcollid ? array($tagcollid) : + array_keys(self::get_collections(true))); + $whereclause .= ' AND tg.tagcollid ' . $sql; + if (!empty($tagtype)) { + $whereclause .= ' AND tg.tagtype = ?'; + $params[] = $tagtype; + } + $context = $ctx ? context::instance_by_id($ctx) : context_system::instance(); + if ($rec && $context->contextlevel != CONTEXT_SYSTEM) { + $fromclause .= ' JOIN {context} ctx ON ctx.id = ti.contextid '; + $whereclause .= ' AND ctx.path LIKE ?'; + $params[] = $context->path . '%'; + } else if (!$rec) { + $whereclause .= ' AND ti.contextid = ?'; + $params[] = $context->id; + } + if (strval($search) !== '') { + $whereclause .= ' AND tg.name LIKE ?'; + $params[] = '%' . core_text::strtolower($search) . '%'; + } + $tagsincloud = $DB->get_records_sql( + "SELECT tg.id, tg.rawname, tg.name, tg.tagtype, COUNT(ti.id) AS count, tg.flag, tg.tagcollid + $fromclause + $whereclause + GROUP BY tg.id, tg.rawname, tg.name, tg.flag, tg.tagtype, tg.tagcollid + ORDER BY count DESC, tg.name ASC", + $params, 0, $limit); + + $tagscount = count($tagsincloud); + if ($tagscount == $limit) { + $tagscount = $DB->get_field_sql("SELECT COUNT(DISTINCT tg.id) $fromclause $whereclause", $params); + } + + self::$cloudsortfield = $sort; + usort($tagsincloud, "self::cloud_sort"); + + return new core_tag\output\tagcloud($tagsincloud, $tagscount, $fromctx, $ctx, $rec); + } + + /** + * This function is used to sort the tags in the cloud. + * + * @param string $a Tag name to compare against $b + * @param string $b Tag name to compare against $a + * @return int The result of the comparison/validation 1, 0 or -1 + */ + public static function cloud_sort($a, $b) { + $tagsort = self::$cloudsortfield ?: 'name'; + + if (is_numeric($a->$tagsort)) { + return ($a->$tagsort == $b->$tagsort) ? 0 : ($a->$tagsort > $b->$tagsort) ? 1 : -1; + } else if (is_string($a->$tagsort)) { + return strcmp($a->$tagsort, $b->$tagsort); + } else { + return 0; + } + } +} \ No newline at end of file diff --git a/tag/classes/collection_form.php b/tag/classes/collection_form.php new file mode 100644 index 00000000000..04a95b75aeb --- /dev/null +++ b/tag/classes/collection_form.php @@ -0,0 +1,79 @@ +. + +/** + * Contains class core_tag_collection_form + * + * @package core + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); + +/** + * Form for editing tag collection + * + * @package core + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_tag_collection_form extends moodleform { + + /** + * Form definition + */ + public function definition() { + $data = fullclone($this->_customdata); + if (isset($data->id)) { + $data->tc = $data->id; + $data->action = 'colledit'; + } else { + $data = new stdClass(); + $data->action = 'colladd'; + $data->isdefault = false; + } + + $mform = $this->_form; + $mform->addElement('hidden', 'tc'); + $mform->setType('tc', PARAM_INT); + $mform->addElement('hidden', 'action'); + $mform->setType('action', PARAM_ALPHA); + + $mform->addElement('text', 'name', get_string('name')); + $mform->setType('name', PARAM_NOTAGS); + $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + if (empty($data->isdefault)) { + $mform->addRule('name', get_string('required'), 'required', null, 'client'); + } else { + $mform->addElement('static', 'collnameexplained', '', get_string('collnameexplained', 'tag', + get_string('defautltagcoll', 'tag'))); + } + + $mform->addElement('advcheckbox', 'searchable', get_string('searchable', 'tag')); + $mform->addHelpButton('searchable', 'searchable', 'tag'); + $mform->setDefault('searchable', 1); + if (!empty($data->isdefault)) { + $mform->freeze('searchable'); + } + + $this->add_action_buttons(); + + $this->set_data($data); + } +} diff --git a/tag/classes/collections_table.php b/tag/classes/collections_table.php new file mode 100644 index 00000000000..28ae760e0c7 --- /dev/null +++ b/tag/classes/collections_table.php @@ -0,0 +1,100 @@ +. + +/** + * Contains class core_tag_collections_table + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Table with the list of tag collections for "Manage tags" page. + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_tag_collections_table extends html_table { + + /** + * Constructor + * @param string|moodle_url $pageurl + */ + public function __construct($pageurl) { + global $OUTPUT; + parent::__construct(); + + $this->attributes['class'] = 'generaltable tag-collections-table'; + + $this->head = array( + get_string('name'), + get_string('component', 'tag'), + get_string('tagareas', 'tag'), + get_string('searchable', 'tag') . $OUTPUT->help_icon('searchable', 'tag'), + '' + ); + + $this->data = array(); + + $tagcolls = core_tag_collection::get_collections(); + $idx = 0; + foreach ($tagcolls as $tagcoll) { + $actions = ''; + $name = core_tag_collection::display_name($tagcoll); + $url = new moodle_url($pageurl, array('sesskey' => sesskey(), 'tc' => $tagcoll->id)); + if (!$tagcoll->isdefault) { + // Move up. + if ($idx > 1) { + $url->param('action', 'collmoveup'); + $actions .= $OUTPUT->action_icon($url, new pix_icon('t/up', get_string('moveup'))); + } + // Move down. + if ($idx < count($tagcolls) - 1) { + $url->param('action', 'collmovedown'); + $actions .= $OUTPUT->action_icon($url, new pix_icon('t/down', get_string('movedown'))); + } + } + if (empty($tagcoll->component)) { + // Edit. + $url->param('action', 'colledit'); + $actions .= $OUTPUT->action_icon($url, new pix_icon('t/edit', get_string('edittagcoll', 'tag', $name))); + } + if (!$tagcoll->isdefault && empty($tagcoll->component)) { + // Delete. + $url->param('action', 'colldelete'); + $actions .= $OUTPUT->action_icon($url, new pix_icon('t/delete', get_string('delete'))); + } + $manageurl = new moodle_url('/tag/manage.php', array('tc' => $tagcoll->id)); + $component = ''; + if ($tagcoll->component) { + $component = ($tagcoll->component === 'core' || preg_match('/^core_/', $tagcoll->component)) ? + get_string('coresystem') : get_string('pluginname', $tagcoll->component); + } + $this->data[] = array( + html_writer::link($manageurl, $name), + $component, + join(', ', core_tag_collection::get_areas_names($tagcoll->id)), + $tagcoll->searchable ? get_string('yes') : '-', + $actions); + $idx++; + } + + } +} \ No newline at end of file diff --git a/tag/classes/external.php b/tag/classes/external.php index 2bb564f335b..324ecad963c 100644 --- a/tag/classes/external.php +++ b/tag/classes/external.php @@ -66,7 +66,6 @@ class core_tag_external extends external_api { */ public static function update_tags($tags) { global $CFG, $PAGE, $DB; - require_once($CFG->dirroot.'/tag/lib.php'); // Validate and normalize parameters. $tags = self::validate_parameters(self::update_tags_parameters(), array('tags' => $tags)); @@ -87,8 +86,6 @@ class core_tag_external extends external_api { $tag['rawname'] = clean_param($tag['rawname'], PARAM_TAG); if (empty($tag['rawname'])) { unset($tag['rawname']); - } else { - $tag['name'] = core_text::strtolower($tag['rawname']); } } if (!$canmanage) { @@ -109,7 +106,7 @@ class core_tag_external extends external_api { ); continue; } - if (!$tagobject = $DB->get_record('tag', array('id' => $tag['id']))) { + if (!$tagobject = core_tag_tag::get($tag['id'], '*')) { $warnings[] = array( 'item' => $tag['id'], 'warningcode' => 'tagnotfound', @@ -118,7 +115,7 @@ class core_tag_external extends external_api { continue; } // First check if new tag name is allowed. - if (!empty($tag['name']) && ($existing = $DB->get_record('tag', array('name' => $tag['name']), 'id'))) { + if (!empty($tag['rawname']) && ($existing = core_tag_tag::get_by_name($tagobject->tagcollid, $tag['rawname']))) { if ($existing->id != $tag['id']) { $warnings[] = array( 'item' => $tag['id'], @@ -132,23 +129,18 @@ class core_tag_external extends external_api { $tag['tagtype'] = $tag['official'] ? 'official' : 'default'; unset($tag['official']); } - $tag['timemodified'] = time(); - $DB->update_record('tag', $tag); - - foreach ($tag as $key => $value) { - $tagobject->$key = $value; + if (isset($tag['flag'])) { + if ($tag['flag']) { + $tagobject->flag(); + } else { + $tagobject->reset_flag(); + } + unset($tag['flag']); + } + unset($tag['id']); + if (count($tag)) { + $tagobject->update($tag); } - - $event = \core\event\tag_updated::create(array( - 'objectid' => $tagobject->id, - 'relateduserid' => $tagobject->userid, - 'context' => context_system::instance(), - 'other' => array( - 'name' => $tagobject->name, - 'rawname' => $tagobject->rawname - ) - )); - $event->trigger(); } return array('warnings' => $warnings); } @@ -192,7 +184,6 @@ class core_tag_external extends external_api { */ public static function get_tags($tags) { global $CFG, $PAGE, $DB; - require_once($CFG->dirroot.'/tag/lib.php'); // Validate and normalize parameters. $tags = self::validate_parameters(self::get_tags_parameters(), array('tags' => $tags)); @@ -248,6 +239,7 @@ class core_tag_external extends external_api { 'tags' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'tag id'), + 'tagcollid' => new external_value(PARAM_INT, 'tag collection id'), 'name' => new external_value(PARAM_TAG, 'name'), 'rawname' => new external_value(PARAM_RAW, 'tag raw name (may contain capital letters)'), 'description' => new external_value(PARAM_RAW, 'tag description'), @@ -263,4 +255,81 @@ class core_tag_external extends external_api { ) ); } + + /** + * Parameters for function get_tagindex() + * + * @return external_function_parameters + */ + public static function get_tagindex_parameters() { + return new external_function_parameters( + array( + 'tagindex' => new external_single_structure(array( + 'tag' => new external_value(PARAM_TAG, 'tag name'), + 'tc' => new external_value(PARAM_INT, 'tag collection id'), + 'ta' => new external_value(PARAM_INT, 'tag area id'), + 'excl' => new external_value(PARAM_BOOL, 'exlusive mode for this tag area', VALUE_OPTIONAL, 0), + 'from' => new external_value(PARAM_INT, 'context id where the link was displayed', VALUE_OPTIONAL, 0), + 'ctx' => new external_value(PARAM_INT, 'context id where to search for items', VALUE_OPTIONAL, 0), + 'rec' => new external_value(PARAM_INT, 'search in the context recursive', VALUE_OPTIONAL, 1), + 'page' => new external_value(PARAM_INT, 'page number (0-based)', VALUE_OPTIONAL, 0), + ), 'parameters') + ) + ); + } + + /** + * Get tags by their ids + * + * @param array $params + */ + public static function get_tagindex($params) { + global $PAGE; + // Validate and normalize parameters. + $tagindex = self::validate_parameters( + self::get_tagindex_parameters(), array('tagindex' => $params)); + $params = $tagindex['tagindex'] + array( + 'excl' => 0, + 'from' => 0, + 'ctx' => 0, + 'rec' => 1, + 'page' => 0 + ); + + // Login to the course / module if applicable. + $context = $params['ctx'] ? context::instance_by_id($params['ctx']) : context_system::instance(); + require_login(null, false, null, false, true); + self::validate_context($context); + + $tag = core_tag_tag::get_by_name($params['tc'], $params['tag'], '*', MUST_EXIST); + $tagareas = core_tag_collection::get_areas($params['tc']); + $tagindex = $tag->get_tag_index($tagareas[$params['ta']], $params['excl'], $params['from'], + $params['ctx'], $params['rec'], $params['page']); + $renderer = $PAGE->get_renderer('core'); + return $tagindex->export_for_template($renderer); + } + + /** + * Return structure for get_tag() + * + * @return external_description + */ + public static function get_tagindex_returns() { + return new external_single_structure( + array( + 'tagid' => new external_value(PARAM_INT, 'tag id'), + 'ta' => new external_value(PARAM_INT, 'tag area id'), + 'component' => new external_value(PARAM_COMPONENT, 'component'), + 'itemtype' => new external_value(PARAM_NOTAGS, 'itemtype'), + 'nextpageurl' => new external_value(PARAM_URL, 'URL for the next page', VALUE_OPTIONAL), + 'prevpageurl' => new external_value(PARAM_URL, 'URL for the next page', VALUE_OPTIONAL), + 'exclusiveurl' => new external_value(PARAM_URL, 'URL for exclusive link', VALUE_OPTIONAL), + 'exclusivetext' => new external_value(PARAM_TEXT, 'text for exclusive link', VALUE_OPTIONAL), + 'title' => new external_value(PARAM_RAW, 'title'), + 'content' => new external_value(PARAM_RAW, 'title'), + 'hascontent' => new external_value(PARAM_INT, 'whether the content is present'), + 'anchor' => new external_value(PARAM_TEXT, 'name of anchor', VALUE_OPTIONAL), + ), 'tag index' + ); + } } diff --git a/tag/classes/manage_table.php b/tag/classes/manage_table.php index 61d3b19375d..48af9026876 100644 --- a/tag/classes/manage_table.php +++ b/tag/classes/manage_table.php @@ -38,16 +38,24 @@ class core_tag_manage_table extends table_sql { /** @var int stores the total number of found tags */ public $totalcount = null; + /** @var int */ + protected $tagcollid; + /** * Constructor + * + * @param int $tagcollid */ - public function __construct() { + public function __construct($tagcollid) { global $USER, $CFG, $PAGE; parent::__construct('tag-management-list-'.$USER->id); + $this->tagcollid = $tagcollid; + $perpage = optional_param('perpage', DEFAULT_PAGE_SIZE, PARAM_INT); $page = optional_param('page', 0, PARAM_INT); - $baseurl = new moodle_url('/tag/manage.php', array('perpage' => $perpage, 'page' => $page)); + $baseurl = new moodle_url('/tag/manage.php', array('tc' => $tagcollid, + 'perpage' => $perpage, 'page' => $page)); $tablecolumns = array('select', 'name', 'fullname', 'count', 'flag', 'timemodified', 'tagtype', 'controls'); $tableheaders = array(get_string('select', 'tag'), @@ -80,8 +88,10 @@ class core_tag_manage_table extends table_sql { $this->set_attribute('id', 'tag-management-list'); $this->set_attribute('class', 'admintable generaltable tag-management-table'); - $totalcount = "SELECT COUNT(id) FROM {tag}"; - $params = array(); + $totalcount = "SELECT COUNT(id) + FROM {tag} + WHERE tagcollid = :tagcollid"; + $params = array('tagcollid' => $this->tagcollid); $this->set_count_sql($totalcount, $params); @@ -135,13 +145,13 @@ class core_tag_manage_table extends table_sql { $sql = " SELECT tg.id, tg.name, tg.rawname, tg.tagtype, tg.flag, tg.timemodified, u.id AS owner, $allusernames, - COUNT(ti.id) AS count + COUNT(ti.id) AS count, tg.tagcollid FROM {tag} tg LEFT JOIN {tag_instance} ti ON ti.tagid = tg.id LEFT JOIN {user} u ON u.id = tg.userid - WHERE 1 = 1 $where + WHERE tagcollid = :tagcollid $where GROUP BY tg.id, tg.name, tg.rawname, tg.tagtype, tg.flag, tg.timemodified, - u.id, $allusernames + u.id, $allusernames, tg.tagcollid ORDER BY $sort"; if (!$this->is_downloading()) { diff --git a/tag/classes/output/tag.php b/tag/classes/output/tag.php index 74aa47d6c8d..dd8559f5723 100644 --- a/tag/classes/output/tag.php +++ b/tag/classes/output/tag.php @@ -39,15 +39,19 @@ use moodle_url; */ class tag implements renderable, templatable { - /** @var stdClass */ + /** @var \core_tag_tag|stdClass */ protected $record; /** * Constructor * - * @param stdClass $tag + * @param \core_tag_tag|stdClass $tag */ public function __construct($tag) { + if ($tag instanceof \core_tag_tag) { + $this->record = $tag; + return; + } $tag = (array)$tag + array( 'name' => '', @@ -56,7 +60,8 @@ class tag implements renderable, templatable { 'descriptionformat' => FORMAT_HTML, 'flag' => 0, 'tagtype' => 'default', - 'id' => 0 + 'id' => 0, + 'tagcollid' => 0, ); $this->record = (object)$tag; } @@ -73,6 +78,7 @@ class tag implements renderable, templatable { $r = new stdClass(); $r->id = (int)$this->record->id; + $r->tagcollid = clean_param($this->record->tagcollid, PARAM_INT); $r->rawname = clean_param($this->record->rawname, PARAM_TAG); $r->name = clean_param($this->record->name, PARAM_TAG); $format = clean_param($this->record->descriptionformat, PARAM_INT); @@ -85,7 +91,7 @@ class tag implements renderable, templatable { $r->official = ($this->record->tagtype === 'official') ? 1 : 0; } - $url = new moodle_url('/tag/index.php', array('id' => $this->record->id)); + $url = \core_tag_tag::make_url($r->tagcollid, $r->rawname); $r->viewurl = $url->out(false); $manageurl = new moodle_url('/tag/manage.php', array('sesskey' => sesskey(), diff --git a/tag/classes/output/tagcloud.php b/tag/classes/output/tagcloud.php new file mode 100644 index 00000000000..85d61ce7350 --- /dev/null +++ b/tag/classes/output/tagcloud.php @@ -0,0 +1,121 @@ +. + +/** + * Contains class core_tag\output\tagindex + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_tag\output; + +use renderable; +use templatable; +use renderer_base; +use stdClass; +use moodle_url; +use core_tag_tag; + +/** + * Class to display a tag cloud - set of tags where each has a weight. + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tagcloud implements templatable { + + /** @var array */ + protected $tagset; + + /** @var int */ + protected $totalcount; + + /** + * Constructor + * + * @param array $tagset array of core_tag or stdClass elements, each of them must have attributes: + * name, rawname, tagcollid + * preferrably also have attributes: + * tagtype, count, flag + * @param int $totalcount total count of tags (for example to indicate that there are more tags than the count of tagset) + * leave 0 if count of tagset is the actual count of tags + * @param int $fromctx context id where this tag cloud is displayed + * @param int $ctx context id for tag view link + * @param int $rec recursive argument for tag view link + */ + public function __construct($tagset, $totalcount = 0, $fromctx = 0, $ctx = 0, $rec = 1) { + $canmanagetags = has_capability('moodle/tag:manage', \context_system::instance()); + + $maxcount = 1; + foreach ($tagset as $tag) { + if (isset($tag->count) && $tag->count > $maxcount) { + $maxcount = $tag->count; + } + } + + $this->tagset = array(); + foreach ($tagset as $idx => $tag) { + $this->tagset[$idx] = new stdClass(); + + $this->tagset[$idx]->name = core_tag_tag::make_display_name($tag, false); + + if ($canmanagetags && !empty($tag->flag)) { + $this->tagset[$idx]->flag = 1; + } + + $viewurl = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, 0, $fromctx, $ctx, $rec); + $this->tagset[$idx]->viewurl = $viewurl->out(false); + + if (!empty($tag->tagtype)) { + $this->tagset[$idx]->tagtype = $tag->tagtype; + } + + if (!empty($tag->count)) { + $this->tagset[$idx]->count = $tag->count; + $this->tagset[$idx]->size = (int)($tag->count / $maxcount * 20); + } + } + + $this->totalcount = $totalcount ? $totalcount : count($this->tagset); + } + + /** + * Returns number of tags in the cloud + * @return int + */ + public function get_count() { + return count($this->tagset); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $cnt = count($this->tagset); + return (object)array( + 'tags' => $this->tagset, + 'tagscount' => $cnt, + 'totalcount' => $this->totalcount, + 'overflow' => ($this->totalcount > $cnt) ? 1 : 0, + ); + } +} diff --git a/tag/classes/output/tagfeed.php b/tag/classes/output/tagfeed.php new file mode 100644 index 00000000000..4c79e405c48 --- /dev/null +++ b/tag/classes/output/tagfeed.php @@ -0,0 +1,82 @@ +. + +/** + * Contains class core_tag\output\tagfeed + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_tag\output; + +use templatable; +use renderer_base; +use stdClass; + +/** + * Class to display feed of tagged items + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tagfeed implements templatable { + + /** @var array */ + protected $items; + + /** + * Constructor + * + * Usually the most convenient way is to call constructor without arguments and + * add items later using add() method. + * + * @param array $items + */ + public function __construct($items = array()) { + $this->items = array(); + if ($items) { + foreach ($items as $item) { + $item = (array)$item + array('img' => '', 'heading' => '', 'details' => ''); + $this->add($item['img'], $item['heading'], $item['details']); + } + } + } + + /** + * Adds one item to the tagfeed + * + * @param string $img HTML code representing image (or image wrapped in a link), note that + * core_tag/tagfeed template expects image to be 35x35 px + * @param string $heading HTML for item heading + * @param string $details HTML for item details (keep short) + */ + public function add($img, $heading, $details = '') { + $this->items[] = array('img' => $img, 'heading' => $heading, 'details' => $details); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + return array('items' => $this->items); + } +} diff --git a/tag/classes/output/tagindex.php b/tag/classes/output/tagindex.php new file mode 100644 index 00000000000..f888d6f2008 --- /dev/null +++ b/tag/classes/output/tagindex.php @@ -0,0 +1,161 @@ +. + +/** + * Contains class core_tag\output\tagindex + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_tag\output; + +use renderable; +use templatable; +use renderer_base; +use stdClass; +use moodle_url; +use core_tag_tag; + +/** + * Class to display items tagged with a specific tag + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tagindex implements templatable { + + /** @var core_tag_tag|stdClass */ + protected $tag; + + /** @var stdClass */ + protected $tagarea; + + /** @var stdClass */ + protected $record; + + /** + * Constructor + * + * @param core_tag_tag|stdClass $tag + * @param string $component + * @param string $itemtype + * @param string $content + * @param bool $exclusivemode + * @param int $fromctx context id where the link was displayed, may be used by callbacks + * to display items in the same context first + * @param int $ctx context id where we need to search for items + * @param int $rec search items in sub contexts as well + * @param int $page + * @param bool $totalpages + */ + public function __construct($tag, $component, $itemtype, $content, + $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0, $totalpages = 1) { + $this->record = new stdClass(); + $this->tag = $tag; + + $tagareas = \core_tag_area::get_areas(); + if (!isset($tagareas[$itemtype][$component])) { + throw new \coding_exception('Tag area for component '.$component.' and itemtype '.$itemtype.' is not defined'); + } + $this->tagarea = $tagareas[$itemtype][$component]; + $this->record->tagid = $tag->id; + $this->record->ta = $this->tagarea->id; + $this->record->itemtype = $itemtype; + $this->record->component = $component; + + $a = (object)array( + 'tagarea' => \core_tag_area::display_name($component, $itemtype), + 'tag' => \core_tag_tag::make_display_name($tag) + ); + if ($exclusivemode) { + $this->record->title = get_string('itemstaggedwith', 'tag', $a); + } else { + $this->record->title = (string)$a->tagarea; + } + $this->record->content = $content; + + $this->record->nextpageurl = null; + $this->record->prevpageurl = null; + $this->record->exclusiveurl = null; + + $url = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, $exclusivemode, $fromctx, $ctx, $rec); + $urlparams = array('ta' => $this->tagarea->id); + if ($totalpages > $page + 1) { + $this->record->nextpageurl = new moodle_url($url, $urlparams + array('page' => $page + 1)); + } + if ($page > 0) { + $this->record->prevpageurl = new moodle_url($url, $urlparams + array('page' => $page - 1)); + } + if (!$exclusivemode && ($totalpages > 1 || $page)) { + $this->record->exclusiveurl = new moodle_url($url, $urlparams + array('excl' => 1)); + } + $this->record->exclusivetext = get_string('exclusivemode', 'tag', $a); + $this->record->hascontent = ($totalpages > 1 || $page || $content); + $this->record->anchor = $component . '_' . $itemtype; + } + + /** + * Magic setter + * + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) { + $this->record->$name = $value; + } + + /** + * Magic getter + * + * @param string $name + * @return mixed + */ + public function __get($name) { + return $this->record->$name; + } + + /** + * Magic isset + * + * @param string $name + * @return bool + */ + public function __isset($name) { + return isset($this->record->$name); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + if ($this->record->nextpageurl && $this->record->nextpageurl instanceof moodle_url) { + $this->record->nextpageurl = $this->record->nextpageurl->out(false); + } + if ($this->record->prevpageurl && $this->record->prevpageurl instanceof moodle_url) { + $this->record->prevpageurl = $this->record->prevpageurl->out(false); + } + if ($this->record->exclusiveurl && $this->record->exclusiveurl instanceof moodle_url) { + $this->record->exclusiveurl = $this->record->exclusiveurl->out(false); + } + return $this->record; + } +} diff --git a/tag/classes/output/taglist.php b/tag/classes/output/taglist.php new file mode 100644 index 00000000000..d0ad198591d --- /dev/null +++ b/tag/classes/output/taglist.php @@ -0,0 +1,114 @@ +. + +/** + * Contains class core_tag\output\taglist + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_tag\output; + +use templatable; +use renderer_base; +use stdClass; +use core_tag_tag; +use context; + +/** + * Class to preapare a list of tags for display, usually the list of tags some entry is tagged with. + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class taglist implements templatable { + + /** @var array */ + protected $tags; + + /** @var string */ + protected $label; + + /** @var string */ + protected $classes; + + /** @var int */ + protected $limit; + + /** + * Constructor + * + * @param array $tags list of instances of \core_tag_tag or \stdClass + * @param string $label label to display in front, by default 'Tags' (get_string('tags')), set to null + * to use default, set to '' (empty string) to omit the label completely + * @param string $classes additional classes for the enclosing div element + * @param int $limit limit the number of tags to display, if size of $tags is more than this limit the "more" link + * will be appended to the end, JS will toggle the rest of the tags + * @param context $pagecontext specify if needed to overwrite the current page context for the view tag link + */ + public function __construct($tags, $label = null, $classes = '', $limit = 10, $pagecontext = null) { + global $PAGE; + $canmanagetags = has_capability('moodle/tag:manage', \context_system::instance()); + + $this->label = ($label === null) ? get_string('tags') : $label; + $this->classes = $classes; + $fromctx = $pagecontext ? $pagecontext->id : + (($PAGE->context->contextlevel == CONTEXT_SYSTEM) ? 0 : $PAGE->context->id); + + $this->tags = array(); + foreach ($tags as $idx => $tag) { + $this->tags[$idx] = new stdClass(); + + $this->tags[$idx]->name = core_tag_tag::make_display_name($tag, false); + + if ($canmanagetags && !empty($tag->flag)) { + $this->tags[$idx]->flag = 1; + } + + $viewurl = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, 0, $fromctx); + $this->tags[$idx]->viewurl = $viewurl->out(false); + + if (!empty($tag->tagtype)) { + $this->tags[$idx]->tagtype = $tag->tagtype; + } + + if ($limit && count($this->tags) > $limit) { + $this->tags[$idx]->overlimit = 1; + } + } + $this->limit = $limit; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $cnt = count($this->tags); + return (object)array( + 'tags' => array_values($this->tags), + 'label' => $this->label, + 'tagscount' => $cnt, + 'overflow' => ($this->limit && $cnt > $this->limit) ? 1 : 0, + 'classes' => $this->classes, + ); + } +} diff --git a/tag/classes/renderer.php b/tag/classes/renderer.php new file mode 100644 index 00000000000..3949c5e81e3 --- /dev/null +++ b/tag/classes/renderer.php @@ -0,0 +1,180 @@ +. + +/** + * Contains class core_tag_renderer + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class core_tag_renderer + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_tag_renderer extends plugin_renderer_base { + + /** + * Renders the tag search page + * + * @param string $query + * @param int $tagcollid + * @return string + */ + public function tag_search_page($query = '', $tagcollid = 0) { + $rv = $this->output->heading(get_string('searchtags', 'tag'), 2); + + $searchbox = $this->search_form($query, $tagcollid); + $rv .= html_writer::div($searchbox, '', array('id' => 'tag-search-box')); + + $tagcloud = core_tag_collection::get_tag_cloud($tagcollid, '', 150, 'name', $query); + $searchresults = ''; + if ($tagcloud->get_count()) { + $searchresults = $this->output->render_from_template('core_tag/tagcloud', + $tagcloud->export_for_template($this->output)); + $rv .= html_writer::div($searchresults, '', array('id' => 'tag-search-results')); + } else if (strval($query) !== '') { + $rv .= '
    ' . get_string('notagsfound', 'tag', s($query)) . '
    '; + } + + return $rv; + } + + /** + * Renders the tag index page + * + * @param core_tag_tag $tag + * @param \core_tag\output\tagindex[] $entities + * @param int $tagareaid + * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag + * are displayed on the page and the per-page limit may be bigger + * @param int $fromctx context id where the link was displayed, may be used by callbacks + * to display items in the same context first + * @param int $ctx context id where to search for records + * @param bool $rec search in subcontexts as well + * @param int $page 0-based number of page being displayed + * @return string + */ + public function tag_index_page($tag, $entities, $tagareaid, $exclusivemode, $fromctx, $ctx, $rec, $page) { + global $CFG, $OUTPUT; + $this->page->requires->js_call_amd('core/tag', 'init_tagindex_page'); + + $tagname = $tag->get_display_name(); + $systemcontext = context_system::instance(); + + if ($tag->flag > 0 && has_capability('moodle/tag:manage', $systemcontext)) { + $tagname = '' . $tagname . ''; + } + + $rv = ''; + $rv .= $this->output->heading($tagname, 2); + + $rv .= $this->tag_links($tag); + + if ($desciption = $tag->get_formatted_description()) { + $rv .= $this->output->box($desciption, 'generalbox tag-description'); + } + + $relatedtagslimit = 10; + $relatedtags = $tag->get_related_tags(); + $taglist = new \core_tag\output\taglist($relatedtags, get_string('relatedtags', 'tag'), + 'tag-relatedtags', $relatedtagslimit); + $rv .= $OUTPUT->render_from_template('core_tag/taglist', $taglist->export_for_template($OUTPUT)); + + // Display quick menu of the item types (if more than one item type found). + $entitylinks = array(); + foreach ($entities as $entity) { + if (!empty($entity->hascontent)) { + $entitylinks[] = '
  • ' . + core_tag_area::display_name($entity->component, $entity->itemtype) . '
  • '; + } + } + + if (count($entitylinks) > 1) { + $rv .= '
      ' . join('', $entitylinks) . '
    '; + } else if (!$entitylinks) { + $rv .= '
    ' . get_string('noresultsfor', 'tag', $tagname) . '
    '; + } + + // Display entities tagged with the tag. + $content = ''; + foreach ($entities as $entity) { + if (!empty($entity->hascontent)) { + $content .= $this->output->render_from_template('core_tag/index', $entity->export_for_template($this->output)); + } + } + + if ($exclusivemode) { + $rv .= $content; + } else if ($content) { + $rv .= html_writer::div($content, 'tag-index-items'); + } + + // Display back link if we are browsing one tag area. + if ($tagareaid) { + $url = $tag->get_view_url(0, $fromctx, $ctx, $rec); + $rv .= '
    ' . + html_writer::link($url, get_string('backtoallitems', 'tag', $tag->get_display_name())) . + '
    '; + } + + return $rv; + } + + /** + * Prints a box that contains the management links of a tag + * + * @param core_tag_tag $tag + * @return string + */ + protected function tag_links($tag) { + if ($links = $tag->get_links()) { + $content = ''; + return html_writer::div($content, 'tag-management-box'); + } + return ''; + } + + /** + * Prints the tag search box + * + * @param string $query last search string + * @param int $tagcollid last selected tag collection id + * @return string + */ + protected function search_form($query = '', $tagcollid = 0) { + $searchurl = new moodle_url('/tag/search.php'); + $output = ''; + $output .= ''; + $output .= ''; + $tagcolls = core_tag_collection::get_collections_menu(false, true, get_string('inalltagcoll', 'tag')); + if (count($tagcolls) > 1) { + $output .= ''; + $output .= html_writer::select($tagcolls, 'tc', $tagcollid, null, array('id' => 'searchform_tc')); + } + $output .= ''; + $output .= ''; + + return $output; + } + +} diff --git a/tag/classes/tag.php b/tag/classes/tag.php new file mode 100644 index 00000000000..a7a62c7112f --- /dev/null +++ b/tag/classes/tag.php @@ -0,0 +1,1414 @@ +. + +/** + * Contains class core_tag_tag + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Represents one tag and also contains lots of useful tag-related methods as static functions. + * + * Tags can be added to any database records. + * $itemtype refers to the DB table name + * $itemid refers to id field in this DB table + * $component is the component that is responsible for the tag instance + * $context is the affected context + * + * BASIC INSTRUCTIONS : + * - to "tag a blog post" (for example): + * core_tag_tag::set_item_tags('post', 'core', $blogpost->id, $context, $arrayoftags); + * + * - to "remove all the tags on a blog post": + * core_tag_tag::remove_all_item_tags('post', 'core', $blogpost->id); + * + * set_item_tags() will create tags that do not exist yet. + * + * @property-read int $id + * @property-read string $name + * @property-read string $rawname + * @property-read int $tagcollid + * @property-read int $userid + * @property-read string $tagtype "official" or "default" + * @property-read string $description + * @property-read int $descriptionformat + * @property-read int $flag 0 if not flagged or positive integer if flagged + * @property-read int $timemodified + * + * @package core_tag + * @copyright 2015 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_tag_tag { + + /** @var stdClass data about the tag */ + protected $record = null; + + /** + * Constructor. Use functions get(), get_by_name(), etc. + * + * @param stdClass $record + */ + protected function __construct($record) { + if (empty($record->id)) { + throw new coding_exeption("Record must contain at least field 'id'"); + } + $this->record = $record; + } + + /** + * Magic getter + * + * @param string $name + * @return mixed + */ + public function __get($name) { + return $this->record->$name; + } + + /** + * Magic isset method + * + * @param string $name + * @return bool + */ + public function __isset($name) { + return isset($this->record->$name); + } + + /** + * Converts to object + * + * @return stdClass + */ + public function to_object() { + return fullclone($this->record); + } + + /** + * Returns tag name ready to be displayed + * + * @param bool $ashtml (default true) if true will return htmlspecialchars encoded string + * @return string + */ + public function get_display_name($ashtml = true) { + return static::make_display_name($this->record, $ashtml); + } + + /** + * Prepares tag name ready to be displayed + * + * @param stdClass|core_tag_tag $tag record from db table tag, must contain properties name and rawname + * @param bool $ashtml (default true) if true will return htmlspecialchars encoded string + * @return string + */ + public static function make_display_name($tag, $ashtml = true) { + global $CFG; + + if (empty($CFG->keeptagnamecase)) { + // This is the normalized tag name. + $tagname = core_text::strtotitle($tag->name); + } else { + // Original casing of the tag name. + $tagname = $tag->rawname; + } + + // Clean up a bit just in case the rules change again. + $tagname = clean_param($tagname, PARAM_TAG); + + return $ashtml ? htmlspecialchars($tagname) : $tagname; + } + + /** + * Adds one or more tag in the database. This function should not be called directly : you should + * use tag_set. + * + * @param int $tagcollid + * @param string|array $tags one tag, or an array of tags, to be created + * @param bool $isofficial type of tag to be created. An official tag is kept even if there are no records tagged with it. + * @return array tag objects indexed by their lowercase normalized names. Any boolean false in the array + * indicates an error while adding the tag. + */ + protected static function add($tagcollid, $tags, $isofficial = false) { + global $USER, $DB; + + $tagobject = new stdClass(); + $tagobject->tagtype = $isofficial ? 'official' : 'default'; + $tagobject->userid = $USER->id; + $tagobject->timemodified = time(); + $tagobject->tagcollid = $tagcollid; + + $rv = array(); + foreach ($tags as $veryrawname) { + $rawname = clean_param($veryrawname, PARAM_TAG); + if (!$rawname) { + $rv[$rawname] = false; + } else { + $obj = (object)(array)$tagobject; + $obj->rawname = $rawname; + $obj->name = core_text::strtolower($rawname); + $obj->id = $DB->insert_record('tag', $obj); + $rv[$obj->name] = new static($obj); + + \core\event\tag_created::create_from_tag($rv[$obj->name])->trigger(); + } + } + + return $rv; + } + + /** + * Simple function to just return a single tag object by its id + * + * @param int $id + * @param string $returnfields which fields do we want returned from table {tag}. + * Default value is 'id,name,rawname,tagcollid', + * specify '*' to include all fields. + * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; + * IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended); + * MUST_EXIST means throw exception if no record or multiple records found + * @return core_tag_tag|false tag object + */ + public static function get($id, $returnfields = 'id, name, rawname, tagcollid', $strictness = IGNORE_MISSING) { + global $DB; + $record = $DB->get_record('tag', array('id' => $id), $returnfields, $strictness); + if ($record) { + return new static($record); + } + return false; + } + + /** + * Simple function to just return a single tag object by tagcollid and name + * + * @param int $tagcollid tag collection to use, + * if 0 is given we will try to guess the tag collection and return the first match + * @param string $name tag name + * @param string $returnfields which fields do we want returned. This is a comma separated string + * containing any combination of 'id', 'name', 'rawname', 'tagcollid' or '*' to include all fields. + * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; + * IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended); + * MUST_EXIST means throw exception if no record or multiple records found + * @return core_tag_tag|false tag object + */ + public static function get_by_name($tagcollid, $name, $returnfields='id, name, rawname, tagcollid', + $strictness = IGNORE_MISSING) { + global $DB; + if ($tagcollid == 0) { + $tags = static::guess_by_name($name, $returnfields); + if ($tags) { + $tag = reset($tags); + return $tag; + } else if ($strictness == MUST_EXIST) { + throw new dml_missing_record_exception('tag', 'name=?', array($name)); + } + return false; + } + $name = core_text::strtolower($name); // To cope with input that might just be wrong case. + $params = array('name' => $name, 'tagcollid' => $tagcollid); + $record = $DB->get_record('tag', $params, $returnfields, $strictness); + if ($record) { + return new static($record); + } + return false; + } + + /** + * Looking in all tag collections for the tag with the given name + * + * @param string $name tag name + * @param string $returnfields + * @return array array of core_tag_tag instances + */ + public static function guess_by_name($name, $returnfields='id, name, rawname, tagcollid') { + global $DB; + if (empty($name)) { + return array(); + } + $tagcolls = core_tag_collection::get_collections(); + list($sql, $params) = $DB->get_in_or_equal(array_keys($tagcolls), SQL_PARAMS_NAMED); + $params['name'] = core_text::strtolower($name); + $tags = $DB->get_records_select('tag', 'name = :name AND tagcollid ' . $sql, $params, '', $returnfields); + if (count($tags) > 1) { + // Sort in the same order as tag collections. + uasort($tags, create_function('$a,$b', '$tagcolls = core_tag_collection::get_collections(); ' . + 'return $tagcolls[$a->tagcollid]->sortorder < $tagcolls[$b->tagcollid]->sortorder ? -1 : 1;')); + } + $rv = array(); + foreach ($tags as $id => $tag) { + $rv[$id] = new static($tag); + } + return $rv; + } + + /** + * Returns the list of tag objects by tag collection id and the list of tag names + * + * @param int $tagcollid + * @param array $tags array of tags to look for + * @param string $returnfields list of DB fields to return, must contain 'id', 'name' and 'rawname' + * @return array tag-indexed array of objects. No value for a key means the tag wasn't found. + */ + public static function get_by_name_bulk($tagcollid, $tags, $returnfields = 'id, name, rawname, tagcollid') { + global $DB; + + if (empty($tags)) { + return array(); + } + + $cleantags = self::normalize(self::normalize($tags, false)); // Format: rawname => normalised name. + + list($namesql, $params) = $DB->get_in_or_equal(array_values($cleantags)); + array_unshift($params, $tagcollid); + + $recordset = $DB->get_recordset_sql("SELECT $returnfields FROM {tag} WHERE tagcollid = ? AND name $namesql", $params); + + $result = array_fill_keys($cleantags, null); + foreach ($recordset as $record) { + $result[$record->name] = new static($record); + } + $recordset->close(); + return $result; + } + + + /** + * Function that normalizes a list of tag names. + * + * @param array $rawtags array of tags + * @param bool $tolowercase convert to lower case? + * @return array lowercased normalized tags, indexed by the normalized tag, in the same order as the original array. + * (Eg: 'Banana' => 'banana'). + */ + public static function normalize($rawtags, $tolowercase = true) { + $result = array(); + foreach ($rawtags as $rawtag) { + $rawtag = trim($rawtag); + if (strval($rawtag) !== '') { + $clean = clean_param($rawtag, PARAM_TAG); + if ($tolowercase) { + $result[$rawtag] = core_text::strtolower($clean); + } else { + $result[$rawtag] = $clean; + } + } + } + return $result; + } + + /** + * Retrieves tags and/or creates them if do not exist yet + * + * @param int $tagcollid + * @param array $tags array of raw tag names, do not have to be normalised + * @param bool $createasofficial + * @return core_tag_tag[] array of tag objects indexed with lowercase normalised tag name + */ + public static function create_if_missing($tagcollid, $tags, $createasofficial = false) { + $cleantags = self::normalize(array_filter(self::normalize($tags, false))); // Array rawname => normalised name . + + $result = static::get_by_name_bulk($tagcollid, $tags, '*'); + $existing = array_filter($result); + $missing = array_diff_key(array_flip($cleantags), $existing); // Array normalised name => rawname. + if ($missing) { + $newtags = static::add($tagcollid, array_values($missing), $createasofficial); + foreach ($newtags as $tag) { + $result[$tag->name] = $tag; + } + } + return $result; + } + + /** + * Creates a URL to view a tag + * + * @param int $tagcollid + * @param string $name + * @param int $exclusivemode + * @param int $fromctx context id where this tag cloud is displayed + * @param int $ctx context id for tag view link + * @param int $rec recursive argument for tag view link + * @return \moodle_url + */ + public static function make_url($tagcollid, $name, $exclusivemode = 0, $fromctx = 0, $ctx = 0, $rec = 1) { + $coll = core_tag_collection::get_by_id($tagcollid); + if (!empty($coll->customurl)) { + $url = '/' . ltrim(trim($coll->customurl), '/'); + } else { + $url = '/tag/index.php'; + } + $params = array('tc' => $tagcollid, 'tag' => $name); + if ($exclusivemode) { + $params['excl'] = 1; + } + if ($fromctx) { + $params['from'] = $fromctx; + } + if ($ctx) { + $params['ctx'] = $ctx; + } + if (!$rec) { + $params['rec'] = 0; + } + return new moodle_url($url, $params); + } + + /** + * Returns URL to view the tag + * + * @param int $exclusivemode + * @param int $fromctx context id where this tag cloud is displayed + * @param int $ctx context id for tag view link + * @param int $rec recursive argument for tag view link + * @return \moodle_url + */ + public function get_view_url($exclusivemode = 0, $fromctx = 0, $ctx = 0, $rec = 1) { + return static::make_url($this->record->tagcollid, $this->record->rawname, + $exclusivemode, $fromctx, $ctx, $rec); + } + + /** + * Validates that the required fields were retrieved and retrieves them if missing + * + * @param array $list array of the fields that need to be validated + * @param string $caller name of the function that requested it, for the debugging message + */ + protected function ensure_fields_exist($list, $caller) { + global $DB; + $missing = array_diff($list, array_keys((array)$this->record)); + if ($missing) { + debugging('core_tag_tag::' . $caller . '() must be called on fully retrieved tag object. Missing fields: '. + join(', ', $missing), DEBUG_DEVELOPER); + $this->record = $DB->get_record('tag', array('id' => $this->record->id), '*', MUST_EXIST); + } + } + + /** + * Deletes the tag instance given the record from tag_instance DB table + * + * @param stdClass $taginstance + * @param bool $fullobject whether $taginstance contains all fields from DB table tag_instance + * (in this case it is safe to add a record snapshot to the event) + * @return bool + */ + protected function delete_instance_as_record($taginstance, $fullobject = false) { + global $DB; + + $this->ensure_fields_exist(array('name', 'rawname', 'tagtype'), 'delete_instance_as_record'); + + $DB->delete_records('tag_instance', array('id' => $taginstance->id)); + + // We can not fire an event with 'null' as the contextid. + if (is_null($taginstance->contextid)) { + $taginstance->contextid = context_system::instance()->id; + } + + // Trigger tag removed event. + $taginstance->tagid = $this->id; + \core\event\tag_removed::create_from_tag_instance($taginstance, $this->name, $this->rawname, $fullobject)->trigger(); + + // If there are no other instances of the tag then consider deleting the tag as well. + if ($this->tagtype === 'default') { + if (!$DB->record_exists('tag_instance', array('tagid' => $this->id))) { + self::delete_tags($this->id); + } + } + + return true; + } + + /** + * Delete one instance of a tag. If the last instance was deleted, it will also delete the tag, unless its type is 'official'. + * + * @param string $component component responsible for tagging. For BC it can be empty but in this case the + * query will be slow because DB index will not be used. + * @param string $itemtype the type of the record for which to remove the instance + * @param int $itemid the id of the record for which to remove the instance + * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course) + */ + protected function delete_instance($component, $itemtype, $itemid, $tiuserid = 0) { + global $DB; + $params = array('tagid' => $this->id, + 'itemtype' => $itemtype, 'itemid' => $itemid); + if ($tiuserid) { + $params['tiuserid'] = $tiuserid; + } + if ($component) { + $params['component'] = $component; + } + + $taginstance = $DB->get_record('tag_instance', $params); + if (!$taginstance) { + return; + } + $this->delete_instance_as_record($taginstance, true); + } + + /** + * Bulk delete all tag instances for a component or tag area + * + * @param string $component + * @param string $itemtype (optional) + * @param int $contextid (optional) + */ + public static function delete_instances($component, $itemtype = null, $contextid = null) { + global $DB; + + $sql = "SELECT ti.*, t.name, t.rawname, t.tagtype + FROM {tag_instance} ti + JOIN {tag} t + ON ti.tagid = t.id + WHERE ti.component = :component"; + $params = array('component' => $component); + if (!is_null($contextid)) { + $sql .= " AND ti.contextid = :contextid"; + $params['contextid'] = $contextid; + } + if (!is_null($itemtype)) { + $sql .= " AND ti.itemtype = :itemtype"; + $params['itemtype'] = $itemtype; + } + if ($taginstances = $DB->get_records_sql($sql, $params)) { + // Now remove all the tag instances. + $DB->delete_records('tag_instance', $params); + // Save the system context in case the 'contextid' column in the 'tag_instance' table is null. + $syscontextid = context_system::instance()->id; + // Loop through the tag instances and fire an 'tag_removed' event. + foreach ($taginstances as $taginstance) { + // We can not fire an event with 'null' as the contextid. + if (is_null($taginstance->contextid)) { + $taginstance->contextid = $syscontextid; + } + + // Trigger tag removed event. + \core\event\tag_removed::create_from_tag_instance($taginstance, $taginstance->name, + $taginstance->rawname, true)->trigger(); + } + } + } + + /** + * Adds a tag instance + * + * @param string $component + * @param string $itemtype + * @param string $itemid + * @param context $context + * @param int $ordering + * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course) + * @return int id of tag_instance + */ + protected function add_instance($component, $itemtype, $itemid, context $context, $ordering, $tiuserid = 0) { + global $DB; + $this->ensure_fields_exist(array('name', 'rawname'), 'add_instance'); + + $taginstance = new StdClass; + $taginstance->tagid = $this->id; + $taginstance->component = $component ? $component : ''; + $taginstance->itemid = $itemid; + $taginstance->itemtype = $itemtype; + $taginstance->contextid = $context->id; + $taginstance->ordering = $ordering; + $taginstance->timecreated = time(); + $taginstance->timemodified = $taginstance->timecreated; + $taginstance->tiuserid = $tiuserid; + + $taginstance->id = $DB->insert_record('tag_instance', $taginstance); + + // Trigger tag added event. + \core\event\tag_added::create_from_tag_instance($taginstance, $this->name, $this->rawname, true)->trigger(); + + return $taginstance->id; + } + + /** + * Updates the ordering on tag instance + * + * @param int $instanceid + * @param int $ordering + */ + protected function update_instance_ordering($instanceid, $ordering) { + global $DB; + $data = new stdClass(); + $data->id = $instanceid; + $data->ordering = $ordering; + $data->timemodified = time(); + + $DB->update_record('tag_instance', $data); + } + + /** + * Get the array of core_tag_tag objects associated with an item (instances). + * + * Use {@link core_tag_tag::get_item_tags_array()} if you wish to get the same data as simple array. + * + * @param string $component component responsible for tagging. For BC it can be empty but in this case the + * query will be slow because DB index will not be used. + * @param string $itemtype type of the tagged item + * @param int $itemid + * @param null|bool $official - true - official only, false - non official only, null - any (default) + * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging + * @return core_tag_tag[] each object contains additional fields taginstanceid, taginstancecontextid and ordering + */ + public static function get_item_tags($component, $itemtype, $itemid, $official = null, $tiuserid = 0) { + global $DB; + + if (static::is_enabled($component, $itemtype) === false) { + // Tagging area is properly defined but not enabled - return empty array. + return array(); + } + + // Note: if the fields in this query are changed, you need to do the same changes in tag_get_correlated(). + $sql = "SELECT ti.id AS taginstanceid, tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag, + tg.tagcollid, ti.ordering, ti.contextid AS taginstancecontextid + FROM {tag_instance} ti + JOIN {tag} tg ON tg.id = ti.tagid + WHERE ti.itemtype = :itemtype AND ti.itemid = :itemid ". + ($component ? "AND ti.component = :component " : ""). + ($tiuserid ? "AND ti.tiuserid = :tiuserid " : ""). + (($official === true) ? "AND tg.tagtype = :official " : ""). + (($official === false) ? "AND tg.tagtype <> :official " : ""). + "ORDER BY ti.ordering ASC, ti.id"; + + $params = array(); + $params['itemtype'] = $itemtype; + $params['itemid'] = $itemid; + $params['component'] = $component; + $params['official'] = 'official'; + $params['tiuserid'] = $tiuserid; + + $records = $DB->get_records_sql($sql, $params); + $result = array(); + foreach ($records as $id => $record) { + $result[$id] = new static($record); + } + return $result; + } + + /** + * Returns the list of display names of the tags that are associated with an item + * + * This method is usually used to prefill the form data for the 'tags' form element + * + * @param string $component component responsible for tagging. For BC it can be empty but in this case the + * query will be slow because DB index will not be used. + * @param string $itemtype type of the tagged item + * @param int $itemid + * @param null|bool $official - true - official only, false - non official only, null - any (default) + * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging + * @param bool $ashtml (default true) if true will return htmlspecialchars encoded tag names + * @return string[] array of tags display names + */ + public static function get_item_tags_array($component, $itemtype, $itemid, $official = null, $tiuserid = 0, $ashtml = true) { + $tags = array(); + foreach (static::get_item_tags($component, $itemtype, $itemid, $official, $tiuserid) as $tag) { + $tags[$tag->id] = $tag->get_display_name($ashtml); + } + return $tags; + } + + /** + * Sets the list of tag instances for one item (table record). + * + * Extra exsisting instances are removed, new ones are added. New tags are created if needed. + * + * This method can not be used for setting tags relations, please use set_related_tags() + * + * @param string $component component responsible for tagging + * @param string $itemtype type of the tagged item + * @param int $itemid + * @param context $context + * @param array $tagnames + * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course) + */ + public static function set_item_tags($component, $itemtype, $itemid, context $context, $tagnames, $tiuserid = 0) { + if ($itemtype === 'tag') { + if ($tiuserid) { + throw new coding_exeption('Related tags can not have tag instance userid'); + } + debugging('You can not use set_item_tags() for tagging a tag, please use set_related_tags()', DEBUG_DEVELOPER); + static::get($itemid, '*', MUST_EXIST)->set_related_tags($tagnames); + return; + } + + if ($tagnames !== null && static::is_enabled($component, $itemtype) === false) { + // Tagging area is properly defined but not enabled - do nothing. + // Unless we are deleting the item tags ($tagnames === null), in which case proceed with deleting. + return; + } + + // Apply clean_param() to all tags. + if ($tagnames) { + $tagcollid = core_tag_area::get_collection($component, $itemtype); + $tagobjects = static::create_if_missing($tagcollid, $tagnames); + } else { + $tagobjects = array(); + } + + $currenttags = static::get_item_tags($component, $itemtype, $itemid, null, $tiuserid); + + // For data coherence reasons, it's better to remove deleted tags + // before adding new data: ordering could be duplicated. + foreach ($currenttags as $currenttag) { + if (!array_key_exists($currenttag->name, $tagobjects)) { + $taginstance = (object)array('id' => $currenttag->taginstanceid, + 'itemtype' => $itemtype, 'itemid' => $itemid, + 'contextid' => $currenttag->taginstancecontextid, 'tiuserid' => $tiuserid); + $currenttag->delete_instance_as_record($taginstance, false); + } + } + + $ordering = -1; + foreach ($tagobjects as $name => $tag) { + $ordering++; + foreach ($currenttags as $currenttag) { + if (strval($currenttag->name) === strval($name)) { + if ($currenttag->ordering != $ordering) { + $currenttag->update_instance_ordering($currenttag->taginstanceid, $ordering); + } + continue 2; + } + } + $tag->add_instance($component, $itemtype, $itemid, $context, $ordering, $tiuserid); + } + } + + /** + * Removes all tags from an item. + * + * All tags will be removed even if tagging is disabled in this area. This is + * usually called when the item itself has been deleted. + * + * @param string $component component responsible for tagging + * @param string $itemtype type of the tagged item + * @param int $itemid + * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course) + */ + public static function remove_all_item_tags($component, $itemtype, $itemid, $tiuserid = 0) { + $context = context_system::instance(); // Context will not be used. + static::set_item_tags($component, $itemtype, $itemid, $context, null, $tiuserid); + } + + /** + * Adds a tag to an item, without overwriting the current tags. + * + * If the tag has already been added to the record, no changes are made. + * + * @param string $component the component that was tagged + * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.) + * @param int $itemid the id of the record to tag + * @param context $context the context of where this tag was assigned + * @param string $tagname the tag to add + * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course) + * @return int id of tag_instance that was either created or already existed or null if tagging is not enabled + */ + public static function add_item_tag($component, $itemtype, $itemid, context $context, $tagname, $tiuserid = 0) { + global $DB; + + if (static::is_enabled($component, $itemtype) === false) { + // Tagging area is properly defined but not enabled - do nothing. + return null; + } + + $rawname = clean_param($tagname, PARAM_TAG); + $normalisedname = core_text::strtolower($rawname); + $tagcollid = core_tag_area::get_collection($component, $itemtype); + + $usersql = $tiuserid ? " AND ti.tiuserid = :tiuserid " : ""; + $sql = 'SELECT t.*, ti.id AS taginstanceid + FROM {tag} t + LEFT JOIN {tag_instance} ti ON ti.tagid = t.id AND ti.itemtype = :itemtype '. + $usersql . + 'AND ti.itemid = :itemid AND ti.component = :component + WHERE t.name = :name AND t.tagcollid = :tagcollid'; + $params = array('name' => $normalisedname, 'tagcollid' => $tagcollid, 'itemtype' => $itemtype, + 'itemid' => $itemid, 'component' => $component, 'tiuserid' => $tiuserid); + $record = $DB->get_record_sql($sql, $params); + if ($record) { + if ($record->taginstanceid) { + // Tag was already added to the item, nothing to do here. + return $record->taginstanceid; + } + $tag = new static($record); + } else { + // The tag does not exist yet, create it. + $tags = static::add($tagcollid, array($tagname)); + $tag = reset($tags); + } + + $ordering = $DB->get_field_sql('SELECT MAX(ordering) FROM {tag_instance} ti + WHERE ti.itemtype = :itemtype AND ti.itemid = itemid AND + ti.component = :component' . $usersql, $params); + + return $tag->add_instance($component, $itemtype, $itemid, $context, $ordering + 1, $tiuserid); + } + + /** + * Removes the tag from an item without changing the other tags + * + * @param string $component the component that was tagged + * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.) + * @param int $itemid the id of the record to tag + * @param string $tagname the tag to remove + * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course) + */ + public static function remove_item_tag($component, $itemtype, $itemid, $tagname, $tiuserid = 0) { + global $DB; + + if (static::is_enabled($component, $itemtype) === false) { + // Tagging area is properly defined but not enabled - do nothing. + return array(); + } + + $rawname = clean_param($tagname, PARAM_TAG); + $normalisedname = core_text::strtolower($rawname); + + $usersql = $tiuserid ? " AND tiuserid = :tiuserid " : ""; + $componentsql = $component ? " AND ti.component = :component " : ""; + $sql = 'SELECT t.*, ti.id AS taginstanceid, ti.contextid AS taginstancecontextid, ti.ordering + FROM {tag} t JOIN {tag_instance} ti ON ti.tagid = t.id ' . $usersql . ' + WHERE t.name = :name AND ti.itemtype = :itemtype + AND ti.itemid = :itemid ' . $componentsql; + $params = array('name' => $normalisedname, + 'itemtype' => $itemtype, 'itemid' => $itemid, 'component' => $component, + 'tiuserid' => $tiuserid); + if ($record = $DB->get_record_sql($sql, $params)) { + $taginstance = (object)array('id' => $record->taginstanceid, + 'itemtype' => $itemtype, 'itemid' => $itemid, + 'contextid' => $record->taginstancecontextid, 'tiuserid' => $tiuserid); + $tag = new static($record); + $tag->delete_instance_as_record($taginstance, false); + $sql = "UPDATE {tag_instance} ti SET ordering = ordering - 1 + WHERE ti.itemtype = :itemtype + AND ti.itemid = :itemid $componentsql $usersql + AND ti.ordering > :ordering"; + $params['ordering'] = $record->ordering; + $DB->execute($sql, $params); + } + } + + /** + * Allows to move all tag instances from one context to another + * + * @param string $component the component that was tagged + * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.) + * @param context $oldcontext + * @param context $newcontext + */ + public static function move_context($component, $itemtype, $oldcontext, $newcontext) { + global $DB; + if ($oldcontext instanceof context) { + $oldcontext = $oldcontext->id; + } + if ($newcontext instanceof context) { + $newcontext = $newcontext->id; + } + $DB->set_field('tag_instance', 'contextid', $newcontext, + array('component' => $component, 'itemtype' => $itemtype, 'contextid' => $oldcontext)); + } + + /** + * Moves all tags of the specified items to the new context + * + * @param string $component the component that was tagged + * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.) + * @param array $itemids + * @param context|int $newcontext target context to move tags to + */ + public static function change_items_context($component, $itemtype, $itemids, $newcontext) { + global $DB; + if (empty($itemids)) { + return; + } + if (!is_array($itemids)) { + $itemids = array($itemids); + } + list($sql, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED); + $params['component'] = $component; + $params['itemtype'] = $itemtype; + if ($newcontext instanceof context) { + $newcontext = $newcontext->id; + } + $DB->set_field_select('tag_instance', 'contextid', $newcontext, + 'component = :component AND itemtype = :itemtype AND itemid ' . $sql, $params); + } + + /** + * Updates the information about the tag + * + * @param array|stdClass $data data to update, may contain: tagtype, description, descriptionformat, rawname + * @return bool whether the tag was updated. False may be returned if: all new values match the existing, + * or an invalid tagtype was supplied, or it was attempted to rename the tag to the name that is already used. + */ + public function update($data) { + global $DB, $COURSE; + + $allowedfields = array('tagtype', 'description', 'descriptionformat', 'rawname'); + + $data = (array)$data; + if ($extrafields = array_diff(array_keys($data), $allowedfields)) { + debugging('The field(s) '.join(', ', $extrafields).' will be ignored when updating the tag', + DEBUG_DEVELOPER); + } + $data = array_intersect_key($data, array_fill_keys($allowedfields, 1)); + $this->ensure_fields_exist(array_merge(array('tagcollid', 'userid', 'name', 'rawname'), array_keys($data)), 'update'); + + // Validate the tag name. + if (array_key_exists('rawname', $data)) { + $data['rawname'] = clean_param($data['rawname'], PARAM_TAG); + $name = core_text::strtolower($data['rawname']); + + if (!$name) { + unset($data['rawname']); + } else if ($existing = static::get_by_name($this->tagcollid, $name, 'id')) { + // Prevent the rename if a tag with that name already exists. + if ($existing->id != $this->id) { + debugging('New tag name already exists, you should check it before calling core_tag_tag::update()', + DEBUG_DEVELOPER); + unset($data['rawname']); + } + } + if (isset($data['rawname'])) { + $data['name'] = $name; + } + } + + // Validate the tag type. + if (array_key_exists('tagtype', $data) && $data['tagtype'] !== 'official' && $data['tagtype'] !== 'default') { + debugging('Unrecognised tag type "'.$data['tagtype'].'" ignored when updating the tag', DEBUG_DEVELOPER); + unset($data['tagtype']); + } + + // Find only the attributes that need to be changed. + $originalname = $this->name; + foreach ($data as $key => $value) { + if ($this->record->$key !== $value) { + $this->record->$key = $value; + } else { + unset($data[$key]); + } + } + if (empty($data)) { + return false; + } + + $data['id'] = $this->id; + $data['timemodified'] = time(); + $DB->update_record('tag', $data); + + $event = \core\event\tag_updated::create(array( + 'objectid' => $this->id, + 'relateduserid' => $this->userid, + 'context' => context_system::instance(), + 'other' => array( + 'name' => $this->name, + 'rawname' => $this->rawname + ) + )); + if (isset($data['rawname'])) { + $event->set_legacy_logdata(array($COURSE->id, 'tag', 'update', 'index.php?id='. $this->id, + $originalname . '->'. $this->name)); + } + $event->trigger(); + return true; + } + + /** + * Flag a tag as inappropriate + */ + public function flag() { + global $DB; + + $this->ensure_fields_exist(array('name', 'userid', 'rawname', 'flag'), 'flag'); + + // Update all the tags to flagged. + $this->timemodified = time(); + $this->flag++; + $DB->update_record('tag', array('timemodified' => $this->timemodified, + 'flag' => $this->flag, 'id' => $this->id)); + + $event = \core\event\tag_flagged::create(array( + 'objectid' => $this->id, + 'relateduserid' => $this->userid, + 'context' => context_system::instance(), + 'other' => array( + 'name' => $this->name, + 'rawname' => $this->rawname + ) + + )); + $event->trigger(); + } + + /** + * Remove the inappropriate flag on a tag. + */ + public function reset_flag() { + global $DB; + + $this->ensure_fields_exist(array('name', 'userid', 'rawname', 'flag'), 'flag'); + + if (!$this->flag) { + // Nothing to do. + return false; + } + + $this->timemodified = time(); + $this->flag = 0; + $DB->update_record('tag', array('timemodified' => $this->timemodified, + 'flag' => 0, 'id' => $this->id)); + + $event = \core\event\tag_unflagged::create(array( + 'objectid' => $this->id, + 'relateduserid' => $this->userid, + 'context' => context_system::instance(), + 'other' => array( + 'name' => $this->name, + 'rawname' => $this->rawname + ) + )); + $event->trigger(); + } + + /** + * Sets the list of tags related to this one. + * + * Tag relations are recorded by two instances linking two tags to each other. + * For tag relations ordering is not used and may be random. + * + * @param array $tagnames + */ + public function set_related_tags($tagnames) { + $context = context_system::instance(); + $tagobjects = $tagnames ? static::create_if_missing($this->tagcollid, $tagnames) : array(); + unset($tagobjects[$this->name]); // Never link to itself. + + $currenttags = static::get_item_tags('core', 'tag', $this->id); + + // For data coherence reasons, it's better to remove deleted tags + // before adding new data: ordering could be duplicated. + foreach ($currenttags as $currenttag) { + if (!array_key_exists($currenttag->name, $tagobjects)) { + $taginstance = (object)array('id' => $currenttag->taginstanceid, + 'itemtype' => 'tag', 'itemid' => $this->id, + 'contextid' => $context->id); + $currenttag->delete_instance_as_record($taginstance, false); + $this->delete_instance('core', 'tag', $currenttag->id); + } + } + + foreach ($tagobjects as $name => $tag) { + foreach ($currenttags as $currenttag) { + if ($currenttag->name === $name) { + continue 2; + } + } + $this->add_instance('core', 'tag', $tag->id, $context, 0); + $tag->add_instance('core', 'tag', $this->id, $context, 0); + $currenttags[] = $tag; + } + } + + /** + * Adds to the list of related tags without removing existing + * + * Tag relations are recorded by two instances linking two tags to each other. + * For tag relations ordering is not used and may be random. + * + * @param array $tagnames + */ + public function add_related_tags($tagnames) { + $context = context_system::instance(); + $tagobjects = static::create_if_missing($this->tagcollid, $tagnames); + + $currenttags = static::get_item_tags('core', 'tag', $this->id); + + foreach ($tagobjects as $name => $tag) { + foreach ($currenttags as $currenttag) { + if ($currenttag->name === $name) { + continue 2; + } + } + $this->add_instance('core', 'tag', $tag->id, $context, 0); + $tag->add_instance('core', 'tag', $this->id, $context, 0); + $currenttags[] = $tag; + } + } + + /** + * Returns the correlated tags of a tag, retrieved from the tag_correlation table. + * + * Correlated tags are calculated in cron based on existing tag instances. + * + * @param bool $keepduplicates if true, will return one record for each existing + * tag instance which may result in duplicates of the actual tags + * @return core_tag_tag[] an array of tag objects + */ + public function get_correlated_tags($keepduplicates = false) { + global $DB; + + $correlated = $DB->get_field('tag_correlation', 'correlatedtags', array('tagid' => $this->id)); + + if (!$correlated) { + return array(); + } + $correlated = preg_split('/\s*,\s*/', trim($correlated), -1, PREG_SPLIT_NO_EMPTY); + list($query, $params) = $DB->get_in_or_equal($correlated); + + // This is (and has to) return the same fields as the query in core_tag_tag::get_item_tags(). + $sql = "SELECT ti.id AS taginstanceid, tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag, + tg.tagcollid, ti.ordering, ti.contextid AS taginstancecontextid + FROM {tag} tg + INNER JOIN {tag_instance} ti ON tg.id = ti.tagid + WHERE tg.id $query + ORDER BY ti.ordering ASC, ti.id"; + $records = $DB->get_records_sql($sql, $params); + $seen = array(); + $result = array(); + foreach ($records as $id => $record) { + if (!$keepduplicates && !empty($seen[$record->id])) { + continue; + } + $result[$id] = new static($record); + $seen[$record->id] = true; + } + return $result; + } + + /** + * Returns tags that this tag was manually set as related to + * + * @return core_tag_tag[] + */ + public function get_manual_related_tags() { + return self::get_item_tags('core', 'tag', $this->id); + } + + /** + * Returns tags related to a tag + * + * Related tags of a tag come from two sources: + * - manually added related tags, which are tag_instance entries for that tag + * - correlated tags, which are calculated + * + * @return core_tag_tag[] an array of tag objects + */ + public function get_related_tags() { + $manual = $this->get_manual_related_tags(); + $automatic = $this->get_correlated_tags(); + $relatedtags = array_merge($manual, $automatic); + + // Remove duplicated tags (multiple instances of the same tag). + $seen = array(); + foreach ($relatedtags as $instance => $tag) { + if (isset($seen[$tag->id])) { + unset($relatedtags[$instance]); + } else { + $seen[$tag->id] = 1; + } + } + + return $relatedtags; + } + + /** + * Find all items tagged with a tag of a given type ('post', 'user', etc.) + * + * @param string $component component responsible for tagging. For BC it can be empty but in this case the + * query will be slow because DB index will not be used. + * @param string $itemtype type to restrict search to + * @param int $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point. + * @param int $limitnum (optional, required if $limitfrom is set) return a subset comprising this many records. + * @param string $subquery additional query to be appended to WHERE clause, refer to the itemtable as 'it' + * @param array $params additional parameters for the DB query + * @return array of matching objects, indexed by record id, from the table containing the type requested + */ + public function get_tagged_items($component, $itemtype, $limitfrom = '', $limitnum = '', $subquery = '', $params = array()) { + global $DB; + + if (empty($itemtype) || !$DB->get_manager()->table_exists($itemtype)) { + return array(); + } + $params = $params ? $params : array(); + + $query = "SELECT it.* + FROM {".$itemtype."} it INNER JOIN {tag_instance} tt ON it.id = tt.itemid + WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid"; + $params['itemtype'] = $itemtype; + $params['tagid'] = $this->id; + if ($component) { + $query .= ' AND tt.component = :component'; + $params['component'] = $component; + } + if ($subquery) { + $query .= ' AND ' . $subquery; + } + $query .= ' ORDER BY it.id'; + + return $DB->get_records_sql($query, $params, $limitfrom, $limitnum); + } + + /** + * Count how many items are tagged with a specific tag. + * + * @param string $component component responsible for tagging. For BC it can be empty but in this case the + * query will be slow because DB index will not be used. + * @param string $itemtype type to restrict search to + * @param string $subquery additional query to be appended to WHERE clause, refer to the itemtable as 'it' + * @param array $params additional parameters for the DB query + * @return int number of mathing tags. + */ + public function count_tagged_items($component, $itemtype, $subquery = '', $params = array()) { + global $DB; + + if (empty($itemtype) || !$DB->get_manager()->table_exists($itemtype)) { + return 0; + } + $params = $params ? $params : array(); + + $query = "SELECT COUNT(it.id) + FROM {".$itemtype."} it INNER JOIN {tag_instance} tt ON it.id = tt.itemid + WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid"; + $params['itemtype'] = $itemtype; + $params['tagid'] = $this->id; + if ($component) { + $query .= ' AND tt.component = :component'; + $params['component'] = $component; + } + if ($subquery) { + $query .= ' AND ' . $subquery; + } + + return $DB->get_field_sql($query, $params); + } + + /** + * Determine if an item is tagged with a specific tag + * + * Note that this is a static method and not a method of core_tag object because the tag might not exist yet, + * for example user searches for "php" and we offer him to add "php" to his interests. + * + * @param string $component component responsible for tagging. For BC it can be empty but in this case the + * query will be slow because DB index will not be used. + * @param string $itemtype the record type to look for + * @param int $itemid the record id to look for + * @param string $tagname a tag name + * @return int 1 if it is tagged, 0 otherwise + */ + public static function is_item_tagged_with($component, $itemtype, $itemid, $tagname) { + global $DB; + $tagcollid = core_tag_area::get_collection($component, $itemtype); + $query = 'SELECT 1 FROM {tag} t + JOIN {tag_instance} ti ON ti.tagid = t.id + WHERE t.name = ? AND t.tagcollid = ? AND ti.itemtype = ? AND ti.itemid = ?'; + $cleanname = core_text::strtolower(clean_param($tagname, PARAM_TAG)); + $params = array($cleanname, $tagcollid, $itemtype, $itemid); + if ($component) { + $query .= ' AND ti.component = ?'; + $params[] = $component; + } + return $DB->record_exists_sql($query, $params) ? 1 : 0; + } + + /** + * Returns whether the tag area is enabled + * + * @param string $component component responsible for tagging + * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc. + * @return bool|null + */ + public static function is_enabled($component, $itemtype) { + return core_tag_area::is_enabled($component, $itemtype); + } + + /** + * Retrieves contents of tag area for the tag/index.php page + * + * @param stdClass $tagarea + * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag + * are displayed on the page and the per-page limit may be bigger + * @param int $fromctx context id where the link was displayed, may be used by callbacks + * to display items in the same context first + * @param int $ctx context id where to search for records + * @param bool $rec search in subcontexts as well + * @param int $page 0-based number of page being displayed + * @return \core_tag\output\tagindex + */ + public function get_tag_index($tagarea, $exclusivemode, $fromctx, $ctx, $rec, $page = 0) { + global $CFG; + if (!empty($tagarea->callback)) { + if (!empty($tagarea->callbackfile)) { + require_once($CFG->dirroot . '/' . ltrim($tagarea->callbackfile, '/')); + } + $callback = $tagarea->callback; + return $callback($this, $exclusivemode, $fromctx, $ctx, $rec, $page); + } + return null; + } + + /** + * Returns formatted description of the tag + * + * @param array $options + * @return string + */ + public function get_formatted_description($options = array()) { + $options = empty($options) ? array() : (array)$options; + $options += array('para' => false, 'overflowdiv' => true); + $description = file_rewrite_pluginfile_urls($this->description, 'pluginfile.php', + context_system::instance()->id, 'tag', 'description', $this->id); + return format_text($description, $this->descriptionformat, $options); + } + + /** + * Returns the list of tag links available for the current user (edit, flag, etc.) + * + * @return array + */ + public function get_links() { + global $USER; + $links = array(); + + if (!isloggedin() || isguestuser()) { + return $links; + } + + $tagname = $this->get_display_name(); + $systemcontext = context_system::instance(); + + // Add a link for users to add/remove this from their interests. + if (static::is_enabled('core', 'user') && core_tag_area::get_collection('core', 'user') == $this->tagcollid) { + if (static::is_item_tagged_with('core', 'user', $USER->id, $this->name)) { + $url = new moodle_url('/tag/user.php', array('action' => 'removeinterest', + 'sesskey' => sesskey(), 'tag' => $this->rawname)); + $links[] = html_writer::link($url, get_string('removetagfrommyinterests', 'tag', $tagname), + array('class' => 'removefrommyinterests')); + } else { + $url = new moodle_url('/tag/user.php', array('action' => 'addinterest', + 'sesskey' => sesskey(), 'tag' => $this->rawname)); + $links[] = html_writer::link($url, get_string('addtagtomyinterests', 'tag', $tagname), + array('class' => 'addtomyinterests')); + } + } + + // Flag as inappropriate link. Only people with moodle/tag:flag capability. + if (has_capability('moodle/tag:flag', $systemcontext)) { + $url = new moodle_url('/tag/user.php', array('action' => 'flaginappropriate', + 'sesskey' => sesskey(), 'id' => $this->id)); + $links[] = html_writer::link($url, get_string('flagasinappropriate', 'tag', $tagname), + array('class' => 'flagasinappropriate')); + } + + // Edit tag: Only people with moodle/tag:edit capability who either have it as an interest or can manage tags. + if (has_capability('moodle/tag:edit', $systemcontext) || + has_capability('moodle/tag:manage', $systemcontext)) { + $url = new moodle_url('/tag/edit.php', array('id' => $this->id)); + $links[] = html_writer::link($url, get_string('edittag', 'tag'), + array('class' => 'edittag')); + } + + return $links; + } + + /** + * Delete one or more tag, and all their instances if there are any left. + * + * @param int|array $tagids one tagid (int), or one array of tagids to delete + * @return bool true on success, false otherwise + */ + public static function delete_tags($tagids) { + global $DB; + + if (!is_array($tagids)) { + $tagids = array($tagids); + } + if (empty($tagids)) { + return; + } + + // Use the tagids to create a select statement to be used later. + list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids); + + // Store the tags and tag instances we are going to delete. + $tags = $DB->get_records_select('tag', 'id ' . $tagsql, $tagparams); + $taginstances = $DB->get_records_select('tag_instance', 'tagid ' . $tagsql, $tagparams); + + // Delete all the tag instances. + $select = 'WHERE tagid ' . $tagsql; + $sql = "DELETE FROM {tag_instance} $select"; + $DB->execute($sql, $tagparams); + + // Delete all the tag correlations. + $sql = "DELETE FROM {tag_correlation} $select"; + $DB->execute($sql, $tagparams); + + // Delete all the tags. + $select = 'WHERE id ' . $tagsql; + $sql = "DELETE FROM {tag} $select"; + $DB->execute($sql, $tagparams); + + // Fire an event that these items were untagged. + if ($taginstances) { + // Save the system context in case the 'contextid' column in the 'tag_instance' table is null. + $syscontextid = context_system::instance()->id; + // Loop through the tag instances and fire a 'tag_removed'' event. + foreach ($taginstances as $taginstance) { + // We can not fire an event with 'null' as the contextid. + if (is_null($taginstance->contextid)) { + $taginstance->contextid = $syscontextid; + } + + // Trigger tag removed event. + \core\event\tag_removed::create_from_tag_instance($taginstance, + $tags[$taginstance->tagid]->name, $tags[$taginstance->tagid]->rawname, + true)->trigger(); + } + } + + // Fire an event that these tags were deleted. + if ($tags) { + $context = context_system::instance(); + foreach ($tags as $tag) { + // Delete all files associated with this tag. + $fs = get_file_storage(); + $files = $fs->get_area_files($context->id, 'tag', 'description', $tag->id); + foreach ($files as $file) { + $file->delete(); + } + + // Trigger an event for deleting this tag. + $event = \core\event\tag_deleted::create(array( + 'objectid' => $tag->id, + 'relateduserid' => $tag->userid, + 'context' => $context, + 'other' => array( + 'name' => $tag->name, + 'rawname' => $tag->rawname + ) + )); + $event->add_record_snapshot('tag', $tag); + $event->trigger(); + } + } + + return true; + } +} diff --git a/tag/edit.php b/tag/edit.php index 16bf9e7b04e..a3087577c82 100644 --- a/tag/edit.php +++ b/tag/edit.php @@ -26,8 +26,8 @@ require_once('../config.php'); require_once('lib.php'); require_once('edit_form.php'); -$tag_id = optional_param('id', 0, PARAM_INT); -$tag_name = optional_param('tag', '', PARAM_TAG); +$tagid = optional_param('id', 0, PARAM_INT); +$tagname = optional_param('tag', '', PARAM_TAG); $returnurl = optional_param('returnurl', '', PARAM_LOCALURL); require_login(); @@ -40,34 +40,50 @@ if (empty($CFG->usetags)) { $systemcontext = context_system::instance(); require_capability('moodle/tag:edit', $systemcontext); -if ($tag_name) { - $tag = tag_get('name', $tag_name, '*'); -} else if ($tag_id) { - $tag = tag_get('id', $tag_id, '*'); +if ($tagname) { + $tagcollid = optional_param('tc', 0, PARAM_INT); + if (!$tagcollid) { + // Tag name specified but tag collection was not. Try to guess it. + $tags = core_tag_tag::guess_by_name($tagname, '*'); + if (count($tags) > 1) { + // This tag was found in more than one collection, redirect to search. + redirect(new moodle_url('/tag/search.php', array('tag' => $tagname))); + } else if (count($tags) == 1) { + $tag = reset($tags); + } + } else { + if (!$tag = core_tag_tag::get_by_name($tagcollid, $tagname, '*')) { + redirect(new moodle_url('/tag/search.php', array('tagcollid' => $tagcollid))); + } + } +} else if ($tagid) { + $tag = core_tag_tag::get($tagid, '*'); } if (empty($tag)) { - redirect($CFG->wwwroot.'/tag/search.php'); + redirect(new moodle_url('/tag/search.php')); } -$PAGE->set_url('/tag/index.php', array('id' => $tag->id)); +$PAGE->set_url($tag->get_view_url()); $PAGE->set_subpage($tag->id); $PAGE->set_context($systemcontext); $PAGE->set_blocks_editing_capability('moodle/tag:editblocks'); $PAGE->set_pagelayout('standard'); -$tagname = tag_display_name($tag); +$tagname = $tag->get_display_name(); +$tagcollid = $tag->tagcollid; // set the relatedtags field of the $tag object that will be passed to the form -$tag->relatedtags = tag_get_tags_array('tag', $tag->id); +$data = $tag->to_object(); +$data->relatedtags = core_tag_tag::get_item_tags_array('core', 'tag', $tag->id); $options = new stdClass(); $options->smiley = false; $options->filter = false; // convert and remove any XSS -$tag->description = format_text($tag->description, $tag->descriptionformat, $options); -$tag->descriptionformat = FORMAT_HTML; +$data->description = format_text($tag->description, $tag->descriptionformat, $options); +$data->descriptionformat = FORMAT_HTML; $errorstring = ''; @@ -78,76 +94,37 @@ $editoroptions = array( 'context' => $systemcontext, 'subdirs' => file_area_contains_subdirs($systemcontext, 'tag', 'description', $tag->id), ); -$tag = file_prepare_standard_editor($tag, 'description', $editoroptions, $systemcontext, 'tag', 'description', $tag->id); +$data = file_prepare_standard_editor($data, 'description', $editoroptions, $systemcontext, 'tag', 'description', $data->id); -$tagform = new tag_edit_form(null, compact('editoroptions')); -if ( $tag->tagtype == 'official' ) { - $tag->tagtype = '1'; -} else { - $tag->tagtype = '0'; -} +$tagform = new tag_edit_form(null, array('editoroptions' => $editoroptions, 'tag' => $tag)); +$data->tagtype = ($data->tagtype === 'official') ? '1' : '0'; +$data->returnurl = $returnurl; -$tag->returnurl = $returnurl; -$tagform->set_data($tag); +$tagform->set_data($data); -// If new data has been sent, update the tag record if ($tagform->is_cancelled()) { - redirect($returnurl ? new moodle_url($returnurl) : - new moodle_url('/tag/index.php', array('tag' => $tag->name))); + redirect($returnurl ? new moodle_url($returnurl) : $tag->get_view_url()); } else if ($tagnew = $tagform->get_data()) { + // If new data has been sent, update the tag record. + $updatedata = array(); if (has_capability('moodle/tag:manage', $systemcontext)) { - if (($tag->tagtype != 'default') && (!isset($tagnew->tagtype) || ($tagnew->tagtype != '1'))) { - tag_type_set($tag->id, 'default'); - - } elseif (($tag->tagtype != 'official') && ($tagnew->tagtype == '1')) { - tag_type_set($tag->id, 'official'); - } + $updatedata['tagtype'] = empty($tagnew->tagtype) ? 'default' : 'official'; + $updatedata['rawname'] = $tagnew->rawname; } - if (!has_capability('moodle/tag:manage', $systemcontext)) { - unset($tagnew->name); - unset($tagnew->rawname); + $tagnew = file_postupdate_standard_editor($tagnew, 'description', $editoroptions, + $systemcontext, 'tag', 'description', $tag->id); + $updatedata['description'] = $tagnew->description; + $updatedata['descriptionformat'] = $tagnew->descriptionformat; - } else { // They might be trying to change the rawname, make sure it's a change that doesn't affect name - $norm = tag_normalize($tagnew->rawname, TAG_CASE_LOWER); - $tagnew->name = array_shift($norm); + // Update name, description and official type. + $tag->update($updatedata); - if ($tag->rawname !== $tagnew->rawname) { // The name has changed, let's make sure it's not another existing tag - if (($id = tag_get_id($tagnew->name)) && $id != $tag->id) { // Something exists already, so flag an error. - $errorstring = s($tagnew->rawname).': '.get_string('namesalreadybeeingused', 'tag'); - } - } - } + // Updated related tags. + $tag->set_related_tags($tagnew->relatedtags); - if (empty($errorstring)) { // All is OK, let's save it - - $tagnew = file_postupdate_standard_editor($tagnew, 'description', $editoroptions, $systemcontext, 'tag', 'description', $tag->id); - - if ($tag->description != $tagnew->description) { - tag_description_set($tag_id, $tagnew->description, $tagnew->descriptionformat); - } - - $tagnew->timemodified = time(); - - if (has_capability('moodle/tag:manage', $systemcontext)) { - // Check if we need to rename the tag. - if (isset($tagnew->name) && ($tag->rawname != $tagnew->rawname)) { - // Rename the tag. - if (!tag_rename($tag->id, $tagnew->rawname)) { - print_error('errorupdatingrecord', 'tag'); - } - } - } - - //updated related tags - tag_set('tag', $tagnew->id, $tagnew->relatedtags, 'core', $systemcontext->id); - //print_object($tagnew); die(); - - $tagname = isset($tagnew->rawname) ? $tagnew->rawname : $tag->rawname; - redirect($returnurl ? new moodle_url($returnurl) : - new moodle_url('/tag/index.php', array('tag' => $tagname))); - } + redirect($returnurl ? new moodle_url($returnurl) : $tag->get_view_url()); } navigation_node::override_active_url(new moodle_url('/tag/search.php')); diff --git a/tag/edit_form.php b/tag/edit_form.php index c2fcdc381c0..ef2fc8a32f1 100644 --- a/tag/edit_form.php +++ b/tag/edit_form.php @@ -68,10 +68,36 @@ class tag_edit_form extends moodleform { $mform->addElement('checkbox', 'tagtype', get_string('officialtag', 'tag')); } - $mform->addElement('tags', 'relatedtags', get_string('relatedtags','tag')); + $mform->addElement('tags', 'relatedtags', get_string('relatedtags', 'tag'), + array('tagcollid' => $this->_customdata['tag']->tagcollid)); $this->add_action_buttons(true, get_string('updatetag', 'tag')); } + /** + * Custom form validation + * + * @param array $data + * @param array $files + * @return array + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + + if (isset($data['rawname'])) { + $newname = core_text::strtolower($data['rawname']); + $tag = $this->_customdata['tag']; + if ($tag->name != $newname) { + // The name has changed, let's make sure it's not another existing tag. + if (core_tag_tag::get_by_name($tag->tagcollid, $newname)) { + // Something exists already, so flag an error. + $errors['rawname'] = get_string('namesalreadybeeingused', 'tag'); + } + } + } + + return $errors; + } + } diff --git a/tag/index.php b/tag/index.php index 8934d87753d..b0373168bd6 100644 --- a/tag/index.php +++ b/tag/index.php @@ -23,10 +23,8 @@ */ require_once('../config.php'); -require_once('lib.php'); -require_once('locallib.php'); -require_once($CFG->dirroot.'/lib/weblib.php'); -require_once($CFG->dirroot.'/blog/lib.php'); +require_once($CFG->dirroot . '/lib/weblib.php'); +require_once($CFG->dirroot . '/blog/lib.php'); require_login(); @@ -36,166 +34,93 @@ if (empty($CFG->usetags)) { $tagid = optional_param('id', 0, PARAM_INT); // tag id $tagname = optional_param('tag', '', PARAM_TAG); // tag +$tagareaid = optional_param('ta', 0, PARAM_INT); // Tag area id. +$exclusivemode = optional_param('excl', 0, PARAM_BOOL); // Exclusive mode (show entities in one tag area only). +$page = optional_param('page', 0, PARAM_INT); // Page to display. +$fromctx = optional_param('from', null, PARAM_INT); +$ctx = optional_param('ctx', null, PARAM_INT); +$rec = optional_param('rec', 1, PARAM_INT); $edit = optional_param('edit', -1, PARAM_BOOL); -$userpage = optional_param('userpage', 0, PARAM_INT); // which page to show -$perpage = optional_param('perpage', 24, PARAM_INT); $systemcontext = context_system::instance(); if ($tagname) { - $tag = tag_get('name', $tagname, '*'); + $tagcollid = optional_param('tc', 0, PARAM_INT); + if (!$tagcollid) { + // Tag name specified but tag collection was not. Try to guess it. + $tags = core_tag_tag::guess_by_name($tagname, '*'); + if (count($tags) > 1) { + // This tag was found in more than one collection, redirect to search. + redirect(new moodle_url('/tag/search.php', array('query' => $tagname))); + } else if (count($tags) == 1) { + $tag = reset($tags); + } + } else { + if (!$tag = core_tag_tag::get_by_name($tagcollid, $tagname, '*')) { + redirect(new moodle_url('/tag/search.php', array('tc' => $tagcollid, 'query' => $tagname))); + } + } } else if ($tagid) { - $tag = tag_get('id', $tagid, '*'); + $tag = core_tag_tag::get($tagid, '*'); } unset($tagid); if (empty($tag)) { - redirect($CFG->wwwroot.'/tag/search.php'); + redirect(new moodle_url('/tag/search.php')); } -$PAGE->set_url('/tag/index.php', array('id' => $tag->id)); +if ($ctx && ($context = context::instance_by_id($ctx, IGNORE_MISSING)) && $context->contextlevel >= CONTEXT_COURSE) { + list($context, $course, $cm) = get_context_info_array($context->id); + require_login($course, false, $cm, false, true); +} else { + $PAGE->set_context($systemcontext); +} + +$tagcollid = $tag->tagcollid; + +$PAGE->set_url($tag->get_view_url($exclusivemode, $fromctx, $ctx, $rec)); $PAGE->set_subpage($tag->id); -$PAGE->set_context($systemcontext); $tagnode = $PAGE->navigation->find('tags', null); $tagnode->make_active(); $PAGE->set_pagelayout('standard'); $PAGE->set_blocks_editing_capability('moodle/tag:editblocks'); -if (($edit != -1) and $PAGE->user_allowed_editing()) { - $USER->editing = $edit; +$buttons = ''; +if (has_capability('moodle/tag:manage', context_system::instance())) { + $buttons .= $OUTPUT->single_button(new moodle_url('/tag/manage.php'), + get_string('managetags', 'tag'), 'GET'); } - -$tagname = tag_display_name($tag); -$title = get_string('tag', 'tag') .' - '. $tagname; - -$button = ''; -if ($PAGE->user_allowed_editing() ) { - $button = $OUTPUT->edit_button(new moodle_url("$CFG->wwwroot/tag/index.php", array('id' => $tag->id))); +if ($PAGE->user_allowed_editing()) { + if ($edit != -1) { + $USER->editing = $edit; + } + $buttons .= $OUTPUT->edit_button(clone($PAGE->url)); } $PAGE->navbar->add($tagname); -$PAGE->set_title($title); +$PAGE->set_title(get_string('tag', 'tag') .' - '. $tag->get_display_name()); $PAGE->set_heading($COURSE->fullname); -$PAGE->set_button($button); -$courserenderer = $PAGE->get_renderer('core', 'course'); +$PAGE->set_button($buttons); + +// Find all areas in this collection and their items tagged with this tag. +$tagareas = core_tag_collection::get_areas($tagcollid); +if ($tagareaid) { + $tagareas = array_intersect_key($tagareas, array($tagareaid => 1)); +} +if (!$tagareaid && count($tagareas) == 1) { + // Automatically set "exclusive" mode for tag collection with one tag area only. + $exclusivemode = 1; +} +$entities = array(); +foreach ($tagareas as $ta) { + $entities[] = $tag->get_tag_index($ta, $exclusivemode, $fromctx, $ctx, $rec, $page); +} +$entities = array_filter($entities); + +$tagrenderer = $PAGE->get_renderer('core', 'tag'); +$pagecontents = $tagrenderer->tag_index_page($tag, array_filter($entities), $tagareaid, + $exclusivemode, $fromctx, $ctx, $rec, $page); + echo $OUTPUT->header(); - -// Manage all tags links -if (has_capability('moodle/tag:manage', $systemcontext)) { - echo '' ; -} - -$tagname = tag_display_name($tag); - -if ($tag->flag > 0 && has_capability('moodle/tag:manage', $systemcontext)) { - $tagname = '' . $tagname . ''; -} - -echo $OUTPUT->heading($tagname, 2); -tag_print_management_box($tag); -tag_print_description_box($tag); -// Check what type of results are avaialable -$courses = $courserenderer->tagged_courses($tag->id); - -if (!empty($CFG->enableblogs) && has_capability('moodle/blog:view', $systemcontext)) { - require_once($CFG->dirroot.'/blog/lib.php'); - require_once($CFG->dirroot.'/blog/locallib.php'); - - $bloglisting = new blog_listing(array('tag' => $tag->id)); - $limit = 10; - $start = 0; - $blogs = $bloglisting->get_entries($start, $limit); -} -$usercount = tag_record_count('user', $tag->id); - -// Only include 's to those anchors that actually will be shown -$relatedpageslink = ""; -$countanchors = 0; -if (!empty($courses)) { - $relatedpageslink = ''.get_string('courses').''; - $countanchors++; -} -if (!empty($blogs)) { - if ($countanchors > 0) { - $relatedpageslink .= ' | '; - } - $relatedpageslink .= ''.get_string('relatedblogs', 'tag').''; - $countanchors++; -} -if ($usercount > 0) { - if ($countanchors > 0) { - $relatedpageslink .= ' | '; - } - $relatedpageslink .= ''.get_string('users').''; - $countanchors++; -} -// If only one anchor is present, no is needed -if ($countanchors == 0) { - echo '

    '.get_string('noresultsfor', 'tag', $tagname).'

    '; -} elseif ($countanchors > 1) { - echo '

    '.$relatedpageslink.'

    '; -} - -// Display courses tagged with the tag -if (!empty($courses)) { - - echo $OUTPUT->box_start('generalbox', 'tag-blogs'); //could use an id separate from tag-blogs, but would have to copy the css style to make it look the same - - echo "
    "; - echo $courses; - - echo $OUTPUT->box_end(); -} - -// Print up to 10 previous blogs entries - -if (!empty($blogs)) { - echo $OUTPUT->box_start('generalbox', 'tag-blogs'); - $heading = get_string('relatedblogs', 'tag', $tagname). ' ' . get_string('taggedwith', 'tag', $tagname); - echo ""; - echo $OUTPUT->heading($heading, 3); - - echo ''; - - $allblogsurl = new moodle_url('/blog/index.php', array('tagid' => $tag->id)); - echo '

    '.get_string('seeallblogs', 'tag', $tagname).'

    '; - - echo $OUTPUT->box_end(); -} - -if ($usercount > 0) { - - //user table box - echo $OUTPUT->box_start('generalbox', 'tag-user-table'); - - $heading = get_string('users'). ' ' . get_string('taggedwith', 'tag', $tagname) . ': ' . $usercount; - echo ""; - echo $OUTPUT->heading($heading, 3); - - $baseurl = new moodle_url('/tag/index.php', array('id' => $tag->id)); - $pagingbar = new paging_bar($usercount, $userpage, $perpage, $baseurl); - $pagingbar->pagevar = 'userpage'; - echo $OUTPUT->render($pagingbar); - tag_print_tagged_users_table($tag, $userpage * $perpage, $perpage); - echo $OUTPUT->box_end(); -} - +echo $pagecontents; echo $OUTPUT->footer(); diff --git a/tag/lib.php b/tag/lib.php index 15dc38e3316..c399650dad1 100644 --- a/tag/lib.php +++ b/tag/lib.php @@ -14,1544 +14,20 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . - /** - * Moodle tag library + * Functions for component core_tag * - * Tag strings : you can use any character in tags, except the comma (which is the separator) and - * the '\' (backslash). Note that many spaces (or other blank characters) will get "compressed" - * into one. A tag string is always a rawurlencode'd string. This is the same behavior as - * http://del.icio.us. - * - * A "record" is a php array (note that an object will work too) that contains the following - * variables : - * - type: The database table containing the record that we are tagging (eg: for a blog, this is - * the table named 'post', and for a user it is the table name 'user') - * - id: The id of the record - * - * BASIC INSTRUCTIONS : - * - to "tag a blog post" (for example): - * tag_set('post', $blog_post->id, $array_of_tags, 'core', $thecontext); - * - * - to "remove all the tags on a blog post": - * tag_set('post', $blog_post->id, array(), 'core', $thecontext); - * - * Tag set will create tags that need to be created. + * To set or get item tags refer to the class {@link core_tag_tag} * * @package core_tag - * @category tag - * @todo MDL-31090 turn this into a full-fledged categorization system. This could start by - * modifying (removing, probably) the 'tag type' to use another table describing the - * relationship between tags (parents, sibling, etc.), which could then be merged with - * the 'course categorization' system. - * @see http://www.php.net/manual/en/function.urlencode.php * @copyright 2007 Luiz Cruz * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -/** - * Used to require that the return value from a function is an array. - * @see tag_set() - */ -define('TAG_RETURN_ARRAY', 0); -/** - * Used to require that the return value from a function is an object. - * @see tag_set() - */ -define('TAG_RETURN_OBJECT', 1); -/** - * Use to specify that HTML free text is expected to be returned from a function. - * @see tag_display_name() - */ -define('TAG_RETURN_TEXT', 2); -/** - * Use to specify that encoded HTML is expected to be returned from a function. - * @see tag_display_name() - */ -define('TAG_RETURN_HTML', 3); - -/** - * Used to specify that we wish a lowercased string to be returned - * @see tag_normal() - */ -define('TAG_CASE_LOWER', 0); -/** - * Used to specify that we do not wish the case of the returned string to change - * @see tag_normal() - */ -define('TAG_CASE_ORIGINAL', 1); - -/** - * Used to specify that we want all related tags returned, no matter how they are related. - * @see tag_get_related_tags() - */ -define('TAG_RELATED_ALL', 0); -/** - * Used to specify that we only want back tags that were manually related. - * @see tag_get_related_tags() - */ -define('TAG_RELATED_MANUAL', 1); -/** - * Used to specify that we only want back tags where the relationship was automatically correlated. - * @see tag_get_related_tags() - */ -define('TAG_RELATED_CORRELATED', 2); - -/////////////////////////////////////////////////////// -/////////////////// PUBLIC TAG API //////////////////// - -/// Functions for settings tags ////////////////////// - -/** - * Set the tags assigned to a record. This overwrites the current tags. - * - * This function is meant to be fed the string coming up from the user interface, which contains all tags assigned to a record. - * - * @package core_tag - * @category tag - * @access public - * @param string $record_type the type of record to tag ('post' for blogs, 'user' for users, 'tag' for tags, etc.) - * @param int $record_id the id of the record to tag - * @param array $tags the array of tags to set on the record. If given an empty array, all tags will be removed. - * @param string|null $component the component that was tagged - * @param int|null $contextid the context id of where this tag was assigned - * @return bool|null - */ -function tag_set($record_type, $record_id, $tags, $component = null, $contextid = null) { - - static $in_recursion_semaphore = false; // this is to prevent loops when tagging a tag - - if ( $record_type == 'tag' && !$in_recursion_semaphore) { - $current_tagged_tag_name = tag_get_name($record_id); - } - - $tags_ids = tag_get_id($tags, TAG_RETURN_ARRAY); // force an array, even if we only have one tag. - $cleaned_tags = tag_normalize($tags); - //echo 'tags-in-tag_set'; var_dump($tags); var_dump($tags_ids); var_dump($cleaned_tags); - - $current_ids = tag_get_tags_ids($record_type, $record_id); - //var_dump($current_ids); - - // for data coherence reasons, it's better to remove deleted tags - // before adding new data: ordering could be duplicated. - foreach($current_ids as $current_id) { - if (!in_array($current_id, $tags_ids)) { - tag_delete_instance($record_type, $record_id, $current_id); - if ( $record_type == 'tag' && !$in_recursion_semaphore) { - // if we are removing a tag-on-a-tag (manually related tag), - // we need to remove the opposite relationship as well. - tag_delete_instance('tag', $current_id, $record_id); - } - } - } - - if (empty($tags)) { - return true; - } - - foreach($tags as $ordering => $tag) { - $tag = trim($tag); - if (!$tag) { - continue; - } - - $clean_tag = $cleaned_tags[$tag]; - $tag_current_id = $tags_ids[$clean_tag]; - - if ( is_null($tag_current_id) ) { - // create new tags - //echo "call to add tag $tag\n"; - $new_tag = tag_add($tag); - $tag_current_id = $new_tag[$clean_tag]; - } - - tag_assign($record_type, $record_id, $tag_current_id, $ordering, 0, $component, $contextid); - - // if we are tagging a tag (adding a manually-assigned related tag), we - // need to create the opposite relationship as well. - if ( $record_type == 'tag' && !$in_recursion_semaphore) { - $in_recursion_semaphore = true; - tag_set_add('tag', $tag_current_id, $current_tagged_tag_name, $component, $contextid); - $in_recursion_semaphore = false; - } - } -} - -/** - * Adds a tag to a record, without overwriting the current tags. - * - * @package core_tag - * @category tag - * @access public - * @param string $record_type the type of record to tag ('post' for blogs, 'user' for users, etc.) - * @param int $record_id the id of the record to tag - * @param string $tag the tag to add - * @param string|null $component the component that was tagged - * @param int|null $contextid the context id of where this tag was assigned - * @return bool|null - */ -function tag_set_add($record_type, $record_id, $tag, $component = null, $contextid = null) { - - $new_tags = array(); - foreach( tag_get_tags($record_type, $record_id) as $current_tag ) { - $new_tags[] = $current_tag->rawname; - } - $new_tags[] = $tag; - - return tag_set($record_type, $record_id, $new_tags, $component, $contextid); -} - -/** - * Removes a tag from a record, without overwriting other current tags. - * - * @package core_tag - * @category tag - * @access public - * @param string $record_type the type of record to tag ('post' for blogs, 'user' for users, etc.) - * @param int $record_id the id of the record to tag - * @param string $tag the tag to delete - * @param string|null $component the component that was tagged - * @param int|null $contextid the context id of where this tag was assigned - * @return bool|null - */ -function tag_set_delete($record_type, $record_id, $tag, $component = null, $contextid = null) { - - $new_tags = array(); - foreach( tag_get_tags($record_type, $record_id) as $current_tag ) { - if ($current_tag->name != $tag) { // Keep all tags but the one specified - $new_tags[] = $current_tag->name; - } - } - - return tag_set($record_type, $record_id, $new_tags, $component, $contextid); -} - -/** - * Set the type of a tag. At this time (version 2.2) the possible values are 'default' or 'official'. Official tags will be - * displayed separately "at tagging time" (while selecting the tags to apply to a record). - * - * @package core_tag - * @category tag - * @access public - * @param string $tagid tagid to modify - * @param string $type either 'default' or 'official' - * @return bool true on success, false otherwise - */ -function tag_type_set($tagid, $type) { - global $DB; - - if ($tag = $DB->get_record('tag', array('id' => $tagid), 'id, userid, name, rawname')) { - $tag->tagtype = $type; - $tag->timemodified = time(); - $DB->update_record('tag', $tag); - - $event = \core\event\tag_updated::create(array( - 'objectid' => $tag->id, - 'relateduserid' => $tag->userid, - 'context' => context_system::instance(), - 'other' => array( - 'name' => $tag->name, - 'rawname' => $tag->rawname - ) - )); - $event->trigger(); - - return true; - } - return false; -} - -/** - * Set the description of a tag - * - * @package core_tag - * @category tag - * @access public - * @param int $tagid the id of the tag - * @param string $description the tag's description string to be set - * @param int $descriptionformat the moodle text format of the description - * {@link http://docs.moodle.org/dev/Text_formats_2.0#Database_structure} - * @return bool true on success, false otherwise - */ -function tag_description_set($tagid, $description, $descriptionformat) { - global $DB; - - if ($tag = $DB->get_record('tag', array('id' => $tagid), 'id, userid, name, rawname')) { - $tag->description = $description; - $tag->descriptionformat = $descriptionformat; - $tag->timemodified = time(); - $DB->update_record('tag', $tag); - - $event = \core\event\tag_updated::create(array( - 'objectid' => $tag->id, - 'relateduserid' => $tag->userid, - 'context' => context_system::instance(), - 'other' => array( - 'name' => $tag->name, - 'rawname' => $tag->rawname - ) - )); - $event->trigger(); - - return true; - } - - return false; -} - - - - - - -/// Functions for getting information about tags ////// - -/** - * Simple function to just return a single tag object when you know the name or something - * - * @package core_tag - * @category tag - * @access public - * @param string $field which field do we use to identify the tag: id, name or rawname - * @param string $value the required value of the aforementioned field - * @param string $returnfields which fields do we want returned. This is a comma seperated string containing any combination of - * 'id', 'name', 'rawname' or '*' to include all fields. - * @return mixed tag object - */ -function tag_get($field, $value, $returnfields='id, name, rawname') { - global $DB; - - if ($field == 'name') { - $value = core_text::strtolower($value); // To cope with input that might just be wrong case - } - return $DB->get_record('tag', array($field=>$value), $returnfields); -} - - -/** - * Get the array of db record of tags associated to a record (instances). Use {@see tag_get_tags_csv()} if you wish to get the same - * data in a comma-separated string, for instances such as needing to simply display a list of tags to the end user. This should - * really be called tag_get_tag_instances(). - * - * @package core_tag - * @category tag - * @access public - * @param string $record_type the record type for which we want to get the tags - * @param int $record_id the record id for which we want to get the tags - * @param string $type the tag type (either 'default' or 'official'). By default, all tags are returned. - * @param int $userid (optional) only required for course tagging - * @return array the array of tags - */ -function tag_get_tags($record_type, $record_id, $type=null, $userid=0) { - global $CFG, $DB; - - $params = array(); - - if ($type) { - $sql_type = "AND tg.tagtype = :type"; - $params['type'] = $type; - } else { - $sql_type = ''; - } - - $u = null; - if ($userid) { - $u = "AND ti.tiuserid = :userid "; - $params['userid'] = $userid; - } - - $sql = "SELECT ti.id AS taginstanceid, tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering - FROM {tag_instance} ti - JOIN {tag} tg ON tg.id = ti.tagid - WHERE ti.itemtype = :recordtype AND ti.itemid = :recordid $u $sql_type - ORDER BY ti.ordering ASC"; - $params['recordtype'] = $record_type; - $params['recordid'] = $record_id; - - // if the fields in this query are changed, you need to do the same changes in tag_get_correlated_tags - return $DB->get_records_sql($sql, $params); - // This version of the query, reversing the ON clause, "correctly" returns - // a row with NULL values for instances that are still in the DB even though - // the tag has been deleted. This shouldn't happen, but if it did, using - // this query could help "clean it up". This causes bugs at this time. - //$tags = $DB->get_records_sql("SELECT ti.tagid, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering ". - // "FROM {tag_instance} ti LEFT JOIN {tag} tg ON ti.tagid = tg.id ". - // "WHERE ti.itemtype = '{$record_type}' AND ti.itemid = '{$record_id}' {$type} ". - // "ORDER BY ti.ordering ASC"); -} - -/** - * Get the array of tags display names, indexed by id. - * - * @package core_tag - * @category tag - * @access public - * @param string $record_type the record type for which we want to get the tags - * @param int $record_id the record id for which we want to get the tags - * @param string $type the tag type (either 'default' or 'official'). By default, all tags are returned. - * @return array the array of tags (with the value returned by tag_display_name), indexed by id - */ -function tag_get_tags_array($record_type, $record_id, $type=null) { - $tags = array(); - foreach(tag_get_tags($record_type, $record_id, $type) as $tag) { - $tags[$tag->id] = tag_display_name($tag); - } - return $tags; -} - -/** - * Get a comma-separated string of tags associated to a record. Use {@see tag_get_tags()} to get the same information in an array. - * - * @package core_tag - * @category tag - * @access public - * @param string $record_type the record type for which we want to get the tags - * @param int $record_id the record id for which we want to get the tags - * @param int $html either TAG_RETURN_HTML or TAG_RETURN_TEXT, depending on the type of output desired - * @param string $type either 'official' or 'default', if null, all tags are returned - * @return string the comma-separated list of tags. - */ -function tag_get_tags_csv($record_type, $record_id, $html=TAG_RETURN_HTML, $type=null) { - global $CFG; - - $tags_names = array(); - foreach(tag_get_tags($record_type, $record_id, $type) as $tag) { - if ($html == TAG_RETURN_TEXT) { - $tags_names[] = tag_display_name($tag, TAG_RETURN_TEXT); - } else { // TAG_RETURN_HTML - $tags_names[] = ''. tag_display_name($tag) .''; - } - } - return implode(', ', $tags_names); -} - -/** - * Get an array of tag ids associated to a record. - * - * @package core_tag - * @category tag - * @access public - * @todo MDL-31150 Update ordering property - * @param string $record_type the record type for which we want to get the tags - * @param int $record_id the record id for which we want to get the tags - * @return array tag ids, indexed and sorted by 'ordering' - */ -function tag_get_tags_ids($record_type, $record_id) { - $tag_ids = array(); - foreach (tag_get_tags($record_type, $record_id) as $tag) { - if ( array_key_exists($tag->ordering, $tag_ids) ) { - // until we can add a unique constraint, in table tag_instance, - // on (itemtype, itemid, ordering), this is needed to prevent a bug - // TODO MDL-31150 modify database in 2.0 - $tag->ordering++; - } - $tag_ids[$tag->ordering] = $tag->id; - } - ksort($tag_ids); - return $tag_ids; -} - -/** - * Returns the database ID of a set of tags. - * - * @package core_tag - * @category tag - * @access public - * @todo MDL-31152 Test the commented MDL-31152 todo in this function to see if it helps performance - * without breaking anything. - * @param mixed $tags one tag, or array of tags, to look for. - * @param bool $return_value specify the type of the returned value. Either TAG_RETURN_OBJECT, or TAG_RETURN_ARRAY (default). - * If TAG_RETURN_ARRAY is specified, an array will be returned even if only one tag was passed in $tags. - * @return mixed tag-indexed array of ids (or objects, if second parameter is TAG_RETURN_OBJECT), or only an int, if only one tag - * is given *and* the second parameter is null. No value for a key means the tag wasn't found. - */ -function tag_get_id($tags, $return_value=null) { - global $CFG, $DB; - - static $tag_id_cache = array(); - - $return_an_int = false; - if (!is_array($tags)) { - if(is_null($return_value) || $return_value == TAG_RETURN_OBJECT) { - $return_an_int = true; - } - $tags = array($tags); - } - - $result = array(); - - //TODO MDL-31152 test this and see if it helps performance without breaking anything - //foreach($tags as $key => $tag) { - // $clean_tag = core_text::strtolower($tag); - // if ( array_key_exists($clean_tag), $tag_id_cache) ) { - // $result[$clean_tag] = $tag_id_cache[$clean_tag]; - // $tags[$key] = ''; // prevent further processing for this one. - // } - //} - - $tags = array_values(tag_normalize($tags)); - foreach($tags as $key => $tag) { - $tags[$key] = core_text::strtolower($tag); - $result[core_text::strtolower($tag)] = null; // key must exists : no value for a key means the tag wasn't found. - } - - if (empty($tags)) { - return array(); - } - - list($tag_string, $params) = $DB->get_in_or_equal($tags); - - $rs = $DB->get_recordset_sql("SELECT * FROM {tag} WHERE name $tag_string ORDER BY name", $params); - foreach ($rs as $record) { - if ($return_value == TAG_RETURN_OBJECT) { - $result[$record->name] = $record; - } else { // TAG_RETURN_ARRAY - $result[$record->name] = $record->id; - } - } - $rs->close(); - - if ($return_an_int) { - return array_pop($result); - } - - return $result; -} - - -/** - * Returns tags related to a tag - * - * Related tags of a tag come from two sources: - * - manually added related tags, which are tag_instance entries for that tag - * - correlated tags, which are calculated - * - * @package core_tag - * @category tag - * @access public - * @param string $tagid is a single **normalized** tag name or the id of a tag - * @param int $type the function will return either manually (TAG_RELATED_MANUAL) related tags or correlated - * (TAG_RELATED_CORRELATED) tags. Default is TAG_RELATED_ALL, which returns everything. - * @param int $limitnum (optional) return a subset comprising this many records, the default is 10 - * @return array an array of tag objects - */ -function tag_get_related_tags($tagid, $type=TAG_RELATED_ALL, $limitnum=10) { - - $related_tags = array(); - - if ( $type == TAG_RELATED_ALL || $type == TAG_RELATED_MANUAL) { - //gets the manually added related tags - $related_tags = tag_get_tags('tag', $tagid); - } - - if ( $type == TAG_RELATED_ALL || $type == TAG_RELATED_CORRELATED ) { - //gets the correlated tags - $automatic_related_tags = tag_get_correlated($tagid); - $related_tags = array_merge($related_tags, $automatic_related_tags); - } - - // Remove duplicated tags (multiple instances of the same tag). - $seen = array(); - foreach ($related_tags as $instance => $tag) { - if (isset($seen[$tag->id])) { - unset($related_tags[$instance]); - } else { - $seen[$tag->id] = 1; - } - } - - return array_slice($related_tags, 0 , $limitnum); -} - -/** - * Get a comma-separated list of tags related to another tag. - * - * @package core_tag - * @category tag - * @access public - * @param array $related_tags the array returned by tag_get_related_tags - * @param int $html either TAG_RETURN_HTML (default) or TAG_RETURN_TEXT : return html links, or just text. - * @return string comma-separated list - */ -function tag_get_related_tags_csv($related_tags, $html=TAG_RETURN_HTML) { - global $CFG; - - $tags_names = array(); - foreach($related_tags as $tag) { - if ( $html == TAG_RETURN_TEXT) { - $tags_names[] = tag_display_name($tag, TAG_RETURN_TEXT); - } else { - // TAG_RETURN_HTML - $tags_names[] = ''. tag_display_name($tag) .''; - } - } - - return implode(', ', $tags_names); -} - -/** - * Change the "value" of a tag, and update the associated 'name'. - * - * @package core_tag - * @category tag - * @access public - * @param int $tagid the id of the tag to modify - * @param string $newrawname the new rawname - * @return bool true on success, false otherwise - */ -function tag_rename($tagid, $newrawname) { - global $COURSE, $DB; - - $norm = tag_normalize($newrawname, TAG_CASE_ORIGINAL); - if (! $newrawname_clean = array_shift($norm) ) { - return false; - } - - if (! $newname_clean = core_text::strtolower($newrawname_clean)) { - return false; - } - - // Prevent the rename if a tag with that name already exists - if ($existing = tag_get('name', $newname_clean, 'id, name, rawname')) { - if ($existing->id != $tagid) { // Another tag already exists with this name - return false; - } - } - - if ($tag = tag_get('id', $tagid, 'id, userid, name, rawname')) { - // Store the name before we change it. - $oldname = $tag->name; - - $tag->rawname = $newrawname_clean; - $tag->name = $newname_clean; - $tag->timemodified = time(); - $DB->update_record('tag', $tag); - - $event = \core\event\tag_updated::create(array( - 'objectid' => $tag->id, - 'relateduserid' => $tag->userid, - 'context' => context_system::instance(), - 'other' => array( - 'name' => $newname_clean, - 'rawname' => $newrawname_clean - ) - )); - $event->set_legacy_logdata(array($COURSE->id, 'tag', 'update', 'index.php?id='. $tag->id, $oldname . '->'. $tag->name)); - $event->trigger(); - - return true; - } - return false; -} - - -/** - * Delete one or more tag, and all their instances if there are any left. - * - * @package core_tag - * @category tag - * @access public - * @param mixed $tagids one tagid (int), or one array of tagids to delete - * @return bool true on success, false otherwise - */ -function tag_delete($tagids) { - global $DB; - - if (!is_array($tagids)) { - $tagids = array($tagids); - } - - // Use the tagids to create a select statement to be used later. - list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids); - - // Store the tags and tag instances we are going to delete. - $tags = $DB->get_records_select('tag', 'id ' . $tagsql, $tagparams); - $taginstances = $DB->get_records_select('tag_instance', 'tagid ' . $tagsql, $tagparams); - - // Delete all the tag instances. - $select = 'WHERE tagid ' . $tagsql; - $sql = "DELETE FROM {tag_instance} $select"; - $DB->execute($sql, $tagparams); - - // Delete all the tag correlations. - $sql = "DELETE FROM {tag_correlation} $select"; - $DB->execute($sql, $tagparams); - - // Delete all the tags. - $select = 'WHERE id ' . $tagsql; - $sql = "DELETE FROM {tag} $select"; - $DB->execute($sql, $tagparams); - - // Fire an event that these items were untagged. - if ($taginstances) { - // Save the system context in case the 'contextid' column in the 'tag_instance' table is null. - $syscontextid = context_system::instance()->id; - // Loop through the tag instances and fire a 'tag_removed'' event. - foreach ($taginstances as $taginstance) { - // We can not fire an event with 'null' as the contextid. - if (is_null($taginstance->contextid)) { - $taginstance->contextid = $syscontextid; - } - - // Trigger tag removed event. - $event = \core\event\tag_removed::create(array( - 'objectid' => $taginstance->id, - 'contextid' => $taginstance->contextid, - 'other' => array( - 'tagid' => $taginstance->tagid, - 'tagname' => $tags[$taginstance->tagid]->name, - 'tagrawname' => $tags[$taginstance->tagid]->rawname, - 'itemid' => $taginstance->itemid, - 'itemtype' => $taginstance->itemtype - ) - )); - $event->add_record_snapshot('tag_instance', $taginstance); - $event->trigger(); - } - } - - // Fire an event that these tags were deleted. - if ($tags) { - $context = context_system::instance(); - foreach ($tags as $tag) { - // Delete all files associated with this tag - $fs = get_file_storage(); - $files = $fs->get_area_files($context->id, 'tag', 'description', $tag->id); - foreach ($files as $file) { - $file->delete(); - } - - // Trigger an event for deleting this tag. - $event = \core\event\tag_deleted::create(array( - 'objectid' => $tag->id, - 'relateduserid' => $tag->userid, - 'context' => $context, - 'other' => array( - 'name' => $tag->name, - 'rawname' => $tag->rawname - ) - )); - $event->add_record_snapshot('tag', $tag); - $event->trigger(); - } - } - - return true; -} - -/** - * Deletes all the tag instances given a component and an optional contextid. - * - * @param string $component - * @param int $contextid if null, then we delete all tag instances for the $component - */ -function tag_delete_instances($component, $contextid = null) { - global $DB; - - $sql = "SELECT ti.*, t.name, t.rawname - FROM {tag_instance} ti - JOIN {tag} t - ON ti.tagid = t.id "; - if (is_null($contextid)) { - $params = array('component' => $component); - $sql .= "WHERE ti.component = :component"; - } else { - $params = array('component' => $component, 'contextid' => $contextid); - $sql .= "WHERE ti.component = :component - AND ti.contextid = :contextid"; - } - if ($taginstances = $DB->get_records_sql($sql, $params)) { - // Now remove all the tag instances. - $DB->delete_records('tag_instance',$params); - // Save the system context in case the 'contextid' column in the 'tag_instance' table is null. - $syscontextid = context_system::instance()->id; - // Loop through the tag instances and fire an 'tag_removed' event. - foreach ($taginstances as $taginstance) { - // We can not fire an event with 'null' as the contextid. - if (is_null($taginstance->contextid)) { - $taginstance->contextid = $syscontextid; - } - - // Trigger tag removed event. - $event = \core\event\tag_removed::create(array( - 'objectid' => $taginstance->id, - 'contextid' => $taginstance->contextid, - 'other' => array( - 'tagid' => $taginstance->tagid, - 'tagname' => $taginstance->name, - 'tagrawname' => $taginstance->rawname, - 'itemid' => $taginstance->itemid, - 'itemtype' => $taginstance->itemtype - ) - )); - $event->add_record_snapshot('tag_instance', $taginstance); - $event->trigger(); - } - } -} - -/** - * Delete one instance of a tag. If the last instance was deleted, it will also delete the tag, unless its type is 'official'. - * - * @package core_tag - * @category tag - * @access public - * @param string $record_type the type of the record for which to remove the instance - * @param int $record_id the id of the record for which to remove the instance - * @param int $tagid the tagid that needs to be removed - * @param int $userid (optional) the userid - * @return bool true on success, false otherwise - */ -function tag_delete_instance($record_type, $record_id, $tagid, $userid = null) { - global $DB; - - if (is_null($userid)) { - $taginstance = $DB->get_record('tag_instance', array('tagid' => $tagid, 'itemtype' => $record_type, 'itemid' => $record_id)); - } else { - $taginstance = $DB->get_record('tag_instance', array('tagid' => $tagid, 'itemtype' => $record_type, 'itemid' => $record_id, - 'tiuserid' => $userid)); - } - if ($taginstance) { - // Get the tag. - $tag = $DB->get_record('tag', array('id' => $tagid)); - - $DB->delete_records('tag_instance', array('id' => $taginstance->id)); - - // We can not fire an event with 'null' as the contextid. - if (is_null($taginstance->contextid)) { - $taginstance->contextid = context_system::instance()->id; - } - - // Trigger tag removed event. - $event = \core\event\tag_removed::create(array( - 'objectid' => $taginstance->id, - 'contextid' => $taginstance->contextid, - 'other' => array( - 'tagid' => $tag->id, - 'tagname' => $tag->name, - 'tagrawname' => $tag->rawname, - 'itemid' => $taginstance->itemid, - 'itemtype' => $taginstance->itemtype - ) - )); - $event->add_record_snapshot('tag_instance', $taginstance); - $event->trigger(); - - // If there are no other instances of the tag then consider deleting the tag as well. - if (!$DB->record_exists('tag_instance', array('tagid' => $tagid))) { - // If the tag is a personal tag then delete it - don't delete official tags. - if ($tag->tagtype == 'default') { - tag_delete($tagid); - } - } - } else { - return false; - } - - return true; -} - - -/** - * Function that returns the name that should be displayed for a specific tag - * - * @package core_tag - * @category tag - * @access public - * @param object $tagobject a line out of tag table, as returned by the adobd functions - * @param int $html TAG_RETURN_HTML (default) will return htmlspecialchars encoded string, TAG_RETURN_TEXT will not encode. - * @return string - */ -function tag_display_name($tagobject, $html=TAG_RETURN_HTML) { - global $CFG; - - if (!isset($tagobject->name)) { - return ''; - } - - if (empty($CFG->keeptagnamecase)) { - //this is the normalized tag name - $tagname = core_text::strtotitle($tagobject->name); - } else { - //original casing of the tag name - $tagname = $tagobject->rawname; - } - - // clean up a bit just in case the rules change again - $tagname = clean_param($tagname, PARAM_TAG); - - if ($html == TAG_RETURN_TEXT) { - return $tagname; - } else { // TAG_RETURN_HTML - return htmlspecialchars($tagname); - } -} - -/** - * Find all records tagged with a tag of a given type ('post', 'user', etc.) - * - * @package core_tag - * @category tag - * @access public - * @param string $tag tag to look for - * @param string $type type to restrict search to. If null, every matching record will be returned - * @param int $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point. - * @param int $limitnum (optional, required if $limitfrom is set) return a subset comprising this many records. - * @return array of matching objects, indexed by record id, from the table containing the type requested - */ -function tag_find_records($tag, $type, $limitfrom='', $limitnum='') { - global $CFG, $DB; - - if (!$tag || !$type) { - return array(); - } - - $tagid = tag_get_id($tag); - - $query = "SELECT it.* - FROM {".$type."} it INNER JOIN {tag_instance} tt ON it.id = tt.itemid - WHERE tt.itemtype = ? AND tt.tagid = ?"; - $params = array($type, $tagid); - - return $DB->get_records_sql($query, $params, $limitfrom, $limitnum); -} - - - - -/////////////////////////////////////////////////////// -/////////////////// PRIVATE TAG API /////////////////// - -/** - * Adds one or more tag in the database. This function should not be called directly : you should - * use tag_set. - * - * @package core_tag - * @access private - * @param mixed $tags one tag, or an array of tags, to be created - * @param string $type type of tag to be created ("default" is the default value and "official" is the only other supported - * value at this time). An official tag is kept even if there are no records tagged with it. - * @return array $tags ids indexed by their lowercase normalized names. Any boolean false in the array indicates an error while - * adding the tag. - */ -function tag_add($tags, $type="default") { - global $USER, $DB; - - if (!is_array($tags)) { - $tags = array($tags); - } - - $tag_object = new StdClass; - $tag_object->tagtype = $type; - $tag_object->userid = $USER->id; - $tag_object->timemodified = time(); - - $clean_tags = tag_normalize($tags, TAG_CASE_ORIGINAL); - - $tags_ids = array(); - foreach($clean_tags as $tag) { - $tag = trim($tag); - if (!$tag) { - $tags_ids[$tag] = false; - } else { - // note that the difference between rawname and name is only - // capitalization : the rawname is NOT the same at the rawtag. - $tag_object->rawname = $tag; - $tag_name_lc = core_text::strtolower($tag); - $tag_object->name = $tag_name_lc; - //var_dump($tag_object); - $tags_ids[$tag_name_lc] = $DB->insert_record('tag', $tag_object); - - $event = \core\event\tag_created::create(array( - 'objectid' => $tags_ids[$tag_name_lc], - 'relateduserid' => $tag_object->userid, - 'context' => context_system::instance(), - 'other' => array( - 'name' => $tag_object->name, - 'rawname' => $tag_object->rawname - ) - )); - $event->trigger(); - } - } - - return $tags_ids; -} - -/** - * Assigns a tag to a record; if the record already exists, the time and ordering will be updated. - * - * @package core_tag - * @access private - * @param string $record_type the type of the record that will be tagged - * @param int $record_id the id of the record that will be tagged - * @param string $tagid the tag id to set on the record. - * @param int $ordering the order of the instance for this record - * @param int $userid (optional) only required for course tagging - * @param string|null $component the component that was tagged - * @param int|null $contextid the context id of where this tag was assigned - * @return bool true on success, false otherwise - */ -function tag_assign($record_type, $record_id, $tagid, $ordering, $userid = 0, $component = null, $contextid = null) { - global $DB; - - if ($component === null || $contextid === null) { - debugging('You should specify the component and contextid of the item being tagged in your call to tag_assign.', - DEBUG_DEVELOPER); - } - - // Get the tag. - $tag = $DB->get_record('tag', array('id' => $tagid), 'name, rawname', MUST_EXIST); - - if ( $tag_instance_object = $DB->get_record('tag_instance', array('tagid'=>$tagid, 'itemtype'=>$record_type, 'itemid'=>$record_id, 'tiuserid'=>$userid), 'id')) { - $tag_instance_object->ordering = $ordering; - $tag_instance_object->timemodified = time(); - - $DB->update_record('tag_instance', $tag_instance_object); - } else { - $tag_instance_object = new StdClass; - $tag_instance_object->tagid = $tagid; - $tag_instance_object->component = $component; - $tag_instance_object->itemid = $record_id; - $tag_instance_object->itemtype = $record_type; - $tag_instance_object->contextid = $contextid; - $tag_instance_object->ordering = $ordering; - $tag_instance_object->timecreated = time(); - $tag_instance_object->timemodified = $tag_instance_object->timecreated; - $tag_instance_object->tiuserid = $userid; - - $tag_instance_object->id = $DB->insert_record('tag_instance', $tag_instance_object); - } - - // We can not fire an event with 'null' as the contextid. - if (is_null($contextid)) { - $contextid = context_system::instance()->id; - } - - // Trigger tag added event. - $event = \core\event\tag_added::create(array( - 'objectid' => $tag_instance_object->id, - 'contextid' => $contextid, - 'other' => array( - 'tagid' => $tagid, - 'tagname' => $tag->name, - 'tagrawname' => $tag->rawname, - 'itemid' => $record_id, - 'itemtype' => $record_type - ) - )); - $event->trigger(); - - return true; -} - -/** - * Clean up the tag tables, making sure all tagged object still exists. - * - * This should normally not be necessary, but in case related tags are not deleted when the tagged record is removed, this should be - * done once in a while, perhaps on an occasional cron run. On a site with lots of tags, this could become an expensive function to - * call: don't run at peak time. - * - * @package core_tag - * @access private - * @todo MDL-31212 Update tag cleanup sql so that it supports multiple types of tags - */ -function tag_cleanup() { - global $DB; - - // Get ids to delete from instances where the tag has been deleted. This should never happen apparently. - $sql = "SELECT ti.id - FROM {tag_instance} ti - LEFT JOIN {tag} t ON t.id = ti.tagid - WHERE t.id IS null"; - $tagids = $DB->get_records_sql($sql); - $tagarray = array(); - foreach ($tagids as $tagid) { - $tagarray[] = $tagid->id; - } - - // Next get ids from instances that have an owner that has been deleted. - $sql = "SELECT ti.id - FROM {tag_instance} ti, {user} u - WHERE ti.itemid = u.id - AND ti.itemtype = 'user' - AND u.deleted = 1"; - $tagids = $DB->get_records_sql($sql); - foreach ($tagids as $tagid) { - $tagarray[] = $tagid->id; - } - - // Get the other itemtypes. - $sql = "SELECT itemtype - FROM {tag_instance} - WHERE itemtype <> 'user' - GROUP BY itemtype"; - $tagitemtypes = $DB->get_records_sql($sql); - foreach ($tagitemtypes as $key => $notused) { - $sql = 'SELECT ti.id - FROM {tag_instance} ti - LEFT JOIN {' . $key . '} it ON it.id = ti.itemid - WHERE it.id IS null - AND ti.itemtype = \'' . $key . '\''; - $tagids = $DB->get_records_sql($sql); - foreach ($tagids as $tagid) { - $tagarray[] = $tagid->id; - } - } - - // Get instances for each of the ids to be deleted. - if (count($tagarray) > 0) { - list($sqlin, $params) = $DB->get_in_or_equal($tagarray); - $sql = "SELECT ti.*, COALESCE(t.name, 'deleted') AS name, COALESCE(t.rawname, 'deleted') AS rawname - FROM {tag_instance} ti - LEFT JOIN {tag} t ON t.id = ti.tagid - WHERE ti.id $sqlin"; - $instances = $DB->get_records_sql($sql, $params); - tag_bulk_delete_instances($instances); - } - - // TODO MDL-31212 this will only clean tags of type 'default'. This is good as - // it won't delete 'official' tags, but the day we get more than two - // types, we need to fix this. - $unused_tags = $DB->get_recordset_sql("SELECT tg.id - FROM {tag} tg - WHERE tg.tagtype = 'default' - AND NOT EXISTS ( - SELECT 'x' - FROM {tag_instance} ti - WHERE ti.tagid = tg.id - )"); - - // cleanup tags - foreach ($unused_tags as $unused_tag) { - tag_delete($unused_tag->id); - //debugging('deleting unused tag #'. $unused_tag->id, DEBUG_DEVELOPER); - } - $unused_tags->close(); -} - -/** - * This function will delete numerous tag instances efficiently. - * This removes tag instances only. It doesn't check to see if it is the last use of a tag. - * - * @param array $instances An array of tag instance objects with the addition of the tagname and tagrawname - * (used for recording a delete event). - */ -function tag_bulk_delete_instances($instances) { - global $DB; - - $instanceids = array(); - foreach ($instances as $instance) { - $instanceids[] = $instance->id; - } - - // This is a multi db compatible method of creating the correct sql when using the 'IN' value. - // $insql is the sql statement, $params are the id numbers. - list($insql, $params) = $DB->get_in_or_equal($instanceids); - $sql = 'id ' . $insql; - $DB->delete_records_select('tag_instance', $sql, $params); - - // Now go through and record each tag individually with the event system. - foreach ($instances as $instance) { - // Trigger tag removed event (i.e. The tag instance has been removed). - $event = \core\event\tag_removed::create(array( - 'objectid' => $instance->id, - 'contextid' => $instance->contextid, - 'other' => array( - 'tagid' => $instance->tagid, - 'tagname' => $instance->name, - 'tagrawname' => $instance->rawname, - 'itemid' => $instance->itemid, - 'itemtype' => $instance->itemtype - ) - )); - unset($instance->name); - unset($instance->rawname); - $event->add_record_snapshot('tag_instance', $instance); - $event->trigger(); - } -} - -/** - * Calculates and stores the correlated tags of all tags. The correlations are stored in the 'tag_correlation' table. - * - * Two tags are correlated if they appear together a lot. Ex.: Users tagged with "computers" will probably also be tagged with "algorithms". - * - * The rationale for the 'tag_correlation' table is performance. It works as a cache for a potentially heavy load query done at the - * 'tag_instance' table. So, the 'tag_correlation' table stores redundant information derived from the 'tag_instance' table. - * - * @package core_tag - * @access private - * @param int $mincorrelation Only tags with more than $mincorrelation correlations will be identified. - */ -function tag_compute_correlations($mincorrelation = 2) { - global $DB; - - // This mighty one line query fetches a row from the database for every - // individual tag correlation. We then need to process the rows collecting - // the correlations for each tag id. - // The fields used by this query are as follows: - // tagid : This is the tag id, there should be at least $mincorrelation - // rows for each tag id. - // correlation : This is the tag id that correlates to the above tagid field. - // correlationid : This is the id of the row in the tag_correlation table that - // relates to the tagid field and will be NULL if there are no - // existing correlations - $sql = 'SELECT pairs.tagid, pairs.correlation, pairs.ocurrences, co.id AS correlationid - FROM ( - SELECT ta.tagid, tb.tagid AS correlation, COUNT(*) AS ocurrences - FROM {tag_instance} ta - JOIN {tag_instance} tb ON (ta.itemtype = tb.itemtype AND ta.itemid = tb.itemid AND ta.tagid <> tb.tagid) - GROUP BY ta.tagid, tb.tagid - HAVING COUNT(*) > :mincorrelation - ) pairs - LEFT JOIN {tag_correlation} co ON co.tagid = pairs.tagid - ORDER BY pairs.tagid ASC, pairs.ocurrences DESC, pairs.correlation ASC'; - $rs = $DB->get_recordset_sql($sql, array('mincorrelation' => $mincorrelation)); - - // Set up an empty tag correlation object - $tagcorrelation = new stdClass; - $tagcorrelation->id = null; - $tagcorrelation->tagid = null; - $tagcorrelation->correlatedtags = array(); - - // We store each correlation id in this array so we can remove any correlations - // that no longer exist. - $correlations = array(); - - // Iterate each row of the result set and build them into tag correlations. - // We add all of a tag's correlations to $tagcorrelation->correlatedtags[] - // then save the $tagcorrelation object - foreach ($rs as $row) { - if ($row->tagid != $tagcorrelation->tagid) { - // The tag id has changed so we have all of the correlations for this tag - $tagcorrelationid = tag_process_computed_correlation($tagcorrelation); - if ($tagcorrelationid) { - $correlations[] = $tagcorrelationid; - } - // Now we reset the tag correlation object so we can reuse it and set it - // up for the current record. - $tagcorrelation = new stdClass; - $tagcorrelation->id = $row->correlationid; - $tagcorrelation->tagid = $row->tagid; - $tagcorrelation->correlatedtags = array(); - } - //Save the correlation on the tag correlation object - $tagcorrelation->correlatedtags[] = $row->correlation; - } - // Update the current correlation after the last record. - $tagcorrelationid = tag_process_computed_correlation($tagcorrelation); - if ($tagcorrelationid) { - $correlations[] = $tagcorrelationid; - } - - - // Close the recordset - $rs->close(); - - // Remove any correlations that weren't just identified - if (empty($correlations)) { - //there are no tag correlations - $DB->delete_records('tag_correlation'); - } else { - list($sql, $params) = $DB->get_in_or_equal($correlations, SQL_PARAMS_NAMED, 'param0000', false); - $DB->delete_records_select('tag_correlation', 'id '.$sql, $params); - } -} - -/** - * This function processes a tag correlation and makes changes in the database as required. - * - * The tag correlation object needs have both a tagid property and a correlatedtags property that is an array. - * - * @package core_tag - * @access private - * @param stdClass $tagcorrelation - * @return int/bool The id of the tag correlation that was just processed or false. - */ -function tag_process_computed_correlation(stdClass $tagcorrelation) { - global $DB; - - // You must provide a tagid and correlatedtags must be set and be an array - if (empty($tagcorrelation->tagid) || !isset($tagcorrelation->correlatedtags) || !is_array($tagcorrelation->correlatedtags)) { - return false; - } - - $tagcorrelation->correlatedtags = join(',', $tagcorrelation->correlatedtags); - if (!empty($tagcorrelation->id)) { - // The tag correlation already exists so update it - $DB->update_record('tag_correlation', $tagcorrelation); - } else { - // This is a new correlation to insert - $tagcorrelation->id = $DB->insert_record('tag_correlation', $tagcorrelation); - } - return $tagcorrelation->id; -} - -/** - * Tasks that should be performed at cron time - * - * @package core_tag - * @access private - */ -function tag_cron() { - tag_compute_correlations(); - tag_cleanup(); -} - -/** - * Search for tags with names that match some text - * - * @package core_tag - * @access private - * @param string $text escaped string that the tag names will be matched against - * @param bool $ordered If true, tags are ordered by their popularity. If false, no ordering. - * @param int/string $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point. - * @param int/string $limitnum (optional, required if $limitfrom is set) return a subset comprising this many records. - * @return array/boolean an array of objects, or false if no records were found or an error occured. - */ -function tag_find_tags($text, $ordered=true, $limitfrom='', $limitnum='') { - global $DB; - - $norm = tag_normalize($text, TAG_CASE_LOWER); - $text = array_shift($norm); - - if ($ordered) { - $query = "SELECT tg.id, tg.name, tg.rawname, COUNT(ti.id) AS count - FROM {tag} tg LEFT JOIN {tag_instance} ti ON tg.id = ti.tagid - WHERE tg.name LIKE ? - GROUP BY tg.id, tg.name, tg.rawname - ORDER BY count DESC"; - } else { - $query = "SELECT tg.id, tg.name, tg.rawname - FROM {tag} tg - WHERE tg.name LIKE ?"; - } - $params = array("%{$text}%"); - return $DB->get_records_sql($query, $params, $limitfrom , $limitnum); -} - -/** - * Get the name of a tag - * - * @package core_tag - * @access private - * @param mixed $tagids the id of the tag, or an array of ids - * @return mixed string name of one tag, or id-indexed array of strings - */ -function tag_get_name($tagids) { - global $DB; - - if (!is_array($tagids)) { - if ($tag = $DB->get_record('tag', array('id'=>$tagids))) { - return $tag->name; - } - return false; - } - - $tag_names = array(); - foreach($DB->get_records_list('tag', 'id', $tagids) as $tag) { - $tag_names[$tag->id] = $tag->name; - } - - return $tag_names; -} - -/** - * Returns the correlated tags of a tag, retrieved from the tag_correlation table. Make sure cron runs, otherwise the table will be - * empty and this function won't return anything. - * - * Correlated tags are calculated in cron based on existing tag instances. - * - * This function will return as many entries as there are existing tag instances, - * which means that there will be duplicates for each tag. - * - * If you need only one record for each correlated tag please call: - * tag_get_related_tags($tag_id, TAG_RELATED_CORRELATED); - * - * @package core_tag - * @access private - * @param int $tag_id is a single tag id - * @param int $notused this argument is no longer used - * @return array an array of tag objects or an empty if no correlated tags are found - */ -function tag_get_correlated($tag_id, $notused = null) { - global $DB; - - $tag_correlation = $DB->get_record('tag_correlation', array('tagid'=>$tag_id)); - - if (!$tag_correlation || empty($tag_correlation->correlatedtags)) { - return array(); - } - - // this is (and has to) return the same fields as the query in tag_get_tags - $sql = "SELECT ti.id AS taginstanceid, tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering - FROM {tag} tg - INNER JOIN {tag_instance} ti ON tg.id = ti.tagid - WHERE tg.id IN ({$tag_correlation->correlatedtags}) - ORDER BY ti.ordering ASC"; - return $DB->get_records_sql($sql); -} - -/** - * Function that normalizes a list of tag names. - * - * @package core_tag - * @access private - * @param array/string $rawtags array of tags, or a single tag. - * @param int $case case to use for returned value (default: lower case). Either TAG_CASE_LOWER (default) or TAG_CASE_ORIGINAL - * @return array lowercased normalized tags, indexed by the normalized tag, in the same order as the original array. - * (Eg: 'Banana' => 'banana'). - */ -function tag_normalize($rawtags, $case = TAG_CASE_LOWER) { - - // cache normalized tags, to prevent costly repeated calls to clean_param - static $cleaned_tags_lc = array(); // lower case - use for comparison - static $cleaned_tags_mc = array(); // mixed case - use for saving to database - - if ( !is_array($rawtags) ) { - $rawtags = array($rawtags); - } - - $result = array(); - foreach($rawtags as $rawtag) { - $rawtag = trim($rawtag); - if (!$rawtag) { - continue; - } - if ( !array_key_exists($rawtag, $cleaned_tags_lc) ) { - $cleaned_tags_lc[$rawtag] = core_text::strtolower( clean_param($rawtag, PARAM_TAG) ); - $cleaned_tags_mc[$rawtag] = clean_param($rawtag, PARAM_TAG); - } - if ( $case == TAG_CASE_LOWER ) { - $result[$rawtag] = $cleaned_tags_lc[$rawtag]; - } else { // TAG_CASE_ORIGINAL - $result[$rawtag] = $cleaned_tags_mc[$rawtag]; - } - } - - return $result; -} - -/** - * Count how many records are tagged with a specific tag. - * - * @package core_tag - * @access private - * @param string $record_type record to look for ('post', 'user', etc.) - * @param int $tagid is a single tag id - * @return int number of mathing tags. - */ -function tag_record_count($record_type, $tagid) { - global $DB; - return $DB->count_records('tag_instance', array('itemtype'=>$record_type, 'tagid'=>$tagid)); -} - -/** - * Determine if a record is tagged with a specific tag - * - * @package core_tag - * @access private - * @param string $record_type the record type to look for - * @param int $record_id the record id to look for - * @param string $tag a tag name - * @return bool/int true if it is tagged, 0 (false) otherwise - */ -function tag_record_tagged_with($record_type, $record_id, $tag) { - global $DB; - if ($tagid = tag_get_id($tag)) { - return $DB->count_records('tag_instance', array('itemtype'=>$record_type, 'itemid'=>$record_id, 'tagid'=>$tagid)); - } else { - return 0; // tag doesn't exist - } -} - -/** - * Flag a tag as inappropriate. - * - * @param int|array $tagids a single tagid, or an array of tagids - */ -function tag_set_flag($tagids) { - global $DB; - - $tagids = (array) $tagids; - - // Use the tagids to create a select statement to be used later. - list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids, SQL_PARAMS_NAMED); - - // Update all the tags to flagged. - $sql = "UPDATE {tag} - SET flag = flag + 1, timemodified = :time - WHERE id $tagsql"; - - // Update all the tags. - $DB->execute($sql, array_merge(array('time' => time()), $tagparams)); - - // Get all the tags. - if ($tags = $DB->get_records_select('tag', 'id '. $tagsql, $tagparams, 'id ASC')) { - // Loop through and fire an event for each tag that it was flagged. - foreach ($tags as $tag) { - $event = \core\event\tag_flagged::create(array( - 'objectid' => $tag->id, - 'relateduserid' => $tag->userid, - 'context' => context_system::instance(), - 'other' => array( - 'name' => $tag->name, - 'rawname' => $tag->rawname - ) - - )); - $event->add_record_snapshot('tag', $tag); - $event->trigger(); - } - } -} - -/** - * Remove the inappropriate flag on a tag. - * - * @param int|array $tagids a single tagid, or an array of tagids - */ -function tag_unset_flag($tagids) { - global $DB; - - $tagids = (array) $tagids; - - // Use the tagids to create a select statement to be used later. - list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids, SQL_PARAMS_NAMED); - - // Update all the tags to unflagged. - $sql = "UPDATE {tag} - SET flag = 0, timemodified = :time - WHERE id $tagsql"; - - // Update all the tags. - $DB->execute($sql, array_merge(array('time' => time()), $tagparams)); - - // Get all the tags. - if ($tags = $DB->get_records_select('tag', 'id '. $tagsql, $tagparams, 'id ASC')) { - // Loop through and fire an event for each tag that it was unflagged. - foreach ($tags as $tag) { - $event = \core\event\tag_unflagged::create(array( - 'objectid' => $tag->id, - 'relateduserid' => $tag->userid, - 'context' => context_system::instance(), - 'other' => array( - 'name' => $tag->name, - 'rawname' => $tag->rawname - ) - )); - $event->add_record_snapshot('tag', $tag); - $event->trigger(); - } - } -} - /** * Return a list of page types * * @package core_tag - * @access private * @param string $pagetype current page type * @param stdClass $parentcontext Block's parent context * @param stdClass $currentcontext Current context of block diff --git a/tag/locallib.php b/tag/locallib.php index 55cd31e278a..a56a8fb48e7 100644 --- a/tag/locallib.php +++ b/tag/locallib.php @@ -23,421 +23,5 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ -require_once($CFG->dirroot.'/tag/lib.php'); -require_once($CFG->libdir.'/filelib.php'); - -/** - * Prints or returns a HTML tag cloud with varying classes styles depending on the popularity and type of each tag. - * - * @package core_tag - * @access public - * @category tag - * @param array $tagset Array of tags to display - * @param int $nr_of_tags Limit for the number of tags to return/display, used if $tagset is null - * @param bool $return if true the function will return the generated tag cloud instead of displaying it. - * @param string $sort (optional) selected sorting, default is alpha sort (name) also timemodified or popularity - * @return string|null a HTML string or null if this function does the output - */ -function tag_print_cloud($tagset=null, $nr_of_tags=150, $return=false, $sort='') { - global $CFG, $DB; - - $can_manage_tags = has_capability('moodle/tag:manage', context_system::instance()); - - if (is_null($tagset)) { - // No tag set received, so fetch tags from database - if ( !$tagsincloud = $DB->get_records_sql('SELECT tg.rawname, tg.id, tg.name, tg.tagtype, COUNT(ti.id) AS count, tg.flag - FROM {tag_instance} ti JOIN {tag} tg ON tg.id = ti.tagid - WHERE ti.itemtype <> \'tag\' - GROUP BY tg.id, tg.rawname, tg.name, tg.flag, tg.tagtype - ORDER BY count DESC, tg.name ASC', null, 0, $nr_of_tags) ) { - $tagsincloud = array(); - } - } else { - $tagsincloud = $tagset; - } - - $tagkeys = array_keys($tagsincloud); - if (!empty($tagkeys)) { - $firsttagkey = $tagkeys[0]; - $maxcount = $tagsincloud[$firsttagkey]->count; - } - - $etags = array(); - - foreach ($tagsincloud as $tag) { - $size = (int) (( $tag->count / $maxcount) * 20); - $tag->class = "$tag->tagtype s$size"; - $etags[] = $tag; - } - - // Set up sort global - used to pass sort type into tag_cloud_sort through usort() avoiding multiple sort functions. - // TODO make calling functions pass 'count' or 'timemodified' not 'popularity' or 'date'. - $oldsort = empty($CFG->tagsort) ? null : $CFG->tagsort; - if ($sort == 'popularity') { - $CFG->tagsort = 'count'; - } else if ($sort == 'date') { - $CFG->tagsort = 'timemodified'; - } else { - $CFG->tagsort = 'name'; - } - usort($etags, "tag_cloud_sort"); - $CFG->tagsort = $oldsort; - - $output = ''; - $output .= "\n
      \n"; - foreach ($etags as $tag) { - if ($tag->flag > 0 && $can_manage_tags) { - $tagname = ''. tag_display_name($tag) .''; - } else { - $tagname = tag_display_name($tag); - } - - $link = $CFG->wwwroot .'/tag/index.php?tag='. rawurlencode($tag->name); - $output .= '
    • '. - $tagname .'
    • '; - } - $output .= "\n
    \n"; - - if ($return) { - return $output; - } else { - echo $output; - } -} - -/** - * This function is used by print_tag_cloud, to usort() the tags in the cloud. See php.net/usort for the parameters documentation. - * This was originally in blocks/blog_tags/block_blog_tags.php, named blog_tags_sort(). - * - * @package core_tag - * @access private - * @param string $a Tag name to compare against $b - * @param string $b Tag name to compare against $a - * @return int The result of the comparison/validation 1, 0 or -1 - */ -function tag_cloud_sort($a, $b) { - global $CFG; - - if (empty($CFG->tagsort)) { - $tagsort = 'name'; // by default, sort by name - } else { - $tagsort = $CFG->tagsort; - } - - if (is_numeric($a->$tagsort)) { - return ($a->$tagsort == $b->$tagsort) ? 0 : ($a->$tagsort > $b->$tagsort) ? 1 : -1; - } elseif (is_string($a->$tagsort)) { - return strcmp($a->$tagsort, $b->$tagsort); - } else { - return 0; - } -} - -/** - * Prints a box with the description of a tag and its related tags - * - * @package core_tag - * @access public - * @todo MDL-31149 create a system setting for $max_tags_displayed, instead of using an in code literal - * @param stdClass $tag_object - * @param bool $return if true the function will return the generated tag cloud instead of displaying it. - * @return string/null a HTML box showing a description of the tag object and it's relationsips or null if output is done directly - * in the function. - */ -function tag_print_description_box($tag_object, $return=false) { - - global $USER, $CFG, $OUTPUT; - - $max_tags_displayed = 10; - - $tagname = tag_display_name($tag_object); - $related_tags = tag_get_related_tags($tag_object->id, TAG_RELATED_ALL, $max_tags_displayed+1); // this gets one more than we want - - $content = !empty($tag_object->description) || $related_tags; - $output = ''; - - if ($content) { - $output .= $OUTPUT->box_start('generalbox', 'tag-description'); - } - - if (!empty($tag_object->description)) { - $options = new stdClass(); - $options->para = false; - $options->overflowdiv = true; - $tag_object->description = file_rewrite_pluginfile_urls($tag_object->description, 'pluginfile.php', context_system::instance()->id, 'tag', 'description', $tag_object->id); - $output .= format_text($tag_object->description, $tag_object->descriptionformat, $options); - } - - if ($related_tags) { - $more_links = false; - if (count($related_tags) > $max_tags_displayed) { - array_pop($related_tags); - $more_links = true; - } - $output .= '

    '. get_string('relatedtags', 'tag') .': '. tag_get_related_tags_csv($related_tags); - if ($more_links) { - $output .= ' ...'; - } - } - - if ($content) { - $output .= $OUTPUT->box_end(); - } - - if ($return) { - return $output; - } else { - echo $output; - } -} - -/** - * Prints a box that contains the management links of a tag - * - * @access public - * @param stdClass $tag_object - * @param bool $return if true the function will return the generated tag cloud instead of displaying it. - * @return string|null a HTML string or null if this function does the output - */ -function tag_print_management_box($tag_object, $return=false) { - - global $USER, $CFG, $OUTPUT; - - $tagname = tag_display_name($tag_object); - $output = ''; - - if (!isguestuser()) { - $output .= $OUTPUT->box_start('box','tag-management-box'); - $systemcontext = context_system::instance(); - $links = array(); - - // Add a link for users to add/remove this from their interests - if (tag_record_tagged_with('user', $USER->id, $tag_object->name)) { - $links[] = ''. get_string('removetagfrommyinterests', 'tag', $tagname) .''; - } else { - $links[] = ''. get_string('addtagtomyinterests', 'tag', $tagname) .''; - } - - // Flag as inappropriate link. Only people with moodle/tag:flag capability. - if (has_capability('moodle/tag:flag', $systemcontext)) { - $links[] = ''. get_string('flagasinappropriate', 'tag', rawurlencode($tagname)) .''; - } - - // Edit tag: Only people with moodle/tag:edit capability who either have it as an interest or can manage tags - if (has_capability('moodle/tag:edit', $systemcontext) || - has_capability('moodle/tag:manage', $systemcontext)) { - $links[] = ''. get_string('edittag', 'tag') .''; - } - - $output .= implode(' | ', $links); - $output .= $OUTPUT->box_end(); - } - - if ($return) { - return $output; - } else { - echo $output; - } -} - -/** - * Prints the tag search box - * - * @access public - * @param bool $return if true return html string - * @return string|null a HTML string or null if this function does the output - */ -function tag_print_search_box($return=false) { - global $CFG, $OUTPUT; - - $output = $OUTPUT->box_start('','tag-search-box'); - $output .= '
    '; - $output .= '
    '; - $output .= ''; - $output .= ''; - $output .= '
    '; - $output .= '
    '; - $output .= ''; - $output .= $OUTPUT->box_end(); - - if ($return) { - return $output; - } - else { - echo $output; - } -} - -/** - * Prints the tag search results - * - * @access public - * @param string $query text that tag names will be matched against - * @param int $page current page - * @param int $perpage nr of users displayed per page - * @param bool $return if true return html string - * @return string|null a HTML string or null if this function does the output - */ -function tag_print_search_results($query, $page, $perpage, $return=false) { - - global $CFG, $USER, $OUTPUT; - - $norm = tag_normalize($query, TAG_CASE_ORIGINAL); - $query = array_shift($norm); - - $count = sizeof(tag_find_tags($query, false)); - $tags = array(); - - if ( $found_tags = tag_find_tags($query, true, $page * $perpage, $perpage) ) { - $tags = array_values($found_tags); - } - - $baseurl = $CFG->wwwroot.'/tag/search.php?query='. rawurlencode($query); - $output = ''; - - // link "Add $query to my interests" - $addtaglink = ''; - if( !tag_record_tagged_with('user', $USER->id, $query) ) { - $addtaglink = ''; - $addtaglink .= get_string('addtagtomyinterests', 'tag', htmlspecialchars($query)) .''; - } - - if ( !empty($tags) ) { // there are results to display!! - $output .= $OUTPUT->heading(get_string('searchresultsfor', 'tag', htmlspecialchars($query)) ." : {$count}", 3, 'main'); - - //print a link "Add $query to my interests" - if (!empty($addtaglink)) { - $output .= $OUTPUT->box($addtaglink, 'box', 'tag-management-box'); - } - - $nr_of_lis_per_ul = 6; - $nr_of_uls = ceil( sizeof($tags) / $nr_of_lis_per_ul ); - - $output .= '
      '; - for($i = 0; $i < $nr_of_uls; $i++) { - $output .= '
    • '; - foreach (array_slice($tags, $i * $nr_of_lis_per_ul, $nr_of_lis_per_ul) as $tag) { - $tag_link = ' '. tag_display_name($tag) .''; - $output .= '•'. $tag_link .'
      '; - } - $output .= '
    • '; - } - $output .= '
    '; - $output .= '
     
    '; // <-- small layout hack in order to look good in Firefox - - $output .= $OUTPUT->paging_bar($count, $page, $perpage, $baseurl); - } - else { //no results were found!! - $output .= $OUTPUT->heading(get_string('noresultsfor', 'tag', htmlspecialchars($query)), 3, 'main'); - - //print a link "Add $query to my interests" - if (!empty($addtaglink)) { - $output .= $OUTPUT->box($addtaglink, 'box', 'tag-management-box'); - } - } - - if ($return) { - return $output; - } - else { - echo $output; - } -} - -/** - * Prints a table of the users tagged with the tag passed as argument - * - * @param int $tag_object the tag we wish to return data for - * @param int $limitfrom (optional, required if $limitnum is set) prints users starting at this point. - * @param int $limitnum (optional, required if $limitfrom is set) prints this many users. - * @param bool $return if true return html string - * @return string|null a HTML string or null if this function does the output - */ -function tag_print_tagged_users_table($tag_object, $limitfrom='', $limitnum='', $return=false) { - - //List of users with this tag - $userlist = tag_find_records($tag_object->name, 'user', $limitfrom, $limitnum); - - $output = tag_print_user_list($userlist, true); - - if ($return) { - return $output; - } - else { - echo $output; - } -} - -/** - * Prints an individual user box - * - * @param user_object $user (contains the following fields: id, firstname, lastname and picture) - * @param bool $return if true return html string - * @return string|null a HTML string or null if this function does the output - */ -function tag_print_user_box($user, $return=false) { - global $CFG, $OUTPUT; - - $usercontext = context_user::instance($user->id); - $profilelink = ''; - - if ($usercontext and (has_capability('moodle/user:viewdetails', $usercontext) || has_coursecontact_role($user->id))) { - $profilelink = $CFG->wwwroot .'/user/view.php?id='. $user->id; - } - - $output = $OUTPUT->box_start('user-box', 'user'. $user->id); - $fullname = fullname($user); - $alt = ''; - - if (!empty($profilelink)) { - $output .= ''; - $alt = $fullname; - } - - $output .= $OUTPUT->user_picture($user, array('size'=>100)); - $output .= '
    '; - - if (!empty($profilelink)) { - $output .= '
    '; - } - - //truncate name if it's too big - if (core_text::strlen($fullname) > 26) { - $fullname = core_text::substr($fullname, 0, 26) .'...'; - } - - $output .= ''. $fullname .''; - $output .= $OUTPUT->box_end(); - - if ($return) { - return $output; - } - else { - echo $output; - } -} - -/** - * Prints a list of users - * - * @param array $userlist an array of user objects - * @param bool $return if true return html string, otherwise output the result - * @return string|null a HTML string or null if this function does the output - */ -function tag_print_user_list($userlist, $return=false) { - - $output = '
      '; - - foreach ($userlist as $user){ - $output .= '
    • '. tag_print_user_box($user, true) ."
    • \n"; - } - $output .= "
    \n"; - - if ($return) { - return $output; - } - else { - echo $output; - } -} +debugging('All functions from /tags/locallib.php were deprecated and it will be removed soon, '. + 'do not include this file in your code', DEBUG_DEVELOPER); diff --git a/tag/manage.php b/tag/manage.php index d83aeca8918..3deb5c4e75c 100644 --- a/tag/manage.php +++ b/tag/manage.php @@ -1,5 +1,4 @@ . /** - * @package core - * @subpackage tag + * Managing tags, tag areas and tags collections + * + * @package core_tag * @copyright 2007 Luiz Cruz * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -37,12 +37,8 @@ $action = optional_param('action', '', PARAM_ALPHA); $perpage = optional_param('perpage', DEFAULT_PAGE_SIZE, PARAM_INT); $page = optional_param('page', 0, PARAM_INT); $notice = optional_param('notice', '', PARAM_ALPHA); - -require_login(); - -if (empty($CFG->usetags)) { - print_error('tagsaredisabled', 'tag'); -} +$tagcollid = optional_param('tc', 0, PARAM_INT); +$tagareaid = optional_param('ta', null, PARAM_INT); $params = array(); if ($perpage != DEFAULT_PAGE_SIZE) { @@ -51,60 +47,168 @@ if ($perpage != DEFAULT_PAGE_SIZE) { if ($page > 0) { $params['page'] = $page; } + admin_externalpage_setup('managetags', '', $params, '', array('pagelayout' => 'report')); +if (empty($CFG->usetags)) { + print_error('tagsaredisabled', 'tag'); +} + +$tagobject = null; +if ($tagid) { + $tagobject = core_tag_tag::get($tagid, '*', MUST_EXIST); + $tagcollid = $tagobject->tagcollid; +} +$tagcoll = core_tag_collection::get_by_id($tagcollid); +$tagarea = core_tag_area::get_by_id($tagareaid); +$manageurl = new moodle_url('/tag/manage.php'); +if ($tagcoll) { + // We are inside a tag collection - add it to the page url and the breadcrumb. + $PAGE->set_url(new moodle_url($PAGE->url, array('tc' => $tagcoll->id))); + $PAGE->navbar->add(core_tag_collection::display_name($tagcoll), + new moodle_url($manageurl, array('tc' => $tagcoll->id))); +} + $PAGE->set_blocks_editing_capability('moodle/tag:editblocks'); switch($action) { + case 'colladd': + case 'colledit': + if ($action === 'colladd' || ($action === 'colledit' && $tagcoll && empty($tagcoll->component))) { + $form = new core_tag_collection_form($manageurl, $tagcoll); + if ($form->is_cancelled()) { + redirect($manageurl); + } else if ($data = $form->get_data()) { + if ($action === 'colladd') { + core_tag_collection::create($data); + } else { + core_tag_collection::update($tagcoll, $data); + } + redirect($manageurl); + } else { + $title = ($action === 'colladd') ? + get_string('addtagcoll', 'tag') : + get_string('edittagcoll', 'tag', core_tag_collection::display_name($tagcoll)); + $PAGE->navbar->add($title); + echo $OUTPUT->header(); + echo $OUTPUT->heading($title, 2); + $form->display(); + echo $OUTPUT->footer(); + exit; + } + } + break; + + case 'colldelete': + $confirm = optional_param('confirm', false, PARAM_BOOL); + if (!$confirm) { + echo $OUTPUT->header(); + $strconfirm = get_string('suredeletecoll', 'tag', core_tag_collection::display_name($tagcoll)); + $params = array('tc' => $tagcoll->id, 'confirm' => 1, 'sesskey' => sesskey(), 'action' => 'colldelete'); + $formcontinue = new single_button(new moodle_url($manageurl, $params), get_string('yes')); + $formcancel = new single_button($manageurl, get_string('no'), 'get'); + echo $OUTPUT->confirm($strconfirm, $formcontinue, $formcancel); + echo $OUTPUT->footer(); + die; + } + if ($tagcoll && !$tagcoll->component) { + require_sesskey(); + core_tag_collection::delete($tagcoll); + redirect(new moodle_url($manageurl, array('notice' => 'changessaved'))); + } + redirect($manageurl); + break; + + case 'collmoveup': + if ($tagcoll) { + require_sesskey(); + core_tag_collection::change_sortorder($tagcoll, -1); + redirect(new moodle_url($manageurl, array('notice' => 'changessaved'))); + } + redirect($manageurl); + break; + + case 'collmovedown': + if ($tagcoll) { + require_sesskey(); + core_tag_collection::change_sortorder($tagcoll, 1); + redirect(new moodle_url($manageurl, array('notice' => 'changessaved'))); + } + redirect($manageurl); + break; + + case 'areaenable': + case 'areadisable': + if ($tagarea) { + require_sesskey(); + $data = array('enabled' => ($action === 'areaenable') ? 1 : 0); + core_tag_area::update($tagarea, $data); + redirect(new moodle_url($manageurl, array('notice' => 'changessaved'))); + } + redirect($manageurl); + break; + + case 'areasetcoll': + if ($tagarea) { + require_sesskey(); + if ($newtagcollid = optional_param('areacollid', null, PARAM_INT)) { + core_tag_area::update($tagarea, array('tagcollid' => $newtagcollid)); + redirect(new moodle_url($manageurl, array('notice' => 'changessaved'))); + } + } + redirect($manageurl); + break; + case 'delete': require_sesskey(); if (!$tagschecked && $tagid) { $tagschecked = array($tagid); } - tag_delete($tagschecked); - redirect(new moodle_url($PAGE->url, array('notice' => 'deleted'))); + core_tag_tag::delete_tags($tagschecked); + redirect(new moodle_url($PAGE->url, $tagschecked ? array('notice' => 'deleted') : null)); break; case 'setflag': require_sesskey(); - tag_set_flag($tagid); - redirect(new moodle_url($PAGE->url, array('notice' => 'flagged'))); + if ($tagid) { + $tagobject->flag(); + redirect(new moodle_url($PAGE->url, array('notice' => 'flagged'))); + } + redirect($PAGE->url); break; case 'resetflag': require_sesskey(); - tag_unset_flag($tagid); - redirect(new moodle_url($PAGE->url, array('notice' => 'resetflag'))); + if ($tagid) { + $tagobject->reset_flag(); + redirect(new moodle_url($PAGE->url, array('notice' => 'resetflag'))); + } + redirect($PAGE->url); break; case 'changetype': require_sesskey(); - if ($tagtype === 'official' || $tagtype === 'default') { - if (tag_type_set($tagid, $tagtype)) { - redirect(new moodle_url($PAGE->url, array('notice' => 'typechanged'))); - } + if ($tagid && $tagobject->update(array('tagtype' => $tagtype))) { + redirect(new moodle_url($PAGE->url, array('notice' => 'typechanged'))); } redirect($PAGE->url); break; case 'addofficialtag': require_sesskey(); - $otagsadd = optional_param('otagsadd', '', PARAM_RAW); - $newtags = preg_split('/\s*,\s*/', trim($otagsadd), -1, PREG_SPLIT_NO_EMPTY); - $newtags = array_filter(tag_normalize($newtags, TAG_CASE_ORIGINAL)); - if (!$newtags) { - redirect($PAGE->url); + $tagobjects = null; + if ($tagcoll) { + $otagsadd = optional_param('otagsadd', '', PARAM_RAW); + $newtags = preg_split('/\s*,\s*/', trim($otagsadd), -1, PREG_SPLIT_NO_EMPTY); + $tagobjects = core_tag_tag::create_if_missing($tagcoll->id, $newtags, true); } - foreach ($newtags as $newotag) { - if ($newotagid = tag_get_id($newotag) ) { - // Tag exists, change the type. - tag_type_set($newotagid, 'official'); - } else { - tag_add($newotag, 'official'); + foreach ($tagobjects as $tagobject) { + if ($tagobject->tagtype !== 'official') { + $tagobject->update(array('tagtype' => 'official')); } } - redirect(new moodle_url($PAGE->url, array('notice' => 'added'))); + redirect(new moodle_url($PAGE->url, $tagobjects ? array('notice' => 'added') : null)); break; } @@ -114,8 +218,28 @@ if ($notice && get_string_manager()->string_exists($notice, 'tag')) { echo $OUTPUT->notification(get_string($notice, 'tag'), 'notifysuccess'); } +if (!$tagcoll) { + // Tag collection is not specified. Display the overview of tag collections and tag areas. + $tagareastable = new core_tag_areas_table($manageurl); + $colltable = new core_tag_collections_table($manageurl); + + echo $OUTPUT->heading(get_string('tagcollections', 'core_tag'), 3); + echo html_writer::table($colltable); + $url = new moodle_url($manageurl, array('action' => 'colladd')); + echo html_writer::div(html_writer::link($url, get_string('addtagcoll', 'tag')), 'mdl-right addtagcoll'); + + echo $OUTPUT->heading(get_string('tagareas', 'core_tag'), 3); + echo html_writer::table($tagareastable); + + echo $OUTPUT->footer(); + exit; +} + +// Tag collection is specified. Manage tags in this collection. + // Small form to add an official tag. print('
    '); +print(''); print(''); print(''); print(''); @@ -128,18 +252,21 @@ print('