Merge branch 'wip-MDL-43218-master' of git://github.com/marinaglancy/moodle
authorSam Hemelryk <sam@moodle.com>
Wed, 11 Dec 2013 02:32:29 +0000 (15:32 +1300)
committerSam Hemelryk <sam@moodle.com>
Wed, 11 Dec 2013 02:32:29 +0000 (15:32 +1300)
229 files changed:
.jshintrc
admin/index.php
admin/renderer.php
admin/settings/appearance.php
admin/tests/behat/behat_admin.php
admin/tests/behat/upload_users.feature
admin/tool/behat/tests/behat/basic_actions.feature
admin/tool/behat/tests/behat/data_generators.feature
admin/tool/replace/classes/form.php [new file with mode: 0644]
admin/tool/replace/index.php
admin/tool/replace/lang/en/tool_replace.php
admin/tool/replace/version.php
admin/tool/uploadcourse/tests/behat/create.feature
admin/tool/uploadcourse/tests/behat/update.feature
admin/webservice/forms.php
auth/db/auth.php
auth/email/auth.php
auth/ldap/auth.php
backup/backup.php
backup/moodle2/backup_custom_fields.php
backup/util/dbops/restore_dbops.class.php
backup/util/ui/base_moodleform.class.php
backup/util/ui/tests/behat/backup_courses.feature
backup/util/ui/tests/behat/behat_backup.php
backup/util/ui/tests/behat/duplicate_activities.feature
backup/util/ui/yui/build/moodle-backup-backupselectall/moodle-backup-backupselectall-debug.js [new file with mode: 0644]
backup/util/ui/yui/build/moodle-backup-backupselectall/moodle-backup-backupselectall-min.js [new file with mode: 0644]
backup/util/ui/yui/build/moodle-backup-backupselectall/moodle-backup-backupselectall.js [new file with mode: 0644]
backup/util/ui/yui/build/moodle-backup-confirmcancel/moodle-backup-confirmcancel-debug.js [new file with mode: 0644]
backup/util/ui/yui/build/moodle-backup-confirmcancel/moodle-backup-confirmcancel-min.js [new file with mode: 0644]
backup/util/ui/yui/build/moodle-backup-confirmcancel/moodle-backup-confirmcancel.js [new file with mode: 0644]
backup/util/ui/yui/confirmcancel/confirmcancel.js [deleted file]
backup/util/ui/yui/src/backupselectall/build.json [new file with mode: 0644]
backup/util/ui/yui/src/backupselectall/js/backupselectall.js [moved from backup/util/ui/yui/backupselectall/backupselectall.js with 88% similarity]
backup/util/ui/yui/src/backupselectall/js/backupselectall.json [new file with mode: 0644]
backup/util/ui/yui/src/backupselectall/meta/backupselectall.json [new file with mode: 0644]
backup/util/ui/yui/src/confirmcancel/build.json [new file with mode: 0644]
backup/util/ui/yui/src/confirmcancel/js/confirmcancel.js [new file with mode: 0644]
backup/util/ui/yui/src/confirmcancel/meta/confirmcancel.json [new file with mode: 0644]
badges/tests/behat/add_badge.feature
badges/tests/behat/award_badge.feature
blocks/blog_recent/block_blog_recent.php
blocks/comments/tests/behat/behat_block_comments.php
blocks/navigation/tests/behat/view_my_courses.feature
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js
blocks/navigation/yui/src/navigation/js/navigation.js
blocks/tests/behat/behat_blocks.php
blog/tests/behat/comment.feature
cohort/tests/behat/upload_cohort_users.feature
completion/tests/behat/enable_manual_complete_mark.feature
completion/tests/behat/restrict_activity_by_date.feature
completion/tests/behat/restrict_activity_by_grade.feature
config-dist.php
course/lib.php
course/tests/behat/activities_group_icons.feature
course/tests/behat/add_activities.feature
course/tests/behat/behat_course.php
course/tests/behat/course_category_management_listing.feature
course/tests/behat/course_controls.feature
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js [new file with mode: 0644]
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js [new file with mode: 0644]
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js [new file with mode: 0644]
course/yui/src/toolboxes/build.json [new file with mode: 0644]
course/yui/src/toolboxes/js/resource.js [new file with mode: 0644]
course/yui/src/toolboxes/js/section.js [new file with mode: 0644]
course/yui/src/toolboxes/js/shared.js [new file with mode: 0644]
course/yui/src/toolboxes/js/toolbox.js [new file with mode: 0644]
course/yui/src/toolboxes/meta/toolboxes.json [new file with mode: 0644]
course/yui/toolboxes/toolboxes.js [deleted file]
enrol/guest/lang/en/enrol_guest.php
enrol/guest/lib.php
enrol/yui/notification/notification.js [deleted file]
group/clientlib.js
group/tests/behat/behat_groups.php
group/tests/behat/create_groups.feature
group/tests/behat/groups_import.feature
lang/en/admin.php
lang/en/moodle.php
lib/adminlib.php
lib/behat/behat_base.php
lib/behat/behat_files.php
lib/behat/classes/behat_config_manager.php
lib/behat/form_field/behat_form_editor.php
lib/behat/form_field/behat_form_field.php
lib/behat/form_field/behat_form_select.php
lib/classes/component.php
lib/classes/event/base.php
lib/classes/event/course_module_instance_list_viewed.php [new file with mode: 0644]
lib/classes/event/course_module_instances_list_viewed.php
lib/classes/update/checker.php
lib/classes/update/deployer.php
lib/classes/useragent.php
lib/deprecatedlib.php
lib/dml/moodle_database.php
lib/dml/mssql_native_moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/dml/tests/dml_test.php
lib/editor/tinymce/module.js
lib/editor/tinymce/plugins/managefiles/manage_form.php
lib/editor/tinymce/tests/behat/edit_available_icons.feature
lib/form/yui/build/moodle-form-shortforms/moodle-form-shortforms-debug.js [new file with mode: 0644]
lib/form/yui/build/moodle-form-shortforms/moodle-form-shortforms-min.js [new file with mode: 0644]
lib/form/yui/build/moodle-form-shortforms/moodle-form-shortforms.js [new file with mode: 0644]
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-debug.js [new file with mode: 0644]
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-min.js [new file with mode: 0644]
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced.js [new file with mode: 0644]
lib/form/yui/shortforms/shortforms.js [deleted file]
lib/form/yui/showadvanced/showadvanced.js [deleted file]
lib/form/yui/src/shortforms/build.json [new file with mode: 0644]
lib/form/yui/src/shortforms/js/shortforms.js [new file with mode: 0644]
lib/form/yui/src/shortforms/meta/shortforms.json [new file with mode: 0644]
lib/form/yui/src/showadvanced/build.json [new file with mode: 0644]
lib/form/yui/src/showadvanced/js/showadvanced.js [new file with mode: 0644]
lib/form/yui/src/showadvanced/meta/showadvanced.json [new file with mode: 0644]
lib/grouplib.php
lib/javascript-static.js
lib/navigationlib.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/testing/generator/block_generator.php
lib/testing/generator/data_generator.php
lib/testing/generator/module_generator.php
lib/tests/behat/behat_deprecated.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_hooks.php
lib/tests/behat/behat_navigation.php
lib/tests/behat/behat_permissions.php
lib/tests/event_course_module_instance_list_viewed.php [new file with mode: 0644]
lib/tests/event_deprecated_test.php [new file with mode: 0644]
lib/tests/event_test.php
lib/tests/fixtures/event_mod_badfixtures.php [moved from lib/phpunit/generatorlib.php with 59% similarity]
lib/tests/fixtures/event_mod_fixtures.php [new file with mode: 0644]
lib/tests/grouplib_test.php
lib/tests/theme_config_test.php
lib/tests/useragent_test.php
lib/upgrade.txt
lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu-debug.js
lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu-min.js
lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu.js
lib/yui/build/moodle-core-blocks/moodle-core-blocks-debug.js
lib/yui/build/moodle-core-blocks/moodle-core-blocks-min.js
lib/yui/build/moodle-core-blocks/moodle-core-blocks.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js [new file with mode: 0644]
lib/yui/dragdrop/dragdrop.js [deleted file]
lib/yui/src/actionmenu/js/actionmenu.js
lib/yui/src/actionmenu/meta/actionmenu.json
lib/yui/src/blocks/js/blocks.js
lib/yui/src/dragdrop/build.json [new file with mode: 0644]
lib/yui/src/dragdrop/js/dragdrop.js [new file with mode: 0644]
lib/yui/src/dragdrop/meta/dragdrop.json [new file with mode: 0644]
lib/yuilib/3.13.0/io-upload-iframe/io-upload-iframe-coverage.js
lib/yuilib/3.13.0/io-upload-iframe/io-upload-iframe-debug.js
lib/yuilib/3.13.0/io-upload-iframe/io-upload-iframe-min.js
lib/yuilib/3.13.0/io-upload-iframe/io-upload-iframe.js
mdeploy.php
mod/assign/backup/moodle2/restore_assign_activity_task.class.php
mod/assign/feedback/offline/importgradesform.php
mod/assign/feedback/offline/locallib.php
mod/assign/module.js
mod/assign/tests/behat/file_submission.feature
mod/book/classes/event/course_module_instance_list_viewed.php [new file with mode: 0644]
mod/book/classes/event/instances_list_viewed.php [deleted file]
mod/book/index.php
mod/book/lang/en/book.php
mod/book/tests/events_test.php
mod/chat/classes/event/course_module_instance_list_viewed.php [new file with mode: 0644]
mod/chat/classes/event/instances_list_viewed.php [deleted file]
mod/chat/index.php
mod/chat/lang/en/chat.php
mod/chat/tests/events_test.php
mod/choice/classes/event/course_module_instance_list_viewed.php [new file with mode: 0644]
mod/choice/classes/event/instances_list_viewed.php [deleted file]
mod/choice/index.php
mod/choice/lang/en/choice.php
mod/choice/tests/events_test.php
mod/data/data.js
mod/feedback/classes/event/course_module_instance_list_viewed.php [new file with mode: 0644]
mod/feedback/classes/event/instances_list_viewed.php [deleted file]
mod/feedback/index.php
mod/feedback/lang/en/feedback.php
mod/forum/tests/behat/add_forum.feature
mod/forum/tests/behat/edit_post_teacher.feature
mod/forum/tests/behat/track_read_posts.feature
mod/page/classes/event/course_module_instance_list_viewed.php [new file with mode: 0644]
mod/page/classes/event/instances_list_viewed.php [deleted file]
mod/page/index.php
mod/page/lang/en/page.php
mod/scorm/tests/behat/add_scorm.feature
mod/wiki/module.js
mod/wiki/tests/behat/preview_page.feature
question/editlib.php
question/tests/behat/behat_question.php
question/tests/behat/edit_questions.feature
question/tests/behat/preview_question.feature
repository/filesystem/lib.php
repository/recent/tests/behat/add_recent.feature
repository/recent/tests/behat/behat_repository_recent.php [deleted file]
repository/tests/behat/behat_filepicker.php
repository/tests/behat/cancel_add_file.feature
repository/tests/behat/create_folders.feature
repository/tests/behat/create_shortcut.feature [new file with mode: 0644]
repository/tests/behat/delete_files.feature
repository/tests/behat/overwrite_file.feature [new file with mode: 0644]
repository/tests/behat/zip_and_unzip.feature
repository/upload/tests/behat/behat_repository_upload.php
repository/upload/tests/behat/upload_file.feature
theme/afterburner/style/afterburner_styles.css
theme/base/style/core.css
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/readme_moodle.txt
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/yui/build/moodle-theme_bootstrapbase-bootstrap/moodle-theme_bootstrapbase-bootstrap-debug.js
theme/bootstrapbase/yui/build/moodle-theme_bootstrapbase-bootstrap/moodle-theme_bootstrapbase-bootstrap-min.js
theme/bootstrapbase/yui/build/moodle-theme_bootstrapbase-bootstrap/moodle-theme_bootstrapbase-bootstrap.js
theme/bootstrapbase/yui/src/bootstrap/js/bootstrapcollapse.js
theme/bootstrapbase/yui/src/bootstrap/js/bootstrapdropdown.js
theme/bootstrapbase/yui/src/bootstrap/js/bootstrapengine.js
theme/canvas/style/core.css
theme/clean/layout/secure.php
user/editlib.php
user/externallib.php
user/lib.php

index 06de646..89f05f1 100644 (file)
--- a/.jshintrc
+++ b/.jshintrc
@@ -42,7 +42,7 @@
     "sub":          false,
     "supernew":     false,
     "maxerr":       500,
-    "maxlen":       150,
+    "maxlen":       180,
     "passfail":     false,
     "latedef":      true
 }
index ea9f37d..a07104c 100644 (file)
@@ -47,7 +47,9 @@ if (!function_exists('iconv')) {
 
 define('NO_OUTPUT_BUFFERING', true);
 
-if (empty($_GET['cache']) and empty($_POST['cache']) and empty($_GET['sesskey']) and empty($_POST['sesskey'])) {
+if ((isset($_GET['cache']) and $_GET['cache'] === '0')
+        or (isset($_POST['cache']) and $_POST['cache'] === '0')
+        or (!isset($_POST['cache']) and !isset($_GET['cache']) and empty($_GET['sesskey']) and empty($_POST['sesskey']))) {
     // Prevent caching at all cost when visiting this page directly,
     // we redirect to self once we known no upgrades are necessary.
     // Note: $_GET and $_POST are used here intentionally because our param cleaning is not loaded yet.
@@ -90,9 +92,7 @@ $newaddonreq    = optional_param('installaddonrequest', null, PARAM_RAW);
 
 // Set up PAGE.
 $url = new moodle_url('/admin/index.php');
-if ($cache) {
-    $url->param('cache', 1);
-}
+$url->param('cache', $cache);
 $PAGE->set_url($url);
 unset($url);
 
@@ -267,12 +267,13 @@ if (!$cache and $version > $CFG->version) {  // upgrade
     $PAGE->set_pagelayout('maintenance');
     $PAGE->set_popup_notification_allowed(false);
 
+    /** @var core_admin_renderer $output */
+    $output = $PAGE->get_renderer('core', 'admin');
+
     if (upgrade_stale_php_files_present()) {
         $PAGE->set_title($stradministration);
         $PAGE->set_cacheable(false);
 
-        /** @var core_admin_renderer $output */
-        $output = $PAGE->get_renderer('core', 'admin');
         echo $output->upgrade_stale_php_files_page();
         die();
     }
@@ -287,8 +288,6 @@ if (!$cache and $version > $CFG->version) {  // upgrade
         $PAGE->set_heading($strdatabasechecking);
         $PAGE->set_cacheable(false);
 
-        /** @var core_admin_renderer $output */
-        $output = $PAGE->get_renderer('core', 'admin');
         echo $output->upgrade_confirm_page($a->newversion, $maturity, $testsite);
         die();
 
@@ -302,8 +301,6 @@ if (!$cache and $version > $CFG->version) {  // upgrade
         $PAGE->set_heading($strcurrentrelease);
         $PAGE->set_cacheable(false);
 
-        /** @var core_admin_renderer $output */
-        $output = $PAGE->get_renderer('core', 'admin');
         echo $output->upgrade_environment_page($release, $envstatus, $environment_results);
         die();
 
@@ -315,23 +312,13 @@ if (!$cache and $version > $CFG->version) {  // upgrade
         $PAGE->set_heading($strplugincheck);
         $PAGE->set_cacheable(false);
 
-        $reloadurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1));
-
-        /** @var core_admin_renderer $output */
-        $output = $PAGE->get_renderer('core', 'admin');
-
-        // check plugin dependencies first
-        $failed = array();
-        if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
-            echo $output->unsatisfied_dependencies_page($version, $failed, $reloadurl);
-            die();
-        }
-        unset($failed);
+        $reloadurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0));
 
         if ($fetchupdates) {
-            // no sesskey support guaranteed here
-            if (empty($CFG->disableupdatenotifications)) {
-                \core\update\checker::instance()->fetch();
+            // No sesskey support guaranteed here, because sessions might not work yet.
+            $updateschecker = \core\update\checker::instance();
+            if ($updateschecker->enabled()) {
+                $updateschecker->fetch();
             }
             redirect($reloadurl);
         }
@@ -342,6 +329,7 @@ if (!$cache and $version > $CFG->version) {  // upgrade
 
             $deploydata = $deployer->submitted_data();
             if (!empty($deploydata)) {
+                // No sesskey support guaranteed here, because sessions might not work yet.
                 echo $output->upgrade_plugin_confirm_deploy_page($deployer, $deploydata);
                 die();
             }
@@ -349,11 +337,22 @@ if (!$cache and $version > $CFG->version) {  // upgrade
 
         echo $output->upgrade_plugin_check_page(core_plugin_manager::instance(), \core\update\checker::instance(),
                 $version, $showallplugins, $reloadurl,
-                new moodle_url('/admin/index.php', array('confirmupgrade'=>1, 'confirmrelease'=>1, 'confirmplugincheck'=>1)));
+                new moodle_url('/admin/index.php', array('confirmupgrade'=>1, 'confirmrelease'=>1, 'confirmplugincheck'=>1, 'cache'=>0)));
         die();
 
     } else {
-        // Launch main upgrade
+        // Always verify plugin dependencies!
+        $failed = array();
+        if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+            $PAGE->set_pagelayout('maintenance');
+            $PAGE->set_popup_notification_allowed(false);
+            $reloadurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0));
+            echo $output->unsatisfied_dependencies_page($version, $failed, $reloadurl);
+            die();
+        }
+        unset($failed);
+
+        // Launch main upgrade.
         upgrade_core($version, true);
     }
 } else if ($version < $CFG->version) {
@@ -373,6 +372,10 @@ if (!$cache and $branch <> $CFG->branch) {  // Update the branch
 if (!$cache and moodle_needs_upgrading()) {
     if (!$PAGE->headerprinted) {
         // means core upgrade or installation was not already done
+
+        /** @var core_admin_renderer $output */
+        $output = $PAGE->get_renderer('core', 'admin');
+
         if (!$confirmplugins) {
             $strplugincheck = get_string('plugincheck');
 
@@ -384,40 +387,46 @@ if (!$cache and moodle_needs_upgrading()) {
             $PAGE->set_cacheable(false);
 
             if ($fetchupdates) {
-                // no sesskey support guaranteed here
-                \core\update\checker::instance()->fetch();
+                require_sesskey();
+                $updateschecker = \core\update\checker::instance();
+                if ($updateschecker->enabled()) {
+                    $updateschecker->fetch();
+                }
                 redirect($PAGE->url);
             }
 
-            $output = $PAGE->get_renderer('core', 'admin');
-
             $deployer = \core\update\deployer::instance();
             if ($deployer->enabled()) {
                 $deployer->initialize($PAGE->url, $PAGE->url);
 
                 $deploydata = $deployer->submitted_data();
                 if (!empty($deploydata)) {
+                    require_sesskey();
                     echo $output->upgrade_plugin_confirm_deploy_page($deployer, $deploydata);
                     die();
                 }
             }
 
-            // check plugin dependencies first
-            $failed = array();
-            if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
-                echo $output->unsatisfied_dependencies_page($version, $failed, $PAGE->url);
-                die();
-            }
-            unset($failed);
-
-            // dependencies check passed, let's rock!
+            // Show plugins info.
             echo $output->upgrade_plugin_check_page(core_plugin_manager::instance(), \core\update\checker::instance(),
                     $version, $showallplugins,
                     new moodle_url($PAGE->url),
-                    new moodle_url('/admin/index.php', array('confirmplugincheck'=>1)));
+                    new moodle_url('/admin/index.php', array('confirmplugincheck'=>1, 'cache'=>0)));
+            die();
+        }
+
+        // Make sure plugin dependencies are always checked.
+        $failed = array();
+        if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+            $PAGE->set_pagelayout('maintenance');
+            $PAGE->set_popup_notification_allowed(false);
+            $reloadurl = new moodle_url('/admin/index.php', array('cache' => 0));
+            echo $output->unsatisfied_dependencies_page($version, $failed, $reloadurl);
             die();
         }
+        unset($failed);
     }
+
     // install/upgrade all plugins and other parts
     upgrade_noncore(true);
 }
@@ -477,6 +486,17 @@ if (during_initial_install()) {
     upgrade_finished('upgradesettings.php');
 }
 
+if (has_capability('moodle/site:config', context_system::instance())) {
+    if ($fetchupdates) {
+        require_sesskey();
+        $updateschecker = \core\update\checker::instance();
+        if ($updateschecker->enabled()) {
+            $updateschecker->fetch();
+        }
+        redirect(new moodle_url('/admin/index.php', array('cache' => 0)));
+    }
+}
+
 // Now we can be sure everything was upgraded and caches work fine,
 // redirect if necessary to make sure caching is enabled.
 if (!$cache) {
@@ -564,12 +584,6 @@ $registered = $DB->count_records('registration_hubs', array('huburl' => HUB_MOOD
 
 admin_externalpage_setup('adminnotifications');
 
-if ($fetchupdates) {
-    require_sesskey();
-    $updateschecker->fetch();
-    redirect(new moodle_url('/admin/index.php'));
-}
-
 $output = $PAGE->get_renderer('core', 'admin');
 echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed,
         $cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
index f86097b..3ba44bb 100644 (file)
@@ -140,7 +140,7 @@ class core_admin_renderer extends plugin_renderer_base {
     public function upgrade_confirm_page($strnewversion, $maturity, $testsite) {
         $output = '';
 
-        $continueurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1));
+        $continueurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'cache' => 0));
         $continue = new single_button($continueurl, get_string('continue'), 'get');
         $cancelurl = new moodle_url('/admin/index.php');
 
@@ -170,7 +170,7 @@ class core_admin_renderer extends plugin_renderer_base {
         $output .= $this->environment_check_table($envstatus, $environment_results);
 
         if (!$envstatus) {
-            $output .= $this->upgrade_reload(new moodle_url('/admin/index.php'), array('confirmupgrade' => 1));
+            $output .= $this->upgrade_reload(new moodle_url('/admin/index.php'), array('confirmupgrade' => 1, 'cache' => 0));
 
         } else {
             $output .= $this->notification(get_string('environmentok', 'admin'), 'notifysuccess');
@@ -179,7 +179,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 $output .= $this->box(get_string('langpackwillbeupdated', 'admin'), 'generalbox', 'notice');
             }
 
-            $output .= $this->continue_button(new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1)));
+            $output .= $this->continue_button(new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0)));
         }
 
         $output .= $this->footer();
@@ -711,7 +711,7 @@ class core_admin_renderer extends plugin_renderer_base {
         }
 
         $updateinfo .= $this->container_start('checkforupdates');
