Merge branch 'MDL-52486-master' of git://github.com/cameron1729/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 18 Jan 2016 11:19:55 +0000 (11:19 +0000)
committerDan Poltawski <dan@moodle.com>
Mon, 18 Jan 2016 11:19:55 +0000 (11:19 +0000)
257 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/admin_bookmarks/tests/behat/bookmark_admin_pages.feature [new file with mode: 0644]
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/rss_client/block_rss_client.php
blocks/rss_client/classes/output/block.php [new file with mode: 0644]
blocks/rss_client/classes/output/channel_image.php [new file with mode: 0644]
blocks/rss_client/classes/output/feed.php [new file with mode: 0644]
blocks/rss_client/classes/output/footer.php [new file with mode: 0644]
blocks/rss_client/classes/output/item.php [new file with mode: 0644]
blocks/rss_client/classes/output/renderer.php [new file with mode: 0644]
blocks/rss_client/templates/block.mustache [new file with mode: 0644]
blocks/rss_client/templates/channel_image.mustache [new file with mode: 0644]
blocks/rss_client/templates/feed.mustache [new file with mode: 0644]
blocks/rss_client/templates/footer.mustache [new file with mode: 0644]
blocks/rss_client/templates/item.mustache [new file with mode: 0644]
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/grading/form/rubric/edit_form.php
grade/grading/form/rubric/js/rubric.js
grade/grading/form/rubric/js/rubriceditor.js
grade/grading/form/rubric/lang/en/gradingform_rubric.php
grade/grading/form/rubric/lib.php
grade/grading/form/rubric/renderer.php
grade/grading/form/rubric/rubriceditor.php
grade/grading/form/rubric/tests/behat/behat_gradingform_rubric.php
grade/grading/form/rubric/tests/behat/edit_rubric.feature
grade/grading/form/rubric/tests/behat/reuse_own_rubrics.feature
grade/report/grader/lib.php
grade/report/history/index.php
grade/report/history/tests/behat/basic_functionality.feature
install/lang/hi/admin.php
install/lang/lt/moodle.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/externallib.php
message/lib.php
message/tests/externallib_test.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
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/lesson/lib.php
mod/lesson/tests/behat/lesson_outline_report.feature [new file with mode: 0644]
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/admin_bookmarks/tests/behat/bookmark_admin_pages.feature b/blocks/admin_bookmarks/tests/behat/bookmark_admin_pages.feature
new file mode 100644 (file)
index 0000000..1574fe8
--- /dev/null
@@ -0,0 +1,36 @@
+@block @block_admin_bookmarks
+Feature: Add a bookmarks to an admin pages
+  In order to speed up common tasks
+  As an admin
+  I need to add and access pages through bookmarks
+
+  Background:
+    Given I log in as "admin"
+    And I navigate to "Scheduled tasks" node in "Site administration > Server"
+    And I click on "Bookmark this page" "link" in the "Admin bookmarks" "block"
+    And I log out
+
+  # Test bookmark functionality using the "User profile fields" page as our bookmark.
+  Scenario: Admin page can be bookmarked
+    Given I log in as "admin"
+    And I navigate to "User profile fields" node in "Site administration > Users > Accounts"
+    When I click on "Bookmark this page" "link" in the "Admin bookmarks" "block"
+    Then I should see "User profile fields" in the "Admin bookmarks" "block"
+    # See the existing bookmark is there too.
+    And I should see "Scheduled tasks" in the "Admin bookmarks" "block"
+
+  Scenario: Admin page can be accessed through bookmarks block
+    Given I log in as "admin"
+    And I navigate to "Notifications" node in "Site administration"
+    And I click on "Scheduled tasks" "link" in the "Admin bookmarks" "block"
+    # Verify that we are on the right page.
+    Then I should see "Scheduled tasks" in the "h1" "css_element"
+
+  Scenario: Admin page can be removed from bookmarks
+    Given I log in as "admin"
+    And I navigate to "Notifications" node in "Site administration"
+    And I click on "Scheduled tasks" "link" in the "Admin bookmarks" "block"
+    When I click on "Unbookmark this page" "link" in the "Admin bookmarks" "block"
+    Then I should see "Bookmark deleted"
+    And I wait to be redirected
+    And I should not see "Scheduled tasks" in the "Admin bookmarks" "block"
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 68a13ac..e41e68c 100644 (file)
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
+/**
+ * Contains block_rss_client
+ * @package    block_rss_client
+ * @copyright  Daryl Hawes
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL
+ */
+
 /**
  * A block which displays Remote feeds
  *
         }
     }
 
+    /**
+     * Gets the footer, which is the channel link of the last feed in our list of feeds
+     *
+     * @param array $feedrecords The feed records from the database.
+     * @return block_rss_client\output\footer|null The renderable footer or null if none should be displayed.
+     */
+    protected function get_footer($feedrecords) {
+        $footer = null;
+
+        if ($this->config->block_rss_client_show_channel_link) {
+            global $CFG;
+            require_once($CFG->libdir.'/simplepie/moodle_simplepie.php');
+
+            $feedrecord     = array_pop($feedrecords);
+            $feed           = new moodle_simplepie($feedrecord->url);
+            $channellink    = new moodle_url($feed->get_link());
+
+            if (!empty($channellink)) {
+                $footer = new block_rss_client\output\footer($channellink);
+            }
+        }
+
+        return $footer;
+    }
+
     function get_content() {
         global $CFG, $DB;
 
             $maxentries = intval($CFG->block_rss_client_num_entries);
         }
 
-
         /* ---------------------------------
          * Begin Normal Display of Block Content
          * --------------------------------- */
 
-        $output = '';
-
+        $renderer = $this->page->get_renderer('block_rss_client');
+        $block = new \block_rss_client\output\block();
 
         if (!empty($this->config->rssid)) {
-            list($rss_ids_sql, $params) = $DB->get_in_or_equal($this->config->rssid);
-
-            $rss_feeds = $DB->get_records_select('block_rss_client', "id $rss_ids_sql", $params);
+            list($rssidssql, $params) = $DB->get_in_or_equal($this->config->rssid);
+            $rssfeeds = $DB->get_records_select('block_rss_client', "id $rssidssql", $params);
+
+            if (!empty($rssfeeds)) {
+                $showtitle = false;
+                if (count($rssfeeds) > 1) {
+                    // When many feeds show the title for each feed.
+                    $showtitle = true;
+                }
 
-            $showtitle = false;
-            if (count($rss_feeds) > 1) {
-                // when many feeds show the title for each feed
-                $showtitle = true;
-            }
+                foreach ($rssfeeds as $feed) {
+                    if ($renderablefeed = $this->get_feed($feed, $maxentries, $showtitle)) {
+                        $block->add_feed($renderablefeed);
+                    }
+                }
 
-            foreach($rss_feeds as $feed){
-                $output.= $this->get_feed_html($feed, $maxentries, $showtitle);
+                $footer = $this->get_footer($rssfeeds);
             }
         }
 
-        $this->content->text = $output;
+        $this->content->text = $renderer->render_block($block);
+        if (isset($footer)) {
+            $this->content->footer = $renderer->render_footer($footer);
+        }
 
         return $this->content;
     }
      * @param mixed feedrecord The feed record from the database
      * @param int maxentries The maximum number of entries to be displayed
      * @param boolean showtitle Should the feed title be displayed in html
-     * @return string html representing the rss feed content
+     * @return block_rss_client\output\feed|null The renderable feed or null of there is an error
      */
