Merge branch 'install_master' of https://git.in.moodle.com/amosbot/moodle-install
authorDan Poltawski <dan@moodle.com>
Thu, 14 Jan 2016 10:09:52 +0000 (10:09 +0000)
committerDan Poltawski <dan@moodle.com>
Thu, 14 Jan 2016 10:09:52 +0000 (10:09 +0000)
225 files changed:
Gruntfile.js
admin/settings/courses.php
admin/tool/langimport/index.php
admin/tool/uploadcourse/classes/course.php
admin/tool/uploadcourse/tests/course_test.php
admin/tool/uploaduser/index.php
admin/webservice/testclient.php
backup/moodle2/restore_stepslib.php
backup/util/dbops/restore_dbops.class.php
blocks/blog_menu/tests/behat/block_blog_menu.feature [new file with mode: 0644]
blocks/blog_menu/tests/behat/block_blog_menu_course.feature [new file with mode: 0644]
blocks/blog_menu/tests/behat/block_blog_menu_frontpage.feature [new file with mode: 0644]
blocks/blog_tags/block_blog_tags.php
blocks/moodleblock.class.php
blocks/news_items/block_news_items.php
blocks/tag_flickr/block_tag_flickr.php
blocks/tag_flickr/edit_form.php
blocks/tag_youtube/block_tag_youtube.php
blocks/tags/backup/moodle2/restore_tags_block_task.class.php [new file with mode: 0644]
blocks/tags/block_tags.php
blocks/tags/edit_form.php
blocks/tags/lang/en/block_tags.php
blocks/tags/tests/behat/tagcloud.feature
blog/edit.php
blog/edit_form.php
blog/external_blog_edit.php
blog/external_blog_edit_form.php
blog/index.php
blog/lib.php
blog/locallib.php
blog/renderer.php
blog/rsslib.php
blog/tests/lib_test.php
calendar/lib.php
course/edit.php
course/edit_form.php
course/lib.php
course/renderer.php
course/tags.php
course/tags_form.php
course/tests/courselib_test.php
course/tests/externallib_test.php
enrol/manual/yui/quickenrolment/quickenrolment.js
enrol/paypal/ipn.php
enrol/tests/behat/manage_enrolments_from_participants.feature [new file with mode: 0644]
grade/grading/form/guide/amd/build/comment_chooser.min.js [new file with mode: 0644]
grade/grading/form/guide/amd/src/comment_chooser.js [new file with mode: 0644]
grade/grading/form/guide/edit_form.php
grade/grading/form/guide/guideeditor.php
grade/grading/form/guide/js/guideeditor.js
grade/grading/form/guide/lang/en/gradingform_guide.php
grade/grading/form/guide/lib.php
grade/grading/form/guide/renderer.php
grade/grading/form/guide/templates/comment_chooser.mustache [new file with mode: 0644]
grade/grading/form/guide/tests/behat/behat_gradingform_guide.php [new file with mode: 0644]
grade/grading/form/guide/tests/behat/edit_guide.feature [new file with mode: 0644]
grade/report/grader/lib.php
lang/en/backup.php
lang/en/cache.php
lang/en/deprecated.txt
lang/en/moodle.php
lang/en/question.php
lang/en/tag.php
lang/en/webservice.php
lib/adminlib.php
lib/amd/build/tag.min.js
lib/amd/src/tag.js
lib/bennu/readme_moodle.txt
lib/blocklib.php
lib/classes/event/tag_added.php
lib/classes/event/tag_collection_created.php [new file with mode: 0644]
lib/classes/event/tag_collection_deleted.php [new file with mode: 0644]
lib/classes/event/tag_collection_updated.php [new file with mode: 0644]
lib/classes/event/tag_created.php
lib/classes/event/tag_removed.php
lib/classes/plugin_manager.php
lib/classes/plugininfo/cachestore.php
lib/classes/string_manager_standard.php
lib/classes/task/tag_cron_task.php
lib/coursecatlib.php
lib/db/caches.php
lib/db/install.xml
lib/db/services.php
lib/db/tag.php [new file with mode: 0644]
lib/db/tasks.php
lib/db/upgrade.php
lib/ddl/mysql_sql_generator.php
lib/deprecatedlib.php
lib/dml/moodle_database.php
lib/form/tags.php
lib/modinfolib.php
lib/moodlelib.php
lib/myprofilelib.php
lib/navigationlib.php
lib/outputrenderers.php
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/tests/advanced_test.php
lib/questionlib.php
lib/testing/generator/data_generator.php
lib/testing/tests/generator_test.php
lib/tests/questionlib_test.php
lib/upgrade.txt
lib/upgradelib.php
local/upgrade.txt
login/token.php
message/upgrade.txt
mod/assign/feedback/editpdf/tests/behat/annotate_pdf.feature
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/chat/lib.php
mod/chat/tests/behat/chat_course_reset.feature [new file with mode: 0644]
mod/forum/backup/moodle2/backup_forum_stepslib.php
mod/forum/classes/event/discussion_pinned.php [new file with mode: 0644]
mod/forum/classes/event/discussion_unpinned.php [new file with mode: 0644]
mod/forum/classes/output/forum_post.php
mod/forum/classes/post_form.php
mod/forum/db/access.php
mod/forum/db/install.xml [changed mode: 0644->0755]
mod/forum/db/log.php
mod/forum/db/upgrade.php
mod/forum/discuss.php
mod/forum/externallib.php
mod/forum/lang/en/deprecated.txt
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/pix/i/pinned.png [new file with mode: 0644]
mod/forum/pix/i/pinned.svg [new file with mode: 0644]
mod/forum/post.php
mod/forum/styles.css
mod/forum/templates/forum_post_emaildigestbasic_htmlemail.mustache
mod/forum/tests/behat/discussion_navigation.feature
mod/forum/tests/behat/move_discussion.feature
mod/forum/tests/behat/posts_ordering_blog.feature
mod/forum/tests/behat/posts_ordering_general.feature
mod/forum/tests/externallib_test.php
mod/forum/tests/generator/lib.php
mod/forum/tests/generator_test.php
mod/forum/tests/lib_test.php
mod/forum/version.php
mod/forum/view.php
mod/wiki/backup/moodle2/backup_wiki_stepslib.php
mod/wiki/backup/moodle2/restore_wiki_stepslib.php
mod/wiki/db/tag.php [moved from webservice/amf/lang/en/webservice_amf.php with 68% similarity]
mod/wiki/edit_form.php
mod/wiki/lang/en/wiki.php
mod/wiki/lib.php
mod/wiki/locallib.php
mod/wiki/pagelib.php
mod/wiki/styles.css
mod/wiki/tests/behat/edit_tags.feature
mod/wiki/version.php
question/format.php
question/format/xml/format.php
question/format/xml/tests/xmlformat_test.php
question/question.php
question/type/edit_question_form.php
repository/s3/lib.php
tag/classes/area.php [new file with mode: 0644]
tag/classes/areas_table.php [new file with mode: 0644]
tag/classes/collection.php [new file with mode: 0644]
tag/classes/collection_form.php [new file with mode: 0644]
tag/classes/collections_table.php [new file with mode: 0644]
tag/classes/external.php
tag/classes/manage_table.php
tag/classes/output/tag.php
tag/classes/output/tagcloud.php [new file with mode: 0644]
tag/classes/output/tagfeed.php [new file with mode: 0644]
tag/classes/output/tagindex.php [new file with mode: 0644]
tag/classes/output/taglist.php [new file with mode: 0644]
tag/classes/renderer.php [new file with mode: 0644]
tag/classes/tag.php [new file with mode: 0644]
tag/edit.php
tag/edit_form.php
tag/index.php
tag/lib.php
tag/locallib.php
tag/manage.php
tag/search.php
tag/templates/index.mustache [new file with mode: 0644]
tag/templates/tagcloud.mustache [new file with mode: 0644]
tag/templates/tagfeed.mustache [new file with mode: 0644]
tag/templates/taglist.mustache [new file with mode: 0644]
tag/tests/behat/collections.feature [new file with mode: 0644]
tag/tests/behat/delete_tag.feature
tag/tests/behat/edit_tag.feature
tag/tests/behat/flag_tags.feature
tag/tests/behat/official_tags.feature
tag/tests/behat/tagindex.feature [new file with mode: 0644]
tag/tests/events_test.php
tag/tests/external_test.php
tag/tests/taglib_test.php
tag/upgrade.txt
tag/user.php
theme/base/style/core.css
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/style/moodle.css
theme/canvas/style/core.css
user/edit.php
user/editadvanced.php
user/editlib.php
user/externallib.php
user/index.php
user/lib.php
user/profile.php
user/renderer.php
user/tests/externallib_test.php
user/view.php
version.php
webservice/amf/db/access.php [deleted file]
webservice/amf/introspector.php [deleted file]
webservice/amf/locallib.php [deleted file]
webservice/amf/server.php [deleted file]
webservice/amf/simpleserver.php [deleted file]
webservice/amf/testclient/AMFTester.mxml [deleted file]
webservice/amf/testclient/AMFTester.swf [deleted file]
webservice/amf/testclient/customValidators/JSONValidator.as [deleted file]
webservice/amf/testclient/flashcompilationinstructions.txt [deleted file]
webservice/amf/testclient/index.php [deleted file]
webservice/amf/version.php [deleted file]
webservice/externallib.php
webservice/lib.php
webservice/tests/externallib_test.php
webservice/upgrade.txt

