Merge branch 'wip-mdl-40126' of git://github.com/rajeshtaneja/moodle
authorMarina Glancy <marina@moodle.com>
Tue, 6 Aug 2013 12:53:19 +0000 (22:53 +1000)
committerMarina Glancy <marina@moodle.com>
Tue, 6 Aug 2013 12:53:19 +0000 (22:53 +1000)
356 files changed:
admin/cli/install.php
admin/environment.xml
admin/settings/appearance.php
admin/tool/uploadcourse/classes/course.php
admin/tool/uploadcourse/classes/helper.php
admin/tool/uploadcourse/lang/en/tool_uploadcourse.php
admin/tool/uploadcourse/tests/course_test.php
admin/tool/uploadcourse/tests/helper_test.php
admin/tool/uploaduser/locallib.php
auth/email/auth.php
auth/ldap/auth.php
auth/manual/auth.php
auth/nologin/auth.php
auth/none/auth.php
auth/tests/auth_test.php [deleted file]
auth/upgrade.txt
backup/controller/tests/controller_test.php
backup/converter/moodle1/tests/moodle1_converter_test.php [moved from backup/converter/moodle1/tests/lib_test.php with 99% similarity]
backup/moodle2/restore_stepslib.php
backup/upgrade.txt
backup/util/dbops/backup_controller_dbops.class.php
backup/util/dbops/restore_dbops.class.php
backup/util/dbops/tests/backup_dbops_test.php
backup/util/ui/tests/behat/duplicate_activities.feature
badges/lib/backpacklib.php
badges/tests/badgeslib_test.php
blocks/comments/tests/behat/behat_block_comments.php
blocks/course_overview/db/access.php
blocks/course_overview/locallib.php
blocks/course_overview/version.php
blocks/navigation/tests/behat/view_my_courses.feature
blocks/site_main_menu/block_site_main_menu.php
blocks/social_activities/block_social_activities.php
blog/locallib.php
blog/tests/behat/comment.feature
blog/tests/bloglib_test.php
cache/stores/file/lib.php
cache/tests/administration_helper_test.php [new file with mode: 0644]
cache/tests/cache_test.php
cache/tests/config_writer_test.php [moved from cache/tests/locallib_test.php with 57% similarity]
calendar/tests/externallib_test.php
calendar/tests/ical_test.php [moved from calendar/tests/calendarical_test.php with 100% similarity]
cohort/tests/cohortlib_test.php
cohort/tests/externallib_test.php
comment/comment.js
course/dndupload.js
course/dnduploadlib.php
course/format/upgrade.txt
course/lib.php
course/renderer.php
course/tests/behat/activities_indentation.feature
course/tests/behat/behat_course.php
course/tests/behat/course_controls.feature
course/tests/courselib_test.php
course/tests/courserequest_test.php
course/tests/externallib_test.php
course/yui/dragdrop/dragdrop.js
course/yui/toolboxes/toolboxes.js
enrol/locallib.php
enrol/meta/locallib.php
enrol/meta/settings.php
enrol/meta/tests/plugin_test.php [new file with mode: 0644]
enrol/tests/enrollib_test.php
enrol/tests/externallib_test.php
enrol/tests/role_external_test.php [moved from enrol/tests/externallib_role_test.php with 98% similarity]
files/renderer.php
files/tests/externallib_test.php
grade/export/grade_export_form.php
grade/export/key_form.php
grade/grading/tests/grading_manager_test.php [moved from grade/grading/tests/lib_test.php with 98% similarity]
grade/import/key_form.php
grade/report/user/lang/en/gradereport_user.php
grade/report/user/lib.php
grade/report/user/styles.css
grade/tests/edittreelib_test.php [moved from grade/tests/edittree_test.php with 96% similarity]
grade/tests/externallib_test.php
grade/tests/querylib_test.php
grade/tests/report_graderlib_test.php [moved from grade/tests/reportgrader_test.php with 98% similarity]
grade/tests/reportlib_test.php
grade/tests/reportuserlib_test.php [moved from grade/tests/reportuser_test.php with 99% similarity]
group/tests/externallib_test.php
install.php
install/lang/ca/error.php
install/lang/fa/admin.php
install/lang/fa/moodle.php
install/lang/ru/install.php
iplookup/tests/geoip_test.php
iplookup/tests/geoplugin_test.php
lang/en/admin.php
lang/en/auth.php
lang/en/blog.php
lang/en/error.php
lang/en/message.php
lang/en/question.php
lang/en/role.php
lib/ajax/tests/ajaxlib_test.php
lib/authlib.php
lib/behat/lib.php
lib/blocklib.php
lib/classes/event/base.php
lib/classes/event/blog_entry_created.php [new file with mode: 0644]
lib/classes/event/manager.php
lib/classes/event/role_assigned.php
lib/classes/event/role_unassigned.php
lib/classes/event/user_loggedin.php [moved from auth/classes/event/user_loggedin.php with 87% similarity]
lib/classes/minify.php [new file with mode: 0644]
lib/conditionlib.php
lib/csslib.php
lib/db/caches.php
lib/db/upgrade.php
lib/db/upgradelib.php
lib/ddl/mssql_sql_generator.php
lib/ddl/tests/ddl_test.php
lib/deprecatedlib.php
lib/dml/mssql_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/dml/tests/dml_test.php
lib/editor/tinymce/plugins/loader.php
lib/editor/tinymce/plugins/managefiles/tinymce/editor_plugin.js
lib/editor/tinymce/plugins/pdw/readme_moodle.txt
lib/editor/tinymce/plugins/pdw/tinymce/editor_plugin.js
lib/editor/tinymce/plugins/pdw/tinymce/editor_plugin_src.js [deleted file]
lib/eventslib.php
lib/external/tests/external_test.php [moved from lib/external/tests/externallib_test.php with 100% similarity]
lib/filestorage/tests/file_storage_test.php
lib/filestorage/tests/zip_packer_test.php
lib/filestorage/zip_archive.php
lib/grade/tests/grade_category_test.php
lib/grade/tests/grade_grade_test.php
lib/grade/tests/grade_item_test.php
lib/grade/tests/grade_outcome_test.php
lib/grade/tests/grade_scale_test.php
lib/grouplib.php
lib/javascript.php
lib/jslib.php
lib/minify/config.php
lib/minify/lib/CSSmin.php [new file with mode: 0644]
lib/minify/lib/DooDigestAuth.php [new file with mode: 0644]
lib/minify/lib/Minify.php
lib/minify/lib/Minify/Build.php
lib/minify/lib/Minify/CSS.php
lib/minify/lib/Minify/Cache/File.php
lib/minify/lib/Minify/Cache/XCache.php [new file with mode: 0644]
lib/minify/lib/Minify/ClosureCompiler.php [new file with mode: 0644]
lib/minify/lib/Minify/Controller/Base.php
lib/minify/lib/Minify/Controller/Files.php
lib/minify/lib/Minify/Controller/Groups.php
lib/minify/lib/Minify/Controller/MinApp.php
lib/minify/lib/Minify/Controller/Page.php
lib/minify/lib/Minify/Controller/Version1.php
lib/minify/lib/Minify/HTML.php
lib/minify/lib/Minify/HTML/Helper.php
lib/minify/lib/Minify/JS/ClosureCompiler.php
lib/minify/lib/Minify/Lines.php
lib/minify/lib/Minify/Loader.php [new file with mode: 0644]
lib/minify/lib/Minify/YUICompressor.php
lib/minify/lib/MrClay/Cli.php
lib/minify/lib/MrClay/Cli/Arg.php
lib/minify/readme_moodle.txt
lib/minify/utils.php
lib/modinfolib.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/classes/autoloader.php
lib/phpunit/classes/event_mock.php [new file with mode: 0644]
lib/phpunit/lib.php
lib/questionlib.php
lib/setuplib.php
lib/tablelib.php
lib/testing/classes/util.php
lib/tests/accesslib_test.php
lib/tests/admintree_test.php
lib/tests/authlib_test.php
lib/tests/behat/behat_hooks.php
lib/tests/blocklib_test.php
lib/tests/code_test.php
lib/tests/collator_test.php
lib/tests/completionlib_advanced_test.php [deleted file]
lib/tests/completionlib_test.php
lib/tests/component_test.php
lib/tests/componentlib_test.php
lib/tests/conditionlib_test.php
lib/tests/configonlylib_test.php
lib/tests/coursecatlib_test.php
lib/tests/csslib_test.php
lib/tests/csvclass_test.php
lib/tests/datalib_test.php
lib/tests/environment_test.php
lib/tests/event_test.php
lib/tests/eventslib_test.php
lib/tests/externallib_test.php
lib/tests/filelib_test.php
lib/tests/filterlib_test.php [moved from lib/tests/filter_test.php with 74% similarity]
lib/tests/fixtures/event_fixtures.php
lib/tests/formslib_test.php
lib/tests/gradelib_test.php
lib/tests/grouplib_test.php
lib/tests/html2text_test.php
lib/tests/html_writer_test.php [moved from lib/tests/htmlwriter_test.php with 69% similarity]
lib/tests/htmlpurifier_test.php
lib/tests/markdown_test.php
lib/tests/mathslib_test.php
lib/tests/medialib_test.php
lib/tests/messagelib_test.php
lib/tests/minify_test.php [new file with mode: 0644]
lib/tests/modinfolib_test.php
lib/tests/moodle_page_test.php [moved from lib/tests/pagelib_test.php with 71% similarity]
lib/tests/moodlelib_test.php
lib/tests/navigationlib_test.php
lib/tests/outputcomponents_test.php
lib/tests/outputrequirementslib_test.php
lib/tests/pluginlib_test.php
lib/tests/questionlib_test.php
lib/tests/rsslib_test.php
lib/tests/setuplib_test.php
lib/tests/statslib_test.php
lib/tests/string_manager_test.php [moved from lib/tests/string_test.php with 94% similarity]
lib/tests/text_test.php
lib/tests/theme_config_test.php [moved from lib/tests/outputlib_test.php with 53% similarity]
lib/tests/upgradelib_test.php
lib/tests/weblib_test.php
lib/tests/xhtml_container_stack_test.php [new file with mode: 0644]
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/weblib.php
lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification/moodle-core-notification-debug.js
lib/yui/build/moodle-core-notification/moodle-core-notification-min.js
lib/yui/build/moodle-core-notification/moodle-core-notification.js
lib/yui/src/actionmenu/build.json [new file with mode: 0644]
lib/yui/src/actionmenu/js/actionmenu.js [new file with mode: 0644]
lib/yui/src/actionmenu/meta/actionmenu.json [new file with mode: 0644]
lib/yui/src/notification/build.json
lib/yui/src/notification/js/ajaxexception.js [new file with mode: 0644]
lib/yui/src/notification/js/alert.js [new file with mode: 0644]
lib/yui/src/notification/js/confirm.js [new file with mode: 0644]
lib/yui/src/notification/js/dialogue.js [new file with mode: 0644]
lib/yui/src/notification/js/exception.js [new file with mode: 0644]
lib/yui/src/notification/js/notification.js
lib/yui/src/notification/js/shared.js [new file with mode: 0644]
lib/yui/src/notification/meta/notification.json
message/bell.mp3 [new file with mode: 0644]
message/bell.ogg [new file with mode: 0644]
message/edit.php
message/renderer.php
message/tests/externallib_test.php
mod/assign/db/services.php
mod/assign/externallib.php
mod/assign/feedback/file/importziplib.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/tests/externallib_test.php
mod/assign/version.php
mod/assign/yui/history/history.js
mod/assignment/lib.php
mod/choice/styles.css
mod/feedback/import_form.php
mod/feedback/lib.php
mod/feedback/styles.css
mod/forum/backup/moodle2/restore_forum_stepslib.php
mod/forum/lib.php
mod/label/lib.php
mod/lesson/locallib.php
mod/lesson/pagetypes/endofbranch.php
mod/lesson/pagetypes/endofcluster.php
mod/lesson/pagetypes/essay.php
mod/lesson/pagetypes/matching.php
mod/lesson/pagetypes/truefalse.php
mod/lti/db/access.php
mod/lti/lang/en/lti.php
mod/lti/lib.php
mod/lti/locallib.php
mod/lti/mod_form.php
mod/lti/service.php
mod/page/lib.php
mod/quiz/report/statistics/lib.php
mod/quiz/report/statistics/report.php
mod/resource/lib.php
mod/url/lib.php
notes/tests/externallib_test.php
phpunit.xml.dist
question/behaviour/behaviourbase.php
question/behaviour/deferredcbm/tests/walkthrough_test.php
question/behaviour/deferredfeedback/tests/walkthrough_test.php
question/editlib.php
question/engine/questionattempt.php
question/engine/tests/helpers.php
question/format/aiken/format.php
question/format/aiken/lang/en/qformat_aiken.php
question/format/aiken/version.php
question/format/blackboard_six/format.php
question/format/blackboard_six/formatqti.php
question/format/examview/format.php
question/format/gift/format.php
question/format/gift/lang/en/qformat_gift.php
question/format/gift/tests/giftformat_test.php
question/format/gift/version.php
question/format/learnwise/format.php
question/format/learnwise/lang/en/qformat_learnwise.php
question/format/learnwise/version.php
question/format/missingword/format.php
question/format/missingword/lang/en/qformat_missingword.php
question/format/missingword/version.php
question/format/multianswer/lang/en/qformat_multianswer.php
question/format/multianswer/version.php
question/format/upgrade.txt
question/format/webct/TODO.txt
question/format/webct/format.php
question/format/webct/lang/en/qformat_webct.php
question/format/webct/version.php
question/format/xhtml/format.php
question/format/xhtml/lang/en/qformat_xhtml.php
question/format/xhtml/version.php
question/format/xhtml/xhtml.css
question/format/xml/format.php
question/format/xml/lang/en/qformat_xml.php
question/format/xml/tests/xmlformat_test.php
question/format/xml/version.php
question/upgrade.txt [new file with mode: 0644]
report/log/locallib.php
repository/s3/README_MOODLE.txt
repository/s3/S3.php
repository/tests/generator_test.php
repository/tests/repositorylib_test.php [moved from repository/tests/repository_test.php with 99% similarity]
theme/base/style/core.css
theme/base/style/course.css
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/style/moodle.css
theme/javascript.php
theme/upgrade.txt
user/filters/date.php
user/tests/externallib_test.php
version.php
webservice/tests/externallib_test.php

index e8cffe0..2d992b5 100644 (file)
@@ -309,6 +309,8 @@ if ($interactive) {
     }
 }
 $CFG->directorypermissions = $chmod;
+$CFG->filepermissions      = ($CFG->directorypermissions & 0666);
+$CFG->umaskpermissions     = (($CFG->directorypermissions & 0777) ^ 0777);
 
 //We need wwwroot before we test dataroot
 $wwwroot = clean_param($options['wwwroot'], PARAM_URL);
index 5fac54c..1e937cd 100644 (file)
           <ON_CHECK message="settingfileuploads" />
         </FEEDBACK>
       </PHP_SETTING>
+      <PHP_SETTING name="opcache.enable" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opcacherecommended" />
+        </FEEDBACK>
+      </PHP_SETTING>
     </PHP_SETTINGS>
   </MOODLE>
 </COMPATIBILITY_MATRIX>
index 1367452..1ee3a7a 100644 (file)
@@ -214,6 +214,8 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $setting->set_updatedcallback('js_reset_all_caches');
     $temp->add($setting);
     $temp->add(new admin_setting_configcheckbox('modchooserdefault', new lang_string('modchooserdefault', 'admin'), new lang_string('configmodchooserdefault', 'admin'), 1));
+    $temp->add(new admin_setting_configcheckbox('modeditingmenu', new lang_string('modeditingmenu', 'admin'), new lang_string('modeditingmenu_desc', 'admin'), 1));
+    $temp->add(new admin_setting_configcheckbox('blockeditingmenu', new lang_string('blockeditingmenu', 'admin'), new lang_string('blockeditingmenu_desc', 'admin'), 1));
     $ADMIN->add('appearance', $temp);
 
     // link to tag management interface