-        $fetchurl = new moodle_url('/admin/index.php', array('fetchupdates' => 1, 'sesskey' => sesskey(), 'cache' => 1));
+        $fetchurl = new moodle_url('/admin/index.php', array('fetchupdates' => 1, 'sesskey' => sesskey(), 'cache' => 0));
         $updateinfo .= $this->single_button($fetchurl, get_string('checkforupdates', 'core_plugin'));
         if ($fetch) {
             $updateinfo .= $this->container(get_string('checkforupdateslast', 'core_plugin',
@@ -962,7 +962,7 @@ class core_admin_renderer extends plugin_renderer_base {
             $out .= $this->output->heading(get_string('nonehighlighted', 'core_plugin'));
             if (empty($options['full'])) {
                 $out .= html_writer::link(new moodle_url('/admin/index.php',
-                    array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 1)),
+                    array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 1, 'cache' => 0)),
                     get_string('nonehighlightedinfo', 'core_plugin'));
             }
             $out .= $this->output->container_end();
@@ -972,11 +972,11 @@ class core_admin_renderer extends plugin_renderer_base {
             $out .= $this->output->heading(get_string('somehighlighted', 'core_plugin', $sumofhighlighted));
             if (empty($options['full'])) {
                 $out .= html_writer::link(new moodle_url('/admin/index.php',
-                    array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 1)),
+                    array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 1, 'cache' => 0)),
                     get_string('somehighlightedinfo', 'core_plugin'));
             } else {
                 $out .= html_writer::link(new moodle_url('/admin/index.php',
-                    array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 0)),
+                    array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 0, 'cache' => 0)),
                     get_string('somehighlightedonly', 'core_plugin'));
             }
             $out .= $this->output->container_end();
index 6505c96..2cf6f0f 100644 (file)
@@ -41,8 +41,11 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) { // sp
     }
 
 
-    // calendar
+    // Calendar settings.
     $temp = new admin_settingpage('calendar', new lang_string('calendarsettings','admin'));