index 665bb44..3dd3ef0 100644 (file)
@@ -26,13 +26,14 @@ module.exports = function(grunt) {
     var path = require('path'),
         fs = require('fs'),
         tasks = {},
-        cwd = process.env.PWD || process.cwd();
+        cwd = process.env.PWD || process.cwd(),
+        inAMD = path.basename(cwd) == 'amd';
 
     // Project configuration.
     grunt.initConfig({
         jshint: {
             options: {jshintrc: '.jshintrc'},
-            files: ['**/amd/src/*.js']
+            files: [inAMD ? cwd + '/src/*.js' : '**/amd/src/*.js']
         },
         uglify: {
             dynamic_mappings: {
@@ -222,7 +223,7 @@ module.exports = function(grunt) {
         if (path.basename(path.resolve(cwd, '../../')) == 'yui') {
             grunt.task.run('shifter');
         // Are we in an AMD directory?
-        } else if (path.basename(cwd) == 'amd') {
+        } else if (inAMD) {
             grunt.task.run('amd');
         } else {
             // Run them all!.
index bd88630..21ecab7 100644 (file)
@@ -205,6 +205,9 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     // Create a page for general import configuration and defaults.
     $temp = new admin_settingpage('importgeneralsettings', new lang_string('importgeneralsettings', 'backup'), 'moodle/backup:backupcourse');
     $temp->add(new admin_setting_configtext('backup/import_general_maxresults', new lang_string('importgeneralmaxresults', 'backup'), new lang_string('importgeneralmaxresults_desc', 'backup'), 10));
+    $temp->add(new admin_setting_configcheckbox('backup/import_general_duplicate_admin_allowed',
+            new lang_string('importgeneralduplicateadminallowed', 'backup'),
+            new lang_string('importgeneralduplicateadminallowed_desc', 'backup'), 0));
     $ADMIN->add('backups', $temp);
 
     // Create a page for automated backups configuration and defaults.
index c1ee2fb..6017717 100644 (file)
@@ -188,7 +188,7 @@ echo html_writer::end_tag('td');
 $options = array();
 foreach ($availablelangs as $alang) {
     if (!empty($alang[0]) and trim($alang[0]) !== 'en' and !$controller->is_installed_lang($alang[0], $alang[1])) {
-        $options[$alang[0]] = $alang[2].' ('.$alang[0].')';
+        $options[$alang[0]] = $alang[2].' &lrm;('.$alang[0].')&lrm;';
     }
 }
 if (!empty($options)) {
index ff22833..4486cde 100644 (file)
@@ -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().
index a173e72..20bbaa3 100644 (file)
@@ -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();
index e687fe3..2b3f43e 100644 (file)
@@ -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) {
index 25e4070..d14dbb9 100644 (file)
@@ -95,10 +95,6 @@ if (!$function or !$protocol) {
     $descparams = new stdClass();
     $descparams->atag = $atag;
     $descparams->mode = get_string('debugnormal', 'admin');
-    $amfclienturl = new moodle_url('/webservice/amf/testclient/index.php');
-    $amfclientatag =html_writer::tag('a', get_string('amftestclient', 'webservice'),
-            array('href' => $amfclienturl));
-    $descparams->amfatag = $amfclientatag;
     echo get_string('testclientdescription', 'webservice', $descparams);
     echo $OUTPUT->box_end();
 
index a41292d..caf5f07 100644 (file)
@@ -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);
         }
     }
 
index 880e75d..120ea6c 100644 (file)
@@ -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
@@ -1287,7 +1285,11 @@ abstract class restore_dbops {
     *      1F - None of the above, return true => User needs to be created
     *
     *  if restoring from another site backup (cannot match by id here, replace it by email/firstaccess combination):
-    *      2A - Normal check: If match by username and mnethost and (email or non-zero firstaccess) => ok, return target user
+    *      2A - Normal check:
+    *           2A1 - If match by username and mnethost and (email or non-zero firstaccess) => ok, return target user
+    *           2A2 - Exceptional handling (MDL-21912): Match "admin" username. Then, if import_general_duplicate_admin_allowed is
+    *                 enabled, attempt to map the admin user to the user 'admin_[oldsiteid]' if it exists. If not,
+    *                 the user 'admin_[oldsiteid]' will be created in precheck_included users
     *      2B - Handle users deleted in DB and "alive" in backup file:
     *           2B1 - If match by mnethost and user is deleted in DB and not empty email = md5(username) and
     *                 (username LIKE 'backup_email.%' or non-zero firstaccess) => ok, return target user
@@ -1305,7 +1307,7 @@ abstract class restore_dbops {
     * Note: for DB deleted users md5(username) is stored *sometimes* in the email field,
     *       hence we are looking there for usernames if not empty. See delete_user()
     */
-    protected static function precheck_user($user, $samesite) {
+    protected static function precheck_user($user, $samesite, $siteid = null) {
         global $CFG, $DB;
 
         // Handle checks from same site backups
@@ -1376,7 +1378,7 @@ abstract class restore_dbops {
         // Handle checks from different site backups
         } else {
 
-            // 2A - If match by username and mnethost and
+            // 2A1 - If match by username and mnethost and
             //     (email or non-zero firstaccess) => ok, return target user
             if ($rec = $DB->get_record_sql("SELECT *
                                               FROM {user} u
@@ -1393,6 +1395,14 @@ abstract class restore_dbops {
                 return $rec; // Matching user found, return it
             }
 
+            // 2A2 - If we're allowing conflicting admins, attempt to map user to admin_[oldsiteid].
+            if (get_config('backup', 'import_general_duplicate_admin_allowed') && $user->username === 'admin' && $siteid
+                    && $user->mnethostid == $CFG->mnet_localhost_id) {
+                if ($rec = $DB->get_record('user', array('username' => 'admin_' . $siteid))) {
+                    return $rec;
+                }
+            }
+
             // 2B - Handle users deleted in DB and "alive" in backup file
             // Note: for DB deleted users email is stored in username field, hence we
             //       are looking there for emails. See delete_user()
@@ -1500,6 +1510,9 @@ abstract class restore_dbops {
         // Calculate the context we are going to use for capability checking
         $context = context_course::instance($courseid);
 
+        // When conflicting users are detected we may need original site info.
+        $restoreinfo = restore_controller_dbops::load_controller($restoreid)->get_info();
+
         // Calculate if we have perms to create users, by checking:
         // to 'moodle/restore:createuser' and 'moodle/restore:userinfo'
         // and also observe $CFG->disableusercreationonrestore
@@ -1535,14 +1548,28 @@ abstract class restore_dbops {
             }
 
             // Now, precheck that user and, based on returned results, annotate action/problem
-            $usercheck = self::precheck_user($user, $samesite);
+            $usercheck = self::precheck_user($user, $samesite, $restoreinfo->original_site_identifier_hash);
 
             if (is_object($usercheck)) { // No problem, we have found one user in DB to be mapped to
                 // Annotate it, for later process. Set newitemid to mapping user->id
                 self::set_backup_ids_record($restoreid, 'user', $recuser->itemid, $usercheck->id);
 
             } else if ($usercheck === false) { // Found conflict, report it as problem
-                 $problems[] = get_string('restoreuserconflict', '', $user->username);
+                if (!get_config('backup', 'import_general_duplicate_admin_allowed')) {
+                    $problems[] = get_string('restoreuserconflict', '', $user->username);
+                } else if ($user->username == 'admin') {
+                    if (!$cancreateuser) {
+                        $problems[] = get_string('restorecannotcreateuser', '', $user->username);
+                    }
+                    if ($user->mnethostid != $CFG->mnet_localhost_id) {
+                        $problems[] = get_string('restoremnethostidmismatch', '', $user->username);
+                    }
+                    if (!$problems) {
+                        // Duplicate admin allowed, append original site idenfitier to username.
+                        $user->username .= '_' . $restoreinfo->original_site_identifier_hash;
+                        self::set_backup_ids_record($restoreid, 'user', $recuser->itemid, 0, null, (array)$user);
+                    }
+                }
 
             } else if ($usercheck === true) { // User needs to be created, check if we are able
                 if ($cancreateuser) { // Can create user, set newitemid to 0 so will be created later
diff --git a/blocks/blog_menu/tests/behat/block_blog_menu.feature b/blocks/blog_menu/tests/behat/block_blog_menu.feature
new file mode 100644 (file)
index 0000000..b93728f
--- /dev/null
@@ -0,0 +1,81 @@
+@block @block_blog_menu
+Feature: Enable Block blog menu in a course
+  In order to enable the blog menu in a course
+  As a teacher
+  I can add blog menu block to a course
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+
+  Scenario: Add the block to a the course when blogs are disabled
+    Given I log in as "admin"
+    And the following config values are set as admin:
+      | enableblogs | 0 |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Blog menu" block
+    Then I should see "Blogging is disabled!" in the "Blog menu" "block"
+
+  Scenario: Add the block to a the course when blog associations are disabled
+    Given I log in as "admin"
+    And the following config values are set as admin:
+      | useblogassociations | 0 |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Blog menu" block
+    Then I should see "Blog entries" in the "Blog menu" "block"
+    And I should see "Add a new entry" in the "Blog menu" "block"
+    And I should not see "View all entries for this course" in the "Blog menu" "block"
+    And I should not see "View my entries about this course" in the "Blog menu" "block"
+    And I should not see "Add an entry about this course" in the "Blog menu" "block"
+
+  Scenario: Add the block to a the course when blog associations are enabled
+    Given I log in as "admin"
+    And the following config values are set as admin:
+      | useblogassociations | 1 |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Blog menu" block
+    Then I should see "Blog entries" in the "Blog menu" "block"
+    And I should see "Add a new entry" in the "Blog menu" "block"
+    And I should see "View all entries for this course" in the "Blog menu" "block"
+    And I should see "View my entries about this course" in the "Blog menu" "block"
+    And I should see "Add an entry about this course" in the "Blog menu" "block"
+
+  Scenario: Add the block to a the course when RSS is disabled
+    Given I log in as "admin"
+    And the following config values are set as admin:
+      | enablerssfeeds | 0 |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Blog menu" block
+    Then I should not see "Blog RSS feed" in the "Blog menu" "block"
+    And I should see "Add a new entry" in the "Blog menu" "block"
+
+  Scenario: Add the block to a the course when RSS is enabled
+    Given I log in as "admin"
+    And the following config values are set as admin:
+      | enablerssfeeds | 1 |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Blog menu" block
+    Then I should see "Blog RSS feed" in the "Blog menu" "block"
+    And I should see "Add a new entry" in the "Blog menu" "block"
diff --git a/blocks/blog_menu/tests/behat/block_blog_menu_course.feature b/blocks/blog_menu/tests/behat/block_blog_menu_course.feature
new file mode 100644 (file)
index 0000000..f45f62f
--- /dev/null
@@ -0,0 +1,199 @@
+@block @block_blog_menu
+Feature: Enable Block blog menu in a course
+  In order to enable the blog menu in a course
+  As a teacher
+  I can add blog menu block to a course
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+      | student1 | Student | 1 | student1@example.com | S1 |
+      | student2 | Student | 2 | student2@example.com | S2 |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add the "Blog menu" block
+    And I log out
+
+  Scenario: Students use the blog menu block to post blogs
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Add a new entry"
+    When I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    Then I should see "S1 First Blog"
+    And I should see "This is my awesome blog!"
+    And I follow "Dashboard"
+    And I follow "Course 1"
+    And I follow "Blog entries"
+    And I should see "S1 First Blog"
+    And I should see "This is my awesome blog!"
+
+  Scenario: Students use the blog menu block to view their blogs about the course
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Add an entry about this course"
+    And I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog about this course! |
+    And I press "Save changes"
+    And I should see "S1 First Blog"
+    And I should see "This is my awesome blog about this course!"
+    And I should see "Associated Course: C1"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Add a new entry"
+    And I set the following fields to these values:
+      | Entry title | S2 Second Blog |
+      | Blog entry body | My unrelated blog! |
+    And I press "Save changes"
+    And I should see "S2 Second Blog"
+    And I should see "My unrelated blog!"
+    And I should not see "Associated Course: C1"
+    And I follow "Dashboard"
+    And I follow "Course 1"
+    And I follow "Add an entry about this course"
+    And I set the following fields to these values:
+      | Entry title | S2 First Blog |
+      | Blog entry body | My course blog is better! |
+    And I press "Save changes"
+    And I should see "S2 First Blog"
+    And I should see "My course blog is better!"
+    And I should see "Associated Course: C1"
+    And I follow "Dashboard"
+    And I follow "Course 1"
+    When I follow "View my entries about this course"
+    Then I should see "S2 First Blog"
+    And I should not see "S2 Second Blog"
+    And I should not see "S1 First Blog"
+
+  Scenario: Students use the blog menu block to view all blogs about the course
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Add an entry about this course"
+    And I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog about this course! |
+    And I press "Save changes"
+    And I should see "S1 First Blog"
+    And I should see "This is my awesome blog about this course!"
+    And I should see "Associated Course: C1"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Add a new entry"
+    And I set the following fields to these values:
+      | Entry title | S2 Second Blog |
+      | Blog entry body | My unrelated blog! |
+    And I press "Save changes"
+    And I should see "S2 Second Blog"
+    And I should see "My unrelated blog!"
+    And I should not see "Associated Course: C1"
+    And I follow "Dashboard"
+    And I follow "Course 1"
+    And I follow "Add an entry about this course"
+    And I set the following fields to these values:
+      | Entry title | S2 First Blog |
+      | Blog entry body | My course blog is better! |
+    And I press "Save changes"
+    And I should see "S2 First Blog"
+    And I should see "My course blog is better!"
+    And I should see "Associated Course: C1"
+    And I follow "Dashboard"
+    And I follow "Course 1"
+    When I follow "View all entries for this course"
+    Then I should see "S1 First Blog"
+    And I should see "S2 First Blog"
+    And I should not see "S2 Second Blog"
+
+  Scenario: Students use the blog menu block to view all their blog entries
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Add an entry about this course"
+    And I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog about this course! |
+    And I press "Save changes"
+    And I should see "S1 First Blog"
+    And I should see "This is my awesome blog about this course!"
+    And I should see "Associated Course: C1"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Add a new entry"
+    And I set the following fields to these values:
+      | Entry title | S2 Second Blog |
+      | Blog entry body | My unrelated blog! |
+    And I press "Save changes"
+    And I should see "S2 Second Blog"
+    And I should see "My unrelated blog!"
+    And I should not see "Associated Course: C1"
+    And I follow "Dashboard"
+    And I follow "Course 1"
+    And I follow "Add an entry about this course"
+    And I set the following fields to these values:
+      | Entry title | S2 First Blog |
+      | Blog entry body | My course blog is better! |
+    And I press "Save changes"
+    And I should see "S2 First Blog"
+    And I should see "My course blog is better!"
+    And I should see "Associated Course: C1"
+    And I follow "Dashboard"
+    And I follow "Course 1"
+    When I follow "Blog entries"
+    Then I should see "S2 First Blog"
+    And I should see "S2 Second Blog"
+    And I should not see "S1 First Blog"
+
+  Scenario: Teacher searches for student blogs
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Add an entry about this course"
+    And I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog about this course! |
+    And I press "Save changes"
+    And I should see "S1 First Blog"
+    And I should see "This is my awesome blog about this course!"
+    And I should see "Associated Course: C1"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Add a new entry"
+    And I set the following fields to these values:
+      | Entry title | S2 Second Blog |
+      | Blog entry body | My unrelated blog! |
+    And I press "Save changes"
+    And I should see "S2 Second Blog"
+    And I should see "My unrelated blog!"
+    And I should not see "Associated Course: C1"
+    And I follow "Dashboard"
+    And I follow "Course 1"
+    And I follow "Add an entry about this course"
+    And I set the following fields to these values:
+      | Entry title | S2 First Blog |
+      | Blog entry body | My course blog is better! |
+    And I press "Save changes"
+    And I should see "S2 First Blog"
+    And I should see "My course blog is better!"
+    And I should see "Associated Course: C1"
+    And I log out
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I set the field "blogsearchquery" to "First"
+    And I press "Search"
+    Then I should see "S1 First Blog"
+    And I should see "S2 First Blog"
+    And I should not see "S2 Second Blog"
diff --git a/blocks/blog_menu/tests/behat/block_blog_menu_frontpage.feature b/blocks/blog_menu/tests/behat/block_blog_menu_frontpage.feature
new file mode 100644 (file)
index 0000000..3c2936a
--- /dev/null
@@ -0,0 +1,30 @@
+@block @block_blog_menu
+Feature: Enable Block blog menu on the frontpage
+  In order to enable the blog menu on the frontpage
+  As an admin
+  I can add blog menu block to the frontpage
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | student1 | Student | 1 | student1@example.com | S1 |
+    And I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I add the "Blog menu" block
+    And I log out
+
+  Scenario: Students use the blog menu block to post blogs
+    Given I log in as "student1"
+    And I am on site homepage
+    And I follow "Add a new entry"
+    When I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    Then I should see "S1 First Blog"
+    And I should see "This is my awesome blog!"
+    And I am on site homepage
+    And I follow "Blog entries"
+    And I should see "S1 First Blog"
+    And I should see "This is my awesome blog!"
index de32984..6edc7b5 100644 (file)
@@ -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 .= '<li>' . $link . '</li> ';
             }
             $this->content->text .= "\n</ul>\n";
index f16b0bc..57ae2b3 100644 (file)
@@ -344,16 +344,9 @@ class block_base {
      * You don't need to override this if you 're satisfied with the above
      *
      * @deprecated since Moodle 2.9 MDL-49385 - Please use Admin Settings functionality to save block configuration.
-     * @todo MDL-49553 This will be deleted in Moodle 3.1
-     * @param array $data
-     * @return boolean
      */
     function config_save($data) {
-        debugging('config_save($data) is deprecated, use Admin Settings functionality to save block configuration.', DEBUG_DEVELOPER);
-        foreach ($data as $name => $value) {
-            set_config($name, $value);
-        }
-        return true;
+        throw new coding_exception('config_save() can not be used any more, use Admin Settings functionality to save block configuration.');
     }
 
     /**
index de57a2d..c8b7f2c 100644 (file)
@@ -93,7 +93,8 @@ class block_news_items extends block_base {
             // descending order. The call to default sort order here will use
             // that unless the discussion that post is in has a timestart set
             // in the future.
-            $sort = forum_get_default_sort_order(true, 'p.modified');
+            // This sort will ignore pinned posts as we want the most recent.
+            $sort = forum_get_default_sort_order(true, 'p.modified', 'd', false);
             if (! $discussions = forum_get_discussions($cm, $sort, false,
                                                        $currentgroup, $this->page->course->newsitems) ) {
                 $text .= '('.get_string('nonews', 'forum').')';
index 06f691d..22471f3 100644 (file)
@@ -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);
 
index e67e039..da0a23f 100644 (file)
@@ -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);
index 7ab13be..9c79fe8 100644 (file)
@@ -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/backup/moodle2/restore_tags_block_task.class.php b/blocks/tags/backup/moodle2/restore_tags_block_task.class.php
new file mode 100644 (file)
index 0000000..11d2087
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * @package   block_tags
+ * @copyright 2016 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Specialised restore task for the tags block
+ * (using execute_after_tasks for recoding of tag collection id)
+ *
+ * @package   block_tags
+ * @copyright 2016 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class restore_tags_block_task extends restore_block_task {
+
+    protected function define_my_settings() {
+    }
+
+    protected function define_my_steps() {
+    }
+
+    public function get_fileareas() {
+        return array(); // No associated fileareas.
+    }
+
+    public function get_configdata_encoded_attributes() {
+        return array(); // No special handling of configdata.
+    }
+
+    /**
+     * This function, executed after all the tasks in the plan
+     * have been executed, will remove tag collection reference in case block was restored into another site.
+     * Also get mapping of contextid.
+     */
+    public function after_restore() {
+        global $DB;
+
+        // Get the blockid.
+        $blockid = $this->get_blockid();
+
+        // Extract block configdata and remove tag collection reference if this is another site. Also map contextid.
+        if ($configdata = $DB->get_field('block_instances', 'configdata', array('id' => $blockid))) {
+            $config = unserialize(base64_decode($configdata));
+            $changed = false;
+            if (!empty($config->tagcoll) && $config->tagcoll > 1 && !$this->is_samesite()) {
+                $config->tagcoll = 0;
+                $changed = true;
+            }
+            if (!empty($config->ctx)) {
+                if ($ctxmap = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $config->ctx)) {
+                    $config->ctx = $ctxmap->newitemid;
+                } else {
+                    $config->ctx = 0;
+                }
+                $changed = true;
+            }
+            if ($changed) {
+                $configdata = base64_encode(serialize($config));
+                $DB->set_field('block_instances', 'configdata', $configdata, array('id' => $blockid));
+            }
+        }
+    }
+
+    static public function define_decode_contents() {
+        return array();
+    }
+
+    static public function define_decode_rules() {
+        return array();
+    }
+}
index 329eb3f..31ced58 100644 (file)
@@ -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;
     }
index e6c80f1..89742c5 100644 (file)
@@ -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);
     }
 }
index edf9669..02dd7a1 100644 (file)
  * @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:';
index ed80f7b..7a25490 100644 (file)
@@ -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
index 634c511..8248039 100644 (file)
  * @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;
index 773f153..7060c93 100644 (file)
@@ -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();
 
index 069ef96..3f615bd 100644 (file)
@@ -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");
 
index fc28e9a..8de3bdc 100644 (file)
@@ -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');
index bc3dd1e..1b64b42 100644 (file)
@@ -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);
index 00a1ffa..06b4af8 100644 (file)
@@ -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;
+}
index 7968483..af1b72e 100644 (file)
@@ -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,9 +629,9 @@ 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.
+        $requiredfields = "p.*, $allnamefields";  // The SELECT clause.
         $tables = array('p' => 'post', 'u' => 'user');   // Components of the FROM clause (table_id => table_name).
         // Components of the WHERE clause (conjunction).
         $conditions = array('u.deleted = 0', 'p.userid = u.id', '(p.module = \'blog\' OR p.module = \'blog_external\')');
@@ -707,13 +710,8 @@ class blog_listing {
 
         $morelink = '<br />&nbsp;&nbsp;';
 
-        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);
index 1045a89..6406865 100644 (file)
@@ -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)) {
index cad38d8..366c5cd 100644 (file)
@@ -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;
index 1eb6583..81eac30 100644 (file)
@@ -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());
+    }
 }
 
index 0e2a196..184997e 100644 (file)
@@ -393,7 +393,7 @@ function calendar_get_mini($courses, $groups, $users, $calmonth = false, $calyea
                 // Show ical source if needed.
                 if (!empty($event->subscription) && $CFG->calendar_showicalsource) {
                     $a = new stdClass();
-                    $a->name = $name;
+                    $a->name = format_string($event->name, true);
                     $a->source = $event->subscription->name;
                     $name = get_string('namewithsource', 'calendar', $a);
                 } else {
index 0189fae..d79c0d6 100644 (file)
@@ -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.
index 94d260c..7c9f43e 100644 (file)
@@ -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.
index 21c4481..0c2acd3 100644 (file)
@@ -1648,7 +1648,6 @@ function course_delete_module($cmid) {
     require_once($CFG->libdir.'/questionlib.php');
     require_once($CFG->dirroot.'/blog/lib.php');
     require_once($CFG->dirroot.'/calendar/lib.php');
-    require_once($CFG->dirroot.'/tag/lib.php');
 
     // Get the course module.
     if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) {
@@ -1717,7 +1716,7 @@ function course_delete_module($cmid) {
                                                             'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY));
 
     // Delete all tag instances associated with the instance of this module.
-    tag_delete_instances('mod_' . $modulename, $modcontext->id);
+    core_tag_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);
+}
index ec26762..b71d488 100644 (file)
@@ -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 '';
     }
index 8e81fb1..1c7ad66 100644 (file)
@@ -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);
 }
 
index 08cacc0..fc54c03 100644 (file)
@@ -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);
index 421b1e4..de74065 100644 (file)
@@ -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);
index c5a6464..10a84ba 100644 (file)
@@ -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);
index d05b1cb..3c8517f 100644 (file)
@@ -291,6 +291,13 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                 y = parseInt(base.get('winHeight'))*0.1;
             }
             base.setXY([x,y]);