index 629e498..782e1e8 100644 (file)
@@ -399,7 +399,7 @@ class tool_uploadcourse_course {
      * @return bool false is any error occured.
      */
     public function prepare() {
-        global $DB;
+        global $DB, $SITE;
         $this->prepared = true;
 
         // Validate the shortname.
@@ -432,6 +432,12 @@ class tool_uploadcourse_course {
                 $this->error('courseexistsanduploadnotallowed',
                     new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse'));
                 return false;
+            } else if ($this->can_update()) {
+                // We can never allow for any front page changes!
+                if ($this->shortname == $SITE->shortname) {
+                    $this->error('cannotupdatefrontpage', new lang_string('cannotupdatefrontpage', 'tool_uploadcourse'));
+                    return false;
+                }
             }
         } else {
             if (!$this->can_create()) {
@@ -608,6 +614,13 @@ class tool_uploadcourse_course {
         if ($exists) {
             $missingonly = ($updatemode === tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS);
             $coursedata = $this->get_final_update_data($coursedata, $usedefaults, $missingonly);
+
+            // Make sure we are not trying to mess with the front page, though we should never get here!
+            if ($coursedata['id'] == $SITE->id) {
+                $this->error('cannotupdatefrontpage', new lang_string('cannotupdatefrontpage', 'tool_uploadcourse'));
+                return false;
+            }
+
             $this->do = self::DO_UPDATE;
         } else {
             $coursedata = $this->get_final_create_data($coursedata);
@@ -715,6 +728,7 @@ class tool_uploadcourse_course {
                 $this->error('errorwhilerestoringcourse', new lang_string('errorwhilerestoringthecourse', 'tool_uploadcourse'));
             }
             $rc->destroy();
+            unset($rc); // File logging is a mess, we can only try to rely on gc to close handles.
         }
 
         // Proceed with enrolment data.
index b328a42..11ff373 100644 (file)
@@ -44,6 +44,13 @@ class tool_uploadcourse_helper {
      */
     public static function clean_restore_content() {
         global $CFG;
+
+        // There are some sloppy unclosed file handles in backup/restore code,
+        // let's hope somebody unset all controllers before calling this
+        // and destroy magic will close all remaining open file handles,
+        // otherwise Windows will fail deleting the directory.
+        gc_collect_cycles();
+
         if (!empty($CFG->keeptempdirectoriesonbackup)) {
             $cache = cache::make('tool_uploadcourse', 'helper');
             $backupids = (array) $cache->get('backupids');
index 83138d3..3be7ec4 100644 (file)
@@ -32,6 +32,7 @@ $string['cannotreadbackupfile'] = 'Cannot read the backup file';
 $string['cannotrenamecoursenotexist'] = 'Cannot rename a course that does not exist';
 $string['cannotrenameidnumberconflict'] = 'Cannot rename the course, the ID number conflicts with an existing course';
 $string['cannotrenameshortnamealreadyinuse'] = 'Cannot rename the course, the shortname is already used';
+$string['cannotupdatefrontpage'] = 'It is forbidden to modify the front page';
 $string['canonlyrenameinupdatemode'] = 'Can only rename a course when update is allowed';
 $string['canonlyresetcourseinupdatemode'] = 'Can only reset a course in update mode';
 $string['couldnotresolvecatgorybyid'] = 'Could not resolve category by ID';
index f04a2ea..ee979b4 100644 (file)
@@ -768,6 +768,7 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
     }
 
     public function test_create_bad_category() {
+        global $DB;
         $this->resetAfterTest(true);
 
         // Ensure fails when category cannot be resolved upon creation.
@@ -778,6 +779,14 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $this->assertFalse($co->prepare());
         $this->assertArrayHasKey('couldnotresolvecatgorybyid', $co->get_errors());
 
+        // Ensure fails when category is 0 on create.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'c1', 'summary' => 'summary', 'fullname' => 'FN', 'category' => '0');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('missingmandatoryfields', $co->get_errors());
+
         // Ensure fails when category cannot be resolved upon update.
         $c1 = $this->getDataGenerator()->create_course();
         $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
@@ -786,6 +795,31 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $co = new tool_uploadcourse_course($mode, $updatemode, $data);
         $this->assertFalse($co->prepare());
         $this->assertArrayHasKey('couldnotresolvecatgorybyid', $co->get_errors());
+
+        // Ensure does not update the category when it is 0.
+        $c1 = $this->getDataGenerator()->create_course();
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $c1->shortname, 'category' => '0');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $this->assertEmpty($co->get_errors());
+        $this->assertEmpty($co->get_statuses());
+        $co->proceed();
+        $this->assertEquals($c1->category, $DB->get_field('course', 'category', array('id' => $c1->id)));
+
+        // Ensure does not update the category when it is set to 0 in the defaults.
+        $c1 = $this->getDataGenerator()->create_course();
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS;
+        $data = array('shortname' => $c1->shortname);
+        $defaults = array('category' => '0');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaults);
+        $this->assertTrue($co->prepare());
+        $this->assertEmpty($co->get_errors());
+        $this->assertEmpty($co->get_statuses());
+        $co->proceed();
+        $this->assertEquals($c1->category, $DB->get_field('course', 'category', array('id' => $c1->id)));
     }
 
     public function test_enrolment_data() {
@@ -920,4 +954,47 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $this->assertArrayHasKey('courseshortnameincremented', $co->get_statuses());
     }
 
+    public function test_mess_with_frontpage() {
+        global $SITE;
+        $this->resetAfterTest(true);
+
+        // Updating the front page.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $SITE->shortname, 'idnumber' => 'NewIDN');
+        $importoptions = array();
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('cannotupdatefrontpage', $co->get_errors());
+
+        // Updating the front page.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $SITE->shortname, 'idnumber' => 'NewIDN');
+        $importoptions = array();
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('cannotupdatefrontpage', $co->get_errors());
+
+        // Generating a shortname should not be allowed in update mode, and so we cannot update the front page.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('idnumber' => 'NewIDN', 'fullname' => 'FN', 'category' => 1);
+        $importoptions = array('shortnametemplate' => $SITE->shortname);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('cannotgenerateshortnameupdatemode', $co->get_errors());
+
+        // Renaming to the front page should not be allowed.
+        $c1 = $this->getDataGenerator()->create_course();
+        $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $c1->shortname, 'fullname' => 'FN', 'idnumber' => 'NewIDN', 'rename' => $SITE->shortname);
+        $importoptions = array('canrename' => true);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('cannotrenameshortnamealreadyinuse', $co->get_errors());
+
+    }
+
 }
index 873e221..31e53e0 100644 (file)
@@ -129,6 +129,7 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase {
         $this->assertTrue(isset($result['backup_destination']));
         $c1backupfile = $result['backup_destination']->copy_content_to_temp();
         $bc->destroy();
+        unset($bc); // File logging is a mess, we can only try to rely on gc to close handles.
 
         // Creating backup file.
         $bc = new backup_controller(backup::TYPE_1COURSE, $c2->id, backup::FORMAT_MOODLE,
@@ -138,6 +139,7 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase {
         $this->assertTrue(isset($result['backup_destination']));
         $c2backupfile = $result['backup_destination']->copy_content_to_temp();
         $bc->destroy();
+        unset($bc); // File logging is a mess, we can only try to rely on gc to close handles.
 
         // Checking restore dir.
         $dir = tool_uploadcourse_helper::get_restore_content_dir($c1backupfile, null);
index 928b13e..a050e80 100644 (file)
@@ -321,12 +321,13 @@ function uu_process_template_callback($username, $firstname, $lastname, $block)
  * @return array type=>name
  */
 function uu_supported_auths() {
-    // only following plugins are guaranteed to work properly
-    $whitelist = array('manual', 'nologin', 'none', 'email');
+    // Get all the enabled plugins.
     $plugins = get_enabled_auth_plugins();
     $choices = array();
     foreach ($plugins as $plugin) {
-        if (!in_array($plugin, $whitelist)) {
+        $objplugin = get_auth_plugin($plugin);
+        // If the plugin can not be manually set skip it.
+        if (!$objplugin->can_be_manually_set()) {
             continue;
         }
         $choices[$plugin] = get_string('pluginname', "auth_{$plugin}");
index 9ac59bd..e459004 100644 (file)
@@ -195,6 +195,15 @@ class auth_plugin_email extends auth_plugin_base {
         return true;
     }
 
+    /**
+     * Returns true if plugin can be manually set.
+     *
+     * @return bool
+     */
+    function can_be_manually_set() {
+        return true;
+    }
+
     /**
      * Prints a form for configuring this authentication plugin.
      *
index e9d82b7..dae9923 100644 (file)
@@ -509,6 +509,15 @@ class auth_plugin_ldap extends auth_plugin_base {
         return !empty($this->config->stdchangepassword);
     }
 
+    /**
+     * Returns true if plugin can be manually set.
+     *
+     * @return bool
+     */
+    function can_be_manually_set() {
+        return true;
+    }
+
     /**
      * Returns true if plugin allows signup and user creation.
      *
index 3b4fdae..1c7ff9e 100644 (file)
@@ -129,6 +129,15 @@ class auth_plugin_manual extends auth_plugin_base {
         return true;
     }
 
+    /**
+     * Returns true if plugin can be manually set.
+     *
+     * @return bool
+     */
+    function can_be_manually_set() {
+        return true;
+    }
+
     /**
      * Prints a form for configuring this authentication plugin.
      *
index 29f018e..c3ed391 100644 (file)
@@ -85,6 +85,14 @@ class auth_plugin_nologin extends auth_plugin_base {
         return false;
     }
 
+    /**
+     * Returns true if plugin can be manually set.
+     *
+     * @return bool
+     */
+    function can_be_manually_set() {
+        return true;
+    }
 }
 
 
index 9d3bfc2..fcfce4c 100644 (file)
@@ -115,6 +115,15 @@ class auth_plugin_none extends auth_plugin_base {
         return true;
     }
 
+    /**
+     * Returns true if plugin can be manually set.
+     *
+     * @return bool
+     */
+    function can_be_manually_set() {
+        return true;
+    }
+
     /**
      * Prints a form for configuring this authentication plugin.
      *
diff --git a/auth/tests/auth_test.php b/auth/tests/auth_test.php
deleted file mode 100644 (file)
index 0b2b8ae..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-<?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/>.
-
-/**
- * Tests for auth.
- *
- * @package    core_auth
- * @copyright  2013 Frédéric Massart
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->libdir . '/authlib.php');
-
-/**
- * Auth testcase class.
- *
- * @package    core_auth
- * @copyright  2013 Frédéric Massart
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class auth_testcase extends advanced_testcase {
-
-    public function test_user_loggedin_event() {
-        global $USER;
-        $this->resetAfterTest(true);
-        $this->setAdminUser();
-
-        $sink = $this->redirectEvents();
-        $user = clone($USER);
-        login_attempt_valid($user);
-        $events = $sink->get_events();
-        $sink->close();
-
-        $this->assertCount(1, $events);
-        $event = reset($events);
-        $this->assertInstanceOf('\core_auth\event\user_loggedin', $event);
-        $this->assertEquals('user', $event->objecttable);
-        $this->assertEquals('2', $event->objectid);
-        $this->assertEquals(context_system::instance()->id, $event->contextid);
-        $this->assertEquals($user, $event->get_record_snapshot('user', 2));
-    }
-
-    public function test_user_loggedin_event_exceptions() {
-        try {
-            $event = \core_auth\event\user_loggedin::create(array('objectid' => 1));
-            $this->fail('\core_auth\event\user_loggedin requires other[\'username\']');
-        } catch(Exception $e) {
-            $this->assertInstanceOf('coding_exception', $e);
-        }
-
-        try {
-            $event = \core_auth\event\user_loggedin::create(array('other' => array('username' => 'test')));
-            $this->fail('\core_auth\event\user_loggedin requires objectid');
-        } catch(Exception $e) {
-            $this->assertInstanceOf('coding_exception', $e);
-        }
-    }
-
-}
index 4618dd5..6085397 100644 (file)
@@ -1,6 +1,11 @@
 This files describes API changes in /auth/* - plugins,
 information provided here is intended especially for developers.
 
+=== 2.6 ===
+
+* can_be_manually_set() - This function was introduced in the base class and returns false by default. If overriden by
+  an authentication plugin to return true, the authentication plugin will be able to be manually set for users. For example,
+  when bulk uploading users you will be able to select it as the authentication method they use.
 
 === 2.4 ===
 
index 6b44703..a485145 100644 (file)
@@ -30,7 +30,7 @@ require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
 /*
  * controller tests (all)
  */
-class backup_controller_testcase extends advanced_testcase {
+class core_backup_controller_testcase extends advanced_testcase {
 
     protected $moduleid;  // course_modules id used for testing
     protected $sectionid; // course_sections id used for testing
@@ -30,7 +30,7 @@ global $CFG;
 require_once($CFG->dirroot . '/backup/converter/moodle1/lib.php');
 
 
-class moodle1_converter_testcase extends advanced_testcase {
+class core_backup_moodle1_converter_testcase extends advanced_testcase {
 
     /** @var string the name of the directory containing the unpacked Moodle 1.9 backup */
     protected $tempdir;
index ed36b0a..2b6cdab 100644 (file)
@@ -510,11 +510,11 @@ class restore_review_pending_block_positions extends restore_execution_step {
 
         // Get all the block_position objects pending to match
         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position');
-        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid');
+        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
         // Process block positions, creating them or accumulating for final step
         foreach($rs as $posrec) {
-            // Get the complete position object (stored as info)
-            $position = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'block_position', $posrec->itemid)->info;
+            // Get the complete position object out of the info field.
+            $position = backup_controller_dbops::decode_backup_temp_info($posrec->info);
             // If position is for one already mapped (known) contextid
             // process it now, creating the position, else nothing to
             // do, position finally discarded
@@ -546,12 +546,12 @@ class restore_process_course_modules_availability extends restore_execution_step
 
         // Get all the module_availability objects to process
         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'module_availability');
-        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid');
+        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
         // Process availabilities, creating them if everything matches ok
         foreach($rs as $availrec) {
             $allmatchesok = true;
             // Get the complete availabilityobject
-            $availability = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'module_availability', $availrec->itemid)->info;
+            $availability = backup_controller_dbops::decode_backup_temp_info($availrec->info);
             // Map the sourcecmid if needed and possible
             if (!empty($availability->sourcecmid)) {
                 $newcm = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'course_module', $availability->sourcecmid);
@@ -3624,7 +3624,7 @@ class restore_process_file_aliases_queue extends restore_execution_step {
 
         // Iterate over aliases in the queue.
         foreach ($rs as $record) {
-            $info = unserialize(base64_decode($record->info));
+            $info = restore_dbops::decode_backup_temp_info($record->info);
 
             // Try to pick a repository instance that should serve the alias.
             $repository = $this->choose_repository($info);
@@ -3657,7 +3657,7 @@ class restore_process_file_aliases_queue extends restore_execution_step {
                 $source = null;
 
                 foreach ($candidates as $candidate) {
-                    $candidateinfo = unserialize(base64_decode($candidate->info));
+                    $candidateinfo = backup_controller_dbops::decode_backup_temp_info($candidate->info);
                     if ($candidateinfo->filename === $reference['filename']
                             and $candidateinfo->filepath === $reference['filepath']
                             and !is_null($candidate->newcontextid)
index 586ee70..26e2e00 100644 (file)
@@ -7,6 +7,12 @@ information provided here is intended especially for developers.
   method is not available anymore. Temp tables must be created
   inline always.
 
+* Using the info field from backup_ids_temp or backup_files_temp
+  must now go via backup_controller_dbops::decode_backup_temp_info() and
+  backup_controller_dbops::encode_backup_temp_info(). The implementation
+  of the encoding has changed.  These new functions encapsulate any future
+  changes to the encoding.
+
 === 2.5 ===
 
 * New optional param $sortby in backup set_source_table() allows to
index 085fe22..668ff61 100644 (file)
@@ -154,6 +154,44 @@ abstract class backup_controller_dbops extends backup_dbops {
         $dbman->drop_table($table); // And drop it
     }
 
+    /**
+     * Decode the info field from backup_ids_temp or backup_files_temp.
+     *
+     * @param mixed $info The info field data to decode, may be an object or a simple integer.
+     * @return mixed The decoded information.  For simple types it returns, for complex ones we decode.
+     */
+    public static function decode_backup_temp_info($info) {
+        // We encode all data except null.
+        if ($info != null) {
+            if (extension_loaded('zlib')) {
+                return unserialize(gzuncompress(base64_decode($info)));
+            } else {
+                return unserialize(base64_decode($info));
+            }
+        }
+        return $info;
+    }
+
+    /**
+     * Encode the info field for backup_ids_temp or backup_files_temp.
+     *
+     * @param mixed $info string The info field data to encode.
+     * @return string An encoded string of data or null if the input is null.
+     */
+    public static function encode_backup_temp_info($info) {
+        // We encode if there is any information to keep the translations simpler.
+        if ($info != null) {
+            // We compress if possible. It reduces db, network and memory storage. The saving is greater than CPU compression cost.
+            // Compression level 1 is chosen has it produces good compression with the smallest possible overhead, see MDL-40618.
+            if (extension_loaded('zlib')) {
+                return base64_encode(gzcompress(serialize($info), 1));
+            } else {
+                return base64_encode(serialize($info));
+            }
+        }
+        return $info;
+    }
+
     /**
      * Given one type and id from controller, return the corresponding courseid
      */
index 12f2601..0e5fabc 100644 (file)
@@ -152,7 +152,7 @@ abstract class restore_dbops {
         $problems = array(); // To store warnings/errors
 
         // Get loaded roles from backup_ids
-        $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid');
+        $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid, info');
         foreach ($rs as $recrole) {
             // If the rolemappings->modified flag is set, that means that we are coming from
             // manually modified mappings (by UI), so accept those mappings an put them to backup_ids
@@ -163,14 +163,13 @@ abstract class restore_dbops {
             // Else, we haven't any info coming from UI, let's calculate the mappings, matching
             // in multiple ways and checking permissions. Note mapping to 0 means "skip"
             } else {
-                $role = (object)self::get_backup_ids_record($restoreid, 'role', $recrole->itemid)->info;
+                $role = (object)backup_controller_dbops::decode_backup_temp_info($recrole->info);
                 $match = self::get_best_assignable_role($role, $courseid, $userid, $samesite);
                 // Send match to backup_ids
                 self::set_backup_ids_record($restoreid, 'role', $recrole->itemid, $match);
                 // Build the rolemappings element for controller
                 unset($role->id);
                 unset($role->nameincourse);
-                unset($role->nameincourse);
                 $role->targetroleid = $match;
                 $rolemappings->mappings[$recrole->itemid] = $role;
                 // Prepare warning if no match found
@@ -673,20 +672,21 @@ abstract class restore_dbops {
         global $DB;
 
         $results = array();
-        $qcats = $DB->get_records_sql("SELECT itemid, parentitemid AS contextid
+        $qcats = $DB->get_recordset_sql("SELECT itemid, parentitemid AS contextid, info
                                          FROM {backup_ids_temp}
                                        WHERE backupid = ?
                                          AND itemname = 'question_category'", array($restoreid));
         foreach ($qcats as $qcat) {
             // If this qcat context haven't been acummulated yet, do that
             if (!isset($results[$qcat->contextid])) {
-                $temprec = self::get_backup_ids_record($restoreid, 'question_category', $qcat->itemid);
+                $info = backup_controller_dbops::decode_backup_temp_info($qcat->info);
                 // Filter by contextlevel if necessary
-                if (is_null($contextlevel) || $contextlevel == $temprec->info->contextlevel) {
-                    $results[$qcat->contextid] = $temprec->info->contextlevel;
+                if (is_null($contextlevel) || $contextlevel == $info->contextlevel) {
+                    $results[$qcat->contextid] = $info->contextlevel;
                 }
             }
         }
+        $qcats->close();
         // Sort by value (contextlevel from CONTEXT_SYSTEM downto CONTEXT_MODULE)
         asort($results);
         return $results;
@@ -700,15 +700,16 @@ abstract class restore_dbops {
         global $DB;
 
         $results = array();
-        $qcats = $DB->get_records_sql("SELECT itemid
+        $qcats = $DB->get_recordset_sql("SELECT itemid, info
                                          FROM {backup_ids_temp}
                                         WHERE backupid = ?
                                           AND itemname = 'question_category'
                                           AND parentitemid = ?", array($restoreid, $contextid));
         foreach ($qcats as $qcat) {
-            $temprec = self::get_backup_ids_record($restoreid, 'question_category', $qcat->itemid);
-            $results[$qcat->itemid] = $temprec->info;
+            $results[$qcat->itemid] = backup_controller_dbops::decode_backup_temp_info($qcat->info);
         }
+        $qcats->close();
+
         return $results;
     }
 
@@ -798,15 +799,15 @@ abstract class restore_dbops {
         global $DB;
 
         $results = array();
-        $qs = $DB->get_records_sql("SELECT itemid
+        $qs = $DB->get_recordset_sql("SELECT itemid, info
                                       FROM {backup_ids_temp}
                                      WHERE backupid = ?
                                        AND itemname = 'question'
                                        AND parentitemid = ?", array($restoreid, $qcatid));
         foreach ($qs as $q) {
-            $temprec = self::get_backup_ids_record($restoreid, 'question', $q->itemid);
-            $results[$q->itemid] = $temprec->info;
+            $results[$q->itemid] = backup_controller_dbops::decode_backup_temp_info($q->info);
         }
+        $qs->close();
         return $results;
     }
 
@@ -893,7 +894,7 @@ abstract class restore_dbops {
         $basepath = $basepath . '/files/';// Get backup file pool base
         $rs = $DB->get_recordset_sql($sql, $params);
         foreach ($rs as $rec) {
-            $file = (object)unserialize(base64_decode($rec->info));
+            $file = (object)backup_controller_dbops::decode_backup_temp_info($rec->info);
 
             // ignore root dirs (they are created automatically)
             if ($file->filepath == '/' && $file->filename == '.') {
@@ -1018,9 +1019,9 @@ abstract class restore_dbops {
         $themes    = get_list_of_themes(); // Get themes for quick search later
 
         // Iterate over all the included users with newitemid = 0, have to create them
-        $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user', 'newitemid' => 0), '', 'itemid, parentitemid');
+        $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user', 'newitemid' => 0), '', 'itemid, parentitemid, info');
         foreach ($rs as $recuser) {
-            $user = (object)self::get_backup_ids_record($restoreid, 'user', $recuser->itemid)->info;
+            $user = (object)backup_controller_dbops::decode_backup_temp_info($recuser->info);
 
             // if user lang doesn't exist here, use site default
             if (!array_key_exists($user->lang, $languages)) {
@@ -1409,9 +1410,9 @@ abstract class restore_dbops {
         }
 
         // Iterate over all the included users
-        $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user'), '', 'itemid');
+        $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user'), '', 'itemid, info');
         foreach ($rs as $recuser) {
-            $user = (object)self::get_backup_ids_record($restoreid, 'user', $recuser->itemid)->info;
+            $user = (object)backup_controller_dbops::decode_backup_temp_info($recuser->info);
 
             // Find the correct mnethostid for user before performing any further check
             if (empty($user->mnethosturl) || $user->mnethosturl === $CFG->wwwroot) {
@@ -1496,7 +1497,7 @@ abstract class restore_dbops {
         global $DB;
 
         // Store external files info in `info` field
-        $filerec->info     = base64_encode(serialize($filerec)); // Serialize the whole rec in info
+        $filerec->info     = backup_controller_dbops::encode_backup_temp_info($filerec); // Encode the whole record into info.
         $filerec->backupid = $restoreid;
         $DB->insert_record('backup_files_temp', $filerec);
     }
@@ -1511,7 +1512,7 @@ abstract class restore_dbops {
             $extrarecord['parentitemid'] = $parentitemid;
         }
         if ($info != null) {
-            $extrarecord['info'] = base64_encode(serialize($info));
+            $extrarecord['info'] = backup_controller_dbops::encode_backup_temp_info($info);
         }
 
         self::set_backup_ids_cached($restoreid, $itemname, $itemid, $extrarecord);
@@ -1520,8 +1521,9 @@ abstract class restore_dbops {
     public static function get_backup_ids_record($restoreid, $itemname, $itemid) {
         $dbrec = self::get_backup_ids_cached($restoreid, $itemname, $itemid);
 
+        // We must test if info is a string, as the cache stores info in object form.
         if ($dbrec && isset($dbrec->info) && is_string($dbrec->info)) {
-            $dbrec->info = unserialize(base64_decode($dbrec->info));
+            $dbrec->info = backup_controller_dbops::decode_backup_temp_info($dbrec->info);
         }
 
         return $dbrec;
@@ -1566,18 +1568,17 @@ abstract class restore_dbops {
         // Get the course context
         $coursectx = context_course::instance($courseid);
         // Get all the mapped roles we have
-        $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid');
+        $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid, info, newitemid');
         foreach ($rs as $recrole) {
-            // Get the complete temp_ids record
-            $role = (object)self::get_backup_ids_record($restoreid, 'role', $recrole->itemid);
+            $info = backup_controller_dbops::decode_backup_temp_info($recrole->info);
             // If it's one mapped role and we have one name for it
-            if (!empty($role->newitemid) && !empty($role->info['nameincourse'])) {
+            if (!empty($recrole->newitemid) && !empty($info['nameincourse'])) {
                 // If role name doesn't exist, add it
                 $rolename = new stdclass();
-                $rolename->roleid = $role->newitemid;
+                $rolename->roleid = $recrole->newitemid;
                 $rolename->contextid = $coursectx->id;
                 if (!$DB->record_exists('role_names', (array)$rolename)) {
-                    $rolename->name = $role->info['nameincourse'];
+                    $rolename->name = $info['nameincourse'];
                     $DB->insert_record('role_names', $rolename);
                 }
             }
index 55d57ba..e57dc7e 100644 (file)
@@ -147,6 +147,18 @@ class backup_dbops_testcase extends advanced_testcase {
         // Drop and check it doesn't exists anymore
         backup_controller_dbops::drop_backup_ids_temp_table('testingid');
         $this->assertFalse($dbman->table_exists('backup_ids_temp'));
+
+        // Test encoding/decoding of backup_ids_temp,backup_files_temp encode/decode functions.
+        // We need to handle both objects and data elements.
+        $object = new stdClass();
+        $object->item1 = 10;
+        $object->item2 = 'a String';
+        $testarray = array($object, 10, null, 'string', array('a' => 'b', 1 => 1));
+        foreach ($testarray as $item) {
+            $encoded = backup_controller_dbops::encode_backup_temp_info($item);
+            $decoded = backup_controller_dbops::decode_backup_temp_info($encoded);
+            $this->assertEquals($item, $decoded);
+        }
     }
 
     /**
index 3af2839..099a498 100644 (file)
@@ -21,7 +21,8 @@ Feature: Duplicate activities
     And I add a "Database" to section "1" and I fill the form with:
       | Name | Test database name |
       | Description | Test database description |
-    When I click on "Duplicate" "link" in the "#section-1" "css_element"
+    And I click on "Actions" "link" in the "Test database name" activity
+    When I click on "Duplicate" "link" in the "Test database name" activity
     And I press "Continue"
     And I press "Edit the new copy"
     And I fill the moodle form with:
index 58a9b74..4ecf084 100644 (file)
@@ -60,10 +60,11 @@ class OpenBadgesBackpackHandler {
         }
 
         $options = array(
-            'FRESH_CONNECT' => true,
+            'FRESH_CONNECT'  => true,
             'RETURNTRANSFER' => true,
-            'FORBID_REUSE' => true,
-            'HEADER' => 0,
+            'FORBID_REUSE'   => true,
+            'HEADER'         => 0,
+            'HTTPHEADER'     => array('Expect:'),
             'CONNECTTIMEOUT' => 3,
         );
 
index b51d155..9ccafc1 100644 (file)
@@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die();
 global $CFG;
 require_once($CFG->libdir . '/badgeslib.php');
 
-class badges_testcase extends advanced_testcase {
+class core_badgeslib_testcase extends advanced_testcase {
     protected $badgeid;
 
     protected function setUp() {
index 3cac07a..e7eaf88 100644 (file)
@@ -103,10 +103,6 @@ class behat_block_comments extends behat_base {
         $deleteicon = $this->find('css', '.comment-delete a img', $deleteexception, $commentnode);
         $deleteicon->click();
 
-        // Yes confirm.
-        $confirmnode = $this->find('xpath', "//div[@class='comment-delete-confirm']/descendant::a[contains(., '" . get_string('yes') . "')]");
-        $confirmnode->click();
-
         // Wait for the AJAX request.
         $this->getSession()->wait(4 * 1000, false);
     }
index 00752d3..95abb63 100644 (file)
@@ -34,5 +34,17 @@ $capabilities = array(
         ),
 
         'clonepermissionsfrom' => 'moodle/my:manageblocks'
+    ),
+
+    'block/course_overview:addinstance' => array(
+        'riskbitmask' => RISK_SPAM | RISK_XSS,
+
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_BLOCK,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW
+        ),
+
+        'clonepermissionsfrom' => 'moodle/site:manageblocks'
     )
 );
index bc159de..71267b6 100644 (file)
 function block_course_overview_get_overviews($courses) {
     $htmlarray = array();
     if ($modules = get_plugin_list_with_function('mod', 'print_overview')) {
-        foreach ($modules as $fname) {
-            $fname($courses,$htmlarray);
+        // Split courses list into batches with no more than MAX_MODINFO_CACHE_SIZE courses in one batch.
+        // Otherwise we exceed the cache limit in get_fast_modinfo() and rebuild it too often.
+        if (defined('MAX_MODINFO_CACHE_SIZE') && MAX_MODINFO_CACHE_SIZE > 0 && count($courses) > MAX_MODINFO_CACHE_SIZE) {
+            $batches = array_chunk($courses, MAX_MODINFO_CACHE_SIZE, true);
+        } else {
+            $batches = array($courses);
+        }
+        foreach ($batches as $courses) {
+            foreach ($modules as $fname) {
+                $fname($courses, $htmlarray);
+            }
         }
     }
     return $htmlarray;
index 3a6878d..8607737 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2013050100;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2013073000;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2013050100;        // Requires this Moodle version
 $plugin->component = 'block_course_overview'; // Full name of the plugin (used for diagnostics)
index 102a03c..567250b 100644 (file)
@@ -57,12 +57,15 @@ Feature: View my courses in navigation block
     Then I should see "cat1" in the "div.block_navigation .type_system" "css_element"
     And I should see "cat3" in the "div.block_navigation .type_system" "css_element"
     And I should not see "cat2" in the "div.block_navigation .type_system" "css_element"
-    When I expand "cat3" node
-    Then I should see "cat31" in the "div.block_navigation .type_system" "css_element"
+    And I expand "cat3" node
+    And I wait "2" seconds
+    And I should see "cat31" in the "div.block_navigation .type_system" "css_element"
     And I should see "cat33" in the "div.block_navigation .type_system" "css_element"
     And I should not see "cat32" in the "div.block_navigation .type_system" "css_element"
-    When I expand "cat31" node
-    Then I should see "c31" in the "div.block_navigation .type_system" "css_element"
-    When I expand "cat33" node
+    And I expand "cat31" node
+    And I wait "2" seconds
+    And I should see "c31" in the "div.block_navigation .type_system" "css_element"
+    And I expand "cat33" node
+    And I wait "2" seconds
     And I should see "c331" in the "div.block_navigation .type_system" "css_element"
     And I should not see "c332" in the "div.block_navigation .type_system" "css_element"
index bc4420c..fc150c2 100644 (file)
@@ -59,7 +59,8 @@ class block_site_main_menu extends block_list {
             return $this->content;
         }
 
-/// slow & hacky editing mode
+        // Slow & hacky editing mode.
+        /** @var core_course_renderer $courserenderer */
         $courserenderer = $this->page->get_renderer('core', 'course');
         $ismoving = ismoving($course->id);
         course_create_sections_if_missing($course, 0);
@@ -72,7 +73,7 @@ class block_site_main_menu extends block_list {
             $strcancel= get_string('cancel');
             $stractivityclipboard = $USER->activitycopyname;
         }
-    /// Casting $course->modinfo to string prevents one notice when the field is null
+        // Casting $course->modinfo to string prevents one notice when the field is null.
         $editbuttons = '';
 
         if ($ismoving) {
@@ -90,8 +91,9 @@ class block_site_main_menu extends block_list {
                 if (!$ismoving) {
                     $actions = course_get_cm_edit_actions($mod, -1);
                     $editbuttons = html_writer::tag('div',
-                            $courserenderer->course_section_cm_edit_actions($actions),
-                            array('class' => 'buttons'));
+                        $courserenderer->course_section_cm_edit_actions($actions, $mod, array('donotenhance' => true)),
+                        array('class' => 'buttons')
+                    );
                 } else {
                     $editbuttons = '';
                 }
index 5e394e0..bcab936 100644 (file)
@@ -62,7 +62,8 @@ class block_social_activities extends block_list {
         }
 
 
-/// slow & hacky editing mode
+        // Slow & hacky editing mode.
+        /** @var core_course_renderer $courserenderer */
         $courserenderer = $this->page->get_renderer('core', 'course');
         $ismoving = ismoving($course->id);
         $modinfo = get_fast_modinfo($course);
@@ -74,7 +75,7 @@ class block_social_activities extends block_list {
             $strcancel= get_string('cancel');
             $stractivityclipboard = $USER->activitycopyname;
         }
-    /// Casting $course->modinfo to string prevents one notice when the field is null
+        // Casting $course->modinfo to string prevents one notice when the field is null.
         $editbuttons = '';
 
         if ($ismoving) {
@@ -92,7 +93,7 @@ class block_social_activities extends block_list {
                 if (!$ismoving) {
                     $actions = course_get_cm_edit_actions($mod, -1);
                     $editbuttons = '<br />'.
-                            $courserenderer->course_section_cm_edit_actions($actions);
+                            $courserenderer->course_section_cm_edit_actions($actions, $mod);
                 } else {
                     $editbuttons = '';
                 }
index a508052..7038060 100644 (file)
@@ -239,7 +239,6 @@ class blog_entry implements renderable {
     /**
      * Inserts this entry in the database. Access control checks must be done by calling code.
      * TODO Set the publishstate correctly
-     * @param mform $form Used for attachments
      * @return void
      */
     public function add() {
@@ -259,11 +258,17 @@ class blog_entry implements renderable {
 
         if (!empty($CFG->useblogassociations)) {
             $this->add_associations();
-            add_to_log(SITEID, 'blog', 'add', 'index.php?userid='.$this->userid.'&entryid='.$this->id, $this->subject);
         }
 
         tag_set('post', $this->id, $this->tags);
-        events_trigger('blog_entry_added', $this);
+
+        // Trigger an event for the new entry.
+        $event = \core\event\blog_entry_created::create(array('objectid' => $this->id,
+                                                            'userid'   => $this->userid,
+                                                            'other'    => array ("subject" => $this->subject)
+                                                      ));
+        $event->set_custom_data($this);
+        $event->trigger();
     }
 
     /**
index 7828194..1527a3c 100644 (file)
@@ -55,7 +55,6 @@ Feature: Comment on a blog entry
     And I follow "Save comment"
     And I wait "4" seconds
     When I click on ".comment-delete a" "css_element"
-    And I click on "Yes" "link"
     And I wait "4" seconds
     Then I should not see "$My own >nasty< \"string\"!"
     And I follow "Blog post from user 1"
index f385eb3..6ed0658 100644 (file)
@@ -31,7 +31,7 @@ require_once($CFG->dirroot . '/blog/lib.php');
 /**
  * Test functions that rely on the DB tables
  */
-class bloglib_testcase extends advanced_testcase {
+class core_bloglib_testcase extends advanced_testcase {
 
     private $courseid; // To store important ids to be used in tests
     private $cmid;
@@ -150,4 +150,33 @@ class bloglib_testcase extends advanced_testcase {
         $blog_headers = blog_get_headers($this->courseid);
         $this->assertNotEquals($blog_headers['heading'], '');
     }
+
+    /**
+     * Test various blog related events.
+     */
+    public function test_blog_entry_events() {
+        global $USER;
+
+        $this->setAdminUser();
+        $this->resetAfterTest();
+
+        // Create a blog entry.
+        $blog = new blog_entry();
+        $blog->summary = "This is summary of blog";
+        $blog->subject = "Subject of blog";
+        $states = blog_entry::get_applicable_publish_states();
+        $blog->publishstate = reset($states);
+        $sink = $this->redirectEvents();
+        $blog->add();
+        $events = $sink->get_events();
+        $event = reset($events);
+        $sitecontext = context_system::instance();
+
+        // Validate event data.
+        $this->assertInstanceOf('\core\event\blog_entry_created', $event);
+        $this->assertEquals($sitecontext->id, $event->contextid);
+        $this->assertEquals($blog->id, $event->objectid);
+        $this->assertEquals($USER->id, $event->userid);
+        $this->assertEquals("post", $event->objecttable);
+    }
 }
index 96d11b2..adc1a9b 100644 (file)
@@ -266,7 +266,7 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
         $this->definition = $definition;
         $hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id());
         $this->path = $this->filestorepath.'/'.$hash;
-        make_writable_directory($this->path);
+        make_writable_directory($this->path, false);
         if ($this->prescan && $definition->get_mode() !== self::MODE_REQUEST) {
             $this->prescan = false;
         }
@@ -314,11 +314,13 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
             return $this->path . '/' . $key . '.cache';
         } else {
             // We are using a single subdirectory to achieve 1 level.
-            $subdir = substr($key, 0, 3);
+           // We suffix the subdir so it does not clash with any windows
+           // reserved filenames like 'con'.
+            $subdir = substr($key, 0, 3) . '-cache';
             $dir = $this->path . '/' . $subdir;
             if ($create) {
                 // Create the directory. This function does it recursivily!
-                make_writable_directory($dir);
+                make_writable_directory($dir, false);
             }
             return $dir . '/' . $key . '.cache';
         }
diff --git a/cache/tests/administration_helper_test.php b/cache/tests/administration_helper_test.php
new file mode 100644 (file)
index 0000000..6a1630d
--- /dev/null
@@ -0,0 +1,236 @@
+<?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/>.
+
+/**
+ * PHPunit tests for the cache API and in particular things in locallib.php
+ *
+ * This file is part of Moodle's cache API, affectionately called MUC.
+ * It contains the components that are requried in order to use caching.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+// Include the necessary evils.
+global $CFG;
+require_once($CFG->dirroot.'/cache/locallib.php');
+require_once($CFG->dirroot.'/cache/tests/fixtures/lib.php');
+
+
+/**
+ * PHPunit tests for the cache API and in particular the cache_administration_helper
+ *
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_cache_administration_helper_testcase extends advanced_testcase {
+
+    /**
+     * Set things back to the default before each test.
+     */
+    public function setUp() {
+        parent::setUp();
+        cache_factory::reset();
+        cache_config_phpunittest::create_default_configuration();
+    }
+
+    /**
+     * Final task is to reset the cache system
+     */
+    public static function tearDownAfterClass() {
+        parent::tearDownAfterClass();
+        cache_factory::reset();
+    }
+
+    /**
+     * Test the numerous summaries the helper can produce.
+     */
+    public function test_get_summaries() {
+        // First the preparation.
+        $config = cache_config_writer::instance();
+        $this->assertTrue($config->add_store_instance('summariesstore', 'file'));
+        $config->set_definition_mappings('core/eventinvalidation', array('summariesstore'));
+        $this->assertTrue($config->set_mode_mappings(array(
+            cache_store::MODE_APPLICATION => array('summariesstore'),
+            cache_store::MODE_SESSION => array('default_session'),
+            cache_store::MODE_REQUEST => array('default_request'),
+        )));
+
+        $storesummaries = cache_administration_helper::get_store_instance_summaries();
+        $this->assertInternalType('array', $storesummaries);
+        $this->assertArrayHasKey('summariesstore', $storesummaries);
+        $summary = $storesummaries['summariesstore'];
+        // Check the keys
+        $this->assertArrayHasKey('name', $summary);
+        $this->assertArrayHasKey('plugin', $summary);
+        $this->assertArrayHasKey('default', $summary);
+        $this->assertArrayHasKey('isready', $summary);
+        $this->assertArrayHasKey('requirementsmet', $summary);
+        $this->assertArrayHasKey('mappings', $summary);
+        $this->assertArrayHasKey('modes', $summary);
+        $this->assertArrayHasKey('supports', $summary);
+        // Check the important/known values
+        $this->assertEquals('summariesstore', $summary['name']);
+        $this->assertEquals('file', $summary['plugin']);
+        $this->assertEquals(0, $summary['default']);
+        $this->assertEquals(1, $summary['isready']);
+        $this->assertEquals(1, $summary['requirementsmet']);
+        $this->assertEquals(1, $summary['mappings']);
+
+        $definitionsummaries = cache_administration_helper::get_definition_summaries();
+        $this->assertInternalType('array', $definitionsummaries);
+        $this->assertArrayHasKey('core/eventinvalidation', $definitionsummaries);
+        $summary = $definitionsummaries['core/eventinvalidation'];
+        // Check the keys
+        $this->assertArrayHasKey('id', $summary);
+        $this->assertArrayHasKey('name', $summary);
+        $this->assertArrayHasKey('mode', $summary);
+        $this->assertArrayHasKey('component', $summary);
+        $this->assertArrayHasKey('area', $summary);
+        $this->assertArrayHasKey('mappings', $summary);
+        // Check the important/known values
+        $this->assertEquals('core/eventinvalidation', $summary['id']);
+        $this->assertInstanceOf('lang_string', $summary['name']);
+        $this->assertEquals(cache_store::MODE_APPLICATION, $summary['mode']);
+        $this->assertEquals('core', $summary['component']);
+        $this->assertEquals('eventinvalidation', $summary['area']);
+        $this->assertInternalType('array', $summary['mappings']);
+        $this->assertContains('summariesstore', $summary['mappings']);
+
+        $pluginsummaries = cache_administration_helper::get_store_plugin_summaries();
+        $this->assertInternalType('array', $pluginsummaries);
+        $this->assertArrayHasKey('file', $pluginsummaries);
+        $summary = $pluginsummaries['file'];
+        // Check the keys
+        $this->assertArrayHasKey('name', $summary);
+        $this->assertArrayHasKey('requirementsmet', $summary);
+        $this->assertArrayHasKey('instances', $summary);
+        $this->assertArrayHasKey('modes', $summary);
+        $this->assertArrayHasKey('supports', $summary);
+        $this->assertArrayHasKey('canaddinstance', $summary);
+
+        $locksummaries = cache_administration_helper::get_lock_summaries();
+        $this->assertInternalType('array', $locksummaries);
+        $this->assertTrue(count($locksummaries) > 0);
+
+        $mappings = cache_administration_helper::get_default_mode_stores();
+        $this->assertInternalType('array', $mappings);
+        $this->assertCount(3, $mappings);
+        $this->assertArrayHasKey(cache_store::MODE_APPLICATION, $mappings);
+        $this->assertInternalType('array', $mappings[cache_store::MODE_APPLICATION]);
+        $this->assertContains('summariesstore', $mappings[cache_store::MODE_APPLICATION]);
+
+        $potentials = cache_administration_helper::get_definition_store_options('core', 'eventinvalidation');
+        $this->assertInternalType('array', $potentials); // Currently used, suitable, default
+        $this->assertCount(3, $potentials);
+        $this->assertArrayHasKey('summariesstore', $potentials[0]);
+        $this->assertArrayHasKey('summariesstore', $potentials[1]);
+        $this->assertArrayHasKey('default_application', $potentials[1]);
+    }
+
+    /**
+     * Test instantiating an add store form.
+     */
+    public function test_get_add_store_form() {
+        $form = cache_administration_helper::get_add_store_form('file');
+        $this->assertInstanceOf('moodleform', $form);
+
+        try {
+            $form = cache_administration_helper::get_add_store_form('somethingstupid');
+            $this->fail('You should not be able to create an add form for a store plugin that does not exist.');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e, 'Needs to be: ' .get_class($e)." ::: ".$e->getMessage());
+        }
+    }
+
+    /**
+     * Test instantiating a form to edit a store instance.
+     */
+    public function test_get_edit_store_form() {
+        $config = cache_config_writer::instance();
+        $this->assertTrue($config->add_store_instance('summariesstore', 'file'));
+
+        $form = cache_administration_helper::get_edit_store_form('file', 'summariesstore');
+        $this->assertInstanceOf('moodleform', $form);
+
+        try {
+            $form = cache_administration_helper::get_edit_store_form('somethingstupid', 'moron');
+            $this->fail('You should not be able to create an edit form for a store plugin that does not exist.');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        try {
+            $form = cache_administration_helper::get_edit_store_form('file', 'blisters');
+            $this->fail('You should not be able to create an edit form for a store plugin that does not exist.');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+    }
+
+    /**
+     * Test the hash_key functionality.
+     */
+    public function test_hash_key() {
+        global $CFG;
+
+        $currentdebugging = $CFG->debug;
+
+        $CFG->debug = E_ALL;
+
+        // First with simplekeys
+        $instance = cache_config_phpunittest::instance(true);
+        $instance->phpunit_add_definition('phpunit/hashtest', array(
+            'mode' => cache_store::MODE_APPLICATION,
+            'component' => 'phpunit',
+            'area' => 'hashtest',
+            'simplekeys' => true
+        ));
+        $factory = cache_factory::instance();
+        $definition = $factory->create_definition('phpunit', 'hashtest');
+
+        $result = cache_helper::hash_key('test', $definition);
+        $this->assertEquals('test-'.$definition->generate_single_key_prefix(), $result);
+
+        try {
+            cache_helper::hash_key('test/test', $definition);
+            $this->fail('Invalid key was allowed, you should see this.');
+        } catch (coding_exception $e) {
+            $this->assertEquals('test/test', $e->debuginfo);
+        }
+
+        // Second without simple keys
+        $instance->phpunit_add_definition('phpunit/hashtest2', array(
+            'mode' => cache_store::MODE_APPLICATION,
+            'component' => 'phpunit',
+            'area' => 'hashtest2',
+            'simplekeys' => false
+        ));
+        $definition = $factory->create_definition('phpunit', 'hashtest2');
+
+        $result = cache_helper::hash_key('test', $definition);
+        $this->assertEquals(sha1($definition->generate_single_key_prefix().'-test'), $result);
+
+        $result = cache_helper::hash_key('test/test', $definition);
+        $this->assertEquals(sha1($definition->generate_single_key_prefix().'-test/test'), $result);
+
+        $CFG->debug = $currentdebugging;
+    }
+}
index 5121875..e7f4cdc 100644 (file)
@@ -39,7 +39,7 @@ require_once($CFG->dirroot.'/cache/tests/fixtures/lib.php');
  * @copyright  2012 Sam Hemelryk
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class cache_phpunit_tests extends advanced_testcase {
+class core_cache_testcase extends advanced_testcase {
 
     /**
      * Set things back to the default before each test.
@@ -119,6 +119,23 @@ class cache_phpunit_tests extends advanced_testcase {
         }
     }
 
+    /**
+     * Tests for cache keys that would break on windows.
+     */
+    public function test_windows_nasty_keys() {
+        $instance = cache_config_phpunittest::instance();
+        $instance->phpunit_add_definition('phpunit/windowskeytest', array(
+            'mode' => cache_store::MODE_APPLICATION,
+            'component' => 'phpunit',
+            'area' => 'windowskeytest',
+            'simplekeys' => true,
+            'simpledata' => true
+        ));
+        $cache = cache::make('phpunit', 'windowskeytest');
+        $this->assertTrue($cache->set('contest', 'test data 1'));
+        $this->assertEquals('test data 1', $cache->get('contest'));
+    }
+
     /**
      * Tests the default application cache
      */
@@ -191,9 +208,9 @@ class cache_phpunit_tests extends advanced_testcase {
      * @param cache_loader $cache
      */
     protected function run_on_cache(cache_loader $cache) {
-        $key = 'testkey';
+        $key = 'contestkey';
         $datascalars = array('test data', null);
-        $dataarray = array('test' => 'data', 'part' => 'two');
+        $dataarray = array('contest' => 'data', 'part' => 'two');
         $dataobject = (object)$dataarray;
 
         foreach ($datascalars as $datascalar) {
@@ -850,7 +867,7 @@ class cache_phpunit_tests extends advanced_testcase {
         // OK data added, data invalidated, and invalidation time has been set.
         // Now we need to manually add back the data and adjust the invalidation time.
         $hash = md5(cache_store::MODE_APPLICATION.'/phpunit/eventinvalidationtest/'.$CFG->wwwroot.'phpunit');
-        $timefile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las/lastinvalidation-$hash.cache";
+        $timefile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las-cache/lastinvalidation-$hash.cache";
         // Make sure the file is correct.
         $this->assertTrue(file_exists($timefile));
         $timecont = serialize(cache::now() - 60); // Back 60sec in the past to force it to re-invalidate.
@@ -858,7 +875,7 @@ class cache_phpunit_tests extends advanced_testcase {
         file_put_contents($timefile, $timecont);
         $this->assertTrue(file_exists($timefile));
 
-        $datafile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/tes/testkey1-$hash.cache";
+        $datafile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/tes-cache/testkey1-$hash.cache";
         $datacont = serialize("test data 1");
         make_writable_directory(dirname($datafile));
         file_put_contents($datafile, $datacont);
similarity index 57%
rename from cache/tests/locallib_test.php
rename to cache/tests/config_writer_test.php
index 56397f2..f49f65e 100644 (file)
@@ -39,7 +39,7 @@ require_once($CFG->dirroot.'/cache/tests/fixtures/lib.php');
  * @copyright  2012 Sam Hemelryk
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class cache_config_writer_phpunit_tests extends advanced_testcase {
+class core_cache_config_writer_testcase extends advanced_testcase {
 
     /**
      * Set things back to the default before each test.
@@ -288,204 +288,3 @@ class cache_config_writer_phpunit_tests extends advanced_testcase {
         }
     }
 }
-
-/**
- * PHPunit tests for the cache API and in particular the cache_administration_helper
- *
- * @copyright  2012 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class cache_administration_helper_phpunit_tests extends advanced_testcase {
-
-    /**
-     * Set things back to the default before each test.
-     */
-    public function setUp() {
-        parent::setUp();
-        cache_factory::reset();
-        cache_config_phpunittest::create_default_configuration();
-    }
-
-    /**
-     * Final task is to reset the cache system
-     */
-    public static function tearDownAfterClass() {
-        parent::tearDownAfterClass();
-        cache_factory::reset();
-    }
-
-    /**
-     * Test the numerous summaries the helper can produce.
-     */
-    public function test_get_summaries() {
-        // First the preparation.
-        $config = cache_config_writer::instance();
-        $this->assertTrue($config->add_store_instance('summariesstore', 'file'));
-        $config->set_definition_mappings('core/eventinvalidation', array('summariesstore'));
-        $this->assertTrue($config->set_mode_mappings(array(
-            cache_store::MODE_APPLICATION => array('summariesstore'),
-            cache_store::MODE_SESSION => array('default_session'),
-            cache_store::MODE_REQUEST => array('default_request'),
-        )));
-
-        $storesummaries = cache_administration_helper::get_store_instance_summaries();
-        $this->assertInternalType('array', $storesummaries);
-        $this->assertArrayHasKey('summariesstore', $storesummaries);
-        $summary = $storesummaries['summariesstore'];
-        // Check the keys
-        $this->assertArrayHasKey('name', $summary);
-        $this->assertArrayHasKey('plugin', $summary);
-        $this->assertArrayHasKey('default', $summary);
-        $this->assertArrayHasKey('isready', $summary);
-        $this->assertArrayHasKey('requirementsmet', $summary);
-        $this->assertArrayHasKey('mappings', $summary);
-        $this->assertArrayHasKey('modes', $summary);
-        $this->assertArrayHasKey('supports', $summary);
-        // Check the important/known values
-        $this->assertEquals('summariesstore', $summary['name']);
-        $this->assertEquals('file', $summary['plugin']);
-        $this->assertEquals(0, $summary['default']);
-        $this->assertEquals(1, $summary['isready']);
-        $this->assertEquals(1, $summary['requirementsmet']);
-        $this->assertEquals(1, $summary['mappings']);
-
-        $definitionsummaries = cache_administration_helper::get_definition_summaries();
-        $this->assertInternalType('array', $definitionsummaries);
-        $this->assertArrayHasKey('core/eventinvalidation', $definitionsummaries);
-        $summary = $definitionsummaries['core/eventinvalidation'];
-        // Check the keys
-        $this->assertArrayHasKey('id', $summary);
-        $this->assertArrayHasKey('name', $summary);
-        $this->assertArrayHasKey('mode', $summary);
-        $this->assertArrayHasKey('component', $summary);
-        $this->assertArrayHasKey('area', $summary);
-        $this->assertArrayHasKey('mappings', $summary);
-        // Check the important/known values
-        $this->assertEquals('core/eventinvalidation', $summary['id']);
-        $this->assertInstanceOf('lang_string', $summary['name']);
-        $this->assertEquals(cache_store::MODE_APPLICATION, $summary['mode']);
-        $this->assertEquals('core', $summary['component']);
-        $this->assertEquals('eventinvalidation', $summary['area']);
-        $this->assertInternalType('array', $summary['mappings']);
-        $this->assertContains('summariesstore', $summary['mappings']);
-
-        $pluginsummaries = cache_administration_helper::get_store_plugin_summaries();
-        $this->assertInternalType('array', $pluginsummaries);
-        $this->assertArrayHasKey('file', $pluginsummaries);
-        $summary = $pluginsummaries['file'];
-        // Check the keys
-        $this->assertArrayHasKey('name', $summary);
-        $this->assertArrayHasKey('requirementsmet', $summary);
-        $this->assertArrayHasKey('instances', $summary);
-        $this->assertArrayHasKey('modes', $summary);
-        $this->assertArrayHasKey('supports', $summary);
-        $this->assertArrayHasKey('canaddinstance', $summary);
-
-        $locksummaries = cache_administration_helper::get_lock_summaries();
-        $this->assertInternalType('array', $locksummaries);
-        $this->assertTrue(count($locksummaries) > 0);
-
-        $mappings = cache_administration_helper::get_default_mode_stores();
-        $this->assertInternalType('array', $mappings);
-        $this->assertCount(3, $mappings);
-        $this->assertArrayHasKey(cache_store::MODE_APPLICATION, $mappings);
-        $this->assertInternalType('array', $mappings[cache_store::MODE_APPLICATION]);
-        $this->assertContains('summariesstore', $mappings[cache_store::MODE_APPLICATION]);
-
-        $potentials = cache_administration_helper::get_definition_store_options('core', 'eventinvalidation');
-        $this->assertInternalType('array', $potentials); // Currently used, suitable, default
-        $this->assertCount(3, $potentials);
-        $this->assertArrayHasKey('summariesstore', $potentials[0]);
-        $this->assertArrayHasKey('summariesstore', $potentials[1]);
-        $this->assertArrayHasKey('default_application', $potentials[1]);
-    }
-
-    /**
-     * Test instantiating an add store form.
-     */
-    public function test_get_add_store_form() {
-        $form = cache_administration_helper::get_add_store_form('file');
-        $this->assertInstanceOf('moodleform', $form);
-
-        try {
-            $form = cache_administration_helper::get_add_store_form('somethingstupid');
-            $this->fail('You should not be able to create an add form for a store plugin that does not exist.');
-        } catch (moodle_exception $e) {
-            $this->assertInstanceOf('coding_exception', $e, 'Needs to be: ' .get_class($e)." ::: ".$e->getMessage());
-        }
-    }
-
-    /**
-     * Test instantiating a form to edit a store instance.
-     */
-    public function test_get_edit_store_form() {
-        $config = cache_config_writer::instance();
-        $this->assertTrue($config->add_store_instance('summariesstore', 'file'));
-
-        $form = cache_administration_helper::get_edit_store_form('file', 'summariesstore');
-        $this->assertInstanceOf('moodleform', $form);
-
-        try {
-            $form = cache_administration_helper::get_edit_store_form('somethingstupid', 'moron');
-            $this->fail('You should not be able to create an edit form for a store plugin that does not exist.');
-        } catch (moodle_exception $e) {
-            $this->assertInstanceOf('coding_exception', $e);
-        }
-
-        try {
-            $form = cache_administration_helper::get_edit_store_form('file', 'blisters');
-            $this->fail('You should not be able to create an edit form for a store plugin that does not exist.');
-        } catch (moodle_exception $e) {
-            $this->assertInstanceOf('coding_exception', $e);
-        }
-    }
-
-    /**
-     * Test the hash_key functionality.
-     */
-    public function test_hash_key() {
-        global $CFG;
-
-        $currentdebugging = $CFG->debug;
-
-        $CFG->debug = E_ALL;
-
-        // First with simplekeys
-        $instance = cache_config_phpunittest::instance(true);
-        $instance->phpunit_add_definition('phpunit/hashtest', array(
-            'mode' => cache_store::MODE_APPLICATION,
-            'component' => 'phpunit',
-            'area' => 'hashtest',
-            'simplekeys' => true
-        ));
-        $factory = cache_factory::instance();
-        $definition = $factory->create_definition('phpunit', 'hashtest');
-
-        $result = cache_helper::hash_key('test', $definition);
-        $this->assertEquals('test-'.$definition->generate_single_key_prefix(), $result);
-
-        try {
-            cache_helper::hash_key('test/test', $definition);
-            $this->fail('Invalid key was allowed, you should see this.');
-        } catch (coding_exception $e) {
-            $this->assertEquals('test/test', $e->debuginfo);
-        }
-
-        // Second without simple keys
-        $instance->phpunit_add_definition('phpunit/hashtest2', array(
-            'mode' => cache_store::MODE_APPLICATION,
-            'component' => 'phpunit',
-            'area' => 'hashtest2',
-            'simplekeys' => false
-        ));
-        $definition = $factory->create_definition('phpunit', 'hashtest2');
-
-        $result = cache_helper::hash_key('test', $definition);
-        $this->assertEquals(sha1($definition->generate_single_key_prefix().'-test'), $result);
-
-        $result = cache_helper::hash_key('test/test', $definition);
-        $this->assertEquals(sha1($definition->generate_single_key_prefix().'-test/test'), $result);
-
-        $CFG->debug = $currentdebugging;
-    }
-}
index e38538e..44bb9e7 100644 (file)
@@ -38,7 +38,7 @@ require_once($CFG->dirroot . '/webservice/tests/helpers.php');
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @since Moodle 2.5
  */
-class core_calendar_external_testcase extends externallib_advanced_testcase {
+class core_calendar_externallib_testcase extends externallib_advanced_testcase {
 
     /**
      * Tests set up
index db0b2f0..e6446e7 100644 (file)
@@ -37,7 +37,7 @@ require_once("$CFG->dirroot/cohort/lib.php");
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class cohort_testcase extends advanced_testcase {
+class core_cohort_cohortlib_testcase extends advanced_testcase {
 
     public function test_cohort_add_cohort() {
         global $DB;
index c8137fb..8f7f4ba 100644 (file)
@@ -30,7 +30,7 @@ global $CFG;
 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
 require_once($CFG->dirroot . '/cohort/externallib.php');
 
-class core_cohort_external_testcase extends externallib_advanced_testcase {
+class core_cohort_externallib_testcase extends externallib_advanced_testcase {
 
     /**
      * Test create_cohorts
index 544d82f..d1cf768 100644 (file)
@@ -59,11 +59,6 @@ M.core_comment = {
                     }, this);
                 }
                 scope.toggle_textarea(false);
-                CommentHelper.confirmoverlay = new Y.Overlay({
-bodyContent: '<div class="comment-delete-confirm"><a href="#" id="confirmdelete-'+this.client_id+'">'+M.str.moodle.yes+'</a> <a href="#" id="canceldelete-'+this.client_id+'">'+M.str.moodle.no+'</a></div>',
-                                        visible: false
-                                        });
-                CommentHelper.confirmoverlay.render(document.body);
             },
             post: function() {
                 var ta = Y.one('#dlg-content-'+this.client_id);
@@ -238,7 +233,6 @@ bodyContent: '<div class="comment-delete-confirm"><a href="#" id="confirmdelete-
             dodelete: function(id) { // note: delete is a reserved word in javascript, chrome and safary do not like it at all here!
                 var scope = this;
                 var params = {'commentid': id};
-                scope.cancel_delete();
                 function remove_dom(type, anim, cmt) {
                     cmt.remove();
                 }
@@ -294,37 +288,23 @@ bodyContent: '<div class="comment-delete-confirm"><a href="#" id="confirmdelete-
                         if (commentid[1]) {
                             Y.Event.purgeElement('#'+theid, false, 'click');
                         }
-                        node.on('click', function(e, node) {
+                        node.on('click', function(e) {
                             e.preventDefault();
-                            var width = CommentHelper.confirmoverlay.bodyNode.getStyle('width');
-                            var re = new RegExp("(\\d+).*", "i");
-                            var result = width.match(re);
-                            if (result[1]) {
-                                width = Number(result[1]);
-                            } else {
-                                width = 0;
+                            if (commentid[1]) {
+                                scope.dodelete(commentid[1]);
                             }
-                            //CommentHelper.confirmoverlay.set('xy', [e.pageX-(width/2), e.pageY]);
-                            CommentHelper.confirmoverlay.set('xy', [e.pageX-width-5, e.pageY]);
-                            CommentHelper.confirmoverlay.set('visible', true);
-                            Y.one('#canceldelete-'+scope.client_id).on('click', function(e) {
-                                e.preventDefault();
-                                scope.cancel_delete();
-                                });
-                            Y.Event.purgeElement('#confirmdelete-'+scope.client_id, false, 'click');
-                            Y.one('#confirmdelete-'+scope.client_id).on('click', function(e) {
-                                e.preventDefault();
-                                if (commentid[1]) {
-                                    scope.dodelete(commentid[1]);
-                                }
-                            });
-                        }, scope, node);
+                        });
+                        // Also handle space/enter key.
+                        node.on('key', function(e) {
+                            e.preventDefault();
+                            if (commentid[1]) {
+                                scope.dodelete(commentid[1]);
+                            }
+                        }, '13,32');
+                        // 13 and 32 are the keycodes for space and enter.
                     }
                 );
             },
-            cancel_delete: function() {
-                CommentHelper.confirmoverlay.set('visible', false);
-            },
             register_pagination: function() {
                 var scope = this;
                 // page buttons
@@ -374,7 +354,7 @@ bodyContent: '<div class="comment-delete-confirm"><a href="#" id="confirmdelete-
                 if (ta) {
                     //toggle_textarea.apply(ta, [false]);
                     //// reset textarea size
-                    ta.on('click', function() {
+                    ta.on('focus', function() {
                         this.toggle_textarea(true);
                     }, this);
                     //ta.onkeypress = function() {
index b1dbc63..118647c 100644 (file)
@@ -1053,8 +1053,12 @@ M.course_dndupload = {
      * @param sectionnumber the number of the selected course section
      */
     add_editing: function(elementid) {
+        var node = Y.one('#' + elementid);
         YUI().use('moodle-course-coursebase', function(Y) {
-            M.course.coursebase.invoke_function('setup_for_resource', '#' + elementid);
+            M.course.register_new_module(node);
         });
+        if (M.core.actionmenu && M.core.actionmenu.newDOMNode) {
+            M.core.actionmenu.newDOMNode(node);
+        }
     }
 };
index a1b8d02..fee7ec0 100644 (file)
@@ -744,11 +744,11 @@ class dndupload_ajax_processor {
         $resp->content = $mod->get_content();
         $resp->elementid = 'module-'.$mod->id;
         $actions = course_get_cm_edit_actions($mod, 0, $mod->sectionnum);
-        $resp->commands = ' '. $courserenderer->course_section_cm_edit_actions($actions);
+        $resp->commands = ' '. $courserenderer->course_section_cm_edit_actions($actions, $mod);
         $resp->onclick = $mod->get_on_click();
         $resp->visible = $mod->visible;
 
-        // if using groupings, then display grouping name
+        // If using groupings, then display grouping name.
         if (!empty($mod->groupingid) && has_capability('moodle/course:managegroups', $this->context)) {
             $groupings = groups_get_all_groupings($this->course->id);
             $resp->groupingname = format_string($groupings[$mod->groupingid]->name);
index 2481c55..f29a06e 100644 (file)
@@ -2,6 +2,14 @@ This files describes API changes for course formats
 
 Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
 
+=== 2.6 ===
+
+* core_course_renderer::course_section_cm_edit_actions has two new optional arguments and now uses and action_menu component.
+* core_course_renderer::course_section_cm has been altered to call core_course_renderer::course_section_cm_edit_actions with the two new arguments
+* An additional course renderer function has been created which allows you to
+  specify the wrapper for a course module within a section (e.g. the <li>).  This can be
+  found in core_course_renderer::course_section_cm_list_item().
+
 === 2.5 ===
 
 * Functions responsible for output in course/lib.php are deprecated, the code is moved to
index 2c54cdb..670e6d2 100644 (file)
@@ -1773,7 +1773,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
     $editcaps = array('moodle/course:manageactivities', 'moodle/course:activityvisibility', 'moodle/role:assign');
     $dupecaps = array('moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport');
 
-    // no permission to edit anything
+    // No permission to edit anything.
     if (!has_any_capability($editcaps, $modcontext) and !has_all_capabilities($dupecaps, $coursecontext)) {
         return array();
     }
@@ -1799,20 +1799,20 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
     }
     $actions = array();
 
-    // AJAX edit title
+    // AJAX edit title.
     if ($mod->has_view() && $hasmanageactivities &&
                 (($mod->course == $COURSE->id && course_ajax_enabled($COURSE)) ||
                  ($mod->course == SITEID && course_ajax_enabled($SITE)))) {
         // we will not display link if we are on some other-course page (where we should not see this module anyway)
-        $actions['title'] = new action_link(
+        $actions['title'] = new action_menu_link_secondary(
             new moodle_url($baseurl, array('update' => $mod->id)),
             new pix_icon('t/editstring', $str->edittitle, 'moodle', array('class' => 'iconsmall visibleifjs', 'title' => '')),
-            null,
-            array('class' => 'editing_title', 'title' => $str->edittitle)
+            $str->edittitle,
+            array('class' => 'editing_title', 'data-action' => 'edittitle')
         );
     }
 
-    // leftright
+    // Indent.
     if ($hasmanageactivities) {
         if (right_to_left()) {   // Exchange arrows on RTL
             $rightarrow = 't/left';
@@ -1822,86 +1822,90 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
             $leftarrow  = 't/left';
         }
 
+        $hiddenclass = 'hidden';
         if ($indent > 0) {
-            $actions['moveleft'] = new action_link(
-                new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '-1')),
-                new pix_icon($leftarrow, $str->moveleft, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-                null,
-                array('class' => 'editing_moveleft', 'title' => $str->moveleft)
-            );
+            $hiddenclass = '';
         }
+        $actions['moveleft'] = new action_menu_link_secondary(
+            new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '-1')),
+            new pix_icon($leftarrow, $str->moveleft, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+            $str->moveleft,
+            array('class' => 'editing_moveleft ' . $hiddenclass, 'data-action' => 'moveleft')
+        );
+        $hiddenclass = 'hidden';
         if ($indent >= 0) {
-            $actions['moveright'] = new action_link(
-                new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '1')),
-                new pix_icon($rightarrow, $str->moveright, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-                null,
-                array('class' => 'editing_moveright', 'title' => $str->moveright)
-            );
+            $hiddenclass = '';
         }
+        $actions['moveright'] = new action_menu_link_secondary(
+            new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '1')),
+            new pix_icon($rightarrow, $str->moveright, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+            $str->moveright,
+            array('class' => 'editing_moveright ' . $hiddenclass, 'data-action' => 'moveright')
+        );
     }
 
-    // move
+    // Move.
     if ($hasmanageactivities) {
-        $actions['move'] = new action_link(
+        $actions['move'] = new action_menu_link_primary(
             new moodle_url($baseurl, array('copy' => $mod->id)),
             new pix_icon('t/move', $str->move, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-            null,
-            array('class' => 'editing_move', 'title' => $str->move)
+            $str->move,
+            array('class' => 'editing_move status', 'data-action' => 'move')
         );
     }
 
-    // Update
+    // Update.
     if ($hasmanageactivities) {
-        $actions['update'] = new action_link(
+        $actions['update'] = new action_menu_link_secondary(
             new moodle_url($baseurl, array('update' => $mod->id)),
             new pix_icon('t/edit', $str->update, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-            null,
-            array('class' => 'editing_update', 'title' => $str->update)
+            $str->update,
+            array('class' => 'editing_update', 'data-action' => 'update')
         );
     }
 
     // Duplicate (require both target import caps to be able to duplicate and backup2 support, see modduplicate.php)
-    // note that restoring on front page is never allowed
+    // Note that restoring on front page is never allowed.
     if ($mod->course != SITEID && has_all_capabilities($dupecaps, $coursecontext) &&
             plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2)) {
-        $actions['duplicate'] = new action_link(
+        $actions['duplicate'] = new action_menu_link_secondary(
             new moodle_url($baseurl, array('duplicate' => $mod->id)),
             new pix_icon('t/copy', $str->duplicate, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-            null,
-            array('class' => 'editing_duplicate', 'title' => $str->duplicate)
+            $str->duplicate,
+            array('class' => 'editing_duplicate', 'data-action' => 'duplicate')
         );
     }
 
-    // Delete
+    // Delete.
     if ($hasmanageactivities) {
-        $actions['delete'] = new action_link(
+        $actions['delete'] = new action_menu_link_secondary(
             new moodle_url($baseurl, array('delete' => $mod->id)),
             new pix_icon('t/delete', $str->delete, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-            null,
-            array('class' => 'editing_delete', 'title' => $str->delete)
+            $str->delete,
+            array('class' => 'editing_delete', 'data-action' => 'delete')
         );
     }
 
-    // hideshow
+    // Hide/Show.
     if (has_capability('moodle/course:activityvisibility', $modcontext)) {
         if ($mod->visible) {
-            $actions['hide'] = new action_link(
+            $actions['hide'] = new action_menu_link_primary(
                 new moodle_url($baseurl, array('hide' => $mod->id)),
                 new pix_icon('t/hide', $str->hide, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-                null,
-                array('class' => 'editing_hide', 'title' => $str->hide)
+                $str->hide,
+                array('class' => 'editing_hide', 'data-action' => 'hide')
             );
         } else {
-            $actions['show'] = new action_link(
+            $actions['show'] = new action_menu_link_primary(
                 new moodle_url($baseurl, array('show' => $mod->id)),
                 new pix_icon('t/show', $str->show, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-                null,
-                array('class' => 'editing_show', 'title' => $str->show)
+                $str->show,
+                array('class' => 'editing_show', 'data-action' => 'show')
             );
         }
     }
 
-    // groupmode
+    // Groupmode.
     if ($hasmanageactivities and plugin_supports('mod', $mod->modname, FEATURE_GROUPS, 0)) {
         if ($mod->coursegroupmodeforce) {
             $modgroupmode = $mod->coursegroupmode;
@@ -1909,43 +1913,43 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
             $modgroupmode = $mod->groupmode;
         }
         if ($modgroupmode == SEPARATEGROUPS) {
-            $groupmode = NOGROUPS;
+            $nextgroupmode = VISIBLEGROUPS;
             $grouptitle = $str->groupsseparate;
             $forcedgrouptitle = $str->forcedgroupsseparate;
             $actionname = 'groupsseparate';
             $groupimage = 't/groups';
         } else if ($modgroupmode == VISIBLEGROUPS) {
-            $groupmode = SEPARATEGROUPS;
+            $nextgroupmode = NOGROUPS;
             $grouptitle = $str->groupsvisible;
             $forcedgrouptitle = $str->forcedgroupsvisible;
             $actionname = 'groupsvisible';
             $groupimage = 't/groupv';
         } else {
-            $groupmode = VISIBLEGROUPS;
+            $nextgroupmode = SEPARATEGROUPS;
             $grouptitle = $str->groupsnone;
             $forcedgrouptitle = $str->forcedgroupsnone;
             $actionname = 'groupsnone';
             $groupimage = 't/groupn';
         }
         if (!$mod->coursegroupmodeforce) {
-            $actions[$actionname] = new action_link(
-                new moodle_url($baseurl, array('id' => $mod->id, 'groupmode' => $groupmode)),
+            $actions[$actionname] = new action_menu_link_primary(
+                new moodle_url($baseurl, array('id' => $mod->id, 'groupmode' => $nextgroupmode)),
                 new pix_icon($groupimage, $grouptitle, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-                null,
-                array('class' => 'editing_'. $actionname, 'title' => $grouptitle)
+                $grouptitle,
+                array('class' => 'editing_'. $actionname, 'data-action' => $actionname, 'data-nextgroupmode' => $nextgroupmode)
             );
         } else {
-            $actions[$actionname] = new pix_icon($groupimage, $forcedgrouptitle, 'moodle', array('title' => $forcedgrouptitle, 'class' => 'iconsmall'));
+            $actions[$actionname] = new pix_icon($groupimage, $forcedgrouptitle, 'moodle', array('title' => '', 'class' => 'iconsmall'));
         }
     }
 
-    // Assign
+    // Assign.
     if (has_capability('moodle/role:assign', $modcontext)){
-        $actions['assign'] = new action_link(
+        $actions['assign'] = new action_menu_link_secondary(
             new moodle_url('/admin/roles/assign.php', array('contextid' => $modcontext->id)),
             new pix_icon('t/assignroles', $str->assign, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-            null,
-            array('class' => 'editing_assign', 'title' => $str->assign)
+            $str->assign,
+            array('class' => 'editing_assign', 'data-action' => 'assignroles')
         );
     }
 
index 884b14a..608ba69 100644 (file)
@@ -325,20 +325,54 @@ class core_course_renderer extends plugin_renderer_base {
      *
      * @see course_get_cm_edit_actions()
      *
-     * @param array $actions array of action_link or pix_icon objects
+     * @param action_link[] $actions Array of action_link objects
+     * @param cm_info $mod The module we are displaying actions for.
+     * @param array $displayoptions additional display options:
+     *     ownerselector => A JS/CSS selector that can be used to find an cm node.
+     *         If specified the owning node will be given the class 'action-menu-shown' when the action
+     *         menu is being displayed.
+     *     constraintselector => A JS/CSS selector that can be used to find the parent node for which to constrain
+     *         the action menu to when it is being displayed.
+     *     donotenhance => If set to true the action menu that gets displayed won't be enhanced by JS.
      * @return string
      */
-    public function course_section_cm_edit_actions($actions) {
-        $output = html_writer::start_tag('span', array('class' => 'commands'));
+    public function course_section_cm_edit_actions($actions, cm_info $mod = null, $displayoptions = array()) {
+        global $CFG;
+
+        if (empty($actions)) {
+            return '';
+        }
+
+        if (isset($displayoptions['ownerselector'])) {
+            $ownerselector = $displayoptions['ownerselector'];
+        } else if ($mod) {
+            $ownerselector = '#module-'.$mod->id;
+        } else {
+            debugging('You should upgrade your call to '.__FUNCTION__.' and provide $mod', DEBUG_DEVELOPER);
+            $ownerselector = 'li.activity';
+        }
+
+        if (isset($displayoptions['constraintselector'])) {
+            $constraint = $displayoptions['constraintselector'];
+        } else {
+            $constraint = '.course-content';
+        }
+
+        $menu = new action_menu();
+        $menu->set_owner_selector($ownerselector);
+        $menu->set_contraint($constraint);
+        $menu->set_alignment(action_menu::TL, action_menu::TR);
+        if (isset($CFG->modeditingmenu) && !$CFG->modeditingmenu || !empty($displayoptions['donotenhance'])) {
+            $menu->do_not_enhance();
+        }
         foreach ($actions as $action) {
-            if ($action instanceof renderable) {
-                $output .= $this->output->render($action);
-            } else {
-                $output .= $action;
+            if ($action instanceof action_menu_link) {
+                $action->add_class('cm-edit-action');
             }
+            $menu->add($action);
         }
-        $output .= html_writer::end_tag('span');
-        return $output;
+        $menu->attributes['class'] .= ' section-cm-edit-actions commands';
+        return $this->render($menu);
     }
 
     /**
@@ -751,11 +785,20 @@ class core_course_renderer extends plugin_renderer_base {
             return $output;
         }
         $content = $mod->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
-        $conditionalhidden = $this->is_cm_conditionally_hidden($mod);
-        $accessiblebutdim = !$mod->visible || $conditionalhidden;
+        if ($this->page->user_is_editing()) {
+            // In editing mode, when an item is conditionally hidden from some users
+            // we show it as greyed out.
+            $conditionalhidden = $this->is_cm_conditionally_hidden($mod);
+            $dim = !$mod->visible || $conditionalhidden;
+        } else {
+            // When not in editing mode, we only show item as hidden if it is
+            // actually not available to the user
+            $conditionalhidden = false;
+            $dim = !$mod->uservisible;
+        }
         $textclasses = '';
         $accesstext = '';
-        if ($accessiblebutdim) {
+        if ($dim) {
             $textclasses .= ' dimmed_text';
             if ($conditionalhidden) {
                 $textclasses .= ' conditionalhidden';
@@ -820,6 +863,28 @@ class core_course_renderer extends plugin_renderer_base {
         return '';
     }
 
+    /**
+     * Renders HTML to display one course module for display within a section.
+     *
+     * This function calls:
+     * {@link core_course_renderer::course_section_cm()}
+     *
+     * @param stdClass $course
+     * @param completion_info $completioninfo
+     * @param cm_info $mod
+     * @param int|null $sectionreturn
+     * @param array $displayoptions
+     * @return String
+     */
+    public function course_section_cm_list_item($course, &$completioninfo, cm_info $mod, $sectionreturn, $displayoptions = array()) {
+        $output = '';
+        if ($modulehtml = $this->course_section_cm($course, $completioninfo, $mod, $sectionreturn, $displayoptions)) {
+            $modclasses = 'activity ' . $mod->modname . ' modtype_' . $mod->modname . ' ' . $mod->get_extra_classes();
+            $output .= html_writer::tag('li', $modulehtml, array('class' => $modclasses, 'id' => 'module-' . $mod->id));
+        }
+        return $output;
+    }
+
     /**
      * Renders HTML to display one course module in a course section
      *
@@ -895,7 +960,7 @@ class core_course_renderer extends plugin_renderer_base {
 
         if ($this->page->user_is_editing()) {
             $editactions = course_get_cm_edit_actions($mod, $mod->indent, $sectionreturn);
-            $output .= ' '. $this->course_section_cm_edit_actions($editactions);
+            $output .= ' '. $this->course_section_cm_edit_actions($editactions, $mod, $displayoptions);
             $output .= $mod->get_after_edit_icons();
         }
 
@@ -956,40 +1021,35 @@ class core_course_renderer extends plugin_renderer_base {
                     continue;
                 }
 
-                if ($modulehtml = $this->course_section_cm($course,
+                if ($modulehtml = $this->course_section_cm_list_item($course,
                         $completioninfo, $mod, $sectionreturn, $displayoptions)) {
                     $moduleshtml[$modnumber] = $modulehtml;
                 }
             }
         }
 
+        $sectionoutput = '';
         if (!empty($moduleshtml) || $ismoving) {
-
-            $output .= html_writer::start_tag('ul', array('class' => 'section img-text'));
-
             foreach ($moduleshtml as $modnumber => $modulehtml) {
                 if ($ismoving) {
                     $movingurl = new moodle_url('/course/mod.php', array('moveto' => $modnumber, 'sesskey' => sesskey()));
-                    $output .= html_writer::tag('li', html_writer::link($movingurl, $this->output->render($movingpix)),
+                    $sectionoutput .= html_writer::tag('li', html_writer::link($movingurl, $this->output->render($movingpix)),
                             array('class' => 'movehere', 'title' => $strmovefull));
                 }
 
-                $mod = $modinfo->cms[$modnumber];
-                $modclasses = 'activity '. $mod->modname. ' modtype_'.$mod->modname. ' '. $mod->get_extra_classes();
-                $output .= html_writer::start_tag('li', array('class' => $modclasses, 'id' => 'module-'. $mod->id));
-                $output .= $modulehtml;
-                $output .= html_writer::end_tag('li');
+                $sectionoutput .= $modulehtml;
             }
 
             if ($ismoving) {
                 $movingurl = new moodle_url('/course/mod.php', array('movetosection' => $section->id, 'sesskey' => sesskey()));
-                $output .= html_writer::tag('li', html_writer::link($movingurl, $this->output->render($movingpix)),
+                $sectionoutput .= html_writer::tag('li', html_writer::link($movingurl, $this->output->render($movingpix)),
                         array('class' => 'movehere', 'title' => $strmovefull));
             }
-
-            $output .= html_writer::end_tag('ul'); // .section
         }
 
+        // Always output the section module list.
+        $output .= html_writer::tag('ul', $sectionoutput, array('class' => 'section img-text'));
+
         return $output;
     }
 
index 7cd0079..2937c17 100644 (file)
@@ -26,7 +26,7 @@ Feature: Indent items on the course page
     When I indent right "Test glossary name" activity
     Then "#section-1 li.glossary div.mod-indent-1" "css_element" should exists
     And I indent right "Test glossary name" activity
-    And "//li[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should exists
+    And "//li[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[not(contains(concat(' ', @class, ' '), ' hidden '))]/descendant::span[normalize-space(.)='Move left']" "xpath_element" should exists
     And "#section-1 li.glossary div.mod-indent-2" "css_element" should exists
     And I reload the page
     And "#section-1 li.glossary div.mod-indent-2" "css_element" should exists
@@ -34,4 +34,4 @@ Feature: Indent items on the course page
     And I indent left "Test glossary name" activity
     And "#section-1 li.glossary div.mod-indent-2" "css_element" should not exists
     And "#section-1 li.glossary div.mod-indent-1" "css_element" should not exists
-    And "//li[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should not exists
+    And "//li[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[not(contains(concat(' ', @class, ' '), ' hidden '))]/descendant::span[normalize-space(.)='Move left']" "xpath_element" should not exists
index 49145f2..35e9164 100644 (file)
@@ -485,8 +485,10 @@ class behat_course extends behat_base {
         }
 
         // Adding chr(10) to save changes.
+        $activity = $this->escape($activityname);
         return array(
-            new Given('I click on "' . get_string('edittitle') . '" "link" in the "' . $this->escape($activityname) .'" activity'),
+            new Given('I click on "' . get_string('actions', 'moodle') . '" "link" in the "' . $activity . '" activity'),
+            new Given('I click on "' . get_string('edittitle') . '" "link" in the "' . $activity .'" activity'),
             new Given('I fill in "title" with "' . $this->escape($newactivityname) . chr(10) . '"'),
             new Given('I wait "2" seconds')
         );
@@ -500,9 +502,12 @@ class behat_course extends behat_base {
      */
     public function i_indent_right_activity($activityname) {
 
-        $steps = array(
-            new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $this->escape($activityname) . '" activity')
-        );
+        $steps = array();
+        $activity = $this->escape($activityname);
+        if ($this->running_javascript()) {
+            $steps[] = new Given('I click on "' . get_string('actions', 'moodle') . '" "link" in the "' . $activity . '" activity');
+        }
+        $steps[] = new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $activity . '" activity');
 
         if ($this->running_javascript()) {
             $steps[] = new Given('I wait "2" seconds');
@@ -519,9 +524,12 @@ class behat_course extends behat_base {
      */
     public function i_indent_left_activity($activityname) {
 
-        $steps = array(
-            new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $this->escape($activityname) . '" activity')
-        );
+        $steps = array();
+        $activity = $this->escape($activityname);
+        if ($this->running_javascript()) {
+            $steps[] = new Given('I click on "' . get_string('actions', 'moodle') . '" "link" in the "' . $activity . '" activity');
+        }
+        $steps[] = new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $activity . '" activity');
 
         if ($this->running_javascript()) {
             $steps[] = new Given('I wait "2" seconds');
@@ -572,11 +580,15 @@ class behat_course extends behat_base {
      * @param string $activityname
      */
     public function i_duplicate_activity($activityname) {
-        return array(
-            new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $this->escape($activityname) . '" activity'),
-            new Given('I press "' . get_string('continue') .'"'),
-            new Given('I press "' . get_string('duplicatecontcourse') .'"')
-        );
+        $steps = array();
+        $activity = $this->escape($activityname);
+        if ($this->running_javascript()) {
+            $steps[] = new Given('I click on "' . get_string('actions', 'moodle') . '" "link" in the "' . $activity . '" activity');
+        }
+        $steps[] = new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activity . '" activity');
+        $steps[] = new Given('I press "' . get_string('continue') .'"');
+        $steps[] = new Given('I press "' . get_string('duplicatecontcourse') .'"');
+        return $steps;
     }
 
     /**
@@ -587,13 +599,17 @@ class behat_course extends behat_base {
      * @param TableNode $data
      */
     public function i_duplicate_activity_editing_the_new_copy_with($activityname, TableNode $data) {
-        return array(
-            new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $this->escape($activityname) . '" activity'),
-            new Given('I press "' . get_string('continue') .'"'),
-            new Given('I press "' . get_string('duplicatecontedit') . '"'),
-            new Given('I fill the moodle form with:', $data),
-            new Given('I press "' . get_string('savechangesandreturntocourse') . '"')
-        );
+        $steps = array();
+        $activity = $this->escape($activityname);
+        if ($this->running_javascript()) {
+            $steps[] = new Given('I click on "' . get_string('actions', 'moodle') . '" "link" in the "' . $activity . '" activity');
+        }
+        $steps[] = new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activity . '" activity');
+        $steps[] = new Given('I press "' . get_string('continue') .'"');
+        $steps[] = new Given('I press "' . get_string('duplicatecontedit') . '"');
+        $steps[] = new Given('I fill the moodle form with:', $data);
+        $steps[] = new Given('I press "' . get_string('savechangesandreturntocourse') . '"');
+        return $steps;
     }
 
     /**
index 973c287..8b2ef82 100644 (file)
@@ -40,6 +40,7 @@ Feature: Course activity controls works as expected
     And I should see "Turn editing on"
     And "Turn editing on" "button" should exists
     And I turn editing mode on
+    And I click on "Actions" "link" in the ".block_recent_activity" "css_element"
     And I click on "Delete Recent activity block" "link"
     And I press "Yes"
     And "#section-2" "css_element" <should_see_other_sections> exists
@@ -54,11 +55,13 @@ Feature: Course activity controls works as expected
     And "#section-2" "css_element" <should_see_other_sections> exists
     And I indent left "Test forum name 1" activity
     And "#section-2" "css_element" <should_see_other_sections> exists
+    And I click on "Actions" "link" in the "Test forum name 1" activity
     And I click on "Update" "link" in the "Test forum name 1" activity
     And I should see "Updating Forum"
     And I should see "Display description on course page"
     And I press "Save and return to course"
     And "#section-2" "css_element" <should_see_other_sections> exists
+    And I click on "Actions" "link" in the "Test forum name 1" activity
     And I click on "Hide" "link" in the "Test forum name 1" activity
     And "#section-2" "css_element" <should_see_other_sections> exists
     And I duplicate "Test forum name 2" activity editing the new copy with:
@@ -111,6 +114,7 @@ Feature: Course activity controls works as expected
     And I should see "Turn editing on"
     And "Turn editing on" "button" should exists
     And I turn editing mode on
+    And I click on "Actions" "link" in the ".block_recent_activity" "css_element"
     And I click on "Delete Recent activity block" "link"
     And I press "Yes"
     And "#section-2" "css_element" <should_see_other_sections> exists
@@ -125,11 +129,13 @@ Feature: Course activity controls works as expected
     And "#section-2" "css_element" <should_see_other_sections> exists
     And I indent left "Test forum name 1" activity
     And "#section-2" "css_element" <should_see_other_sections> exists
+    And I click on "Actions" "link" in the "Test forum name 1" activity
     And I click on "Update" "link" in the "Test forum name 1" activity
     And I should see "Updating Forum"
     And I should see "Display description on course page"
     And I press "Save and return to course"
     And "#section-2" "css_element" <should_see_other_sections> exists
+    And I click on "Actions" "link" in the "Test forum name 1" activity
     And I click on "Hide" "link" in the "Test forum name 1" activity
     And "#section-2" "css_element" <should_see_other_sections> exists
     And I delete "Test forum name 1" activity
index 50d191c..55db3b2 100644 (file)
@@ -28,7 +28,7 @@ defined('MOODLE_INTERNAL') || die();
 global $CFG;
 require_once($CFG->dirroot.'/course/lib.php');
 
-class courselib_testcase extends advanced_testcase {
+class core_course_courselib_testcase extends advanced_testcase {
 
     /**
      * Set forum specific test values for calling create_module().
index d376772..0b23a4e 100644 (file)
@@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die();
 global $CFG;
 require_once($CFG->dirroot.'/course/lib.php');
 
-class courserequest_testcase extends advanced_testcase {
+class core_course_courserequest_testcase extends advanced_testcase {
 
     public function test_create_request() {
         global $DB, $USER;
index ab0a67e..8693632 100644 (file)
@@ -37,7 +37,7 @@ require_once($CFG->dirroot . '/webservice/tests/helpers.php');
  * @copyright  2012 Jerome Mouneyrac
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class core_course_external_testcase extends externallib_advanced_testcase {
+class core_course_externallib_testcase extends externallib_advanced_testcase {
 
     /**
      * Tests set up
index 6872ece..0145f0f 100644 (file)
@@ -2,7 +2,7 @@ YUI.add('moodle-course-dragdrop', function(Y) {
 
     var CSS = {
         ACTIVITY : 'activity',
-        COMMANDSPAN : 'span.commands',
+        COMMANDSPAN : '.commands',
         CONTENT : 'content',
         COURSECONTENT : 'course-content',
         EDITINGMOVE : 'editing_move',
index 0955095..2b160b8 100644 (file)
@@ -1,43 +1,47 @@
 YUI.add('moodle-course-toolboxes', function(Y) {
-    WAITICON = {'pix':"i/loading_small",'component':'moodle'};
-    // The CSS selectors we use
+
+    // The following properties contain common strings.
+    // We separate them out here because when this JS is minified the content is less as
+    // Variables get compacted to single/double characters and the full length of the string
+    // exists only once.
+
+    // The CSS classes we use.
     var CSS = {
-        ACTIVITYLI : 'li.activity',
-        COMMANDSPAN : 'span.commands',
-        SPINNERCOMMANDSPAN : 'span.commands',
-        CONTENTAFTERLINK : 'div.contentafterlink',
-        DELETE : 'a.editing_delete',
+        ACTIVITYINSTANCE : 'activityinstance',
+        AVAILABILITYINFODIV : 'div.availabilityinfo',
+        CONDITIONALHIDDEN : 'conditionalhidden',
         DIMCLASS : 'dimmed',
         DIMMEDTEXT : 'dimmed_text',
-        EDITTITLE : 'a.editing_title',
-        EDITTITLECLASS : 'edittitle',
-        GENERICICONCLASS : 'iconsmall',
-        GROUPSNONE : 'a.editing_groupsnone',
-        GROUPSSEPARATE : 'a.editing_groupsseparate',
-        GROUPSVISIBLE : 'a.editing_groupsvisible',
-        HIDE : 'a.editing_hide',
-        HIGHLIGHT : 'a.editing_highlight',
-        INSTANCENAME : 'span.instancename',
-        LIGHTBOX : 'lightbox',
+        EDITINSTRUCTIONS : 'editinstructions',
+        HIDE : 'hide',
         MODINDENTCOUNT : 'mod-indent-',
-        MODINDENTDIV : 'div.mod-indent',
         MODINDENTHUGE : 'mod-indent-huge',
         MODULEIDPREFIX : 'module-',
-        MOVELEFT : 'a.editing_moveleft',
-        MOVELEFTCLASS : 'editing_moveleft',
-        MOVERIGHT : 'a.editing_moveright',
-        PAGECONTENT : 'div#page-content',
-        RIGHTSIDE : '.right',
         SECTIONHIDDENCLASS : 'hidden',
         SECTIONIDPREFIX : 'section-',
+        SHOW : 'editing_show',
+        TITLEEDITOR : 'titleeditor'
+    },
+    // The CSS selectors we use.
+    SELECTOR = {
+        ACTIONLINKTEXT : '.actionlinktext',
+        ACTIVITYACTION : 'a.cm-edit-action[data-action]',
+        ACTIVITYFORM : 'form.'+CSS.ACTIVITYINSTANCE,
+        ACTIVITYICON : 'img.activityicon',
+        ACTIVITYLI : 'li.activity',
+        ACTIVITYTITLE : 'input[name=title]',
+        COMMANDSPAN : '.commands',
+        CONTENTAFTERLINK : 'div.contentafterlink',
+        HIDE : 'a.editing_hide',
+        HIGHLIGHT : 'a.editing_highlight',
+        INSTANCENAME : 'span.instancename',
+        MODINDENTDIV : 'div.mod-indent',
+        PAGECONTENT : 'div#page-content',
         SECTIONLI : 'li.section',
-        SHOW : 'a.editing_show',
-        SHOWHIDE : 'a.editing_showhide',
-        CONDITIONALHIDDEN : 'conditionalhidden',
-        AVAILABILITYINFODIV : 'div.availabilityinfo',
-        SHOWCLASS : 'editing_show',
-        HIDECLASS : 'hide'
-    };
+        SHOW : 'a.'+CSS.SHOW,
+        SHOWHIDE : 'a.editing_showhide'
+    },
+    BODY = Y.one(document.body);
 
     /**
      * The toolbox classes
@@ -51,57 +55,6 @@ YUI.add('moodle-course-toolboxes', function(Y) {
     }
 
     Y.extend(TOOLBOX, Y.Base, {
-        /**
-         * Toggle the visibility and availability for the specified
-         * resource show/hide button
-         */
-        toggle_hide_resource_ui : function(button) {
-            var element = button.ancestor(CSS.ACTIVITYLI);
-            var hideicon = button.one('img');
-
-            var dimarea;
-            var toggle_class;
-            if (this.get_instance_name(element) == null) {
-                toggle_class = CSS.DIMMEDTEXT;
-                dimarea = element.all(CSS.MODINDENTDIV + ' > div').item(1);
-            } else {
-                toggle_class = CSS.DIMCLASS;
-                dimarea = element.one('a');
-            }
-
-            var status = '';
-            var value;
-            if (button.hasClass(CSS.SHOWCLASS)) {
-                status = 'hide';
-                value = 1;
-            } else {
-                status = 'show';
-                value = 0;
-            }
-            // Update button info.
-            var newstring = M.util.get_string(status, 'moodle');
-            hideicon.setAttrs({
-                'alt' : newstring,
-                'src'   : M.util.image_url('t/' + status)
-            });
-            button.set('title', newstring);
-            button.set('className', 'editing_'+status);
-
-            // If activity is conditionally hidden, then don't toggle.
-            if (!dimarea.hasClass(CSS.CONDITIONALHIDDEN)) {
-                // Change the UI.
-                dimarea.toggleClass(toggle_class);
-                // We need to toggle dimming on the description too.
-                element.all(CSS.CONTENTAFTERLINK).toggleClass(CSS.DIMMEDTEXT);
-            }
-            // Toggle availablity info for conditional activities.
-            var availabilityinfo = element.one(CSS.AVAILABILITYINFODIV);
-
-            if (availabilityinfo) {
-                availabilityinfo.toggleClass(CSS.HIDECLASS);
-            }
-            return value;
-        },
         /**
          * Send a request using the REST API
          *
@@ -180,8 +133,8 @@ YUI.add('moodle-course-toolboxes', function(Y) {
          * @return string|null Instance name
          */
         get_instance_name : function(target) {
-            if (target.one(CSS.INSTANCENAME)) {
-                return target.one(CSS.INSTANCENAME).get('firstChild').get('data');
+            if (target.one(SELECTOR.INSTANCENAME)) {
+                return target.one(SELECTOR.INSTANCENAME).get('firstChild').get('data');
             }
             return null;
         },
@@ -221,81 +174,158 @@ YUI.add('moodle-course-toolboxes', function(Y) {
     }
     );
 
-
+    /**
+     * Resource and activity toolbox class.
+     *
+     * This class is responsible for managing AJAX interactions with activities and resources
+     * when viewing a course in editing mode.
+     *
+     * @namespace M.course.toolbox
+     * @class ResourceToolbox
+     * @constructor
+     */
     var RESOURCETOOLBOX = function() {
         RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
     }
 
     Y.extend(RESOURCETOOLBOX, TOOLBOX, {
-        // Variables
+        /**
+         * No groups are being used.
+         * @static
+         * @const GROUPS_NONE
+         * @type Number
+         */
         GROUPS_NONE     : 0,
+        /**
+         * Separate groups are being used.
+         * @static
+         * @const GROUPS_SEPARATE
+         * @type Number
+         */
         GROUPS_SEPARATE : 1,
+        /**
+         * Visible groups are being used.
+         * @static
+         * @const GROUPS_VISIBLE
+         * @type Number
+         */
         GROUPS_VISIBLE  : 2,
 
+        /**
+         * Events that were added when editing a title.
+         * These should all be detached when editing is complete.
+         * @property edittitleevents
+         * @type {Event[]}
+         * @protected
+         */
+        edittitleevents : [],
+
         /**
          * Initialize the resource toolbox
          *
-         * Updates all span.commands with relevant handlers and other required changes
+         * For each activity the commands are updated and a reference to the activity is attached.
+         * This way it doesn't matter where the commands are going to called from they have a reference to the
+         * activity that they relate to.
+         * This is essential as some of the actions are displayed in an actionmenu which removes them from the
+         * page flow.
+         *
+         * This function also creates a single event delegate to manage all AJAX actions for all activities on
+         * the page.
+         *
+         * @method initializer
          */
         initializer : function(config) {
-            this.setup_for_resource();
             M.course.coursebase.register_module(this);
-
-            var prefix = CSS.ACTIVITYLI + ' ' + CSS.COMMANDSPAN + ' ';
-            Y.delegate('click', this.edit_resource_title, CSS.PAGECONTENT, prefix + CSS.EDITTITLE, this);
-            Y.delegate('click', this.move_left, CSS.PAGECONTENT, prefix + CSS.MOVELEFT, this);
-            Y.delegate('click', this.move_right, CSS.PAGECONTENT, prefix + CSS.MOVERIGHT, this);
-            Y.delegate('click', this.delete_resource, CSS.PAGECONTENT, prefix + CSS.DELETE, this);
-            Y.delegate('click', this.toggle_hide_resource, CSS.PAGECONTENT, prefix + CSS.HIDE, this);
-            Y.delegate('click', this.toggle_hide_resource, CSS.PAGECONTENT, prefix + CSS.SHOW, this);
-            Y.delegate('click', this.toggle_groupmode, CSS.PAGECONTENT, prefix + CSS.GROUPSNONE, this);
-            Y.delegate('click', this.toggle_groupmode, CSS.PAGECONTENT, prefix + CSS.GROUPSSEPARATE, this);
-            Y.delegate('click', this.toggle_groupmode, CSS.PAGECONTENT, prefix + CSS.GROUPSVISIBLE, this);
+            Y.all(SELECTOR.ACTIVITYLI).each(function(activity){
+                activity.setData('toolbox', this);
+                activity.all(SELECTOR.COMMANDSPAN+ ' ' + SELECTOR.ACTIVITYACTION).each(function(){
+                    this.setData('activity', activity);
+                });
+            }, this);
+            Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this);
         },
 
         /**
-         * Update any span.commands within the scope of the specified
-         * selector with AJAX equivelants
+         * Handles the delegation event. When this is fired someone has triggered an action.
          *
-         * @param baseselector The selector to limit scope to
-         * @return void
+         * Note not all actions will result in an AJAX enhancement.
+         *
+         * @protected
+         * @method handle_data_action
+         * @param {EventFacade} ev The event that was triggered.
+         * @returns {boolean}
          */
-        setup_for_resource : function(baseselector) {
-            if (!baseselector) {
-                var baseselector = CSS.PAGECONTENT + ' ' + CSS.ACTIVITYLI;
+        handle_data_action : function(ev) {
+            // We need to get the anchor element that triggered this event.
+            var node = ev.target;
+            if (!node.test('a')) {
+                node = node.ancestor(SELECTOR.ACTIVITYACTION);
             }
 
-            Y.all(baseselector).each(this._setup_for_resource, this);
-        },
-        _setup_for_resource : function(toolboxtarget) {
-            toolboxtarget = Y.one(toolboxtarget);
-
-            // Set groupmode attribute for use by this.toggle_groupmode()
-            var groups;
-            groups = toolboxtarget.all(CSS.COMMANDSPAN + ' ' + CSS.GROUPSNONE);
-            groups.setAttribute('groupmode', this.GROUPS_NONE);
-
-            groups = toolboxtarget.all(CSS.COMMANDSPAN + ' ' + CSS.GROUPSSEPARATE);
-            groups.setAttribute('groupmode', this.GROUPS_SEPARATE);
+            // From the anchor we can get both the activity (added during initialisation) and the action being
+            // performed (added by the UI as a data attribute).
+            var action = node.getData('action'),
+                activity = node.getData('activity');
+            if (!node.test('a') || !action || !activity) {
+                // It wasn't a valid action node.
+                return;
+            }
 
-            groups = toolboxtarget.all(CSS.COMMANDSPAN + ' ' + CSS.GROUPSVISIBLE);
-            groups.setAttribute('groupmode', this.GROUPS_VISIBLE);
-        },
-        move_left : function(e) {
-            this.move_leftright(e, -1);
-        },
-        move_right : function(e) {
-            this.move_leftright(e, 1);
+            // Switch based upon the action and do the desired thing.
+            switch (action) {
+                case 'edittitle' :
+                    // The user wishes to edit the title of the event.
+                    this.edit_title(ev, node, activity, action);
+                    break;
+                case 'moveleft' :
+                case 'moveright' :
+                    // The user changing the indent of the activity.
+                    this.change_indent(ev, node, activity, action);
+                    break;
+                case 'delete' :
+                    // The user is deleting the activity.
+                    this.delete_with_confirmation(ev, node, activity, action);
+                    break;
+                case 'hide' :
+                case 'show' :
+                    // The user is changing the visibility of the activity.
+                    this.change_visibility(ev, node, activity, action);
+                    break;
+                case 'groupsseparate' :
+                case 'groupsvisible' :
+                case 'groupsnone' :
+                    // The user is changing the group mode.
+                    callback = 'change_groupmode';
+                    this.change_groupmode(ev, node, activity, action);
+                    break;
+                case 'move' :
+                case 'update' :
+                case 'duplicate' :
+                case 'assignroles' :
+                default:
+                    // Nothing to do here!
+                    break;
+            }
         },
-        move_leftright : function(e, direction) {
+
+        /**
+         * Change the indent of the activity or resource.
+         *
+         * @protected
+         * @method change_indent
+         * @param {EventFacade} ev The event that was fired.
+         * @param {Node} button The button that triggered this action.
+         * @param {Node} activity The activity node that this action will be performed on.
+         * @param {String} action The action that has been requested. Will be 'moveleft' or 'moveright'.
+         */
+        change_indent : function(ev, button, activity, action) {
             // Prevent the default button action
-            e.preventDefault();
+            ev.preventDefault();
 
-            // Get the element we're working on
-            var element = e.target.ancestor(CSS.ACTIVITYLI);
+            var direction = (action === 'moveleft') ? -1 : 1;
 
             // And we need to determine the current and new indent level
-            var indentdiv = element.one(CSS.MODINDENTDIV);
+            var indentdiv = activity.one(SELECTOR.MODINDENTDIV);
             var indent = indentdiv.getAttribute('class').match(/mod-indent-(\d{1,})/);
 
             if (indent) {
@@ -313,16 +343,25 @@ YUI.add('moodle-course-toolboxes', function(Y) {
                 'class' : 'resource',
                 'field' : 'indent',
                 'value' : newindent,
-                'id'    : this.get_element_id(element)
+                'id'    : this.get_element_id(activity)
             };
-            var spinner = M.util.add_spinner(Y, element.one(CSS.SPINNERCOMMANDSPAN));
+            var commands = activity.one(SELECTOR.COMMANDSPAN);
+            var spinner = M.util.add_spinner(Y, commands).setStyles({
+                position: 'absolute',
+                top: 0
+            });
+            if (BODY.hasClass('dir-ltr')) {
+                spinner.setStyle('left', '100%');
+            }  else {
+                spinner.setStyle('right', '100%');
+            }
             this.send_request(data, spinner);
 
-            // Handle removal/addition of the moveleft button
+            // Handle removal/addition of the moveleft button.
             if (newindent == 0) {
-                element.one(CSS.MOVELEFT).remove();
+                button.addClass('hidden');
             } else if (newindent == 1 && oldindent == 0) {
-                this.add_moveleft(element);
+                button.ancestor('.menu').one('[data-action=moveleft]').removeClass('hidden');
             }
 
             // Handle massive indentation to match non-ajax display
@@ -333,12 +372,23 @@ YUI.add('moodle-course-toolboxes', function(Y) {
                 indentdiv.removeClass(CSS.MODINDENTHUGE);
             }
         },
-        delete_resource : function(e) {
+
+        /**
+         * Deletes the given activity or resource after confirmation.
+         *
+         * @protected
+         * @method delete_with_confirmation
+         * @param {EventFacade} ev The event that was fired.
+         * @param {Node} button The button that triggered this action.
+         * @param {Node} activity The activity node that this action will be performed on.
+         * @return Boolean
+         */
+        delete_with_confirmation : function(ev, button, activity) {
             // Prevent the default button action
-            e.preventDefault();
+            ev.preventDefault();
 
             // Get the element we're working on
-            var element   = e.target.ancestor(CSS.ACTIVITYLI);
+            var element   = activity
 
             // Create confirm string (different if element has or does not have name)
             var confirmstring = '';
@@ -365,23 +415,35 @@ YUI.add('moodle-course-toolboxes', function(Y) {
                 'id'    : this.get_element_id(element)
             };
             this.send_request(data);
+            if (M.core.actionmenu && M.core.actionmenu.instance) {
+                M.core.actionmenu.instance.hideMenu();
+            }
         },
-        toggle_hide_resource : function(e) {
+
+        /**
+         * Changes the visibility of this activity or resource.
+         *
+         * @protected
+         * @method change_visibility
+         * @param {EventFacade} ev The event that was fired.
+         * @param {Node} button The button that triggered this action.
+         * @param {Node} activity The activity node that this action will be performed on.
+         * @param {String} action The action that has been requested.
+         * @return Boolean
+         */
+        change_visibility : function(ev, button, activity, action) {
             // Prevent the default button action
-            e.preventDefault();
+            ev.preventDefault();
 
             // Return early if the current section is hidden
-            var section = e.target.ancestor(M.course.format.get_section_selector(Y));
+            var section = activity.ancestor(M.course.format.get_section_selector(Y));
             if (section && section.hasClass(CSS.SECTIONHIDDENCLASS)) {
                 return;
             }
 
             // Get the element we're working on
-            var element = e.target.ancestor(CSS.ACTIVITYLI);
-
-            var button = e.target.ancestor('a', true);
-
-            var value = this.toggle_hide_resource_ui(button);
+            var element = activity;
+            var value = this.handle_resource_dim(button, activity, action);
 
             // Send the request
             var data = {
@@ -390,238 +452,276 @@ YUI.add('moodle-course-toolboxes', function(Y) {
                 'value' : value,
                 'id'    : this.get_element_id(element)
             };
-            var spinner = M.util.add_spinner(Y, element.one(CSS.SPINNERCOMMANDSPAN));
+            var spinner = M.util.add_spinner(Y, element.one(SELECTOR.COMMANDSPAN));
             this.send_request(data, spinner);
             return false; // Need to return false to stop the delegate for the new state firing
         },
-        toggle_groupmode : function(e) {
-            // Prevent the default button action
-            e.preventDefault();
 
-            // Get the element we're working on
-            var element = e.target.ancestor(CSS.ACTIVITYLI);
+        /**
+         * Handles the UI aspect of dimming the activity or resource.
+         *
+         * @protected
+         * @method handle_resource_dim
+         * @param {Node} button The button that triggered the action.
+         * @param {Node} activity The activity node that this action will be performed on.
+         * @param {String} status Whether the activity was shown or hidden.
+         * @returns {number} 1 if we were changing to visible, 0 if we were hiding.
+         */
+        handle_resource_dim : function(button, activity, status) {
+            var toggleclass = CSS.DIMCLASS,
+                dimarea = activity.one('a'),
+                availabilityinfo = activity.one(CSS.AVAILABILITYINFODIV),
+                newstatus = (status === 'hide') ? 'show' : 'hide',
+                newstring = M.util.get_string(newstatus, 'moodle');
 
-            var button = e.target.ancestor('a', true);
-            var icon = button.one('img');
+            // Update button info.
+            button.one('img').setAttrs({
+                'alt' : newstring,
+                'src'   : M.util.image_url('t/' + newstatus)
+            });
+            button.set('title', newstring);
+            button.replaceClass('editing_'+status, 'editing_'+newstatus)
+            button.setData('action', newstatus);
+
+            // If activity is conditionally hidden, then don't toggle.
+            if (this.get_instance_name(activity) == null) {
+                toggleclass = CSS.DIMMEDTEXT;
+                dimarea = activity.all(SELECTOR.MODINDENTDIV + ' > div').item(1);
+            }
+            if (!dimarea.hasClass(CSS.CONDITIONALHIDDEN)) {
+                // Change the UI.
+                dimarea.toggleClass(toggleclass);
+                // We need to toggle dimming on the description too.
+                activity.all(SELECTOR.CONTENTAFTERLINK).toggleClass(CSS.DIMMEDTEXT);
+            }
+            // Toggle availablity info for conditional activities.
+            if (availabilityinfo) {
+                availabilityinfo.toggleClass(CSS.HIDE);
+            }
+            return (status === 'hide') ? 0 : 1;
+        },
+
+        /**
+         * Changes the groupmode of the activity to the next groupmode in the sequence.
+         *
+         * @protected
+         * @method change_groupmode
+         * @param {EventFacade} ev The event that was fired.
+         * @param {Node} button The button that triggered this action.
+         * @param {Node} activity The activity node that this action will be performed on.
+         * @param {String} action The action that has been requested.
+         * @return Boolean
+         */
+        change_groupmode : function(ev, button, activity, action) {
+            // Prevent the default button action.
+            ev.preventDefault();
 
             // Current Mode
-            var groupmode = button.getAttribute('groupmode');
-            groupmode++;
-            if (groupmode > 2) {
-                groupmode = 0;
-            }
-            button.setAttribute('groupmode', groupmode);
-
-            var newtitle = '';
-            var iconsrc = '';
-            switch (groupmode) {
-                case this.GROUPS_NONE:
-                    newtitle = 'groupsnone';
-                    iconsrc = M.util.image_url('t/groupn');
-                    break;
-                case this.GROUPS_SEPARATE:
-                    newtitle = 'groupsseparate';
-                    iconsrc = M.util.image_url('t/groups');
-                    break;
-                case this.GROUPS_VISIBLE:
-                    newtitle = 'groupsvisible';
-                    iconsrc = M.util.image_url('t/groupv');
-                    break;
+            var groupmode = parseInt(button.getData('nextgroupmode'), 10),
+                newtitle = '',
+                iconsrc = '',
+                newtitlestr,
+                data,
+                spinner,
+                nextgroupmode = groupmode + 1;
+
+            if (nextgroupmode > 2) {
+                nextgroupmode = 0;
             }
-            newtitle = M.util.get_string('clicktochangeinbrackets', 'moodle',
-                    M.util.get_string(newtitle, 'moodle'));
+
+            if (groupmode === this.GROUPS_NONE) {
+                newtitle = 'groupsnone';
+                iconsrc = M.util.image_url('t/groupn', 'moodle');
+            } else if (groupmode === this.GROUPS_SEPARATE) {
+                newtitle = 'groupsseparate';
+                iconsrc = M.util.image_url('t/groups', 'moodle');
+            } else if (groupmode === this.GROUPS_VISIBLE) {
+                newtitle = 'groupsvisible';
+                iconsrc = M.util.image_url('t/groupv', 'moodle');
+            }
+            newtitlestr = M.util.get_string(newtitle, 'moodle'),
+            newtitlestr = M.util.get_string('clicktochangeinbrackets', 'moodle', newtitlestr);
 
             // Change the UI
-            icon.setAttrs({
-                'alt' : newtitle,
+            button.one('img').setAttrs({
+                'alt' : newtitlestr,
                 'src' : iconsrc
             });
-            button.setAttribute('title', newtitle);
+            button.setAttribute('title', newtitlestr).setData('action', newtitle).setData('nextgroupmode', nextgroupmode);
 
             // And send the request
-            var data = {
+            data = {
                 'class' : 'resource',
                 'field' : 'groupmode',
                 'value' : groupmode,
-                'id'    : this.get_element_id(element)
+                'id'    : this.get_element_id(activity)
             };
-            var spinner = M.util.add_spinner(Y, element.one(CSS.SPINNERCOMMANDSPAN));
+
+            spinner = M.util.add_spinner(Y, activity.one(SELECTOR.COMMANDSPAN));
             this.send_request(data, spinner);
             return false; // Need to return false to stop the delegate for the new state firing
         },
-        /**
-         * Add the moveleft button
-         * This is required after moving left from an initial position of 0
-         *
-         * @param target The encapsulating <li> element
-         */
-        add_moveleft : function(target) {
-            var left_string = M.util.get_string('moveleft', 'moodle');
-            var moveimage = 't/left'; // ltr mode
-            if ( Y.one(document.body).hasClass('dir-rtl') ) {
-                moveimage = 't/right';
-            } else {
-                moveimage = 't/left';
-            }
-            var newicon = Y.Node.create('<img />')
-                .addClass(CSS.GENERICICONCLASS)
-                .setAttrs({
-                    'src'   : M.util.image_url(moveimage, 'moodle'),
-                    'alt'   : left_string
-                });
-            var moveright = target.one(CSS.MOVERIGHT);
-            var newlink = moveright.getAttribute('href').replace('indent=1', 'indent=-1');
-            var anchor = new Y.Node.create('<a />')
-                .setStyle('cursor', 'pointer')
-                .addClass(CSS.MOVELEFTCLASS)
-                .setAttribute('href', newlink)
-                .setAttribute('title', left_string);
-            anchor.appendChild(newicon);
-            moveright.insert(anchor, 'before');
-        },
+
         /**
          * Edit the title for the resource
+         *
+         * @protected
+         * @method edit_title
+         * @param {EventFacade} ev The event that was fired.
+         * @param {Node} button The button that triggered this action.
+         * @param {Node} activity The activity node that this action will be performed on.
+         * @param {String} action The action that has been requested.
+         * @return Boolean
          */
-        edit_resource_title : function(e) {
+        edit_title : function(ev, button, activity) {
             // Get the element we're working on
-            var element = e.target.ancestor(CSS.ACTIVITYLI);
-            var elementdiv = element.one('div');
-            var instancename  = element.one(CSS.INSTANCENAME);
-            var currenttitle = instancename.get('firstChild');
-            var oldtitle = currenttitle.get('data');
-            var titletext = oldtitle;
-            var editbutton = element.one('a.' + CSS.EDITTITLECLASS + ' img');
-
-            // Handle events for edit_resource_title
-            var listenevents = [];
-            var thisevent;
-
-            // Grab the anchor so that we can swap it with the edit form
-            var anchor = instancename.ancestor('a');
+            var activityid = this.get_element_id(activity),
+                instancename  = activity.one(SELECTOR.INSTANCENAME),
+                currenttitle = instancename.get('firstChild'),
+                oldtitle = currenttitle.get('data'),
+                titletext = oldtitle,
+                thisevent,
+                anchor = instancename.ancestor('a'),// Grab the anchor so that we can swap it with the edit form.
+                data = {
+                    'class'   : 'resource',
+                    'field'   : 'gettitle',
+                    'id'      : activityid
+                },
+                response = this.send_request(data);
 
-            var data = {
-                'class'   : 'resource',
-                'field'   : 'gettitle',
-                'id'      : this.get_element_id(element)
-            };
+            if (M.core.actionmenu && M.core.actionmenu.instance) {
+                M.core.actionmenu.instance.hideMenu();
+            }
 
             // Try to retrieve the existing string from the server
-            var response = this.send_request(data, editbutton);
             if (response.instancename) {
                 titletext = response.instancename;
             }
 
             // Create the editor and submit button
-            var editor = Y.Node.create('<input />')
-                .setAttrs({
-                    'name'  : 'title',
-                    'value' : titletext,
-                    'autocomplete' : 'off',
-                    'aria-describedby' : 'id_editinstructions',
-                    'maxLength' : '255'
-                })
-                .addClass('titleeditor');
-            var editform = Y.Node.create('<form />')
-                .addClass('activityinstance')
-                .setAttribute('action', '#');
-            var editinstructions = Y.Node.create('<span />')
-                .addClass('editinstructions')
-                .setAttrs({'id' : 'id_editinstructions'})
+            var editform = Y.Node.create('<form class="'+CSS.ACTIVITYINSTANCE+'" action="#" />');
+            var editinstructions = Y.Node.create('<span class="'+CSS.EDITINSTRUCTIONS+'" id="id_editinstructions" />')
                 .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
-            var activityicon = element.one('img.activityicon').cloneNode();
+            var editor = Y.Node.create('<input name="title" type="text" class="'+CSS.TITLEEDITOR+'" />').setAttrs({
+                'value' : titletext,
+                'autocomplete' : 'off',
+                'aria-describedby' : 'id_editinstructions',
+                'maxLength' : '255'
+            })
 
             // Clear the existing content and put the editor in
-            currenttitle.set('data', '');
-            editform.appendChild(activityicon);
+            editform.appendChild(activity.one(SELECTOR.ACTIVITYICON).cloneNode());
             editform.appendChild(editor);
+            editform.setData('anchor', anchor);
             anchor.replace(editform);
-            elementdiv.appendChild(editinstructions);
-            e.preventDefault();
+            activity.one('div').appendChild(editinstructions);
+            ev.preventDefault();
 
             // Focus and select the editor text
             editor.focus().select();
 
-            // Handle removal of the editor
-            var clear_edittitle = function() {
-                // Detach all listen events to prevent duplicate triggers
-                var thisevent;
-                while (thisevent = listenevents.shift()) {
-                    thisevent.detach();
-                }
+            // Cancel the edit if we lose focus or the escape key is pressed.
+            thisevent = editor.on('blur', this.edit_title_cancel, this, activity, false);
+            this.edittitleevents.push(thisevent);
+            thisevent = editor.on('key', this.edit_title_cancel, 'esc', this, activity, true);
+            this.edittitleevents.push(thisevent);
 
-                if (editinstructions) {
-                    // Convert back to anchor and remove instructions
-                    editform.replace(anchor);
-                    editinstructions.remove();
-                    editinstructions = null;
+            // Handle form submission.
+            thisevent = editform.on('submit', this.edit_title_submit, this, activity, oldtitle);
+            this.edittitleevents.push(thisevent);
+        },
+
+        /**
+         * Handles the submit event when editing the activity or resources title.
+         *
+         * @protected
+         * @method edit_title_submit
+         * @param {EventFacade} ev The event that triggered this.
+         * @param {Node} activity The activity whose title we are altering.
+         * @param {String} originaltitle The original title the activity or resource had.
+         */
+        edit_title_submit : function(ev, activity, originaltitle) {
+            // We don't actually want to submit anything
+            ev.preventDefault();
+
+            var newtitle = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYTITLE).get('value'));
+            this.edit_title_clear(activity);
+            var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.INSTANCENAME));
+            if (newtitle != null && newtitle != "" && newtitle != originaltitle) {
+                var data = {
+                    'class'   : 'resource',
+                    'field'   : 'updatetitle',
+                    'title'   : newtitle,
+                    'id'      : this.get_element_id(activity)
+                };
+                var response = this.send_request(data, spinner);
+                if (response.instancename) {
+                    activity.one(SELECTOR.INSTANCENAME).setContent(response.instancename);
                 }
             }
+        },
 
-            // Handle cancellation of the editor
-            var cancel_edittitle = function(e) {
-                clear_edittitle();
-
-                // Set the title and anchor back to their previous settings
-                currenttitle.set('data', oldtitle);
-            };
+        /**
+         * Handles the cancel event when editing the activity or resources title.
+         *
+         * @protected
+         * @method edit_title_cancel
+         * @param {EventFacade} ev The event that triggered this.
+         * @param {Node} activity The activity whose title we are altering.
+         * @param {Boolean} preventdefault If true we should prevent the default action from occuring.
+         */
+        edit_title_cancel : function(ev, activity, preventdefault) {
+            if (preventdefault) {
+                ev.preventDefault();
+            }
+            this.edit_title_clear(activity);
+        },
 
-            // Cancel the edit if we lose focus or the escape key is pressed
-            thisevent = editor.on('blur', cancel_edittitle);
-            listenevents.push(thisevent);
-            thisevent = Y.one('document').on('keydown', function(e) {
-                if (e.keyCode === 27) {
-                    e.preventDefault();
-                    cancel_edittitle(e);
-                }
-            });
-            listenevents.push(thisevent);
-
-            // Handle form submission
-            thisevent = editform.on('submit', function(e) {
-                // We don't actually want to submit anything
-                e.preventDefault();
-
-                // Clear the edit title boxes
-                clear_edittitle();
-
-                // We only accept strings which have valid content
-                var newtitle = Y.Lang.trim(editor.get('value'));
-                if (newtitle != null && newtitle != "" && newtitle != titletext) {
-                    var data = {
-                        'class'   : 'resource',
-                        'field'   : 'updatetitle',
-                        'title'   : newtitle,
-                        'id'      : this.get_element_id(element)
-                    };
-                    var response = this.send_request(data, editbutton);
-                    if (response.instancename) {
-                        currenttitle.set('data', response.instancename);
-                    }
-                } else {
-                    // Invalid content. Set the title back to it's original contents
-                    currenttitle.set('data', oldtitle);
-                }
-            }, this);
-            listenevents.push(thisevent);
+        /**
+         * Handles clearing the editing UI and returning things to the original state they were in.
+         *
+         * @protected
+         * @method edit_title_clear
+         * @param {Node} activity  The activity whose title we were altering.
+         */
+        edit_title_clear : function(activity) {
+            // Detach all listen events to prevent duplicate triggers
+            var thisevent;
+            while (thisevent = this.edittitleevents.shift()) {
+                thisevent.detach();
+            }
+            var editform = activity.one(SELECTOR.ACTIVITYFORM),
+                instructions = activity.one('#id_editinstructions');
+            if (editform) {
+                editform.replace(editform.getData('anchor'));
+            }
+            if (instructions) {
+                instructions.remove();
+            }
         },
+
         /**
          * Set the visibility of the current resource (identified by the element)
          * to match the hidden parameter (this is not a toggle).
          * Only changes the visibility in the browser (no ajax update).
+         *
+         * @public This method is used by other modules.
+         * @method set_visibility_resource_ui
          * @param args An object with 'element' being the A node containing the resource
-         *             and 'visible' being the state that the visiblity should be set to.
-         * @return void
+         *             and 'visible' being the state that the visibility should be set to.
          */
         set_visibility_resource_ui: function(args) {
-            var element = args.element;
-            var shouldbevisible = args.visible;
-            var buttonnode = element.one(CSS.SHOW);
-            var visible = (buttonnode === null);
+            var element = args.element,
+                shouldbevisible = args.visible,
+                buttonnode = element.one(SELECTOR.SHOW),
+                visible = (buttonnode === null),
+                status = 'hide';
             if (visible) {
-                buttonnode = element.one(CSS.HIDE);
+                buttonnode = element.one(SELECTOR.HIDE);
+                status = 'show'
             }
             if (visible != shouldbevisible) {
-                this.toggle_hide_resource_ui(buttonnode);
+                this.handle_resource_dim(buttonnode, buttonnode.getData('activity'), status);
             }
         }
     }, {
@@ -651,9 +751,9 @@ YUI.add('moodle-course-toolboxes', function(Y) {
             M.course.coursebase.register_module(this);
 
             // Section Highlighting
-            Y.delegate('click', this.toggle_highlight, CSS.PAGECONTENT, CSS.SECTIONLI + ' ' + CSS.HIGHLIGHT, this);
+            Y.delegate('click', this.toggle_highlight, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.HIGHLIGHT, this);
             // Section Visibility
-            Y.delegate('click', this.toggle_hide_section, CSS.PAGECONTENT, CSS.SECTIONLI + ' ' + CSS.SHOWHIDE, this);
+            Y.delegate('click', this.toggle_hide_section, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.SHOWHIDE, this);
         },
         /**
          * Update any section areas within the scope of the specified
@@ -665,7 +765,7 @@ YUI.add('moodle-course-toolboxes', function(Y) {
         setup_for_section : function(baseselector) {
             // Left here for potential future use - not currently needed due to YUI delegation in initializer()
             /*if (!baseselector) {
-                var baseselector = CSS.PAGECONTENT;
+                var baseselector = SELECTOR.PAGECONTENT;
             }
 
             Y.all(baseselector).each(this._setup_for_section, this);*/
@@ -685,17 +785,19 @@ YUI.add('moodle-course-toolboxes', function(Y) {
             // The value to submit
             var value;
             // The status text for strings and images
-            var status;
+            var status,
+                oldstatus;
 
             if (!section.hasClass(CSS.SECTIONHIDDENCLASS)) {
                 section.addClass(CSS.SECTIONHIDDENCLASS);
                 value = 0;
                 status = 'show';
-
+                oldstatus = 'hide';
             } else {
                 section.removeClass(CSS.SECTIONHIDDENCLASS);
                 value = 1;
                 status = 'hide';
+                oldstatus = 'show';
             }
 
             var newstring = M.util.get_string(status + 'fromothers', 'format_' + this.get('format'));
@@ -718,17 +820,17 @@ YUI.add('moodle-course-toolboxes', function(Y) {
 
             var response = this.send_request(data, lightbox);
 
-            var activities = section.all(CSS.ACTIVITYLI);
+            var activities = section.all(SELECTOR.ACTIVITYLI);
             activities.each(function(node) {
-                if (node.one(CSS.SHOW)) {
-                    var button = node.one(CSS.SHOW);
+                if (node.one(SELECTOR.SHOW)) {
+                    var button = node.one(SELECTOR.SHOW);
                 } else {
-                    var button = node.one(CSS.HIDE);
+                    var button = node.one(SELECTOR.HIDE);
                 }
                 var activityid = this.get_element_id(node);
 
                 if (Y.Array.indexOf(response.resourcestotoggle, activityid) != -1) {
-                    this.toggle_hide_resource_ui(button);
+                    node.getData('toolbox').handle_resource_dim(button, node, oldstatus);
                 }
             }, this);
         },
@@ -747,16 +849,16 @@ YUI.add('moodle-course-toolboxes', function(Y) {
 
             // Set the current highlighted item text
             var old_string = M.util.get_string('markthistopic', 'moodle');
-            Y.one(CSS.PAGECONTENT)
-                .all(M.course.format.get_section_selector(Y) + '.current ' + CSS.HIGHLIGHT)
+            Y.one(SELECTOR.PAGECONTENT)
+                .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT)
                 .set('title', old_string);
-            Y.one(CSS.PAGECONTENT)
-                .all(M.course.format.get_section_selector(Y) + '.current ' + CSS.HIGHLIGHT + ' img')
+            Y.one(SELECTOR.PAGECONTENT)
+                .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT + ' img')
                 .set('alt', old_string)
                 .set('src', M.util.image_url('i/marker'));
 
             // Remove the highlighting from all sections
-            var allsections = Y.one(CSS.PAGECONTENT).all(M.course.format.get_section_selector(Y))
+            var allsections = Y.one(SELECTOR.PAGECONTENT).all(M.course.format.get_section_selector(Y))
                 .removeClass('current');
 
             // Then add it if required to the selected section
@@ -794,15 +896,28 @@ YUI.add('moodle-course-toolboxes', function(Y) {
     });
 
     M.course = M.course || {};
-
+    M.course.resource_toolbox = null;
     M.course.init_resource_toolbox = function(config) {
-        return new RESOURCETOOLBOX(config);
+        M.course.resource_toolbox = new RESOURCETOOLBOX(config);
+        return M.course.resource_toolbox;
     };
 
     M.course.init_section_toolbox = function(config) {
         return new SECTIONTOOLBOX(config);
     };
 
+    M.course.register_new_module = function(module) {
+        if (typeof module === 'string') {
+            module = Y.one(module);
+        }
+        if (M.course.resource_toolbox !== null) {
+            module.setData('toolbox', M.course.resource_toolbox);
+            module.all(SELECTOR.COMMANDSPAN+ ' ' + SELECTOR.ACTIVITYACTION).each(function(){
+                this.setData('activity', module);
+            });
+        }
+    }
+
 },
 '@VERSION@', {
     requires : ['base', 'node', 'io', 'moodle-course-coursebase']
index 29f6eeb..1746697 100644 (file)
@@ -960,7 +960,7 @@ class course_enrolment_manager {
                     if (strpos($userrole->component, 'enrol_') === 0) {
                         $plugin = substr($userrole->component, 6);
                         if (isset($plugins[$plugin])) {
-                            $changeable = !$plugin[$plugin]->roles_protected();
+                            $changeable = !$plugins[$plugin]->roles_protected();
                         }
                     }
                 }
index a23f9b5..3a3c26b 100644 (file)
@@ -90,12 +90,6 @@ class enrol_meta_handler {
 
         $context = context_course::instance($instance->courseid);
 
-        if (!$parentcontext = context_course::instance($instance->customint1, IGNORE_MISSING)) {
-            // linking to missing course is not possible
-            role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_meta'));
-            return;
-        }
-
         // list of enrolments in parent course (we ignore meta enrols in parents completely)
         list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e');
         $params['userid'] = $userid;
@@ -114,10 +108,8 @@ class enrol_meta_handler {
             return;
         }
 
-        if (!enrol_is_enabled('meta')) {
-            if ($ue) {
-                role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_meta'));
-            }
+        if (!$parentcontext = context_course::instance($instance->customint1, IGNORE_MISSING)) {
+            // Weird, we should not get here.
             return;
         }
 
@@ -172,9 +164,13 @@ class enrol_meta_handler {
             $ue->status = $parentstatus;
         }
 
+        $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
+
         // only active users in enabled instances are supposed to have roles (we can reassign the roles any time later)
         if ($ue->status != ENROL_USER_ACTIVE or $instance->status != ENROL_INSTANCE_ENABLED) {
-            if ($roles) {
+            if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) {
+                // Always keep the roles.
+            } else if ($roles) {
                 role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$instance->id));
             }
             return;
@@ -187,6 +183,11 @@ class enrol_meta_handler {
             }
         }
 
+        if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) {
+            // Always keep the roles.
+            return;
+        }
+
         // remove roles
         foreach ($roles as $rid) {
             if (!isset($parentroles[$rid])) {
@@ -206,25 +207,30 @@ class enrol_meta_handler {
      */
     protected static function user_not_supposed_to_be_here($instance, $ue, context_course $context, $plugin) {
         if (!$ue) {
-            // not enrolled yet - simple!
+            // Not enrolled yet - simple!
             return;
         }
 
         $userid = $ue->userid;
-        $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL);
+        $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
 
         if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) {
-            // purges grades, group membership, preferences, etc. - admins were warned!
+            // Purges grades, group membership, preferences, etc. - admins were warned!
             $plugin->unenrol_user($instance, $userid);
-            return;
 
-        } else { // ENROL_EXT_REMOVED_SUSPENDNOROLES
-            // just suspend users and remove all roles (we can reassign the roles any time later)
+        } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) {
             if ($ue->status != ENROL_USER_SUSPENDED) {
                 $plugin->update_user_enrol($instance, $userid, ENROL_USER_SUSPENDED);
-                role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$instance->id));
             }
-            return;
+
+        } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
+            if ($ue->status != ENROL_USER_SUSPENDED) {
+                $plugin->update_user_enrol($instance, $userid, ENROL_USER_SUSPENDED);
+            }
+            role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$instance->id));
+
+        } else {
+            debugging('Unknown unenrol action '.$unenrolaction);
         }
     }
 
@@ -316,8 +322,10 @@ class enrol_meta_handler {
      * @return bool success
      */
     public static function user_unenrolled($ue) {
-
-        // keep unenrolling even if plugin disabled
+        if (!enrol_is_enabled('meta')) {
+            // This is slow, let enrol_meta_sync() deal with disabled plugin.
+            return true;
+        }
 
         if ($ue->enrol === 'meta') {
             // prevent circular dependencies - we can not sync meta enrolments recursively
@@ -360,7 +368,10 @@ class enrol_meta_handler {
     public static function course_deleted($course) {
         global $DB;
 
-        // NOTE: do not test if plugin enabled, we want to keep disabling instances with invalid course links
+        if (!enrol_is_enabled('meta')) {
+            // This is slow, let enrol_meta_sync() deal with disabled plugin.
+            return true;
+        }
 
         // does anything want to sync with this parent?
         if (!$enrols = $DB->get_records('enrol', array('customint1'=>$course->id, 'enrol'=>'meta'), 'courseid ASC, id ASC')) {
@@ -368,19 +379,29 @@ class enrol_meta_handler {
         }
 
         $plugin = enrol_get_plugin('meta');
+        $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
 
-        // hack the DB info for all courses first
-        foreach ($enrols as $enrol) {
-            $enrol->customint1 = 0;
-            $enrol->status = ENROL_INSTANCE_DISABLED;
-            $DB->update_record('enrol', $enrol);
-            $context = context_course::instance($enrol->courseid);
-            role_unassign_all(array('contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$enrol->id));
+        if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) {
+            // Simple, just delete this instance which purges all enrolments,
+            // admins were warned that this is risky setting!
+            foreach ($enrols as $enrol) {
+                $plugin->delete_instance($enrol);
+            }
+            return true;
         }
 
-        // now trigger sync for each instance and purge caches
         foreach ($enrols as $enrol) {
-            $plugin->update_status($enrol, ENROL_INSTANCE_DISABLED);
+            $enrol->customint = 0;
+            $DB->update_record('enrol', $enrol);
+
+            if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND or $unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
+                // This makes all enrolments suspended very quickly.
+                $plugin->update_status($enrol, ENROL_INSTANCE_DISABLED);
+            }
+            if ($unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
+                $context = context_course::instance($enrol->courseid);
+                role_unassign_all(array('contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$enrol->id));
+            }
         }
 
         return true;
@@ -419,7 +440,7 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
 
     $meta = enrol_get_plugin('meta');
 
-    $unenrolaction = $meta->get_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL);
+    $unenrolaction = $meta->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
     $skiproles     = $meta->get_config('nosyncroleids', '');
     $skiproles     = empty($skiproles) ? array() : explode(',', $skiproles);
     $syncall       = $meta->get_config('syncall', 1);
@@ -493,19 +514,24 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
             if ($verbose) {
                 mtrace("  unenrolling: $ue->userid ==> $instance->courseid");
             }
-            continue;
 
-        } else { // ENROL_EXT_REMOVED_SUSPENDNOROLES
-            // just disable and ignore any changes
+        } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) {
+            if ($ue->status != ENROL_USER_SUSPENDED) {
+                $meta->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
+                if ($verbose) {
+                    mtrace("  suspending: $ue->userid ==> $instance->courseid");
+                }
+            }
+
+        } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
             if ($ue->status != ENROL_USER_SUSPENDED) {
                 $meta->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
                 $context = context_course::instance($instance->courseid);
-                role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$context->id, 'component'=>'enrol_meta'));
+                role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$instance->id));
                 if ($verbose) {
                     mtrace("  suspending and removing all roles: $ue->userid ==> $instance->courseid");
                 }
             }
-            continue;
         }
     }
     $rs->close();
@@ -617,6 +643,7 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
     } else {
         $notignored = "";
     }
+
     $sql = "SELECT ra.roleid, ra.userid, ra.contextid, ra.itemid, e.courseid
               FROM {role_assignments} ra
               JOIN {enrol} e ON (e.id = ra.itemid AND ra.component = 'enrol_meta' AND e.enrol = 'meta' $onecourse)
@@ -625,14 +652,16 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
          LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid AND ue.status = :activeuser)
              WHERE pra.id IS NULL OR ue.id IS NULL OR e.status <> :enabledinstance";
 
-    $rs = $DB->get_recordset_sql($sql, $params);
-    foreach($rs as $ra) {
-        role_unassign($ra->roleid, $ra->userid, $ra->contextid, 'enrol_meta', $ra->itemid);
-        if ($verbose) {
-            mtrace("  unassigning role: $ra->userid ==> $ra->courseid as ".$allroles[$ra->roleid]->shortname);
+    if ($unenrolaction != ENROL_EXT_REMOVED_SUSPEND) {
+        $rs = $DB->get_recordset_sql($sql, $params);
+        foreach($rs as $ra) {
+            role_unassign($ra->roleid, $ra->userid, $ra->contextid, 'enrol_meta', $ra->itemid);
+            if ($verbose) {
+                mtrace("  unassigning role: $ra->userid ==> $ra->courseid as ".$allroles[$ra->roleid]->shortname);
+            }
         }
+        $rs->close();
     }
-    $rs->close();
 
 
     // kick out or suspend users without synced roles if syncall disabled
index d2f5d93..3cbc4e1 100644 (file)
@@ -36,8 +36,10 @@ if ($ADMIN->fulltree) {
         $settings->add(new admin_setting_configcheckbox('enrol_meta/syncall', get_string('syncall', 'enrol_meta'), get_string('syncall_desc', 'enrol_meta'), 1));
 
         $options = array(
-            ENROL_EXT_REMOVED_UNENROL        => get_string('extremovedunenrol', 'enrol'),
-            ENROL_EXT_REMOVED_SUSPENDNOROLES => get_string('extremovedsuspendnoroles', 'enrol'));
+            ENROL_EXT_REMOVED_UNENROL        => get_string('extremovedunenrol', 'core_enrol'),
+            ENROL_EXT_REMOVED_SUSPEND        => get_string('extremovedsuspend', 'core_enrol'),
+            ENROL_EXT_REMOVED_SUSPENDNOROLES => get_string('extremovedsuspendnoroles', 'core_enrol'),
+        );
         $settings->add(new admin_setting_configselect('enrol_meta/unenrolaction', get_string('extremovedaction', 'enrol'), get_string('extremovedaction_help', 'enrol'), ENROL_EXT_REMOVED_SUSPENDNOROLES, $options));
     }
 }
diff --git a/enrol/meta/tests/plugin_test.php b/enrol/meta/tests/plugin_test.php
new file mode 100644 (file)
index 0000000..fdfec33
--- /dev/null
@@ -0,0 +1,442 @@
+<?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/>.
+
+/**
+ * Meta enrolment sync functional test.
+ *
+ * @package    enrol_meta
+ * @category   phpunit
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+class enrol_meta_plugin_testcase extends advanced_testcase {
+
+    protected function enable_plugin() {
+        $enabled = enrol_get_plugins(true);
+        $enabled['meta'] = true;
+        $enabled = array_keys($enabled);
+        set_config('enrol_plugins_enabled', implode(',', $enabled));
+    }
+
+    protected function disable_plugin() {
+        $enabled = enrol_get_plugins(true);
+        unset($enabled['meta']);
+        $enabled = array_keys($enabled);
+        set_config('enrol_plugins_enabled', implode(',', $enabled));
+    }
+
+    protected function is_meta_enrolled($user, $enrol, $role = null) {
+        global $DB;
+
+        if (!$DB->record_exists('user_enrolments', array('enrolid'=>$enrol->id, 'userid'=>$user->id))) {
+            return false;
+        }
+
+        if ($role === null) {
+            return true;
+        }
+
+        return $this->has_role($user, $enrol, $role);
+    }
+
+    protected function has_role($user, $enrol, $role) {
+        global $DB;
+
+        $context = context_course::instance($enrol->courseid);
+
+        if ($role === false) {
+            if ($DB->record_exists('role_assignments', array('contextid'=>$context->id, 'userid'=>$user->id, 'component'=>'enrol_meta', 'itemid'=>$enrol->id))) {
+                return false;
+            }
+        } else if (!$DB->record_exists('role_assignments', array('contextid'=>$context->id, 'userid'=>$user->id, 'roleid'=>$role->id, 'component'=>'enrol_meta', 'itemid'=>$enrol->id))) {
+            return false;
+        }
+
+        return true;
+    }
+
+    public function test_sync() {
+        global $CFG, $DB;
+
+        $this->resetAfterTest(true);
+
+        $metalplugin = enrol_get_plugin('meta');
+        $manplugin = enrol_get_plugin('manual');
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+        $user5 = $this->getDataGenerator()->create_user();
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $course3 = $this->getDataGenerator()->create_course();
+        $course4 = $this->getDataGenerator()->create_course();
+        $manual1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'manual'), '*', MUST_EXIST);
+        $manual2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'manual'), '*', MUST_EXIST);
+        $manual3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'manual'), '*', MUST_EXIST);
+        $manual4 = $DB->get_record('enrol', array('courseid'=>$course4->id, 'enrol'=>'manual'), '*', MUST_EXIST);
+
+        $student = $DB->get_record('role', array('shortname'=>'student'));
+        $teacher = $DB->get_record('role', array('shortname'=>'teacher'));
+        $manager = $DB->get_record('role', array('shortname'=>'manager'));
+
+        $this->disable_plugin();
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, $student->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id, $student->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course1->id, 0);
+        $this->getDataGenerator()->enrol_user($user4->id, $course1->id, $teacher->id);
+        $this->getDataGenerator()->enrol_user($user5->id, $course1->id, $manager->id);
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course2->id, $student->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course2->id, $teacher->id);
+
+        $this->assertEquals(7, $DB->count_records('user_enrolments'));
+        $this->assertEquals(6, $DB->count_records('role_assignments'));
+
+        set_config('syncall', 0, 'enrol_meta');
+        set_config('nosyncroleids', $manager->id, 'enrol_meta');
+
+        require_once($CFG->dirroot.'/enrol/meta/locallib.php');
+
+        enrol_meta_sync(null, false);
+        $this->assertEquals(7, $DB->count_records('user_enrolments'));
+        $this->assertEquals(6, $DB->count_records('role_assignments'));
+
+        $this->enable_plugin();
+        enrol_meta_sync(null, false);
+        $this->assertEquals(7, $DB->count_records('user_enrolments'));
+        $this->assertEquals(6, $DB->count_records('role_assignments'));
+
+        $e1 = $metalplugin->add_instance($course3, array('customint1'=>$course1->id));
+        $e2 = $metalplugin->add_instance($course3, array('customint1'=>$course2->id));
+        $e3 = $metalplugin->add_instance($course4, array('customint1'=>$course2->id));
+        $enrol1 = $DB->get_record('enrol', array('id'=>$e1));
+        $enrol2 = $DB->get_record('enrol', array('id'=>$e2));
+        $enrol3 = $DB->get_record('enrol', array('id'=>$e3));
+
+        enrol_meta_sync($course4->id, false);
+        $this->assertEquals(9, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol3, $student));
+        $this->assertTrue($this->is_meta_enrolled($user2, $enrol3, $teacher));
+
+        enrol_meta_sync(null, false);
+        $this->assertEquals(14, $DB->count_records('user_enrolments'));
+        $this->assertEquals(13, $DB->count_records('role_assignments'));
+
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        $this->assertTrue($this->is_meta_enrolled($user2, $enrol1, $student));
+        $this->assertFalse($this->is_meta_enrolled($user3, $enrol1));
+        $this->assertTrue($this->is_meta_enrolled($user4, $enrol1, $teacher));
+        $this->assertFalse($this->is_meta_enrolled($user5, $enrol1));
+
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol2, $student));
+        $this->assertTrue($this->is_meta_enrolled($user2, $enrol2, $teacher));
+
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol3, $student));
+        $this->assertTrue($this->is_meta_enrolled($user2, $enrol3, $teacher));
+
+        set_config('syncall', 1, 'enrol_meta');
+        enrol_meta_sync(null, false);
+        $this->assertEquals(16, $DB->count_records('user_enrolments'));
+        $this->assertEquals(13, $DB->count_records('role_assignments'));
+
+        $this->assertTrue($this->is_meta_enrolled($user3, $enrol1, false));
+        $this->assertTrue($this->is_meta_enrolled($user5, $enrol1, false));
+
+        $this->assertEquals(16, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->disable_plugin();
+        $manplugin->unenrol_user($manual1, $user1->id);
+        $manplugin->unenrol_user($manual2, $user1->id);
+
+        $this->assertEquals(14, $DB->count_records('user_enrolments'));
+        $this->assertEquals(11, $DB->count_records('role_assignments'));
+        $this->assertEquals(14, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+
+        $this->enable_plugin();
+
+        set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPEND, 'enrol_meta');
+        enrol_meta_sync($course4->id, false);
+        $this->assertEquals(14, $DB->count_records('user_enrolments'));
+        $this->assertEquals(11, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol3, $student));
+        $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$enrol3->id, 'status'=>ENROL_USER_SUSPENDED, 'userid'=>$user1->id)));
+
+        enrol_meta_sync(null, false);
+        $this->assertEquals(14, $DB->count_records('user_enrolments'));
+        $this->assertEquals(11, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$enrol1->id, 'status'=>ENROL_USER_SUSPENDED, 'userid'=>$user1->id)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol2, $student));
+        $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$enrol2->id, 'status'=>ENROL_USER_SUSPENDED, 'userid'=>$user1->id)));
+
+        set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES, 'enrol_meta');
+        enrol_meta_sync($course4->id, false);
+        $this->assertEquals(14, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol3, false));
+        $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$enrol3->id, 'status'=>ENROL_USER_SUSPENDED, 'userid'=>$user1->id)));
+
+        enrol_meta_sync(null, false);
+        $this->assertEquals(14, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, false));
+        $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$enrol1->id, 'status'=>ENROL_USER_SUSPENDED, 'userid'=>$user1->id)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol2, false));
+        $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$enrol2->id, 'status'=>ENROL_USER_SUSPENDED, 'userid'=>$user1->id)));
+
+        set_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL, 'enrol_meta');
+        enrol_meta_sync($course4->id, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol3));
+
+        enrol_meta_sync(null, false);
+        $this->assertEquals(11, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol2));
+
+
+        // Now try sync triggered by events.
+
+        set_config('syncall', 1, 'enrol_meta');
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, $student->id);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+
+        $manplugin->unenrol_user($manual1, $user1->id);
+        $this->assertEquals(11, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(11, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1));
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 0);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, false));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, false));
+
+        $manplugin->unenrol_user($manual1, $user1->id);
+        $this->assertEquals(11, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(11, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1));
+
+        set_config('syncall', 0, 'enrol_meta');
+        enrol_meta_sync(null, false);
+        $this->assertEquals(9, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(9, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1));
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 0);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(10, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(10, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1, $student));
+
+        role_assign($teacher->id, $user1->id, context_course::instance($course1->id)->id);
+        $this->assertEquals(11, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $teacher));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(11, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $teacher));
+
+        role_unassign($teacher->id, $user1->id, context_course::instance($course1->id)->id);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(10, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(10, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1, $student));
+
+        $manplugin->unenrol_user($manual1, $user1->id);
+        $this->assertEquals(9, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(9, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1));
+
+        set_config('syncall', 1, 'enrol_meta');
+        set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPEND, 'enrol_meta');
+        enrol_meta_sync(null, false);
+        $this->assertEquals(11, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, $student->id);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+
+        $manplugin->update_user_enrol($manual1, $user1->id, ENROL_USER_SUSPENDED);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+
+        $manplugin->unenrol_user($manual1, $user1->id);
+        $this->assertEquals(12, $DB->count_records('user_enrolments'));
+        $this->assertEquals(9, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(12, $DB->count_records('user_enrolments'));
+        $this->assertEquals(9, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, $student->id);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+
+        set_config('syncall', 1, 'enrol_meta');
+        set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES, 'enrol_meta');
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, $student->id);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+
+        $manplugin->unenrol_user($manual1, $user1->id);
+        $this->assertEquals(12, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, false));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(12, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, false));
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, $student->id);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+
+
+        set_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL, 'enrol_meta');
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+
+        delete_course($course1, false);
+        $this->assertEquals(3, $DB->count_records('user_enrolments'));
+        $this->assertEquals(3, $DB->count_records('role_assignments'));
+        $this->assertEquals(3, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(3, $DB->count_records('user_enrolments'));
+        $this->assertEquals(3, $DB->count_records('role_assignments'));
+        $this->assertEquals(3, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+
+        delete_course($course2, false);
+        $this->assertEquals(0, $DB->count_records('user_enrolments'));
+        $this->assertEquals(0, $DB->count_records('role_assignments'));
+        $this->assertEquals(0, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(0, $DB->count_records('user_enrolments'));
+        $this->assertEquals(0, $DB->count_records('role_assignments'));
+        $this->assertEquals(0, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+
+        delete_course($course3, false);
+        delete_course($course4, false);
+
+    }
+}
index 5505cb2..250fcd8 100644 (file)
@@ -34,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class core_enrol_testcase extends advanced_testcase {
+class core_enrollib_testcase extends advanced_testcase {
 
     public function test_enrol_get_all_users_courses() {
         global $DB, $CFG;
index b4e8c50..073448e 100644 (file)
@@ -30,7 +30,7 @@ require_once($CFG->dirroot . '/enrol/externallib.php');
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @since Moodle 2.4
  */
-class core_enrol_external_testcase extends externallib_advanced_testcase {
+class core_enrol_externallib_testcase extends externallib_advanced_testcase {
 
     /**
      * Test get_enrolled_users
similarity index 98%
rename from enrol/tests/externallib_role_test.php
rename to enrol/tests/role_external_test.php
index f3b77f0..2a8c4a8 100644 (file)
@@ -30,7 +30,7 @@ require_once($CFG->dirroot . '/enrol/externallib.php');
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @since Moodle 2.4
  */
-class core_role_external_testcase extends externallib_advanced_testcase {
+class core_enrol_role_external_testcase extends externallib_advanced_testcase {
 
     /**
      * Tests set up
index 5f9eb61..a48187e 100644 (file)
@@ -304,9 +304,9 @@ class core_files_renderer extends plugin_renderer_base {
      */
     private function fm_js_template_mkdir() {
         $rv = '
-<div class="filemanager fp-mkdir-dlg">
+<div class="filemanager fp-mkdir-dlg" role="dialog" aria-live="assertive" aria-labelledby="fp-mkdir-dlg-title">
     <div class="fp-mkdir-dlg-text">
-        <label>' . get_string('newfoldername', 'repository') . '</label><br/>
+        <label id="fp-mkdir-dlg-title">' . get_string('newfoldername', 'repository') . '</label><br/>
         <input type="text" />
     </div>
     <button class="{!}fp-dlg-butcreate">'.get_string('makeafolder').'</button>
@@ -803,8 +803,8 @@ class core_files_renderer extends plugin_renderer_base {
      */
     private function fp_js_template_message() {
         $rv = '
-<div class="file-picker fp-msg">
-    <p class="{!}fp-msg-text"></p>
+<div class="file-picker fp-msg" role="alertdialog" aria-live="assertive" aria-labelledby="fp-msg-labelledby">
+    <p class="{!}fp-msg-text" id="fp-msg-labelledby"></p>
     <button class="{!}fp-msg-butok">'.get_string('ok').'</button>
 </div>';
         return preg_replace('/\{\!\}/', '', $rv);
index ce91c1c..88fbb34 100644 (file)
@@ -31,7 +31,7 @@ global $CFG;
 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
 require_once($CFG->dirroot . '/files/externallib.php');
 
-class test_external_files extends advanced_testcase {
+class core_files_externallib_testcase extends advanced_testcase {
 
     /*
      * Test core_files_external::upload().
index 9081f00..1772b0a 100644 (file)
@@ -110,10 +110,12 @@ class grade_export_form extends moodleform {
             $mform->addElement('text', 'iprestriction', get_string('keyiprestriction', 'userkey'), array('size'=>80));
             $mform->addHelpButton('iprestriction', 'keyiprestriction', 'userkey');
             $mform->setDefault('iprestriction', getremoteaddr()); // own IP - just in case somebody does not know what user key is
+            $mform->setType('iprestriction', PARAM_RAW_TRIMMED);
 
             $mform->addElement('date_time_selector', 'validuntil', get_string('keyvaliduntil', 'userkey'), array('optional'=>true));
             $mform->addHelpButton('validuntil', 'keyvaliduntil', 'userkey');
             $mform->setDefault('validuntil', time()+3600*24*7); // only 1 week default duration - just in case somebody does not know what user key is
+            $mform->setType('validuntil', PARAM_INT);
 
             $mform->disabledIf('iprestriction', 'key', 'noteq', 1);
             $mform->disabledIf('validuntil', 'key', 'noteq', 1);
index 8c44f19..e30a3a5 100644 (file)
@@ -39,7 +39,10 @@ class key_form extends moodleform {
 
         $mform->addElement('static', 'value', get_string('keyvalue', 'userkey'));
         $mform->addElement('text', 'iprestriction', get_string('keyiprestriction', 'userkey'), array('size'=>80));
+        $mform->setType('iprestriction', PARAM_RAW_TRIMMED);
+
         $mform->addElement('date_time_selector', 'validuntil', get_string('keyvaliduntil', 'userkey'), array('optional'=>true));
+        $mform->setType('validuntil', PARAM_INT);
 
         $mform->addHelpButton('iprestriction', 'keyiprestriction', 'userkey');
         $mform->addHelpButton('validuntil', 'keyvaliduntil', 'userkey');
similarity index 98%
rename from grade/grading/tests/lib_test.php
rename to grade/grading/tests/grading_manager_test.php
index 087f40c..0e806e8 100644 (file)
@@ -49,7 +49,7 @@ class testable_grading_manager extends grading_manager {
  * @copyright  2011 David Mudrak <david@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class grading_manager_testcase extends advanced_testcase {
+class core_grade_grading_manager_testcase extends advanced_testcase {
     public function test_basic_instantiation() {
         $manager1 = get_grading_manager();
 
index 6706381..afda858 100644 (file)
@@ -43,6 +43,7 @@ class key_form extends moodleform {
 
         $mform->addHelpButton('iprestriction', 'keyiprestriction', 'userkey');
         $mform->addHelpButton('validuntil', 'keyvaliduntil', 'userkey');
+        $mform->setType('iprestriction', PARAM_RAW_TRIMMED);
 
         $mform->addElement('hidden','id');
         $mform->setType('id', PARAM_INT);
index 70da1b1..73c2474 100644 (file)
@@ -24,3 +24,4 @@
 
 $string['pluginname'] = 'User report';
 $string['user:view'] = 'View your own grade report';
+$string['tablesummary'] = 'The table is arranged as a list of graded items including categories of graded items. When items are in a category they will be indicated as such.';
index c5920ff..adb3bad 100644 (file)
@@ -337,6 +337,9 @@ class grade_report_user extends grade_report {
 
         /// Process those items that have scores associated
         if ($type == 'item' or $type == 'categoryitem' or $type == 'courseitem') {
+            $header_row = "row_{$eid}_{$this->user->id}";
+            $header_cat = "cat_{$grade_object->categoryid}_{$this->user->id}";
+
             if (! $grade_grade = grade_grade::fetch(array('itemid'=>$grade_object->id,'userid'=>$this->user->id))) {
                 $grade_grade = new grade_grade();
                 $grade_grade->userid = $this->user->id;
@@ -365,7 +368,8 @@ class grade_report_user extends grade_report {
                     $cm = $instances[$grade_object->iteminstance];
                     if (!$cm->uservisible) {
                         // Further checks are required to determine whether the activity is entirely hidden or just greyed out.
-                        if ($cm->is_user_access_restricted_by_group() || $cm->is_user_access_restricted_by_conditional_access()) {
+                        if ($cm->is_user_access_restricted_by_group() || $cm->is_user_access_restricted_by_conditional_access() ||
+                                $cm->is_user_access_restricted_by_capability()) {
                             $hide = true;
                         }
                     }
@@ -386,11 +390,16 @@ class grade_report_user extends grade_report {
                 } else {
                    $class .= ($type == 'categoryitem' or $type == 'courseitem') ? " ".$alter."d$depth baggb" : " item b1b";
                 }
+                if ($type == 'categoryitem' or $type == 'courseitem') {
+                    $header_cat = "cat_{$grade_object->iteminstance}_{$this->user->id}";
+                }
 
                 /// Name
                 $data['itemname']['content'] = $fullname;
                 $data['itemname']['class'] = $class;
                 $data['itemname']['colspan'] = ($this->maxdepth - $depth);
+                $data['itemname']['celltype'] = 'th';
+                $data['itemname']['id'] = $header_row;
 
                 /// Actual Grade
                 $gradeval = $grade_grade->finalgrade;
@@ -403,6 +412,7 @@ class grade_report_user extends grade_report {
                 if ($this->showweight) {
                     $data['weight']['class'] = $class;
                     $data['weight']['content'] = '-';
+                    $data['weight']['headers'] = "$header_cat $header_row weight";
                     // has a weight assigned, might be extra credit