+
+    $temp->add(new admin_setting_configselect('calendartype', new lang_string('calendartype', 'admin'),
+        new lang_string('calendartype_desc', 'admin'), 'gregorian', \core_calendar\type_factory::get_list_of_calendar_types()));
     $temp->add(new admin_setting_special_adminseesall());
     //this is hacky because we do not want to include the stuff from calendar/lib.php
     $temp->add(new admin_setting_configselect('calendar_site_timeformat', new lang_string('pref_timeformat', 'calendar'),
index 13d5f2f..58b7775 100644 (file)
@@ -59,7 +59,7 @@ class behat_admin extends behat_base {
             // We expect admin block to be visible, otherwise go to homepage.
             if (!$this->getSession()->getPage()->find('css', '.block_settings')) {
                 $this->getSession()->visit($this->locate_path('/'));
-                $this->wait(self::TIMEOUT, '(document.readyState === "complete")');
+                $this->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
             }
 
             // Search by label.
@@ -68,7 +68,7 @@ class behat_admin extends behat_base {
             $submitsearch = $this->find('css', 'form.adminsearchform input[type=submit]');
             $submitsearch->press();
 
-            $this->wait(self::TIMEOUT, '(document.readyState === "complete")');
+            $this->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
 
             // Admin settings does not use the same DOM structure than other moodle forms
             // but we also need to use lib/behat/form_field/* to deal with the different moodle form elements.
index ae9d83b..b276f44 100644 (file)
@@ -19,7 +19,7 @@ Feature: Upload users
     And I expand "Users" node
     And I expand "Accounts" node
     And I follow "Upload users"
-    When I upload "lib/tests/fixtures/upload_users.csv" file to "File" filepicker
+    When I upload "lib/tests/fixtures/upload_users.csv" file to "File" filemanager
     And I press "Upload users"
     Then I should see "Upload users preview"
     And I should see "Tom"
@@ -42,5 +42,4 @@ Feature: Upload users
     And I expand "Users" node
     And I follow "Groups"
     And I select "Section 1 (1)" from "groups"
-    And I wait "4" seconds
     And the "members" select box should contain "Tom Jones"
index 5b1bd50..83f0320 100644 (file)
@@ -39,7 +39,7 @@ Feature: Page contents assertions
       | Course 1 | C1 | 0 |
     And I log in as "admin"
     And I follow "Course 1"
-    When I click on "Move this to the dock" "button" in the "Administration" "block"
+    When I dock "Administration" block
     Then I should not see "Question bank" in the "region-pre" "region"
     And I click on "//div[@id='dock']/descendant::h2[normalize-space(.)='Administration']" "xpath_element"
 
@@ -49,5 +49,5 @@ Feature: Page contents assertions
       | fullname | shortname | category |
       | Course 1 | C1 | 0 |
     And I log in as "admin"
-    When I click on "Move this to the dock" "button" in the "Administration" "block"
+    When I dock "Administration" block
     Then I should not see "Turn editing on" in the "region-pre" "region"
index 063e198..8f21a72 100644 (file)
@@ -230,8 +230,6 @@ Feature: Set up contextual data for tests
     Then the "groups" select box should contain "Group 1 (1)"
     And the "groups" select box should contain "Group 2 (1)"
     And I select "Group 1 (1)" from "groups"
-    And I wait "5" seconds
     And the "members" select box should contain "Student 1"
     And I select "Group 2 (1)" from "groups"
-    And I wait "5" seconds
     And the "members" select box should contain "Student 2"
diff --git a/admin/tool/replace/classes/form.php b/admin/tool/replace/classes/form.php
new file mode 100644 (file)
index 0000000..5c60510
--- /dev/null
@@ -0,0 +1,69 @@
+<?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/>.
+
+/**
+ * Site wide search-replace form.
+ *
+ * @package    tool_replace
+ * @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();
+
+require_once("$CFG->libdir/formslib.php");
+
+/**
+ * Site wide search-replace form.
+ */
+class tool_replace_form extends moodleform {
+    function definition() {
+        global $CFG, $DB;
+
+        $mform = $this->_form;
+
+        $mform->addElement('header', 'searchhdr', get_string('pluginname', 'tool_replace'));
+        $mform->setExpanded('searchhdr', true);
+
+        $mform->addElement('text', 'search', get_string('searchwholedb', 'tool_replace'), 'size="50"');
+        $mform->setType('search', PARAM_RAW);
+        $mform->addElement('static', 'searchst', '', get_string('searchwholedbhelp', 'tool_replace'));
+        $mform->addRule('search', get_string('required'), 'required', null, 'client');
+
+        $mform->addElement('text', 'replace', get_string('replacewith', 'tool_replace'), 'size="50"', PARAM_RAW);
+        $mform->addElement('static', 'replacest', '', get_string('replacewithhelp', 'tool_replace'));
+        $mform->setType('replace', PARAM_RAW);
+        $mform->addElement('checkbox', 'shorten', get_string('shortenoversized', 'tool_replace'));
+        $mform->addRule('replace', get_string('required'), 'required', null, 'client');
+
+        $mform->addElement('header', 'confirmhdr', get_string('confirm'));
+        $mform->setExpanded('confirmhdr', true);
+        $mform->addElement('checkbox', 'sure', get_string('disclaimer', 'tool_replace'));
+        $mform->addRule('sure', get_string('required'), 'required', null, 'client');
+
+        $this->add_action_buttons(false, get_string('doit', 'tool_replace'));
+    }
+
+    function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+
+        if (empty($data['shorten']) and core_text::strlen($data['search']) < core_text::strlen($data['replace'])) {
+            $errors['shorten'] = get_string('required');
+        }
+
+        return $errors;
+    }
+}
index 67d945e..b3013d2 100644 (file)
@@ -17,8 +17,7 @@
 /**
  * Search and replace strings throughout all texts in the whole database
  *
- * @package    tool
- * @subpackage replace
+ * @package    tool_replace
  * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -31,56 +30,38 @@ require_once($CFG->libdir.'/adminlib.php');
 
 admin_externalpage_setup('toolreplace');
 
-$search  = optional_param('search', '', PARAM_RAW);
-$replace = optional_param('replace', '', PARAM_RAW);
-$sure    = optional_param('sure', 0, PARAM_BOOL);
-
-###################################################################
 echo $OUTPUT->header();
 
 echo $OUTPUT->heading(get_string('pageheader', 'tool_replace'));
 
-if ($DB->get_dbfamily() !== 'mysql' and $DB->get_dbfamily() !== 'postgres') {
-    //TODO: add $DB->text_replace() to DML drivers
+if (!$DB->replace_all_text_supported()) {
     echo $OUTPUT->notification(get_string('notimplemented', 'tool_replace'));
     echo $OUTPUT->footer();
     die;
 }
 
-if (!data_submitted() or !$search or !$replace or !confirm_sesskey() or !$sure) {   /// Print a form
-    echo $OUTPUT->notification(get_string('notsupported', 'tool_replace'));
-    echo $OUTPUT->notification(get_string('excludedtables', 'tool_replace'));
+echo $OUTPUT->box_start();
+echo $OUTPUT->notification(get_string('notsupported', 'tool_replace'));
+echo $OUTPUT->notification(get_string('excludedtables', 'tool_replace'));
+echo $OUTPUT->box_end();
+
+$form = new tool_replace_form();
 
-    echo $OUTPUT->box_start();
-    echo '<div class="mdl-align">';
-    echo '<form action="index.php" method="post"><div>';
-    echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
-    echo '<div><label for="search">'.get_string('searchwholedb', 'tool_replace').
-            ' </label><input id="search" type="text" name="search" size="40" /> ('.
-            get_string('searchwholedbhelp', 'tool_replace').')</div>';
-    echo '<div><label for="replace">'.get_string('replacewith', 'tool_replace').
-            ' </label><input type="text" id="replace" name="replace" size="40" /> ('.
-            get_string('replacewithhelp', 'tool_replace').')</div>';
-    echo '<div><label for="sure">'.get_string('disclaimer', 'tool_replace').' </label><input type="checkbox" id="sure" name="sure" value="1" /></div>';
-    echo '<div class="buttons"><input type="submit" class="singlebutton" value="Yes, do it now" /></div>';
-    echo '</div></form>';
-    echo '</div>';
-    echo $OUTPUT->box_end();
+if (!$data = $form->get_data()) {
+    $form->display();
     echo $OUTPUT->footer();
-    die;
+    die();
 }
 
+// Scroll to the end when finished.
+$PAGE->requires->js_init_code("window.scrollTo(0, 5000000);");
+
 echo $OUTPUT->box_start();
-db_replace($search, $replace);
+db_replace($data->search, $data->replace);
 echo $OUTPUT->box_end();
 
-/// Rebuild course cache which might be incorrect now
-echo $OUTPUT->notification(get_string('notifyrebuilding', 'tool_replace'), 'notifysuccess');
-rebuild_course_cache();
-echo $OUTPUT->notification(get_string('notifyfinished', 'tool_replace'), 'notifysuccess');
+// Course caches are now rebuilt on the fly.
 
 echo $OUTPUT->continue_button(new moodle_url('/admin/index.php'));
 
 echo $OUTPUT->footer();
-
-
index 9103e89..919ed67 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['disclaimer'] = 'I understand the risks of this operation:';
+$string['cannotfit'] = 'The replacement is longer than original and shortening is not allow, cannot continue.';
+$string['disclaimer'] = 'I understand the risks of this operation';
+$string['doit'] = 'Yes, do it!';
 $string['excludedtables'] = 'Several tables are not updated as part of the text replacement. This include configuration, log, events, and session tables.';
 $string['pageheader'] = 'Search and replace text throughout the whole database';
 $string['notifyfinished'] = '...finished';
 $string['notifyrebuilding'] = 'Rebuilding course cache...';
-$string['notimplemented'] = 'Sorry, this feature is implemented only for MySQL and PostgreSQL databases.';
+$string['notimplemented'] = 'Sorry, this feature is not implemented in your database driver.';
 $string['notsupported'] ='This script is not supported, always make complete backup before proceeding!<br />This operation can not be reverted!';
 $string['pluginname'] = 'DB search and replace';
-$string['replacewith'] = 'Replace with this string:';
+$string['replacewith'] = 'Replace with this string';
 $string['replacewithhelp'] = 'usually new server URL';
-$string['searchwholedb'] = 'Search whole database for:';
+$string['searchwholedb'] = 'Search whole database for';
 $string['searchwholedbhelp'] = 'usually previous server URL';
+$string['shortenoversized'] = 'Shorten result if necessary';
index 4496dcd..307be8f 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2013110500; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2013110501; // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2013110500; // Requires this Moodle version
 $plugin->component = 'tool_replace'; // Full name of the plugin (used for diagnostics)
 
index ee95ca1..ce2c98b 100644 (file)
@@ -15,7 +15,7 @@ Feature: An admin can create courses using a CSV file
 
   @javascript
   Scenario: Creation of unexisting courses
-    Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filepicker
+    Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filemanager
     And I click on "Preview" "button"
     When I click on "Upload courses" "button"
     Then I should see "The course exists and update is not allowed"
@@ -29,7 +29,7 @@ Feature: An admin can create courses using a CSV file
 
   @javascript
   Scenario: Creation of existing courses
-    Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filepicker
+    Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filemanager
     And I select "Create all, increment shortname if needed" from "Upload mode"
     And I click on "Preview" "button"
     When I click on "Upload courses" "button"
index 3a06d16..457f07d 100644 (file)
@@ -15,7 +15,7 @@ Feature: An admin can update courses using a CSV file
 
   @javascript
   Scenario: Updating a course fullname
-    Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filepicker
+    Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filemanager
     And I select "Only update existing courses" from "Upload mode"
     And I select "Update with CSV data only" from "Update mode"
     And I click on "Preview" "button"
index 3aef57c..dea693b 100644 (file)
@@ -207,12 +207,14 @@ class web_service_token_form extends moodleform {
 
             if ($usertotal < 500) {
                 list($sort, $params) = users_order_by_sql('u');
-                //user searchable selector - get all users (admin and guest included)
-                //user must be confirmed, not deleted, not suspended, not guest
-                $sql = "SELECT u.id, u.firstname, u.lastname
-                            FROM {user} u
-                            WHERE u.deleted = 0 AND u.confirmed = 1 AND u.suspended = 0 AND u.id != :siteguestid
-                            ORDER BY $sort";
+                // User searchable selector - return users who are confirmed, not deleted, not suspended and not a guest.
+                $sql = 'SELECT u.id, ' . get_all_user_name_fields(true, 'u') . '
+                        FROM {user} u
+                        WHERE u.deleted = 0
+                        AND u.confirmed = 1
+                        AND u.suspended = 0
+                        AND u.id != :siteguestid
+                        ORDER BY ' . $sort;
                 $params['siteguestid'] = $CFG->siteguest;
                 $users = $DB->get_records_sql($sql, $params);
                 $options = array();
index d2087f7..3b80e26 100644 (file)
@@ -399,6 +399,9 @@ class auth_plugin_db extends auth_plugin_base {
                 if (empty($user->lang)) {
                     $user->lang = $CFG->lang;
                 }
+                if (empty($user->calendartype)) {
+                    $user->calendartype = $CFG->calendartype;
+                }
                 $user->timecreated = time();
                 $user->timemodified = $user->timecreated;
                 if ($collision = $DB->get_record_select('user', "username = :username AND mnethostid = :mnethostid AND auth <> :auth", array('username'=>$user->username, 'mnethostid'=>$CFG->mnet_localhost_id, 'auth'=>$this->authtype), 'id,username,auth')) {
index a670cfc..b071e32 100644 (file)
@@ -90,6 +90,9 @@ class auth_plugin_email extends auth_plugin_base {
         require_once($CFG->dirroot.'/user/lib.php');
 
         $user->password = hash_internal_user_password($user->password);
+        if (empty($user->calendartype)) {
+            $user->calendartype = $CFG->calendartype;
+        }
 
         $user->id = user_create_user($user, false);
 
index a3fc3a8..a847112 100644 (file)
@@ -945,6 +945,9 @@ class auth_plugin_ldap extends auth_plugin_base {
                 if (empty($user->lang)) {
                     $user->lang = $CFG->lang;
                 }
+                if (empty($user->calendartype)) {
+                    $user->calendartype = $CFG->calendartype;
+                }
 
                 $id = user_create_user($user, false);
                 echo "\t"; print_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)); echo "\n";
index 1c1cf29..0f3bf7a 100644 (file)
@@ -130,6 +130,13 @@ if ($backup->get_stage() == backup_ui::STAGE_FINAL) {
     // Carry out actual backup.
     $backup->execute();
 
+    // Backup controller gets saved/loaded so the logger object changes and we
+    // have to retrieve it.
+    $logger = $backup->get_controller()->get_logger();
+    while (!is_a($logger, 'core_backup_html_logger')) {
+        $logger = $logger->get_next();
+    }
+
     // Get HTML from logger.
     $loghtml = $logger->get_html();
 
index 12929de..b5c32a5 100644 (file)
@@ -107,7 +107,19 @@ class file_nested_element extends backup_nested_element {
             backup_file_manager::copy_file_moodle2backup($this->backupid, $values);
         } catch (file_exception $e) {
             $this->add_result(array('missing_files_in_pool' => true));
-            $this->add_log('missing file in pool: ' . $e->debuginfo, backup::LOG_WARNING);
+
+            // Build helpful log message with all information necessary to identify
+            // file location.
+            $context = context::instance_by_id($values->contextid, IGNORE_MISSING);
+            $contextname = '';
+            if ($context) {
+                $contextname = ' \'' . $context->get_context_name() . '\'';
+            }
+            $message = 'Missing file in pool: ' . $values->filepath  . $values->filename .
+                    ' (context ' . $values->contextid . $contextname . ', component ' .
+                    $values->component . ', filearea ' . $values->filearea . ', itemid ' .
+                    $values->itemid . ') [' . $e->debuginfo . ']';
+            $this->add_log($message, backup::LOG_WARNING);
         }
     }
 }
index a6d5bce..00fb8f3 100644 (file)
@@ -995,11 +995,7 @@ abstract class restore_dbops {
                 if ($includesfiles) {
                     // The file is not found in the backup.
                     if (!file_exists($backuppath)) {
-                        $result = new stdClass();
-                        $result->code = 'file_missing_in_backup';
-                        $result->message = sprintf('missing file %s%s in backup', $file->filepath, $file->filename);
-                        $result->level = backup::LOG_WARNING;
-                        $results[] = $result;
+                        $results[] = self::get_missing_file_result($file);
                         continue;
                     }
 
@@ -1028,11 +1024,7 @@ abstract class restore_dbops {
                             $fs->create_file_from_storedfile($file_record, $foundfile->id);
                         } else {
                             // A matching existing file record was not found in the database.
-                            $result = new stdClass();
-                            $result->code = 'file_missing_in_backup';
-                            $result->message = sprintf('missing file %s%s in backup', $file->filepath, $file->filename);
-                            $result->level = backup::LOG_WARNING;
-                            $results[] = $result;
+                            $results[] = self::get_missing_file_result($file);
                             continue;
                         }
                     }
@@ -1063,6 +1055,22 @@ abstract class restore_dbops {
         return $results;
     }
 
+    /**
+     * Returns suitable entry to include in log when there is a missing file.
+     *
+     * @param stdClass $file File definition
+     * @return stdClass Log entry
+     */
+    protected static function get_missing_file_result($file) {
+        $result = new stdClass();
+        $result->code = 'file_missing_in_backup';
+        $result->message = 'Missing file in backup: ' . $file->filepath  . $file->filename .
+                ' (old context ' . $file->contextid . ', component ' . $file->component .
+                ', filearea ' . $file->filearea . ', old itemid ' . $file->itemid . ')';
+        $result->level = backup::LOG_WARNING;
+        return $result;
+    }
+
     /**
      * Given one restoreid, create in DB all the users present
      * in backup_ids having newitemid = 0, as far as
index 762ea70..262142e 100644 (file)
@@ -348,12 +348,12 @@ abstract class base_moodleform extends moodleform {
         $config->yesLabel = get_string('confirmcancelyes', 'backup');
         $config->noLabel = get_string('confirmcancelno', 'backup');
         $config->closeButtonTitle = get_string('close', 'editor');
-        $PAGE->requires->yui_module('moodle-backup-confirmcancel', 'M.core_backup.watch_cancel_buttons', array($config));
+        $PAGE->requires->yui_module('moodle-backup-confirmcancel', 'M.core_backup.confirmcancel.watch_cancel_buttons', array($config));
 
         // Get list of module types on course.
         $modinfo = get_fast_modinfo($COURSE);
         $modnames = $modinfo->get_used_module_names(true);
-        $PAGE->requires->yui_module('moodle-backup-backupselectall', 'M.core_backup.select_all_init',
+        $PAGE->requires->yui_module('moodle-backup-backupselectall', 'M.core_backup.backupselectall',
                 array($modnames));
         $PAGE->requires->strings_for_js(array('select', 'all', 'none'), 'moodle');
         $PAGE->requires->strings_for_js(array('showtypes', 'hidetypes'), 'backup');
index ddc406e..60b1871 100644 (file)
@@ -38,7 +38,7 @@ Feature: Backup Moodle courses
     And I press "Continue"
     And I click on "Continue" "button" in the ".bcs-current-course" "css_element"
     And "//div[contains(concat(' ', normalize-space(@class), ' '), ' fitem ')][contains(., 'Include calendar events')]/descendant::img" "xpath_element" should exists
-    And I check "Include course logs"
+    And "Include course logs" "checkbox" should exists
     And I press "Next"
 
   @javascript
index d06185e..84fb672 100644 (file)
@@ -56,27 +56,32 @@ class behat_backup extends behat_base {
 
         // Go to homepage.
         $this->getSession()->visit($this->locate_path('/'));
+        $this->wait();
 
         // Click the course link.
         $this->find_link($backupcourse)->click();
+        $this->wait();
 
         // Click the backup link.
         $this->find_link(get_string('backup'))->click();
+        $this->wait();
 
         // Initial settings.
         $this->fill_backup_restore_form($options);
         $this->find_button(get_string('backupstage1action', 'backup'))->press();
+        $this->wait();
 
         // Schema settings.
         $this->fill_backup_restore_form($options);
         $this->find_button(get_string('backupstage2action', 'backup'))->press();
+        $this->wait();
 
         // Confirmation and review, backup filename can also be specified.
         $this->fill_backup_restore_form($options);
         $this->find_button(get_string('backupstage4action', 'backup'))->press();
 
         // Waiting for it to finish.
-        $this->wait(10);
+        $this->wait(self::EXTENDED_TIMEOUT);
 
         // Last backup continue button.
         $this->find_button(get_string('backupstage16action', 'backup'))->press();
@@ -101,12 +106,15 @@ class behat_backup extends behat_base {
 
         // Go to homepage.
         $this->getSession()->visit($this->locate_path('/'));
+        $this->wait();
 
         // Click the course link.
         $this->find_link($tocourse)->click();
+        $this->wait();
 
         // Click the import link.
         $this->find_link(get_string('import'))->click();
+        $this->wait();
 
         // Select the course.
         $exception = new ExpectationException('"' . $fromcourse . '" course not found in the list of courses to import from', $this->getSession());
@@ -121,18 +129,21 @@ class behat_backup extends behat_base {
         $radionode->click();
 
         $this->find_button(get_string('continue'))->press();
+        $this->wait();
 
         // Initial settings.
         $this->fill_backup_restore_form($options);
         $this->find_button(get_string('importbackupstage1action', 'backup'))->press();
+        $this->wait();
 
         // Schema settings.
         $this->fill_backup_restore_form($options);
         $this->find_button(get_string('importbackupstage2action', 'backup'))->press();
+        $this->wait();
 
         // Run it.
         $this->find_button(get_string('importbackupstage4action', 'backup'))->press();
-        $this->wait();
+        $this->wait(self::EXTENDED_TIMEOUT);
 
         // Continue and redirect to 'to' course.
         $this->find_button(get_string('continue'))->press();
@@ -294,17 +305,22 @@ class behat_backup extends behat_base {
         // Settings.
         $this->fill_backup_restore_form($options);
         $this->find_button(get_string('restorestage4action', 'backup'))->press();
+        $this->wait();
 
         // Schema.
         $this->fill_backup_restore_form($options);
         $this->find_button(get_string('restorestage8action', 'backup'))->press();
+        $this->wait();
 
         // Review, no options here.
         $this->find_button(get_string('restorestage16action', 'backup'))->press();
-        $this->wait(10);
+        $this->wait();
 
         // Last restore continue button, redirected to restore course after this.
         $this->find_button(get_string('restorestage32action', 'backup'))->press();
+
+        // Long wait when waiting for the restore to finish.
+        $this->wait(self::EXTENDED_TIMEOUT);
     }
 
     /**
@@ -325,11 +341,15 @@ class behat_backup extends behat_base {
             return;
         }
 
+        // Wait for the page to be loaded and the JS ready.
+        $this->wait();
+
         // If we find any of the provided options in the current form we should set the value.
         $datahash = $options->getRowsHash();
         foreach ($datahash as $locator => $value) {
 
             try {
+                // Using $this->find* to enforce stability over speed.
                 $fieldnode = $this->find_field($locator);
                 $field = behat_field_manager::get_form_field($fieldnode, $this->getSession());
                 $field->set_value($value);
@@ -341,9 +361,9 @@ class behat_backup extends behat_base {
     }
 
     /**
-     * Waits until the DOM is ready.
+     * Waits until the DOM and the page Javascript code is ready.
      *
-     * @param int To override the default timeout
+     * @param int $timeout The number of seconds that we wait.
      * @return void
      */
     protected function wait($timeout = false) {
@@ -355,7 +375,8 @@ class behat_backup extends behat_base {
         if (!$timeout) {
             $timeout = self::TIMEOUT;
         }
-        $this->getSession()->wait($timeout, '(document.readyState === "complete")');
+
+        $this->getSession()->wait($timeout * 1000, self::PAGE_READY_JS);
     }
 
 }
index 9675a62..3314372 100644 (file)
@@ -21,8 +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 |
-    And I open "Test database name" actions menu
-    When I click on "Duplicate" "link" in the "Test database name" activity
+    And I duplicate "Test database name" activity
+    And I wait until section "1" is available
     And I open "Test database name" actions menu
     And I click on "Edit settings" "link" in the "Test database name" activity
     And I fill the moodle form with:
diff --git a/backup/util/ui/yui/build/moodle-backup-backupselectall/moodle-backup-backupselectall-debug.js b/backup/util/ui/yui/build/moodle-backup-backupselectall/moodle-backup-backupselectall-debug.js
new file mode 100644 (file)
index 0000000..3de94d5
Binary files /dev/null and b/backup/util/ui/yui/build/moodle-backup-backupselectall/moodle-backup-backupselectall-debug.js differ
diff --git a/backup/util/ui/yui/build/moodle-backup-backupselectall/moodle-backup-backupselectall-min.js b/backup/util/ui/yui/build/moodle-backup-backupselectall/moodle-backup-backupselectall-min.js
new file mode 100644 (file)
index 0000000..b001213
Binary files /dev/null and b/backup/util/ui/yui/build/moodle-backup-backupselectall/moodle-backup-backupselectall-min.js differ
diff --git a/backup/util/ui/yui/build/moodle-backup-backupselectall/moodle-backup-backupselectall.js b/backup/util/ui/yui/build/moodle-backup-backupselectall/moodle-backup-backupselectall.js
new file mode 100644 (file)
index 0000000..3de94d5
Binary files /dev/null and b/backup/util/ui/yui/build/moodle-backup-backupselectall/moodle-backup-backupselectall.js differ
diff --git a/backup/util/ui/yui/build/moodle-backup-confirmcancel/moodle-backup-confirmcancel-debug.js b/backup/util/ui/yui/build/moodle-backup-confirmcancel/moodle-backup-confirmcancel-debug.js
new file mode 100644 (file)
index 0000000..173f290
Binary files /dev/null and b/backup/util/ui/yui/build/moodle-backup-confirmcancel/moodle-backup-confirmcancel-debug.js differ
diff --git a/backup/util/ui/yui/build/moodle-backup-confirmcancel/moodle-backup-confirmcancel-min.js b/backup/util/ui/yui/build/moodle-backup-confirmcancel/moodle-backup-confirmcancel-min.js
new file mode 100644 (file)
index 0000000..f19f1a9
Binary files /dev/null and b/backup/util/ui/yui/build/moodle-backup-confirmcancel/moodle-backup-confirmcancel-min.js differ
diff --git a/backup/util/ui/yui/build/moodle-backup-confirmcancel/moodle-backup-confirmcancel.js b/backup/util/ui/yui/build/moodle-backup-confirmcancel/moodle-backup-confirmcancel.js
new file mode 100644 (file)
index 0000000..173f290
Binary files /dev/null and b/backup/util/ui/yui/build/moodle-backup-confirmcancel/moodle-backup-confirmcancel.js differ
diff --git a/backup/util/ui/yui/confirmcancel/confirmcancel.js b/backup/util/ui/yui/confirmcancel/confirmcancel.js
deleted file mode 100644 (file)
index 77deaf1..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-YUI.add('moodle-backup-confirmcancel', function(Y) {
-
-// Namespace for the backup
-M.core_backup = M.core_backup || {};
-/**
- * Adds confirmation dialogues to the cancel buttons on the page.
- *
- * @param {object} config
- */
-M.core_backup.watch_cancel_buttons = function(config) {
-    Y.all('.confirmcancel').each(function(){
-        this._confirmationListener = this._confirmationListener || this.on('click', function(e){
-            // Prevent the default event (sumbit) from firing
-            e.preventDefault();
-            // Create the confirm box
-            var confirm = new M.core.confirm(config);
-            // If the user clicks yes
-            confirm.on('complete-yes', function(e){
-                // Detach the listener for the confirm box so it doesn't fire again.
-                this._confirmationListener.detach();
-                // Simulate the original cancel button click
-                this.simulate('click');
-            }, this);
-            // Show the confirm box
-            confirm.show();
-        }, this);
-    });
-}
-
-}, '@VERSION@', {'requires':['base','node','node-event-simulate','moodle-core-notification']});
diff --git a/backup/util/ui/yui/src/backupselectall/build.json b/backup/util/ui/yui/src/backupselectall/build.json
new file mode 100644 (file)
index 0000000..85b6072
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "name": "moodle-backup-backupselectall",
+  "builds": {
+    "moodle-backup-backupselectall": {
+      "jsfiles": [
+        "backupselectall.js"
+      ]
+    }
+  }
+}
@@ -1,12 +1,18 @@
-YUI.add('moodle-backup-backupselectall', function(Y) {
+/**
+ * Adds select all/none links to the top of the backup/restore/import schema page.
+ *
+ * @module moodle-backup-backupselectall
+ */
 
 // Namespace for the backup
 M.core_backup = M.core_backup || {};
 
 /**
  * Adds select all/none links to the top of the backup/restore/import schema page.
+ *
+ * @class M.core_backup.backupselectall
  */
-M.core_backup.select_all_init = function(modnames) {
+M.core_backup.backupselectall = function(modnames) {
     var formid = null;
 
     var helper = function(e, check, type, mod) {
@@ -23,7 +29,7 @@ M.core_backup.select_all_init = function(modnames) {
             if (prefix && name.substring(0, prefix.length) !== prefix) {
                 return;
             }
-            if (name.substring(name.length - len) == type) {
+            if (name.substring(name.length - len) === type) {
                 checkbox.set('checked', check);
             }
         });
@@ -67,9 +73,9 @@ M.core_backup.select_all_init = function(modnames) {
     var withuserdata = false;
     Y.all('input[type="checkbox"]').each(function(checkbox) {
         var name = checkbox.get('name');
-        if (name.substring(name.length - 9) == '_userdata') {
+        if (name.substring(name.length - 9) === '_userdata') {
             withuserdata = '_userdata';
-        } else if (name.substring(name.length - 9) == '_userinfo') {
+        } else if (name.substring(name.length - 9) === '_userinfo') {
             withuserdata = '_userinfo';
         }
     });
@@ -103,7 +109,7 @@ M.core_backup.select_all_init = function(modnames) {
         if (!modnames.hasOwnProperty(mod)) {
             continue;
         }
-        var html = html_generator('include_setting section_level', 'mod_' + mod, modnames[mod]);
+        html = html_generator('include_setting section_level', 'mod_' + mod, modnames[mod]);
         if (withuserdata) {
             html += html_generator('normal_setting', 'userdata-mod_' + mod, modnames[mod]);
         }
@@ -127,26 +133,27 @@ M.core_backup.select_all_init = function(modnames) {
         modlist.currentlyshown = !modlist.currentlyshown;
 
         // Either hide or show the links.
-        var animcfg = { node: modlist, duration: 0.2 };
+        var animcfg = { node: modlist, duration: 0.2 },
+            anim;
         if (modlist.currentlyshown) {
             // Animate reveal of the module links.
             modlist.show();
             animcfg.to = { maxHeight: modlist.get('clientHeight') + 'px' };
             modlist.setStyle('maxHeight', '0px');
-            var anim = new Y.Anim(animcfg);
+            anim = new Y.Anim(animcfg);
             anim.on('end', function() { modlist.setStyle('maxHeight', 'none'); });
             anim.run();
         } else {
             // Animate hide of the module links.
             animcfg.to = { maxHeight: '0px' };
             modlist.setStyle('maxHeight', modlist.get('clientHeight') + 'px');
-            var anim = new Y.Anim(animcfg);
+            anim = new Y.Anim(animcfg);
             anim.on('end', function() { modlist.hide(); modlist.setStyle('maxHeight', 'none'); });
             anim.run();
         }
 
     };
-    Y.one('#backup-bytype').on('click', function(e) { toggletypes(); });
+    Y.one('#backup-bytype').on('click', function() { toggletypes(); });
 
     Y.one('#backup-all-included').on('click',  function(e) { helper(e, true,  '_included'); });
     Y.one('#backup-none-included').on('click', function(e) { helper(e, false, '_included'); });
@@ -154,6 +161,4 @@ M.core_backup.select_all_init = function(modnames) {
         Y.one('#backup-all-userdata').on('click',  function(e) { helper(e, true,  withuserdata); });
         Y.one('#backup-none-userdata').on('click', function(e) { helper(e, false, withuserdata); });
     }
-}
-
-}, '@VERSION@', {'requires':['base', 'node', 'event', 'node-event-simulate', 'anim']});
+};
diff --git a/backup/util/ui/yui/src/backupselectall/js/backupselectall.json b/backup/util/ui/yui/src/backupselectall/js/backupselectall.json
new file mode 100644 (file)
index 0000000..81f6ce7
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "moodle-backup-backupselectall": {
+    "requires": [
+        "node",
+        "event",
+        "node-event-simulate",
+        "anim"
+    ]
+  }
+}
diff --git a/backup/util/ui/yui/src/backupselectall/meta/backupselectall.json b/backup/util/ui/yui/src/backupselectall/meta/backupselectall.json
new file mode 100644 (file)
index 0000000..81f6ce7
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "moodle-backup-backupselectall": {
+    "requires": [
+        "node",
+        "event",
+        "node-event-simulate",
+        "anim"
+    ]
+  }
+}
diff --git a/backup/util/ui/yui/src/confirmcancel/build.json b/backup/util/ui/yui/src/confirmcancel/build.json
new file mode 100644 (file)
index 0000000..603210c
--- /dev/null
@@ -0,0 +1,10 @@
+{
+    "name": "moodle-backup-confirmcancel",
+    "builds": {
+        "moodle-backup-confirmcancel": {
+            "jsfiles": [
+                "confirmcancel.js"
+            ]
+        }
+    }
+}
diff --git a/backup/util/ui/yui/src/confirmcancel/js/confirmcancel.js b/backup/util/ui/yui/src/confirmcancel/js/confirmcancel.js
new file mode 100644 (file)
index 0000000..ddaeb52
--- /dev/null
@@ -0,0 +1,77 @@
+/**
+ * Add a confirmation dialogue when cancelling a backup.
+ *
+ * @module moodle-backup-confirmcancel
+ */
+
+/**
+ * Add a confirmation dialogue when cancelling a backup.
+ *
+ * @class M.core_backup.confirmcancel
+ */
+
+
+// Namespace for the backup.
+M.core_backup = M.core_backup || {};
+
+M.core_backup.confirmcancel = {
+    /**
+     * An array of EventHandlers which call the confirm_cancel dialogue.
+     *
+     * @property listeners
+     * @protected
+     * @type Array
+     */
+    listeners: [],
+
+    /**
+     * The configuration supplied to this instance.
+     *
+     * @property config
+     * @protected
+     * @type Object
+     */
+    config: {},
+
+    /**
+     * Initializer to watch all cancel buttons.
+     *
+     * @method watch_cancel_buttons
+     * @param {Object} config The configuration for the confirmation dialogue.
+     */
+    watch_cancel_buttons: function(config) {
+        this.config = config;
+
+        this.listeners.push(
+            Y.one(Y.config.doc.body).delegate('click', this.confirm_cancel, '.confirmcancel', this)
+        );
+    },
+
+    /**
+     * Display the confirmation dialogue.
+     *
+     * @method confirm_cancel
+     * @protected
+     * @param {EventFacade} e
+     */
+    confirm_cancel: function(e) {
+        // Prevent the default event (submit) from firing.
+        e.preventDefault();
+
+        // Create the confirmation dialogue.
+        var confirm = new M.core.confirm(this.config);
+
+        // If the user clicks yes.
+        confirm.on('complete-yes', function(){
+            // Detach the listeners for the confirm box so they don't fire again.
+            new Y.EventHandle(M.core_backup.confirmcancel.listeners).detach();
+
+            // Simulate the original cancel button click.
+            c.currentTarget.simulate('click');
+        }, this);
+
+
+        // Show the confirm box.
+        confirm.show();
+    }
+};
diff --git a/backup/util/ui/yui/src/confirmcancel/meta/confirmcancel.json b/backup/util/ui/yui/src/confirmcancel/meta/confirmcancel.json
new file mode 100644 (file)
index 0000000..e352bdf
--- /dev/null
@@ -0,0 +1,9 @@
+{
+    "moodle-backup-confirmcancel": {
+        "requires": [
+            "node",
+            "node-event-simulate",
+            "moodle-core-notification-confirm"
+        ]
+    }
+}
index 9d4b5fa..64bf704 100644 (file)
@@ -36,7 +36,7 @@ Feature: Add badges to the system
       | Description | Test badge description |
       | issuername | Test Badge Site |
       | issuercontact | testuser@test-badge-site.com |
-    And I upload "badges/tests/behat/badge.png" file to "Image" filepicker
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     When I press "Create badge"
     Then I should see "Edit details"
     And I should see "Test Badge"
index d34d36e..458df34 100644 (file)
@@ -4,13 +4,10 @@ Feature: Award badges
   As an admin
   I need to add criteria to badges in the system
 
-  Background:
-    Given I am on homepage
-    And I log in as "admin"
-
   @javascript
   Scenario: Award profile badge
-    Given I expand "Site administration" node
+    Given I log in as "admin"
+    And I expand "Site administration" node
     And I expand "Badges" node
     And I follow "Add a new badge"
     And I fill the moodle form with:
@@ -18,7 +15,7 @@ Feature: Award badges
       | Description | Test badge description |
       | issuername | Test Badge Site |
       | issuercontact | testuser@test-badge-site.com |
-    And I upload "badges/tests/behat/badge.png" file to "Image" filepicker
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I select "Profile completion" from "type"
     And I check "First name"
@@ -46,6 +43,7 @@ Feature: Award badges
       | username | firstname | lastname | email |
       | teacher | teacher | 1 | teacher1@asd.com |
       | student | student | 1 | student1@asd.com |
+    And I log in as "admin"
     And I expand "Site administration" node
     And I expand "Badges" node
     And I follow "Add a new badge"
@@ -53,7 +51,7 @@ Feature: Award badges
       | Name | Site Badge |
       | Description | Site badge description |
       | issuername | Tester of site badge |
-    And I upload "badges/tests/behat/badge.png" file to "Image" filepicker
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I select "Manual issue by role" from "type"
     And I check "Teacher"
@@ -89,7 +87,6 @@ Feature: Award badges
       | teacher1 | C1 | editingteacher |
       | student1 | C1 | student |
       | student2 | C1 | student |
-    And I log out
     And I log in as "teacher1"
     And I follow "Course 1"
     And I click on "//span[text()='Badges']" "xpath_element" in the "Administration" "block"
@@ -98,7 +95,7 @@ Feature: Award badges
       | Name | Course Badge |
       | Description | Course badge description |
       | issuername | Tester of course badge |
-    And I upload "badges/tests/behat/badge.png" file to "Image" filepicker
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I select "Manual issue by role" from "type"
     And I check "Teacher"
@@ -133,7 +130,6 @@ Feature: Award badges
       | user | course | role |
       | teacher1 | C1 | editingteacher |
       | student1 | C1 | student |
-    And I log out
     And I log in as "admin"
     And I set the following administration settings values:
       | Enable completion tracking | 1 |
@@ -156,7 +152,7 @@ Feature: Award badges
       | Name | Course Badge |
       | Description | Course badge description |
       | issuername | Tester of course badge |
-    And I upload "badges/tests/behat/badge.png" file to "Image" filepicker
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I select "Activity completion" from "type"
     And I check "Test assignment name"
@@ -172,7 +168,6 @@ Feature: Award badges
     And I follow "Home"
     And I follow "Course 1"
     And I press "Mark as complete: Test assignment name"
-    And I wait "2" seconds
     And I expand "My profile" node
     And I follow "My badges"
     Then I should see "Course Badge"
@@ -190,7 +185,6 @@ Feature: Award badges
       | user | course | role |
       | teacher1 | C1 | editingteacher |
       | student1 | C1 | student |
-    And I log out
     And I log in as "admin"
     And I set the following administration settings values:
       | Enable completion tracking | 1 |
@@ -219,7 +213,7 @@ Feature: Award badges
       | Name | Course Badge |
       | Description | Course badge description |
       | issuername | Tester of course badge |
-    And I upload "badges/tests/behat/badge.png" file to "Image" filepicker
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I select "Course completion" from "type"
     And I fill the moodle form with:
index 1bee81e..e002c94 100644 (file)
@@ -83,6 +83,7 @@ class block_blog_recent extends block_base {
 
         $this->content = new stdClass();
         $this->content->footer = '';
+        $this->content->text = '';
 
         $context = $this->page->context;
 
index e7eaf88..059fe1d 100644 (file)
@@ -65,9 +65,6 @@ class behat_block_comments extends behat_base {
 
             $this->find_link(get_string('savecomment'))->click();
 
-            // Wait for the AJAX request.
-            $this->getSession()->wait(4 * 1000, false);
-
         } else {
 
             $commentstextarea = $this->find('css', '.block_comments form textarea', $exception);
@@ -103,7 +100,7 @@ class behat_block_comments extends behat_base {
         $deleteicon = $this->find('css', '.comment-delete a img', $deleteexception, $commentnode);
         $deleteicon->click();
 
-        // Wait for the AJAX request.
+        // Wait for the animation to finish, in theory is just 1 sec, adding 4 just in case.
         $this->getSession()->wait(4 * 1000, false);
     }
 
index 6287bad..d1670b4 100644 (file)
@@ -58,14 +58,11 @@ Feature: View my courses in navigation block
     And I should see "cat3" in the "Navigation" "block"
     And I should not see "cat2" in the "Navigation" "block"
     And I expand "cat3" node
-    And I wait "2" seconds
     And I should see "cat31" in the "Navigation" "block"
     And I should see "cat33" in the "Navigation" "block"
     And I should not see "cat32" in the "Navigation" "block"
     And I expand "cat31" node
-    And I wait "2" seconds
     And I should see "c31" in the "Navigation" "block"
     And I expand "cat33" node
-    And I wait "2" seconds
     And I should see "c331" in the "Navigation" "block"
     And I should not see "c332" in the "Navigation" "block"
index b6e6822..7a116ee 100644 (file)
Binary files a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js and b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js differ
index e474ba8..c48a7c3 100644 (file)
Binary files a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js and b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js differ
index 368a23d..0ecd3b9 100644 (file)
Binary files a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js and b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js differ
index 0748b98..7db6ca9 100644 (file)
@@ -409,7 +409,7 @@ Y.extend(TREE, Y.Base, TREE.prototype, {
         instance : {
             value : false,
             setter : function(val) {
-                return parseInt(val);
+                return parseInt(val, 10);
             }
         }
     }
index 36d8be2..28d59ca 100644 (file)
@@ -58,6 +58,20 @@ class behat_blocks extends behat_base {
         return $steps;
     }
 
+    /**
+     * Docks a block. Editing mode should be previously enabled.
+     *
+     * @Given /^I dock "(?P<block_name_string>(?:[^"]|\\")*)" block$/
+     * @param string $blockname
+     * @return Given
+     */
+    public function i_dock_block($blockname) {
+
+        // Looking for both title and alt.
+        $xpath = "//input[@type='image'][@title='" . get_string('dockblock', 'block', $blockname) . "' or @alt='" . get_string('addtodock', 'block') . "']";
+        return new Given('I click on " ' . $xpath . '" "xpath_element" in the "' . $this->escape($blockname) . '" "block"');
+    }
+
     /**
      * Opens a block's actions menu if it is not already opened.
      *
index 1527a3c..84501bd 100644 (file)
@@ -33,11 +33,9 @@ Feature: Comment on a blog entry
     And I follow "Comments (0)"
     When I fill in "content" with "$My own >nasty< \"string\"!"
     And I follow "Save comment"
-    And I wait "4" seconds
     Then I should see "$My own >nasty< \"string\"!"
     And I fill in "content" with "Another $Nasty <string?>"
     And I follow "Save comment"
-    And I wait "4" seconds
     And I should see "Comments (2)" in the ".comment-link" "css_element"
 
   @javascript
@@ -53,8 +51,8 @@ Feature: Comment on a blog entry
     And I follow "Comments (0)"
     And I fill in "content" with "$My own >nasty< \"string\"!"
     And I follow "Save comment"
-    And I wait "4" seconds
     When I click on ".comment-delete a" "css_element"
+    # Waiting for the animation to finish.
     And I wait "4" seconds
     Then I should not see "$My own >nasty< \"string\"!"
     And I follow "Blog post from user 1"
@@ -73,5 +71,4 @@ Feature: Comment on a blog entry
     When I follow "Comments (0)"
     And I fill in "content" with "$My own >nasty< \"string\"!"
     And I follow "Save comment"
-    And I wait "4" seconds
     Then I should see "$My own >nasty< \"string\"!"
index 5e6a5ad..eb0ea17 100644 (file)
@@ -27,7 +27,7 @@ Feature: Upload users to a cohort
     And I expand "Users" node
     And I expand "Accounts" node
     When I follow "Upload users"
-    And I upload "lib/tests/fixtures/upload_users_cohorts.csv" file to "File" filepicker
+    And I upload "lib/tests/fixtures/upload_users_cohorts.csv" file to "File" filemanager
     And I press "Upload users"
     And I press "Upload users"
     And I press "Continue"
index a14b9b9..98d2982 100644 (file)
@@ -37,7 +37,6 @@ Feature: Allow students to manually mark an activity as complete
     And I log in as "student1"
     And I follow "Course 1"
     And I press "Mark as complete: Test forum name"
-    And I wait "3" seconds
     And I log out
     And I log in as "teacher1"
     And I follow "Course 1"
index fe1a0f2..8e700b1 100644 (file)
@@ -20,13 +20,16 @@ Feature: Restrict activity availability through date conditions
     And I set the following administration settings values:
       | Enable conditional access | 1 |
     And I log out
-
-  @javascript
-  Scenario: Show activity greyed-out to students when available from date is in future
-    Given I log in as "teacher1"
+    And I log in as "teacher1"
     And I follow "Course 1"
     And I turn editing mode on
+    # Adding the page like this because id_available*_enabled needs to be clicked to trigger the action.
     And I add a "Assignment" to section "1"
+    And I expand all fieldsets
+
+  @javascript
+  Scenario: Show activity greyed-out to students when available from date is in future
+    Given I click on "id_availablefrom_enabled" "checkbox"
     And I fill the moodle form with:
       | Assignment name | Test assignment 1 |
       | Description | This assignment is restricted by date |
@@ -36,7 +39,6 @@ Feature: Restrict activity availability through date conditions
       | id_availablefrom_month | 12 |
       | id_availablefrom_year | 2050 |
       | id_showavailability | 1 |
-    And I click on "id_availablefrom_enabled" "checkbox"
     And I press "Save and return to course"
     And I log out
     When I log in as "student1"
@@ -47,10 +49,7 @@ Feature: Restrict activity availability through date conditions
 
   @javascript
   Scenario: Show activity hidden to students when available until date is in past
-    Given I log in as "teacher1"
-    And I follow "Course 1"
-    And I turn editing mode on
-    And I add a "Assignment" to section "2"
+    Given I click on "id_availableuntil_enabled" "checkbox"
     And I fill the moodle form with:
       | Assignment name | Test assignment 2 |
       | Description | This assignment is restricted by date |
@@ -60,7 +59,6 @@ Feature: Restrict activity availability through date conditions
       | id_availableuntil_month | 2 |
       | id_availableuntil_year | 2013 |
       | id_showavailability | 0 |
-    And I click on "id_availableuntil_enabled" "checkbox"
     And I press "Save and return to course"
     And I log out
     When I log in as "student1"
index e3de103..72154be 100644 (file)
@@ -29,13 +29,18 @@ Feature: Restrict activity availability through grade conditions
       | Description | Grade this assignment to revoke restriction on restricted assignment |
       | assignsubmission_onlinetext_enabled | 1 |
       | assignsubmission_file_enabled | 0 |
-    And I add a "Page" to section "2" and I fill the form with:
+    # Adding the page like this because id_availableform_enabled needs to be clicked to trigger the action.
+    And I add a "Page" to section "2"
+    And I expand all fieldsets
+    And I click on "id_availablefrom_enabled" "checkbox"
+    And I fill the moodle form with:
       | Name | Test page name |
       | Description | Restricted page, till grades in Grade assignment is at least 20% |
       | Page content | Test page contents |
       | id_conditiongradegroup_0_conditiongradeitemid | 2 |
       | id_conditiongradegroup_0_conditiongrademin | 20 |
       | id_showavailability | 1 |
+    And I press "Save and return to course"
     And I log out
     When I log in as "student1"
     And I follow "Course 1"
index cafa530..e7f1631 100644 (file)
@@ -672,6 +672,11 @@ $CFG->admin = 'admin';
 // Example:
 //   $CFG->behat_usedeprecated = true;
 //
+// Including feature files from directories outside the dirroot is possible if required. The setting
+// requires that the running user has executable permissions on all parent directories in the paths.
+// Example:
+//   $CFG->behat_additionalfeatures = array('/home/developer/code/wipfeatures');
+//
 //=========================================================================
 // 12. DEVELOPER DATA GENERATOR
 //=========================================================================
index ce72a9c..2f3ef23 100644 (file)
@@ -2183,7 +2183,7 @@ function course_get_cm_move(cm_info $mod, $sr = null) {
     if ($hasmanageactivities) {
         $pixicon = 'i/dragdrop';
 
-        if ($mod->course == SITEID) {
+        if (!course_ajax_enabled($mod->get_course())) {
             // Override for course frontpage until we get drag/drop working there.
             $pixicon = 't/move';
         }
index 055cb1a..2fde998 100644 (file)
@@ -29,14 +29,12 @@ Feature: Toggle activities groups mode from the course page
     Then "No groups (Click to change)" "link" should exists
     And "//a/child::img[contains(@src, 'groupn')]" "xpath_element" should exists
     And I click on "No groups (Click to change)" "link" in the "Test forum name" activity
-    And I wait "3" seconds
     And "Separate groups (Click to change)" "link" should exists
     And "//a/child::img[contains(@src, 'groups')]" "xpath_element" should exists
     And I reload the page
     And "Separate groups (Click to change)" "link" should exists
     And "//a/child::img[contains(@src, 'groups')]" "xpath_element" should exists
     And I click on "Separate groups (Click to change)" "link" in the "Test forum name" activity
-    And I wait "3" seconds
     And "Visible groups (Click to change)" "link" should exists
     And "//a/child::img[contains(@src, 'groupv')]" "xpath_element" should exists
     And I reload the page
index db8879c..de34b84 100644 (file)
@@ -40,8 +40,10 @@ Feature: Add activities to courses
   Scenario: Add an activity without the required fields
     When I add a "Database" to section "3" and I fill the form with:
       | Name | Test name |
+    And I press "Save and return to course"
     Then I should see "Adding a new"
     And I should see "Required"
+    And I press "Cancel"
 
   Scenario: Add an activity to a course with Javascript disabled
     Then I should see "Add a resource to section 'Topic 1'"
index 87f8495..fcceee3 100644 (file)
@@ -67,15 +67,50 @@ class behat_course extends behat_base {
      * @return Given[]
      */
     public function i_create_a_course_with(TableNode $table) {
-        return array(
+
+        $steps = array(
             new Given('I go to the courses management page'),
             new Given('I should see the "'.get_string('categories').'" management page'),
             new Given('I click on category "'.get_string('miscellaneous').'" in the management interface'),
             new Given('I should see the "'.get_string('categoriesandcoures').'" management page'),
-            new Given('I click on "'.get_string('createnewcourse').'" "link" in the "#course-listing" "css_element"'),
-            new Given('I fill the moodle form with:', $table),
-            new Given('I press "' . get_string('savechanges') . '"')
+            new Given('I click on "'.get_string('createnewcourse').'" "link" in the "#course-listing" "css_element"')
         );
+
+        // If the course format is one of the fields we change how we
+        // fill the form as we need to wait for the form to be set.
+        $rowshash = $table->getRowsHash();
+        $formatfieldrefs = array(get_string('format'), 'format', 'id_format');
+        foreach ($formatfieldrefs as $fieldref) {
+            if (!empty($rowshash[$fieldref])) {
+                $formatfield = $fieldref;
+            }
+        }
+
+        // Setting the format separately.
+        if (!empty($formatfield)) {
+
+            // Removing the format field from the TableNode.
+            $rows = $table->getRows();
+            $formatvalue = $rowshash[$formatfield];
+            foreach ($rows as $key => $row) {
+                if ($row[0] == $formatfield) {
+                    unset($rows[$key]);
+                }
+            }
+            $table->setRows($rows);
+
+            // Adding a forced wait until editors are loaded as otherwise selenium sometimes tries clicks on the
+            // format field when the editor is being rendered and the click misses the field coordinates.
+            $steps[] = new Given('I wait until the editors are loaded');
+            $steps[] = new Given('I select "' . $formatvalue . '" from "' . $formatfield . '"');
+            $steps[] = new Given('I fill the moodle form with:', $table);
+        } else {
+            $steps[] = new Given('I fill the moodle form with:', $table);
+        }
+
+        $steps[] = new Given('I press "' . get_string('savechanges') . '"');
+
+        return $steps;
     }
 
     /**
@@ -181,10 +216,7 @@ class behat_course extends behat_base {
         // Ensures the section exists.
         $xpath = $this->section_exists($sectionnumber);
 
-        return array(
-            new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'),
-            new Given('I wait "2" seconds')
-        );
+        return new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"');
     }
 
     /**
@@ -199,10 +231,7 @@ class behat_course extends behat_base {
         // Ensures the section exists.
         $xpath = $this->section_exists($sectionnumber);
 
-        return array(
-            new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'),
-            new Given('I wait "2" seconds')
-        );
+        return new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"');
     }
 
     /**
@@ -215,9 +244,9 @@ class behat_course extends behat_base {
         $showlink = $this->show_section_icon_exists($sectionnumber);
         $showlink->click();
 
-        // It requires time.
         if ($this->running_javascript()) {
-            $this->getSession()->wait(5000, false);
+            $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
+            $this->i_wait_until_section_is_available($sectionnumber);
         }
     }
 
@@ -231,9 +260,9 @@ class behat_course extends behat_base {
         $hidelink = $this->hide_section_icon_exists($sectionnumber);
         $hidelink->click();
 
-        // It requires time.
         if ($this->running_javascript()) {
-            $this->getSession()->wait(5000, false);
+            $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
+            $this->i_wait_until_section_is_available($sectionnumber);
         }
     }
 
@@ -314,6 +343,11 @@ class behat_course extends behat_base {
 
         $sectionxpath = $this->section_exists($sectionnumber);
 
+        // Preventive in case there is any action in progress.
+        // Adding it here because we are interacting (click) with
+        // the elements, not necessary when we just find().
+        $this->i_wait_until_section_is_available($sectionnumber);
+
         // Section should be hidden.
         $exception = new ExpectationException('The section is not hidden', $this->getSession());
         $this->find('xpath', $sectionxpath . "[contains(concat(' ', normalize-space(@class), ' '), ' hidden ')]", $exception);
@@ -338,9 +372,12 @@ class behat_course extends behat_base {
                     // Non-JS browsers can not click on img elements.
                     if ($this->running_javascript()) {
 
-                        // Expanding the actions menu.
-                        $actionsmenu = $this->find('css', "a[role='menuitem']", false, $activity);
-                        $actionsmenu->click();
+                        // Expanding the actions menu if it is not shown.
+                        $classes = array_flip(explode(' ', $activity->getAttribute('class')));
+                        if (empty($classes['action-menu-shown'])) {
+                            $actionsmenu = $this->find('css', "a[role='menuitem']", false, $activity);
+                            $actionsmenu->click();
+                        }
 
                         // To check that the visibility is not clickable we check the funcionality rather than the applied style.
                         $visibilityiconnode = $this->find('css', 'a.editing_show img', false, $activity);
@@ -349,6 +386,17 @@ class behat_course extends behat_base {
 
                     // We ensure that we still see the show icon.
                     $visibilityiconnode = $this->find('css', 'a.editing_show img', $visibilityexception, $activity);
+
+                    // It is there only when running JS scenarios.
+                    if ($this->running_javascript()) {
+
+                        // Collapse the actions menu if it is displayed.
+                        $classes = array_flip(explode(' ', $activity->getAttribute('class')));
+                        if (!empty($classes['action-menu-shown'])) {
+                            $actionsmenu = $this->find('css', "a[role='menuitem']", false, $activity);
+                            $actionsmenu->click();
+                        }
+                    }
                 }
             }
 
@@ -543,8 +591,7 @@ class behat_course extends behat_base {
         $activity = $this->escape($activityname);
         return array(
             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')
+            new Given('I fill in "title" with "' . $this->escape($newactivityname) . chr(10) . '"')
         );
     }
 
@@ -572,6 +619,30 @@ class behat_course extends behat_base {
         return new Given('I click on "a[role=\'menuitem\']" "css_element" in the "' . $this->escape($activityname) . '" activity');
     }
 
+    /**
+     * Closes an activity actions menu if it is not already closed.
+     *
+     * @Given /^I close "(?P<activity_name_string>(?:[^"]|\\")*)" actions menu$/
+     * @throws DriverException The step is not available when Javascript is disabled
+     * @param string $activityname
+     * @return Given
+     */
+    public function i_close_actions_menu($activityname) {
+
+        if (!$this->running_javascript()) {
+            throw new DriverException('Activities actions menu not available when Javascript is disabled');
+        }
+
+        // If it is already closed we do nothing.
+        $activitynode = $this->get_activity_node($activityname);
+        $classes = array_flip(explode(' ', $activitynode->getAttribute('class')));
+        if (empty($classes['action-menu-shown'])) {
+            return;
+        }
+
+        return new Given('I click on "a[role=\'menuitem\']" "css_element" in the "' . $this->escape($activityname) . '" activity');
+    }
+
     /**
      * Indents to the right the activity or resource specified by it's name. Editing mode should be on.
      *
@@ -588,10 +659,6 @@ class behat_course extends behat_base {
         }
         $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');
-        }
-
         return $steps;
     }
 
@@ -611,10 +678,6 @@ class behat_course extends behat_base {
         }
         $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');
-        }
-
         return $steps;
 
     }
@@ -640,8 +703,6 @@ class behat_course extends behat_base {
 
             $this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
 
-            $this->getSession()->wait(2 * 1000, false);
-
         } else {
 
             // With JS disabled.
@@ -668,10 +729,7 @@ class behat_course extends behat_base {
             $steps[] = new Given('I open "' . $activity . '" actions menu');
         }
         $steps[] = new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activity . '" activity');
-        if ($this->running_javascript()) {
-            // Temporary wait until MDL-41030 lands.
-            $steps[] = new Given('I wait "4" seconds');
-        } else {
+        if (!$this->running_javascript()) {
             $steps[] = new Given('I press "' . get_string('continue') .'"');
             $steps[] = new Given('I press "' . get_string('duplicatecontcourse') .'"');
         }
@@ -691,12 +749,22 @@ class behat_course extends behat_base {
         $steps = array();
 
         $activity = $this->escape($activityname);
+        $activityliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($activityname);
 
         if ($this->running_javascript()) {
             $steps[] = new Given('I duplicate "' . $activity . '" activity');
 
+            // We wait until the AJAX request finishes and the section is visible again.
+            $hiddenlightboxxpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityliteral)]" .
+                "/ancestor::li[contains(concat(' ', normalize-space(@class), ' '), ' section ')]" .
+                "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]";
+            $steps[] = new Given('I wait until the page is ready');
+            $steps[] = new Given('I wait until "' . $this->escape($hiddenlightboxxpath) .'" "xpath_element" exists');
+
+            // Close the original activity actions menu.
+            $steps[] = new Given('I close "' . $activity . '" actions menu');
+
             // Determine the future new activity xpath from the former one.
-            $activityliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($activityname);
             $duplicatedxpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityliteral)]" .
                 "/following-sibling::li";
             $duplicatedactionsmenuxpath = $duplicatedxpath . "/descendant::a[@role='menuitem']";
@@ -717,6 +785,32 @@ class behat_course extends behat_base {
         return $steps;
     }
 
+    /**
+     * Waits until the section is available to interact with it. Useful when the section is performing an action and the section is overlayed with a loading layout.
+     *
+     * Using the protected method as this method will be usually
+     * called by other methods which are not returning a set of
+     * steps and performs the actions directly, so it would not
+     * be executed if it returns another step.
+     *
+     * Hopefully we would not require test writers to use this step
+     * and we will manage it from other step definitions.
+     *
+     * @Given /^I wait until section "(?P<section_number>\d+)" is available$/
+     * @param int $sectionnumber
+     * @return void
+     */
+    public function i_wait_until_section_is_available($sectionnumber) {
+
+        // Looks for a hidden lightbox or a non-existent lightbox in that section.
+        $sectionxpath = $this->section_exists($sectionnumber);
+        $hiddenlightboxxpath = $sectionxpath . "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]" .
+            " | " .
+            $sectionxpath . "[count(child::div[contains(@class, 'lightbox')]) = 0]";
+
+        $this->ensure_element_exists($hiddenlightboxxpath, 'xpath_element');
+    }
+
     /**
      * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on.
      *
@@ -882,6 +976,18 @@ class behat_course extends behat_base {
         return $this->find('xpath', $xpath);
     }
 
+    /**
+     * Gets the activity instance name from the activity node.
+     *
+     * @throws ElementNotFoundException
+     * @param NodeElement $activitynode
+     * @return string
+     */
+    protected function get_activity_name($activitynode) {
+        $instancenamenode = $this->find('xpath', "//span[contains(concat(' ', normalize-space(@class), ' '), ' instancename ')]", false, $activitynode);
+        return $instancenamenode->getText();
+    }
+
     /**
      * Returns whether the user can edit the course contents or not.
      *
index 14a6820..d7a96a1 100644 (file)
@@ -1,4 +1,4 @@
-@core @core_course @test
+@core @core_course
 Feature: Course category management interface performs as expected
   In order to test JS enhanced display of categories and subcategories.
   As a moodle admin
index 87adb97..0e58239 100644 (file)
@@ -1,4 +1,4 @@
-@core @core_course
+@core @core_course @_alerts
 Feature: Course activity controls works as expected
   In order to manage my course's activities
   As a teacher
@@ -59,11 +59,16 @@ Feature: Course activity controls works as expected
     And I click on "Edit settings" "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 I fill the moodle form with:
+      | Forum name | Just to check that I can edit the name |
+      | Description | Just to check that I can edit the description |
+      | Display description on course page | 1 |
+    And I click on "Cancel" "button"
     And "#section-2" "css_element" <should_see_other_sections> exists
     And I open "Test forum name 1" actions menu
     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 close "Test forum name 1" actions menu
     And I duplicate "Test forum name 2" activity editing the new copy with:
       | Forum name | Edited test forum name 2 |
     And "#section-2" "css_element" <should_see_other_sections> exists
diff --git a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js
new file mode 100644 (file)
index 0000000..3268417
Binary files /dev/null and b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js differ
diff --git a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js
new file mode 100644 (file)
index 0000000..67d01c9
Binary files /dev/null and b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js differ
diff --git a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js
new file mode 100644 (file)
index 0000000..3268417
Binary files /dev/null and b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js differ
diff --git a/course/yui/src/toolboxes/build.json b/course/yui/src/toolboxes/build.json
new file mode 100644 (file)
index 0000000..ffd5939
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "name": "moodle-course-toolboxes",
+    "builds": {
+        "moodle-course-toolboxes": {
+            "jsfiles": [
+                "toolbox.js",
+                "resource.js",
+                "section.js"
+            ]
+        }
+    }
+}
diff --git a/course/yui/src/toolboxes/js/resource.js b/course/yui/src/toolboxes/js/resource.js
new file mode 100644 (file)
index 0000000..2e98bdf
--- /dev/null
@@ -0,0 +1,695 @@
+/**
+ * Resource and activity toolbox class.
+ *
+ * This class is responsible for managing AJAX interactions with activities and resources
+ * when viewing a course in editing mode.
+ *
+ * @module moodle-course-toolboxes
+ * @namespace M.course.toolboxes
+ */
+
+/**
+ * Resource and activity toolbox class.
+ *
+ * This is a class extending TOOLBOX containing code specific to resources
+ *
+ * This class is responsible for managing AJAX interactions with activities and resources
+ * when viewing a course in editing mode.
+ *
+ * @class resources
+ * @constructor
+ * @extends M.course.toolboxes.toolbox
+ */
+var RESOURCETOOLBOX = function() {
+    RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
+};
+
+Y.extend(RESOURCETOOLBOX, TOOLBOX, {
+    /**
+     * No groups are being used.
+     *
+     * @property GROUPS_NONE
+     * @protected
+     * @type Number
+     */
+    GROUPS_NONE: 0,
+
+    /**
+     * Separate groups are being used.
+     *
+     * @property GROUPS_SEPARATE
+     * @protected
+     * @type Number
+     */
+    GROUPS_SEPARATE: 1,
+
+    /**
+     * Visible groups are being used.
+     *
+     * @property GROUPS_VISIBLE
+     * @protected
+     * @type Number
+     */
+    GROUPS_VISIBLE: 2,
+
+    /**
+     * An Array of events added when editing a title.
+     * These should all be detached when editing is complete.
+     *
+     * @property edittitleevents
+     * @protected
+     * @type Array
+     * @protected
+     */
+    edittitleevents: [],
+
+    /**
+     * Initialize the resource toolbox
+     *
+     * 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
+     * @protected
+     */
+    initializer: function() {
+        M.course.coursebase.register_module(this);
+        BODY.delegate('key', this.handle_data_action, 'down:enter', SELECTOR.ACTIVITYACTION, this);
+        Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this);
+    },
+
+    /**
+     * Handles the delegation event. When this is fired someone has triggered an action.
+     *
+     * 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}
+     */
+    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);
+        }
+
+        // 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.ancestor(SELECTOR.ACTIVITYLI);
+
+        if (!node.test('a') || !action || !activity) {
+            // It wasn't a valid action node.
+            return;
+        }
+
+        // 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 'duplicate':
+                // The user is duplicating the activity.
+                this.duplicate(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':
+                break;
+            default:
+                // Nothing to do here!
+                break;
+        }
+    },
+
+    /**
+     * Add a loading icon to the specified activity.
+     *
+     * The icon is added within the action area.
+     *
+     * @method add_spinner
+     * @param {Node} activity The activity to add a loading icon to
+     * @return {Node|null} The newly created icon, or null if the action area was not found.
+     */
+    add_spinner: function(activity) {
+        var actionarea = activity.one(SELECTOR.ACTIONAREA);
+        if (actionarea) {
+            return M.util.add_spinner(Y, actionarea);
+        }
+        return null;
+    },
+
+    /**
+     * Change the indent of the activity or resource.
+     *
+     * @method change_indent
+     * @protected
+     * @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
+        ev.preventDefault();
+
+        var direction = (action === 'moveleft') ? -1: 1;
+
+        // And we need to determine the current and new indent level
+        var indentdiv = activity.one(SELECTOR.MODINDENTDIV),
+            indent = indentdiv.getAttribute('class').match(/mod-indent-(\d{1,})/),
+            oldindent = 0,
+            newindent;
+
+        if (indent) {
+            oldindent = parseInt(indent[1], 10);
+        }
+        newindent = oldindent + parseInt(direction, 10);
+
+        if (newindent < INDENTLIMITS.MIN || newindent > INDENTLIMITS.MAX) {
+            return;
+        }
+
+        if (indent) {
+            indentdiv.removeClass(indent[0]);
+        }
+
+        // Perform the move
+        indentdiv.addClass(CSS.MODINDENTCOUNT + newindent);
+        var data = {
+            'class': 'resource',
+            'field': 'indent',
+            'value': newindent,
+            'id': Y.Moodle.core_course.util.cm.getId(activity)
+        };
+        var spinner = this.add_spinner(activity);
+        this.send_request(data, spinner);
+
+        var remainingmove;
+
+        // Handle removal/addition of the moveleft button.
+        if (newindent === INDENTLIMITS.MIN) {
+            button.addClass('hidden');
+            remainingmove = activity.one('.editing_moveright');
+        } else if (newindent > INDENTLIMITS.MIN && oldindent === INDENTLIMITS.MIN) {
+            button.ancestor('.menu').one('[data-action=moveleft]').removeClass('hidden');
+        }
+
+        if (newindent === INDENTLIMITS.MAX) {
+            button.addClass('hidden');
+            remainingmove = activity.one('.editing_moveleft');
+        } else if (newindent < INDENTLIMITS.MAX && oldindent === INDENTLIMITS.MAX) {
+            button.ancestor('.menu').one('[data-action=moveright]').removeClass('hidden');
+        }
+
+        // Handle massive indentation to match non-ajax display
+        var hashugeclass = indentdiv.hasClass(CSS.MODINDENTHUGE);
+        if (newindent > 15 && !hashugeclass) {
+            indentdiv.addClass(CSS.MODINDENTHUGE);
+        } else if (newindent <= 15 && hashugeclass) {
+            indentdiv.removeClass(CSS.MODINDENTHUGE);
+        }
+
+        if (ev.type && ev.type === "key" && remainingmove) {
+            remainingmove.focus();
+        }
+    },
+
+    /**
+     * 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.
+     * @chainable
+     */
+    delete_with_confirmation: function(ev, button, activity) {
+        // Prevent the default button action
+        ev.preventDefault();
+
+        // Get the element we're working on
+        var element   = activity,
+            // Create confirm string (different if element has or does not have name)
+            confirmstring = '',
+            plugindata = {
+                type: M.util.get_string('pluginname', element.getAttribute('class').match(/modtype_([^\s]*)/)[1])
+            };
+        if (Y.Moodle.core_course.util.cm.getName(element) !== null) {
+            plugindata.name = Y.Moodle.core_course.util.cm.getName(element);
+            confirmstring = M.util.get_string('deletechecktypename', 'moodle', plugindata);
+        } else {
+            confirmstring = M.util.get_string('deletechecktype', 'moodle', plugindata);
+        }
+
+        // Confirm element removal
+        if (!confirm(confirmstring)) {
+            return this;
+        }
+
+        // Actually remove the element
+        element.remove();
+        var data = {
+            'class': 'resource',
+            'action': 'DELETE',
+            'id': Y.Moodle.core_course.util.cm.getId(element)
+        };
+        this.send_request(data);
+        if (M.core.actionmenu && M.core.actionmenu.instance) {
+            M.core.actionmenu.instance.hideMenu();
+        }
+        return this;
+    },
+
+    /**
+     * Duplicates the activity.
+     *
+     * @method duplicate
+     * @protected
+     * @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.
+     * @chainable
+     */
+    duplicate: function(ev, button, activity) {
+        // Prevent the default button action
+        ev.preventDefault();
+
+        // Get the element we're working on
+        var element = activity;
+
+        // Add the lightbox.
+        var section = activity.ancestor(M.course.format.get_section_selector(Y)),
+            lightbox = M.util.add_lightbox(Y, section).show();
+
+        // Build and send the request.
+        var data = {
+            'class': 'resource',
+            'field': 'duplicate',
+            'id': Y.Moodle.core_course.util.cm.getId(element),
+            'sr': button.getData('sr')
+        };
+        this.send_request(data, lightbox, function(response) {
+            var newcm = Y.Node.create(response.fullcontent);
+
+            // Append to the section?
+            activity.insert(newcm, 'after');
+            Y.use('moodle-course-coursebase', function() {
+                M.course.coursebase.invoke_function('setup_for_resource', newcm);
+            });
+            if (M.core.actionmenu && M.core.actionmenu.newDOMNode) {
+                M.core.actionmenu.newDOMNode(newcm);
+            }
+        });
+        return this;
+    },
+
+    /**
+     * Changes the visibility of this activity or resource.
+     *
+     * @method change_visibility
+     * @protected
+     * @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.
+     * @chainable
+     */
+    change_visibility: function(ev, button, activity, action) {
+        // Prevent the default button action
+        ev.preventDefault();
+
+        // Return early if the current section is hidden
+        var section = activity.ancestor(M.course.format.get_section_selector(Y));
+        if (section && section.hasClass(CSS.SECTIONHIDDENCLASS)) {
+            return this;
+        }
+
+        // Get the element we're working on
+        var element = activity;
+        var value = this.handle_resource_dim(button, activity, action);
+
+        // Send the request
+        var data = {
+            'class': 'resource',
+            'field': 'visible',
+            'value': value,
+            'id': Y.Moodle.core_course.util.cm.getId(element)
+        };
+        var spinner = this.add_spinner(element);
+        this.send_request(data, spinner);
+
+        return this;
+    },
+
+    /**
+     * Handles the UI aspect of dimming the activity or resource.
+     *
+     * @method handle_resource_dim
+     * @protected
+     * @param {Node} button The button that triggered the action.
+     * @param {Node} activity The activity node that this action will be performed on.
+     * @param {String} action 'show' or 'hide'.
+     * @returns {Number} 1 if we changed to visible, 0 if we were hiding.
+     */
+    handle_resource_dim: function(button, activity, action) {
+        var toggleclass = CSS.DIMCLASS,
+            dimarea = activity.one([
+                    SELECTOR.ACTIVITYLINK,
+                    SELECTOR.CONTENTWITHOUTLINK
+                ].join(', ')),
+            availabilityinfo = activity.one(CSS.AVAILABILITYINFODIV),
+            nextaction = (action === 'hide') ? 'show': 'hide',
+            buttontext = button.one('span'),
+            newstring = M.util.get_string(nextaction, 'moodle'),
+            buttonimg = button.one('img');
+
+        // Update button info.
+        buttonimg.setAttrs({
+            'src': M.util.image_url('t/' + nextaction)
+        });
+
+        if (Y.Lang.trim(button.getAttribute('title'))) {
+            button.setAttribute('title', newstring);
+        }
+
+        if (Y.Lang.trim(buttonimg.getAttribute('alt'))) {
+            buttonimg.setAttribute('alt', newstring);
+        }
+
+        button.replaceClass('editing_'+action, 'editing_'+nextaction);
+        button.setData('action', nextaction);
+        if (buttontext) {
+            buttontext.set('text', newstring);
+        }
+
+        if (activity.one(SELECTOR.CONTENTWITHOUTLINK)) {
+            dimarea = activity.one(SELECTOR.CONTENTWITHOUTLINK);
+            toggleclass = CSS.DIMMEDTEXT;
+        }
+
+        // If activity is conditionally hidden, then don't toggle.
+        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 (action === 'hide') ? 0: 1;
+    },
+
+    /**
+     * Changes the groupmode of the activity to the next groupmode in the sequence.
+     *
+     * @method change_groupmode
+     * @protected
+     * @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.
+     * @chainable
+     */
+    change_groupmode: function(ev, button, activity) {
+        // Prevent the default button action.
+        ev.preventDefault();
+
+        // Current Mode
+        var groupmode = parseInt(button.getData('nextgroupmode'), 10),
+            newtitle = '',
+            iconsrc = '',
+            newtitlestr,
+            data,
+            spinner,
+            nextgroupmode = groupmode + 1,
+            buttonimg = button.one('img');
+
+        if (nextgroupmode > 2) {
+            nextgroupmode = 0;
+        }
+
+        if (groupmode === this.GROUPS_NONE) {
+            newtitle = 'groupsnone';
+            iconsrc = M.util.image_url('i/groupn', 'moodle');
+        } else if (groupmode === this.GROUPS_SEPARATE) {
+            newtitle = 'groupsseparate';
+            iconsrc = M.util.image_url('i/groups', 'moodle');
+        } else if (groupmode === this.GROUPS_VISIBLE) {
+            newtitle = 'groupsvisible';
+            iconsrc = M.util.image_url('i/groupv', 'moodle');
+        }
+        newtitlestr = M.util.get_string('clicktochangeinbrackets', 'moodle', M.util.get_string(newtitle, 'moodle'));
+
+        // Change the UI
+        buttonimg.setAttrs({
+            'src': iconsrc
+        });
+        if (Y.Lang.trim(button.getAttribute('title'))) {
+            button.setAttribute('title', newtitlestr).setData('action', newtitle).setData('nextgroupmode', nextgroupmode);
+        }
+
+        if (Y.Lang.trim(buttonimg.getAttribute('alt'))) {
+            buttonimg.setAttribute('alt', newtitlestr);
+        }
+
+        // And send the request
+        data = {
+            'class': 'resource',
+            'field': 'groupmode',
+            'value': groupmode,
+            'id': Y.Moodle.core_course.util.cm.getId(activity)
+        };
+
+        spinner = this.add_spinner(activity);
+        this.send_request(data, spinner);
+        return this;
+    },
+
+    /**
+     * Edit the title for the resource
+     *
+     * @method edit_title
+     * @protected
+     * @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.
+     * @chainable
+     */
+    edit_title: function(ev, button, activity) {
+        // Get the element we're working on
+        var activityid = Y.Moodle.core_course.util.cm.getId(activity),
+            instancename  = activity.one(SELECTOR.INSTANCENAME),
+            instance = activity.one(SELECTOR.ACTIVITYINSTANCE),
+            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
+            };
+
+        // Prevent the default actions.
+        ev.preventDefault();
+
+        this.send_request(data, null, function(response) {
+            if (M.core.actionmenu && M.core.actionmenu.instance) {
+                M.core.actionmenu.instance.hideMenu();
+            }
+
+            // Try to retrieve the existing string from the server
+            if (response.instancename) {
+                titletext = response.instancename;
+            }
+
+            // Create the editor and submit button
+            var editform = Y.Node.create('<form action="#" />');
+            var editinstructions = Y.Node.create('<span class="'+CSS.EDITINSTRUCTIONS+'" id="id_editinstructions" />')
+                .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
+            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
+            editform.appendChild(activity.one(SELECTOR.ACTIVITYICON).cloneNode());
+            editform.appendChild(editor);
+            editform.setData('anchor', anchor);
+            instance.insert(editinstructions, 'before');
+            anchor.replace(editform);
+
+            // Force the editing instruction to match the mod-indent position.
+            var padside = 'left';
+            if (right_to_left()) {
+                padside = 'right';
+            }
+
+            // We hide various components whilst editing:
+            activity.addClass(CSS.EDITINGTITLE);
+
+            // Focus and select the editor text
+            editor.focus().select();
+
+            // 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);
+
+            // Handle form submission.
+            thisevent = editform.on('submit', this.edit_title_submit, this, activity, oldtitle);
+            this.edittitleevents.push(thisevent);
+        });
+        return this;
+    },
+
+    /**
+     * Handles the submit event when editing the activity or resources title.
+     *
+     * @method edit_title_submit
+     * @protected
+     * @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 = this.add_spinner(activity);
+        if (newtitle !== null && newtitle !== "" && newtitle !== originaltitle) {
+            var data = {
+                'class': 'resource',
+                'field': 'updatetitle',
+                'title': newtitle,
+                'id': Y.Moodle.core_course.util.cm.getId(activity)
+            };
+            this.send_request(data, spinner, function(response) {
+                if (response.instancename) {
+                    activity.one(SELECTOR.INSTANCENAME).setContent(response.instancename);
+                }
+            });
+        }
+    },
+
+    /**
+     * Handles the cancel event when editing the activity or resources title.
+     *
+     * @method edit_title_cancel
+     * @protected
+     * @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);
+    },
+
+    /**
+     * Handles clearing the editing UI and returning things to the original state they were in.
+     *
+     * @method edit_title_clear
+     * @protected
+     * @param {Node} activity  The activity whose title we were altering.
+     */
+    edit_title_clear: function(activity) {
+        // Detach all listen events to prevent duplicate triggers
+        new Y.EventHandle(this.edittitleevents).detach();
+
+        var editform = activity.one(SELECTOR.ACTIVITYFORM),
+            instructions = activity.one('#id_editinstructions');
+        if (editform) {
+            editform.replace(editform.getData('anchor'));
+        }
+        if (instructions) {
+            instructions.remove();
+        }
+
+        // Remove the editing class again to revert the display.
+        activity.removeClass(CSS.EDITINGTITLE);
+
+        // Refocus the link which was clicked originally so the user can continue using keyboard nav.
+        Y.later(100, this, function() {
+            activity.one(SELECTOR.EDITTITLE).focus();
+        });
+    },
+
+    /**
+     * 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).
+     *
+     * @method set_visibility_resource_ui
+     * @param {Object} args An object with 'element' being the A node containing the resource and 'visible' being the
+     * state that the visibility should be set to.
+     */
+    set_visibility_resource_ui: function(args) {
+        var element = args.element,
+            shouldbevisible = args.visible,
+            buttonnode = element.one(SELECTOR.SHOW),
+            visible = (buttonnode === null),
+            action = 'show';
+        if (visible) {
+            buttonnode = element.one(SELECTOR.HIDE);
+            action = 'hide';
+        }
+        if (visible !== shouldbevisible) {
+            this.handle_resource_dim(buttonnode, element, action);
+        }
+    }
+}, {
+    NAME: 'course-resource-toolbox',
+    ATTRS: {
+    }
+});
+
+M.course.resource_toolbox = null;
+M.course.init_resource_toolbox = function(config) {
+    M.course.resource_toolbox = new RESOURCETOOLBOX(config);
+    return M.course.resource_toolbox;
+};
diff --git a/course/yui/src/toolboxes/js/section.js b/course/yui/src/toolboxes/js/section.js
new file mode 100644 (file)
index 0000000..1298a24
--- /dev/null
@@ -0,0 +1,173 @@
+/**
+ * Resource and activity toolbox class.
+ *
+ * This class is responsible for managing AJAX interactions with activities and resources
+ * when viewing a course in editing mode.
+ *
+ * @module moodle-course-toolboxes
+ * @namespace M.course.toolboxes
+ */
+
+/**
+ * Section toolbox class.
+ *
+ * This class is responsible for managing AJAX interactions with sections
+ * when viewing a course in editing mode.
+ *
+ * @class section
+ * @constructor
+ * @extends M.course.toolboxes.toolbox
+ */
+var SECTIONTOOLBOX = function() {
+    SECTIONTOOLBOX.superclass.constructor.apply(this, arguments);
+};
+
+Y.extend(SECTIONTOOLBOX, TOOLBOX, {
+    /**
+     * Initialize the section toolboxes module.
+     *
+     * Updates all span.commands with relevant handlers and other required changes.
+     *
+     * @method initializer
+     * @protected
+     */
+    initializer : function() {
+        M.course.coursebase.register_module(this);
+
+        // Section Highlighting.
+        Y.delegate('click', this.toggle_highlight, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.HIGHLIGHT, this);
+
+        // Section Visibility.
+        Y.delegate('click', this.toggle_hide_section, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.SHOWHIDE, this);
+    },
+
+    toggle_hide_section : function(e) {
+        // Prevent the default button action.
+        e.preventDefault();
+
+        // Get the section we're working on.
+        var section = e.target.ancestor(M.course.format.get_section_selector(Y)),
+            button = e.target.ancestor('a', true),
+            hideicon = button.one('img'),
+
+        // The value to submit
+            value,
+
+        // The text for strings and images. Also determines the icon to display.
+            action,
+            nextaction;
+
+        if (!section.hasClass(CSS.SECTIONHIDDENCLASS)) {
+            section.addClass(CSS.SECTIONHIDDENCLASS);
+            value = 0;
+            action = 'hide';
+            nextaction = 'show';
+        } else {
+            section.removeClass(CSS.SECTIONHIDDENCLASS);
+            value = 1;
+            action = 'show';
+            nextaction = 'hide';
+        }
+
+        var newstring = M.util.get_string(nextaction + 'fromothers', 'format_' + this.get('format'));
+        hideicon.setAttrs({
+            'alt' : newstring,
+            'src'   : M.util.image_url('i/' + nextaction)
+        });
+        button.set('title', newstring);
+
+        // Change the highlight status
+        var data = {
+            'class' : 'section',
+            'field' : 'visible',
+            'id'    : Y.Moodle.core_course.util.section.getId(section.ancestor(M.course.format.get_section_wrapper(Y), true)),
+            'value' : value
+        };
+
+        var lightbox = M.util.add_lightbox(Y, section);
+        lightbox.show();
+
+        this.send_request(data, lightbox, function(response) {
+            var activities = section.all(SELECTOR.ACTIVITYLI);
+            activities.each(function(node) {
+                var button;
+                if (node.one(SELECTOR.SHOW)) {
+                    button = node.one(SELECTOR.SHOW);
+                } else {
+                    button = node.one(SELECTOR.HIDE);
+                }
+                var activityid = Y.Moodle.core_course.util.cm.getId(node);
+
+                // NOTE: resourcestotoggle is returned as a string instead
+                // of a Number so we must cast our activityid to a String.
+                if (Y.Array.indexOf(response.resourcestotoggle, "" + activityid) !== -1) {
+                    M.course.resource_toolbox.handle_resource_dim(button, node, action);
+                }
+            }, this);
+        });
+    },
+
+    /**
+     * Toggle highlighting the current section.
+     *
+     * @method toggle_highlight
+     * @param {EventFacade} e
+     */
+    toggle_highlight : function(e) {
+        // Prevent the default button action.
+        e.preventDefault();
+
+        // Get the section we're working on.
+        var section = e.target.ancestor(M.course.format.get_section_selector(Y));
+        var button = e.target.ancestor('a', true);
+        var buttonicon = button.one('img');
+
+        // Determine whether the marker is currently set.
+        var togglestatus = section.hasClass('current');
+        var value = 0;
+
+        // Set the current highlighted item text.
+        var old_string = M.util.get_string('markthistopic', 'moodle');
+        Y.one(SELECTOR.PAGECONTENT)
+            .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT)
+            .set('title', old_string);
+        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.
+        Y.one(SELECTOR.PAGECONTENT).all(M.course.format.get_section_selector(Y))
+            .removeClass('current');
+
+        // Then add it if required to the selected section.
+        if (!togglestatus) {
+            section.addClass('current');
+            value = Y.Moodle.core_course.util.section.getId(section.ancestor(M.course.format.get_section_wrapper(Y), true));
+            var new_string = M.util.get_string('markedthistopic', 'moodle');
+            button
+                .set('title', new_string);
+            buttonicon
+                .set('alt', new_string)
+                .set('src', M.util.image_url('i/marked'));
+        }
+
+        // Change the highlight status.
+        var data = {
+            'class' : 'course',
+            'field' : 'marker',
+            'value' : value
+        };
+        var lightbox = M.util.add_lightbox(Y, section);
+        lightbox.show();
+        this.send_request(data, lightbox);
+    }
+}, {
+    NAME : 'course-section-toolbox',
+    ATTRS : {
+    }
+});
+
+M.course.init_section_toolbox = function(config) {
+    return new SECTIONTOOLBOX(config);
+};
diff --git a/course/yui/src/toolboxes/js/shared.js b/course/yui/src/toolboxes/js/shared.js
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/course/yui/src/toolboxes/js/toolbox.js b/course/yui/src/toolboxes/js/toolbox.js
new file mode 100644 (file)
index 0000000..7fdfaac
--- /dev/null
@@ -0,0 +1,202 @@
+/**
+ * Resource and activity toolbox class.
+ *
+ * This class is responsible for managing AJAX interactions with activities and resources
+ * when viewing a course in editing mode.
+ *
+ * @module moodle-course-toolboxes
+ * @namespace M.course.toolboxes
+ */
+
+// The CSS classes we use.
+var CSS = {
+        ACTIVITYINSTANCE : 'activityinstance',
+        AVAILABILITYINFODIV : 'div.availabilityinfo',
+        CONTENTWITHOUTLINK : 'contentwithoutlink',
+        CONDITIONALHIDDEN : 'conditionalhidden',
+        DIMCLASS : 'dimmed',
+        DIMMEDTEXT : 'dimmed_text',
+        EDITINSTRUCTIONS : 'editinstructions',
+        EDITINGTITLE: 'editor_displayed',
+        HIDE : 'hide',
+        MODINDENTCOUNT : 'mod-indent-',
+        MODINDENTHUGE : 'mod-indent-huge',
+        MODULEIDPREFIX : 'module-',
+        SECTIONHIDDENCLASS : 'hidden',
+        SECTIONIDPREFIX : 'section-',
+        SHOW : 'editing_show',
+        TITLEEDITOR : 'titleeditor'
+    },
+    // The CSS selectors we use.
+    SELECTOR = {
+        ACTIONAREA: '.actions',
+        ACTIONLINKTEXT : '.actionlinktext',
+        ACTIVITYACTION : 'a.cm-edit-action[data-action], a.editing_title',
+        ACTIVITYFORM : '.' + CSS.ACTIVITYINSTANCE + ' form',
+        ACTIVITYICON : 'img.activityicon',
+        ACTIVITYINSTANCE : '.' + CSS.ACTIVITYINSTANCE,
+        ACTIVITYLINK: '.' + CSS.ACTIVITYINSTANCE + ' > a',
+        ACTIVITYLI : 'li.activity',
+        ACTIVITYTITLE : 'input[name=title]',
+        COMMANDSPAN : '.commands',
+        CONTENTAFTERLINK : 'div.contentafterlink',
+        CONTENTWITHOUTLINK : 'div.contentwithoutlink',
+        EDITTITLE: 'a.editing_title',
+        HIDE : 'a.editing_hide',
+        HIGHLIGHT : 'a.editing_highlight',
+        INSTANCENAME : 'span.instancename',
+        MODINDENTDIV : '.mod-indent',
+        MODINDENTOUTER : '.mod-indent-outer',
+        PAGECONTENT : 'div#page-content',
+        SECTIONLI : 'li.section',
+        SHOW : 'a.'+CSS.SHOW,
+        SHOWHIDE : 'a.editing_showhide'
+    },
+    INDENTLIMITS = {
+        MIN: 0,
+        MAX: 16
+    },
+    BODY = Y.one(document.body);
+
+// Setup the basic namespace.
+M.course = M.course || {};
+
+/**
+ * The toolbox class is a generic class which should never be directly
+ * instantiated. Please extend it instead.
+ *
+ * @class toolbox
+ * @constructor
+ * @protected
+ * @extends Base
+ */
+var TOOLBOX = function() {
+    TOOLBOX.superclass.constructor.apply(this, arguments);
+};
+
+Y.extend(TOOLBOX, Y.Base, {
+    /**
+     * Send a request using the REST API
+     *
+     * @method send_request
+     * @param {Object} data The data to submit with the AJAX request
+     * @param {Node} [statusspinner] A statusspinner which may contain a section loader
+     * @param {Function} success_callback The callback to use on success
+     * @param {Object} [optionalconfig] Any additional configuration to submit
+     * @chainable
+     */
+    send_request: function(data, statusspinner, success_callback, optionalconfig) {
+        // Default data structure
+        if (!data) {
+            data = {};
+        }
+        // Handle any variables which we must pass back through to
+        var pageparams = this.get('config').pageparams,
+            varname;
+        for (varname in pageparams) {
+            data[varname] = pageparams[varname];
+        }
+
+        data.sesskey = M.cfg.sesskey;
+        data.courseId = this.get('courseid');
+
+        var uri = M.cfg.wwwroot + this.get('ajaxurl');
+
+        // Define the configuration to send with the request
+        var responsetext = [];
+        var config = {
+            method: 'POST',
+            data: data,
+            on: {
+                success: function(tid, response) {
+                    try {
+                        responsetext = Y.JSON.parse(response.responseText);
+                        if (responsetext.error) {
+                            new M.core.ajaxException(responsetext);
+                        }
+                    } catch (e) {}
+
+                    // Run the callback if we have one.
+                    if (success_callback) {
+                        Y.bind(success_callback, this, responsetext)();
+                    }
+
+                    if (statusspinner) {
+                        window.setTimeout(function() {
+                            statusspinner.hide();
+                        }, 400);
+                    }
+                },
+                failure: function(tid, response) {
+                    if (statusspinner) {
+                        statusspinner.hide();
+                    }
+                    new M.core.ajaxException(response);
+                }
+            },
+            context: this
+        };
+
+        // Apply optional config
+        if (optionalconfig) {
+            for (varname in optionalconfig) {
+                config[varname] = optionalconfig[varname];
+            }
+        }
+
+        if (statusspinner) {
+            statusspinner.show();
+        }
+
+        // Send the request
+        Y.io(uri, config);
+        return this;
+    }
+},
+{
+    NAME: 'course-toolbox',
+    ATTRS: {
+        /**
+         * The ID of the Moodle Course being edited.
+         *
+         * @attribute courseid
+         * @default 0
+         * @type Number
+         */
+        courseid: {
+            'value': 0
+        },
+
+        /**
+         * The Moodle course format.
+         *
+         * @attribute format
+         * @default 'topics'
+         * @type String
+         */
+        format: {
+            'value': 'topics'
+        },
+        /**
+         * The URL to use when submitting requests.
+         * @attribute ajaxurl
+         * @default null
+         * @type String
+         */
+        ajaxurl: {
+            'value': null
+        },
+        /**
+         * Any additional configuration passed when creating the instance.
+         *
+         * @attribute config
+         * @default {}
+         * @type Object
+         */
+        config: {
+            'value': {}
+        }
+    }
+}
+);
+
diff --git a/course/yui/src/toolboxes/meta/toolboxes.json b/course/yui/src/toolboxes/meta/toolboxes.json
new file mode 100644 (file)
index 0000000..ce2e74d
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "moodle-course-toolboxes": {
+        "requires": [
+            "node",
+            "base",
+            "event-key",
+            "node",
+            "io",
+            "moodle-course-coursebase",
+            "moodle-course-util"
+        ]
+    }
+}
diff --git a/course/yui/toolboxes/toolboxes.js b/course/yui/toolboxes/toolboxes.js
deleted file mode 100644 (file)
index 9ebc282..0000000
+++ /dev/null
@@ -1,1006 +0,0 @@
-YUI.add('moodle-course-toolboxes', function(Y) {
-
-    // 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 = {
-        ACTIVITYINSTANCE : 'activityinstance',
-        AVAILABILITYINFODIV : 'div.availabilityinfo',
-        CONTENTWITHOUTLINK : 'contentwithoutlink',
-        CONDITIONALHIDDEN : 'conditionalhidden',
-        DIMCLASS : 'dimmed',
-        DIMMEDTEXT : 'dimmed_text',
-        EDITINSTRUCTIONS : 'editinstructions',
-        EDITINGTITLE: 'editor_displayed',
-        HIDE : 'hide',
-        MODINDENTCOUNT : 'mod-indent-',
-        MODINDENTHUGE : 'mod-indent-huge',
-        MODULEIDPREFIX : 'module-',
-        SECTIONHIDDENCLASS : 'hidden',
-        SECTIONIDPREFIX : 'section-',
-        SHOW : 'editing_show',
-        TITLEEDITOR : 'titleeditor'
-    },
-    // The CSS selectors we use.
-    SELECTOR = {
-        ACTIONAREA: '.actions',
-        ACTIONLINKTEXT : '.actionlinktext',
-        ACTIVITYACTION : 'a.cm-edit-action[data-action], a.editing_title',
-        ACTIVITYFORM : '.' + CSS.ACTIVITYINSTANCE + ' form',
-        ACTIVITYICON : 'img.activityicon',
-        ACTIVITYINSTANCE : '.' + CSS.ACTIVITYINSTANCE,
-        ACTIVITYLINK: '.' + CSS.ACTIVITYINSTANCE + ' > a',
-        ACTIVITYLI : 'li.activity',
-        ACTIVITYTITLE : 'input[name=title]',
-        COMMANDSPAN : '.commands',
-        CONTENTAFTERLINK : 'div.contentafterlink',
-        CONTENTWITHOUTLINK : 'div.contentwithoutlink',
-        EDITTITLE: 'a.editing_title',
-        HIDE : 'a.editing_hide',
-        HIGHLIGHT : 'a.editing_highlight',
-        INSTANCENAME : 'span.instancename',
-        MODINDENTDIV : '.mod-indent',
-        MODINDENTOUTER : '.mod-indent-outer',
-        PAGECONTENT : 'div#page-content',
-        SECTIONLI : 'li.section',
-        SHOW : 'a.'+CSS.SHOW,
-        SHOWHIDE : 'a.editing_showhide'
-    },
-    INDENTLIMITS = {
-        MIN: 0,
-        MAX: 16
-    },
-    BODY = Y.one(document.body);
-
-    /**
-     * The toolbox classes
-     *
-     * TOOLBOX is a generic class which should never be directly instantiated
-     * RESOURCETOOLBOX is a class extending TOOLBOX containing code specific to resources
-     * SECTIONTOOLBOX is a class extending TOOLBOX containing code specific to sections
-     */
-    var TOOLBOX = function() {
-        TOOLBOX.superclass.constructor.apply(this, arguments);
-    };
-
-    Y.extend(TOOLBOX, Y.Base, {
-        /**
-         * Send a request using the REST API
-         *
-         * @method send_request
-         * @param data The data to submit
-         * @param statusspinner (optional) A statusspinner which may contain a section loader
-         * @param {Function} success_callback The callback to use on success
-         * @param optionalconfig (optional) Any additional configuration to submit
-         * @return response responseText field from response - please use success_callback instead
-         */
-        send_request : function(data, statusspinner, success_callback, optionalconfig) {
-            // Default data structure
-            if (!data) {
-                data = {};
-            }
-            // Handle any variables which we must pass back through to
-            var pageparams = this.get('config').pageparams,
-                varname;
-            for (varname in pageparams) {
-                data[varname] = pageparams[varname];
-            }
-
-            data.sesskey = M.cfg.sesskey;
-            data.courseId = this.get('courseid');
-
-            var uri = M.cfg.wwwroot + this.get('ajaxurl');
-
-            // Define the configuration to send with the request
-            var responsetext = [];
-            var config = {
-                method: 'POST',
-                data: data,
-                on: {
-                    success: function(tid, response) {
-                        try {
-                            responsetext = Y.JSON.parse(response.responseText);
-                            if (responsetext.error) {
-                                new M.core.ajaxException(responsetext);
-                            }
-                        } catch (e) {}
-
-                        // Run the callback if we have one.
-                        if (success_callback) {
-                            Y.bind(success_callback, this, responsetext)();
-                        }
-
-                        if (statusspinner) {
-                            window.setTimeout(function() {
-                                statusspinner.hide();
-                            }, 400);
-                        }
-                    },
-                    failure : function(tid, response) {
-                        if (statusspinner) {
-                            statusspinner.hide();
-                        }
-                        new M.core.ajaxException(response);
-                    }
-                },
-                context: this
-            };
-
-            // Apply optional config
-            if (optionalconfig) {
-                for (varname in optionalconfig) {
-                    config[varname] = optionalconfig[varname];
-                }
-            }
-
-            if (statusspinner) {
-                statusspinner.show();
-            }
-
-            // Send the request
-            Y.io(uri, config);
-            return responsetext;
-        }
-    },
-    {
-        NAME : 'course-toolbox',
-        ATTRS : {
-            // The ID of the current course
-            courseid : {
-                'value' : 0
-            },
-            ajaxurl : {
-                'value' : 0
-            },
-            config : {
-                'value' : 0
-            }
-        }
-    }
-    );
-
-    /**
-     * 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, {
-        /**
-         * 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
-         *
-         * 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() {
-            M.course.coursebase.register_module(this);
-            BODY.delegate('key', this.handle_data_action, 'down:enter', SELECTOR.ACTIVITYACTION, this);
-            Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this);
-        },
-
-        /**
-         * Handles the delegation event. When this is fired someone has triggered an action.
-         *
-         * 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}
-         */
-        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);
-            }
-
-            // 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.ancestor(SELECTOR.ACTIVITYLI);
-
-            if (!node.test('a') || !action || !activity) {
-                // It wasn't a valid action node.
-                return;
-            }
-            Y.log(ev.type);
-
-            // 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 'duplicate' :
-                    // The user is duplicating the activity.
-                    this.duplicate(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;
-            }
-        },
-        add_spinner: function(activity) {
-            var actionarea = activity.one(SELECTOR.ACTIONAREA);
-            return M.util.add_spinner(Y, actionarea);
-        },
-
-        /**
-         * 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
-            ev.preventDefault();
-
-            var direction = (action === 'moveleft') ? -1 : 1;
-
-            // And we need to determine the current and new indent level
-            var indentdiv = activity.one(SELECTOR.MODINDENTDIV),
-                indent = indentdiv.getAttribute('class').match(/mod-indent-(\d{1,})/),
-                oldindent = 0,
-                newindent;
-
-            if (indent) {
-                oldindent = parseInt(indent[1], 10);
-            }
-            newindent = oldindent + parseInt(direction, 10);
-
-            if (newindent < INDENTLIMITS.MIN || newindent > INDENTLIMITS.MAX) {
-                return;
-            }
-
-            if (indent) {
-                indentdiv.removeClass(indent[0]);
-            }
-
-            // Perform the move
-            indentdiv.addClass(CSS.MODINDENTCOUNT + newindent);
-            var data = {
-                'class' : 'resource',
-                'field' : 'indent',
-                'value' : newindent,
-                'id'    : Y.Moodle.core_course.util.cm.getId(activity)
-            };
-            var spinner = this.add_spinner(activity);
-            this.send_request(data, spinner);
-
-            var remainingmove;
-
-            // Handle removal/addition of the moveleft button.
-            if (newindent === INDENTLIMITS.MIN) {
-                button.addClass('hidden');
-                remainingmove = activity.one('.editing_moveright');
-            } else if (newindent > INDENTLIMITS.MIN && oldindent === INDENTLIMITS.MIN) {
-                button.ancestor('.menu').one('[data-action=moveleft]').removeClass('hidden');
-            }
-
-            if (newindent === INDENTLIMITS.MAX) {
-                button.addClass('hidden');
-                remainingmove = activity.one('.editing_moveleft');
-            } else if (newindent < INDENTLIMITS.MAX && oldindent === INDENTLIMITS.MAX) {
-                button.ancestor('.menu').one('[data-action=moveright]').removeClass('hidden');
-            }
-
-            // Handle massive indentation to match non-ajax display
-            var hashugeclass = indentdiv.hasClass(CSS.MODINDENTHUGE);
-            if (newindent > 15 && !hashugeclass) {
-                indentdiv.addClass(CSS.MODINDENTHUGE);
-            } else if (newindent <= 15 && hashugeclass) {
-                indentdiv.removeClass(CSS.MODINDENTHUGE);
-            }
-
-            if (ev.type && ev.type === "key" && remainingmove) {
-                remainingmove.focus();
-            }
-        },
-
-        /**
-         * 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
-            ev.preventDefault();
-
-            // Get the element we're working on
-            var element   = activity
-
-            // Create confirm string (different if element has or does not have name)
-            var confirmstring = '';
-            var plugindata = {
-                type : M.util.get_string('pluginname', element.getAttribute('class').match(/modtype_([^\s]*)/)[1])
-            }
-            if (Y.Moodle.core_course.util.cm.getName(element) != null) {
-                plugindata.name = Y.Moodle.core_course.util.cm.getName(element)
-                confirmstring = M.util.get_string('deletechecktypename', 'moodle', plugindata);
-            } else {
-                confirmstring = M.util.get_string('deletechecktype', 'moodle', plugindata)
-            }
-
-            // Confirm element removal
-            if (!confirm(confirmstring)) {
-                return false;
-            }
-
-            // Actually remove the element
-            element.remove();
-            var data = {
-                'class' : 'resource',
-                'action' : 'DELETE',
-                'id'    : Y.Moodle.core_course.util.cm.getId(element)
-            };
-            this.send_request(data);
-            if (M.core.actionmenu && M.core.actionmenu.instance) {
-                M.core.actionmenu.instance.hideMenu();
-            }
-        },
-
-        /**
-         * Duplicates the activity
-         *
-         * @protected
-         * @method duplicate
-         * @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
-         */
-        duplicate : function(ev, button, activity) {
-            // Prevent the default button action
-            ev.preventDefault();
-
-            // Get the element we're working on
-            var element = activity;
-
-            // Add the lightbox.
-            var section = activity.ancestor(M.course.format.get_section_selector(Y)),
-                lightbox = M.util.add_lightbox(Y, section).show();
-
-            // Build and send the request.
-            var data = {
-                'class' : 'resource',
-                'field' : 'duplicate',
-                'id'    : Y.Moodle.core_course.util.cm.getId(element),
-                'sr'    : button.getData('sr')
-            };
-            this.send_request(data, lightbox, function(response) {
-                var newcm = Y.Node.create(response.fullcontent);
-
-                // Append to the section?
-                activity.insert(newcm, 'after');
-                Y.use('moodle-course-coursebase', function() {
-                    M.course.coursebase.invoke_function('setup_for_resource', newcm);
-                });
-                if (M.core.actionmenu && M.core.actionmenu.newDOMNode) {
-                    M.core.actionmenu.newDOMNode(newcm);
-                }
-            });
-        },
-
-        /**
-         * 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
-            ev.preventDefault();
-
-            // Return early if the current section is hidden
-            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 = activity;
-            var value = this.handle_resource_dim(button, activity, action);
-
-            // Send the request
-            var data = {
-                'class' : 'resource',
-                'field' : 'visible',
-                'value' : value,
-                'id'    : Y.Moodle.core_course.util.cm.getId(element)
-            };
-            var spinner = this.add_spinner(element);
-            this.send_request(data, spinner);
-            return false; // Need to return false to stop the delegate for the new state firing
-        },
-
-        /**
-         * 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} action 'show' or 'hide'.
-         * @returns {number} 1 if we changed to visible, 0 if we were hiding.
-         */
-        handle_resource_dim : function(button, activity, action) {
-            var toggleclass = CSS.DIMCLASS,
-                dimarea = activity.one([
-                        SELECTOR.ACTIVITYLINK,
-                        SELECTOR.CONTENTWITHOUTLINK
-                    ].join(', ')),
-                availabilityinfo = activity.one(CSS.AVAILABILITYINFODIV),
-                nextaction = (action === 'hide') ? 'show' : 'hide',
-                buttontext = button.one('span'),
-                newstring = M.util.get_string(nextaction, 'moodle'),
-                buttonimg = button.one('img');
-
-            // Update button info.
-            buttonimg.setAttrs({
-                'src'   : M.util.image_url('t/' + nextaction)
-            });
-
-            if (Y.Lang.trim(button.getAttribute('title'))) {
-                button.setAttribute('title', newstring);
-            }
-
-            if (Y.Lang.trim(buttonimg.getAttribute('alt'))) {
-                buttonimg.setAttribute('alt', newstring);
-            }
-
-            button.replaceClass('editing_'+action, 'editing_'+nextaction);
-            button.setData('action', nextaction);
-            if (buttontext) {
-                buttontext.set('text', newstring);
-            }
-
-            if (activity.one(SELECTOR.CONTENTWITHOUTLINK)) {
-                dimarea = activity.one(SELECTOR.CONTENTWITHOUTLINK);
-                toggleclass = CSS.DIMMEDTEXT;
-            }
-
-            // If activity is conditionally hidden, then don't toggle.
-            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 (action === '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 = parseInt(button.getData('nextgroupmode'), 10),
-                newtitle = '',
-                iconsrc = '',
-                newtitlestr,
-                data,
-                spinner,
-                nextgroupmode = groupmode + 1,
-                buttonimg = button.one('img');
-
-            if (nextgroupmode > 2) {
-                nextgroupmode = 0;
-            }
-
-            if (groupmode === this.GROUPS_NONE) {
-                newtitle = 'groupsnone';
-                iconsrc = M.util.image_url('i/groupn', 'moodle');
-            } else if (groupmode === this.GROUPS_SEPARATE) {
-                newtitle = 'groupsseparate';
-                iconsrc = M.util.image_url('i/groups', 'moodle');
-            } else if (groupmode === this.GROUPS_VISIBLE) {
-                newtitle = 'groupsvisible';
-                iconsrc = M.util.image_url('i/groupv', 'moodle');
-            }
-            newtitlestr = M.util.get_string(newtitle, 'moodle'),
-            newtitlestr = M.util.get_string('clicktochangeinbrackets', 'moodle', newtitlestr);
-
-            // Change the UI
-            buttonimg.setAttrs({
-                'src' : iconsrc
-            });
-            if (Y.Lang.trim(button.getAttribute('title'))) {
-                button.setAttribute('title', newtitlestr).setData('action', newtitle).setData('nextgroupmode', nextgroupmode);
-            }
-
-            if (Y.Lang.trim(buttonimg.getAttribute('alt'))) {
-                buttonimg.setAttribute('alt', newtitlestr);
-            }
-
-            // And send the request
-            data = {
-                'class' : 'resource',
-                'field' : 'groupmode',
-                'value' : groupmode,
-                'id'    : Y.Moodle.core_course.util.cm.getId(activity)
-            };
-
-            spinner = this.add_spinner(activity);
-            this.send_request(data, spinner);
-            return false; // Need to return false to stop the delegate for the new state firing
-        },
-
-        /**
-         * 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_title : function(ev, button, activity) {
-            // Get the element we're working on
-            var activityid = Y.Moodle.core_course.util.cm.getId(activity),
-                instancename  = activity.one(SELECTOR.INSTANCENAME),
-                instance = activity.one(SELECTOR.ACTIVITYINSTANCE),
-                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
-                };
-
-            // Prevent the default actions.
-            ev.preventDefault();
-
-            this.send_request(data, null, function(response) {
-                if (M.core.actionmenu && M.core.actionmenu.instance) {
-                    M.core.actionmenu.instance.hideMenu();
-                }
-
-                // Try to retrieve the existing string from the server
-                if (response.instancename) {
-                    titletext = response.instancename;
-                }
-
-                // Create the editor and submit button
-                var editform = Y.Node.create('<form action="#" />');
-                var editinstructions = Y.Node.create('<span class="'+CSS.EDITINSTRUCTIONS+'" id="id_editinstructions" />')
-                    .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
-                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
-                editform.appendChild(activity.one(SELECTOR.ACTIVITYICON).cloneNode());
-                editform.appendChild(editor);
-                editform.setData('anchor', anchor);
-                instance.insert(editinstructions, 'before');
-                anchor.replace(editform);
-
-                // Force the editing instruction to match the mod-indent position.
-                var padside = 'left';
-                if (right_to_left()) {
-                    padside = 'right';
-                }
-
-                // We hide various components whilst editing:
-                activity.addClass(CSS.EDITINGTITLE);
-
-                // Focus and select the editor text
-                editor.focus().select();
-
-                // 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);
-
-                // 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 = this.add_spinner(activity);
-            if (newtitle != null && newtitle != "" && newtitle != originaltitle) {
-                var data = {
-                    'class'   : 'resource',
-                    'field'   : 'updatetitle',
-                    'title'   : newtitle,
-                    'id'      : Y.Moodle.core_course.util.cm.getId(activity)
-                };
-                this.send_request(data, spinner, function(response) {
-                    if (response.instancename) {
-                        activity.one(SELECTOR.INSTANCENAME).setContent(response.instancename);
-                    }
-                });
-            }
-        },
-
-        /**
-         * 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);
-        },
-
-        /**
-         * 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();
-            }
-
-            // Remove the editing class again to revert the display.
-            activity.removeClass(CSS.EDITINGTITLE);
-
-            // Refocus the link which was clicked originally so the user can continue using keyboard nav.
-            Y.later(100, this, function() {
-                activity.one(SELECTOR.EDITTITLE).focus();
-            });
-        },
-
-        /**
-         * 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 visibility should be set to.
-         */
-        set_visibility_resource_ui: function(args) {
-            var element = args.element,
-                shouldbevisible = args.visible,
-                buttonnode = element.one(SELECTOR.SHOW),
-                visible = (buttonnode === null),
-                action = 'show';
-            if (visible) {
-                buttonnode = element.one(SELECTOR.HIDE);
-                action = 'hide';
-            }
-            if (visible != shouldbevisible) {
-                this.handle_resource_dim(buttonnode, element, action);
-            }
-        }
-    }, {
-        NAME : 'course-resource-toolbox',
-        ATTRS : {
-            courseid : {
-                'value' : 0
-            },
-            format : {
-                'value' : 'topics'
-            }
-        }
-    });
-
-    var SECTIONTOOLBOX = function() {
-        SECTIONTOOLBOX.superclass.constructor.apply(this, arguments);
-    }
-
-    Y.extend(SECTIONTOOLBOX, TOOLBOX, {
-        /**
-         * Initialize the toolboxes module
-         *
-         * Updates all span.commands with relevant handlers and other required changes
-         */
-        initializer : function(config) {
-            this.setup_for_section();
-            M.course.coursebase.register_module(this);
-
-            // Section Highlighting
-            Y.delegate('click', this.toggle_highlight, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.HIGHLIGHT, this);
-            // Section Visibility
-            Y.delegate('click', this.toggle_hide_section, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.SHOWHIDE, this);
-        },
-        /**
-         * Update any section areas within the scope of the specified
-         * selector with AJAX equivelants
-         *
-         * @param baseselector The selector to limit scope to
-         * @return void
-         */
-        setup_for_section : function(baseselector) {
-            // Left here for potential future use - not currently needed due to YUI delegation in initializer()
-            /*if (!baseselector) {
-                var baseselector = SELECTOR.PAGECONTENT;
-            }
-
-            Y.all(baseselector).each(this._setup_for_section, this);*/
-        },
-        _setup_for_section : function(toolboxtarget) {
-            // Left here for potential future use - not currently needed due to YUI delegation in initializer()
-        },
-        toggle_hide_section : function(e) {
-            // Prevent the default button action
-            e.preventDefault();
-
-            // Get the section we're working on
-            var section = e.target.ancestor(M.course.format.get_section_selector(Y));
-            var button = e.target.ancestor('a', true);
-            var hideicon = button.one('img');
-
-            // The value to submit
-            var value;
-            // The text for strings and images. Also determines the icon to display.
-            var action,
-                nextaction;
-
-            if (!section.hasClass(CSS.SECTIONHIDDENCLASS)) {
-                section.addClass(CSS.SECTIONHIDDENCLASS);
-                value = 0;
-                action = 'hide';
-                nextaction = 'show';
-            } else {
-                section.removeClass(CSS.SECTIONHIDDENCLASS);
-                value = 1;
-                action = 'show';
-                nextaction = 'hide';
-            }
-
-            var newstring = M.util.get_string(nextaction + 'fromothers', 'format_' + this.get('format'));
-            hideicon.setAttrs({
-                'alt' : newstring,
-                'src'   : M.util.image_url('i/' + nextaction)
-            });
-            button.set('title', newstring);
-
-            // Change the highlight status
-            var data = {
-                'class' : 'section',
-                'field' : 'visible',
-                'id'    : Y.Moodle.core_course.util.section.getId(section.ancestor(M.course.format.get_section_wrapper(Y), true)),
-                'value' : value
-            };
-
-            var lightbox = M.util.add_lightbox(Y, section);
-            lightbox.show();
-
-            this.send_request(data, lightbox, function(response) {
-                var activities = section.all(SELECTOR.ACTIVITYLI);
-                activities.each(function(node) {
-                    var button;
-                    if (node.one(SELECTOR.SHOW)) {
-                        button = node.one(SELECTOR.SHOW);
-                    } else {
-                        button = node.one(SELECTOR.HIDE);
-                    }
-                    var activityid = Y.Moodle.core_course.util.cm.getId(node);
-
-                    // NOTE: resourcestotoggle is returned as a string instead
-                    // of a Number so we must cast our activityid to a String.
-                    if (Y.Array.indexOf(response.resourcestotoggle, "" + activityid) !== -1) {
-                        M.course.resource_toolbox.handle_resource_dim(button, node, action);
-                    }
-                }, this);
-            });
-        },
-        toggle_highlight : function(e) {
-            // Prevent the default button action
-            e.preventDefault();
-
-            // Get the section we're working on
-            var section = e.target.ancestor(M.course.format.get_section_selector(Y));
-            var button = e.target.ancestor('a', true);
-            var buttonicon = button.one('img');
-
-            // Determine whether the marker is currently set
-            var togglestatus = section.hasClass('current');
-            var value = 0;
-
-            // Set the current highlighted item text
-            var old_string = M.util.get_string('markthistopic', 'moodle');
-            Y.one(SELECTOR.PAGECONTENT)
-                .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT)
-                .set('title', old_string);
-            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(SELECTOR.PAGECONTENT).all(M.course.format.get_section_selector(Y))
-                .removeClass('current');
-
-            // Then add it if required to the selected section
-            if (!togglestatus) {
-                section.addClass('current');
-                value = Y.Moodle.core_course.util.section.getId(section.ancestor(M.course.format.get_section_wrapper(Y), true));
-                var new_string = M.util.get_string('markedthistopic', 'moodle');
-                button
-                    .set('title', new_string);
-                buttonicon
-                    .set('alt', new_string)
-                    .set('src', M.util.image_url('i/marked'));
-            }
-
-            // Change the highlight status
-            var data = {
-                'class' : 'course',
-                'field' : 'marker',
-                'value' : value
-            };
-            var lightbox = M.util.add_lightbox(Y, section);
-            lightbox.show();
-            this.send_request(data, lightbox);
-        }
-    }, {
-        NAME : 'course-section-toolbox',
-        ATTRS : {
-            courseid : {
-                'value' : 0
-            },
-            format : {
-                'value' : 'topics'
-            }
-        }
-    });
-
-    M.course = M.course || {};
-    M.course.resource_toolbox = null;
-    M.course.init_resource_toolbox = function(config) {
-        M.course.resource_toolbox = new RESOURCETOOLBOX(config);
-        return M.course.resource_toolbox;
-    };
-
-    M.course.init_section_toolbox = function(config) {
-        return new SECTIONTOOLBOX(config);
-    };
-
-},
-'@VERSION@', {
-    requires : ['base', 'event-key', 'node', 'io', 'moodle-course-coursebase', 'moodle-course-util']
-}
-);
index 606def8..f536bb2 100644 (file)
@@ -24,6 +24,8 @@
 
 $string['allowguests'] = 'This course allows guest users to enter';
 $string['guest:config'] = 'Configure guest access instances';