-    function get_feed_html($feedrecord, $maxentries, $showtitle){
+    public function get_feed($feedrecord, $maxentries, $showtitle) {
         global $CFG;
         require_once($CFG->libdir.'/simplepie/moodle_simplepie.php');
 
-        $feed = new moodle_simplepie($feedrecord->url);
+        $simplepiefeed = new moodle_simplepie($feedrecord->url);
 
         if(isset($CFG->block_rss_client_timeout)){
-            $feed->set_cache_duration($CFG->block_rss_client_timeout*60);
-        }
-
-        if ($CFG->debugdeveloper && $feed->error()) {
-            return '<p>'. $feedrecord->url .' Failed with code: '.$feed->error().'</p>';
+            $simplepiefeed->set_cache_duration($CFG->block_rss_client_timeout * 60);
         }
 
-        $r = ''; // return string
-
-        if($this->config->block_rss_client_show_channel_image){
-            if($image = $feed->get_image_url()){
-                $imagetitle = s($feed->get_image_title());
-                $imagelink  = $feed->get_image_link();
-
-                $r.='<div class="image" title="'.$imagetitle.'">'."\n";
-                if($imagelink){
-                    $r.='<a href="'.$imagelink.'">';
-                }
-                $r.='<img src="'.$image.'" alt="'.$imagetitle.'" />'."\n";
-                if($imagelink){
-                    $r.='</a>';
-                }
-                $r.= '</div>';
-            }
+        if ($simplepiefeed->error()) {
+            debugging($feedrecord->url .' Failed with code: '.$simplepiefeed->error());
+            return null;
         }
 
         if(empty($feedrecord->preferredtitle)){
-            $feedtitle = $this->format_title($feed->get_title());
+            $feedtitle = $this->format_title($simplepiefeed->get_title());
         }else{
             $feedtitle = $this->format_title($feedrecord->preferredtitle);
         }
 
-        if($showtitle){
-            $r.='<div class="title">'.$feedtitle.'</div>';
-        }
-
-
-        $r.='<ul class="list no-overflow">'."\n";
-
-        $feeditems = $feed->get_items(0, $maxentries);
-        foreach($feeditems as $item){
-            $r.= $this->get_item_html($item);
-        }
-
-        $r.='</ul>';
-
-
-        if ($this->config->block_rss_client_show_channel_link) {
-
-            $channellink = $feed->get_link();
-
-            if (!empty($channellink)){
-                //NOTE: this means the 'last feed' display wins the block title - but
-                //this is exiting behaviour..
-                $this->content->footer = '<a href="'.htmlspecialchars(clean_param($channellink,PARAM_URL)).'">'. get_string('clientchannellink', 'block_rss_client') .'</a>';
-            }
-        }
-
         if (empty($this->config->title)){
             //NOTE: this means the 'last feed' displayed wins the block title - but
             //this is exiting behaviour..
             $this->title = strip_tags($feedtitle);
         }
 
-        return $r;
-    }
-
-
-    /**
-     * Returns the html list item of a feed item
-     *
-     * @param mixed item simplepie_item representing the feed item
-     * @return string html li representing the rss feed item
-     */
-    function get_item_html($item){
-
-        $link        = $item->get_link();
-        $title       = $item->get_title();
-        $description = $item->get_description();
-
-
-        if(empty($title)){
-            // no title present, use portion of description
-            $title = core_text::substr(strip_tags($description), 0, 20) . '...';
-        }else{
-            $title = break_up_long_words($title, 30);
+        $feed = new \block_rss_client\output\feed($feedtitle, $showtitle, $this->config->block_rss_client_show_channel_image);
+
+        if ($simplepieitems = $simplepiefeed->get_items(0, $maxentries)) {
+            foreach ($simplepieitems as $simplepieitem) {
+                try {
+                    $item = new \block_rss_client\output\item(
+                        $simplepieitem->get_id(),
+                        new moodle_url($simplepieitem->get_link()),
+                        $simplepieitem->get_title(),
+                        $simplepieitem->get_description(),
+                        new moodle_url($simplepieitem->get_permalink()),
+                        $simplepieitem->get_date('U'),
+                        $this->config->display_description
+                    );
+
+                    $feed->add_item($item);
+                } catch (moodle_exception $e) {
+                    // If there is an error with the RSS item, we don't
+                    // want to crash the page. Specifically, moodle_url can
+                    // throw an exception of the param is an extremely
+                    // malformed url.
+                    debugging($e->getMessage());
+                }
+            }
         }
 
-        if(empty($link)){
-            $link = $item->get_id();
-        } else {
+        // Feed image.
+        if ($imageurl = $simplepiefeed->get_image_url()) {
             try {
-                // URLs in our RSS cache will be escaped (correctly as theyre store in XML)
-                // html_writer::link() will re-escape them. To prevent double escaping unescape here.
-                // This can by done using htmlspecialchars_decode() but moodle_url also has that effect.
-                $link = new moodle_url($link);
+                $image = new \block_rss_client\output\channel_image(
+                    new moodle_url($imageurl),
+                    $simplepiefeed->get_image_title(),
+                    new moodle_url($simplepiefeed->get_image_link())
+                );
+
+                $feed->set_image($image);
             } catch (moodle_exception $e) {
-                // Catching the exception to prevent the whole site to crash in case of malformed RSS feed
-                $link = '';
+                // If there is an error with the RSS image, we don'twant to
+                // crash the page. Specifically, moodle_url can throw an
+                // exception if the param is an extremely malformed url.
+                debugging($e->getMessage());
             }
         }
 
-        $r = html_writer::start_tag('li');
-            $r.= html_writer::start_tag('div',array('class'=>'link'));
-                $r.= html_writer::link($link, s($title), array('onclick'=>'this.target="_blank"'));
-            $r.= html_writer::end_tag('div');
-
-            if($this->config->display_description && !empty($description)){
-
-                $formatoptions = new stdClass();
-                $formatoptions->para = false;
-
-                $r.= html_writer::start_tag('div',array('class'=>'description'));
-                    $description = format_text($description, FORMAT_HTML, $formatoptions, $this->page->course->id);
-                    $description = break_up_long_words($description, 30);
-                    $r.= $description;
-                $r.= html_writer::end_tag('div');
-            }
-        $r.= html_writer::end_tag('li');
-
-        return $r;
+        return $feed;
     }
 
     /**
         return $newskiptime;
     }
 }
-
-
diff --git a/blocks/rss_client/classes/output/block.php b/blocks/rss_client/classes/output/block.php
new file mode 100644 (file)
index 0000000..7789f0c
--- /dev/null
@@ -0,0 +1,104 @@
+<?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/>.
+
+/**
+ * Contains class block_rss_client\output\block
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_rss_client\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class to help display an RSS Feeds block
+ *
+ * @package   block_rss_client
+ * @copyright 2016 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block implements \renderable, \templatable {
+
+    /**
+     * An array of renderable feeds
+     *
+     * @var array
+     */
+    protected $feeds;
+
+    /**
+     * Contruct
+     *
+     * @param array $feeds An array of renderable feeds
+     */
+    public function __construct(array $feeds = array()) {
+        $this->feeds = $feeds;
+    }
+
+    /**
+     * Prepare data for use in a template
+     *
+     * @param \renderer_base $output
+     * @return array
+     */
+    public function export_for_template(\renderer_base $output) {
+        $data = array('feeds' => array());
+
+        foreach ($this->feeds as $feed) {
+            $data['feeds'][] = $feed->export_for_template($output);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Add a feed
+     *
+     * @param \block_rss_client\output\feed $feed
+     * @return \block_rss_client\output\block
+     */
+    public function add_feed(feed $feed) {
+        $this->feeds[] = $feed;
+
+        return $this;
+    }
+
+    /**
+     * Set the feeds
+     *
+     * @param array $feeds
+     * @return \block_rss_client\output\block
+     */
+    public function set_feeds(array $feeds) {
+        $this->feeds = $feeds;
+
+        return $this;
+    }
+
+    /**
+     * Get feeds
+     *
+     * @return array
+     */
+    public function get_feeds() {
+        return $this->feeds;
+    }
+}
diff --git a/blocks/rss_client/classes/output/channel_image.php b/blocks/rss_client/classes/output/channel_image.php
new file mode 100644 (file)
index 0000000..af9e22f
--- /dev/null
@@ -0,0 +1,151 @@
+<?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/>.
+
+/**
+ * Contains class block_rss_client\output\channel_image
+ *
+ * @package   block_rss_client
+ * @copyright 2016 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_rss_client\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class to display RSS channel images
+ *
+ * @package   block_rss_client
+ * @copyright 2016 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class channel_image implements \renderable, \templatable {
+
+    /**
+     * The URL location of the image
+     *
+     * @var string
+     */
+    protected $url;
+
+    /**
+     * The title of the image
+     *
+     * @var string
+     */
+    protected $title;
+
+    /**
+     * The URL of the image link
+     *
+     * @var string
+     */
+    protected $link;
+
+    /**
+     * Contructor
+     *
+     * @param \moodle_url $url The URL location of the image
+     * @param string $title The title of the image
+     * @param \moodle_url $link The URL of the image link
+     */
+    public function __construct(\moodle_url $url, $title, \moodle_url $link = null) {
+        $this->url      = $url;
+        $this->title    = $title;
+        $this->link     = $link;
+    }
+
+    /**
+     * Export this for use in a mustache template context.
+     *
+     * @see templatable::export_for_template()
+     * @param renderer_base $output
+     * @return array The data for the template
+     */
+    public function export_for_template(\renderer_base $output) {
+        return array(
+            'url'   => clean_param($this->url, PARAM_URL),
+            'title' => $this->title,
+            'link'  => clean_param($this->link, PARAM_URL),
+        );
+    }
+
+    /**
+     * Set the URL
+     *
+     * @param \moodle_url $url
+     * @return \block_rss_client\output\channel_image
+     */
+    public function set_url(\moodle_url $url) {
+        $this->url = $url;
+
+        return $this;
+    }
+
+    /**
+     * Get the URL
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return $this->url;
+    }
+
+    /**
+     * Set the title
+     *
+     * @param string $title
+     * @return \block_rss_client\output\channel_image
+     */
+    public function set_title($title) {
+        $this->title = $title;
+
+        return $this;
+    }
+
+    /**
+     * Get the title
+     *
+     * @return string
+     */
+    public function get_title() {
+        return $this->title;
+    }
+
+    /**
+     * Set the link
+     *
+     * @param \moodle_url $link
+     * @return \block_rss_client\output\channel_image
+     */
+    public function set_link($link) {
+        $this->link = $link;
+
+        return $this;
+    }
+
+    /**
+     * Get the link
+     *
+     * @return \moodle_url
+     */
+    public function get_link() {
+        return $this->link;
+    }
+}
diff --git a/blocks/rss_client/classes/output/feed.php b/blocks/rss_client/classes/output/feed.php
new file mode 100644 (file)
index 0000000..02f7e2d
--- /dev/null
@@ -0,0 +1,224 @@
+<?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/>.
+
+/**
+ * Contains class block_rss_client\output\feed
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_rss_client\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class to help display an RSS Feed
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class feed implements \renderable, \templatable {
+
+    /**
+     * The feed's title
+     *
+     * @var string
+     */
+    protected $title = null;
+
+    /**
+     * An array of renderable feed items
+     *
+     * @var array
+     */
+    protected $items = array();
+
+    /**
+     * The channel image
+     *
+     * @var channel_image
+     */
+    protected $image = null;
+
+    /**
+     * Whether or not to show the title
+     *
+     * @var boolean
+     */
+    protected $showtitle;
+
+    /**
+     * Whether or not to show the channel image
+     *
+     * @var boolean
+     */
+    protected $showimage;
+
+    /**
+     * Contructor
+     *
+     * @param string $title The title of the RSS feed
+     * @param boolean $showtitle Whether to show the title
+     * @param boolean $showimage Whether to show the channel image
+     */
+    public function __construct($title, $showtitle = true, $showimage = true) {
+        $this->title = $title;
+        $this->showtitle = $showtitle;
+        $this->showimage = $showimage;
+    }
+
+    /**
+     * Export this for use in a mustache template context.
+     *
+     * @see templatable::export_for_template()
+     * @param renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(\renderer_base $output) {
+        $data = array(
+            'title' => $this->showtitle ? $this->title : null,
+            'image' => null,
+            'items' => array(),
+        );
+
+        if ($this->showimage && $this->image) {
+            $data['image'] = $this->image->export_for_template($output);
+        }
+
+        foreach ($this->items as $item) {
+            $data['items'][] = $item->export_for_template($output);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Set the feed title
+     *
+     * @param string $title
+     * @return \block_rss_client\output\feed
+     */
+    public function set_title($title) {
+        $this->title = $title;
+
+        return $this;
+    }
+
+    /**
+     * Get the feed title
+     *
+     * @return string
+     */
+    public function get_title() {
+        return $this->title;
+    }
+
+    /**
+     * Add an RSS item
+     *
+     * @param \block_rss_client\output\item $item
+     */
+    public function add_item(item $item) {
+        $this->items[] = $item;
+
+        return $this;
+    }
+
+    /**
+     * Set the RSS items
+     *
+     * @param array $items An array of renderable RSS items
+     */
+    public function set_items(array $items) {
+        $this->items = $items;
+
+        return $this;
+    }
+
+    /**
+     * Get the RSS items
+     *
+     * @return array An array of renderable RSS items
+     */
+    public function get_items() {
+        return $this->items;
+    }
+
+    /**
+     * Set the channel image
+     *
+     * @param \block_rss_client\output\channel_image $image
+     */
+    public function set_image(channel_image $image) {
+        $this->image = $image;
+    }
+
+    /**
+     * Get the channel image
+     *
+     * @return channel_image
+     */
+    public function get_image() {
+        return $this->image;
+    }
+
+    /**
+     * Set showtitle
+     *
+     * @param boolean $showtitle
+     * @return \block_rss_client\output\feed
+     */
+    public function set_showtitle($showtitle) {
+        $this->showtitle = boolval($showtitle);
+
+        return $this;
+    }
+
+    /**
+     * Get showtitle
+     *
+     * @return boolean
+     */
+    public function get_showtitle() {
+        return $this->showtitle;
+    }
+
+    /**
+     * Set showimage
+     *
+     * @param boolean $showimage
+     * @return \block_rss_client\output\feed
+     */
+    public function set_showimage($showimage) {
+        $this->showimage = boolval($showimage);
+
+        return $this;
+    }
+
+    /**
+     * Get showimage
+     *
+     * @return boolean
+     */
+    public function get_showimage() {
+        return $this->showimage;
+    }
+}
diff --git a/blocks/rss_client/classes/output/footer.php b/blocks/rss_client/classes/output/footer.php
new file mode 100644 (file)
index 0000000..3da2039
--- /dev/null
@@ -0,0 +1,90 @@
+<?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/>.
+
+/**
+ * Contains class block_rss_client\output\footer
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_rss_client\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class to help display an RSS Block footer
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class footer implements \renderable, \templatable {
+
+    /**
+     * The link provided in the RSS channel
+     *
+     * @var \moodle_url
+     */
+    protected $channelurl;
+
+    /**
+     * Constructor
+     *
+     * @param \moodle_url $channelurl The link provided in the RSS channel
+     */
+    public function __construct(\moodle_url $channelurl) {
+        $this->channelurl = $channelurl;
+    }
+
+    /**
+     * Set the channel url
+     *
+     * @param \moodle_url $channelurl
+     * @return \block_rss_client\output\footer
+     */
+    public function set_channelurl(\moodle_url $channelurl) {
+        $this->channelurl = $channelurl;
+
+        return $this;
+    }
+
+    /**
+     * Get the channel url
+     *
+     * @return \moodle_url
+     */
+    public function get_channelurl() {
+        return $this->channelurl;
+    }
+
+    /**
+     * Export context for use in mustache templates
+     *
+     * @see templatable::export_for_template()
+     * @param renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(\renderer_base $output) {
+        $data = new \stdClass();
+        $data->channellink = clean_param($this->channelurl, PARAM_URL);
+
+        return $data;
+    }
+}
diff --git a/blocks/rss_client/classes/output/item.php b/blocks/rss_client/classes/output/item.php
new file mode 100644 (file)
index 0000000..71a71dc
--- /dev/null
@@ -0,0 +1,286 @@
+<?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/>.
+
+/**
+ * Contains class block_rss_client\output\feed
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_rss_client\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class to help display an RSS Item
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class item implements \renderable, \templatable {
+
+    /**
+     * The unique id of the item
+     *
+     * @var string
+     */
+    protected $id;
+
+    /**
+     * The link to the item
+     *
+     * @var \moodle_url
+     */
+    protected $link;
+
+    /**
+     * The title of the item
+     *
+     * @var string
+     */
+    protected $title;
+
+    /**
+     * The description of the item
+     *
+     * @var string
+     */
+    protected $description;
+
+    /**
+     * The item's permalink
+     *
+     * @var \moodle_url
+     */
+    protected $permalink;
+
+    /**
+     * The publish date of the item in Unix timestamp format
+     *
+     * @var int
+     */
+    protected $timestamp;
+
+    /**
+     * Whether or not to show the item's description
+     *
+     * @var string
+     */
+    protected $showdescription;
+
+    /**
+     * Contructor
+     *
+     * @param string $id The id of the RSS item
+     * @param \moodle_url $link The URL of the RSS item
+     * @param string $title The title pf the RSS item
+     * @param string $description The description of the RSS item
+     * @param \moodle_url $permalink The permalink of the RSS item
+     * @param int $timestamp The Unix timestamp that represents the published date
+     * @param boolean $showdescription Whether or not to show the description
+     */
+    public function __construct($id, \moodle_url $link, $title, $description, \moodle_url $permalink, $timestamp,
+            $showdescription = true) {
+        $this->id               = $id;
+        $this->link             = $link;
+        $this->title            = $title;
+        $this->description      = $description;
+        $this->permalink        = $permalink;
+        $this->timestamp        = $timestamp;
+        $this->showdescription  = $showdescription;
+    }
+
+    /**
+     * Export context for use in mustache templates
+     *
+     * @see templatable::export_for_template()
+     * @param renderer_base $output
+     * @return array
+     */
+    public function export_for_template(\renderer_base $output) {
+        $data = array(
+            'id'            => $this->id,
+            'permalink'     => clean_param($this->permalink, PARAM_URL),
+            'datepublished' => $output->format_published_date($this->timestamp),
+            'link'          => clean_param($this->link, PARAM_URL),
+        );
+
+        // If the item does not have a title, create one from the description.
+        $title = $this->title;
+        if (!$title) {
+            $title = strip_tags($this->description);
+            $title = core_text::substr($title, 0, 20) . '...';
+        }
+
+        // Allow the renderer to format the title and description.
+        $data['title']          = $output->format_title($title);
+        $data['description']    = $this->showdescription ? $output->format_description($this->description) : null;
+
+        return $data;
+    }
+
+    /**
+     * Set id
+     *
+     * @param string $id
+     * @return \block_rss_client\output\item
+     */
+    public function set_id($id) {
+        $this->id = $id;
+
+        return $this;
+    }
+
+    /**
+     * Get id
+     *
+     * @return string
+     */
+    public function get_id() {
+        return $this->id;
+    }
+
+    /**
+     * Set link
+     *
+     * @param \moodle_url $link
+     * @return \block_rss_client\output\item
+     */
+    public function set_link(\moodle_url $link) {
+        $this->link = $link;
+
+        return $this;
+    }
+
+    /**
+     * Get link
+     *
+     * @return \moodle_url
+     */
+    public function get_link() {
+        return $this->link;
+    }
+
+    /**
+     * Set title
+     *
+     * @param string $title
+     * @return \block_rss_client\output\item
+     */
+    public function set_title($title) {
+        $this->title = $title;
+
+        return $this;
+    }
+
+    /**
+     * Get title
+     *
+     * @return string
+     */
+    public function get_title() {
+        return $this->title;
+    }
+
+    /**
+     * Set description
+     *
+     * @param string $description
+     * @return \block_rss_client\output\item
+     */
+    public function set_description($description) {
+        $this->description = $description;
+
+        return $this;
+    }
+
+    /**
+     * Get description
+     *
+     * @return string
+     */
+    public function get_description() {
+        return $this->description;
+    }
+
+    /**
+     * Set permalink
+     *
+     * @param string $permalink
+     * @return \block_rss_client\output\item
+     */
+    public function set_permalink($permalink) {
+        $this->permalink = $permalink;
+
+        return $this;
+    }
+
+    /**
+     * Get permalink
+     *
+     * @return string
+     */
+    public function get_permalink() {
+        return $this->permalink;
+    }
+
+    /**
+     * Set timestamp
+     *
+     * @param int $timestamp
+     * @return \block_rss_client\output\item
+     */
+    public function set_timestamp($timestamp) {
+        $this->timestamp = $timestamp;
+
+        return $this;
+    }
+
+    /**
+     * Get timestamp
+     *
+     * @return string
+     */
+    public function get_timestamp() {
+        return $this->timestamp;
+    }
+
+    /**
+     * Set showdescription
+     *
+     * @param boolean $showdescription
+     * @return \block_rss_client\output\item
+     */
+    public function set_showdescription($showdescription) {
+        $this->showdescription = boolval($showdescription);
+
+        return $this;
+    }
+
+    /**
+     * Get showdescription
+     *
+     * @return boolean
+     */
+    public function get_showdescription() {
+        return $this->showdescription;
+    }
+}
diff --git a/blocks/rss_client/classes/output/renderer.php b/blocks/rss_client/classes/output/renderer.php
new file mode 100644 (file)
index 0000000..7a03280
--- /dev/null
@@ -0,0 +1,121 @@
+<?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/>.
+
+/**
+ * Contains class block_rss_client\output\block_renderer_html
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_rss_client\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Renderer for RSS Client block
+ *
+ * @package   block_rss_client
+ * @copyright 2015 Howard County Public School System
+ * @author    Brendan Anderson <brendan_anderson@hcpss.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends \plugin_renderer_base {
+
+    /**
+     * Render an RSS Item
+     *
+     * @param templatable $item
+     * @return string|boolean
+     */
+    public function render_item(\templatable $item) {
+        $data = $item->export_for_template($this);
+
+        return $this->render_from_template('block_rss_client/item', $data);
+    }
+
+    /**
+     * Render an RSS Feed
+     *
+     * @param templatable $feed
+     * @return string|boolean
+     */
+    public function render_feed(\templatable $feed) {
+        $data = $feed->export_for_template($this);
+
+        return $this->render_from_template('block_rss_client/feed', $data);
+    }
+
+    /**
+     * Render an RSS feeds block
+     *
+     * @param \templatable $block
+     * @return string|boolean
+     */
+    public function render_block(\templatable $block) {
+        $data = $block->export_for_template($this);
+
+        return $this->render_from_template('block_rss_client/block', $data);
+    }
+
+    /**
+     * Render the block footer
+     *
+     * @param templatable $footer
+     * @return string|boolean
+     */
+    public function render_footer(\templatable $footer) {
+        $data = $footer->export_for_template($this);
+
+        return $this->render_from_template('block_rss_client/footer', $data);
+    }
+
+    /**
+     * Format a timestamp to use as a published date
+     *
+     * @param int $timestamp Unix timestamp
+     * @return string
+     */
+    public function format_published_date($timestamp) {
+        return strftime(get_string('strftimerecentfull', 'langconfig'), $timestamp);
+        return date('j F Y, g:i a', $timestamp);
+    }
+
+    /**
+     * Format an RSS item title
+     *
+     * @param string $title
+     * @return string
+     */
+    public function format_title($title) {
+        return break_up_long_words($title, 30);
+    }
+
+    /**
+     * Format an RSS item description
+     *
+     * @param string $description
+     * @return string
+     */
+    public function format_description($description) {
+        $description = format_text($description, FORMAT_HTML, array('para' => false));
+        $description = break_up_long_words($description, 30);
+
+        return $description;
+    }
+}
diff --git a/blocks/rss_client/templates/block.mustache b/blocks/rss_client/templates/block.mustache
new file mode 100644 (file)
index 0000000..6cc2c71
--- /dev/null
@@ -0,0 +1,91 @@
+{{!
+    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 block_rss_client/block
+
+    Template which defines an RSS Feeds block
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * feeds - array: An array of RSS feeds.
+
+    Example context (json):
+    {
+        "feeds": [
+            {
+                "title": "News from around my living room",
+                "image": {
+                    "url": "https://www.example.com/feeds/news/poster.jpg",
+                    "title": "Example News Logo",
+                    "link": "https://www.example.com/feeds/news/"
+                },
+                "items": [
+                    {
+                        "id": "https://www.example.com/node/12",
+                        "link": "https://www.example.com/my-turtle-story.html",
+                        "title": "My Turtle Story",
+                        "description": "This is a story about my turtle.",
+                        "permalink": "https://www.example.com/my-turtle-story.html",
+                        "datepublished": "11 January 2016, 7:11 pm"
+                    },
+                    {
+                        "id": "https://www.example.com/node/12",
+                        "link": "https://www.example.com/my-cat-story.html",
+                        "title": "My Story",
+                        "description": "This is a story about my cats.",
+                        "permalink": "https://www.example.com/my-cat-story.html",
+                        "datepublished": "12 January 2016, 9:12 pm"
+                    }
+                ]
+            },
+            {
+                "title": "News from around my kitchen",
+                "image": {
+                    "url": "https://www.example.com/feeds/news/kitchen.jpg",
+                    "title": "Picture of My Kitchen",
+                    "link": "https://www.example.com/feeds/news/kitchen/"
+                },
+                "items": [
+                    {
+                        "id": "https://www.example.com/node/10",
+                        "link": "https://www.example.com/oven-smoke.html",
+                        "title": "Why is the Oven Smoking?",
+                        "description": "There is something smoking in the oven.",
+                        "permalink": "https://www.example.com/oven-smoke.html",
+                        "datepublished": "10 January 2016, 1:13 pm"
+                    },
+                    {
+                        "id": "https://www.example.com/node/13",
+                        "link": "https://www.example.com/coffee-is-good.html",
+                        "title": "Why My Coffee Machine is So Great!",
+                        "description": "Don't be fancy; drips are best.",
+                        "permalink": "https://www.example.com/oven-smoke.html",
+                        "datepublished": "13 January 2016, 8:25 pm"
+                    }
+                ]
+            }
+        ]
+    }
+}}
+{{#feeds}}
+    {{> block_rss_client/feed}}
+{{/feeds}}
diff --git a/blocks/rss_client/templates/channel_image.mustache b/blocks/rss_client/templates/channel_image.mustache
new file mode 100644 (file)
index 0000000..f20166e
--- /dev/null
@@ -0,0 +1,50 @@
+{{!
+    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 block_rss_client/channel_image
+
+    Template which defines an item in an RSS Feed
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * url - string: The escaped URL of the image.
+    * title - string: The title of the image.
+    * link - string: Optionally, a URL to link the image to. Must be escaped.
+
+    Example context (json):
+    {
+        "url": "http://www.example.com/images/catpic.jpg",
+        "title": "A picture of my cat",
+        "link": "http://www.example.com/cat-news/"
+    }
+}}
+<div class="image" title="{{title}}">
+    {{#link}}
+        <a href="{{{link}}}">
+    {{/link}}
+
+    <img src="{{{url}}}" alt="{{title}}" />
+
+    {{#link}}
+        </a>
+    {{/link}}
+</div>
diff --git a/blocks/rss_client/templates/feed.mustache b/blocks/rss_client/templates/feed.mustache
new file mode 100644 (file)
index 0000000..ad9ae3e
--- /dev/null
@@ -0,0 +1,79 @@
+{{!
+    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 block_rss_client/feed
+
+    Template which defines an item in an RSS Feed
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * channel_image - object: URL, title and link for the channel image.
+    * title - string: The title of the feed.
+    * items - array: An array of feed items.
+
+    Example context (json):
+    {
+        "title": "News from around my living room",
+        "image": {
+            "url": "https://www.example.com/feeds/news/poster.jpg",
+            "title": "Example News Logo",
+            "link": "https://www.example.com/feeds/news/"
+        },
+        "feeditems": [
+            {
+                "id": "https://www.example.com/node/12",
+                "link": "https://www.example.com/my-turtle-story.html",
+                "title": "My Turtle Story",
+                "description": "This is a story about my turtle.",
+                "permalink": "https://www.example.com/my-turtle-story.html",
+                "datepublished": "11 January 2016, 7:11 pm"
+            },
+            {
+                "id": "https://www.example.com/node/12",
+                "link": "https://www.example.com/my-cat-story.html",
+                "title": "My Story",
+                "description": "This is a story about my cats.",
+                "permalink": "https://www.example.com/my-cat-story.html",
+                "datepublished": "12 January 2016, 9:12 pm"
+            }
+        ]
+    }
+}}
+{{$image}}
+    {{#image}}
+        {{> block_rss_client/channel_image}}
+    {{/image}}
+{{/image}}
+
+{{$title}}
+    {{#title}}
+        <div class="title">{{feedtitle}}</div>
+    {{/title}}
+{{/title}}
+
+{{$items}}
+    <ul class="list no-overflow">
+        {{#items}}
+            {{> block_rss_client/item}}
+        {{/items}}
+    </ul>
+{{/items}}
diff --git a/blocks/rss_client/templates/footer.mustache b/blocks/rss_client/templates/footer.mustache
new file mode 100644 (file)
index 0000000..b1aa373
--- /dev/null
@@ -0,0 +1,36 @@
+{{!
+    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 block_rss_client/footer
+
+    Template which defines an item in an RSS Feed
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * channellink - string: The channel URL. Must be escaped.
+
+    Example context (json):
+    {
+        "channellink": "https://www.example.com/feeds/rss"
+    }
+}}
+<a href="{{{channellink}}}">{{#str}} clientchannellink, block_rss_client {{/str}}</a>
diff --git a/blocks/rss_client/templates/item.mustache b/blocks/rss_client/templates/item.mustache
new file mode 100644 (file)
index 0000000..b21bf11
--- /dev/null
@@ -0,0 +1,60 @@
+{{!
+    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 block_rss_client/item
+
+    Template which defines an item in an RSS Feed
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * id - string: A unique id for the feed item.
+    * link - string: The URL of the feed item. Must already be escaped.
+    * title - string: The title of the feed item.
+    * description - string: The text description of the feed item.
+    * permalink - string: The permalink of the feed item. Must already be escaped.
+    * datepublished - string: The date the feed item was published.
+
+    Example context (json):
+    {
+        "id": "https://www.example.com/node",
+        "link": "https://www.example.com/my-cat-story.html",
+        "title": "My Story",
+        "description": "This is a story about my cats.",
+        "permalink": "https://www.example.com/my-cat-story.html",
+        "datepublished": "12 January 2016, 9:12 pm"
+    }
+}}
+<li>
+    {{$title}}
+        <div class="link">
+            <a href="{{{link}}}" onclick='this.target="_blank"'>{{title}}</a>
+        </div>
+    {{/title}}
+
+    {{$content}}
+        {{#description}}
+            <div class="description">
+                {{{description}}}
+            </div>
+        {{/description}}
+    {{/content}}
+</li>
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 94e329a..67aaf11 100644 (file)
@@ -50,8 +50,8 @@ class gradingform_rubric_editrubric extends moodleform {
         $form->setType('returnurl', PARAM_LOCALURL);
 
         // name
-        $form->addElement('text', 'name', get_string('name', 'gradingform_rubric'), array('size'=>52));
-        $form->addRule('name', get_string('required'), 'required');
+        $form->addElement('text', 'name', get_string('name', 'gradingform_rubric'), array('size' => 52, 'aria-required' => 'true'));
+        $form->addRule('name', get_string('required'), 'required', null, 'client');
         $form->setType('name', PARAM_TEXT);
 
         // description
index d44639b..dbed9a8 100644 (file)
@@ -5,6 +5,10 @@ M.gradingform_rubric = {};
  */
 M.gradingform_rubric.init = function(Y, options) {
     Y.on('click', M.gradingform_rubric.levelclick, '#rubric-'+options.name+' .level', null, Y, options.name);
+    // Capture also space and enter keypress.
+    Y.on('key', M.gradingform_rubric.levelclick, '#rubric-' + options.name + ' .level', 'space', Y, options.name);
+    Y.on('key', M.gradingform_rubric.levelclick, '#rubric-' + options.name + ' .level', 'enter', Y, options.name);
+
     Y.all('#rubric-'+options.name+' .radio').setStyle('display', 'none')
     Y.all('#rubric-'+options.name+' .level').each(function (node) {
       if (node.one('input[type=radio]').get('checked')) {
@@ -19,12 +23,19 @@ M.gradingform_rubric.levelclick = function(e, Y, name) {
     if (!el) return
     e.preventDefault();
     el.siblings().removeClass('checked');
+
+    // Set aria-checked attribute for siblings to false.
+    el.siblings().setAttribute('aria-checked', 'false');
     chb = el.one('input[type=radio]')
     if (!chb.get('checked')) {
         chb.set('checked', true)
         el.addClass('checked')
+        // Set aria-checked attribute to true if checked.
+        el.setAttribute('aria-checked', 'true');
     } else {
         el.removeClass('checked');
+        // Set aria-checked attribute to false if unchecked.
+        el.setAttribute('aria-checked', 'false');
         el.get('parentNode').all('input[type=radio]').set('checked', false)
     }
 }
index 5304b82..7b84e29 100644 (file)
@@ -143,9 +143,11 @@ M.gradingform_rubriceditor.buttonclick = function(e, confirmed) {
         elements_str = '#rubric-'+name+' .criterion'
     }
     // prepare the id of the next inserted level or criterion
+    var newlevid = 0;
+    var newid = 0;
     if (action == 'addcriterion' || action == 'addlevel' || action == 'duplicate' ) {
-        var newid = M.gradingform_rubriceditor.calculatenewid('#rubric-'+name+' .criterion')
-        var newlevid = M.gradingform_rubriceditor.calculatenewid('#rubric-'+name+' .level')
+        newid = M.gradingform_rubriceditor.calculatenewid('#rubric-'+name+' .criterion');
+        newlevid = M.gradingform_rubriceditor.calculatenewid('#rubric-'+name+' .level');
     }
     var dialog_options = {
         'scope' : this,
@@ -164,7 +166,10 @@ M.gradingform_rubriceditor.buttonclick = function(e, confirmed) {
         for (levidx;levidx<3;levidx++) levelsscores[levidx] = parseFloat(levelsscores[levidx-1])+1
         var levelsstr = '';
         for (levidx=0;levidx<levelsscores.length;levidx++) {
-            levelsstr += M.gradingform_rubriceditor.templates[name]['level'].replace(/\{LEVEL-id\}/g, 'NEWID'+(newlevid+levidx)).replace(/\{LEVEL-score\}/g, levelsscores[levidx])
+            levelsstr += M.gradingform_rubriceditor.templates[name].level.
+                replace(/\{LEVEL-id\}/g, 'NEWID'+(newlevid+levidx)).
+                replace(/\{LEVEL-score\}/g, levelsscores[levidx]).
+                replace(/\{LEVEL-index\}/g, levidx + 1);
         }
         var newcriterion = M.gradingform_rubriceditor.templates[name]['criterion'].replace(/\{LEVELS\}/, levelsstr)
         parentel.append(newcriterion.replace(/\{CRITERION-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, ''))
@@ -172,14 +177,23 @@ M.gradingform_rubriceditor.buttonclick = function(e, confirmed) {
         M.gradingform_rubriceditor.addhandlers();
         M.gradingform_rubriceditor.disablealleditors()
         M.gradingform_rubriceditor.assignclasses(elements_str)
-        M.gradingform_rubriceditor.editmode(Y.one('#rubric-'+name+' #'+name+'-criteria-NEWID'+newid+'-description'),true)
+        M.gradingform_rubriceditor.editmode(
+            Y.one('#rubric-' + name + ' #' + name + '-criteria-NEWID' + newid + '-description-cell'), true
+        );
     } else if (chunks.length == 5 && action == 'addlevel') {
         // ADD NEW LEVEL
         var newscore = 0;
         parent = Y.one('#'+name+'-criteria-'+chunks[2]+'-levels')
-        parent.all('.level').each(function (node) { newscore = Math.max(newscore, parseFloat(node.one('.score input[type=text]').get('value'))+1) })
+        var levelIndex = 1;
+        parent.all('.level').each(function (node) {
+            newscore = Math.max(newscore, parseFloat(node.one('.score input[type=text]').get('value')) + 1);
+            levelIndex++;
+        });
         var newlevel = M.gradingform_rubriceditor.templates[name]['level'].
-            replace(/\{CRITERION-id\}/g, chunks[2]).replace(/\{LEVEL-id\}/g, 'NEWID'+newlevid).replace(/\{LEVEL-score\}/g, newscore).replace(/\{.+?\}/g, '')
+            replace(/\{CRITERION-id\}/g, chunks[2]).replace(/\{LEVEL-id\}/g, 'NEWID'+newlevid).
+            replace(/\{LEVEL-score\}/g, newscore).
+            replace(/\{LEVEL-index\}/g, levelIndex).
+            replace(/\{.+?\}/g, '');
         parent.append(newlevel)
         M.gradingform_rubriceditor.addhandlers();
         M.gradingform_rubriceditor.disablealleditors()
@@ -238,7 +252,7 @@ M.gradingform_rubriceditor.buttonclick = function(e, confirmed) {
         M.gradingform_rubriceditor.addhandlers();
         M.gradingform_rubriceditor.disablealleditors();
         M.gradingform_rubriceditor.assignclasses(elements_str);
-        M.gradingform_rubriceditor.editmode(Y.one('#rubric-'+name+' #'+name+'-criteria-NEWID'+newid+'-description'),true);
+        M.gradingform_rubriceditor.editmode(Y.one('#rubric-'+name+' #'+name+'-criteria-NEWID'+newid+'-description-cell'),true);
     } else if (chunks.length == 6 && action == 'delete') {
         // DELETE LEVEL
         if (confirmed) {
index f23b914..cf9455d 100644 (file)
@@ -29,12 +29,14 @@ $string['alwaysshowdefinition'] = 'Allow users to preview rubric used in the mod
 $string['backtoediting'] = 'Back to editing';
 $string['confirmdeletecriterion'] = 'Are you sure you want to delete this criterion?';
 $string['confirmdeletelevel'] = 'Are you sure you want to delete this level?';
+$string['criterion'] = 'Criterion {$a}';
 $string['criterionaddlevel'] = 'Add level';
 $string['criteriondelete'] = 'Delete criterion';
 $string['criterionduplicate'] = 'Duplicate criterion';
 $string['criterionempty'] = 'Click to edit criterion';
 $string['criterionmovedown'] = 'Move down';
 $string['criterionmoveup'] = 'Move up';
+$string['criterionremark'] = 'Remark for criterion {$a->description}: {$a->remark}';
 $string['definerubric'] = 'Define rubric';
 $string['description'] = 'Description';
 $string['enableremarks'] = 'Allow grader to add text remarks for each criterion';
@@ -45,8 +47,11 @@ $string['err_nodescription'] = 'Criterion description can not be empty';
 $string['err_scoreformat'] = 'Number of points for each level must be a valid non-negative number';
 $string['err_totalscore'] = 'Maximum number of points possible when graded by the rubric must be more than zero';
 $string['gradingof'] = '{$a} grading';
-$string['leveldelete'] = 'Delete level';
+$string['level'] = 'Level {$a->definition}, {$a->score} points.';
+$string['leveldelete'] = 'Delete level {$a}';
+$string['leveldefinition'] = 'Level {$a} definition';
 $string['levelempty'] = 'Click to edit level';
+$string['levelsgroup'] = 'Levels group';
 $string['name'] = 'Name';
 $string['needregrademessage'] = 'The rubric definition was changed after this student had been graded. The student can not see this rubric until you check the rubric and update the grade.';
 $string['pluginname'] = 'Rubric';
@@ -68,6 +73,7 @@ $string['rubricstatus'] = 'Current rubric status';
 $string['save'] = 'Save';
 $string['saverubric'] = 'Save rubric and make it ready';
 $string['saverubricdraft'] = 'Save as draft';
+$string['scoreinputforlevel'] = 'Score input for level {$a}';
 $string['scorepostfix'] = '{$a}points';
 $string['showdescriptionstudent'] = 'Display rubric description to those being graded';
 $string['showdescriptionteacher'] = 'Display rubric description during evaluation';
index d4d4b6f..9504b42 100644 (file)
@@ -927,7 +927,8 @@ class gradingform_rubric_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_rubric'), array('class' => 'gradingform_rubric-regrade'));
+            $html .= html_writer::div(get_string('needregrademessage', 'gradingform_rubric'), 'gradingform_rubric-regrade',
+                                      array('role' => 'alert'));
         }
         $haschanges = false;
         if ($currentinstance) {
index c539535..6ea8a4b 100644 (file)
@@ -74,12 +74,22 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
             foreach (array('moveup', 'delete', 'movedown', 'duplicate') as $key) {
                 $value = get_string('criterion'.$key, 'gradingform_rubric');
                 $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));
                 $criteriontemplate .= html_writer::tag('div', $button, array('class' => $key));
             }
+            $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
+                                                                        'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]',
+                                                                        'value' => $criterion['sortorder']));
             $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']));
-            $description = html_writer::tag('textarea', s($criterion['description']), array('name' => '{NAME}[criteria][{CRITERION-id}][description]', 'cols' => '10', 'rows' => '5'));
+
+            // Criterion description text area.
+            $descriptiontextareaparams = array(
+                'name' => '{NAME}[criteria][{CRITERION-id}][description]',
+                'id' => '{NAME}-criteria-{CRITERION-id}-description',
+                'aria-label' => get_string('criterion', 'gradingform_rubric', ''),
+                'cols' => '10', 'rows' => '5'
+            );
+            $description = html_writer::tag('textarea', s($criterion['description']), $descriptiontextareaparams);
         } else {
             if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
                 $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder']));
@@ -91,8 +101,35 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
         if (isset($criterion['error_description'])) {
             $descriptionclass .= ' error';
         }
-        $criteriontemplate .= html_writer::tag('td', $description, array('class' => $descriptionclass, 'id' => '{NAME}-criteria-{CRITERION-id}-description'));
-        $levelsstrtable = html_writer::tag('table', html_writer::tag('tr', $levelsstr, array('id' => '{NAME}-criteria-{CRITERION-id}-levels')));
+
+        // Description cell params.
+        $descriptiontdparams = array(
+            'class' => $descriptionclass,
+            'id' => '{NAME}-criteria-{CRITERION-id}-description-cell'
+        );
+        if ($mode != gradingform_rubric_controller::DISPLAY_EDIT_FULL &&
+            $mode != gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
+            // Set description's cell as tab-focusable.
+            $descriptiontdparams['tabindex'] = '0';
+            // Set label for the criterion cell.
+            $descriptiontdparams['aria-label'] = get_string('criterion', 'gradingform_rubric', s($criterion['description']));
+        }
+
+        // Description cell.
+        $criteriontemplate .= html_writer::tag('td', $description, $descriptiontdparams);
+
+        // Levels table.
+        $levelsrowparams = array('id' => '{NAME}-criteria-{CRITERION-id}-levels');
+        if ($mode != gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
+            $levelsrowparams['role'] = 'radiogroup';
+        }
+        $levelsrow = html_writer::tag('tr', $levelsstr, $levelsrowparams);
+
+        $levelstableparams = array(
+            'id' => '{NAME}-criteria-{CRITERION-id}-levels-table',
+            'aria-label' => get_string('levelsgroup', 'gradingform_rubric')
+        );
+        $levelsstrtable = html_writer::tag('table', $levelsrow, $levelstableparams);
         $levelsclass = 'levels';
         if (isset($criterion['error_levels'])) {
             $levelsclass .= ' error';
@@ -101,7 +138,7 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
         if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
             $value = get_string('criterionaddlevel', 'gradingform_rubric');
             $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][addlevel]',
-                'id' => '{NAME}-criteria-{CRITERION-id}-levels-addlevel', 'value' => $value, 'title' => $value));
+                'id' => '{NAME}-criteria-{CRITERION-id}-levels-addlevel', 'value' => $value));
             $criteriontemplate .= html_writer::tag('td', $button, array('class' => 'addlevel'));
         }
         $displayremark = ($options['enableremarks'] && ($mode != gradingform_rubric_controller::DISPLAY_VIEW || $options['showremarksstudent']));
@@ -110,13 +147,34 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
             if (isset($value['remark'])) {
                 $currentremark = $value['remark'];
             }
+
+            // Label for criterion remark.
+            $remarkinfo = new stdClass();
+            $remarkinfo->description = s($criterion['description']);
+            $remarkinfo->remark = $currentremark;
+            $remarklabeltext = get_string('criterionremark', 'gradingform_rubric', $remarkinfo);
+
             if ($mode == gradingform_rubric_controller::DISPLAY_EVAL) {
-                $input = html_writer::tag('textarea', s($currentremark), array('name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'cols' => '10', 'rows' => '5'));
+                // HTML parameters for remarks text area.
+                $remarkparams = array(
+                    'name' => '{NAME}[criteria][{CRITERION-id}][remark]',
+                    'id' => '{NAME}-criteria-{CRITERION-id}-remark',
+                    'cols' => '10', 'rows' => '5',
+                    'aria-label' => $remarklabeltext
+                );
+                $input = html_writer::tag('textarea', s($currentremark), $remarkparams);
                 $criteriontemplate .= html_writer::tag('td', $input, array('class' => 'remark'));
             } else if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN) {
                 $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'value' => $currentremark));
             }else if ($mode == gradingform_rubric_controller::DISPLAY_REVIEW || $mode == gradingform_rubric_controller::DISPLAY_VIEW) {
-                $criteriontemplate .= html_writer::tag('td', s($currentremark), array('class' => 'remark'));
+                // HTML parameters for remarks cell.
+                $remarkparams = array(
+                    'class' => 'remark',
+                    'tabindex' => '0',
+                    'id' => '{NAME}-criteria-{CRITERION-id}-remark',
+                    'aria-label' => $remarklabeltext
+                );
+                $criteriontemplate .= html_writer::tag('td', s($currentremark), $remarkparams);
             }
         }
         $criteriontemplate .= html_writer::end_tag('tr'); // .criterion
@@ -153,7 +211,7 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
         if (!isset($level['id'])) {
             $level = array('id' => '{LEVEL-id}', 'definition' => '{LEVEL-definition}', 'score' => '{LEVEL-score}', 'class' => '{LEVEL-class}', 'checked' => false);
         } else {
-            foreach (array('score', 'definition', 'class', 'checked') as $key) {
+            foreach (array('score', 'definition', 'class', 'checked', 'index') as $key) {
                 // set missing array elements to empty strings to avoid warnings
                 if (!array_key_exists($key, $level)) {
                     $level[$key] = '';
@@ -161,17 +219,37 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
             }
         }
 
+        // Get level index.
+        $levelindex = isset($level['index']) ? $level['index'] : '{LEVEL-index}';
+
         // Template for one level within one criterion
-        $tdattributes = array('id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}', 'class' => 'level'. $level['class']);
+        $tdattributes = array(
+            'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}',
+            'class' => 'level' . $level['class']
+        );
         if (isset($level['tdwidth'])) {
             $tdattributes['width'] = round($level['tdwidth']).'%';
         }
-        $leveltemplate = html_writer::start_tag('td', $tdattributes);
-        $leveltemplate .= html_writer::start_tag('div', array('class' => 'level-wrapper'));
+
+        $leveltemplate = html_writer::start_tag('div', array('class' => 'level-wrapper'));
         if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
-            $definition = html_writer::tag('textarea', s($level['definition']), array('name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]', 'cols' => '10', 'rows' => '4'));
-            $score = html_writer::label(get_string('criterionempty', 'gradingform_rubric'), '{NAME}criteria{CRITERION-id}levels{LEVEL-id}', false, array('class' => 'accesshide'));
-            $score .= html_writer::empty_tag('input', array('type' => 'text','id' => '{NAME}criteria{CRITERION-id}levels{LEVEL-id}', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]', 'size' => '3', 'value' => $level['score']));
+            $definitionparams = array(
+                'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-definition',
+                'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]',
+                'aria-label' => get_string('leveldefinition', 'gradingform_rubric', $levelindex),
+                'cols' => '10', 'rows' => '4'
+            );
+            $definition = html_writer::tag('textarea', s($level['definition']), $definitionparams);
+
+            $scoreparams = array(
+                'type' => 'text',
+                'id' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]',
+                'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]',
+                'aria-label' => get_string('scoreinputforlevel', 'gradingform_rubric', $levelindex),
+                'size' => '3',
+                'value' => $level['score']
+            );
+            $score = html_writer::empty_tag('input', $scoreparams);
         } else {
             if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
                 $leveltemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]', 'value' => $level['definition']));
@@ -181,19 +259,58 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
             $score = $level['score'];
         }
         if ($mode == gradingform_rubric_controller::DISPLAY_EVAL) {
-            $input = html_writer::empty_tag('input', array('type' => 'radio', 'name' => '{NAME}[criteria][{CRITERION-id}][levelid]', 'value' => $level['id']) +
-                    ($level['checked'] ? array('checked' => 'checked') : array()));
-            $leveltemplate .= html_writer::tag('div', $input, array('class' => 'radio'));
+            $levelradioparams = array(
+                'type' => 'radio',
+                'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-definition',
+                'name' => '{NAME}[criteria][{CRITERION-id}][levelid]',
+                'value' => $level['id']
+            );
+            if ($level['checked']) {
+                $levelradioparams['checked'] = 'checked';
+            }
+            $input = html_writer::empty_tag('input', $levelradioparams);
+            $leveltemplate .= html_writer::div($input, 'radio');
         }
         if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN && $level['checked']) {
-            $leveltemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levelid]', 'value' => $level['id']));
+            $leveltemplate .= html_writer::empty_tag('input',
+                array(
+                    'type' => 'hidden',
+                    'name' => '{NAME}[criteria][{CRITERION-id}][levelid]',
+                    'value' => $level['id']
+                )
+            );
         }
         $score = html_writer::tag('span', $score, array('id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-score', 'class' => 'scorevalue'));
         $definitionclass = 'definition';
         if (isset($level['error_definition'])) {
             $definitionclass .= ' error';
         }
-        $leveltemplate .= html_writer::tag('div', $definition, array('class' => $definitionclass, 'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-definition'));
+
+        if ($mode != gradingform_rubric_controller::DISPLAY_EDIT_FULL &&
+            $mode != gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
+
+            $tdattributes['tabindex'] = '0';
+            $levelinfo = new stdClass();
+            $levelinfo->definition = s($level['definition']);
+            $levelinfo->score = $level['score'];
+            $tdattributes['aria-label'] = get_string('level', 'gradingform_rubric', $levelinfo);
+
+            if ($mode != gradingform_rubric_controller::DISPLAY_PREVIEW &&
+                $mode != gradingform_rubric_controller::DISPLAY_PREVIEW_GRADED) {
+                // Add role of radio button to level cell if not in edit and preview mode.
+                $tdattributes['role'] = 'radio';
+                if ($level['checked']) {
+                    $tdattributes['aria-checked'] = 'true';
+                } else {
+                    $tdattributes['aria-checked'] = 'false';
+                }
+            }
+        }
+
+        $leveltemplateparams = array(
+            'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-definition-container'
+        );
+        $leveltemplate .= html_writer::div($definition, $definitionclass, $leveltemplateparams);
         $displayscore = true;
         if (!$options['showscoreteacher'] && in_array($mode, array(gradingform_rubric_controller::DISPLAY_EVAL, gradingform_rubric_controller::DISPLAY_EVAL_FROZEN, gradingform_rubric_controller::DISPLAY_REVIEW))) {
             $displayscore = false;
@@ -209,12 +326,19 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
             $leveltemplate .= html_writer::tag('div', get_string('scorepostfix', 'gradingform_rubric', $score), array('class' => $scoreclass));
         }
         if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
-            $value = get_string('leveldelete', 'gradingform_rubric');
-            $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][delete]', 'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-delete', 'value' => $value, 'title' => $value, 'tabindex' => -1));
+            $value = get_string('leveldelete', 'gradingform_rubric', $levelindex);
+            $buttonparams = array(
+                'type' => 'submit',
+                'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][delete]',
+                'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-delete',
+                'value' => $value
+            );
+            $button = html_writer::empty_tag('input', $buttonparams);
             $leveltemplate .= html_writer::tag('div', $button, array('class' => 'delete'));
         }
         $leveltemplate .= html_writer::end_tag('div'); // .level-wrapper
-        $leveltemplate .= html_writer::end_tag('td'); // .level
+
+        $leveltemplate = html_writer::tag('td', $leveltemplate, $tdattributes); // The .level cell.
 
         $leveltemplate = str_replace('{NAME}', $elementname, $leveltemplate);
         $leveltemplate = str_replace('{CRITERION-id}', $criterionid, $leveltemplate);
@@ -262,10 +386,23 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
         }
 
         $rubrictemplate = html_writer::start_tag('div', array('id' => 'rubric-{NAME}', 'class' => 'clearfix gradingform_rubric'.$classsuffix));
-        $rubrictemplate .= html_writer::tag('table', $criteriastr, array('class' => 'criteria', 'id' => '{NAME}-criteria'));
+
+        // Rubric table.
+        $rubrictableparams = array(
+            'class' => 'criteria',
+            'id' => '{NAME}-criteria',
+            'aria-label' => get_string('rubric', 'gradingform_rubric'));
+        $rubrictable = html_writer::tag('table', $criteriastr, $rubrictableparams);
+        $rubrictemplate .= $rubrictable;
         if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
             $value = get_string('addcriterion', 'gradingform_rubric');
-            $input = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][addcriterion]', 'id' => '{NAME}-criteria-addcriterion', 'value' => $value, 'title' => $value));
+            $criteriainputparams = array(
+                'type' => 'submit',
+                'name' => '{NAME}[criteria][addcriterion]',
+                'id' => '{NAME}-criteria-addcriterion',
+                'value' => $value
+            );
+            $input = html_writer::empty_tag('input', $criteriainputparams);
             $rubrictemplate .= html_writer::tag('div', $input, array('class' => 'addcriterion'));
         }
         $rubrictemplate .= $this->rubric_edit_options($mode, $options);
@@ -364,6 +501,7 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
             } else {
                 $criterionvalue = null;
             }
+            $index = 1;
             foreach ($criterion['levels'] as $levelid => $level) {
                 $level['id'] = $levelid;
                 $level['class'] = $this->get_css_class_suffix($levelcnt++, sizeof($criterion['levels']) -1);
@@ -376,7 +514,9 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
                     $level['class'] .= ' currentchecked';
                 }
                 $level['tdwidth'] = 100/count($criterion['levels']);
+                $level['index'] = $index;
                 $levelsstr .= $this->level_template($mode, $options, $elementname, $id, $level);
+                $index++;
             }
             $criteriastr .= $this->criterion_template($mode, $options, $elementname, $criterion, $levelsstr, $criterionvalue);
         }
@@ -462,7 +602,7 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
      * @return string
      */
     public function display_regrade_confirmation($elementname, $changelevel, $value) {
-        $html = html_writer::start_tag('div', array('class' => 'gradingform_rubric-regrade'));
+        $html = html_writer::start_tag('div', array('class' => 'gradingform_rubric-regrade', 'role' => 'alert'));
         if ($changelevel<=2) {
             $html .= html_writer::label(get_string('regrademessage1', 'gradingform_rubric'), 'menu' . $elementname . 'regrade');
             $selectoptions = array(
index d990126..533e81e 100644 (file)
@@ -119,7 +119,7 @@ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
                 'requires' => array('base', 'dom', 'event', 'event-touch', 'escape'),
                 'strings' => array(array('confirmdeletecriterion', 'gradingform_rubric'), array('confirmdeletelevel', 'gradingform_rubric'),
                     array('criterionempty', 'gradingform_rubric'), array('levelempty', 'gradingform_rubric')
-                    ));
+                ));
             $PAGE->requires->js_init_call('M.gradingform_rubriceditor.init', array(
                 array('name' => $this->getName(),
                     'criteriontemplate' => $renderer->criterion_template($mode, $data['options'], $this->getName()),
@@ -141,7 +141,7 @@ class MoodleQuickForm_rubriceditor 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_rubric($data['criteria'], $data['options'], $mode, $this->getName());
         return $html;
index 4ab11d2..e01387b 100644 (file)
@@ -81,7 +81,7 @@ class behat_gradingform_rubric extends behat_base {
         $addcriterionbutton = $this->find_button(get_string('addcriterion', 'gradingform_rubric'));
 
         // Cleaning the current ones.
-        $deletebuttons = $this->find_all('css', "input[title='" . get_string('criteriondelete', 'gradingform_rubric') . "']");
+        $deletebuttons = $this->find_all('css', "input[value='" . get_string('criteriondelete', 'gradingform_rubric') . "']");
         if ($deletebuttons) {
 
             // We should reverse the deletebuttons because otherwise once we delete
@@ -100,6 +100,12 @@ class behat_gradingform_rubric extends behat_base {
 
         if ($criteria) {
             foreach ($criteria as $criterionit => $criterion) {
+                // Unset empty levels in criterion.
+                foreach ($criterion as $i => $value) {
+                    if (empty($value)) {
+                        unset($criterion[$i]);
+                    }
+                }
 
                 // Checking the number of cells.
                 if (count($criterion) % 2 === 0) {
index e699683..c2ee6f4 100644 (file)
@@ -35,9 +35,9 @@ Feature: Rubrics can be created and edited
       | TMP Criterion 4 | TMP Level 41 | 41 | TMP Level 42 | 42 |
     # Checking that only the last ones are saved.
     And I define the following rubric:
-      | Criterion 1 | Level 11 | 1 | Level 12 | 20 | Level 13 | 40 | Level 14 | 50 |
-      | Criterion 2 | Level 21 | 10 | Level 22 | 20 | Level 23 | 30 |
-      | Criterion 3 | Level 31 | 5 | Level 32 | 20 |
+      | Criterion 1 | Level 11 | 1  | Level 12 | 20 | Level 13 | 40 | Level 14  | 50  |
+      | Criterion 2 | Level 21 | 10 | Level 22 | 20 | Level 23 | 30 |           |     |
+      | Criterion 3 | Level 31 | 5  | Level 32 | 20 |          |    |           |     |
     And I press "Save as draft"
     And I go to "Test assignment 1 name" advanced grading definition page
     And I click on "Move down" "button" in the "Criterion 1" "table_row"
index e26c776..1f19673 100644 (file)
@@ -28,7 +28,7 @@ Feature: Reuse my rubrics in other activities
     And I define the following rubric:
       | Criterion 1 | Level 11 | 11 | Level 12 | 12 | Level 3 | 13 |
       | Criterion 2 | Level 21 | 21 | Level 22 | 22 | Level 3 | 23 |
-      | Criterion 3 | Level 31 | 31 | Level 32 | 32 |
+      | Criterion 3 | Level 31 | 31 | Level 32 | 32 |         |    |
     And I press "Save rubric and make it ready"
     And I follow "Course 1"