+            var zindex = 0;
+            Y.all('.moodle-has-zindex').each(function() {
+                if (parseInt(this.getComputedStyle('zIndex'), 10) > zindex) {
+                    zindex = parseInt(this.getComputedStyle('zIndex'), 10);
+                }
+            });
+            base.setStyle('zIndex', zindex + 1);
 
             if (this.get(UEP.USERS)===null) {
                 this.search(e, false);
index 8f972d8..197044f 100644 (file)
@@ -58,7 +58,7 @@ $data = new stdClass();
 
 foreach ($_POST as $key => $value) {
     $req .= "&$key=".urlencode($value);
-    $data->$key = $value;
+    $data->$key = fix_utf8($value);
 }
 
 $custom = explode('-', $data->custom);
@@ -211,6 +211,8 @@ if (strlen($result) > 0) {
             die;
 
         }
+        // Use the queried course's full name for the item_name field.
+        $data->item_name = $course->fullname;
 
         // ALL CLEAR !
 
diff --git a/enrol/tests/behat/manage_enrolments_from_participants.feature b/enrol/tests/behat/manage_enrolments_from_participants.feature
new file mode 100644 (file)
index 0000000..82e7f11
--- /dev/null
@@ -0,0 +1,35 @@
+@core_enrol
+Feature: Manage enrollments from participants page
+  In order to manage course participants
+  As a teacher
+  In need to get to the enrolment page from the course participants page
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+      | student2 | Student | 2 | student2@example.com |
+      | teacher1 | teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Participants" node in "Current course > C1"
+
+  Scenario: Check the participants link when "All partipants" selected
+    Given I select "All participants" from the "roleid" singleselect
+    When I follow "Edit"
+    Then I should see "Enrolled users" in the "h2" "css_element"
+    And the field "Role" matches value "All"
+
+  Scenario: Check the participants link when "Student" selected
+    Given I select "Student" from the "roleid" singleselect
+    When I follow "Edit"
+    Then I should see "Enrolled users" in the "h2" "css_element"
+    And the field "Role" matches value "Student"
diff --git a/grade/grading/form/guide/amd/build/comment_chooser.min.js b/grade/grading/form/guide/amd/build/comment_chooser.min.js
new file mode 100644 (file)
index 0000000..acb8809
Binary files /dev/null and b/grade/grading/form/guide/amd/build/comment_chooser.min.js differ
diff --git a/grade/grading/form/guide/amd/src/comment_chooser.js b/grade/grading/form/guide/amd/src/comment_chooser.js
new file mode 100644 (file)
index 0000000..fdee3cb
--- /dev/null
@@ -0,0 +1,139 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * AMD code for the frequently used comments chooser for the marking guide grading form.
+ *
+ * @module     gradingform_guide/comment_chooser
+ * @class      comment_chooser
+ * @package    core
+ * @copyright  2015 Jun Pataleta <jun@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+  */
+define(['jquery', 'core/templates', 'core/notification', 'core/yui'], function ($, templates, notification) {
+
+    // Private variables and functions.
+
+    return /** @alias module:gradingform_guide/comment_chooser */ {
+        // Public variables and functions.
+        /**
+         * Initialises the module.
+         *
+         * Basically, it performs the binding and handling of the button click event for
+         * the 'Insert frequently used comment' button.
+         *
+         * @param criterionId The criterion ID.
+         * @param buttonId The element ID of the button which the handler will be bound to.
+         * @param remarkId The element ID of the remark text area where the text of the selected comment will be copied to.
+         * @param commentOptions The array of frequently used comments to be used as options.
+         */
+        initialise: function (criterionId, buttonId, remarkId, commentOptions) {
+            var chooserDialog;
+
+            /**
+             * Display the chooser dialog using the compiled HTML from the mustache template
+             * and binds onclick events for the generated comment options.
+             *
+             * @param compiledSource The compiled HTML from the mustache template
+             * @param comments Array containing comments.
+             */
+            function displayChooserDialog(compiledSource, comments) {
+                var titleLabel = '<label>' + M.util.get_string('insertcomment', 'gradingform_guide') + '</label>';
+                var cancelButtonId = 'comment-chooser-' + criterionId + '-cancel';
+                var cancelButton = '<button id="' + cancelButtonId + '">' + M.util.get_string('cancel', 'moodle') + '</button>';
+
+                if (typeof chooserDialog === 'undefined') {
+                    // Set dialog's body content.
+                    chooserDialog = new M.core.dialogue({
+                        modal: true,
+                        headerContent: titleLabel,
+                        bodyContent: compiledSource,
+                        footerContent: cancelButton,
+                        focusAfterHide: '#' + remarkId,
+                        id: "comments-chooser-dialog-" + criterionId
+                    });
+
+                    // Bind click event to the cancel button.
+                    $("#" + cancelButtonId).click(function() {
+                        if (typeof chooserDialog !== 'undefined') {
+                            chooserDialog.hide();
+                        }
+                    });
+
+                    // Loop over each comment item and bind click events.
+                    $.each(comments, function (index, comment) {
+                        var commentOptionId = '#comment-option-' + criterionId + '-' + comment.id;
+
+                        // Delegate click event for the generated option link.
+                        $(commentOptionId).click(function () {
+                            var remarkTextArea = $('#' + remarkId);
+                            var remarkText = remarkTextArea.val();
+
+                            // Add line break if the current value of the remark text is not empty.
+                            if ($.trim(remarkText) !== '') {
+                                remarkText += '\n';
+                            }
+                            remarkText += comment.description;
+
+                            remarkTextArea.val(remarkText);
+
+                            if (typeof chooserDialog !== 'undefined') {
+                                chooserDialog.hide();
+                            }
+                        });
+
+                        // Handle keypress on list items.
+                        $(document).off('keypress', commentOptionId).on('keypress', commentOptionId, function () {
+                            var keyCode = event.which || event.keyCode;
+
+                            // Enter or space key.
+                            if (keyCode == 13 || keyCode == 32) {
+                                // Trigger click event.
+                                $(commentOptionId).click();
+                            }
+                        });
+                    });
+                }
+
+                // Show dialog.
+                chooserDialog.show();
+            }
+
+            /**
+             * Generates the comments chooser dialog from the grading_form/comment_chooser mustache template.
+             */
+            function generateCommentsChooser() {
+                // Template context.
+                var context = {
+                    criterionId: criterionId,
+                    comments: commentOptions
+                };
+
+                // Render the template and display the comment chooser dialog.
+                templates.render('gradingform_guide/comment_chooser', context)
+                    .done(function (compiledSource) {
+                        displayChooserDialog(compiledSource, commentOptions);
+                    })
+                    .fail(notification.exception);
+            }
+
+            // Bind click event for the comments chooser button.
+            $("#" + buttonId).click(function (e) {
+                e.preventDefault();
+                generateCommentsChooser();
+            });
+        }
+    };
+});
index 5437bee..e632bd1 100644 (file)
@@ -53,7 +53,7 @@ class gradingform_guide_editguide extends moodleform {
         // Name.
         $form->addElement('text', 'name', get_string('name', 'gradingform_guide'),
             array('size' => 52, 'maxlength' => 255));
-        $form->addRule('name', get_string('required'), 'required');
+        $form->addRule('name', get_string('required'), 'required', null, 'client');
         $form->setType('name', PARAM_TEXT);
         $form->addRule('name', null, 'maxlength', 255, 'client');
 
index a6e43a1..e044059 100644 (file)
@@ -137,7 +137,7 @@ class moodlequickform_guideeditor extends HTML_QuickForm_input {
             $html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
         }
         if ($this->validationerrors) {
-            $html .= $renderer->notification($this->validationerrors, 'error');
+            $html .= html_writer::div($renderer->notification($this->validationerrors, 'error'), '', array('role' => 'alert'));
         }
         $html .= $renderer->display_guide($data['criteria'], $data['comments'], $data['options'], $mode, $this->getName());
         return $html;
index d298b15..1175aea 100644 (file)
@@ -168,14 +168,15 @@ M.gradingform_guideeditor.buttonclick = function(e, confirmed) {
         return;
     }
     // prepare the id of the next inserted criterion
-
+    var elements_str;
     if (section == 'criteria') {
         elements_str = '#guide-'+name+' .criteria .criterion'
     } else if (section == 'comments') {
         elements_str = '#guide-'+name+' .comments .criterion'
     }
+    var newid = 0;
     if (action == 'addcriterion' || action == 'addcomment') {
-        var newid = M.gradingform_guideeditor.calculatenewid(elements_str)
+        newid = M.gradingform_guideeditor.calculatenewid(elements_str);
     }
     var dialog_options = {
         'scope' : this,
@@ -199,7 +200,14 @@ M.gradingform_guideeditor.buttonclick = function(e, confirmed) {
         M.gradingform_guideeditor.addhandlers();
         M.gradingform_guideeditor.disablealleditors()
         M.gradingform_guideeditor.assignclasses(elements_str)
-        //M.gradingform_guideeditor.editmode(Y.one('#guide-'+name+' #'+name+'-'+section+'-NEWID'+newid+'-shortname'),true)
+
+        // Enable edit mode of the newly added criterion/comment entry.
+        var inputTarget = 'shortname';
+        if (action == 'addcomment') {
+            inputTarget = 'description';
+        }
+        var inputTargetId = '#guide-' + name + ' #' + name + '-' + section + '-NEWID' + newid + '-' + inputTarget;
+        M.gradingform_guideeditor.editmode(Y.one(inputTargetId), true);
     } else if (chunks.length == 4 && action == 'moveup') {
         // MOVE UP
         el = Y.one('#'+name+'-'+section+'-'+chunks[2])
index a36eabc..87f7a11 100644 (file)
@@ -31,6 +31,7 @@ $string['backtoediting'] = 'Back to editing';
 $string['clicktocopy'] = 'Click to copy this text into the criteria feedback';
 $string['clicktoedit'] = 'Click to edit';
 $string['clicktoeditname'] = 'Click to edit criterion name';
+$string['comment'] = 'Comment';
 $string['comments'] = 'Frequently used comments';
 $string['commentsdelete'] = 'Delete comment';
 $string['commentsempty'] = 'Click to edit comment';
@@ -38,12 +39,13 @@ $string['commentsmovedown'] = 'Move down';
 $string['commentsmoveup'] = 'Move up';
 $string['confirmdeletecriterion'] = 'Are you sure you want to delete this item?';
 $string['confirmdeletelevel'] = 'Are you sure you want to delete this level?';
-$string['criterion'] = 'Criterion';
+$string['criterion'] = 'Criterion name';
 $string['criteriondelete'] = 'Delete criterion';
 $string['criterionempty'] = 'Click to edit criterion';
 $string['criterionmovedown'] = 'Move down';
 $string['criterionmoveup'] = 'Move up';
 $string['criterionname'] = 'Criterion name';
+$string['criterionremark'] = '{$a} criterion remark';
 $string['definemarkingguide'] = 'Define marking guide';
 $string['description'] = 'Description';
 $string['descriptionmarkers'] = 'Description for Markers';
@@ -57,6 +59,7 @@ $string['err_noshortname'] = 'Criterion name can not be empty';
 $string['err_shortnametoolong'] = 'Criterion name must be less than 256 characters';
 $string['err_scoreinvalid'] = 'The score given to {$a->criterianame} is not valid, the max score is: {$a->maxscore}';
 $string['gradingof'] = '{$a} grading';
+$string['guide'] = 'Marking guide';
 $string['guidemappingexplained'] = 'WARNING: Your marking guide has a maximum grade of <b>{$a->maxscore} points</b>┬ábut the maximum grade set in your activity is {$a->modulegrade}  The maximum score set in your marking guide will be scaled to the maximum grade in the module.<br />
     Intermediate scores will be converted respectively and rounded to the nearest available grade.';
 $string['guidenotcompleted'] = 'Please provide a valid grade for each criterion';
@@ -64,6 +67,7 @@ $string['guideoptions'] = 'Marking guide options';
 $string['guidestatus'] = 'Current marking guide status';
 $string['hidemarkerdesc'] = 'Hide marker criterion descriptions';
 $string['hidestudentdesc'] = 'Hide student criterion descriptions';
+$string['insertcomment'] = 'Insert frequently used comment';
 $string['maxscore'] = 'Maximum mark';
 $string['name'] = 'Name';
 $string['needregrademessage'] = 'The marking guide definition was changed after this student had been graded. The student can not see this marking guide until you check the marking guide and update the grade.';
index 4a896ea..a6c17eb 100644 (file)
@@ -951,7 +951,7 @@ class gradingform_guide_instance extends gradingform_instance {
         $currentinstance = $this->get_current_instance();
         if ($currentinstance && $currentinstance->get_status() == gradingform_instance::INSTANCE_STATUS_NEEDUPDATE) {
             $html .= html_writer::tag('div', get_string('needregrademessage', 'gradingform_guide'),
-                array('class' => 'gradingform_guide-regrade'));
+                array('class' => 'gradingform_guide-regrade', 'role' => 'alert'));
         }
         $haschanges = false;
         if ($currentinstance) {
index c99219d..caa4e19 100644 (file)
@@ -55,10 +55,13 @@ class gradingform_guide_renderer extends plugin_renderer_base {
      * @param array $criterion criterion data
      * @param array $value (only in view mode) teacher's feedback on this criterion
      * @param array $validationerrors An array containing validation errors to be shown
+     * @param array $comments Array of frequently used comments.
      * @return string
      */
     public function criterion_template($mode, $options, $elementname = '{NAME}', $criterion = null, $value = null,
-                                       $validationerrors = null) {
+                                       $validationerrors = null, $comments = null) {
+        global $PAGE;
+
         if ($criterion === null || !is_array($criterion) || !array_key_exists('id', $criterion)) {
             $criterion = array('id' => '{CRITERION-id}',
                                'description' => '{CRITERION-description}',
@@ -85,24 +88,28 @@ class gradingform_guide_renderer extends plugin_renderer_base {
                 $value = get_string('criterion'.$key, 'gradingform_guide');
                 $button = html_writer::empty_tag('input', array('type' => 'submit',
                     'name' => '{NAME}[criteria][{CRITERION-id}]['.$key.']',
-                    'id' => '{NAME}-criteria-{CRITERION-id}-'.$key, 'value' => $value, 'title' => $value, 'tabindex' => -1));
+                    'id' => '{NAME}-criteria-{CRITERION-id}-'.$key, 'value' => $value, 'title' => $value));
                 $criteriontemplate .= html_writer::tag('div', $button, array('class' => $key));
             }
             $criteriontemplate .= html_writer::end_tag('td'); // Controls.
             $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
                 'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder']));
 
-            $shortname = html_writer::empty_tag('input', array('type'=> 'text',
-                'name' => '{NAME}[criteria][{CRITERION-id}][shortname]',  'value' => $criterion['shortname'],
-                'id ' => '{NAME}[criteria][{CRITERION-id}][shortname]'));
-            $shortname = html_writer::tag('div', $shortname, array('class'=>'criterionname'));
-            $description = html_writer::tag('textarea', s($criterion['description']),
-                array('name' => '{NAME}[criteria][{CRITERION-id}][description]', 'cols' => '65', 'rows' => '5'));
-            $description = html_writer::tag('div', $description, array('class'=>'criteriondesc'));
-
-            $descriptionmarkers = html_writer::tag('textarea', s($criterion['descriptionmarkers']),
-                array('name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]', 'cols' => '65', 'rows' => '5'));
-            $descriptionmarkers = html_writer::tag('div', $descriptionmarkers, array('class'=>'criteriondescmarkers'));
+            $shortnameinput = html_writer::empty_tag('input', array('type' => 'text',
+                'name' => '{NAME}[criteria][{CRITERION-id}][shortname]',
+                'id ' => '{NAME}-criteria-{CRITERION-id}-shortname',
+                'value' => $criterion['shortname'],
+                'aria-labelledby' => '{NAME}-criterion-name-label'));
+            $shortname = html_writer::tag('div', $shortnameinput, array('class' => 'criterionname'));
+            $descriptioninput = html_writer::tag('textarea', s($criterion['description']),
+                array('name' => '{NAME}[criteria][{CRITERION-id}][description]',
+                      'id' => '{NAME}[criteria][{CRITERION-id}][description]', 'cols' => '65', 'rows' => '5'));
+            $description = html_writer::tag('div', $descriptioninput, array('class' => 'criteriondesc'));
+
+            $descriptionmarkersinput = html_writer::tag('textarea', s($criterion['descriptionmarkers']),
+                array('name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]',
+                      'id' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]', 'cols' => '65', 'rows' => '5'));
+            $descriptionmarkers = html_writer::tag('div', $descriptionmarkersinput, array('class' => 'criteriondescmarkers'));
 
             $maxscore = html_writer::empty_tag('input', array('type'=> 'text',
                 'name' => '{NAME}[criteria][{CRITERION-id}][maxscore]', 'size' => '3',
@@ -125,8 +132,14 @@ class gradingform_guide_renderer extends plugin_renderer_base {
                        $mode == gradingform_guide_controller::DISPLAY_VIEW) {
                 $descriptionclass = 'descriptionreadonly';
             }
-            $shortname   = html_writer::tag('div', s($criterion['shortname']),
-                array('class'=>'criterionshortname', 'name' => '{NAME}[criteria][{CRITERION-id}][shortname]'));
+
+            $shortnameparams = array(
+                'name' => '{NAME}[criteria][{CRITERION-id}][shortname]',
+                'id' => '{NAME}[criteria][{CRITERION-id}][shortname]',
+                'aria-describedby' => '{NAME}-criterion-name-label'
+            );
+            $shortname = html_writer::div(s($criterion['shortname']), 'criterionshortname', $shortnameparams);
+
             $descmarkerclass = '';
             $descstudentclass = '';
             if ($mode == gradingform_guide_controller::DISPLAY_EVAL) {
@@ -155,9 +168,7 @@ class gradingform_guide_renderer extends plugin_renderer_base {
             $descriptionclass .= ' error';
         }
 
-        $title = html_writer::tag('label', get_string('criterion', 'gradingform_guide'),
-            array('for'=>'{NAME}[criteria][{CRITERION-id}][shortname]', 'class' => 'criterionnamelabel'));
-        $title .= $shortname;
+        $title = $shortname;
         if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL ||
             $mode == gradingform_guide_controller::DISPLAY_PREVIEW) {
             $title .= html_writer::tag('label', get_string('descriptionstudents', 'gradingform_guide'),
@@ -173,15 +184,26 @@ class gradingform_guide_renderer extends plugin_renderer_base {
                    $mode == gradingform_guide_controller::DISPLAY_VIEW) {
             $title .= $description;
             if (!empty($options['showmarkspercriterionstudents'])) {
-                $title .=  html_writer::tag('label', get_string('maxscore', 'gradingform_guide'),
-                    array('for' => '{NAME}[criteria][{CRITERION-id}][maxscore]'));
+                $title .= html_writer::label(get_string('maxscore', 'gradingform_guide'), null);
                 $title .= $maxscore;
             }
         } else {
             $title .= $description . $descriptionmarkers;
         }
-        $criteriontemplate .= html_writer::tag('td', $title, array('class' => $descriptionclass,
-            'id' => '{NAME}-criteria-{CRITERION-id}-shortname'));
+
+        // Title cell params.
+        $titletdparams = array(
+            'class' => $descriptionclass,
+            'id' => '{NAME}-criteria-{CRITERION-id}-shortname-cell'
+        );
+
+        if ($mode != gradingform_guide_controller::DISPLAY_EDIT_FULL &&
+            $mode != gradingform_guide_controller::DISPLAY_EDIT_FROZEN) {
+            // Set description's cell as tab-focusable.
+            $titletdparams['tabindex'] = '0';
+        }
+
+        $criteriontemplate .= html_writer::tag('td', $title, $titletdparams);
 
         $currentremark = '';
         $currentscore = '';
@@ -191,23 +213,70 @@ class gradingform_guide_renderer extends plugin_renderer_base {
         if (isset($value['score'])) {
             $currentscore = $value['score'];
         }
+
+        // Element ID of the remark text area.
+        $remarkid = $elementname . '-criteria-' . $criterion['id'] . '-remark';
+
         if ($mode == gradingform_guide_controller::DISPLAY_EVAL) {
             $scoreclass = '';
             if (!empty($validationerrors[$criterion['id']]['score'])) {
                 $scoreclass = 'error';
                 $currentscore = $validationerrors[$criterion['id']]['score']; // Show invalid score in form.
             }
-            $input = html_writer::tag('textarea', s($currentremark),
-                array('name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'cols' => '65', 'rows' => '5',
-                      'class' => 'markingguideremark'));
-            $criteriontemplate .= html_writer::tag('td', $input, array('class' => 'remark'));
-            $score = html_writer::tag('label', get_string('score', 'gradingform_guide'),
-                array('for'=>'{NAME}[criteria][{CRITERION-id}][score]', 'class' => $scoreclass));
-            $score .= html_writer::empty_tag('input', array('type'=> 'text',
-                'name' => '{NAME}[criteria][{CRITERION-id}][score]', 'class' => $scoreclass,
-                'id' => '{NAME}[criteria][{CRITERION-id}][score]',
-                'size' => '3', 'value' => $currentscore));
-            $score .= '/'.$maxscore;
+
+            // Grading remark text area parameters.
+            $remarkparams = array(
+                'name' => '{NAME}[criteria][{CRITERION-id}][remark]',
+                'id' => $remarkid,
+                'cols' => '65', 'rows' => '5', 'class' => 'markingguideremark',
+                'aria-labelledby' => '{NAME}-remarklabel{CRITERION-id}'
+            );
+
+            // Grading remark text area.
+            $input = html_writer::tag('textarea', s($currentremark), $remarkparams);
+
+            // Frequently used comments chooser.
+            $chooserbuttonid = 'criteria-' . $criterion['id'] . '-commentchooser';
+            $commentchooserparams = array('id' => $chooserbuttonid, 'class' => 'commentchooser');
+            $commentchooser = html_writer::tag('button', get_string('insertcomment', 'gradingform_guide'), $commentchooserparams);
+
+            // Option items for the frequently used comments chooser dialog.
+            $commentoptions = array();
+            foreach ($comments as $id => $comment) {
+                $commentoption = new stdClass();
+                $commentoption->id = $id;
+                $commentoption->description = s($comment['description']);
+                $commentoptions[] = $commentoption;
+            }
+
+            // Include string for JS for the comment chooser title.
+            $PAGE->requires->string_for_js('insertcomment', 'gradingform_guide');
+            // Include comment_chooser module.
+            $PAGE->requires->js_call_amd('gradingform_guide/comment_chooser', 'initialise',
+                                         array($criterion['id'], $chooserbuttonid, $remarkid, $commentoptions));
+
+            // Hidden marking guide remark label.
+            $remarklabelparams = array(
+                'class' => 'hidden',
+                'id' => '{NAME}-remarklabel{CRITERION-id}'
+            );
+            $remarklabeltext = get_string('criterionremark', 'gradingform_guide', $criterion['shortname']);
+            $remarklabel = html_writer::label($remarklabeltext, $remarkid, false, $remarklabelparams);
+
+            $criteriontemplate .= html_writer::tag('td', $remarklabel . $input . $commentchooser, array('class' => 'remark'));
+
+            // Score input and max score.
+            $scoreinputparams = array(
+                'type' => 'text',
+                'name' => '{NAME}[criteria][{CRITERION-id}][score]',
+                'class' => $scoreclass,
+                'id' => '{NAME}-criteria-{CRITERION-id}-score',
+                'size' => '3',
+                'value' => $currentscore,
+                'aria-labelledby' => '{NAME}-score-label'
+            );
+            $score = html_writer::empty_tag('input', $scoreinputparams);
+            $score .= html_writer::div('/' . s($criterion['maxscore']));
 
             $criteriontemplate .= html_writer::tag('td', $score, array('class' => 'score'));
         } else if ($mode == gradingform_guide_controller::DISPLAY_EVAL_FROZEN) {
@@ -215,10 +284,34 @@ class gradingform_guide_renderer extends plugin_renderer_base {
                 'name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'value' => $currentremark));
         } else if ($mode == gradingform_guide_controller::DISPLAY_REVIEW ||
             $mode == gradingform_guide_controller::DISPLAY_VIEW) {
-            $criteriontemplate .= html_writer::tag('td', s($currentremark), array('class' => 'remark'));
+
+            // Hidden marking guide remark description.
+            $remarkdescparams = array(
+                'id' => '{NAME}-criteria-{CRITERION-id}-remark-desc'
+            );
+            $remarkdesctext = get_string('criterionremark', 'gradingform_guide', $criterion['shortname']);
+            $remarkdesc = html_writer::div($remarkdesctext, 'hidden', $remarkdescparams);
+
+            // Remarks cell.
+            $remarkdiv = html_writer::div(s($currentremark));
+            $remarkcellparams = array(
+                'class' => 'remark',
+                'tabindex' => '0',
+                'id' => '{NAME}-criteria-{CRITERION-id}-remark',
+                'aria-describedby' => '{NAME}-criteria-{CRITERION-id}-remark-desc'
+            );
+            $criteriontemplate .= html_writer::tag('td', $remarkdesc . $remarkdiv, $remarkcellparams);
+
+            // Score cell.
             if (!empty($options['showmarkspercriterionstudents'])) {
-                $criteriontemplate .= html_writer::tag('td', s($currentscore). ' / '.$maxscore,
-                    array('class' => 'score'));
+                $scorecellparams = array(
+                    'class' => 'score',
+                    'tabindex' => '0',
+                    'id' => '{NAME}-criteria-{CRITERION-id}-score',
+                    'aria-describedby' => '{NAME}-score-label'
+                );
+                $scorediv = html_writer::div(s($currentscore) . ' / ' . s($criterion['maxscore']));
+                $criteriontemplate .= html_writer::tag('td', $scorediv, $scorecellparams);
             }
         }
         $criteriontemplate .= html_writer::end_tag('tr'); // Criterion.
@@ -262,28 +355,30 @@ class gradingform_guide_renderer extends plugin_renderer_base {
                 }
             }
         }
-        $criteriontemplate = html_writer::start_tag('tr', array('class' => 'criterion'. $comment['class'],
+        $commenttemplate = html_writer::start_tag('tr', array('class' => 'criterion'. $comment['class'],
             'id' => '{NAME}-comments-{COMMENT-id}'));
         if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) {
-            $criteriontemplate .= html_writer::start_tag('td', array('class' => 'controls'));
+            $commenttemplate .= html_writer::start_tag('td', array('class' => 'controls'));
             foreach (array('moveup', 'delete', 'movedown') as $key) {
                 $value = get_string('comments'.$key, 'gradingform_guide');
                 $button = html_writer::empty_tag('input', array('type' => 'submit',
                     'name' => '{NAME}[comments][{COMMENT-id}]['.$key.']', 'id' => '{NAME}-comments-{COMMENT-id}-'.$key,
-                    'value' => $value, 'title' => $value, 'tabindex' => -1));
-                $criteriontemplate .= html_writer::tag('div', $button, array('class' => $key));
+                    'value' => $value, 'title' => $value));
+                $commenttemplate .= html_writer::tag('div', $button, array('class' => $key));
             }
-            $criteriontemplate .= html_writer::end_tag('td'); // Controls.
-            $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
+            $commenttemplate .= html_writer::end_tag('td'); // Controls.
+            $commenttemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
                 'name' => '{NAME}[comments][{COMMENT-id}][sortorder]', 'value' => $comment['sortorder']));
             $description = html_writer::tag('textarea', s($comment['description']),
-                array('name' => '{NAME}[comments][{COMMENT-id}][description]', 'cols' => '65', 'rows' => '5'));
+                array('name' => '{NAME}[comments][{COMMENT-id}][description]',
+                      'id' => '{NAME}-comments-{COMMENT-id}-description',
+                      'aria-labelledby' => '{NAME}-comment-label', 'cols' => '65', 'rows' => '5'));
             $description = html_writer::tag('div', $description, array('class'=>'criteriondesc'));
         } else {
             if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN) {
-                $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
+                $commenttemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
                     'name' => '{NAME}[comments][{COMMENT-id}][sortorder]', 'value' => $comment['sortorder']));
-                $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
+                $commenttemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
                     'name' => '{NAME}[comments][{COMMENT-id}][description]', 'value' => $comment['description']));
             }
             if ($mode == gradingform_guide_controller::DISPLAY_EVAL) {
@@ -301,13 +396,21 @@ class gradingform_guide_renderer extends plugin_renderer_base {
         if (isset($comment['error_description'])) {
             $descriptionclass .= ' error';
         }
-        $criteriontemplate .= html_writer::tag('td', $description, array('class' => $descriptionclass,
-            'id' => '{NAME}-comments-{COMMENT-id}-description'));
-        $criteriontemplate .= html_writer::end_tag('tr'); // Criterion.
+        $descriptioncellparams = array(
+            'class' => $descriptionclass,
+            'id' => '{NAME}-comments-{COMMENT-id}-description-cell'
+        );
+        // Make description cell tab-focusable when in review mode.
+        if ($mode != gradingform_guide_controller::DISPLAY_EDIT_FULL &&
+            $mode != gradingform_guide_controller::DISPLAY_EDIT_FROZEN) {
+            $descriptioncellparams['tabindex'] = '0';
+        }
+        $commenttemplate .= html_writer::tag('td', $description, $descriptioncellparams);
+        $commenttemplate .= html_writer::end_tag('tr'); // Criterion.
 
-        $criteriontemplate = str_replace('{NAME}', $elementname, $criteriontemplate);
-        $criteriontemplate = str_replace('{COMMENT-id}', $comment['id'], $criteriontemplate);
-        return $criteriontemplate;
+        $commenttemplate = str_replace('{NAME}', $elementname, $commenttemplate);
+        $commenttemplate = str_replace('{COMMENT-id}', $comment['id'], $commenttemplate);
+        return $commenttemplate;
     }
     /**
      * This function returns html code for displaying guide template (content before and after
@@ -358,7 +461,27 @@ class gradingform_guide_renderer extends plugin_renderer_base {
 
         $guidetemplate = html_writer::start_tag('div', array('id' => 'guide-{NAME}',
             'class' => 'clearfix gradingform_guide'.$classsuffix));
-        $guidetemplate .= html_writer::tag('table', $criteriastr, array('class' => 'criteria', 'id' => '{NAME}-criteria'));
+
+        // Hidden guide label.
+        $guidedescparams = array(
+            'id' => 'guide-{NAME}-desc',
+            'aria-hidden' => 'true'
+        );
+        $guidetemplate .= html_writer::div(get_string('guide', 'gradingform_guide'), 'hidden', $guidedescparams);
+
+        // Hidden criterion name label/description.
+        $guidetemplate .= html_writer::div(get_string('criterionname', 'gradingform_guide'), 'hidden',
+            array('id' => '{NAME}-criterion-name-label'));
+
+        // Hidden score label/description.
+        $guidetemplate .= html_writer::div(get_string('score', 'gradingform_guide'), 'hidden', array('id' => '{NAME}-score-label'));
+
+        // Criteria table parameters.
+        $criteriatableparams = array(
+            'class' => 'criteria',
+            'id' => '{NAME}-criteria',
+            'aria-describedby' => 'guide-{NAME}-desc');
+        $guidetemplate .= html_writer::tag('table', $criteriastr, $criteriatableparams);
         if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) {
             $value = get_string('addcriterion', 'gradingform_guide');
             $input = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][addcriterion]',
@@ -367,9 +490,15 @@ class gradingform_guide_renderer extends plugin_renderer_base {
         }
 
         if (!empty($commentstr)) {
-            $guidetemplate .= html_writer::tag('label', get_string('comments', 'gradingform_guide'),
-                array('for' => '{NAME}-comments', 'class' => 'commentheader'));
-            $guidetemplate .= html_writer::tag('table', $commentstr, array('class' => 'comments', 'id' => '{NAME}-comments'));
+            $guidetemplate .= html_writer::div(get_string('comments', 'gradingform_guide'), 'commentheader',
+                array('id' => '{NAME}-comments-label'));
+            $guidetemplate .= html_writer::div(get_string('comment', 'gradingform_guide'), 'hidden',
+                array('id' => '{NAME}-comment-label', 'aria-hidden' => 'true'));
+            $commentstableparams = array(
+                'class' => 'comments',
+                'id' => '{NAME}-comments',
+                'aria-describedby' => '{NAME}-comments-label');
+            $guidetemplate .= html_writer::tag('table', $commentstr, $commentstableparams);
         }
         if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) {
             $value = get_string('addcomment', 'gradingform_guide');
@@ -481,21 +610,21 @@ class gradingform_guide_renderer extends plugin_renderer_base {
                 $criterionvalue = null;
             }
             $criteriastr .= $this->criterion_template($mode, $options, $elementname, $criterion, $criterionvalue,
-                                                      $validationerrors);
+                                                      $validationerrors, $comments);
         }
+
         $cnt = 0;
         $commentstr = '';
         // Check if comments should be displayed.
         if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL ||
             $mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN ||
             $mode == gradingform_guide_controller::DISPLAY_PREVIEW ||
-            $mode == gradingform_guide_controller::DISPLAY_EVAL ||
             $mode == gradingform_guide_controller::DISPLAY_EVAL_FROZEN) {
 
             foreach ($comments as $id => $comment) {
                 $comment['id'] = $id;
                 $comment['class'] = $this->get_css_class_suffix($cnt++, count($comments) -1);
-                $commentstr  .= $this->comment_template($mode, $elementname, $comment);
+                $commentstr .= $this->comment_template($mode, $elementname, $comment);
             }
         }
         $output = $this->guide_template($mode, $options, $elementname, $criteriastr, $commentstr);
@@ -613,7 +742,7 @@ class gradingform_guide_renderer extends plugin_renderer_base {
      * @return string
      */
     public function display_regrade_confirmation($elementname, $changelevel, $value) {
-        $html = html_writer::start_tag('div', array('class' => 'gradingform_guide-regrade'));
+        $html = html_writer::start_tag('div', array('class' => 'gradingform_guide-regrade', 'role' => 'alert'));
         if ($changelevel<=2) {
             $html .= get_string('regrademessage1', 'gradingform_guide');
             $selectoptions = array(
diff --git a/grade/grading/form/guide/templates/comment_chooser.mustache b/grade/grading/form/guide/templates/comment_chooser.mustache
new file mode 100644 (file)
index 0000000..883121c
--- /dev/null
@@ -0,0 +1,59 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template gradingform_guide/comment_chooser
+
+    Moodle comment chooser template for marking guide.
+
+    The purpose of this template is to render a list of frequently used comments that can be used by the comment chooser dialog.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * criterionId The criterion ID this chooser template is being generated for.
+    * comments Array of id / description pairs.
+
+    Example context (json):
+    {
+        "criterionId": "1",
+        "comments": [
+            {
+                "id": "1",
+                "description": "Test comment description 1"
+            },
+            {
+                "id": "2",
+                "description": "Test comment description 2"
+            }
+        ]
+    }
+}}
+<div class="gradingform_guide_comment_chooser" id="comment_chooser">
+    <ul role="list">
+        {{#comments}}
+            <li role="listitem">
+                <button id="comment-option-{{criterionId}}-{{id}}" class="btn btn-link" tabindex="0">
+                    {{description}}
+                </button>
+            </li>
+        {{/comments}}
+    </ul>
+</div>
diff --git a/grade/grading/form/guide/tests/behat/behat_gradingform_guide.php b/grade/grading/form/guide/tests/behat/behat_gradingform_guide.php
new file mode 100644 (file)
index 0000000..7ce443c
--- /dev/null
@@ -0,0 +1,223 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Steps definitions for marking guides.
+ *
+ * @package   gradingform_guide
+ * @category  test
+ * @copyright 2015 Jun Pataleta
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../../../../lib/behat/behat_base.php');
+
+use Behat\Gherkin\Node\TableNode as TableNode,
+    Behat\Behat\Context\Step\Given as Given,
+    Behat\Behat\Context\Step\When as When,
+    Behat\Behat\Context\Step\Then as Then,
+    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
+    Behat\Mink\Exception\ExpectationException as ExpectationException;
+
+/**
+ * Steps definitions to help with marking guides.
+ *
+ * @package   gradingform_guide
+ * @category  test
+ * @copyright 2015 Jun Pataleta
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_gradingform_guide extends behat_base {
+
+    /**
+     * Defines the marking guide with the provided data, following marking guide's definition grid cells.
+     *
+     * This method fills the marking guide of the marking guide definition
+     * form; the provided TableNode should contain one row for
+     * each criterion and each cell of the row should contain:
+     * # Criterion name, a.k.a. shortname
+     * # Description for students
+     * # Description for markers
+     * # Max score
+     *
+     * Works with both JS and non-JS.
+     *
+     * @When /^I define the following marking guide:$/
+     * @throws ExpectationException
+     * @param TableNode $guide
+     */
+    public function i_define_the_following_marking_guide(TableNode $guide) {
+        $steptableinfo = '| Criterion name | Description for students | Description for markers | Maximum mark |';
+
+        if ($criteria = $guide->getHash()) {
+            $addcriterionbutton = $this->find_button(get_string('addcriterion', 'gradingform_guide'));
+
+            foreach ($criteria as $index => $criterion) {
+                // Make sure the criterion array has 4 elements.
+                if (count($criterion) != 4) {
+                    throw new ExpectationException(
+                        'The criterion definition should contain name, description for students and markers, and maximum points. ' .
+                        'Please follow this format: ' . $steptableinfo,
+                        $this->getSession()
+                    );
+                }
+
+                // On load, there's already a criterion template ready.
+                $shortnamevisible = false;
+                if ($index > 0) {
+                    // So if the index is greater than 0, we click the Add new criterion button to add a new criterion.
+                    $addcriterionbutton->click();
+                    $shortnamevisible = true;
+                }
+
+                $criterionroot = 'guide[criteria][NEWID' . ($index + 1) . ']';
+
+                // Set the field value for the Criterion name.
+                $this->set_guide_field_value($criterionroot . '[shortname]', $criterion['Criterion name'], $shortnamevisible);
+
+                // Set the field value for the Description for students field.
+                $this->set_guide_field_value($criterionroot . '[description]', $criterion['Description for students']);
+
+                // Set the field value for the Description for markers field.
+                $this->set_guide_field_value($criterionroot . '[descriptionmarkers]', $criterion['Description for markers']);
+
+                // Set the field value for the Max score field.
+                $this->set_guide_field_value($criterionroot . '[maxscore]', $criterion['Maximum mark']);
+            }
+        }
+    }
+
+    /**
+     * Defines the marking guide with the provided data, following marking guide's definition grid cells.
+     *
+     * This method fills the table of frequently used comments of the marking guide definition form.
+     * The provided TableNode should contain one row for each frequently used comment.
+     * Each row contains:
+     * # Comment
+     *
+     * Works with both JS and non-JS.
+     *
+     * @When /^I define the following frequently used comments:$/
+     * @throws ExpectationException
+     * @param TableNode $commentstable
+     */
+    public function i_define_the_following_frequently_used_comments(TableNode $commentstable) {
+        $steptableinfo = '| Comment |';
+
+        if ($comments = $commentstable->getRows()) {
+            $addcommentbutton = $this->find_button(get_string('addcomment', 'gradingform_guide'));
+
+            foreach ($comments as $index => $comment) {
+                // Make sure the comment array has only 1 element.
+                if (count($comment) != 1) {
+                    throw new ExpectationException(
+                        'The comment cannot be empty. Please follow this format: ' . $steptableinfo,
+                        $this->getSession()
+                    );
+                }
+
+                // On load, there's already a comment template ready.
+                $commentfieldvisible = false;
+                if ($index > 0) {
+                    // So if the index is greater than 0, we click the Add frequently used comment button to add a new criterion.
+                    $addcommentbutton->click();
+                    $commentfieldvisible = true;
+                }
+
+                $commentroot = 'guide[comments][NEWID' . ($index + 1) . ']';
+
+                // Set the field value for the frequently used comment.
+                $this->set_guide_field_value($commentroot . '[description]', $comment[0], $commentfieldvisible);
+            }
+        }
+    }
+
+    /**
+     * Performs grading of the student by filling out the marking guide.
+     * Set one line per criterion and for each criterion set "| Criterion name | Points | Remark |".
+     *
+     * @When /^I grade by filling the marking guide with:$/
+     *
+     * @throws ExpectationException
+     * @param TableNode $guide
+     * @return void
+     */
+    public function i_grade_by_filling_the_marking_guide_with(TableNode $guide) {
+
+        $criteria = $guide->getRowsHash();
+
+        $stepusage = '"I grade by filling the rubric with:" step needs you to provide a table where each row is a criterion' .
+            ' and each criterion has 3 different values: | Criterion name | Number of points | Remark text |';
+
+        // To fill with the steps to execute.
+        $steps = array();
+
+        // First element -> name, second -> points, third -> Remark.
+        foreach ($criteria as $name => $criterion) {
+
+            // We only expect the points and the remark, as the criterion name is $name.
+            if (count($criterion) !== 2) {
+                throw new ExpectationException($stepusage, $this->getSession());
+            }
+
+            // Numeric value here.
+            $points = $criterion[0];
+            if (!is_numeric($points)) {
+                throw new ExpectationException($stepusage, $this->getSession());
+            }
+
+            $criterionid = 0;
+            if ($criterionnamediv = $this->find('xpath', "//div[@class='criterionshortname'][text()='$name']")) {
+                $criteriondivname = $criterionnamediv->getAttribute('name');
+                // Criterion's name is of the format "advancedgrading[criteria][ID][shortname]".
+                // So just explode the string with "][" as delimiter to extract the criterion ID.
+                if ($nameparts = explode('][', $criteriondivname)) {
+                    $criterionid = $nameparts[1];
+                }
+            }
+
+            if ($criterionid) {
+                $criterionroot = 'advancedgrading[criteria]' . '[' . $criterionid . ']';
+
+                $steps[] = new Given('I set the field "' . $criterionroot . '[score]' . '" to "' . $points . '"');
+                $steps[] = new Given('I set the field "' . $criterionroot . '[remark]' . '" to "' . $criterion[1] . '"');
+            }
+        }
+
+        return $steps;
+    }
+
+    /**
+     * Makes a hidden marking guide field visible (if necessary) and sets a value on it.
+     *
+     * @param string $name The name of the field
+     * @param string $value The value to set
+     * @param bool $visible
+     * @return void
+     */
+    protected function set_guide_field_value($name, $value, $visible = false) {
+        // Fields are hidden by default.
+        if ($this->running_javascript() && $visible === false) {
+            $xpath = "//*[@name='$name']/following-sibling::*[contains(concat(' ', normalize-space(@class), ' '), ' plainvalue ')]";
+            $textnode = $this->find('xpath', $xpath);
+            $textnode->click();
+        }
+
+        // Set the value now.
+        $field = $this->find_field($name);
+        $field->setValue($value);
+    }
+}
diff --git a/grade/grading/form/guide/tests/behat/edit_guide.feature b/grade/grading/form/guide/tests/behat/edit_guide.feature
new file mode 100644 (file)
index 0000000..c7ee03d
--- /dev/null
@@ -0,0 +1,102 @@
+@gradingform @gradingform_guide
+Feature: Marking guides can be created and edited
+  In order to use and refine marking guide to grade students
+  As a teacher
+  I need to edit previously used marking guides
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+      | student1 | Student   | 1        | student1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1        | topics |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | student1 | C1     | student        |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assignment 1 name      |
+      | Description     | Test assignment description |
+      | Grading method  | Marking guide               |
+    # Defining a marking guide
+    When I go to "Test assignment 1 name" advanced grading definition page
+    And I set the following fields to these values:
+      | Name        | Assignment 1 marking guide     |
+      | Description | Marking guide test description |
+    And I define the following marking guide:
+      | Criterion name    | Description for students         | Description for markers         | Maximum mark |
+      | Guide criterion A | Guide A description for students | Guide A description for markers | 30           |
+      | Guide criterion B | Guide B description for students | Guide B description for markers | 30           |
+      | Guide criterion C | Guide C description for students | Guide C description for markers | 40           |
+    And I define the following frequently used comments:
+      | Comment 1 |
+      | Comment 2 |
+      | Comment 3 |
+      | Comment 4 |
+    And I press "Save marking guide and make it ready"
+    Then I should see "Ready for use"
+    And I should see "Guide criterion A"
+    And I should see "Guide criterion B"
+    And I should see "Guide criterion C"
+    And I should see "Comment 1"
+    And I should see "Comment 2"
+    And I should see "Comment 3"
+    And I should see "Comment 4"
+
+  @javascript
+  Scenario: Deleting criterion and comment
+    # Deleting criterion
+    When I go to "Test assignment 1 name" advanced grading definition page
+    And I click on "Delete criterion" "button" in the "Guide criterion B" "table_row"
+    And I press "Yes"
+    And I press "Save"
+    Then I should see "Guide criterion A"
+    And I should see "Guide criterion C"
+    And I should see "WARNING: Your marking guide has a maximum grade of 70 points"
+    But I should not see "Guide criterion B"
+    # Deleting a frequently used comment
+    When I go to "Test assignment 1 name" advanced grading definition page
+    And I click on "Delete comment" "button" in the "Comment 3" "table_row"
+    And I press "Yes"
+    And I press "Save"
+    Then I should see "Comment 1"
+    And I should see "Comment 2"
+    And I should see "Comment 4"
+    But I should not see "Comment 3"
+
+  @javascript
+  Scenario: Grading and viewing graded marking guide
+    # Grading a student.
+    When I go to "Student 1" "Test assignment 1 name" activity advanced grading page
+    And I grade by filling the marking guide with:
+      | Guide criterion A | 25 | Very good  |
+      | Guide criterion B | 20 |            |
+      | Guide criterion C | 35 | Nice!      |
+    # Inserting frequently used comment.
+    And I click on "Insert frequently used comment" "button" in the "Guide criterion B" "table_row"
+    And I wait "1" seconds
+    And I press "Comment 4"
+    And I wait "1" seconds
+    Then the field "Guide criterion B criterion remark" matches value "Comment 4"
+    When I press "Save changes"
+    Then I should see "The grade changes were saved"
+    # Checking that the user grade is correct.
+    When I press "Continue"
+    Then I should see "80" in the "Student 1" "table_row"
+    And I log out
+    # Viewing it as a student.
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment 1 name"
+    And I should see "80" in the ".feedback" "css_element"
+    And I should see "Marking guide test description" in the ".feedback" "css_element"
+    And I should see "Very good"
+    And I should see "Comment 4"
+    And I should see "Nice!"
+
+  Scenario: I can use marking guides to grade and edit them later updating students grades with Javascript disabled
index aba460f..64881d0 100644 (file)
@@ -1637,39 +1637,9 @@ class grade_report_grader extends grade_report {
      * Given a category element returns collapsing +/- icon if available
      *
      * @deprecated since Moodle 2.9 MDL-46662 - please do not use this function any more.
-     * @todo MDL-49021 This will be deleted in Moodle 3.1
-     * @see grade_report_grader::get_course_header()
-     * @param object $element
-     * @return string HTML
      */
     protected function get_collapsing_icon($element) {
-        global $OUTPUT;
-        debugging('get_collapsing_icon is deprecated, please use get_course_header instead.', DEBUG_DEVELOPER);
-
-        $icon = '';
-        // If object is a category, display expand/contract icon
-        if ($element['type'] == 'category') {
-            // Load language strings
-            $strswitchminus = $this->get_lang_string('aggregatesonly', 'grades');
-            $strswitchplus  = $this->get_lang_string('gradesonly', 'grades');
-            $strswitchwhole = $this->get_lang_string('fullmode', 'grades');
-
-            $url = new moodle_url($this->gpr->get_return_url(null, array('target'=>$element['eid'], 'sesskey'=>sesskey())));
-
-            if (in_array($element['object']->id, $this->collapsed['aggregatesonly'])) {
-                $url->param('action', 'switch_plus');
-                $icon = $OUTPUT->action_icon($url, new pix_icon('t/switch_plus', $strswitchplus));
-
-            } else if (in_array($element['object']->id, $this->collapsed['gradesonly'])) {
-                $url->param('action', 'switch_whole');
-                $icon = $OUTPUT->action_icon($url, new pix_icon('t/switch_whole', $strswitchwhole));
-
-            } else {
-                $url->param('action', 'switch_minus');
-                $icon = $OUTPUT->action_icon($url, new pix_icon('t/switch_minus', $strswitchminus));
-            }
-        }
-        return $icon;
+        throw new coding_exception('get_collapsing_icon() can not be used any more, please use get_course_header() instead.');
     }
 
     /**
index c0c0078..dc12e73 100644 (file)
@@ -153,6 +153,8 @@ $string['hidetypes'] = 'Hide type options';
 $string['importgeneralsettings'] = 'General import defaults';
 $string['importgeneralmaxresults'] = 'Maximum number of courses listed for import';
 $string['importgeneralmaxresults_desc'] = 'This controls the number of courses that are listed during the first step of the import process';
+$string['importgeneralduplicateadminallowed'] = 'Allow admin conflict resolution';
+$string['importgeneralduplicateadminallowed_desc'] = 'If the site has an account with username \'admin\', then attempting to restore a backup file containing an account with username \'admin\' can cause a conflict. If this setting is enabled, the conflict will be resolved by changing the username in the backup file to \'admin_xyz\'.';
 $string['importfile'] = 'Import a backup file';
 $string['importbackupstage1action'] = 'Next';
 $string['importbackupstage2action'] = 'Next';
index a44742e..3a2c783 100644 (file)
@@ -60,6 +60,7 @@ $string['cachedef_plugin_manager'] = 'Plugin info manager';
 $string['cachedef_questiondata'] = 'Question definitions';
 $string['cachedef_repositories'] = 'Repositories instances data';
 $string['cachedef_string'] = 'Language string cache';
+$string['cachedef_tags'] = 'Tags collections and areas';
 $string['cachedef_userselections'] = 'Data used to persist user selections throughout Moodle';
 $string['cachedef_yuimodules'] = 'YUI Module definitions';
 $string['cachelock_file_default'] = 'Default file locking';
index ab59bdb..366e26b 100644 (file)
@@ -20,3 +20,4 @@ updated,core_tag
 withselectedtags,core_tag
 tag:create,core_role
 categoriesanditems,core_grades
+taggedwith,core_tag
index 59d6080..b126d79 100644 (file)
@@ -1555,6 +1555,7 @@ $string['restorecancelled'] = 'Restore cancelled';
 $string['restorecannotassignroles'] = 'Restore needs to assign roles and you do not have permission to do so';
 $string['restorecannotcreateorassignroles'] = 'Restore needs to create or assign roles and you do not have permission to do so';
 $string['restorecannotcreateuser'] = 'Restore needs to create user \'{$a}\' from backup file and you do not have permission to do so';
+$string['restoremnethostidmismatch'] = 'MNet host id of user \'{$a}\' does not match local MNet host ID.';
 $string['restorecannotoverrideperms'] = 'Restore needs to override permissions and you do not have permission to do so';
 $string['restorecoursenow'] = 'Restore this course now!';
 $string['restoredaccount'] = 'Restored account';
index 2ce0f2c..c4ea1b4 100644 (file)
@@ -421,6 +421,7 @@ $string['submissionoutofsequencefriendlymessage'] = "You have entered data outsi
 $string['submit'] = 'Submit';
 $string['submitandfinish'] = 'Submit and finish';
 $string['submitted'] = 'Submit: {$a}';
+$string['tagarea_question'] = 'Questions';
 $string['technicalinfo'] = 'Technical information';
 $string['technicalinfo_help'] = 'This technical information is probably only useful for developers working on new question types. It may also be helpful when trying to diagnose problems with questions.';
 $string['technicalinfominfraction'] = 'Minimum fraction: {$a}';
index 6065fb4..e20a603 100644 (file)
 
 $string['added'] = 'Official tag(s) added';
 $string['addotags'] = 'Add official tags';
+$string['addtagcoll'] = 'Add tag collection';
 $string['addtagtomyinterests'] = 'Add "{$a}" to my interests';
 $string['alltagpages'] = 'All tag pages';
+$string['backtoallitems'] = 'Back to all items tagged with "{$a}"';
+$string['changessaved'] = 'Changes saved';
+$string['changetagcoll'] = 'Change tag collection of area {$a}';
+$string['collnameexplained'] = 'Leave the field empty to use the default value: {$a}';
+$string['component'] = 'Component';
 $string['confirmdeletetag'] = 'Are you sure you want to delete this tag?';
 $string['confirmdeletetags'] = 'Are you sure you want to delete selected tags?';
 $string['count'] = 'Count';
 $string['coursetags'] = 'Course tags';
+$string['defautltagcoll'] = 'Default collection';
 $string['delete'] = 'Delete';
 $string['deleteselected'] = 'Delete selected';
 $string['deleted'] = 'Tag(s) deleted';
@@ -38,15 +45,20 @@ $string['description'] = 'Description';
 $string['editname'] = 'Edit tag name';
 $string['edittag'] = 'Edit this tag';
 $string['entertags'] = 'Enter tags...';
+$string['edittagcoll'] = 'Edit tag collection {$a}';
 $string['errortagfrontpage'] = 'Tagging the site main page is not allowed';
 $string['errorupdatingrecord'] = 'Error updating tag record';
 $string['eventtagadded'] = 'Tag added to an item';
+$string['eventtagcolldeleted'] = 'Tag collection deleted';
+$string['eventtagcollcreated'] = 'Tag collection created';
+$string['eventtagcollupdated'] = 'Tag collection updatedhp ';
 $string['eventtagcreated'] = 'Tag created';
 $string['eventtagdeleted'] = 'Tag deleted';
 $string['eventtagflagged'] = 'Tag flagged';
 $string['eventtagremoved'] = 'Tag removed from an item';
 $string['eventtagunflagged'] = 'Tag unflagged';
 $string['eventtagupdated'] = 'Tag updated';
+$string['exclusivemode'] = 'Show only tagged {$a->tagarea}';
 $string['flag'] = 'Flag';
 $string['flagged'] = 'Tag flagged';
 $string['flagasinappropriate'] = 'Flag as inappropriate';
@@ -54,17 +66,25 @@ $string['helprelatedtags'] = 'Comma separated related tags';
 $string['changename'] = 'Change tag name';
 $string['changetype'] = 'Change tag type';
 $string['id'] = 'id';
+$string['inalltagcoll'] = 'Everywhere';
+$string['itemstaggedwith'] = '{$a->tagarea} tagged with "{$a->tag}"';
+$string['lesstags'] = 'less...';
 $string['manageofficialtags'] = 'Manage official tags';
 $string['managetags'] = 'Manage tags';
+$string['managetagcolls'] = 'Manage tag collections';
+$string['moretags'] = 'more...';
 $string['name'] = 'Tag name';
 $string['namesalreadybeeingused'] = 'Tag names already being used';
 $string['newnamefor'] = 'New name for tag {$a}';
+$string['nextpage'] = 'More';
+$string['notagsfound'] = 'No tags matching "{$a}" found';
 $string['noresultsfor'] = 'No results for "{$a}"';
 $string['nothingtoupdate'] = 'Nothing to update';
 $string['officialtag'] = 'Official';
 $string['otags'] = 'Official tags';
 $string['othertags'] = 'Other tags';
 $string['owner'] = 'Owner';
+$string['prevpage'] = 'Back';
 $string['ptags'] = 'User defined tags (Comma separated)';
 $string['relatedblogs'] = 'Most recent blog entries';
 $string['relatedtags'] = 'Related tags';
@@ -75,16 +95,29 @@ $string['responsiblewillbenotified'] = 'The person responsible will be notified'
 $string['rssdesc'] = 'This RSS feed was automatically generated by Moodle and contains user generated tags for courses.';
 $string['rsstitle'] = 'Course tags RSS feed for user: {$a}';
 $string['search'] = 'Search';
+$string['searchable'] = 'Searchable';
+$string['searchable_help'] = 'Tags in this tag collection can be searched for on "Search tags" page. If unchecked, tags can still be accessed by clicking on them or via different search interfaces.';
 $string['searchresultsfor'] = 'Search results for "{$a}"';
 $string['searchtags'] = 'Search tags';
-$string['seeallblogs'] = 'See all blog entries tagged with "{$a}"...';
+$string['seeallblogs'] = 'See all blog entries tagged with "{$a}"';
 $string['select'] = 'Select';
+$string['selectcoll'] = 'Select tag collection';
 $string['selecttag'] = 'Select tag {$a}';
 $string['settypedefault'] = 'Remove from official tags';
 $string['settypeofficial'] = 'Make official';
+$string['showingfirsttags'] = 'Showing {$a} most popular tags';
+$string['suredeletecoll'] = 'Are you sure you want to delete tag collection "{$a}"?';
 $string['tag'] = 'Tag';
+$string['tagarea_blog_external'] = 'External blog posts';
+$string['tagarea_post'] = 'Blog posts';
+$string['tagarea_user'] = 'User interests';
+$string['tagarea_course'] = 'Courses';
+$string['tagareaenabled'] = 'Enabled';
+$string['tagareaname'] = 'Name';
+$string['tagareas'] = 'Tag areas';
+$string['tagcollection'] = 'Tag collection';
+$string['tagcollections'] = 'Tag collections';
 $string['tagdescription'] = 'Tag description';
-$string['taggedwith'] = 'tagged with "{$a}"';
 $string['tags'] = 'Tags';
 $string['tagsaredisabled'] = 'Tags are disabled';
 $string['tagtype'] = 'Tag type';
@@ -107,3 +140,7 @@ $string['tagtype_official'] = 'Official';
 $string['thistaghasnodesc'] = 'This tag currently has no description.';
 $string['updated'] = 'Updated';
 $string['withselectedtags'] = 'With selected tags...';
+
+// Deprecated since 3.1 .
+
+$string['taggedwith'] = 'tagged with "{$a}"';
index 092ca07..6eeb775 100644 (file)
@@ -35,7 +35,6 @@ $string['addrequiredcapability'] = 'Assign/unassign the required capability';
 $string['addservice'] = 'Add a new service: {$a->name} (id: {$a->id})';
 $string['addservicefunction'] = 'Add functions to the service "{$a}"';
 $string['allusers'] = 'All users';
-$string['amftestclient'] = 'AMF test client';
 $string['apiexplorer'] = 'API explorer';
 $string['apiexplorernotavalaible'] = 'API explorer not available yet.';
 $string['arguments'] = 'Arguments';
@@ -185,7 +184,7 @@ $string['step'] = 'Step';
 $string['supplyinfo'] = 'More details';
 $string['testauserwithtestclientdescription'] = 'Simulate external access to the service using the web service test client. Before doing so, log in as a user with the moodle/webservice:createtoken capability and obtain the security key (token) via the user\'s preferences page. You will use this token in the test client. In the test client, also choose an enabled protocol with the token authentication. <strong>WARNING: The functions that you test WILL BE EXECUTED for this user, so be careful what you choose to test!</strong>';
 $string['testclient'] = 'Web service test client';
-$string['testclientdescription'] = '* The web service test client <strong>executes</strong> the functions for <strong>REAL</strong>. Do not test functions that you don\'t know. <br/>* All existing web service functions are not yet implemented into the test client. <br/>* In order to check that a user cannot access some functions, you can test some functions that you didn\'t allow.<br/>* To see clearer error messages set the debugging to <strong>{$a->mode}</strong> into {$a->atag}<br/>* Access the {$a->amfatag}.';
+$string['testclientdescription'] = '* The web service test client <strong>executes</strong> the functions for <strong>REAL</strong>. Do not test functions that you don\'t know. <br/>* All existing web service functions are not yet implemented into the test client. <br/>* In order to check that a user cannot access some functions, you can test some functions that you didn\'t allow.<br/>* To see clearer error messages set the debugging to <strong>{$a->mode}</strong> into {$a->atag}.';
 $string['testwithtestclient'] = 'Test the service';
 $string['testwithtestclientdescription'] = 'Simulate external access to the service using the web service test client. Use an enabled protocol with token authentication. <strong>WARNING: The functions that you test WILL BE EXECUTED, so be careful what you choose to test!</strong>';
 $string['token'] = 'Token';
index 81c95d0..c0ca283 100644 (file)
@@ -166,9 +166,8 @@ function uninstall_plugin($type, $name) {
 
     echo $OUTPUT->heading($pluginname);
 
-    // Delete all tag instances associated with this plugin.
-    require_once($CFG->dirroot . '/tag/lib.php');
-    tag_delete_instances($component);
+    // Delete all tag areas, collections and instances associated with this plugin.
+    core_tag_area::uninstall($component);
 
     // Custom plugin uninstall.
     $plugindirectory = core_component::get_plugin_directory($type, $name);
@@ -7612,29 +7611,17 @@ class admin_setting_managerepository extends admin_setting {
  */
 class admin_setting_enablemobileservice extends admin_setting_configcheckbox {
 
-    /** @var boolean True means that the capability 'webservice/xmlrpc:use' is set for authenticated user role */
-    private $xmlrpcuse;
     /** @var boolean True means that the capability 'webservice/rest:use' is set for authenticated user role */
     private $restuse;
 
     /**
-     * Return true if Authenticated user role has the capability 'webservice/xmlrpc:use' and 'webservice/rest:use', otherwise false.
+     * Return true if Authenticated user role has the capability 'webservice/rest:use', otherwise false.
      *
      * @return boolean
      */
     private function is_protocol_cap_allowed() {
         global $DB, $CFG;
 
-        // We keep xmlrpc enabled for backward compatibility.
-        // If the $this->xmlrpcuse variable is not set, it needs to be set.
-        if (empty($this->xmlrpcuse) and $this->xmlrpcuse!==false) {
-            $params = array();
-            $params['permission'] = CAP_ALLOW;
-            $params['roleid'] = $CFG->defaultuserroleid;
-            $params['capability'] = 'webservice/xmlrpc:use';
-            $this->xmlrpcuse = $DB->record_exists('role_capabilities', $params);
-        }
-
         // If the $this->restuse variable is not set, it needs to be set.
         if (empty($this->restuse) and $this->restuse!==false) {
             $params = array();
@@ -7644,11 +7631,11 @@ class admin_setting_enablemobileservice extends admin_setting_configcheckbox {
             $this->restuse = $DB->record_exists('role_capabilities', $params);
         }
 
-        return ($this->xmlrpcuse && $this->restuse);
+        return $this->restuse;
     }
 
     /**
-     * Set the 'webservice/xmlrpc:use'/'webservice/rest:use' to the Authenticated user role (allow or not)
+     * Set the 'webservice/rest:use' to the Authenticated user role (allow or not)
      * @param type $status true to allow, false to not set
      */
     private function set_protocol_cap($status) {
@@ -7664,7 +7651,6 @@ class admin_setting_enablemobileservice extends admin_setting_configcheckbox {
         }
         if (!empty($assign)) {
             $systemcontext = context_system::instance();
-            assign_capability('webservice/xmlrpc:use', $permission, $CFG->defaultuserroleid, $systemcontext->id, true);
             assign_capability('webservice/rest:use', $permission, $CFG->defaultuserroleid, $systemcontext->id, true);
         }
     }
@@ -7755,14 +7741,9 @@ class admin_setting_enablemobileservice extends admin_setting_configcheckbox {
              $mobileservice->enabled = 1;
              $webservicemanager->update_external_service($mobileservice);
 
-             //enable xml-rpc server
+             // Enable REST server.
              $activeprotocols = empty($CFG->webserviceprotocols) ? array() : explode(',', $CFG->webserviceprotocols);
 
-             if (!in_array('xmlrpc', $activeprotocols)) {
-                 $activeprotocols[] = 'xmlrpc';
-                 $updateprotocol = true;
-             }
-
              if (!in_array('rest', $activeprotocols)) {
                  $activeprotocols[] = 'rest';
                  $updateprotocol = true;
@@ -7772,7 +7753,7 @@ class admin_setting_enablemobileservice extends admin_setting_configcheckbox {
                  set_config('webserviceprotocols', implode(',', $activeprotocols));
              }
 
-             //allow xml-rpc:use capability for authenticated user
+             // Allow rest:use capability for authenticated user.
              $this->set_protocol_cap(true);
 
          } else {
@@ -7783,13 +7764,8 @@ class admin_setting_enablemobileservice extends admin_setting_configcheckbox {
              if (empty($otherenabledservices)) {
                  set_config('enablewebservices', false);
 
-                 //also disable xml-rpc server
+                 // Also disable REST server.
                  $activeprotocols = empty($CFG->webserviceprotocols) ? array() : explode(',', $CFG->webserviceprotocols);
-                 $protocolkey = array_search('xmlrpc', $activeprotocols);
-                 if ($protocolkey !== false) {
-                    unset($activeprotocols[$protocolkey]);
-                    $updateprotocol = true;
-                 }
 
                  $protocolkey = array_search('rest', $activeprotocols);
                  if ($protocolkey !== false) {
@@ -7801,7 +7777,7 @@ class admin_setting_enablemobileservice extends admin_setting_configcheckbox {
                     set_config('webserviceprotocols', implode(',', $activeprotocols));
                  }
 
-                 //disallow xml-rpc:use capability for authenticated user
+                 // Disallow rest:use capability for authenticated user.
                  $this->set_protocol_cap(false);
              }
 
index 8d684ae..7a4328f 100644 (file)
Binary files a/lib/amd/build/tag.min.js and b/lib/amd/build/tag.min.js differ
index 0edf6ef..01ef814 100644 (file)
@@ -27,9 +27,37 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
     return /** @alias module:core/tag */ {
 
         /**
-         * Initialises handlers for AJAX methods.
+         * Initialises tag index page.
          *
-         * @method init
+         * @method init_tagindex_page
+         */
+        init_tagindex_page: function() {
+            // Click handler for changing tag type.
+            $('body').delegate('.tagarea[data-ta] a[data-quickload=1]', 'click', function(e) {
+                e.preventDefault();
+                var target = $( this ),
+                    query = target.context.search.replace(/^\?/, ''),
+                    tagarea = target.closest('.tagarea[data-ta]'),
+                    args = query.split('&').reduce(function(s,c){var t=c.split('=');s[t[0]]=decodeURIComponent(t[1]);return s;},{});
+
+                var promises = ajax.call([{
+                    methodname: 'core_tag_get_tagindex',
+                    args: { tagindex: args }
+                }], true);
+
+                $.when.apply($, promises)
+                    .done( function(data) {
+                        templates.render('core_tag/index', data).done(function(html) {
+                            tagarea.replaceWith(html);
+                        });
+                    });
+            });
+        },
+
+        /**
+         * Initialises tag management page.
+         *
+         * @method init_manage_page
          */
         init_manage_page: function() {
 
index 91fcf4c..e33a956 100644 (file)
@@ -6,6 +6,5 @@ modifications:
 3/ replaced explode in iCalendar_component::unserialize() with preg_split to support various line breaks (20 Nov 2012)
 4/ updated rfc2445_is_valid_value() to accept single part rrule as a valid value (16 Jun 2014)
 5/ updated DTEND;TZID and DTSTAR;TZID values to support quotations (7 Nov 2014)
-6/ added calendar_normalize_tz function to convert region timezone to php supported timezone (7 Nov 2014)
-7/ MDL-49032: fixed rfc2445_fold() to fix incorrect RFC2445_WSP definition (16 Sep 2015)
-8/ added timestamp_to_date function to support zero duration events (16 Sept 2015)
+6/ MDL-49032: fixed rfc2445_fold() to fix incorrect RFC2445_WSP definition (16 Sep 2015)
+7/ added timestamp_to_date function to support zero duration events (16 Sept 2015)
index 56208b6..b045eb2 100644 (file)
@@ -1497,10 +1497,10 @@ class block_manager {
             if ($bits[0] == 'tag' && !empty($this->page->subpage)) {
                 // better navbar for tag pages
                 $editpage->navbar->add(get_string('tags'), new moodle_url('/tag/'));
-                $tag = tag_get('id', $this->page->subpage, '*');
+                $tag = core_tag_tag::get($this->page->subpage);
                 // tag search page doesn't have subpageid
                 if ($tag) {
-                    $editpage->navbar->add($tag->name, new moodle_url('/tag/index.php', array('id'=>$tag->id)));
+                    $editpage->navbar->add($tag->get_display_name(), $tag->get_view_url());
                 }
             }
             $editpage->navbar->add($block->get_title());
index 6316cc3..fecf439 100644 (file)
@@ -74,6 +74,34 @@ class tag_added extends base {
             s($this->other['itemtype']) . "' with id '{$this->other['itemid']}'.";
     }
 
+    /**
+     * Creates an event from taginstance object
+     *
+     * @since Moodle 3.1
+     * @param stdClass $taginstance
+     * @param string $tagname
+     * @param string $tagrawname
+     * @param bool $addsnapshot trust that $taginstance has all necessary fields and add it as a record snapshot
+     * @return tag_added
+     */
+    public static function create_from_tag_instance($taginstance, $tagname, $tagrawname, $addsnapshot = false) {
+        $event = self::create(array(
+            'objectid' => $taginstance->id,
+            'contextid' => $taginstance->contextid,
+            'other' => array(
+                'tagid' => $taginstance->tagid,
+                'tagname' => $tagname,
+                'tagrawname' => $tagrawname,
+                'itemid' => $taginstance->itemid,
+                'itemtype' => $taginstance->itemtype
+            )
+        ));
+        if ($addsnapshot) {
+            $event->add_record_snapshot('tag_instance', $taginstance);
+        }
+        return $event;
+    }
+
     /**
      * Return legacy data for add_to_log().
      *
diff --git a/lib/classes/event/tag_collection_created.php b/lib/classes/event/tag_collection_created.php
new file mode 100644 (file)
index 0000000..57573d0
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tag collection created event.
+ *
+ * @package    core
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tag collection created event class.
+ *
+ * @package    core
+ * @since      Moodle 3.0
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tag_collection_created extends base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tag_coll';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Utility method to create new event.
+     *
+     * @param object $tagcoll
+     * @return user_graded
+     */
+    public static function create_from_record($tagcoll) {
+        $event = self::create(array(
+            'objectid' => $tagcoll->id,
+            'context' => \context_system::instance(),
+        ));
+        $event->add_record_snapshot('tag_coll', $tagcoll);
+        return $event;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventtagcollcreated', 'core_tag');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' created the tag collection with id '$this->objectid'";
+    }
+}
diff --git a/lib/classes/event/tag_collection_deleted.php b/lib/classes/event/tag_collection_deleted.php
new file mode 100644 (file)
index 0000000..f9b332f
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tag collection deleted event.
+ *
+ * @package    core
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tag collection deleted event class.
+ *
+ * @package    core
+ * @since      Moodle 3.0
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tag_collection_deleted extends base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tag_coll';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Utility method to create new event.
+     *
+     * @param object $tagcoll
+     * @return user_graded
+     */
+    public static function create_from_record($tagcoll) {
+        $event = self::create(array(
+            'objectid' => $tagcoll->id,
+            'context' => \context_system::instance(),
+        ));
+        $event->add_record_snapshot('tag_coll', $tagcoll);
+        return $event;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventtagcolldeleted', 'core_tag');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' deleted the tag collection with id '$this->objectid'";
+    }
+}
diff --git a/lib/classes/event/tag_collection_updated.php b/lib/classes/event/tag_collection_updated.php
new file mode 100644 (file)
index 0000000..0417b0f
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tag collection updated event.
+ *
+ * @package    core
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tag collection updated event class.
+ *
+ * @package    core
+ * @since      Moodle 3.0
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tag_collection_updated extends base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tag_coll';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Utility method to create new event.
+     *
+     * @param object $tagcoll
+     * @return user_graded
+     */
+    public static function create_from_record($tagcoll) {
+        $event = self::create(array(
+            'objectid' => $tagcoll->id,
+            'context' => \context_system::instance(),
+        ));
+        $event->add_record_snapshot('tag_coll', $tagcoll);
+        return $event;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventtagcollupdated', 'core_tag');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' updated the tag collection with id '$this->objectid'";
+    }
+}
index e239b39..9573a49 100644 (file)
@@ -52,6 +52,26 @@ class tag_created extends base {
         $this->data['edulevel'] = self::LEVEL_OTHER;
     }
 
+    /**
+     * Creates an event from tag object
+     *
+     * @since Moodle 3.1
+     * @param \core_tag_tag|\stdClass $tag
+     * @return tag_created
+     */
+    public static function create_from_tag($tag) {
+        $event = self::create(array(
+            'objectid' => $tag->id,
+            'relateduserid' => $tag->userid,
+            'context' => \context_system::instance(),
+            'other' => array(
+                'name' => $tag->name,
+                'rawname' => $tag->rawname
+            )
+        ));
+        return $event;
+    }
+
     /**
      * Returns localised general event name.
      *
index 04558ac..faa3610 100644 (file)
@@ -74,6 +74,34 @@ class tag_removed extends base {
             s($this->other['itemtype']) . "' with id '{$this->other['itemid']}'.";
     }
 
+    /**
+     * Creates an event from taginstance object
+     *
+     * @since Moodle 3.1
+     * @param stdClass $taginstance
+     * @param string $tagname
+     * @param string $tagrawname
+     * @param bool $addsnapshot trust that $taginstance has all necessary fields and add it as a record snapshot
+     * @return tag_removed
+     */
+    public static function create_from_tag_instance($taginstance, $tagname, $tagrawname, $addsnapshot = false) {
+        $event = self::create(array(
+            'objectid' => $taginstance->id,
+            'contextid' => $taginstance->contextid,
+            'other' => array(
+                'tagid' => $taginstance->tagid,
+                'tagname' => $tagname,
+                'tagrawname' => $tagrawname,
+                'itemid' => $taginstance->itemid,
+                'itemtype' => $taginstance->itemtype
+            )
+        ));
+        if ($addsnapshot) {
+            $event->add_record_snapshot('tag_instance', $taginstance);
+        }
+        return $event;
+    }
+
     /**
      * Custom validation.
      *
index 6b1f3fc..108401b 100644 (file)
@@ -1665,6 +1665,7 @@ class core_plugin_manager {
             'theme' => array('mymobile', 'afterburner', 'anomaly', 'arialist', 'binarius', 'boxxie', 'brick', 'formal_white',
                 'formfactor', 'fusion', 'leatherbound', 'magazine', 'nimble', 'nonzero', 'overlay', 'serenity', 'sky_high',
                 'splash', 'standard', 'standardold'),
+            'webservice' => array('amf'),
         );
 
         if (!isset($plugins[$type])) {
@@ -1897,7 +1898,7 @@ class core_plugin_manager {
             ),
 
             'webservice' => array(
-                'amf', 'rest', 'soap', 'xmlrpc'
+                'rest', 'soap', 'xmlrpc'
             ),
 
             'workshopallocation' => array(
index cf3aeac..28cc82f 100644 (file)
@@ -26,11 +26,17 @@ namespace core\plugininfo;
 defined('MOODLE_INTERNAL') || die();
 
 /**
- * Class for admin tool plugins
+ * Class for cache store plugins
  */
 class cachestore extends base {
 
     public function is_uninstall_allowed() {
-        return false;
+        $instance = \cache_config::instance();
+        foreach ($instance->get_all_stores() as $store) {
+            if ($store['plugin'] == $this->name) {
+                return false;
+            }
+        }
+        return true;
     }
 }
index 38687d1..4d6666f 100644 (file)
@@ -532,6 +532,11 @@ class core_string_manager_standard implements core_string_manager {
         $langdirs = get_list_of_plugins('', 'en', $this->otherroot);
         $langdirs["$CFG->dirroot/lang/en"] = 'en';
 
+        // We use left to right mark to demark the shortcodes contained in LTR brackets, but we need to do
+        // this hacky thing to have the utf8 char until we go php7 minimum and can simply put \u200E in
+        // a double quoted string.
+        $lrm = json_decode('"\u200E"');
+
         // Loop through all langs and get info.
         foreach ($langdirs as $lang) {
             if (strrpos($lang, '_local') !== false) {
@@ -548,7 +553,7 @@ class core_string_manager_standard implements core_string_manager {
             }
             $string = $this->load_component_strings('langconfig', $lang);
             if (!empty($string['thislanguage'])) {
-                $languages[$lang] = $string['thislanguage'].' ('. $lang .')';
+                $languages[$lang] = $string['thislanguage'].' '.$lrm.'('. $lang .')'.$lrm;
             }
         }
 
index cdedf29..721edfb 100644 (file)
@@ -23,6 +23,8 @@
  */
 namespace core\task;
 
+use core_tag_collection, core_tag_tag, core_tag_area, stdClass;
+
 /**
  * Simple task to run the tag cron.
  */
@@ -45,9 +47,224 @@ class tag_cron_task extends scheduled_task {
         global $CFG;
 
         if (!empty($CFG->usetags)) {
-            require_once($CFG->dirroot.'/tag/lib.php');
-            tag_cron();
+            $this->compute_correlations();
+            $this->cleanup();
+        }
+    }
+
+    /**
+     * Calculates and stores the correlated tags of all tags.
+     *
+     * The correlations are stored in the 'tag_correlation' table.
+     *
+     * Two tags are correlated if they appear together a lot. Ex.: Users tagged with "computers"
+     * will probably also be tagged with "algorithms".
+     *
+     * The rationale for the 'tag_correlation' table is performance. It works as a cache
+     * for a potentially heavy load query done at the 'tag_instance' table. So, the
+     * 'tag_correlation' table stores redundant information derived from the 'tag_instance' table.
+     *
+     * @param int $mincorrelation Only tags with more than $mincorrelation correlations will be identified.
+     */
+    public function compute_correlations($mincorrelation = 2) {
+        global $DB;
+
+        // This mighty one line query fetches a row from the database for every
+        // individual tag correlation. We then need to process the rows collecting
+        // the correlations for each tag id.
+        // The fields used by this query are as follows:
+        //   tagid         : This is the tag id, there should be at least $mincorrelation
+        //                   rows for each tag id.
+        //   correlation   : This is the tag id that correlates to the above tagid field.
+        //   correlationid : This is the id of the row in the tag_correlation table that
+        //                   relates to the tagid field and will be NULL if there are no
+        //                   existing correlations.
+        $sql = 'SELECT pairs.tagid, pairs.correlation, pairs.ocurrences, co.id AS correlationid
+                  FROM (
+                           SELECT ta.tagid, tb.tagid AS correlation, COUNT(*) AS ocurrences
+                             FROM {tag_instance} ta
+                             JOIN {tag} tga ON ta.tagid = tga.id
+                             JOIN {tag_instance} tb ON (ta.itemtype = tb.itemtype AND ta.component = tb.component
+                                AND ta.itemid = tb.itemid AND ta.tagid <> tb.tagid)
+                             JOIN {tag} tgb ON tb.tagid = tgb.id AND tgb.tagcollid = tga.tagcollid
+                         GROUP BY ta.tagid, tb.tagid
+                           HAVING COUNT(*) > :mincorrelation
+                       ) pairs
+             LEFT JOIN {tag_correlation} co ON co.tagid = pairs.tagid
+              ORDER BY pairs.tagid ASC, pairs.ocurrences DESC, pairs.correlation ASC';
+        $rs = $DB->get_recordset_sql($sql, array('mincorrelation' => $mincorrelation));
+
+        // Set up an empty tag correlation object.
+        $tagcorrelation = new stdClass;
+        $tagcorrelation->id = null;
+        $tagcorrelation->tagid = null;
+        $tagcorrelation->correlatedtags = array();
+
+        // We store each correlation id in this array so we can remove any correlations
+        // that no longer exist.
+        $correlations = array();
+
+        // Iterate each row of the result set and build them into tag correlations.
+        // We add all of a tag's correlations to $tagcorrelation->correlatedtags[]
+        // then save the $tagcorrelation object.
+        foreach ($rs as $row) {
+            if ($row->tagid != $tagcorrelation->tagid) {
+                // The tag id has changed so we have all of the correlations for this tag.
+                $tagcorrelationid = $this->process_computed_correlation($tagcorrelation);
+                if ($tagcorrelationid) {
+                    $correlations[] = $tagcorrelationid;
+                }
+                // Now we reset the tag correlation object so we can reuse it and set it
+                // up for the current record.
+                $tagcorrelation = new stdClass;
+                $tagcorrelation->id = $row->correlationid;
+                $tagcorrelation->tagid = $row->tagid;
+                $tagcorrelation->correlatedtags = array();
+            }
+            // Save the correlation on the tag correlation object.
+            $tagcorrelation->correlatedtags[] = $row->correlation;
+        }
+        // Update the current correlation after the last record.
+        $tagcorrelationid = $this->process_computed_correlation($tagcorrelation);
+        if ($tagcorrelationid) {
+            $correlations[] = $tagcorrelationid;
+        }
+
+        // Close the recordset.
+        $rs->close();
+
+        // Remove any correlations that weren't just identified.
+        if (empty($correlations)) {
+            // There are no tag correlations.
+            $DB->delete_records('tag_correlation');
+        } else {
+            list($sql, $params) = $DB->get_in_or_equal($correlations,
+                    SQL_PARAMS_NAMED, 'param0000', false);
+            $DB->delete_records_select('tag_correlation', 'id '.$sql, $params);
+        }
+    }
+
+    /**
+     * Clean up the tag tables, making sure all tagged object still exists.
+     *
+     * This method is called from cron.
+     *
+     * This should normally not be necessary, but in case related tags are not deleted
+     * when the tagged record is removed, this should be done once in a while, perhaps
+     * on an occasional cron run.  On a site with lots of tags, this could become an
+     * expensive function to call.
+     */
+    public function cleanup() {
+        global $DB;
+
+        // Get ids to delete from instances where the tag has been deleted. This should never happen apparently.
+        $sql = "SELECT ti.id
+                  FROM {tag_instance} ti
+             LEFT JOIN {tag} t ON t.id = ti.tagid
+                 WHERE t.id IS null";
+        $tagids = $DB->get_records_sql($sql);
+        $tagarray = array();
+        foreach ($tagids as $tagid) {
+            $tagarray[] = $tagid->id;
         }
+
+        // Next get ids from instances that have an owner that has been deleted.
+        $sql = "SELECT ti.id
+                  FROM {tag_instance} ti, {user} u
+                 WHERE ti.itemid = u.id
+                   AND ti.itemtype = 'user'
+                   AND ti.component = 'core'
+                   AND u.deleted = 1";
+        $tagids = $DB->get_records_sql($sql);
+        foreach ($tagids as $tagid) {
+            $tagarray[] = $tagid->id;
+        }
+
+        // Get the other itemtypes.
+        $sql = "SELECT DISTINCT component, itemtype
+                  FROM {tag_instance}
+                 WHERE itemtype <> 'user' or component <> 'core'";
+        $tagareas = $DB->get_records_sql($sql);
+        foreach ($tagareas as $tagarea) {
+            $sql = 'SELECT ti.id
+                      FROM {tag_instance} ti
+                 LEFT JOIN {' . $tagarea->itemtype . '} it ON it.id = ti.itemid
+                     WHERE it.id IS null
+                     AND ti.itemtype = ? AND ti.component = ?';
+            $tagids = $DB->get_records_sql($sql, array($tagarea->itemtype, $tagarea->component));
+            foreach ($tagids as $tagid) {
+                $tagarray[] = $tagid->id;
+            }
+        }
+
+        // Get instances for each of the ids to be deleted.
+        if (count($tagarray) > 0) {
+            list($sqlin, $params) = $DB->get_in_or_equal($tagarray);
+            $sql = "SELECT ti.*, COALESCE(t.name, 'deleted') AS name, COALESCE(t.rawname, 'deleted') AS rawname
+                      FROM {tag_instance} ti
+                 LEFT JOIN {tag} t ON t.id = ti.tagid
+                     WHERE ti.id $sqlin";
+            $instances = $DB->get_records_sql($sql, $params);
+            $this->bulk_delete_instances($instances);
+        }
+
+        core_tag_collection::cleanup_unused_tags();
+    }
+
+    /**
+     * This function processes a tag correlation and makes changes in the database as required.
+     *
+     * The tag correlation object needs have both a tagid property and a correlatedtags property that is an array.
+     *
+     * @param   stdClass $tagcorrelation
+     * @return  int/bool The id of the tag correlation that was just processed or false.
+     */
+    public function process_computed_correlation(stdClass $tagcorrelation) {
+        global $DB;
+
+        // You must provide a tagid and correlatedtags must be set and be an array.
+        if (empty($tagcorrelation->tagid) || !isset($tagcorrelation->correlatedtags) ||
+                !is_array($tagcorrelation->correlatedtags)) {
+            return false;
+        }
+
+        $tagcorrelation->correlatedtags = join(',', $tagcorrelation->correlatedtags);
+        if (!empty($tagcorrelation->id)) {
+            // The tag correlation already exists so update it.
+            $DB->update_record('tag_correlation', $tagcorrelation);
+        } else {
+            // This is a new correlation to insert.
+            $tagcorrelation->id = $DB->insert_record('tag_correlation', $tagcorrelation);
+        }
+        return $tagcorrelation->id;
     }
 
+    /**
+     * This function will delete numerous tag instances efficiently.
+     * This removes tag instances only. It doesn't check to see if it is the last use of a tag.
+     *
+     * @param array $instances An array of tag instance objects with the addition of the tagname and tagrawname
+     *        (used for recording a delete event).
+     */
+    public function bulk_delete_instances($instances) {
+        global $DB;
+
+        $instanceids = array();
+        foreach ($instances as $instance) {
+            $instanceids[] = $instance->id;
+        }
+
+        // This is a multi db compatible method of creating the correct sql when using the 'IN' value.
+        // $insql is the sql statement, $params are the id numbers.
+        list($insql, $params) = $DB->get_in_or_equal($instanceids);
+        $sql = 'id ' . $insql;
+        $DB->delete_records_select('tag_instance', $sql, $params);
+
+        // Now go through and record each tag individually with the event system.
+        foreach ($instances as $instance) {
+            // Trigger tag removed event (i.e. The tag instance has been removed).
+            \core\event\tag_removed::create_from_tag_instance($instance, $instance->name,
+                    $instance->rawname, true)->trigger();
+        }
+    }
 }
index df83db6..fc8811f 100644 (file)
@@ -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();
index 7d90e55..dd8dc96 100644 (file)
@@ -252,6 +252,12 @@ $definitions = array(
         'simpledata' => true,
         'staticacceleration' => true,
         'staticaccelerationsize' => 5
+    ),
+
+    // Caches data about tag collections and areas.
+    'tags' => array(
+        'mode' => cache_store::MODE_REQUEST,
+        'simplekeys' => true,
     )
 
 );
index de8750a..3c9e420 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20150922" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20160111" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <KEY NAME="importer" TYPE="foreign" FIELDS="importer" REFTABLE="user" REFFIELDS="id" COMMENT="user who is importing"/>
       </KEYS>
     </TABLE>
+    <TABLE NAME="tag_coll" COMMENT="Defines different set of tags">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="isdefault" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="sortorder" TYPE="int" LENGTH="5" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="searchable" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Whether the tag collection is searchable"/>
+        <FIELD NAME="customurl" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Custom URL for the tag page instead of /tag/index.php"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+      </KEYS>
+    </TABLE>
+    <TABLE NAME="tag_area" COMMENT="Defines various tag areas, one area is identified by component and itemtype">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="itemtype" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="enabled" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
+        <FIELD NAME="tagcollid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="callback" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="callbackfile" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="tagcollid" TYPE="foreign" FIELDS="tagcollid" REFTABLE="tag_coll" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="compitemtype" UNIQUE="true" FIELDS="component, itemtype"/>
+      </INDEXES>
+    </TABLE>
     <TABLE NAME="tag" COMMENT="Tag table - this generic table will replace the old &quot;tags&quot; table.">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="tagcollid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="rawname" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="The raw, unnormalised name for the tag as entered by users"/>
         <FIELD NAME="tagtype" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
         <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+        <KEY NAME="tagcollid" TYPE="foreign" FIELDS="tagcollid" REFTABLE="tag_coll" REFFIELDS="id"/>
       </KEYS>
       <INDEXES>
-        <INDEX NAME="name" UNIQUE="true" FIELDS="name" COMMENT="tag names are unique"/>
+        <INDEX NAME="tagcollname" UNIQUE="true" FIELDS="tagcollid, name"/>
+        <INDEX NAME="tagcolltype" UNIQUE="false" FIELDS="tagcollid, tagtype"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="tag_correlation" COMMENT="The rationale for the 'tag_correlation' table is performance.   It works as a cache for a potentially heavy load query done at the 'tag_instance' table.   So, the 'tag_correlation' table stores redundant information derived from the 'tag_instance' table">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="tagid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" COMMENT="Defines the Moodle component which the tag was added to"/>
-        <FIELD NAME="itemtype" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false" COMMENT="Defines the Moodle component which the tag was added to"/>
+        <FIELD NAME="itemtype" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="itemid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The context id of the item that was tagged"/>
         <FIELD NAME="tiuserid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <KEY NAME="contextid" TYPE="foreign" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/>
       </KEYS>
       <INDEXES>
-        <INDEX NAME="itemtype-itemid-tagid-tiuserid" UNIQUE="true" FIELDS="itemtype, itemid, tagid, tiuserid"/>
+        <INDEX NAME="taggeditem" UNIQUE="true" FIELDS="component, itemtype, itemid, tiuserid, tagid"/>
+        <INDEX NAME="taglookup" UNIQUE="false" FIELDS="itemtype, component, tagid, contextid"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="groups" COMMENT="Each record represents a group.">
index d9867da..bc3230f 100644 (file)
@@ -1148,6 +1148,14 @@ $functions = array(
         'ajax'        => true
     ),
 
+    'core_tag_get_tagindex' => array(
+        'classname'   => 'core_tag_external',
+        'methodname'  => 'get_tagindex',
+        'description' => 'Gets tag index page for one tag and one tag area',
+        'type'        => 'read',
+        'ajax'        => true
+    ),
+
 );
 
 $services = array(
diff --git a/lib/db/tag.php b/lib/db/tag.php
new file mode 100644 (file)
index 0000000..2923bb5
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tag area definitions
+ *
+ * File db/tag.php lists all available tag areas in core or a plugin.
+ *
+ * Each tag area may have the following attributes:
+ *   - itemtype (required) - what is tagged. Must be name of the existing DB table
+ *   - component - component responsible for tagging, if the tag area is inside a
+ *     plugin the component must be the full frankenstyle name of the plugin
+ *   - collection - name of the custom tag collection that will be used to store
+ *     tags in this area. If specified aministrator will be able to neither add
+ *     any other tag areas to this collection nor move this tag area elsewhere
+ *   - searchable (only if collection is specified) - wether the tag collection
+ *     should be searchable on /tag/search.php
+ *   - customurl (only if collection is specified) - custom url to use instead of
+ *     /tag/search.php to display information about one tag
+ *   - callback - name of the function that returns items tagged with this tag,
+ *     see core_tag_tag::get_tag_index() and existing callbacks for more details,
+ *     callback should return instance of core_tag\output\tagindex
+ *   - callbackfile - file where callback is located (if not an autoloaded location)
+ *
+ * Language file must contain the human-readable names of the tag areas and
+ * collections (either in plugin language file or in component language file or
+ * lang/en/tag.php in case of core):
+ * - for item type "user":
+ *     $string['tagarea_user'] = 'Users';
+ * - for tag collection "mycollection":
+ *     $string['tagcollection_mycollection'] = 'My tag collection';
+ *
+ * @package   core
+ * @copyright 2015 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$tagareas = array(
+    array(
+        'itemtype' => 'user', // Users.
+        'component' => 'core',
+        'callback' => 'user_get_tagged_users',
+        'callbackfile' => '/user/lib.php',
+    ),
+    array(
+        'itemtype' => 'course', // Courses.
+        'component' => 'core',
+        'callback' => 'course_get_tagged_courses',
+        'callbackfile' => '/course/lib.php',
+    ),
+    array(
+        'itemtype' => 'question', // Questions.
+        'component' => 'core_question',
+    ),
+    array(
+        'itemtype' => 'post', // Blog posts.
+        'component' => 'core',
+        'callback' => 'blog_get_tagged_posts',
+        'callbackfile' => '/blog/lib.php',
+    ),
+    array(
+        'itemtype' => 'blog_external', // External blogs.
+        'component' => 'core',
+    ),
+);
index 9723d33..41991f1 100644 (file)
@@ -71,8 +71,8 @@ $tasks = array(
     array(
         'classname' => 'core\task\tag_cron_task',
         'blocking' => 0,
-        'minute' => '20',
-        'hour' => '*',
+        'minute' => 'R',
+        'hour' => '3',
         'day' => '*',
         'dayofweek' => '*',
         'month' => '*'
index 40b21c7..a88ad10 100644 (file)
@@ -4609,5 +4609,203 @@ function xmldb_main_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2016011300.01) {
+
+        // 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, 2016011300.01);
+    }
+
+    if ($oldversion < 2016011300.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, 2016011300.02);
+    }
+
+    if ($oldversion < 2016011300.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, 2016011300.03);
+    }
+
+    if ($oldversion < 2016011300.04) {
+
+        // 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, 2016011300.04);
+    }
+
+    if ($oldversion < 2016011300.05) {
+
+        $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, 2016011300.05);
+    }
+
+    if ($oldversion < 2016011300.06) {
+
+        // 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, 2016011300.06);
+    }
+
+    if ($oldversion < 2016011300.07) {
+
+        // 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, 2016011300.07);
+    }
+
+    if ($oldversion < 2016011301.00) {
+
+        // Force uninstall of deleted tool.
+        if (!file_exists("$CFG->dirroot/webservice/amf")) {
+            // Remove capabilities.
+            capabilities_cleanup('webservice_amf');
+            // Remove all other associated config.
+            unset_all_config_for_plugin('webservice_amf');
+        }
+        upgrade_main_savepoint(true, 2016011301.00);
+    }
+
     return true;
 }
index fb66931..084d2e8 100644 (file)
@@ -119,12 +119,9 @@ class mysql_sql_generator extends sql_generator {
      *       errors and changes of column data types.
      *
      * @deprecated since Moodle 2.9 MDL-49723 - please do not use this function any more.
-     * @param xmldb_field[]|database_column_info[] $columns
-     * @return int approximate row size in bytes
      */
     public function guess_antolope_row_size(array $columns) {
-        debugging('guess_antolope_row_size() is deprecated, please use guess_antelope_row_size() instead.', DEBUG_DEVELOPER);
-        return $this->guess_antelope_row_size($columns);
+        throw new coding_exception('guess_antolope_row_size() can not be used any more, please use guess_antelope_row_size() instead.');
     }
 
     /**
index cd38aa4..879fb48 100644 (file)
@@ -2116,24 +2116,9 @@ function enrol_cohort_search_cohorts(course_enrolment_manager $manager, $offset
  * $user2 will be null if viewing a user's recent conversations
  *
  * @deprecated since Moodle 2.9 MDL-49371 - please do not use this function any more.
- * @todo MDL-49290 This will be deleted in Moodle 3.1.
- * @param stdClass the first user
- * @param stdClass the second user or null
- * @return bool True if the current user is one of either $user1 or $user2
  */
 function message_current_user_is_involved($user1, $user2) {
-    global $USER;
-
-    debugging('message_current_user_is_involved() is deprecated, please do not use this function.', DEBUG_DEVELOPER);
-
-    if (empty($user1->id) || (!empty($user2) && empty($user2->id))) {
-        throw new coding_exception('Invalid user object detected. Missing id.');
-    }
-
-    if ($user1->id != $USER->id && (empty($user2) || $user2->id != $USER->id)) {
-        return false;
-    }
-    return true;
+    throw new coding_exception('message_current_user_is_involved() can not be used any more.');
 }
 
 /**
@@ -2185,86 +2170,9 @@ function profile_display_badges($userid, $courseid = 0) {
  * Adds user preferences elements to user edit form.
  *
  * @deprecated since Moodle 2.9 MDL-45774 - Please do not use this function any more.
- * @todo MDL-49784 Remove this function in Moodle 3.1
- * @param stdClass $user
- * @param moodleform $mform
- * @param array|null $editoroptions
- * @param array|null $filemanageroptions
  */
 function useredit_shared_definition_preferences($user, &$mform, $editoroptions = null, $filemanageroptions = null) {
-    global $CFG;
-
-    debugging('useredit_shared_definition_preferences() is deprecated.', DEBUG_DEVELOPER, backtrace);
-
-    $choices = array();
-    $choices['0'] = get_string('emaildisplayno');
-    $choices['1'] = get_string('emaildisplayyes');
-    $choices['2'] = get_string('emaildisplaycourse');
-    $mform->addElement('select', 'maildisplay', get_string('emaildisplay'), $choices);
-    $mform->setDefault('maildisplay', $CFG->defaultpreference_maildisplay);
-
-    $choices = array();
-    $choices['0'] = get_string('textformat');
-    $choices['1'] = get_string('htmlformat');
-    $mform->addElement('select', 'mailformat', get_string('emailformat'), $choices);
-    $mform->setDefault('mailformat', $CFG->defaultpreference_mailformat);
-
-    if (!empty($CFG->allowusermailcharset)) {
-        $choices = array();
-        $charsets = get_list_of_charsets();
-        if (!empty($CFG->sitemailcharset)) {
-            $choices['0'] = get_string('site').' ('.$CFG->sitemailcharset.')';
-        } else {
-            $choices['0'] = get_string('site').' (UTF-8)';
-        }
-        $choices = array_merge($choices, $charsets);
-        $mform->addElement('select', 'preference_mailcharset', get_string('emailcharset'), $choices);
-    }
-
-    $choices = array();
-    $choices['0'] = get_string('emaildigestoff');
-    $choices['1'] = get_string('emaildigestcomplete');
-    $choices['2'] = get_string('emaildigestsubjects');
-    $mform->addElement('select', 'maildigest', get_string('emaildigest'), $choices);
-    $mform->setDefault('maildigest', $CFG->defaultpreference_maildigest);
-    $mform->addHelpButton('maildigest', 'emaildigest');
-
-    $choices = array();
-    $choices['1'] = get_string('autosubscribeyes');
-    $choices['0'] = get_string('autosubscribeno');
-    $mform->addElement('select', 'autosubscribe', get_string('autosubscribe'), $choices);
-    $mform->setDefault('autosubscribe', $CFG->defaultpreference_autosubscribe);
-
-    if (!empty($CFG->forum_trackreadposts)) {
-        $choices = array();
-        $choices['0'] = get_string('trackforumsno');
-        $choices['1'] = get_string('trackforumsyes');
-        $mform->addElement('select', 'trackforums', get_string('trackforums'), $choices);
-        $mform->setDefault('trackforums', $CFG->defaultpreference_trackforums);
-    }
-
-    $editors = editors_get_enabled();
-    if (count($editors) > 1) {
-        $choices = array('' => get_string('defaulteditor'));
-        $firsteditor = '';
-        foreach (array_keys($editors) as $editor) {
-            if (!$firsteditor) {
-                $firsteditor = $editor;
-            }
-            $choices[$editor] = get_string('pluginname', 'editor_' . $editor);
-        }
-        $mform->addElement('select', 'preference_htmleditor', get_string('textediting'), $choices);
-        $mform->setDefault('preference_htmleditor', '');
-    } else {
-        // Empty string means use the first chosen text editor.
-        $mform->addElement('hidden', 'preference_htmleditor');
-        $mform->setDefault('preference_htmleditor', '');
-        $mform->setType('preference_htmleditor', PARAM_PLUGIN);
-    }
-
-    $mform->addElement('select', 'lang', get_string('preferredlanguage'), get_string_manager()->get_list_of_translations());
-    $mform->setDefault('lang', $CFG->lang);
-
+    throw new coding_exception('useredit_shared_definition_preferences() can not be used any more.');
 }
 
 
@@ -2272,12 +2180,9 @@ function useredit_shared_definition_preferences($user, &$mform, $editoroptions =
  * Convert region timezone to php supported timezone
  *
  * @deprecated since Moodle 2.9
- * @param string $tz value from ical file
- * @return string $tz php supported timezone
  */
 function calendar_normalize_tz($tz) {
-    debugging('calendar_normalize_tz() is deprecated, use core_date::normalise_timezone() instead', DEBUG_DEVELOPER);
-    return core_date::normalise_timezone($tz);
+    throw new coding_exception('calendar_normalize_tz() can not be used any more, please use core_date::normalise_timezone() instead.');
 }
 
 /**
@@ -2617,30 +2522,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 +2552,8 @@ function coursetag_store_keywords($tags, $courseid, $userid=0, $tagtype='officia
 function coursetag_delete_keyword($tagid, $userid, $courseid) {
     debugging('Function coursetag_delete_keyword() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
 
-    tag_delete_instance('course', $courseid, $tagid, $userid);
+    $tag = core_tag_tag::get($tagid);
+    core_tag_tag::remove_item_tag('core', 'course', $courseid, $tag->rawname, $userid);
 }
 
 /**
@@ -2709,21 +2602,428 @@ function coursetag_get_tagged_courses($tagid) {
  * @param   bool     $showfeedback if we should output a notification of the delete to the end user
  */
 function coursetag_delete_course_tags($courseid, $showfeedback=false) {
-    debugging('Function coursetag_delete_course_tags() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
+    debugging('Function coursetag_delete_course_tags() is deprecated. Use core_tag_tag::remove_all_item_tags().', DEBUG_DEVELOPER);
+
+    global $OUTPUT;
+    core_tag_tag::remove_all_item_tags('core', 'course', $courseid);
+
+    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;
+}
 
-    global $DB, $OUTPUT;
+/**
+ * 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);
+}
 
-    if ($taginstances = $DB->get_recordset_select('tag_instance', "itemtype = 'course' AND itemid = :courseid",
-        array('courseid' => $courseid), '', 'tagid, tiuserid')) {
+/**
+ * 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));
+    }
+}
 
-        foreach ($taginstances as $record) {
-            tag_delete_instance('course', $courseid, $record->tagid, $record->tiuserid);
+/**
+ * Get an array of tag ids associated to a record.
+ *
+ * @package  core_tag
+ * @deprecated since 3.1
+ * @param    string    $record_type the record type for which we want to get the tags
+ * @param    int       $record_id the record id for which we want to get the tags
+ * @return   array     tag ids, indexed and sorted by 'ordering'
+ */
+function tag_get_tags_ids($record_type, $record_id) {
+    debugging('Method tag_get_tags_ids() is deprecated. Please consider using core_tag_tag::get_item_tags() or similar methods.', DEBUG_DEVELOPER);
+    $tag_ids = array();
+    $tagobjects = core_tag_tag::get_item_tags(null, $record_type, $record_id);
+    foreach ($tagobjects as $tagobject) {
+        $tag = $tagobject->to_object();
+        if ( array_key_exists($tag->ordering, $tag_ids) ) {
+            $tag->ordering++;
         }
-        $taginstances->close();
+        $tag_ids[$tag->ordering] = $tag->id;
     }
+    ksort($tag_ids);
+    return $tag_ids;
+}
 
-    if ($showfeedback) {
-        echo $OUTPUT->notification(get_string('deletedcoursetags', 'tag'), 'notifysuccess');
+/**
+ * Returns the database ID of a set of tags.
+ *
+ * @deprecated since 3.1
+ * @param    mixed $tags one tag, or array of tags, to look for.
+ * @param    bool  $return_value specify the type of the returned value. Either TAG_RETURN_OBJECT, or TAG_RETURN_ARRAY (default).
+ *                               If TAG_RETURN_ARRAY is specified, an array will be returned even if only one tag was passed in $tags.
+ * @return   mixed tag-indexed array of ids (or objects, if second parameter is TAG_RETURN_OBJECT), or only an int, if only one tag
+ *                 is given *and* the second parameter is null. No value for a key means the tag wasn't found.
+ */
+function tag_get_id($tags, $return_value = null) {
+    global $CFG, $DB;
+    debugging('Method tag_get_id() is deprecated and can be replaced with core_tag_tag::get_by_name() or core_tag_tag::get_by_name_bulk(). ' .
+        'You need to specify tag collection when retrieving tag by name', DEBUG_DEVELOPER);
+
+    if (!is_array($tags)) {
+        if(is_null($return_value) || $return_value == TAG_RETURN_OBJECT) {
+            if ($tagobject = core_tag_tag::get_by_name(core_tag_collection::get_default(), $tags)) {
+                return $tagobject->id;
+            } else {
+                return 0;
+            }
+        }
+        $tags = array($tags);
+    }
+
+    $records = core_tag_tag::get_by_name_bulk(core_tag_collection::get_default(), $tags,
+        $return_value == TAG_RETURN_OBJECT ? '*' : 'id, name');
+    foreach ($records as $name => $record) {
+        if ($return_value != TAG_RETURN_OBJECT) {
+            $records[$name] = $record->id ? $record->id : null;
+        } else {
+            $records[$name] = $record->to_object();
+        }
+    }
+    return $records;
+}
+
+/**
+ * Change the "value" of a tag, and update the associated 'name'.
+ *
+ * @package  core_tag
+ * @deprecated since 3.1
+ * @param    int      $tagid  the id of the tag to modify
+ * @param    string   $newrawname the new rawname
+ * @return   bool     true on success, false otherwise
+ */
+function tag_rename($tagid, $newrawname) {
+    debugging('Function tag_rename() is deprecated and may be replaced with core_tag_tag::get($tagid)->update().', DEBUG_DEVELOPER);
+    if ($tag = core_tag_tag::get($tagid, '*')) {
+        return $tag->update(array('rawname' => $newrawname));
+    }
+    return false;
+}
+
+/**
+ * Delete one instance of a tag.  If the last instance was deleted, it will also delete the tag, unless its type is 'official'.
+ *
+ * @package  core_tag
+ * @deprecated since 3.1
+ * @param    string $record_type the type of the record for which to remove the instance
+ * @param    int    $record_id   the id of the record for which to remove the instance
+ * @param    int    $tagid       the tagid that needs to be removed
+ * @param    int    $userid      (optional) the userid
+ * @return   bool   true on success, false otherwise
+ */
+function tag_delete_instance($record_type, $record_id, $tagid, $userid = null) {
+    debugging('Function tag_delete_instance() is deprecated and replaced with core_tag_tag::remove_item_tag() instead. ' .
+        'Component is required for retrieving instances', DEBUG_DEVELOPER);
+    $tag = core_tag_tag::get($tagid);
+    core_tag_tag::remove_item_tag('', $record_type, $record_id, $tag->rawname, $userid);
+}
+
+/**
+ * Find all records tagged with a tag of a given type ('post', 'user', etc.)
+ *
+ * @package  core_tag
+ * @category tag
+ * @param    string   $tag       tag to look for
+ * @param    string   $type      type to restrict search to.  If null, every matching record will be returned
+ * @param    int      $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point.
+ * @param    int      $limitnum  (optional, required if $limitfrom is set) return a subset comprising this many records.
+ * @return   array of matching objects, indexed by record id, from the table containing the type requested
+ */
+function tag_find_records($tag, $type, $limitfrom='', $limitnum='') {
+    debugging('Function tag_find_records() is deprecated and replaced with core_tag_tag::get_by_name()->get_tagged_items(). '.
+        'You need to specify tag collection when retrieving tag by name', DEBUG_DEVELOPER);
+
+    if (!$tag || !$type) {
+        return array();
+    }
+
+    $tagobject = core_tag_tag::get_by_name(core_tag_area::get_collection('', $type), $tag);
+    return $tagobject->get_tagged_items('', $type, $limitfrom, $limitnum);
+}
+
+/**
+ * Adds one or more tag in the database.  This function should not be called directly : you should
+ * use tag_set.
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param   mixed    $tags     one tag, or an array of tags, to be created
+ * @param   string   $type     type of tag to be created ("default" is the default value and "official" is the only other supported
+ *                             value at this time). An official tag is kept even if there are no records tagged with it.
+ * @return array     $tags ids indexed by their lowercase normalized names. Any boolean false in the array indicates an error while
+ *                             adding the tag.
+ */
+function tag_add($tags, $type="default") {
+    debugging('Function tag_add() is deprecated. You can use core_tag_tag::create_if_missing(), however it should not be necessary ' .
+        'since tags are created automatically when assigned to items', DEBUG_DEVELOPER);
+    if (!is_array($tags)) {
+        $tags = array($tags);
+    }
+    $objects = core_tag_tag::create_if_missing(core_tag_collection::get_default(), $tags, $type === 'official');
+
+    // New function returns the tags in different format, for BC we keep the format that this function used to have.
+    $rv = array();
+    foreach ($objects as $name => $tagobject) {
+        if (isset($tagobject->id)) {
+            $rv[$tagobject->name] = $tagobject->id;
+        } else {
+            $rv[$name] = false;
+        }
+    }
+    return $rv;
+}
+
+/**
+ * Assigns a tag to a record; if the record already exists, the time and ordering will be updated.
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param string $record_type the type of the record that will be tagged
+ * @param int $record_id the id of the record that will be tagged
+ * @param string $tagid the tag id to set on the record.
+ * @param int $ordering the order of the instance for this record
+ * @param int $userid (optional) only required for course tagging
+ * @param string|null $component the component that was tagged
+ * @param int|null $contextid the context id of where this tag was assigned
+ * @return bool true on success, false otherwise
+ */
+function tag_assign($record_type, $record_id, $tagid, $ordering, $userid = 0, $component = null, $contextid = null) {
+    global $DB;
+    $message = 'Function tag_assign() is deprecated. Use core_tag_tag::set_item_tags() or core_tag_tag::add_item_tag() instead. ' .
+        'Tag instance ordering should not be set manually';
+    if ($component === null || $contextid === null) {
+        $message .= '. You should specify the component and contextid of the item being tagged in your call to tag_assign.';
+    }
+    debugging($message, DEBUG_DEVELOPER);
+
+    if ($contextid) {
+        $context = context::instance_by_id($contextid);
+    } else {
+        $context = context_system::instance();
+    }
+
+    // Get the tag.
+    $tag = $DB->get_record('tag', array('id' => $tagid), 'name, rawname', MUST_EXIST);
+
+    $taginstanceid = core_tag_tag::add_item_tag($component, $record_type, $record_id, $context, $tag->rawname, $userid);
+
+    // Alter the "ordering" of tag_instance. This should never be done manually and only remains here for the backward compatibility.
+    $taginstance = new stdClass();
+    $taginstance->id = $taginstanceid;
+    $taginstance->ordering     = $ordering;
+    $taginstance->timemodified = time();
+
+    $DB->update_record('tag_instance', $taginstance);
+
+    return true;
+}
+
+/**
+ * Count how many records are tagged with a specific tag.
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param   string   $record_type record to look for ('post', 'user', etc.)
+ * @param   int      $tagid       is a single tag id
+ * @return  int      number of mathing tags.
+ */
+function tag_record_count($record_type, $tagid) {
+    debugging('Method tag_record_count() is deprecated and replaced with core_tag_tag::get($tagid)->count_tagged_items(). '.
+        'Component is now required when retrieving tag instances.', DEBUG_DEVELOPER);
+    return core_tag_tag::get($tagid)->count_tagged_items('', $record_type);
+}
+
+/**
+ * Determine if a record is tagged with a specific tag
+ *
+ * @package core_tag
+ * @deprecated since 3.1
+ * @param   string   $record_type the record type to look for
+ * @param   int      $record_id   the record id to look for
+ * @param   string   $tag         a tag name
+ * @return  bool/int true if it is tagged, 0 (false) otherwise
+ */
+function tag_record_tagged_with($record_type, $record_id, $tag) {
+    debugging('Method tag_record_tagged_with() is deprecated and replaced with core_tag_tag::get($tagid)->is_item_tagged_with(). '.
+        'Component is now required when retrieving tag instances.', DEBUG_DEVELOPER);
+    return core_tag_tag::is_item_tagged_with('', $record_type, $record_id, $tag);
+}
+
+/**
+ * Flag a tag as inappropriate.
+ *
+ * @deprecated since 3.1
+ * @param int|array $tagids a single tagid, or an array of tagids
+ */
+function tag_set_flag($tagids) {
+    debugging('Function tag_set_flag() is deprecated and replaced with core_tag_tag::get($tagid)->flag().', DEBUG_DEVELOPER);
+    $tagids = (array) $tagids;
+    foreach ($tagids as $tagid) {
+        if ($tag = core_tag_tag::get($tagid, '*')) {
+            $tag->flag();
+        }
+    }
+}
+
+/**
+ * Remove the inappropriate flag on a tag.
+ *
+ * @deprecated since 3.1
+ * @param int|array $tagids a single tagid, or an array of tagids
+ */
+function tag_unset_flag($tagids) {
+    debugging('Function tag_unset_flag() is deprecated and replaced with core_tag_tag::get($tagid)->reset_flag().', DEBUG_DEVELOPER);
+    $tagids = (array) $tagids;
+    foreach ($tagids as $tagid) {
+        if ($tag = core_tag_tag::get($tagid, '*')) {
+            $tag->reset_flag();
+        }
+    }
+}
+
+/**
+ * Prints or returns a HTML tag cloud with varying classes styles depending on the popularity and type of each tag.
+ *
+ * @deprecated since 3.1
+ *
+ * @param    array     $tagset Array of tags to display
+ * @param    int       $nr_of_tags Limit for the number of tags to retu