+$string['guestaccess_withpassword'] = 'Guest access requires password';
+$string['guestaccess_withoutpassword'] = 'Guest access';
 $string['password'] = 'Password';
 $string['password_help'] = 'A password allows guest access to the course to be restricted to only those who know the password. Guests will be required to supply the password each time they access the course.';
 $string['passwordinvalid'] = 'Incorrect access password, please try again';
index 65444bd..2f0d444 100644 (file)
@@ -44,9 +44,9 @@ class enrol_guest_plugin extends enrol_plugin {
     public function get_info_icons(array $instances) {
         foreach ($instances as $instance) {
             if ($instance->password !== '') {
-                return array(new pix_icon('withpassword', get_string('pluginname', 'enrol_guest'), 'enrol_guest'));
+                return array(new pix_icon('withpassword', get_string('guestaccess_withpassword', 'enrol_guest'), 'enrol_guest'));
             } else {
-                return array(new pix_icon('withoutpassword', get_string('pluginname', 'enrol_guest'), 'enrol_guest'));
+                return array(new pix_icon('withoutpassword', get_string('guestaccess_withoutpassword', 'enrol_guest'), 'enrol_guest'));
             }
         }
     }
diff --git a/enrol/yui/notification/notification.js b/enrol/yui/notification/notification.js
deleted file mode 100644 (file)
index f2bf542..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * This module provides backwards compatability and should be removed
- * entirely in Moodle 2.5
- */
-YUI.add('moodle-enrol-notification', function(Y) {
-    console.log("You are using a deprecated name. Please update your YUI module to use moodle-core-notification instead of moodle-enrol-notification");
-}, '@VERSION@', {requires:['base','node','overlay','event-key', 'moodle-core-notification']});
index 83c2c54..fef76b1 100644 (file)
@@ -91,7 +91,7 @@ function UpdatableMembersCombo(wwwRoot, courseId) {
     this.courseId = courseId;
 
     this.connectCallback = {
-        success: function(o) {
+        success: function(t, o) {
 
             if (o.responseText !== undefined) {
                 var selectEl = document.getElementById("members");
@@ -124,7 +124,7 @@ function UpdatableMembersCombo(wwwRoot, courseId) {
             removeLoaderImgs("membersloader", "memberslabel");
         },
 
-        failure: function(o) {
+        failure: function() {
             removeLoaderImgs("membersloader", "memberslabel");
         }
 
@@ -185,9 +185,13 @@ UpdatableMembersCombo.prototype.refreshMembers = function () {
 
     if(singleSelection) {
         var sUrl = this.wwwRoot+"/group/index.php?id="+this.courseId+"&group="+groupId+"&act_ajax_getmembersingroup";
-        var callback = this.connectCallback;
-        YUI().use('yui2-connection', function (Y) {
-            Y.YUI2.util.Connect.asyncRequest("GET", sUrl, callback, null);
+        var self = this;
+        YUI().use('io', function (Y) {
+            Y.io(sUrl, {
+                method: 'GET',
+                context: this,
+                on: self.connectCallback
+            });
         });
     }
 };
@@ -271,4 +275,4 @@ function init_add_remove_members_page(Y) {
 
     addselect = document.getElementById('addselect');
     addselect.onchange = updateUserSummary;
-}
\ No newline at end of file
+}
index 2afb6f3..f32ce79 100644 (file)
@@ -67,7 +67,7 @@ class behat_groups extends behat_base {
         $this->find_button(get_string('adduserstogroup', 'group'))->click();
 
         // Wait for add/remove members page to be loaded.
-        $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")');
+        $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
 
         // Getting the option and selecting it.
         $select = $this->find_field('addselect');
@@ -80,7 +80,7 @@ class behat_groups extends behat_base {
         $this->find_button(get_string('add'))->click();
 
         // Wait for the page to load.
-        $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")');
+        $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
 
         // Returning to the main groups page.
         $this->find_button(get_string('backtogroups', 'group'))->click();
index c6e3f84..7ec7622 100644 (file)
@@ -40,12 +40,10 @@ Feature: Organize students into groups
     And I add "student2" user to "Group 2" group
     And I add "student3" user to "Group 2" group
     Then I select "Group 1 (2)" from "groups"
-    And I wait "5" seconds
     And the "members" select box should contain "Student 0"
     And the "members" select box should contain "Student 1"
     And the "members" select box should not contain "Student 2"
     And I select "Group 2 (2)" from "groups"
-    And I wait "5" seconds
     And the "members" select box should contain "Student 2"
     And the "members" select box should contain "Student 3"
     And the "members" select box should not contain "Student 0"
index 57aca33..ed021a6 100644 (file)
@@ -1,4 +1,4 @@
-@core @core_group
+@core @core_group @_only_local
 Feature: Importing of groups and groupings
   In order to import groups and grouping
   As a teacher
@@ -22,7 +22,7 @@ Feature: Importing of groups and groupings
     And I expand "Users" node
     And I follow "Groups"
     And I press "Import groups"
-    When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filepicker
+    When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filemanager
     And I press "Import groups"
     And I press "Continue"
     Then I should see "group-id-1"
@@ -47,7 +47,7 @@ Feature: Importing of groups and groupings
     And I expand "Users" node
     And I follow "Groups"
     And I press "Import groups"
-    When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filepicker
+    When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filemanager
     And I press "Import groups"
     Then I should see "Group group-id-1 added successfully"
     And I should see "Group group-id-2 added successfully"
@@ -91,7 +91,7 @@ Feature: Importing of groups and groupings
     And I expand "Users" node
     And I follow "Groups"
     And I press "Import groups"
-    When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filepicker
+    When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filemanager
     And I press "Import groups"
     And I press "Continue"
     Then I select "group-id-1" from "groups"
index a6bc7af..7006d1a 100644 (file)
@@ -95,6 +95,8 @@ $string['cachejs_help'] = 'Javascript caching and compression greatly improves p
 $string['cachetext'] = 'Text cache lifetime';
 $string['calendarexportsalt'] = 'Calendar export salt';
 $string['calendarsettings'] = 'Calendar';
+$string['calendartype'] = 'Calendar type';
+$string['calendartype_desc'] = 'Choose a default calendar type for the whole site. This setting can be overridden in the course settings or by users in their personal profile.';
 $string['calendar_weekend'] = 'Weekend days';
 $string['cannotdeletemodfilter'] = 'You cannot uninstall the \'{$a->filter}\' because it is part of the \'{$a->module}\' module.';
 $string['cannotuninstall'] = '{$a} can not be uninstalled.';
index 387d439..d20f174 100644 (file)
@@ -723,6 +723,7 @@ $string['eventcourseresetstarted'] = 'Course reset started';
 $string['eventcourserestored'] = 'Course restored';
 $string['eventcourseupdated'] = 'Course updated';
 $string['eventcoursesectionupdated'] = ' Course section updated';
+$string['eventcoursemoduleinstancelistviewed'] = 'Course module instance list viewed';
 $string['eventusercreated'] = 'User created';
 $string['eventuserdeleted'] = 'User deleted';
 $string['eventuserlistviewed'] = 'User list viewed';
@@ -1333,7 +1334,6 @@ $string['numyears'] = '{$a} years';
 $string['ok'] = 'OK';
 $string['oldpassword'] = 'Current password';
 $string['olduserdirectory'] = 'This is the OLD users directory, and is no longer needed. You may safely delete it. The files it contains have been copied to the NEW user directory.';
-$string['opentoguests'] = 'Guest access';
 $string['optional'] = 'optional';
 $string['options'] = 'options';
 $string['order'] = 'Order';
index 449a88e..a190faa 100644 (file)
@@ -6741,11 +6741,8 @@ function db_replace($search, $replace) {
 
         if ($columns = $DB->get_columns($table)) {
             $DB->set_debug(true);
-            foreach ($columns as $column => $data) {
-                if (in_array($data->meta_type, array('C', 'X'))) {  // Text stuff only
-                    //TODO: this should be definitively moved to DML driver to do the actual replace, this is not going to work for MSSQL and Oracle...
-                    $DB->execute("UPDATE {".$table."} SET $column = REPLACE($column, ?, ?)", array($search, $replace));
-                }
+            foreach ($columns as $column) {
+                $DB->replace_all_text($table, $column, $search, $replace);
             }
             $DB->set_debug(false);
         }
@@ -6776,6 +6773,8 @@ function db_replace($search, $replace) {
         echo $OUTPUT->notification("...finished", 'notifysuccess');
     }
 
+    purge_all_caches();
+
     return true;
 }
 
index 6278ff7..1290493 100644 (file)
@@ -29,7 +29,8 @@
 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 
 use Behat\Mink\Exception\ExpectationException as ExpectationException,
-    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
+    Behat\Mink\Element\NodeElement as NodeElement;
 
 /**
  * Steps definitions base class.
@@ -39,6 +40,11 @@ use Behat\Mink\Exception\ExpectationException as ExpectationException,
  * It can not contain steps definitions to avoid duplicates, only utility
  * methods shared between steps.
  *
+ * @method NodeElement find_field(string $locator) Finds a form element
+ * @method NodeElement find_button(string $locator) Finds a form input submit element or a button
+ * @method NodeElement find_link(string $locator) Finds a link on a page
+ * @method NodeElement find_file(string $locator) Finds a forum input file element
+ *
  * @package   core
  * @category  test
  * @copyright 2012 David Monllaó
@@ -49,7 +55,17 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
     /**
      * The timeout for each Behat step (load page, wait for an element to load...).
      */
-    const TIMEOUT = 6;
+    const TIMEOUT = 3;
+
+    /**
+     * And extended timeout for specific cases.
+     */
+    const EXTENDED_TIMEOUT = 10;
+
+    /**
+     * The JS code to check that the page is ready.
+     */
+    const PAGE_READY_JS = '(M && M.util && M.util.pending_js && !Boolean(M.util.pending_js.length)) && (document.readyState === "complete")';
 
     /**
      * Locates url, based on provided path.
@@ -414,4 +430,194 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
         return get_class($this->getSession()->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver';
     }
 
+    /**
+     * Spins around an element until it exists
+     *
+     * @throws ExpectationException
+     * @param string $element
+     * @param string $selectortype
+     * @return void
+     */
+    protected function ensure_element_exists($element, $selectortype) {
+
+        // Getting the behat selector & locator.
+        list($selector, $locator) = $this->transform_selector($selectortype, $element);
+
+        // Exception if it timesout and the element is still there.
+        $msg = 'The "' . $element . '" element does not exist and should exist';
+        $exception = new ExpectationException($msg, $this->getSession());
+
+        // It will stop spinning once the find() method returns true.
+        $this->spin(
+            function($context, $args) {
+                // We don't use behat_base::find as it is already spinning.
+                if ($context->getSession()->getPage()->find($args['selector'], $args['locator'])) {
+                    return true;
+                }
+                return false;
+            },
+            array('selector' => $selector, 'locator' => $locator),
+            self::EXTENDED_TIMEOUT,
+            $exception,
+            true
+        );
+
+    }
+
+    /**
+     * Spins until the element does not exist
+     *
+     * @throws ExpectationException
+     * @param string $element
+     * @param string $selectortype
+     * @return void
+     */
+    protected function ensure_element_does_not_exist($element, $selectortype) {
+
+        // Getting the behat selector & locator.
+        list($selector, $locator) = $this->transform_selector($selectortype, $element);
+
+        // Exception if it timesout and the element is still there.
+        $msg = 'The "' . $element . '" element exists and should not exist';
+        $exception = new ExpectationException($msg, $this->getSession());
+
+        // It will stop spinning once the find() method returns false.
+        $this->spin(
+            function($context, $args) {
+                // We don't use behat_base::find() as we are already spinning.
+                if (!$context->getSession()->getPage()->find($args['selector'], $args['locator'])) {
+                    return true;
+                }
+                return false;
+            },
+            array('selector' => $selector, 'locator' => $locator),
+            self::EXTENDED_TIMEOUT,
+            $exception,
+            true
+        );
+    }
+
+    /**
+     * Ensures that the provided node is visible and we can interact with it.
+     *
+     * @throws ExpectationException
+     * @param NodeElement $node
+     * @return void Throws an exception if it times out without the element being visible
+     */
+    protected function ensure_node_is_visible($node) {
+
+        if (!$this->running_javascript()) {
+            return;
+        }
+
+        // Exception if it timesout and the element is still there.
+        $msg = 'The "' . $node->getXPath() . '" xpath node is not visible and it should be visible';
+        $exception = new ExpectationException($msg, $this->getSession());
+
+        // It will stop spinning once the isVisible() method returns true.
+        $this->spin(
+            function($context, $args) {
+                if ($args->isVisible()) {
+                    return true;
+                }
+                return false;
+            },
+            $node,
+            self::EXTENDED_TIMEOUT,
+            $exception,
+            true
+        );
+    }
+
+    /**
+     * Ensures that the provided element is visible and we can interact with it.
+     *
+     * Returns the node in case other actions are interested in using it.
+     *
+     * @throws ExpectationException
+     * @param string $element
+     * @param string $selectortype
+     * @return NodeElement Throws an exception if it times out without being visible
+     */
+    protected function ensure_element_is_visible($element, $selectortype) {
+
+        if (!$this->running_javascript()) {
+            return;
+        }
+
+        $node = $this->get_selected_node($selectortype, $element);
+        $this->ensure_node_is_visible($node);
+
+        return $node;
+    }
+
+    /**
+     * Ensures that all the page's editors are loaded.
+     *
+     * This method is expensive as it waits for .mceEditor CSS
+     * so use with caution and only where there will be editors.
+     *
+     * @throws ElementNotFoundException
+     * @throws ExpectationException
+     * @return void
+     */
+    protected function ensure_editors_are_loaded() {
+
+        if (!$this->running_javascript()) {
+            return;
+        }
+
+        // If there are no editors we don't need to wait.
+        try {
+            $this->find('css', '.mceEditor');
+        } catch (ElementNotFoundException $e) {
+            return;
+        }
+
+        // Exception if it timesout and the element is not appearing.
+        $msg = 'The editors are not completely loaded';
+        $exception = new ExpectationException($msg, $this->getSession());
+
+        // Here we know that there are .mceEditor editors in the page and we will
+        // probably need to interact with them, if we use tinyMCE JS var before
+        // it exists it will throw an exception and we want to catch it until all
+        // the page's editors are ready to interact with them.
+        $this->spin(
+            function($context) {
+
+                // It may return 0 if tinyMCE is loaded but not the instances, so we just loop again.
+                $neditors = $context->getSession()->evaluateScript('return tinyMCE.editors.length;');
+                if ($neditors == 0) {
+                    return false;
+                }
+
+                // It may be there but not ready.
+                $iframeready = $context->getSession()->evaluateScript('
+                    var readyeditors = new Array;
+                    for (editorid in tinyMCE.editors) {
+                        if (tinyMCE.editors[editorid].getDoc().readyState === "complete") {
+                            readyeditors[editorid] = editorid;
+                        }
+                    }
+                    if (tinyMCE.editors.length === readyeditors.length) {
+                        return "complete";
+                    }
+                    return "";
+                ');
+
+                // Now we know that the editors are there.
+                if ($iframeready) {
+                    return true;
+                }
+
+                // Loop again if it is not ready.
+                return false;
+            },
+            false,
+            self::EXTENDED_TIMEOUT,
+            $exception,
+            true
+        );
+    }
+
 }
index 3ee17b6..a186574 100644 (file)
@@ -32,7 +32,7 @@
 require_once(__DIR__ . '/behat_base.php');
 
 use Behat\Mink\Exception\ExpectationException as ExpectationException,
-    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+    Behat\Mink\Element\NodeElement as NodeElement;
 
 /**
  * Files-related actions.
@@ -49,9 +49,9 @@ use Behat\Mink\Exception\ExpectationException as ExpectationException,
 class behat_files extends behat_base {
 
     /**
-     * Gets the filepicker NodeElement.
+     * Gets the NodeElement for filepicker of filemanager moodleform element.
      *
-     * The filepicker field label is pointing to a hidden input which is
+     * The filepicker/filemanager element label is pointing to a hidden input which is
      * not recognized as a named selector, as it is hidden...
      *
      * @throws ExpectationException Thrown by behat_base::find
@@ -77,7 +77,7 @@ class behat_files extends behat_base {
     }
 
     /**
-     * Performs $action on a filepicker container element (file or folder).
+     * Performs $action on a filemanager container element (file or folder).
      *
      * It works together with open_element_contextual_menu
      * as this method needs the contextual menu to be opened.
@@ -93,28 +93,32 @@ class behat_files extends behat_base {
         $classname = 'fp-file-' . $action;
         $button = $this->find('css', '.moodle-dialogue-focused button.' . $classname, $exception);
 
+        $this->ensure_node_is_visible($button);
         $button->click();
     }
 
     /**
      * Opens the contextual menu of a folder or a file.
      *
-     * Works both in filepicker elements and when dealing with repository
-     * elements inside modal windows.
+     * Works both in filemanager elements and when dealing with repository
+     * elements inside filepicker modal window.
      *
      * @throws ExpectationException Thrown by behat_base::find
      * @param string $name The name of the folder/file
-     * @param string $filepickerelement The filepicker locator, the whole DOM if false
+     * @param string $filemanagerelement The filemanager form element locator, the repository items are in filepicker modal window if false
      * @return void
      */
-    protected function open_element_contextual_menu($name, $filepickerelement = false) {
+    protected function open_element_contextual_menu($name, $filemanagerelement = false) {
 
-        // If a filepicker is specified we restrict the search to the filepicker descendants.
+        // If a filemanager is specified we restrict the search to the descendants of this particular filemanager form element.
         $containernode = false;
         $exceptionmsg = '"'.$name.'" element can not be found';
-        if ($filepickerelement) {
-            $containernode = $this->get_filepicker_node($filepickerelement);
-            $exceptionmsg = 'The "'.$filepickerelement.'" filepicker ' . $exceptionmsg;
+        if ($filemanagerelement) {
+            $containernode = $this->get_filepicker_node($filemanagerelement);
+            $exceptionmsg = 'The "'.$filemanagerelement.'" filemanager ' . $exceptionmsg;
+            $locatorprefix = "//div[@class='fp-content']";
+        } else {
+            $locatorprefix = "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-items ')]//descendant::div[@class='fp-content']";
         }
 
         $exception = new ExpectationException($exceptionmsg, $this->getSession());
@@ -122,13 +126,13 @@ class behat_files extends behat_base {
         // Avoid quote-related problems.
         $name = $this->getSession()->getSelectorsHandler()->xpathLiteral($name);
 
-        // Get a filepicker element (folder or file).
+        // Get a filepicker/filemanager element (folder or file).
         try {
 
             // First we look at the folder as we need to click on the contextual menu otherwise it would be opened.
             $node = $this->find(
                 'xpath',
-                "//div[@class='fp-content']" .
+                $locatorprefix .
                     "//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]" .
                     "[contains(concat(' ', normalize-space(@class), ' '), ' fp-folder ')]" .
                     "[normalize-space(.)=$name]" .
@@ -142,40 +146,42 @@ class behat_files extends behat_base {
             // Here the contextual menu is hidden, we click on the thumbnail.
             $node = $this->find(
                 'xpath',
-                "//div[@class='fp-content']" .
+                $locatorprefix .
                 "//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]" .
                 "[normalize-space(.)=$name]" .
-                "//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-thumbnail ')]",
+                "//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-filename-field ')]",
                 false,
                 $containernode
             );
         }
 
         // Click opens the contextual menu when clicking on files.
+        $this->ensure_node_is_visible($node);
         $node->click();
     }
 
     /**
-     * Opens the 'add file' modal window and selects the repository.
+     * Opens the filepicker modal window and selects the repository.
      *
      * @throws ExpectationException Thrown by behat_base::find
-     * @param NodeElement $filepickernode The filepicker DOM node.
+     * @param NodeElement $filemanagernode The filemanager or filepicker form element DOM node.
      * @param mixed $repositoryname The repo name.
      * @return void
      */
-    protected function open_add_file_window($filepickernode, $repositoryname) {
+    protected function open_add_file_window($filemanagernode, $repositoryname) {
 
-        $exception = new ExpectationException('No files can be added to the specified filepicker', $this->getSession());
+        $exception = new ExpectationException('No files can be added to the specified filemanager', $this->getSession());
 
-        // We should deal with single-file and multiple-file filepickers,
+        // We should deal with single-file and multiple-file filemanagers,
         // catching the exception thrown by behat_base::find() in case is not multiple
         try {
-            // Looking for the add button inside the specified filepicker.
-            $add = $this->find('css', 'div.fp-btn-add a', $exception, $filepickernode);
+            // Looking for the add button inside the specified filemanager.
+            $add = $this->find('css', 'div.fp-btn-add a', $exception, $filemanagernode);
         } catch (Exception $e) {
-            // Otherwise should be a single-file filepicker.
-            $add = $this->find('css', 'input.fp-btn-choose', $exception, $filepickernode);
+            // Otherwise should be a single-file filepicker form element.
+            $add = $this->find('css', 'input.fp-btn-choose', $exception, $filemanagernode);
         }
+        $this->ensure_node_is_visible($add);
         $add->click();
 
         // Getting the repository link and opening it.
@@ -184,7 +190,7 @@ class behat_files extends behat_base {
         // Avoid problems with both double and single quotes in the same string.
         $repositoryname = $this->getSession()->getSelectorsHandler()->xpathLiteral($repositoryname);
 
-        // Here we don't need to look inside the selected filepicker because there can only be one modal window.
+        // Here we don't need to look inside the selected element because there can only be one modal window.
         $repositorylink = $this->find(
             'xpath',
             "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-area ')]" .
@@ -194,12 +200,16 @@ class behat_files extends behat_base {
         );
 
         // Selecting the repo.
+        $this->ensure_node_is_visible($repositorylink);
         $repositorylink->click();
     }
 
     /**
      * Waits until the file manager modal windows are closed.
      *
+     * This method is not used by any of our step definitions,
+     * keeping it here for users already using it.
+     *
      * @throws ExpectationException
      * @return void
      */
@@ -217,6 +227,9 @@ class behat_files extends behat_base {
     /**
      * Checks that the file manager contents are not being updated.
      *
+     * This method is not used by any of our step definitions,
+     * keeping it here for users already using it.
+     *
      * @throws ExpectationException
      * @param NodeElement $filepickernode The file manager DOM node
      * @return void
@@ -240,9 +253,6 @@ class behat_files extends behat_base {
             $exception,
             $filepickernode
         );
-
-        // After removing the class FileManagerHelper.view_files() performs other actions.
-        $this->getSession()->wait(4 * 1000, false);
     }
 
 }
index c4f8a4b..d559ff4 100644 (file)
@@ -82,6 +82,11 @@ class behat_config_manager {
             $features = array_values($featurespaths);
         }
 
+        // Optionally include features from additional directories.
+        if (!empty($CFG->behat_additionalfeatures)) {
+            $features = array_merge($features, array_map("realpath", $CFG->behat_additionalfeatures));
+        }
+
         // Gets all the components with steps definitions.
         $stepsdefinitions = array();
         $steps = self::get_components_steps_definitions();
index ed3cd27..5773d12 100644 (file)
@@ -48,19 +48,38 @@ class behat_form_editor extends behat_form_field {
      */
     public function set_value($value) {
 
-        // Get tinyMCE editor id if it exists.
-        if ($editorid = $this->get_editor_id()) {
-
-            // Set the value to the iframe and save it to the textarea.
-            $this->session->executeScript('
-                tinyMCE.get("'.$editorid.'").setContent("' . $value . '");
-                tinyMCE.get("'.$editorid.'").save();
-            ');
-
-        } else {
-            // Set the value to a textarea otherwise.
-            parent::set_value($value);
+        $lastexception = null;
+
+        // We want the editor to be ready, otherwise the value can not
+        // be set and an exception is thrown.
+        for ($i = 0; $i < behat_base::EXTENDED_TIMEOUT; $i++) {
+            try {
+                // Get tinyMCE editor id if it exists.
+                if ($editorid = $this->get_editor_id()) {
+
+                    // Set the value to the iframe and save it to the textarea.
+                    $this->session->executeScript('
+                        tinyMCE.get("'.$editorid.'").setContent("' . $value . '");
+                        tinyMCE.get("'.$editorid.'").save();
+                    ');
+
+                } else {
+                    // Set the value to a textarea otherwise.
+                    parent::set_value($value);
+                }
+                return;
+
+            } catch (Exception $e) {
+                // Catching any kind of exception and ignoring it until times out.
+                $lastexception = $e;
+
+                // Waiting 0.1 seconds.
+                usleep(100000);
+            }
         }
+
+        // If it is not available we throw the last exception.
+        throw $lastexception;
     }
 
     /**
@@ -70,14 +89,45 @@ class behat_form_editor extends behat_form_field {
      */
     public function get_value() {
 
-        // Get tinyMCE editor id if it exists.
-        if ($editorid = $this->get_editor_id()) {
+        // Can be be a string value or an exception depending whether the editor loads or not.
+        $lastoutcome = '';
+
+        // We want the editor to be ready to return the correct value, sometimes the
+        // page loads too fast and the returned value may be '' if the editor didn't
+        // have enough time to load completely despite having a different value.
+        for ($i = 0; $i < behat_base::EXTENDED_TIMEOUT; $i++) {
+            try {
+
+                // Get tinyMCE editor id if it exists.
+                if ($editorid = $this->get_editor_id()) {
+
+                    // Save the current iframe value in case default value has been edited.
+                    $this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
+                }
+
+                $lastoutcome = $this->field->getValue();
+
+                // We only want to wait until it times out if the value is empty.
+                if ($lastoutcome != '') {
+                    return $lastoutcome;
+                }
+
+            } catch (Exception $e) {
+                // Catching any kind of exception and ignoring it until times out.
+                $lastoutcome = $e;
+
+                // Waiting 0.1 seconds.
+                usleep(100000);
+            }
+        }
 
-            // Save the current iframe value in case default value has been edited.
-            $this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
+        // If it is not available we throw the last exception.
+        if (is_a($lastoutcome, 'Exception')) {
+            throw $lastoutcome;
         }
 
-        return $this->field->getValue();
+        // Return the value if there are no exceptions it will be '' at this point
+        return $lastoutcome;
     }
 
     /**
@@ -87,7 +137,7 @@ class behat_form_editor extends behat_form_field {
      * can not execute Javascript, also some Moodle settings disables the HTML
      * editor.
      *
-     * @return mixed The id of the editor of false if is not available
+     * @return mixed The id of the editor of false if it is not available
      */
     protected function get_editor_id() {
 
@@ -95,7 +145,7 @@ class behat_form_editor extends behat_form_field {
         try {
             $available = $this->session->evaluateScript('return (typeof tinyMCE != "undefined")');
 
-            // Also checking that it exist a tinyMCE editor for the requested field.
+            // Also checking that it exists a tinyMCE editor for the requested field.
             $editorid = $this->field->getAttribute('id');
             $available = $this->session->evaluateScript('return (typeof tinyMCE.get("'.$editorid.'") != "undefined")');
 
index d5d43e1..8db4579 100644 (file)
@@ -137,6 +137,7 @@ class behat_form_field {
             $classname = 'behat_form_select';
 
         } else {
+            // We can not provide a closer field type.
             return false;
         }
 
@@ -154,4 +155,24 @@ class behat_form_field {
         return get_class($this->session->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver';
     }
 
+    /**
+     * Gets the field internal id used by selenium wire protocol.
+     *
+     * Only available when running_javascript().
+     *
+     * @throws coding_exception
+     * @return int
+     */
+    protected function get_internal_field_id() {
+
+        if (!$this->running_javascript()) {
+            throw new coding_exception('You can only get an internal ID using the selenium driver.');
+        }
+
+        return $this->session->
+            getDriver()->
+            getWebDriverSession()->
+            element('xpath', $this->field->getXPath())->
+            getID();
+    }
 }
index 5be8135..2805a97 100644 (file)
@@ -40,40 +40,82 @@ class behat_form_select extends behat_form_field {
     /**
      * Sets the value of a single select.
      *
+     * Seems an easy select, but there are lots of combinations
+     * of browsers and operative systems and each one manages the
+     * autosubmits and the multiple option selects in a diferent way.
+     *
      * @param string $value
      * @return void
      */
     public function set_value($value) {
-        $this->field->selectOption($value);
 
-        // Adding a click as Selenium requires it to fire some JS events.
+        // In some browsers we select an option and it triggers all the
+        // autosubmits and works as expected but not in all of them, so we
+        // try to catch all the possibilities to make this function work as
+        // expected.
+
+        // Get the internal id of the element we are going to click.
+        // This kind of internal IDs are only available in the selenium wire
+        // protocol, so only available using selenium drivers, phantomjs and family.
         if ($this->running_javascript()) {
+            $currentelementid = $this->get_internal_field_id();
+        }
 
-            // In some browsers the selectOption actions can perform a page reload
-            // so we need to ensure the element is still available to continue interacting
-            // with it. We don't wait here.
-            if (!$this->session->getDriver()->find($this->field->getXpath())) {
-                return;
-            }
+        // Here we select an option.
+        $this->field->selectOption($value);
 
-            // Single select needs an extra click in the option.
-            if (!$this->field->hasAttribute('multiple')) {
+        // With JS disabled this is enough and we finish here.
+        if (!$this->running_javascript()) {
+            return;
+        }
 
-                $value = $this->session->getSelectorsHandler()->xpathLiteral($value);
+        // With JS enabled we add more clicks as some selenium
+        // drivers requires it to fire JS events.
 
-                // Using the driver direcly because Element methods are messy when dealing
-                // with elements inside containers.
-                $optionxpath = $this->field->getXpath() .
-                    "/descendant::option[(./@value=$value or normalize-space(.)=$value)]";
-                $optionnodes = $this->session->getDriver()->find($optionxpath);
-                if ($optionnodes) {
-                    current($optionnodes)->click();
-                }
+        // In some browsers the selectOption actions can perform a form submit or reload page
+        // so we need to ensure the element is still available to continue interacting
+        // with it. We don't wait here.
+        $selectxpath = $this->field->getXpath();
+        if (!$this->session->getDriver()->find($selectxpath)) {
+            return;
+        }
 
-            } else {
-                // Multiple ones needs the click in the select.
-                $this->field->click();
+        // We also check the selenium internal element id, if it have changed
+        // we are dealing with an autosubmit that was already executed, and we don't to
+        // execute anything else as the action we wanted was already performed.
+        if ($currentelementid != $this->get_internal_field_id()) {
+            return;
+        }
+
+        // We also check that the option is still there. We neither wait.
+        $valueliteral = $this->session->getSelectorsHandler()->xpathLiteral($value);
+        $optionxpath = $selectxpath . "/descendant::option[(./@value=$valueliteral or normalize-space(.)=$valueliteral)]";
+        if (!$this->session->getDriver()->find($optionxpath)) {
+            return;
+        }
+
+        // Single select sometimes needs an extra click in the option.
+        if (!$this->field->hasAttribute('multiple')) {
+
+            // Using the driver direcly because Element methods are messy when dealing
+            // with elements inside containers.
+            $optionnodes = $this->session->getDriver()->find($optionxpath);
+            if ($optionnodes) {
+                current($optionnodes)->click();
             }
+
+        } else {
+            // Multiple ones needs the click in the select.
+            $this->field->click();
+
+            // We ensure that the option is still there.
+            if (!$this->session->getDriver()->find($optionxpath)) {
+                return;
+            }
+
+            // Repeating the select as some drivers (chrome that I know) are moving
+            // to another option after the general select field click above.
+            $this->field->selectOption($value);
         }
     }
 
index 53c0241..e6ea562 100644 (file)
@@ -566,6 +566,9 @@ $cache = '.var_export($cache, true).';
         self::load_classes('core', "$CFG->dirroot/lib/classes");
 
         foreach (self::$subsystems as $subsystem => $fulldir) {
+            if (!$fulldir) {
+                continue;
+            }
             self::load_classes('core_'.$subsystem, "$fulldir/classes");
         }
 
index 6a303f2..6cbcbe2 100644 (file)
@@ -113,6 +113,24 @@ abstract class base implements \IteratorAggregate {
      */
     private final function __construct() {
         $this->data = array_fill_keys(self::$fields, null);
+
+        // Define some basic details.
+        $classname = get_called_class();
+        $parts = explode('\\', $classname);
+        if (count($parts) !== 3 or $parts[1] !== 'event') {
+            throw new \coding_exception("Invalid event class name '$classname', it must be defined in component\\event\\
+                    namespace");
+        }
+        $this->data['eventname'] = '\\'.$classname;
+        $this->data['component'] = $parts[0];
+
+        $pos = strrpos($parts[2], '_');
+        if ($pos === false) {
+            throw new \coding_exception("Invalid event class name '$classname', there must be at least one underscore separating
+                    object and action words");
+        }
+        $this->data['target'] = substr($parts[2], 0, $pos);
+        $this->data['action'] = substr($parts[2], $pos + 1);
     }
 
     /**
@@ -146,21 +164,6 @@ abstract class base implements \IteratorAggregate {
         // Set automatic data.
         $event->data['timecreated'] = time();
 
-        $classname = get_class($event);
-        $parts = explode('\\', $classname);
-        if (count($parts) !== 3 or $parts[1] !== 'event') {
-            throw new \coding_exception("Invalid event class name '$classname', it must be defined in component\\event\\ namespace");
-        }
-        $event->data['eventname'] = '\\'.$classname;
-        $event->data['component'] = $parts[0];
-
-        $pos = strrpos($parts[2], '_');
-        if ($pos === false) {
-            throw new \coding_exception("Invalid event class name '$classname', there must be at least one underscore separating object and action words");
-        }
-        $event->data['target'] = substr($parts[2], 0, $pos);
-        $event->data['action'] = substr($parts[2], $pos+1);
-
         // Set optional data or use defaults.
         $event->data['objectid'] = isset($data['objectid']) ? $data['objectid'] : null;
         $event->data['courseid'] = isset($data['courseid']) ? $data['courseid'] : null;
@@ -444,7 +447,9 @@ abstract class base implements \IteratorAggregate {
                 debugging('Number of event data fields must not be changed in event classes', DEBUG_DEVELOPER);
             }
             $encoded = json_encode($this->data['other']);
-            if ($encoded === false or $this->data['other'] !== json_decode($encoded, true)) {
+            // The comparison here is not set to strict as whole float numbers will be converted to integers through JSON encoding /
+            // decoding and send an unwanted debugging message.
+            if ($encoded === false or $this->data['other'] != json_decode($encoded, true)) {
                 debugging('other event data must be compatible with json encoding', DEBUG_DEVELOPER);
             }
             if ($this->data['userid'] and !is_number($this->data['userid'])) {
diff --git a/lib/classes/event/course_module_instance_list_viewed.php b/lib/classes/event/course_module_instance_list_viewed.php
new file mode 100644 (file)
index 0000000..de0e553
--- /dev/null
@@ -0,0 +1,113 @@
+<?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/>.
+
+/**
+ * Course module instance list viewed event.
+ *
+ * @package    core
+ * @copyright  2013 onwards Ankit Agarwal
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Course module instance list viewed event class.
+ *
+ * This is an abstract to guide the developers in using this event name for their events.
+ * It is intended to be used when the user viewes the list of all the instances of a module
+ * in a course. This replaces the historical 'view all' log entry generated in mod/somemod/index.php.
+ *
+ * Example:
+ *
+ *     \mod_chat\event\course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed
+ *
+ * @package    core
+ * @copyright  2013 onwards Ankit Agarwal
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class course_module_instance_list_viewed extends base{
+
+    /** @var string private var to store mod name */
+    private $modname;
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'r';
+        $this->data['level'] = self::LEVEL_OTHER;
+        if (strstr($this->component, 'mod_') === false) {
+            throw new \coding_exception('The event name or namespace is invalid.');
+        } else {
+            $this->modname = str_replace('mod_', '', $this->component);
+        }
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "User with id '$this->userid' viewed instance list for module '$this->modname' in course with id
+                '$this->courseid'";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventcoursemoduleinstancelistviewed', 'core');
+    }
+
+    /**
+     * Get URL related to the action.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url("/mod/$this->modname/index.php", array('id' => $this->courseid));
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        return array($this->courseid, $this->modname, 'view all', 'index.php?id=' . $this->courseid, '');
+    }
+
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        if ($this->context->contextlevel !== CONTEXT_COURSE) {
+            throw new \coding_exception('The context must be a course level context.');
+        }
+    }
+
+}
index b1b6db3..a7c2df1 100644 (file)
@@ -17,6 +17,8 @@
 /**
  * Course module instances list_viewed event.
  *
+ * This class has been deprecated, please use \core\event\course_module_instance_list_viewed.
+ *
  * @package    core
  * @copyright  2013 Frédéric Massart
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -26,56 +28,15 @@ namespace core\event;
 defined('MOODLE_INTERNAL') || die();
 
 /**
- * Course module instances list viewed event class.
- *
- * This is an abstract to guide the developers in using this event name for their events.
- * It is intended to be used when the user viewes the list of all the instances of a module
- * in a course. This replaces the historical 'view all' log entry generated in mod/somemod/index.php.
- *
- * Example:
- *
- *     \mod_chat\event\instances_list_viewed extends \core\event\course_module_instances_list_viewed
+ * This class has been deprecated, please use \core\event\course_module_instance_list_viewed.
  *
+ * @deprecated Since Moodle 2.7
  * @package    core
  * @copyright  2013 Frédéric Massart
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-abstract class course_module_instances_list_viewed extends \core\event\content_viewed {
-
-    /**
-     * Init method.
-     *
-     * @return void
-     */
-    protected function init() {
-        $this->data['crud'] = 'r';
-        $this->data['level'] = self::LEVEL_OTHER;
-    }
-
-    /**
-     * Set page detail.
-     *
-     * Override to prevent its use.
-     */
-    public function set_page_detail() {
-    }
-
-    /**
-     * Custom validation.
-     *
-     * The parent validation is ignored on purpose.
-     *
-     * @throws \coding_exception
-     * @return void
-     */
-    protected function validate_data() {
-        if ($this->context->contextlevel !== CONTEXT_COURSE) {
-           throw new \coding_exception('The context must be a course level context.');
-        }
-        // Hack to by pass the requirement of the parent class. 'view_all' was the old fashioned-way
-        // to describe the page listing all the instances of a module in a course.
-        $this->data['other']['content'] = 'view_all';
-        parent::validate_data();
-    }
-
+abstract class course_module_instances_list_viewed extends course_module_instance_list_viewed {
 }
+
+debugging('core\\event\\course_module_instances_list_viewed has been deperecated. Please use
+        core\\event\\course_module_instance_list_viewed instead', DEBUG_DEVELOPER);
index f3d3582..2277627 100644 (file)
@@ -81,6 +81,18 @@ class checker {
         }
     }
 
+    /**
+     * Is automatic deployment enabled?
+     *
+     * @return bool
+     */
+    public function enabled() {
+        global $CFG;
+
+        // The feature can be prohibited via config.php.
+        return empty($CFG->disableupdateautodeploy);
+    }
+
     /**
      * Returns the timestamp of the last execution of {@link fetch()}
      *
index 89c47d6..0eccb8e 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
  */
 class deployer {
 
-    const HTTP_PARAM_PREFIX     = 'updteautodpldata_';  // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p.
-    const HTTP_PARAM_CHECKER    = 'datapackagesize';    // Name of the parameter that holds the number of items in the received data items.
-
     /** @var \core\update\deployer holds the singleton instance */
     protected static $singletoninstance;
     /** @var moodle_url URL of a page that includes the deployer UI */
@@ -207,9 +204,19 @@ class deployer {
             throw new coding_exception('Illegal method call - deployer not initialized.');
         }
 
-        $params = $this->data_to_params(array(
-            'updateinfo' => (array)$info,   // See http://www.php.net/manual/en/language.types.array.php#language.types.array.casting .
-        ));
+        $params = array(
+            'updateaddon' => $info->component,
+            'version' =>$info->version,
+            'sesskey' => sesskey(),
+        );
+
+        // Append some our own data.
+        if (!empty($this->callerurl)) {
+            $params['callerurl'] = $this->callerurl->out(false);
+        }
+        if (!empty($this->returnurl)) {
+            $params['returnurl'] = $this->returnurl->out(false);
+        }
 
         $widget = new \single_button(
             new moodle_url($this->callerurl, $params),
@@ -301,25 +308,46 @@ class deployer {
      * @return array
      */
     public function submitted_data() {
+        $component = optional_param('updateaddon', '', PARAM_COMPONENT);
+        $version = optional_param('version', '', PARAM_RAW);
+        if (!$component or !$version) {
+            return false;
+        }
+
+        $plugininfo = \core_plugin_manager::instance()->get_plugin_info($component);
+        if (!$plugininfo) {
+            return false;
+       &n