Merge branch 'MDL-58071-master' of git://github.com/jleyva/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 28 Feb 2017 21:57:09 +0000 (22:57 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 28 Feb 2017 21:57:09 +0000 (22:57 +0100)
228 files changed:
admin/cli/cfg.php [new file with mode: 0644]
admin/settings/security.php
admin/settings/subsystems.php
admin/tool/behat/cli/run.php
admin/tool/lp/templates/comment_area.mustache
admin/tool/monitor/classes/eventobservers.php
admin/tool/monitor/classes/subscription.php
admin/tool/monitor/classes/subscription_manager.php
admin/tool/monitor/db/upgrade.php
admin/tool/monitor/index.php
admin/tool/monitor/lib.php
admin/tool/monitor/tests/behat/subscription.feature
admin/tool/monitor/version.php
admin/tool/task/lang/en/tool_task.php
admin/tool/task/renderer.php
admin/tool/task/schedule_task.php [new file with mode: 0644]
admin/tool/task/styles.css
admin/tool/task/templates/link_back.mustache [new file with mode: 0644]
admin/tool/task/tests/behat/run_task_now.feature [new file with mode: 0644]
admin/tool/usertours/classes/helper.php
admin/tool/usertours/lib.php
auth/classes/output/login.php
backup/backup.class.php
backup/controller/restore_controller.class.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_course_task.class.php
backup/moodle2/restore_root_task.class.php
backup/moodle2/restore_stepslib.php
backup/moodle2/tests/moodle2_test.php
backup/util/checks/restore_check.class.php
backup/util/ui/renderer.php
blocks/activity_results/edit_form.php
blocks/login/block_login.php
blocks/site_main_menu/block_site_main_menu.php
blocks/site_main_menu/tests/behat/add_url.feature
blocks/site_main_menu/tests/behat/behat_block_site_main_menu.php [new file with mode: 0644]
blocks/site_main_menu/tests/behat/edit_activities.feature
blocks/social_activities/block_social_activities.php
blocks/social_activities/tests/behat/behat_block_social_activities.php [new file with mode: 0644]
blocks/social_activities/tests/behat/edit_activities.feature
comment/comment.js
comment/lib.php
completion/tests/behat/restrict_activity_by_date.feature
completion/tests/behat/restrict_activity_by_grade.feature
composer.json
composer.lock
course/amd/build/actions.min.js [new file with mode: 0644]
course/amd/src/actions.js [new file with mode: 0644]
course/externallib.php
course/format/lib.php
course/format/renderer.php
course/format/social/lib.php
course/format/topics/lib.php
course/format/topics/renderer.php
course/format/upgrade.txt
course/format/weeks/lib.php
course/lib.php
course/mod.php
course/modlib.php
course/moodleform_mod.php
course/renderer.php
course/rest.php
course/tests/behat/activities_visibility_icons.feature
course/tests/behat/behat_course.php
course/tests/courselib_test.php
course/tests/externallib_test.php
course/tests/modlib_test.php
course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js
course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js
course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js [deleted file]
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js [deleted file]
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js [deleted file]
course/yui/src/dragdrop/js/resource.js
course/yui/src/toolboxes/build.json [deleted file]
course/yui/src/toolboxes/js/resource.js [deleted file]
course/yui/src/toolboxes/js/section.js [deleted file]
course/yui/src/toolboxes/js/shared.js [deleted file]
course/yui/src/toolboxes/js/toolbox.js [deleted file]
course/yui/src/toolboxes/meta/toolboxes.json [deleted file]
enrol/self/lib.php
grade/grading/form/guide/renderer.php
install/lang/bn/admin.php
install/lang/lv/admin.php
lang/en/admin.php
lang/en/backup.php
lang/en/deprecated.txt
lang/en/moodle.php
lib/accesslib.php
lib/behat/classes/behat_selectors.php
lib/behat/classes/exact_named_selector.php
lib/classes/event/base.php
lib/classes/event/user_graded.php
lib/classes/files/curl_security_helper.php
lib/classes/notification.php
lib/classes/persistent.php
lib/classes/session/manager.php
lib/classes/task/manager.php
lib/cronlib.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/db/upgradelib.php
lib/filestorage/file_storage.php
lib/filestorage/file_system.php [new file with mode: 0644]
lib/filestorage/file_system_filedir.php [new file with mode: 0644]
lib/filestorage/stored_file.php
lib/filestorage/tests/file_storage_test.php
lib/filestorage/tests/file_system_filedir_test.php [new file with mode: 0644]
lib/filestorage/tests/file_system_test.php [new file with mode: 0644]
lib/filestorage/tests/fixtures/test.tgz [new file with mode: 0644]
lib/filestorage/tests/mbz_packer_test.php
lib/form/modvisible.php
lib/grade/grade_category.php
lib/grade/grade_item.php
lib/javascript-static.js
lib/modinfolib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/sessionlib.php
lib/setuplib.php
lib/templates/availability_info.mustache [new file with mode: 0644]
lib/templates/login.mustache
lib/testing/generator/module_generator.php
lib/tests/behat/behat_action_menu.php
lib/tests/behat/behat_deprecated.php
lib/tests/moodlelib_test.php
lib/tests/persistent_test.php
lib/tests/upgradelib_test.php
lib/tests/weblib_test.php
lib/upgrade.txt
lib/weblib.php
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/src/notification/js/dialogue.js
login/lib.php
login/set_password_form.php
message/classes/api.php
message/lib.php
message/tests/api_test.php
message/tests/messagelib_test.php
mod/assign/amd/build/grading_navigation.min.js
mod/assign/amd/build/grading_panel.min.js
mod/assign/amd/build/participant_selector.min.js
mod/assign/amd/src/grading_navigation.js
mod/assign/amd/src/grading_panel.js
mod/assign/amd/src/participant_selector.js
mod/assign/classes/output/grading_app.php
mod/assign/externallib.php
mod/assign/feedback/editpdf/classes/document_services.php
mod/assign/feedback/editpdf/classes/pdf.php
mod/assign/feedback/editpdf/fixtures/blank.pdf [new file with mode: 0644]
mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php
mod/assign/feedback/editpdf/tests/editpdf_test.php
mod/assign/feedback/file/locallib.php
mod/assign/gradingoptionsform.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/templates/grading_navigation_user_selector.mustache
mod/assign/tests/behat/submission_comments.feature
mod/assign/tests/externallib_test.php
mod/assign/tests/locallib_test.php
mod/feedback/lib.php
mod/folder/classes/external.php
mod/folder/lib.php
mod/folder/renderer.php
mod/folder/styles.css [new file with mode: 0644]
mod/folder/tests/externallib_test.php
mod/forum/externallib.php
mod/glossary/import.php
mod/label/classes/external.php
mod/label/tests/behat/label_visibility.feature
mod/label/tests/externallib_test.php
mod/lesson/classes/external.php [new file with mode: 0644]
mod/lesson/db/services.php [new file with mode: 0644]
mod/lesson/locallib.php
mod/lesson/mediafile.php
mod/lesson/tests/external_test.php [new file with mode: 0644]
mod/lesson/version.php
mod/lesson/view.php
mod/page/classes/external.php
mod/page/tests/externallib_test.php
mod/resource/classes/external.php
mod/resource/tests/externallib_test.php
mod/survey/amd/build/validation.min.js [new file with mode: 0644]
mod/survey/amd/src/validation.js [new file with mode: 0644]
mod/survey/lib.php
mod/survey/survey.js [deleted file]
mod/survey/view.php
mod/url/classes/external.php
mod/url/tests/externallib_test.php
my/lib.php
portfolio/download/file.php
portfolio/download/helper.js [deleted file]
question/format/blackboard_six/formatbase.php
report/completion/index.php
report/completion/styles.css
report/completion/textrotate.js [deleted file]
report/progress/index.php
report/progress/styles.css
report/progress/textrotate.js [deleted file]
repository/googledocs/db/upgrade.php
repository/googledocs/lang/en/repository_googledocs.php
repository/googledocs/lib.php
repository/googledocs/tests/generator/lib.php
repository/googledocs/version.php
repository/lib.php
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/grade.scss
theme/boost/templates/core/availability_info.mustache [new file with mode: 0644]
theme/boost/templates/core/login.mustache
theme/boost/templates/mod_assign/grading_navigation_user_selector.mustache
theme/boost/tests/behat/behat_theme_boost_behat_action_menu.php
theme/boost/tests/behat/behat_theme_boost_behat_blocks.php
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/core/availability_info.mustache [new file with mode: 0644]
user/lib.php
user/tests/userlib_test.php
version.php
webservice/tests/externallib_test.php

diff --git a/admin/cli/cfg.php b/admin/cli/cfg.php
new file mode 100644 (file)
index 0000000..463a0ab
--- /dev/null
@@ -0,0 +1,173 @@
+<?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/>.
+
+/**
+ * CLI script allowing to get and set config values.
+ *
+ * This is technically just a thin wrapper for {@link get_config()} and
+ * {@link set_config()} functions.
+ *
+ * @package     core
+ * @subpackage  cli
+ * @copyright   2017 David Mudr├ík <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__.'/../../config.php');
+require_once($CFG->libdir.'/clilib.php');
+
+$usage = "Displays the current value of the given site setting. Allows to set it to the given value, too.
+
+Usage:
+    # php cfg.php [--component=<componentname>] [--json] [--shell-arg]
+    # php cfg.php --name=<configname> [--component=<componentname>] [--shell-arg] [--no-eol]
+    # php cfg.php --name=<configname> [--component=<componentname>] --set=<value>
+    # php cfg.php --name=<configname> [--component=<componentname>] --unset
+    # php cfg.php [--help|-h]
+
+Options:
+    -h --help                   Print this help.
+    --component=<frankenstyle>  Name of the component the variable is part of. Defaults to core.
+    --name=<configname>         Name of the configuration variable to get/set. If missing, print all
+                                configuration variables of the given component.
+    --set=<value>               Set the given variable to this value.
+    --unset                     Unset the given variable.
+    --shell-arg                 Escape output values so that they can be directly used as shell script arguments.
+    --json                      Encode output list of values using JSON notation.
+    --no-eol                    Do not include the trailing new line character when printing the value.
+
+The list of all variables of the given component can be printed as
+tab-separated list (default) or JSON object (--json). Particular values are
+printed as raw text values, optionally escaped so that they can be directly
+used as shell script arguments (--shell-arg). Single values are displayed with
+trailing new line by default, unless explicitly disabled (--no-eol).
+
+In the read mode, the script exits with success status 0 if the requested value
+is found. If the requested variable is not set, the script exits with status 3.
+When listing all variables of the component, the exit status is always 0 even
+if no variables for the given component are found. When setting/unsetting a
+value, the exit status is 0. When attempting to set/unset a value that has
+already been hard-set in config.php, the script exits with error status 4. In
+case of unexpected error, the script exits with error status 1.
+
+Examples:
+
+    # php cfg.php
+        Prints tab-separated list of all core configuration variables and their values.
+
+    # php cfg.php --json
+        Prints list of all core configuration variables and their values as a JSON object.
+
+    # php cfg.php --name=release
+        Prints the given configuration variable - e.g. \$CFG->release in this case.
+
+    # php cfg.php --component=tool_recyclebin
+    #   Prints tab-separated list of the plugin's configuration variables.
+
+    # export DATAROOT=\$(php cfg.php --name=dataroot --shell-arg --no-eol)
+        Stores the given configuration variable in the shell variable, escaped
+        so that it can be safely used as a shell argument.
+
+    # php cfg.php --name=theme --set=clean
+        Sets the given configuration variable to the given value.
+
+    # php cfg.php --name=noemailever --unset
+        Unsets the previously configured variable.
+";
+
+list($options, $unrecognised) = cli_get_params([
+    'help' => false,
+    'component' => null,
+    'name' => null,
+    'set' => null,
+    'unset' => false,
+    'shell-arg' => false,
+    'json' => false,
+    'no-eol' => false,
+], [
+    'h' => 'help'
+]);
+
+if ($unrecognised) {
+    $unrecognised = implode(PHP_EOL.'  ', $unrecognised);
+    cli_error(get_string('cliunknowoption', 'core_admin', $unrecognised));
+}
+
+if ($options['help']) {
+    cli_writeln($usage);
+    exit(2);
+}
+
+if ($options['unset'] || $options['set'] !== null) {
+    // Unset the variable or set it to the given value.
+    if (empty($options['name'])) {
+        cli_error('Missing configuration variable name', 2);
+    }
+
+    // Check that the variable is not hard-set in the main config.php already.
+    if (array_key_exists($options['name'], $CFG->config_php_settings)) {
+        cli_error('The configuration variable is hard-set in the config.php, unable to change.', 4);
+    }
+
+    set_config($options['name'], $options['set'], $options['component']);
+    exit(0);
+}
+
+if ($options['name'] === null) {
+    // List all variables provided by the component (defaults to core).
+    $got = get_config($options['component']);
+
+    if ($options['json']) {
+        cli_writeln(json_encode($got));
+
+    } else {
+        foreach ($got as $name => $value) {
+            if ($options['shell-arg']) {
+                $value = escapeshellarg($value);
+            }
+            cli_writeln($name."\t".$value);
+        }
+    }
+
+    exit(0);
+
+} else {
+    // Display the value of a single variable.
+
+    $got = get_config($options['component'], $options['name']);
+
+    if ($got === false) {
+        cli_error('No such configuration variable found.', 3);
+    }
+
+    if ($options['shell-arg']) {
+        $got = escapeshellarg($got);
+    }
+
+    if ($options['json']) {
+        $got = json_encode($got);
+    }
+
+    if ($options['no-eol']) {
+        cli_write($got);
+    } else {
+        cli_writeln($got);
+    }
+
+    exit(0);
+}
index 1d89d42..c36a424 100644 (file)
@@ -59,6 +59,8 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp->add(new admin_setting_configcheckbox('cronclionly', new lang_string('cronclionly', 'admin'), new lang_string
             ('configcronclionly', 'admin'), 1));
     $temp->add(new admin_setting_configpasswordunmask('cronremotepassword', new lang_string('cronremotepassword', 'admin'), new lang_string('configcronremotepassword', 'admin'), ''));
+    $temp->add(new admin_setting_configcheckbox('tool_task/enablerunnow', new lang_string('enablerunnow', 'tool_task'),
+            new lang_string('enablerunnow_desc', 'tool_task'), 1));
 
     $options = array(0=>get_string('no'), 3=>3, 5=>5, 7=>7, 10=>10, 20=>20, 30=>30, 50=>50, 100=>100);
     $temp->add(new admin_setting_configselect('lockoutthreshold', new lang_string('lockoutthreshold', 'admin'), new lang_string('lockoutthreshold_desc', 'admin'), 0, $options));
@@ -117,7 +119,6 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp->add(new admin_setting_configcheckbox('cookiesecure', new lang_string('cookiesecure', 'admin'), new lang_string('configcookiesecure', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('cookiehttponly', new lang_string('cookiehttponly', 'admin'), new lang_string('configcookiehttponly', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('allowframembedding', new lang_string('allowframembedding', 'admin'), new lang_string('allowframembedding_help', 'admin'), 0));
-    $temp->add(new admin_setting_configcheckbox('loginpasswordautocomplete', new lang_string('loginpasswordautocomplete', 'admin'), new lang_string('loginpasswordautocomplete_help', 'admin'), 0));
 
     // Settings elements used by the \core\files\curl_security_helper class.
     $temp->add(new admin_setting_configmixedhostiplist('curlsecurityblockedhosts',
index cc65569..6f5ded5 100644 (file)
@@ -54,4 +54,10 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
 
     $optionalsubsystems->add(new admin_setting_configcheckbox('enableglobalsearch', new lang_string('enableglobalsearch', 'admin'),
         new lang_string('enableglobalsearch_desc', 'admin'), 0, 1, 0));
+
+    $choices = array();
+    $choices[0] = new lang_string('no');
+    $choices[1] = new lang_string('yes');
+    $optionalsubsystems->add(new admin_setting_configselect('allowstealth', new lang_string('allowstealthmodules'),
+        new lang_string('allowstealthmodules_help'), 0, $choices));
 }
index 63e19d2..e402efa 100644 (file)
@@ -55,6 +55,7 @@ list($options, $unrecognised) = cli_get_params(
         'torun'    => 0,
         'single-run' => false,
         'rerun' => 0,
+        'auto-rerun' => 0,
     ),
     array(
         'h' => 'help',
@@ -79,6 +80,7 @@ Options:
 --fromrun          Execute run starting from (Used for parallel runs on different vms)
 --torun            Execute run till (Used for parallel runs on different vms)
 --rerun            Re-run scenarios that failed during last execution.
+--auto-rerun       Automatically re-run scenarios that failed during last execution.
 
 -h, --help         Print out this help
 
@@ -214,8 +216,11 @@ if (empty($parallelrun)) {
     $runtestscommand = behat_command::get_behat_command(false, false, true);
     $runtestscommand .= ' --config ' . behat_config_manager::get_behat_cli_config_filepath();
     $runtestscommand .= ' ' . $extraoptstr;
+    $cmds['singlerun'] = $runtestscommand;
+
     echo "Running single behat site:" . PHP_EOL;
     passthru("php $runtestscommand", $status);
+    $exitcodes['singlerun'] = $status;
     chdir($cwd);
 } else {
 
@@ -284,28 +289,66 @@ if (empty($parallelrun)) {
     print_each_process_info($processes, $verbose, $status);
 }
 
+// Save final exit code containing which run failed.
+behat_config_manager::set_behat_run_config_value('lastcombinedfailedstatus', $status);
+
 // Show exit code from each process, if any process failed and how to rerun failed process.
 if ($verbose || $status) {
-    // Save final exit code containing which run failed.
-    behat_config_manager::set_behat_run_config_value('lastcombinedfailedstatus', $status);
+    // Check if status of last run is failure and rerun is suggested.
+    if (!empty($options['auto-rerun']) && $status) {
+        // Rerun for the number of tries passed.
+        for ($i = 0; $i < $options['auto-rerun']; $i++) {
+
+            // Run individual commands, to avoid parallel failures.
+            foreach ($exitcodes as $behatrunname => $exitcode) {
+                // If not failed in last run, then skip.
+                if ($exitcode == 0) {
+                    continue;
+                }
+
+                // This was a failure.
+                echo "*** Re-running behat run: $behatrunname ***" . PHP_EOL;
+                if ($verbose) {
+                    echo "Executing: " . $cmds[$behatrunname] . " --rerun" . PHP_EOL;
+                }
+
+                passthru("php $cmds[$behatrunname] --rerun", $rerunstatus);
+
+                // Update exit code.
+                $exitcodes[$behatrunname] = $rerunstatus;
+            }
+        }
+
+        // Update status after auto-rerun finished.
+        foreach ($exitcodes as $name => $exitcode) {
+            if ($exitcode) {
+                if (!empty($parallelrun)) {
+                    $runno = str_replace(BEHAT_PARALLEL_SITE_NAME, '', $name);
+                } else {
+                    $runno = 1;
+                }
+                $status |= (1 << ($runno - 1));
+            }
+        }
+    }
 
-    // Show failed re-run commands.
+    // Show final o/p with re-run commands.
     if ($status) {
-        if (!empty($cmds)) {
+        if (!empty($parallelrun)) {
             // Echo exit codes.
             echo "Exit codes for each behat run: " . PHP_EOL;
             foreach ($exitcodes as $run => $exitcode) {
                 echo $run . ": " . $exitcode . PHP_EOL;
             }
+            unset($extraopts['fromrun']);
+            unset($extraopts['torun']);
+            if (!empty($options['replace'])) {
+                $extraopts['replace'] = '--replace="' . $options['replace'] . '"';
+            }
         }
 
         echo "To re-run failed processes, you can use following command:" . PHP_EOL;
-        unset($extraopts['fromrun']);
-        unset($extraopts['torun']);
         $extraopts['rerun'] = '--rerun';
-        if (!empty($options['replace'])) {
-            $extraopts['replace'] =  '--replace="' . $options['replace'] . '"';
-        }
         $extraoptstr = implode(' ', $extraopts);
         echo behat_command::get_behat_command(true, true, true) . " " . $extraoptstr . PHP_EOL;
     }
index a93697a..19f8fdb 100644 (file)
@@ -93,7 +93,7 @@ require(['core/str'], function(Str) {
         { key: 'comments', component: 'moodle' },
         { key: 'commentscount', component: 'moodle' },
         { key: 'commentsrequirelogin', component: 'moodle' },
-        { key: 'deletecomment', component: 'moodle' },
+        { key: 'deletecommentbyon', component: 'moodle' },
     ]).then(function() {
         // Kick off when strings are loaded.
         Y.use('core_comment', function(Y) {
index 793b11f..bc9a0fa 100644 (file)
@@ -53,10 +53,13 @@ class eventobservers {
      * @param \core\event\course_deleted $event The course deleted event.
      */
     public static function course_deleted(\core\event\course_deleted $event) {
+        // Delete rules defined inside this course and associated subscriptions.
         $rules = rule_manager::get_rules_by_courseid($event->courseid, 0, 0, false);
         foreach ($rules as $rule) {
             rule_manager::delete_rule($rule->id, $event->get_context());
         }
+        // Delete remaining subscriptions inside this course (from site-wide rules).
+        subscription_manager::remove_all_subscriptions_in_course($event->get_context());
     }
 
     /**
index 5beddbb..828eff6 100644 (file)
@@ -176,8 +176,12 @@ class subscription {
         if (empty($courseid)) {
             return get_string('site');
         } else {
-            $course = get_course($courseid);
-            return format_string($course->fullname, true, array('context' => $context));
+            try {
+                $course = get_course($courseid);
+                return format_string($course->fullname, true, array('context' => $context));
+            } catch (\dml_exception $e) {
+                return '-';
+            }
         }
     }
 
index c4382e7..df2d3b3 100644 (file)
@@ -118,9 +118,10 @@ class subscription_manager {
 
         // If successful trigger a subscription_deleted event.
         if ($success) {
-            if (!empty($subscription->courseid)) {
+            if (!empty($subscription->courseid) &&
+                    ($coursecontext = \context_course::instance($subscription->courseid, IGNORE_MISSING))) {
                 $courseid = $subscription->courseid;
-                $context = \context_course::instance($subscription->courseid);
+                $context = $coursecontext;
             } else {
                 $courseid = 0;
                 $context = \context_system::instance();
@@ -222,6 +223,31 @@ class subscription_manager {
         return $success;
     }
 
+    /**
+     * Delete all subscriptions in a course.
+     *
+     * This is called after a course was deleted, context no longer exists but we kept the object
+     *
+     * @param \context_course $coursecontext the context of the course
+     */
+    public static function remove_all_subscriptions_in_course($coursecontext) {
+        global $DB;
+
+        // Store all the subscriptions we have to delete.
+        if ($subscriptions = $DB->get_records('tool_monitor_subscriptions', ['courseid' => $coursecontext->instanceid])) {
+            // Delete subscriptions in bulk.
+            $DB->delete_records('tool_monitor_subscriptions', ['courseid' => $coursecontext->instanceid]);
+
+            // Trigger events one by one.
+            foreach ($subscriptions as $subscription) {
+                $params = ['objectid' => $subscription->id, 'context' => $coursecontext];
+                $event = \tool_monitor\event\subscription_deleted::create($params);
+                $event->add_record_snapshot('tool_monitor_subscriptions', $subscription);
+                $event->trigger();
+            }
+        }
+    }
+
     /**
      * Get a subscription instance for an given subscription id.
      *
index b96b105..b9982d5 100644 (file)
@@ -80,5 +80,22 @@ function xmldb_tool_monitor_upgrade($oldversion) {
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2017021300) {
+
+        // Delete "orphaned" subscriptions.
+        $sql = "SELECT DISTINCT s.courseid
+                  FROM {tool_monitor_subscriptions} s
+       LEFT OUTER JOIN {course} c ON c.id = s.courseid
+                 WHERE s.courseid <> 0 and c.id IS NULL";
+        $deletedcourses = $DB->get_field_sql($sql);
+        if ($deletedcourses) {
+            list($sql, $params) = $DB->get_in_or_equal($deletedcourses);
+            $DB->execute("DELETE FROM {tool_monitor_subscriptions} WHERE courseid " . $sql, $params);
+        }
+
+        // Monitor savepoint reached.
+        upgrade_plugin_savepoint(true, 2017021300, 'tool', 'monitor');
+    }
+
     return true;
 }
index b4c724b..65d9cdd 100644 (file)
@@ -41,7 +41,7 @@ if (empty($courseid)) {
     // check system level capability.
     if (!has_capability('tool/monitor:subscribe', $context)) {
         // If not system level then check to see if they have access to any course level rules.
-        if (tool_monitor_get_user_courses()){
+        if (tool_monitor_can_subscribe()) {
             // Make them choose a course.
             $choose = true;
         } else {
index 52543d4..c7493ad 100644 (file)
@@ -75,12 +75,20 @@ function tool_monitor_extend_navigation_frontpage($navigation, $course, $context
  * @param context         $coursecontext     The context of the course
  */
 function tool_monitor_extend_navigation_user_settings($navigation, $user, $usercontext, $course, $coursecontext) {
-    global $USER, $SITE;
+    global $USER, $PAGE;
+
+    // Don't bother doing needless calculations unless we are on the relevant pages.
+    $onpreferencepage = $PAGE->url->compare(new moodle_url('/user/preferences.php'), URL_MATCH_BASE);
+    $onmonitorpage = $PAGE->url->compare(new moodle_url('/admin/tool/monitor/index.php'), URL_MATCH_BASE);
+    if (!$onpreferencepage && !$onmonitorpage) {
+        return null;
+    }
 
     // Don't show the setting if the event monitor isn't turned on. No access to other peoples subscriptions.
     if (get_config('tool_monitor', 'enablemonitor') && $USER->id == $user->id) {
         // Now let's check to see if the user has any courses / site rules that they can subscribe to.
-        if ($courses = tool_monitor_get_user_courses()) {
+        // We skip doing a check here if we are on the event monitor page as the check is done internally on that page.
+        if ($onmonitorpage || tool_monitor_can_subscribe()) {
             $url = new moodle_url('/admin/tool/monitor/index.php');
             $subsnode = navigation_node::create(get_string('managesubscriptions', 'tool_monitor'), $url,
                     navigation_node::TYPE_SETTING, null, 'monitor', new pix_icon('i/settings', ''));
@@ -92,6 +100,19 @@ function tool_monitor_extend_navigation_user_settings($navigation, $user, $userc
     }
 }
 
+/**
+ * Check if the user has the capacity to subscribe to an event monitor anywhere.
+ *
+ * @return bool True if a capability in a course is found. False otherwise.
+ */
+function tool_monitor_can_subscribe() {
+    if (has_capability('tool/monitor:subscribe', context_system::instance())) {
+        return true;
+    }
+    $courses = get_user_capability_course('tool/monitor:subscribe', null, true, '', '', 1);
+    return empty($courses) ? false : true;
+}
+
 /**
  * Get a list of courses and also include 'Site' for site wide rules.
  *
@@ -103,8 +124,11 @@ function tool_monitor_get_user_courses() {
     if (has_capability('tool/monitor:subscribe', context_system::instance())) {
         $options[0] = get_string('site');
     }
-    if ($courses = get_user_capability_course('tool/monitor:subscribe', null, true, 'fullname, visible', $orderby)) {
+
+    $fields = 'fullname, visible, ctxid, ctxpath, ctxdepth, ctxlevel, ctxinstance';
+    if ($courses = get_user_capability_course('tool/monitor:subscribe', null, true, $fields, $orderby)) {
         foreach ($courses as $course) {
+            context_helper::preload_from_record($course);
             $coursectx = context_course::instance($course->id);
             if ($course->visible || has_capability('moodle/course:viewhiddencourses', $coursectx)) {
                 $options[$course->id] = format_string($course->fullname, true, array('context' => $coursectx));
index aa15d83..9acc86d 100644 (file)
@@ -103,7 +103,7 @@ Feature: tool_monitor_subscriptions
     Given I log in as "admin"
     And I follow "Preferences" in the user menu
     And I click on "Notification preferences" "link" in the "#page-content" "css_element"
-    And I click on ".preference-state" "css_element" in the "Notifications of rule subscriptions" "table_row"
+    And I click on "//td[@data-processor-name='popup']//label[@class='preference-state']" "xpath_element" in the "Notifications of rule subscriptions" "table_row"
     And I wait until the page is ready
     And I follow "Preferences" in the user menu
     And I follow "Event monitoring"
@@ -124,7 +124,7 @@ Feature: tool_monitor_subscriptions
     Given I log in as "teacher1"
     And I follow "Preferences" in the user menu
     And I click on "Notification preferences" "link" in the "#page-content" "css_element"
-    And I click on ".preference-state" "css_element" in the "Notifications of rule subscriptions" "table_row"
+    And I click on "//td[@data-processor-name='popup']//label[@class='preference-state']" "xpath_element" in the "Notifications of rule subscriptions" "table_row"
     And I wait until the page is ready
     And I follow "Preferences" in the user menu
     And I follow "Event monitoring"
index 6724fb0..2a46e91 100644 (file)
@@ -26,6 +26,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2016120500;     // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2017021300;     // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2016112900;     // Requires this Moodle version.
 $plugin->component = 'tool_monitor'; // Full name of the plugin (used for diagnostics).
index fa6fe77..593ea98 100644 (file)
@@ -23,6 +23,7 @@
  */
 
 $string['asap'] = 'ASAP';
+$string['backtoscheduledtasks'] = 'Back to scheduled tasks';
 $string['blocking'] = 'Blocking';
 $string['component'] = 'Component';
 $string['corecomponent'] = 'Core';
@@ -30,6 +31,8 @@ $string['default'] = 'Default';
 $string['disabled'] = 'Disabled';
 $string['disabled_help'] = 'Disabled scheduled tasks are not executed from cron, however they can still be executed manually via the CLI tool.';
 $string['edittaskschedule'] = 'Edit task schedule: {$a}';
+$string['enablerunnow'] = 'Allow &lsquo;Run now&rsquo; for scheduled tasks';
+$string['enablerunnow_desc'] = 'Allows administrators to run a single scheduled task immediately, rather than waiting for it to run as scheduled. The task runs on the web server, so some sites may wish to disable this feature to avoid potential performance issues.';
 $string['faildelay'] = 'Fail delay';
 $string['lastruntime'] = 'Last run';
 $string['nextruntime'] = 'Next run';
@@ -37,6 +40,8 @@ $string['plugindisabled'] = 'Plugin disabled';
 $string['pluginname'] = 'Scheduled task configuration';
 $string['resettasktodefaults'] = 'Reset task schedule to defaults';
 $string['resettasktodefaults_help'] = 'This will discard any local changes and revert the schedule for this task back to its original settings.';
+$string['runnow'] = 'Run now';
+$string['runnow_confirm'] = 'Are you sure you want to run this task &lsquo;{$a}&rsquo; now? The task will run on the web server and may take some time to complete.';
 $string['scheduledtasks'] = 'Scheduled tasks';
 $string['scheduledtaskchangesdisabled'] = 'Modifications to the list of scheduled tasks have been prevented in Moodle configuration';
 $string['taskdisabled'] = 'Task disabled';
index 076f092..575aa66 100644 (file)
@@ -104,11 +104,19 @@ class tool_task_renderer extends plugin_renderer_base {
                 $nextrun = $asap;
             }
 
+            $runnow = '';
+            if (!$disabled && get_config('tool_task', 'enablerunnow')) {
+                $runnow = html_writer::div(html_writer::link(
+                        new moodle_url('/admin/tool/task/schedule_task.php',
+                            array('task' => get_class($task))),
+                        get_string('runnow', 'tool_task')), 'task-runnow');
+            }
+
             $row = new html_table_row(array(
                         $namecell,
                         $componentcell,
                         new html_table_cell($editlink),
-                        new html_table_cell($lastrun),
+                        new html_table_cell($lastrun . $runnow),
                         new html_table_cell($nextrun),
                         new html_table_cell($task->get_minute()),
                         new html_table_cell($task->get_hour()),
@@ -133,4 +141,14 @@ class tool_task_renderer extends plugin_renderer_base {
         $table->data = $data;
         return html_writer::table($table);
     }
+
+    /**
+     * Renders a link back to the scheduled tasks page (used from the 'run now' screen).
+     *
+     * @return string HTML code
+     */
+    public function link_back() {
+        return $this->render_from_template('tool_task/link_back',
+                array('url' => new moodle_url('/admin/tool/task/scheduledtasks.php')));
+    }
 }
diff --git a/admin/tool/task/schedule_task.php b/admin/tool/task/schedule_task.php
new file mode 100644 (file)
index 0000000..53136f2
--- /dev/null
@@ -0,0 +1,101 @@
+<?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/>.
+
+/**
+ * Web cron single task
+ *
+ * This script runs a single scheduled task from the web UI.
+ *
+ * @package tool_task
+ * @copyright 2016 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require('../../../config.php');
+
+require_once($CFG->libdir.'/cronlib.php');
+
+/**
+ * Function used to handle mtrace by outputting the text to normal browser window.
+ *
+ * @param string $message Message to output
+ * @param string $eol End of line character
+ */
+function tool_task_mtrace_wrapper($message, $eol) {
+    echo s($message . $eol);
+    // Both types of flush may be necessary in order to actually output progressively to browser.
+    // It depends on the theme.
+    if (ob_get_status()) {
+        ob_flush();
+    }
+    flush();
+}
+
+// Allow execution of single task. This requires login and has different rules.
+$taskname = required_param('task', PARAM_RAW_TRIMMED);
+
+// Basic security checks.
+require_login();
+$context = context_system::instance();
+require_capability('moodle/site:config', $context);
+
+if (!get_config('tool_task', 'enablerunnow')) {
+    print_error('nopermissions', 'error', '', get_string('runnow', 'tool_task'));
+}
+
+// Check input parameter against all existing tasks (this ensures it isn't possible to
+// create some kind of security problem by specifying a class that isn't a task or whatever).
+$task = \core\task\manager::get_scheduled_task($taskname);
+if (!$task) {
+    print_error('cannotfindinfo', 'error', $taskname);
+}
+
+// Start output.
+$PAGE->set_url(new moodle_url('/admin/tool/task/schedule_task.php'));
+$PAGE->set_context($context);
+$PAGE->navbar->add(get_string('scheduledtasks', 'tool_task'), new moodle_url('/admin/tool/task/scheduledtasks.php'));
+$PAGE->navbar->add(s($task->get_name()));
+echo $OUTPUT->header();
+echo $OUTPUT->heading($task->get_name());
+
+// The initial request just shows the confirmation page; we don't do anything further unless
+// they confirm.
+if (!optional_param('confirm', 0, PARAM_INT)) {
+    echo $OUTPUT->confirm(get_string('runnow_confirm', 'tool_task', $task->get_name()),
+            new single_button(new moodle_url('/admin/tool/task/schedule_task.php',
+            array('task' => $taskname, 'confirm' => 1, 'sesskey' => sesskey())),
+            get_string('runnow', 'tool_task')),
+            new single_button(new moodle_url('/admin/tool/task/scheduledtasks.php'),
+            get_string('cancel'), false));
+    echo $OUTPUT->footer();
+    exit;
+}
+
+// Action requires session key.
+require_sesskey();
+
+// Prepare to handle output via mtrace.
+echo html_writer::start_tag('pre');
+$CFG->mtrace_wrapper = 'tool_task_mtrace_wrapper';
+
+// Run the specified task (this will output an error if it doesn't exist).
+cron_run_single_task($task);
+echo html_writer::end_tag('pre');
+
+$output = $PAGE->get_renderer('tool_task');
+echo $output->link_back();
+
+echo $OUTPUT->footer();
index 8aca57c..7714674 100644 (file)
@@ -9,3 +9,7 @@
     /*rtl:ignore*/
     direction: ltr;
 }
+
+#page-admin-tool-task-scheduledtasks .task-runnow {
+    font-size: 0.75em;
+}
diff --git a/admin/tool/task/templates/link_back.mustache b/admin/tool/task/templates/link_back.mustache
new file mode 100644 (file)
index 0000000..b6bd324
--- /dev/null
@@ -0,0 +1,31 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_task/link_back
+
+    Template for displaying link back from another page to the scheduled tasks page.
+
+    Context variables required for this template:
+    * url - moodle_url|string URL of scheduled tasks page
+
+    Example context (json):
+    {
+        "url" : "https://www.example.org/admin/tool/task/scheduledtasks.php"
+    }
+
+}}
+<p><a href="{{url}}">{{# str }} backtoscheduledtasks, tool_task {{/str}}</a></p>
diff --git a/admin/tool/task/tests/behat/run_task_now.feature b/admin/tool/task/tests/behat/run_task_now.feature
new file mode 100644 (file)
index 0000000..4740732
--- /dev/null
@@ -0,0 +1,35 @@
+@tool @tool_task
+Feature: Run tasks from web interface
+  In order to run scheduled tasks immediately
+  As an admin
+  I need to be able to run a task from the web interface
+
+  Scenario: Run a task
+    Given I log in as "admin"
+    When I navigate to "Scheduled tasks" node in "Site administration > Server"
+    Then I should see "Never" in the "Log table cleanup" "table_row"
+
+    And I click on "Run now" "text" in the "Log table cleanup" "table_row"
+    And I should see "Are you sure you want to run this task"
+    And I press "Run now"
+
+    And I should see "Log table cleanup" in the "h2" "css_element"
+    And I should see "Scheduled task complete: Log table cleanup"
+
+    And I follow "Back to scheduled tasks"
+    And I should not see "Never" in the "Log table cleanup" "table_row"
+
+  Scenario: Cancel running a task
+    Given I log in as "admin"
+    When I navigate to "Scheduled tasks" node in "Site administration > Server"
+    And I click on "Run now" "text" in the "Log table cleanup" "table_row"
+    And I press "Cancel"
+    # Confirm we're back on the scheduled tasks page by looking for the table.
+    Then "Log table cleanup" "table_row" should exist
+
+  Scenario: Cannot run a task when the option is disabled
+    Given the following config values are set as admin:
+      | enablerunnow | 0 | tool_task |
+    When I log in as "admin"
+    And I navigate to "Scheduled tasks" node in "Site administration > Server"
+    Then I should not see "Run now"
index ee0eb52..da8a941 100644 (file)
@@ -44,6 +44,11 @@ class helper {
      */
     const MOVE_DOWN = 1;
 
+    /**
+     * @var boolean Has it been bootstrapped?
+     */
+    private static $bootstrapped = false;
+
     /**
      * Get the link to edit the step.
      *
@@ -488,6 +493,11 @@ class helper {
     public static function bootstrap() {
         global $PAGE;
 
+        if (self::$bootstrapped) {
+            return;
+        }
+        self::$bootstrapped = true;
+
         if ($tour = manager::get_current_tour()) {
             $PAGE->requires->js_call_amd('tool_usertours/usertours', 'init', [
                     $tour->get_id(),
index 476dd09..cd3c867 100644 (file)
@@ -67,3 +67,10 @@ function tool_usertours_inplace_editable($itemtype, $itemid, $newvalue) {
 function tool_usertours_extend_navigation_user() {
     \tool_usertours\helper::bootstrap();
 }
+
+/**
+ * Add JS to bootstrap tours. Only in Moodle 3.3+
+ */
+function tool_usertours_before_footer() {
+    \tool_usertours\helper::bootstrap();
+}
index 5b924bb..a58dfe6 100644 (file)
@@ -64,8 +64,6 @@ class login implements renderable, templatable {
     public $instructions;
     /** @var moodle_url The form action login URL. */
     public $loginurl;
-    /** @var bool Whether the password can be auto completed. */
-    public $passwordautocomplete;
     /** @var bool Whether the username should be remembered. */
     public $rememberusername;
     /** @var moodle_url The sign-up URL. */
@@ -90,7 +88,6 @@ class login implements renderable, templatable {
         $this->cookieshelpicon = new help_icon('cookiesenabled', 'core');
 
         $this->autofocusform = !empty($CFG->loginpageautofocus);
-        $this->passwordautocomplete = !empty($CFG->loginpasswordautocomplete);
         $this->rememberusername = isset($CFG->rememberusername) and $CFG->rememberusername == 2;
 
         $this->forgotpasswordurl = new moodle_url($CFG->httpswwwroot . '/login/forgot_password.php');
@@ -149,7 +146,6 @@ class login implements renderable, templatable {
             context_system::instance()->id);
         $data->loginurl = $this->loginurl->out(false);
         $data->rememberusername = $this->rememberusername;
-        $data->passwordautocomplete = $this->passwordautocomplete;
         $data->signupurl = $this->signupurl->out(false);
         $data->username = $this->username;
 
index 3f36f96..0361f10 100644 (file)
@@ -125,6 +125,11 @@ abstract class backup implements checksumable {
     const OPERATION_BACKUP  ='backup'; // We are performing one backup
     const OPERATION_RESTORE ='restore';// We are performing one restore
 
+    // Options for "Include enrolment methods" restore setting.
+    const ENROL_NEVER     = 0;
+    const ENROL_WITHUSERS = 1;
+    const ENROL_ALWAYS    = 2;
+
     // Version and release (to keep CFG->backup_version (and release) updated automatically).
     /**
      * Usually same than major release version, this is used to mark important
index 2829b99..800a2df 100644 (file)
@@ -50,6 +50,7 @@ class restore_controller extends base_controller {
     protected $precheck;    // Results of the execution of restore prechecks
 
     protected $info;   // Information retrieved from backup contents
+    /** @var restore_plan */
     protected $plan;   // Restore execution plan
 
     protected $execution;     // inmediate/delayed
index a51d34d..f81e3be 100644 (file)
@@ -264,7 +264,7 @@ class backup_module_structure_step extends backup_structure_step {
 
         $module = new backup_nested_element('module', array('id', 'version'), array(
             'modulename', 'sectionid', 'sectionnumber', 'idnumber',
-            'added', 'score', 'indent', 'visible',
+            'added', 'score', 'indent', 'visible', 'visibleoncoursepage',
             'visibleold', 'groupmode', 'groupingid',
             'completion', 'completiongradeitemnumber', 'completionview', 'completionexpected',
             'availability', 'showdescription'));
index 733ab1f..49587c8 100644 (file)
@@ -78,7 +78,10 @@ class restore_course_task extends restore_task {
             // No need to do anything with enrolments.
 
         } else if (!$this->get_setting_value('users') or $this->plan->get_mode() == backup::MODE_HUB) {
-            if ($this->get_target() == backup::TARGET_CURRENT_ADDING or $this->get_target() == backup::TARGET_EXISTING_ADDING) {
+            if ($this->get_setting_value('enrolments') == backup::ENROL_ALWAYS && $this->plan->get_mode() != backup::MODE_HUB) {
+                // Restore enrolment methods.
+                $this->add_step(new restore_enrolments_structure_step('course_enrolments', 'enrolments.xml'));
+            } else if ($this->get_target() == backup::TARGET_CURRENT_ADDING or $this->get_target() == backup::TARGET_EXISTING_ADDING) {
                 // Keep current enrolments unchanged.
             } else {
                 // If no instances yet add default enrol methods the same way as when creating new course in UI.
index 42b7c80..eb03318 100644 (file)
@@ -112,12 +112,26 @@ class restore_root_task extends restore_task {
         $users->get_ui()->set_changeable($changeable);
         $this->add_setting($users);
 
-        $rootenrolmanual = new restore_users_setting('enrol_migratetomanual', base_setting::IS_BOOLEAN, false);
-        $rootenrolmanual->set_ui(new backup_setting_ui_checkbox($rootenrolmanual, get_string('rootenrolmanual', 'backup')));
-        $rootenrolmanual->get_ui()->set_changeable(enrol_is_enabled('manual'));
-        $rootenrolmanual->get_ui()->set_changeable($changeable);
-        $this->add_setting($rootenrolmanual);
-        $users->add_dependency($rootenrolmanual);
+        // Restore enrolment methods.
+        if ($changeable) {
+            $options = [
+                backup::ENROL_NEVER     => get_string('rootsettingenrolments_never', 'backup'),
+                backup::ENROL_WITHUSERS => get_string('rootsettingenrolments_withusers', 'backup'),
+                backup::ENROL_ALWAYS    => get_string('rootsettingenrolments_always', 'backup'),
+            ];
+            $enroldefault = backup::ENROL_WITHUSERS;
+        } else {
+            // Users can not be restored, simplify the dropdown.
+            $options = [
+                backup::ENROL_NEVER     => get_string('no'),
+                backup::ENROL_ALWAYS    => get_string('yes')
+            ];
+            $enroldefault = backup::ENROL_NEVER;
+        }
+        $enrolments = new restore_users_setting('enrolments', base_setting::IS_INTEGER, $enroldefault);
+        $enrolments->set_ui(new backup_setting_ui_select($enrolments, get_string('rootsettingenrolments', 'backup'),
+            $options));
+        $this->add_setting($enrolments);
 
         // Define role_assignments (dependent of users)
         $defaultvalue = false;                      // Safer default
index e07b50f..5bf7bdf 100644 (file)
@@ -2154,12 +2154,17 @@ class restore_enrolments_structure_step extends restore_structure_step {
 
     protected function define_structure() {
 
-        $enrol = new restore_path_element('enrol', '/enrolments/enrols/enrol');
-        $enrolment = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment');
+        $userinfo = $this->get_setting_value('users');
+
+        $paths = [];
+        $paths[] = $enrol = new restore_path_element('enrol', '/enrolments/enrols/enrol');
+        if ($userinfo) {
+            $paths[] = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment');
+        }
         // Attach local plugin stucture to enrol element.
         $this->add_plugin_structure('enrol', $enrol);
 
-        return array($enrol, $enrolment);
+        return $paths;
     }
 
     /**
@@ -2203,7 +2208,14 @@ class restore_enrolments_structure_step extends restore_structure_step {
         $data->roleid   = $this->get_mappingid('role', $data->roleid);
         $data->courseid = $courserec->id;
 
-        if ($this->get_setting_value('enrol_migratetomanual')) {
+        if (!$this->get_setting_value('users') && $this->get_setting_value('enrolments') == backup::ENROL_WITHUSERS) {
+            $converttomanual = true;
+        } else {
+            $converttomanual = ($this->get_setting_value('enrolments') == backup::ENROL_NEVER);
+        }
+
+        if ($converttomanual) {
+            // Restore enrolments as manual enrolments.
             unset($data->sortorder); // Remove useless sortorder from <2.4 backups.
             if (!enrol_is_enabled('manual')) {
                 $this->set_mapping('enrol', $oldid, 0);
@@ -2224,7 +2236,7 @@ class restore_enrolments_structure_step extends restore_structure_step {
         } else {
             if (!enrol_is_enabled($data->enrol) or !isset($this->plugins[$data->enrol])) {
                 $this->set_mapping('enrol', $oldid, 0);
-                $message = "Enrol plugin '$data->enrol' data can not be restored because it is not enabled, use migration to manual enrolments";
+                $message = "Enrol plugin '$data->enrol' data can not be restored because it is not enabled, consider restoring without enrolment methods";
                 $this->log($message, backup::LOG_WARNING);
                 return;
             }
@@ -2670,7 +2682,7 @@ class restore_calendarevents_structure_step extends restore_structure_step {
                 'courseid'       => $this->get_courseid(),
                 'groupid'        => $data->groupid,
                 'userid'         => $data->userid,
-                'repeatid'       => $data->repeatid,
+                'repeatid'       => $this->get_mappingid('event', $data->repeatid),
                 'modulename'     => $data->modulename,
                 'eventtype'      => $data->eventtype,
                 'timestart'      => $this->apply_date_offset($data->timestart),
@@ -2688,18 +2700,27 @@ class restore_calendarevents_structure_step extends restore_structure_step {
                   FROM {event}
                  WHERE " . $DB->sql_compare_text('name', 255) . " = " . $DB->sql_compare_text('?', 255) . "
                    AND courseid = ?
-                   AND repeatid = ?
                    AND modulename = ?
                    AND timestart = ?
                    AND timeduration = ?
                    AND " . $DB->sql_compare_text('description', 255) . " = " . $DB->sql_compare_text('?', 255);
-        $arg = array ($params['name'], $params['courseid'], $params['repeatid'], $params['modulename'], $params['timestart'], $params['timeduration'], $params['description']);
+        $arg = array ($params['name'], $params['courseid'], $params['modulename'], $params['timestart'], $params['timeduration'], $params['description']);
         $result = $DB->record_exists_sql($sql, $arg);
         if (empty($result)) {
             $newitemid = $DB->insert_record('event', $params);
             $this->set_mapping('event', $oldid, $newitemid);
             $this->set_mapping('event_description', $oldid, $newitemid, $restorefiles);
         }
+        // With repeating events, each event has the repeatid pointed at the first occurrence.
+        // Since the repeatid will be empty when the first occurrence is restored,
+        // Get the repeatid from the second occurrence of the repeating event and use that to update the first occurrence.
+        // Then keep a list of repeatids so we only perform this update once.
+        static $repeatids = array();
+        if (!empty($params['repeatid']) && !in_array($params['repeatid'], $repeatids)) {
+            // This entry is repeated so the repeatid field must be set.
+            $DB->set_field('event', 'repeatid', $params['repeatid'], array('id' => $params['repeatid']));
+            $repeatids[] = $params['repeatid'];
+        }
 
     }
     protected function after_execute() {
index a3b055a..a5f531c 100644 (file)
@@ -541,4 +541,265 @@ class core_backup_moodle2_testcase extends advanced_testcase {
         }
         return $newcmid;
     }
+
+    /**
+     * Help function for enrolment methods backup/restore tests:
+     *
+     * - Creates a course ($course), adds self-enrolment method and a user
+     * - Makes a backup
+     * - Creates a target course (if requested) ($newcourseid)
+     * - Initialises restore controller for this backup file ($rc)
+     *
+     * @param int $target target for restoring: backup::TARGET_NEW_COURSE etc.
+     * @param array $additionalcaps - additional capabilities to give to user
+     * @return array array of original course, new course id, restore controller: [$course, $newcourseid, $rc]
+     */
+    protected function prepare_for_enrolments_test($target, $additionalcaps = []) {
+        global $CFG, $DB;
+        $this->resetAfterTest(true);
+
+        // Turn off file logging, otherwise it can't delete the file (Windows).
+        $CFG->backup_file_logger_level = backup::LOG_NONE;
+
+        $user = $this->getDataGenerator()->create_user();
+        $roleidcat = create_role('Category role', 'dummyrole1', 'dummy role description');
+
+        $course = $this->getDataGenerator()->create_course();
+
+        // Enable instance of self-enrolment plugin (it should already be present) and enrol a student with it.
+        $selfplugin = enrol_get_plugin('self');
+        $selfinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'self'));
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $selfplugin->update_status($selfinstance, ENROL_INSTANCE_ENABLED);
+        $selfplugin->enrol_user($selfinstance, $user->id, $studentrole->id);
+
+        // Give current user capabilities to do backup and restore and assign student role.
+        $categorycontext = context_course::instance($course->id)->get_parent_context();
+
+        $caps = array_merge([
+            'moodle/course:view',
+            'moodle/course:create',
+            'moodle/backup:backupcourse',
+            'moodle/backup:configure',
+            'moodle/backup:backuptargetimport',
+            'moodle/restore:restorecourse',
+            'moodle/role:assign',
+            'moodle/restore:configure',
+        ], $additionalcaps);
+
+        foreach ($caps as $cap) {
+            assign_capability($cap, CAP_ALLOW, $roleidcat, $categorycontext);
+        }
+
+        allow_assign($roleidcat, $studentrole->id);
+        role_assign($roleidcat, $user->id, $categorycontext);
+        accesslib_clear_all_caches_for_unit_testing();
+
+        $this->setUser($user);
+
+        // Do backup with default settings. MODE_IMPORT means it will just
+        // create the directory and not zip it.
+        $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
+            backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_SAMESITE,
+            $user->id);
+        $backupid = $bc->get_backupid();
+        $backupbasepath = $bc->get_plan()->get_basepath();
+        $bc->execute_plan();
+        $results = $bc->get_results();
+        $file = $results['backup_destination'];
+        $bc->destroy();
+
+        // Restore the backup immediately.
+
+        // Check if we need to unzip the file because the backup temp dir does not contains backup files.
+        if (!file_exists($backupbasepath . "/moodle_backup.xml")) {
+            $file->extract_to_pathname(get_file_packer('application/vnd.moodle.backup'), $backupbasepath);
+        }
+
+        if ($target == backup::TARGET_NEW_COURSE) {
+            $newcourseid = restore_dbops::create_new_course($course->fullname . '_2',
+                $course->shortname . '_2',
+                $course->category);
+        } else {
+            $newcourse = $this->getDataGenerator()->create_course();
+            $newcourseid = $newcourse->id;
+        }
+        $rc = new restore_controller($backupid, $newcourseid,
+            backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $user->id, $target);
+
+        return [$course, $newcourseid, $rc];
+    }
+
+    /**
+     * Backup a course with enrolment methods and restore it without user data and without enrolment methods
+     */
+    public function test_restore_without_users_without_enrolments() {
+        global $DB;
+
+        list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE);
+
+        // Ensure enrolment methods will not be restored without capability.
+        $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value());
+        $this->assertEquals(false, $rc->get_plan()->get_setting('users')->get_value());
+
+        $this->assertTrue($rc->execute_precheck());
+        $rc->execute_plan();
+        $rc->destroy();
+
+        // Self-enrolment method was not enabled, users were not restored.
+        $this->assertEmpty($DB->count_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
+            'status' => ENROL_INSTANCE_ENABLED]));
+        $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue
+          join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?";
+        $enrolments = $DB->get_records_sql($sql, [$newcourseid]);
+        $this->assertEmpty($enrolments);
+    }
+
+    /**
+     * Backup a course with enrolment methods and restore it without user data with enrolment methods
+     */
+    public function test_restore_without_users_with_enrolments() {
+        global $DB;
+
+        list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE,
+            ['moodle/course:enrolconfig']);
+
+        // Ensure enrolment methods will be restored.
+        $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value());
+        $this->assertEquals(false, $rc->get_plan()->get_setting('users')->get_value());
+        // Set "Include enrolment methods" to "Always" so they can be restored without users.
+        $rc->get_plan()->get_setting('enrolments')->set_value(backup::ENROL_ALWAYS);
+
+        $this->assertTrue($rc->execute_precheck());
+        $rc->execute_plan();
+        $rc->destroy();
+
+        // Self-enrolment method was restored (it is enabled), users were not restored.
+        $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
+            'status' => ENROL_INSTANCE_ENABLED]);
+        $this->assertNotEmpty($enrol);
+
+        $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue
+            join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?";
+        $enrolments = $DB->get_records_sql($sql, [$newcourseid]);
+        $this->assertEmpty($enrolments);
+    }
+
+    /**
+     * Backup a course with enrolment methods and restore it with user data and without enrolment methods
+     */
+    public function test_restore_with_users_without_enrolments() {
+        global $DB;
+
+        list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE,
+            ['moodle/backup:userinfo', 'moodle/restore:userinfo']);
+
+        // Ensure enrolment methods will not be restored without capability.
+        $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value());
+        $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value());
+
+        global $qwerty;
+        $qwerty = 1;
+        $this->assertTrue($rc->execute_precheck());
+        $rc->execute_plan();
+        $rc->destroy();
+        $qwerty = 0;
+
+        // Self-enrolment method was not restored, student was restored as manual enrolment.
+        $this->assertEmpty($DB->count_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
+            'status' => ENROL_INSTANCE_ENABLED]));
+
+        $enrol = $DB->get_record('enrol', ['enrol' => 'manual', 'courseid' => $newcourseid]);
+        $this->assertEquals(1, $DB->count_records('user_enrolments', ['enrolid' => $enrol->id]));
+    }
+
+    /**
+     * Backup a course with enrolment methods and restore it with user data with enrolment methods
+     */
+    public function test_restore_with_users_with_enrolments() {
+        global $DB;
+
+        list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE,
+            ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']);
+
+        // Ensure enrolment methods will be restored.
+        $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value());
+        $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value());
+
+        $this->assertTrue($rc->execute_precheck());
+        $rc->execute_plan();
+        $rc->destroy();
+
+        // Self-enrolment method was restored (it is enabled), student was restored.
+        $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
+            'status' => ENROL_INSTANCE_ENABLED]);
+        $this->assertNotEmpty($enrol);
+
+        $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue
+            join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?";
+        $enrolments = $DB->get_records_sql($sql, [$newcourseid]);
+        $this->assertEquals(1, count($enrolments));
+        $enrolment = reset($enrolments);
+        $this->assertEquals('self', $enrolment->enrol);
+    }
+
+    /**
+     * Backup a course with enrolment methods and restore it with user data with enrolment methods merging into another course
+     */
+    public function test_restore_with_users_with_enrolments_merging() {
+        global $DB;
+
+        list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_EXISTING_ADDING,
+            ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']);
+
+        // Ensure enrolment methods will be restored.
+        $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value());
+        $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value());
+
+        $this->assertTrue($rc->execute_precheck());
+        $rc->execute_plan();
+        $rc->destroy();
+
+        // User was restored with self-enrolment method.
+        $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
+            'status' => ENROL_INSTANCE_ENABLED]);
+        $this->assertNotEmpty($enrol);
+
+        $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue
+            join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?";
+        $enrolments = $DB->get_records_sql($sql, [$newcourseid]);
+        $this->assertEquals(1, count($enrolments));
+        $enrolment = reset($enrolments);
+        $this->assertEquals('self', $enrolment->enrol);
+    }
+
+    /**
+     * Backup a course with enrolment methods and restore it with user data with enrolment methods into another course deleting it's contents
+     */
+    public function test_restore_with_users_with_enrolments_deleting() {
+        global $DB;
+
+        list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_EXISTING_DELETING,
+            ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']);
+
+        // Ensure enrolment methods will be restored.
+        $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value());
+        $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value());
+
+        $this->assertTrue($rc->execute_precheck());
+        $rc->execute_plan();
+        $rc->destroy();
+
+        // Self-enrolment method was restored (it is enabled), student was restored.
+        $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
+            'status' => ENROL_INSTANCE_ENABLED]);
+        $this->assertNotEmpty($enrol);
+
+        $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue
+            join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?";
+        $enrolments = $DB->get_records_sql($sql, [$newcourseid]);
+        $this->assertEquals(1, count($enrolments));
+        $enrolment = reset($enrolments);
+        $this->assertEquals('self', $enrolment->enrol);
+    }
 }
index fd13cbb..867d7cc 100644 (file)
@@ -203,6 +203,20 @@ abstract class restore_check {
                 $overwritesetting = $restore_controller->get_plan()->get_setting('overwrite_conf');
                 $overwritesetting->set_status(base_setting::LOCKED_BY_PERMISSION);
             }
+
+            // Ensure the user has the capability to manage enrolment methods. If not we want to unset and lock
+            // the setting so that they cannot change it.
+            $hasmanageenrolcap = has_capability('moodle/course:enrolconfig', $coursectx, $userid);
+            if (!$hasmanageenrolcap) {
+                if ($restore_controller->get_plan()->setting_exists('enrolments')) {
+                    $enrolsetting = $restore_controller->get_plan()->get_setting('enrolments');
+                    if ($enrolsetting->get_value() != backup::ENROL_NEVER) {
+                        $enrolsetting->set_status(base_setting::NOT_LOCKED); // In case it was locked earlier.
+                        $enrolsetting->set_value(backup::ENROL_NEVER);
+                    }
+                    $enrolsetting->set_status(base_setting::LOCKED_BY_PERMISSION);
+                }
+            }
         }
 
         return true;
index 3015d22..0cb151b 100644 (file)
@@ -519,7 +519,7 @@ class core_backup_renderer extends plugin_renderer_base {
             $method = 'get';
         }
         $url->param('sesskey', sesskey());
-        $button = new single_button($url, get_string('continue'), $method);
+        $button = new single_button($url, get_string('continue'), $method, true);
         $button->class = 'continuebutton';
         return $this->render($button);
     }
index aa3f0b0..0342ee6 100644 (file)
@@ -52,6 +52,7 @@ class block_activity_results_edit_form extends block_edit_form {
         $sql = 'SELECT id, itemname FROM {grade_items} WHERE courseid = ? and itemtype = ? and (gradetype = ? or gradetype = ?)';
         $params = array($this->page->course->id, 'mod', GRADE_TYPE_VALUE, GRADE_TYPE_SCALE);
         $activities = $DB->get_records_sql_menu($sql, $params);
+        core_collator::asort($activities);
 
         if (empty($activities)) {
             $mform->addElement('static', 'noactivitieswarning', get_string('config_select_activity', 'block_activity_results'),
index be906de..c08835d 100644 (file)
@@ -56,11 +56,6 @@ class block_login extends block_base {
         // TODO: now that we have multiauth it is hard to find out if there is a way to change password
         $forgot = $wwwroot . '/login/forgot_password.php';
 
-        if (!empty($CFG->loginpasswordautocomplete)) {
-            $autocomplete = 'autocomplete="off"';
-        } else {
-            $autocomplete = '';
-        }
 
         $username = get_moodle_cookie();
 
@@ -75,14 +70,14 @@ class block_login extends block_base {
                 $strusername = get_string('usernameemail');
             }
 
-            $this->content->text .= "\n".'<form class="loginform" id="login" method="post" action="'.get_login_url().'" '.$autocomplete.'>';
+            $this->content->text .= "\n".'<form class="loginform" id="login" method="post" action="'.get_login_url().'">';
 
             $this->content->text .= '<div class="form-group"><label for="login_username">'.$strusername.'</label>';
             $this->content->text .= '<input type="text" name="username" id="login_username" class="form-control" value="'.s($username).'" /></div>';
 
             $this->content->text .= '<div class="form-group"><label for="login_password">'.get_string('password').'</label>';
 
-            $this->content->text .= '<input type="password" name="password" id="login_password" class="form-control" value="" '.$autocomplete.' /></div>';
+            $this->content->text .= '<input type="password" name="password" id="login_password" class="form-control" value="" /></div>';
 
             if (isset($CFG->rememberusername) and $CFG->rememberusername == 2) {
                 $checked = $username ? 'checked="checked"' : '';
index 26bbf00..60b20a3 100644 (file)
@@ -59,7 +59,7 @@ class block_site_main_menu extends block_list {
             if (!empty($modinfo->sections[0])) {
                 foreach($modinfo->sections[0] as $cmid) {
                     $cm = $modinfo->cms[$cmid];
-                    if (!$cm->uservisible) {
+                    if (!$cm->uservisible || !$cm->is_visible_on_course_page()) {
                         continue;
                     }
 
@@ -72,7 +72,7 @@ class block_site_main_menu extends block_list {
                     if (!empty($cm->url)) {
                         $content = html_writer::div($courserenderer->course_section_cm_name($cm), 'activity');
                     } else {
-                        $content = $cm->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
+                        $content = $courserenderer->course_section_cm_text($cm);
                     }
 
                     $this->content->items[] = $indent . html_writer::div($content, 'main-menu-content');
@@ -103,7 +103,7 @@ class block_site_main_menu extends block_list {
         if (!empty($modinfo->sections[0])) {
             foreach ($modinfo->sections[0] as $modnumber) {
                 $mod = $modinfo->cms[$modnumber];
-                if (!$mod->uservisible) {
+                if (!$mod->uservisible || !$mod->is_visible_on_course_page()) {
                     continue;
                 }
                 if (!$ismoving) {
@@ -138,7 +138,7 @@ class block_site_main_menu extends block_list {
                         $indent = '';
                     }
                     if (!$mod->url) {
-                        $content = $mod->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
+                        $content = $courserenderer->course_section_cm_text($mod);
                     } else {
                         $content = html_writer::div($courserenderer->course_section_cm_name($mod), ' activity');
                     }
index 62054a0..a9cfb61 100644 (file)
@@ -1,4 +1,4 @@
-@block @block_main_menu
+@block @block_site_main_menu
 Feature: Add URL to main menu block
   In order to add helpful resources for students
   As a admin
diff --git a/blocks/site_main_menu/tests/behat/behat_block_site_main_menu.php b/blocks/site_main_menu/tests/behat/behat_block_site_main_menu.php
new file mode 100644 (file)
index 0000000..f523116
--- /dev/null
@@ -0,0 +1,157 @@
+<?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/>.
+
+/**
+ * Behat steps definitions for block site main menu
+ *
+ * @package    block_site_main_menu
+ * @category   test
+ * @copyright  2016 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
+
+use Behat\Mink\Exception\ExpectationException as ExpectationException,
+    Behat\Mink\Exception\DriverException as DriverException,
+    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+
+/**
+ * Behat steps definitions for block site main menu
+ *
+ * @package    block_site_main_menu
+ * @category   test
+ * @copyright  2016 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_block_site_main_menu extends behat_base {
+
+    /**
+     * Returns the DOM node of the activity in the site menu block
+     *
+     * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param string $activityname The activity name
+     * @return NodeElement
+     */
+    protected function get_site_menu_activity_node($activityname) {
+        $activityname = behat_context_helper::escape($activityname);
+        $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]//li[contains(., $activityname)]";
+
+        return $this->find('xpath', $xpath);
+    }
+
+    /**
+     * Checks that the specified activity's action menu contains an item.
+     *
+     * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" activity in site main menu block should have "(?P<icon_name_string>(?:[^"]|\\")*)" editing icon$/
+     * @param string $activityname
+     * @param string $iconname
+     */
+    public function activity_in_site_main_menu_block_should_have_editing_icon($activityname, $iconname) {
+        $activitynode = $this->get_site_menu_activity_node($activityname);
+
+        $notfoundexception = new ExpectationException('"' . $activityname . '" doesn\'t have a "' .
+            $iconname . '" editing icon', $this->getSession());
+        $this->find('named_partial', array('link', $iconname), $notfoundexception, $activitynode);
+    }
+
+    /**
+     * Checks that the specified activity's action menu contains an item.
+     *
+     * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" activity in site main menu block should not have "(?P<icon_name_string>(?:[^"]|\\")*)" editing icon$/
+     * @param string $activityname
+     * @param string $iconname
+     */
+    public function activity_in_site_main_menu_block_should_not_have_editing_icon($activityname, $iconname) {
+        $activitynode = $this->get_site_menu_activity_node($activityname);
+
+        try {
+            $this->find('named_partial', array('link', $iconname), false, $activitynode);
+            throw new ExpectationException('"' . $activityname . '" has a "' . $iconname .
+                '" editing icon when it should not', $this->getSession());
+        } catch (ElementNotFoundException $e) {
+            // This is good, the menu item should not be there.
+        }
+    }
+
+    /**
+     * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on.
+     *
+     * @Given /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" in the "(?P<activity_name_string>(?:[^"]|\\")*)" activity in site main menu block$/
+     * @param string $element
+     * @param string $selectortype
+     * @param string $activityname
+     */
+    public function i_click_on_in_the_activity_in_site_main_menu_block($element, $selectortype, $activityname) {
+        $element = $this->get_site_menu_activity_element($element, $selectortype, $activityname);
+        $element->click();
+    }
+
+    /**
+     * Clicks on the specified element inside the activity container.
+     *
+     * @throws ElementNotFoundException
+     * @param string $element
+     * @param string $selectortype
+     * @param string $activityname
+     * @return NodeElement
+     */
+    protected function get_site_menu_activity_element($element, $selectortype, $activityname) {
+        $activitynode = $this->get_site_menu_activity_node($activityname);
+
+        // Transforming to Behat selector/locator.
+        list($selector, $locator) = $this->transform_selector($selectortype, $element);
+        $exception = new ElementNotFoundException($this->getSession(), '"' . $element . '" "' .
+            $selectortype . '" in "' . $activityname . '" ');
+
+        return $this->find($selector, $locator, $exception, $activitynode);
+    }
+
+    /**
+     * Checks that the specified activity is hidden.
+     *
+     * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" activity in site main menu block should be hidden$/
+     * @param string $activityname
+     */
+    public function activity_in_site_main_menu_block_should_be_hidden($activityname) {
+        $this->get_site_menu_activity_element("a.dimmed", "css_element", $activityname);
+    }
+
+    /**
+     * Checks that the specified activity is hidden.
+     *
+     * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" activity in site main menu block should be available but hidden from course page$/
+     * @param string $activityname
+     */
+    public function activity_in_site_main_menu_block_should_be_available_but_hidden_from_course_page($activityname) {
+        $this->get_site_menu_activity_element("a.stealth", "css_element", $activityname);
+    }
+
+    /**
+     * Opens an activity actions menu if it is not already opened.
+     *
+     * @Given /^I open "(?P<activity_name_string>(?:[^"]|\\")*)" actions menu in site main menu block$/
+     * @throws DriverException The step is not available when Javascript is disabled
+     * @param string $activityname
+     */
+    public function i_open_actions_menu_in_site_main_menu_block($activityname) {
+        $activityname = behat_context_helper::escape($activityname);
+        $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]//li[contains(., $activityname)]";
+        $this->execute('behat_action_menu::i_open_the_action_menu_in', [$xpath, 'xpath_element']);
+    }
+}
index 4799495..6cb07df 100644 (file)
@@ -1,4 +1,4 @@
-@block @block_main_menu
+@block @block_site_main_menu
 Feature: Edit activities in main menu block
   In order to use main menu block
   As an admin
@@ -12,7 +12,7 @@ Feature: Edit activities in main menu block
     And I add the "Main menu" block
     When I add a "Forum" to section "0" and I fill the form with:
       | Forum name | My forum name |
-    And I click on "Edit title" "link" in the "//*[contains(@class,'block_site_main_menu')]//li[contains(.,'My forum name')]" "xpath_element"
+    And I click on "Edit title" "link" in the "My forum name" activity in site main menu block
     And I set the field "New name for activity My forum name" to "New forum name"
     And I press key "13" in the field "New name for activity My forum name"
     Then I should not see "My forum name"
@@ -20,3 +20,49 @@ Feature: Edit activities in main menu block
     And I follow "New forum name"
     And I should not see "My forum name"
     And I should see "New forum name"
+
+  @javascript
+  Scenario: Activities in main menu block can be made available but not visible on a course page
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | allowstealth | 1 |
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I add the "Main menu" block
+    When I add a "Forum" to section "0" and I fill the form with:
+      | Forum name | Visible forum |
+    When I add a "Forum" to section "0" and I fill the form with:
+      | Forum name | My forum name |
+    And "My forum name" activity in site main menu block should have "Hide" editing icon
+    And "My forum name" activity in site main menu block should not have "Show" editing icon
+    And "My forum name" activity in site main menu block should not have "Make available" editing icon
+    And "My forum name" activity in site main menu block should not have "Make unavailable" editing icon
+    And I open "My forum name" actions menu in site main menu block
+    And I click on "Hide" "link" in the "My forum name" activity in site main menu block
+    And "My forum name" activity in site main menu block should be hidden
+    And "My forum name" activity in site main menu block should not have "Hide" editing icon
+    And "My forum name" activity in site main menu block should have "Show" editing icon
+    And "My forum name" activity in site main menu block should have "Make available" editing icon
+    And "My forum name" activity in site main menu block should not have "Make unavailable" editing icon
+    And I open "My forum name" actions menu in site main menu block
+    And I click on "Make available" "link" in the "My forum name" activity in site main menu block
+    And "My forum name" activity in site main menu block should be available but hidden from course page
+    And "My forum name" activity in site main menu block should not have "Hide" editing icon
+    And "My forum name" activity in site main menu block should have "Show" editing icon
+    And "My forum name" activity in site main menu block should not have "Make available" editing icon
+    And "My forum name" activity in site main menu block should have "Make unavailable" editing icon
+    # Make sure that "Visible" dropdown in the edit menu has three options.
+    And I open "My forum name" actions menu in site main menu block
+    And I click on "Edit settings" "link" in the "My forum name" activity in site main menu block
+    And I expand all fieldsets
+    And the "Visible" select box should contain "Show"
+    And the "Visible" select box should contain "Hidden from students"
+    And the "Visible" select box should not contain "Hide"
+    And the field "Visible" matches value "Available but not displayed on course page"
+    And I press "Save and return to course"
+    And "My forum name" activity in site main menu block should be available but hidden from course page
+    And I navigate to "Turn editing off" node in "Front page settings"
+    And "My forum name" activity in site main menu block should be available but hidden from course page
+    And I log out
+    And I should not see "My forum name" in the "Main menu" "block"
+    And I should see "Visible forum" in the "Main menu" "block"
index 2553413..63e9cbb 100644 (file)
@@ -61,12 +61,12 @@ class block_social_activities extends block_list {
             if (!empty($modinfo->sections[0])) {
                 foreach($modinfo->sections[0] as $cmid) {
                     $cm = $modinfo->cms[$cmid];
-                    if (!$cm->uservisible) {
+                    if (!$cm->uservisible || !$cm->is_visible_on_course_page()) {
                         continue;
                     }
 
                     if (!$cm->url) {
-                        $content = $cm->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
+                        $content = $courserenderer->course_section_cm_text($cm);
                         $this->content->items[] = $content;
                         $this->content->icons[] = '';
                     } else {
@@ -98,7 +98,7 @@ class block_social_activities extends block_list {
         if (!empty($modinfo->sections[0])) {
             foreach ($modinfo->sections[0] as $modnumber) {
                 $mod = $modinfo->cms[$modnumber];
-                if (!$mod->uservisible) {
+                if (!$mod->uservisible || !$mod->is_visible_on_course_page()) {
                     continue;
                 }
                 if (!$ismoving) {
@@ -128,7 +128,7 @@ class block_social_activities extends block_list {
                         $this->content->icons[] = '';
                     }
                     if (!$mod->url) {
-                        $content = $mod->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
+                        $content = $courserenderer->course_section_cm_text($mod);
                         $this->content->items[] = $content . $editbuttons;
                         $this->content->icons[] = '';
                     } else {
diff --git a/blocks/social_activities/tests/behat/behat_block_social_activities.php b/blocks/social_activities/tests/behat/behat_block_social_activities.php
new file mode 100644 (file)
index 0000000..2fe5526
--- /dev/null
@@ -0,0 +1,157 @@
+<?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/>.
+
+/**
+ * Behat steps definitions for block social activities
+ *
+ * @package    block_social_activities
+ * @category   test
+ * @copyright  2016 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
+
+use Behat\Mink\Exception\ExpectationException as ExpectationException,
+    Behat\Mink\Exception\DriverException as DriverException,
+    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+
+/**
+ * Behat steps definitions for block social activities
+ *
+ * @package    block_social_activities
+ * @category   test
+ * @copyright  2016 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_block_social_activities extends behat_base {
+
+    /**
+     * Returns the DOM node of the activity in the social activities block
+     *
+     * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param string $activityname The activity name
+     * @return NodeElement
+     */
+    protected function get_social_block_activity_node($activityname) {
+        $activityname = behat_context_helper::escape($activityname);
+        $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_social_activities ')]//li[contains(., $activityname)]";
+
+        return $this->find('xpath', $xpath);
+    }
+
+    /**
+     * Checks that the specified activity's action menu contains an item.
+     *
+     * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" activity in social activities block should have "(?P<icon_name_string>(?:[^"]|\\")*)" editing icon$/
+     * @param string $activityname
+     * @param string $iconname
+     */
+    public function activity_in_social_activities_block_should_have_editing_icon($activityname, $iconname) {
+        $activitynode = $this->get_social_block_activity_node($activityname);
+
+        $notfoundexception = new ExpectationException('"' . $activityname . '" doesn\'t have a "' .
+            $iconname . '" editing icon', $this->getSession());
+        $this->find('named_partial', array('link', $iconname), $notfoundexception, $activitynode);
+    }
+
+    /**
+     * Checks that the specified activity's action menu contains an item.
+     *
+     * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" activity in social activities block should not have "(?P<icon_name_string>(?:[^"]|\\")*)" editing icon$/
+     * @param string $activityname
+     * @param string $iconname
+     */
+    public function activity_in_social_activities_block_should_not_have_editing_icon($activityname, $iconname) {
+        $activitynode = $this->get_social_block_activity_node($activityname);
+
+        try {
+            $this->find('named_partial', array('link', $iconname), false, $activitynode);
+            throw new ExpectationException('"' . $activityname . '" has a "' . $iconname .
+                '" editing icon when it should not', $this->getSession());
+        } catch (ElementNotFoundException $e) {
+            // This is good, the menu item should not be there.
+        }
+    }
+
+    /**
+     * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on.
+     *
+     * @Given /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" in the "(?P<activity_name_string>(?:[^"]|\\")*)" activity in social activities block$/
+     * @param string $element
+     * @param string $selectortype
+     * @param string $activityname
+     */
+    public function i_click_on_in_the_activity_in_social_activities_block($element, $selectortype, $activityname) {
+        $element = $this->get_social_block_activity_element($element, $selectortype, $activityname);
+        $element->click();
+    }
+
+    /**
+     * Clicks on the specified element inside the activity container.
+     *
+     * @throws ElementNotFoundException
+     * @param string $element
+     * @param string $selectortype
+     * @param string $activityname
+     * @return NodeElement
+     */
+    protected function get_social_block_activity_element($element, $selectortype, $activityname) {
+        $activitynode = $this->get_social_block_activity_node($activityname);
+
+        // Transforming to Behat selector/locator.
+        list($selector, $locator) = $this->transform_selector($selectortype, $element);
+        $exception = new ElementNotFoundException($this->getSession(), '"' . $element . '" "' .
+            $selectortype . '" in "' . $activityname . '" ');
+
+        return $this->find($selector, $locator, $exception, $activitynode);
+    }
+
+    /**
+     * Checks that the specified activity is hidden.
+     *
+     * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" activity in social activities block should be hidden$/
+     * @param string $activityname
+     */
+    public function activity_in_social_activities_block_should_be_hidden($activityname) {
+        $this->get_social_block_activity_element("a.dimmed", "css_element", $activityname);
+    }
+
+    /**
+     * Checks that the specified activity is hidden.
+     *
+     * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" activity in social activities block should be available but hidden from course page$/
+     * @param string $activityname
+     */
+    public function activity_in_social_activities_block_should_be_available_but_hidden_from_course_page($activityname) {
+        $this->get_social_block_activity_element("a.stealth", "css_element", $activityname);
+    }
+
+    /**
+     * Opens an activity actions menu if it is not already opened.
+     *
+     * @Given /^I open "(?P<activity_name_string>(?:[^"]|\\")*)" actions menu in social activities block$/
+     * @throws DriverException The step is not available when Javascript is disabled
+     * @param string $activityname
+     */
+    public function i_open_actions_menu_in_social_activities_block($activityname) {
+        $activityname = behat_context_helper::escape($activityname);
+        $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_social_activities ')]//li[contains(., $activityname)]";
+        $this->execute('behat_action_menu::i_open_the_action_menu_in', [$xpath, 'xpath_element']);
+    }
+}
index 07b58e3..a29b066 100644 (file)
@@ -1,27 +1,31 @@
-@block @block_social_activities
+@block @block_social_activities @format_social
 Feature: Edit activities in social activities block
   In order to use social activities block
   As a teacher
   I need to add and edit activities there
 
-  @javascript
-  Scenario: Edit name of acitivity in-place in social activities block
+  Background:
     Given the following "courses" exist:
       | fullname | shortname | format |
       | Course 1 | C1        | social |
     And the following "users" exist:
       | username | firstname | lastname |
       | user1 | User | One |
+      | student1 | Student | One |
     And the following "course enrolments" exist:
       | user | course | role |
       | user1 | C1 | editingteacher |
+      | student1 | C1 | student |
+
+  @javascript
+  Scenario: Edit name of acitivity in-place in social activities block
     Given I log in as "user1"
     And I follow "Course 1"
     And I turn editing mode on
     And I set the field "Add an activity to section 'section 0'" to "Forum"
     And I set the field "Forum name" to "My forum name"
     And I press "Save and return to course"
-    And I click on "Edit title" "link" in the "//*[contains(@class,'block_social_activities')]//li[contains(.,'My forum name')]" "xpath_element"
+    And I click on "Edit title" "link" in the "My forum name" activity in social activities block
     And I set the field "New name for activity My forum name" to "New forum name"
     And I press key "13" in the field "New name for activity My forum name"
     Then I should not see "My forum name" in the "Social activities" "block"
@@ -29,3 +33,56 @@ Feature: Edit activities in social activities block
     And I follow "New forum name"
     And I should not see "My forum name"
     And I should see "New forum name"
+
+  @javascript
+  Scenario: Activities in social activities block can be made available but not visible on a course page
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | allowstealth | 1 |
+    And I log out
+    And I log in as "user1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add the "Recent activity" block
+    And I set the field "Add an activity to section 'section 0'" to "Forum"
+    And I set the field "Forum name" to "My forum name"
+    And I press "Save and return to course"
+    And "My forum name" activity in social activities block should have "Hide" editing icon
+    And "My forum name" activity in social activities block should not have "Show" editing icon
+    And "My forum name" activity in social activities block should not have "Make available" editing icon
+    And "My forum name" activity in social activities block should not have "Make unavailable" editing icon
+    And I wait until the page is ready
+    And I open "My forum name" actions menu in social activities block
+    And I click on "Hide" "link" in the "My forum name" activity in social activities block
+    And "My forum name" activity in social activities block should be hidden
+    And "My forum name" activity in social activities block should not have "Hide" editing icon
+    And "My forum name" activity in social activities block should have "Show" editing icon
+    And "My forum name" activity in social activities block should have "Make available" editing icon
+    And "My forum name" activity in social activities block should not have "Make unavailable" editing icon
+    And I open "My forum name" actions menu in social activities block
+    And I click on "Make available" "link" in the "My forum name" activity in social activities block
+    And "My forum name" activity in social activities block should be available but hidden from course page
+    And "My forum name" activity in social activities block should not have "Hide" editing icon
+    And "My forum name" activity in social activities block should have "Show" editing icon
+    And "My forum name" activity in social activities block should not have "Make available" editing icon
+    And "My forum name" activity in social activities block should have "Make unavailable" editing icon
+    # Make sure that "Visible" dropdown in the edit menu has three options.
+    And I open "My forum name" actions menu in social activities block
+    And I click on "Edit settings" "link" in the "My forum name" activity in social activities block
+    And I expand all fieldsets
+    And the "Visible" select box should contain "Show"
+    And the "Visible" select box should contain "Hidden from students"
+    And the "Visible" select box should not contain "Hide"
+    And the field "Visible" matches value "Available but not displayed on course page"
+    And I press "Save and return to course"
+    And "My forum name" activity in social activities block should be available but hidden from course page
+    And I turn editing mode off
+    And "My forum name" activity in social activities block should be available but hidden from course page
+    And I log out
+    # Student will not see the module on the course page but can access it from other reports and blocks:
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I should not see "My forum name" in the "Social activities" "block"
+    And I click on "My forum name" "link" in the "Recent activity" "block"
+    And I should see "My forum name" in the ".breadcrumb" "css_element"
+    And I log out
index bdcc7bc..d93a357 100644 (file)
@@ -192,7 +192,17 @@ M.core_comment = {
                         val = val.replace('___name___', list[i].fullname);
                     }
                     if (list[i]['delete']||newcmt) {
-                        list[i].content = '<div class="comment-delete"><a href="#" id ="comment-delete-'+this.client_id+'-'+list[i].id+'" title="'+M.util.get_string('deletecomment', 'moodle')+'"><img alt="" src="'+M.util.image_url('t/delete', 'core')+'" /></a></div>' + list[i].content;
+                        var tokens = {
+                            user: list[i].fullname,
+                            time: list[i].time
+                        };
+                        var deleteStr = Y.Escape.html(M.util.get_string('deletecommentbyon', 'moodle', tokens));
+                        list[i].content = '<div class="comment-delete">' +
+                            '<a href="#" role="button" id ="comment-delete-' + this.client_id + '-' + list[i].id + '"' +
+                            '   title="' + deleteStr + '">' +
+                            '<img alt="' + deleteStr + '" src="' + M.util.image_url('t/delete', 'core') + '" />' +
+                            '</a>' +
+                            '</div>' + list[i].content;
                     }
                     val = val.replace('___time___', list[i].time);
                     val = val.replace('___picture___', list[i].avatar);
index 8bc0197..1a770f8 100644 (file)
@@ -263,7 +263,7 @@ class comment {
                 'comments',
                 'commentscount',
                 'commentsrequirelogin',
-                'deletecomment',
+                'deletecommentbyon',
             ),
             'moodle'
         );
index 5d19955..3f94122 100644 (file)
@@ -42,7 +42,8 @@ Feature: Restrict activity availability through date conditions
     And I am on site homepage
     And I follow "Course 1"
     Then I should see "Available from 31 December 2037"
-    And "Test assignment 1" activity should be hidden
+    And "Test assignment 1" activity should be dimmed
+    And "Test assignment 1" "link" should not exist
     And I log out
 
   @javascript
index 040bf16..6c970e8 100644 (file)
@@ -44,7 +44,8 @@ Feature: Restrict activity availability through grade conditions
     And I am on site homepage
     And I follow "Course 1"
     Then I should see "Not available unless: You achieve a required score in Grade assignment"
-    And "Test page name" activity should be hidden
+    And "Test page name" activity should be dimmed
+    And "Test page name" "link" should not exist
     And I follow "Grade assignment"
     And I press "Add submission"
     And I set the following fields to these values:
index 9c795b6..92dfb41 100644 (file)
@@ -7,6 +7,7 @@
     "require-dev": {
         "phpunit/phpunit": "5.5.*",
         "phpunit/dbUnit": "1.4.*",
-        "moodlehq/behat-extension": "3.33.1"
+        "moodlehq/behat-extension": "3.33.1",
+        "mikey179/vfsStream": "^1.6"
     }
 }
index c757473..1fca2b9 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "f4cfcd74744fbbced495458ea82fd314",
+    "content-hash": "751fc6623b264b33856167019b391053",
     "packages": [],
     "packages-dev": [
         {
             ],
             "time": "2015-06-15T20:19:33+00:00"
         },
+        {
+            "name": "mikey179/vfsStream",
+            "version": "v1.6.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/mikey179/vfsStream.git",
+                "reference": "0247f57b2245e8ad2e689d7cee754b45fbabd592"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/mikey179/vfsStream/zipball/0247f57b2245e8ad2e689d7cee754b45fbabd592",
+                "reference": "0247f57b2245e8ad2e689d7cee754b45fbabd592",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.6.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "org\\bovigo\\vfs\\": "src/main/php"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Frank Kleine",
+                    "homepage": "http://frankkleine.de/",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Virtual file system to mock the real file system in unit tests.",
+            "homepage": "http://vfs.bovigo.org/",
+            "time": "2016-07-18T14:02:57+00:00"
+        },
         {
             "name": "moodlehq/behat-extension",
             "version": "v3.33.1",
         },
         {
             "name": "myclabs/deep-copy",
-            "version": "1.5.5",
+            "version": "1.6.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/myclabs/DeepCopy.git",
-                "reference": "399c1f9781e222f6eb6cc238796f5200d1b7f108"
+                "reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/399c1f9781e222f6eb6cc238796f5200d1b7f108",
-                "reference": "399c1f9781e222f6eb6cc238796f5200d1b7f108",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/5a5a9fc8025a08d8919be87d6884d5a92520cefe",
+                "reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe",
                 "shasum": ""
             },
             "require": {
                 "object",
                 "object graph"
             ],
-            "time": "2016-10-31T17:19:45+00:00"
+            "time": "2017-01-26T22:05:40+00:00"
         },
         {
             "name": "phpdocumentor/reflection-common",
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "4.0.4",
+            "version": "4.0.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "c14196e64a78570034afd0b7a9f3757ba71c2a0a"
+                "reference": "c19cfc7cbb0e9338d8c469c7eedecc2a428b0971"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c14196e64a78570034afd0b7a9f3757ba71c2a0a",
-                "reference": "c14196e64a78570034afd0b7a9f3757ba71c2a0a",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c19cfc7cbb0e9338d8c469c7eedecc2a428b0971",
+                "reference": "c19cfc7cbb0e9338d8c469c7eedecc2a428b0971",
                 "shasum": ""
             },
             "require": {
                 "testing",
                 "xunit"
             ],
-            "time": "2016-12-20T15:22:42+00:00"
+            "time": "2017-01-20T15:06:43+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
         },
         {
             "name": "sebastian/comparator",
-            "version": "1.2.2",
+            "version": "1.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/comparator.git",
-                "reference": "6a1ed12e8b2409076ab22e3897126211ff8b1f7f"
+                "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a1ed12e8b2409076ab22e3897126211ff8b1f7f",
-                "reference": "6a1ed12e8b2409076ab22e3897126211ff8b1f7f",
+                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be",
+                "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be",
                 "shasum": ""
             },
             "require": {
                 "compare",
                 "equality"
             ],
-            "time": "2016-11-19T09:18:40+00:00"
+            "time": "2017-01-29T09:50:25+00:00"
         },
         {
             "name": "sebastian/diff",
diff --git a/course/amd/build/actions.min.js b/course/amd/build/actions.min.js
new file mode 100644 (file)
index 0000000..797a71e
Binary files /dev/null and b/course/amd/build/actions.min.js differ
diff --git a/course/amd/src/actions.js b/course/amd/src/actions.js
new file mode 100644 (file)
index 0000000..5c4a72c
--- /dev/null
@@ -0,0 +1,594 @@
+// 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/>.
+
+/**
+ * Various actions on modules and sections in the editing mode - hiding, duplicating, deleting, etc.
+ *
+ * @module     core_course/actions
+ * @package    core
+ * @copyright  2016 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      3.3
+ */
+define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/url', 'core/yui'],
+    function($, ajax, templates, notification, str, url, Y) {
+        var CSS = {
+            EDITINPROGRESS: 'editinprogress',
+            SECTIONDRAGGABLE: 'sectiondraggable',
+            EDITINGMOVE: 'editing_move'
+        };
+        var SELECTOR = {
+            ACTIVITYLI: 'li.activity',
+            ACTIONAREA: '.actions',
+            ACTIVITYACTION: 'a.cm-edit-action',
+            MENU: '.moodle-actionmenu[data-enhance=moodle-core-actionmenu]',
+            TOGGLE: '.toggle-display,.dropdown-toggle',
+            SECTIONLI: 'li.section',
+            SECTIONACTIONMENU: '.section_action_menu'
+        };
+
+        Y.use('moodle-course-coursebase', function() {
+            var courseformatselector = M.course.format.get_section_selector();
+            if (courseformatselector) {
+                SELECTOR.SECTIONLI = courseformatselector;
+            }
+        });
+
+        /**
+         * Wrapper for Y.Moodle.core_course.util.cm.getId
+         *
+         * @param {JQuery} element
+         * @returns {Integer}
+         */
+        var getModuleId = function(element) {
+            var id;
+            Y.use('moodle-course-util', function(Y) {
+                id = Y.Moodle.core_course.util.cm.getId(Y.Node(element.get(0)));
+            });
+            return id;
+        };
+
+        /**
+         * Wrapper for Y.Moodle.core_course.util.cm.getName
+         *
+         * @param {JQuery} element
+         * @returns {String}
+         */
+        var getModuleName = function(element) {
+            var name;
+            Y.use('moodle-course-util', function(Y) {
+                name = Y.Moodle.core_course.util.cm.getName(Y.Node(element.get(0)));
+            });
+            return name;
+        };
+
+        /**
+         * Wrapper for M.util.add_spinner for an activity
+         *
+         * @param {JQuery} activity
+         * @returns {Node}
+         */
+        var addActivitySpinner = function(activity) {
+            activity.addClass(CSS.EDITINPROGRESS);
+            var actionarea = activity.find(SELECTOR.ACTIONAREA).get(0);
+            if (actionarea) {
+                var spinner = M.util.add_spinner(Y, Y.Node(actionarea));
+                spinner.show();
+                return spinner;
+            }
+            return null;
+        };
+
+        /**
+         * Wrapper for M.util.add_spinner for a section
+         *
+         * @param {JQuery} sectionelement
+         * @returns {Node}
+         */
+        var addSectionSpinner = function(sectionelement) {
+            sectionelement.addClass(CSS.EDITINPROGRESS);
+            var actionarea = sectionelement.find(SELECTOR.SECTIONACTIONMENU).get(0);
+            if (actionarea) {
+                var spinner = M.util.add_spinner(Y, Y.Node(actionarea));
+                spinner.show();
+                return spinner;
+            }
+            return null;
+        };
+
+        /**
+         * Wrapper for M.util.add_lightbox
+         *
+         * @param {JQuery} sectionelement
+         * @returns {Node}
+         */
+        var addSectionLightbox = function(sectionelement) {
+            var lightbox = M.util.add_lightbox(Y, Y.Node(sectionelement.get(0)));
+            lightbox.show();
+            return lightbox;
+        };
+
+        /**
+         * Removes the spinner element
+         *
+         * @param {JQuery} element
+         * @param {Node} spinner
+         * @param {Number} delay
+         */
+        var removeSpinner = function(element, spinner, delay) {
+            window.setTimeout(function() {
+                element.removeClass(CSS.EDITINPROGRESS);
+                if (spinner) {
+                    spinner.hide();
+                }
+            }, delay);
+        };
+
+        /**
+         * Removes the lightbox element
+         *
+         * @param {Node} lightbox lighbox YUI element returned by addSectionLightbox
+         * @param {Number} delay
+         */
+        var removeLightbox = function(lightbox, delay) {
+            if (lightbox) {
+                window.setTimeout(function() {
+                    lightbox.hide();
+                }, delay);
+            }
+        };
+
+        /**
+         * Initialise action menu for the element (section or module)
+         *
+         * @param {String} elementid CSS id attribute of the element
+         * @param {Boolean} openmenu whether to open menu - this can be used when re-initiating menu after indent action was pressed
+         */
+        var initActionMenu = function(elementid, openmenu) {
+            // Initialise action menu in the new activity.
+            Y.use('moodle-course-coursebase', function() {
+                M.course.coursebase.invoke_function('setup_for_resource', '#' + elementid);
+            });
+            if (M.core.actionmenu && M.core.actionmenu.newDOMNode) {
+                M.core.actionmenu.newDOMNode(Y.one('#' + elementid));
+            }
+            // Open action menu if the original element had data-keepopen.
+            if (openmenu) {
+                // We must use YUI click simulate here so the toggle works in Clean theme. This toggle is not
+                // needed in Boost because we use standard bootstrapbase action menu.
+                var toggle = Y.one('#' + elementid + ' ' + SELECTOR.MENU).one(SELECTOR.TOGGLE);
+                if (toggle && toggle.simulate) {
+                    toggle.simulate('click');
+                }
+            }
+        };
+
+        /**
+         * Returns focus to the element that was clicked or "Edit" link if element is no longer visible.
+         *
+         * @param {String} elementId CSS id attribute of the element
+         * @param {String} action data-action property of the element that was clicked
+         */
+        var focusActionItem = function(elementId, action) {
+            var mainelement = $('#' + elementId);
+            var selector = '[data-action=' + action + ']';
+            if (action === 'groupsseparate' || action === 'groupsvisible' || action === 'groupsnone') {
+                // New element will have different data-action.
+                selector = '[data-action=groupsseparate],[data-action=groupsvisible],[data-action=groupsnone]';
+            }
+            if (mainelement.find(selector).is(':visible')) {
+                mainelement.find(selector).focus();
+            } else {
+                // Element not visible, focus the "Edit" link.
+                mainelement.find(SELECTOR.MENU).find(SELECTOR.TOGGLE).focus();
+            }
+        };
+
+        /**
+         * Find next <a> after the element
+         *
+         * @param {JQuery} mainElement element that is about to be deleted
+         * @returns {JQuery}
+         */
+        var findNextFocusable = function(mainElement) {
+            var tabables = $("a:visible");
+            var isInside = false, foundElement = null;
+            tabables.each(function() {
+                if ($.contains(mainElement[0], this)) {
+                    isInside = true;
+                } else if (isInside) {
+                    foundElement = this;
+                    return false; // Returning false in .each() is equivalent to "break;" inside the loop in php.
+                }
+            });
+            return foundElement;
+        };
+
+        /**
+         * Performs an action on a module (moving, deleting, duplicating, hiding, etc.)
+         *
+         * @param {JQuery} moduleElement activity element we perform action on
+         * @param {Number} cmid
+         * @param {JQuery} target the element (menu item) that was clicked
+         */
+        var editModule = function(moduleElement, cmid, target) {
+            var keepopen = target.attr('data-keepopen'),
+                    action = target.attr('data-action');
+            var spinner = addActivitySpinner(moduleElement);
+            var promises = ajax.call([{
+                methodname: 'core_course_edit_module',
+                args: {id: cmid,
+                    action: action,
+                    sectionreturn: target.attr('data-sectionreturn') ? target.attr('data-sectionreturn') : 0
+                }
+            }], true);
+
+            var lightbox;
+            if (action === 'duplicate') {
+                lightbox = addSectionLightbox(target.closest(SELECTOR.SECTIONLI));
+            }
+            $.when.apply($, promises)
+                .done(function(data) {
+                    var elementToFocus = findNextFocusable(moduleElement);
+                    moduleElement.replaceWith(data);
+                    // Initialise action menu for activity(ies) added as a result of this.
+                    $('<div>' + data + '</div>').find(SELECTOR.ACTIVITYLI).each(function(index) {
+                        initActionMenu($(this).attr('id'), keepopen);
+                        if (index === 0) {
+                            focusActionItem($(this).attr('id'), action);
+                            elementToFocus = null;
+                        }
+                    });
+                    // In case of activity deletion focus the next focusable element.
+                    if (elementToFocus) {
+                        elementToFocus.focus();
+                    }
+                    // Remove spinner and lightbox with a delay.
+                    removeSpinner(moduleElement, spinner, 400);
+                    removeLightbox(lightbox, 400);
+                    // Trigger event that can be observed by course formats.
+                    moduleElement.trigger($.Event('coursemoduleedited', {ajaxreturn: data, action: action}));
+                }).fail(function(ex) {
+                    // Remove spinner and lightbox.
+                    removeSpinner(moduleElement, spinner);
+                    removeLightbox(lightbox);
+                    // Trigger event that can be observed by course formats.
+                    var e = $.Event('coursemoduleeditfailed', {exception: ex, action: action});
+                    moduleElement.trigger(e);
+                    if (!e.isDefaultPrevented()) {
+                        notification.exception(ex);
+                    }
+                });
+        };
+
+        /**
+         * Requests html for the module via WS core_course_get_module and updates the module on the course page
+         *
+         * Used after d&d of the module to another section
+         *
+         * @param {JQuery} activityElement
+         * @param {Number} cmid
+         * @param {Number} sectionreturn
+         */
+        var refreshModule = function(activityElement, cmid, sectionreturn) {
+            var spinner = addActivitySpinner(activityElement);
+            var promises = ajax.call([{
+                methodname: 'core_course_get_module',
+                args: {id: cmid, sectionreturn: sectionreturn}
+            }], true);
+
+            $.when.apply($, promises)
+                .done(function(data) {
+                    removeSpinner(activityElement, spinner, 400);
+                    replaceActivityHtmlWith(data);
+                }).fail(function() {
+                    removeSpinner(activityElement, spinner);
+                });
+        };
+
+        /**
+         * Displays the delete confirmation to delete a module
+         *
+         * @param {JQuery} mainelement activity element we perform action on
+         * @param {function} onconfirm function to execute on confirm
+         */
+        var confirmDeleteModule = function(mainelement, onconfirm) {
+            var modtypename = mainelement.attr('class').match(/modtype_([^\s]*)/)[1];
+            var modulename = getModuleName(mainelement);
+
+            str.get_string('pluginname', modtypename).done(function(pluginname) {
+                var plugindata = {
+                    type: pluginname,
+                    name: modulename
+                };
+                str.get_strings([
+                    {key: 'confirm'},
+                    {key: modulename === null ? 'deletechecktype' : 'deletechecktypename', param: plugindata},
+                    {key: 'yes'},
+                    {key: 'no'}
+                ]).done(function(s) {
+                        notification.confirm(s[0], s[1], s[2], s[3], onconfirm);
+                    }
+                );
+            });
+        };
+
+        /**
+         * Displays the delete confirmation to delete a section
+         *
+         * @param {String} message confirmation message
+         * @param {function} onconfirm function to execute on confirm
+         */
+        var confirmEditSection = function(message, onconfirm) {
+            str.get_strings([
+                {key: 'confirm'}, // TODO link text
+                {key: 'yes'},
+                {key: 'no'}
+            ]).done(function(s) {
+                    notification.confirm(s[0], message, s[1], s[2], onconfirm);
+                }
+            );
+        };
+
+        /**
+         * Replaces an action menu item with another one (for example Show->Hide, Set marker->Remove marker)
+         *
+         * @param {JQuery} actionitem
+         * @param {String} image new image name ("i/show", "i/hide", etc.)
+         * @param {String} stringname new string for the action menu item
+         * @param {String} stringcomponent
+         * @param {String} titlestr string for "title" attribute (if different from stringname)
+         * @param {String} titlecomponent
+         * @param {String} newaction new value for data-action attribute of the link
+         */
+        var replaceActionItem = function(actionitem, image, stringname,
+                                           stringcomponent, titlestr, titlecomponent, newaction) {
+            actionitem.find('img').attr('src', url.imageUrl(image, 'core'));
+            str.get_string(stringname, stringcomponent).done(function(newstring) {
+                actionitem.find('span.menu-action-text').html(newstring);
+                actionitem.attr('title', newstring);
+            });
+            if (titlestr) {
+                str.get_string(titlestr, titlecomponent).done(function(newtitle) {
+                    actionitem.attr('title', newtitle);
+                });
+            }
+            actionitem.attr('data-action', newaction);
+        };
+
+        /**
+         * Default post-processing for section AJAX edit actions.
+         *
+         * This can be overridden in course formats by listening to event coursesectionedited:
+         *
+         * $('body').on('coursesectionedited', 'li.section', function(e) {
+         *     var action = e.action,
+         *         sectionElement = $(e.target),
+         *         data = e.ajaxreturn;
+         *     // ... Do some processing here.
+         *     e.preventDefault(); // Prevent default handler.
+         * });
+         *
+         * @param {JQuery} sectionElement
+         * @param {JQuery} actionItem
+         * @param {Object} data
+         * @param {String} courseformat
+         */
+        var defaultEditSectionHandler = function(sectionElement, actionItem, data, courseformat) {
+            var action = actionItem.attr('data-action');
+            if (action === 'hide' || action === 'show') {
+                if (action === 'hide') {
+                    sectionElement.addClass('hidden');
+                    replaceActionItem(actionItem, 'i/show',
+                        'showfromothers', 'format_' + courseformat, null, null, 'show');
+                } else {
+                    sectionElement.removeClass('hidden');
+                    replaceActionItem(actionItem, 'i/hide',
+                        'hidefromothers', 'format_' + courseformat, null, null, 'hide');
+                }
+                // Replace the modules with new html (that indicates that they are now hidden or not hidden).
+                if (data.modules !== undefined) {
+                    for (var i in data.modules) {
+                        replaceActivityHtmlWith(data.modules[i]);
+                    }
+                }
+                // Replace the section availability information.
+                if (data.section_availability !== undefined) {
+                    sectionElement.find('.section_availability').first().replaceWith(data.section_availability);
+                }
+            } else if (action === 'setmarker') {
+                var oldmarker = $(SELECTOR.SECTIONLI + '.current'),
+                    oldActionItem = oldmarker.find(SELECTOR.SECTIONACTIONMENU + ' ' + 'a[data-action=removemarker]');
+                oldmarker.removeClass('current');
+                replaceActionItem(oldActionItem, 'i/marker',
+                    'highlight', 'core', 'markthistopic', 'core', 'setmarker');
+                sectionElement.addClass('current');
+                replaceActionItem(actionItem, 'i/marked',
+                    'highlightoff', 'core', 'markedthistopic', 'core', 'removemarker');
+            } else if (action === 'removemarker') {
+                sectionElement.removeClass('current');
+                replaceActionItem(actionItem, 'i/marker',
+                    'highlight', 'core', 'markthistopic', 'core', 'setmarker');
+            }
+        };
+
+        /**
+         * Replaces the course module with the new html (used to update module after it was edited or its visibility was changed).
+         *
+         * @param {String} activityHTML
+         */
+        var replaceActivityHtmlWith = function(activityHTML) {
+            $('<div>' + activityHTML + '</div>').find(SELECTOR.ACTIVITYLI).each(function() {
+                // Extract id from the new activity html.
+                var id = $(this).attr('id');
+                // Find the existing element with the same id and replace its contents with new html.
+                $(SELECTOR.ACTIVITYLI + '#' + id).replaceWith(activityHTML);
+                // Initialise action menu.
+                initActionMenu(id, false);
+            });
+        };
+
+        /**
+         * Performs an action on a module (moving, deleting, duplicating, hiding, etc.)
+         *
+         * @param {JQuery} sectionElement section element we perform action on
+         * @param {Nunmber} sectionid
+         * @param {JQuery} target the element (menu item) that was clicked
+         * @param {String} courseformat
+         */
+        var editSection = function(sectionElement, sectionid, target, courseformat) {
+            var action = target.attr('data-action'),
+                sectionreturn = target.attr('data-sectionreturn') ? target.attr('data-sectionreturn') : 0;
+            var spinner = addSectionSpinner(sectionElement);
+            var promises = ajax.call([{
+                methodname: 'core_course_edit_section',
+                args: {id: sectionid, action: action, sectionreturn: sectionreturn}
+            }], true);
+
+            var lightbox = addSectionLightbox(sectionElement);
+            $.when.apply($, promises)
+                .done(function(dataencoded) {
+                    var data = $.parseJSON(dataencoded);
+                    removeSpinner(sectionElement, spinner);
+                    removeLightbox(lightbox);
+                    sectionElement.find(SELECTOR.SECTIONACTIONMENU).find(SELECTOR.TOGGLE).focus();
+                    // Trigger event that can be observed by course formats.
+                    var e = $.Event('coursesectionedited', {ajaxreturn: data, action: action});
+                    sectionElement.trigger(e);
+                    if (!e.isDefaultPrevented()) {
+                        defaultEditSectionHandler(sectionElement, target, data, courseformat);
+                    }
+                }).fail(function(ex) {
+                    // Remove spinner and lightbox.
+                    removeSpinner(sectionElement, spinner);
+                    removeLightbox(lightbox);
+                    // Trigger event that can be observed by course formats.
+                    var e = $.Event('coursesectioneditfailed', {exception: ex, action: action});
+                    sectionElement.trigger(e);
+                    if (!e.isDefaultPrevented()) {
+                        notification.exception(ex);
+                    }
+                });
+        };
+
+        // Register a function to be executed after D&D of an activity.
+        Y.use('moodle-course-coursebase', function() {
+            M.course.coursebase.register_module({
+                // Ignore camelcase eslint rule for the next line because it is an expected name of the callback.
+                // eslint-disable-next-line camelcase
+                set_visibility_resource_ui: function(args) {
+                    var mainelement = $(args.element.getDOMNode());
+                    var cmid = getModuleId(mainelement);
+                    if (cmid) {
+                        var sectionreturn = mainelement.find('.' + CSS.EDITINGMOVE).attr('data-sectionreturn');
+                        refreshModule(mainelement, cmid, sectionreturn);
+                    }
+                }
+            });
+        });
+
+        return /** @alias module:core_course/actions */ {
+
+            /**
+             * Initialises course page
+             *
+             * @method init
+             * @param {String} courseformat name of the current course format (for fetching strings)
+             */
+            initCoursePage: function(courseformat) {
+
+                // Add a handler for course module actions.
+                $('body').on('click keypress', SELECTOR.ACTIVITYLI + ' ' +
+                        SELECTOR.ACTIVITYACTION + '[data-action]', function(e) {
+                    if (e.type === 'keypress' && e.keyCode !== 13) {
+                        return;
+                    }
+                    var actionItem = $(this),
+                        moduleElement = actionItem.closest(SELECTOR.ACTIVITYLI),
+                        action = actionItem.attr('data-action'),
+                        moduleId = getModuleId(moduleElement);
+                    switch (action) {
+                        case 'moveleft':
+                        case 'moveright':
+                        case 'delete':
+                        case 'duplicate':
+                        case 'hide':
+                        case 'stealth':
+                        case 'show':
+                        case 'groupsseparate':
+                        case 'groupsvisible':
+                        case 'groupsnone':
+                            break;
+                        default:
+                            // Nothing to do here!
+                            return;
+                    }
+                    if (!moduleId) {
+                        return;
+                    }
+                    e.preventDefault();
+                    if (action === 'delete') {
+                        // Deleting requires confirmation.
+                        confirmDeleteModule(moduleElement, function() {
+                            editModule(moduleElement, moduleId, actionItem);
+                        });
+                    } else {
+                        editModule(moduleElement, moduleId, actionItem);
+                    }
+                });
+
+                // Add a handler for section show/hide actions.
+                $('body').on('click keypress', SELECTOR.SECTIONLI + ' ' +
+                            SELECTOR.SECTIONACTIONMENU + '[data-sectionid] ' +
+                            'a[data-action]', function(e) {
+                    if (e.type === 'keypress' && e.keyCode !== 13) {
+                        return;
+                    }
+                    var actionItem = $(this),
+                        sectionElement = actionItem.closest(SELECTOR.SECTIONLI),
+                        sectionId = actionItem.closest(SELECTOR.SECTIONACTIONMENU).attr('data-sectionid');
+                    e.preventDefault();
+                    if (actionItem.attr('data-confirm')) {
+                        // Action requires confirmation.
+                        confirmEditSection(actionItem.attr('data-confirm'), function() {
+                            editSection(sectionElement, sectionId, actionItem, courseformat);
+                        });
+                    } else {
+                        editSection(sectionElement, sectionId, actionItem, courseformat);
+                    }
+                });
+            },
+
+            /**
+             * Replaces a section action menu item with another one (for example Show->Hide, Set marker->Remove marker)
+             *
+             * This method can be used by course formats in their listener to the coursesectionedited event
+             *
+             * @param {JQuery} sectionelement
+             * @param {String} selector CSS selector inside the section element, for example "a[data-action=show]"
+             * @param {String} image new image name ("i/show", "i/hide", etc.)
+             * @param {String} stringname new string for the action menu item
+             * @param {String} stringcomponent
+             * @param {String} titlestr string for "title" attribute (if different from stringname)
+             * @param {String} titlecomponent
+             * @param {String} newaction new value for data-action attribute of the link
+             */
+            replaceSectionActionItem: function(sectionelement, selector, image, stringname,
+                                                    stringcomponent, titlestr, titlecomponent, newaction) {
+                var actionitem = sectionelement.find(SELECTOR.SECTIONACTIONMENU + ' ' + selector);
+                replaceActionItem(actionitem, image, stringname, stringcomponent, titlestr, titlecomponent, newaction);
+            }
+        };
+    });
\ No newline at end of file
index d509033..8b9aeb8 100644 (file)
@@ -270,6 +270,7 @@ class core_course_external extends external_api {
                                             context_module::instance($cm->id));
                         //user that can view hidden module should know about the visibility
                         $module['visible'] = $cm->visible;
+                        $module['visibleoncoursepage'] = $cm->visibleoncoursepage;
 
                         // Availability date (also send to user who can see hidden module).
                         if ($CFG->enableavailability && ($canviewhidden || $canupdatecourse)) {
@@ -342,6 +343,8 @@ class core_course_external extends external_api {
                                     'instance' => new external_value(PARAM_INT, 'instance id', VALUE_OPTIONAL),
                                     'description' => new external_value(PARAM_RAW, 'activity description', VALUE_OPTIONAL),
                                     'visible' => new external_value(PARAM_INT, 'is the module visible', VALUE_OPTIONAL),
+                                    'visibleoncoursepage' => new external_value(PARAM_INT, 'is the module visible on course page',
+                                        VALUE_OPTIONAL),
                                     'modicon' => new external_value(PARAM_URL, 'activity icon url'),
                                     'modname' => new external_value(PARAM_PLUGIN, 'activity module type'),
                                     'modplural' => new external_value(PARAM_TEXT, 'activity module plural name'),
@@ -1052,6 +1055,7 @@ class core_course_external extends external_api {
                                             "blocks" (int) Include course blocks (default to 1 that is equal to yes),
                                             "filters" (int) Include course filters  (default to 1 that is equal to yes),
                                             "users" (int) Include users (default to 0 that is equal to no),
+                                            "enrolments" (int) Include enrolment methods (default to 1 - restore only with users),
                                             "role_assignments" (int) Include role assignments  (default to 0 that is equal to no),
                                             "comments" (int) Include user comments  (default to 0 that is equal to no),
                                             "userscompletion" (int) Include user course completion information  (default to 0 that is equal to no),
@@ -1116,6 +1120,7 @@ class core_course_external extends external_api {
             'blocks' => 1,
             'filters' => 1,
             'users' => 0,
+            'enrolments' => backup::ENROL_WITHUSERS,
             'role_assignments' => 0,
             'comments' => 0,
             'userscompletion' => 0,
@@ -1171,7 +1176,9 @@ class core_course_external extends external_api {
         backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id);
 
         foreach ($backupsettings as $name => $value) {
-            $bc->get_plan()->get_setting($name)->set_value($value);
+            if ($setting = $bc->get_plan()->get_setting($name)) {
+                $bc->get_plan()->get_setting($name)->set_value($value);
+            }
         }
 
         $backupid       = $bc->get_backupid();
@@ -2558,6 +2565,7 @@ class core_course_external extends external_api {
                         'score' => new external_value(PARAM_INT, 'Score', VALUE_OPTIONAL),
                         'indent' => new external_value(PARAM_INT, 'Indentation', VALUE_OPTIONAL),
                         'visible' => new external_value(PARAM_INT, 'If visible', VALUE_OPTIONAL),
+                        'visibleoncoursepage' => new external_value(PARAM_INT, 'If visible on course page', VALUE_OPTIONAL),
                         'visibleold' => new external_value(PARAM_INT, 'Visible old', VALUE_OPTIONAL),
                         'completiongradeitemnumber' => new external_value(PARAM_INT, 'Completion grade item', VALUE_OPTIONAL),
                         'completionview' => new external_value(PARAM_INT, 'Completion view setting', VALUE_OPTIONAL),
@@ -3235,4 +3243,226 @@ class core_course_external extends external_api {
     public static function get_updates_since_returns() {
         return self::check_updates_returns();
     }
+
+    /**
+     * Parameters for function edit_module()
+     *
+     * @since Moodle 3.3
+     * @return external_function_parameters
+     */
+    public static function edit_module_parameters() {
+        return new external_function_parameters(
+            array(
+                'action' => new external_value(PARAM_ALPHA,
+                    'action: hide, show, stealth, duplicate, delete, moveleft, moveright, group...', VALUE_REQUIRED),
+                'id' => new external_value(PARAM_INT, 'course module id', VALUE_REQUIRED),
+                'sectionreturn' => new external_value(PARAM_INT, 'section to return to', VALUE_DEFAULT, null),
+            ));
+    }
+
+    /**
+     * Performs one of the edit module actions and return new html for AJAX
+     *
+     * Returns html to replace the current module html with, for example:
+     * - empty string for "delete" action,
+     * - two modules html for "duplicate" action
+     * - updated module html for everything else
+     *
+     * Throws exception if operation is not permitted/possible
+     *
+     * @since Moodle 3.3
+     * @param string $action
+     * @param int $id
+     * @param null|int $sectionreturn
+     * @return string
+     */
+    public static function edit_module($action, $id, $sectionreturn = null) {
+        global $PAGE, $DB;
+        // Validate and normalize parameters.
+        $params = self::validate_parameters(self::edit_module_parameters(),
+            array('action' => $action, 'id' => $id, 'sectionreturn' => $sectionreturn));
+        $action = $params['action'];
+        $id = $params['id'];
+        $sectionreturn = $params['sectionreturn'];
+
+        list($course, $cm) = get_course_and_cm_from_cmid($id);
+        $modcontext = context_module::instance($cm->id);
+        $coursecontext = context_course::instance($course->id);
+        self::validate_context($modcontext);
+        $courserenderer = $PAGE->get_renderer('core', 'course');
+        $completioninfo = new completion_info($course);
+
+        switch($action) {
+            case 'hide':
+            case 'show':
+            case 'stealth':
+                require_capability('moodle/course:activityvisibility', $modcontext);
+                $visible = ($action === 'hide') ? 0 : 1;
+                $visibleoncoursepage = ($action === 'stealth') ? 0 : 1;
+                set_coursemodule_visible($id, $visible, $visibleoncoursepage);
+                \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger();
+                break;
+            case 'duplicate':
+                require_capability('moodle/course:manageactivities', $coursecontext);
+                require_capability('moodle/backup:backuptargetimport', $coursecontext);
+                require_capability('moodle/restore:restoretargetimport', $coursecontext);
+                if (!course_allowed_module($course, $cm->modname)) {
+                    throw new moodle_exception('No permission to create that activity');
+                }
+                if ($newcm = duplicate_module($course, $cm)) {
+                    $cm = get_fast_modinfo($course)->get_cm($id);
+                    $newcm = get_fast_modinfo($course)->get_cm($newcm->id);
+                    return $courserenderer->course_section_cm_list_item($course, $completioninfo, $cm, $sectionreturn) .
+                        $courserenderer->course_section_cm_list_item($course, $completioninfo, $newcm, $sectionreturn);
+                }
+                break;
+            case 'groupsseparate':
+            case 'groupsvisible':
+            case 'groupsnone':
+                require_capability('moodle/course:manageactivities', $modcontext);
+                if ($action === 'groupsseparate') {
+                    $newgroupmode = SEPARATEGROUPS;
+                } else if ($action === 'groupsvisible') {
+                    $newgroupmode = VISIBLEGROUPS;
+                } else {
+                    $newgroupmode = NOGROUPS;
+                }
+                if (set_coursemodule_groupmode($cm->id, $newgroupmode)) {
+                    \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger();
+                }
+                break;
+            case 'moveleft':
+            case 'moveright':
+                require_capability('moodle/course:manageactivities', $modcontext);
+                $indent = $cm->indent + (($action === 'moveright') ? 1 : -1);
+                if ($cm->indent >= 0) {
+                    $DB->update_record('course_modules', array('id' => $cm->id, 'indent' => $indent));
+                    rebuild_course_cache($cm->course);
+                }
+                break;
+            case 'delete':
+                require_capability('moodle/course:manageactivities', $modcontext);
+                course_delete_module($cm->id, true);
+                return '';
+            default:
+                throw new coding_exception('Unrecognised action');
+        }
+
+        $cm = get_fast_modinfo($course)->get_cm($id);
+        return $courserenderer->course_section_cm_list_item($course, $completioninfo, $cm, $sectionreturn);
+    }
+
+    /**
+     * Return structure for edit_module()
+     *
+     * @since Moodle 3.3
+     * @return external_description
+     */
+    public static function edit_module_returns() {
+        return new external_value(PARAM_RAW, 'html to replace the current module with');
+    }
+
+    /**
+     * Parameters for function get_module()
+     *
+     * @since Moodle 3.3
+     * @return external_function_parameters
+     */
+    public static function get_module_parameters() {
+        return new external_function_parameters(
+            array(
+                'id' => new external_value(PARAM_INT, 'course module id', VALUE_REQUIRED),
+                'sectionreturn' => new external_value(PARAM_INT, 'section to return to', VALUE_DEFAULT, null),
+            ));
+    }
+
+    /**
+     * Returns html for displaying one activity module on course page
+     *
+     * @since Moodle 3.3
+     * @param int $id
+     * @param null|int $sectionreturn
+     * @return string
+     */
+    public static function get_module($id, $sectionreturn = null) {
+        global $PAGE;
+        // Validate and normalize parameters.
+        $params = self::validate_parameters(self::get_module_parameters(),
+            array('id' => $id, 'sectionreturn' => $sectionreturn));
+        $id = $params['id'];
+        $sectionreturn = $params['sectionreturn'];
+
+        // Validate access to the course (note, this is html for the course view page, we don't validate access to the module).
+        list($course, $cm) = get_course_and_cm_from_cmid($id);
+        self::validate_context(context_course::instance($course->id));
+
+        $courserenderer = $PAGE->get_renderer('core', 'course');
+        $completioninfo = new completion_info($course);
+        return $courserenderer->course_section_cm_list_item($course, $completioninfo, $cm, $sectionreturn);
+    }
+
+    /**
+     * Return structure for edit_module()
+     *
+     * @since Moodle 3.3
+     * @return external_description
+     */
+    public static function get_module_returns() {
+        return new external_value(PARAM_RAW, 'html to replace the current module with');
+    }
+
+    /**
+     * Parameters for function edit_section()
+     *
+     * @since Moodle 3.3
+     * @return external_function_parameters
+     */
+    public static function edit_section_parameters() {
+        return new external_function_parameters(
+            array(
+                'action' => new external_value(PARAM_ALPHA, 'action: hide, show, stealth, setmarker, removemarker', VALUE_REQUIRED),
+                'id' => new external_value(PARAM_INT, 'course section id', VALUE_REQUIRED),
+                'sectionreturn' => new external_value(PARAM_INT, 'section to return to', VALUE_DEFAULT, null),
+            ));
+    }
+
+    /**
+     * Performs one of the edit section actions
+     *
+     * @since Moodle 3.3
+     * @param string $action
+     * @param int $id section id
+     * @param int $sectionreturn section to return to
+     * @return string
+     */
+    public static function edit_section($action, $id, $sectionreturn) {
+        global $DB;
+        // Validate and normalize parameters.
+        $params = self::validate_parameters(self::edit_section_parameters(),
+            array('action' => $action, 'id' => $id, 'sectionreturn' => $sectionreturn));
+        $action = $params['action'];
+        $id = $params['id'];
+        $sr = $params['sectionreturn'];
+
+        $section = $DB->get_record('course_sections', array('id' => $id), '*', MUST_EXIST);
+        $coursecontext = context_course::instance($section->course);
+        self::validate_context($coursecontext);
+
+        $rv = course_get_format($section->course)->section_action($section, $action, $sectionreturn);
+        if ($rv) {
+            return json_encode($rv);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Return structure for edit_section()
+     *
+     * @since Moodle 3.3
+     * @return external_description
+     */
+    public static function edit_section_returns() {
+        return new external_value(PARAM_RAW, 'Additional data for javascript (JSON-encoded string)');
+    }
 }
index 3bb9830..0c7a4f2 100644 (file)
@@ -1170,6 +1170,68 @@ abstract class format_base {
         $startdate = $mform->getElementValue($fieldnames['startdate']);
         return $mform->getElement($fieldnames['startdate'])->exportValue($startdate);
     }
+
+    /**
+     * Returns whether this course format allows the activity to
+     * have "triple visibility state" - visible always, hidden on course page but available, hidden.
+     *
+     * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module)
+     * @param stdClass|section_info $section section where this module is located or will be added to
+     * @return bool
+     */
+    public function allow_stealth_module_visibility($cm, $section) {
+        return false;
+    }
+
+    /**
+     * Callback used in WS core_course_edit_section when teacher performs an AJAX action on a section (show/hide)
+     *
+     * Access to the course is already validated in the WS but the callback has to make sure
+     * that particular action is allowed by checking capabilities
+     *
+     * Course formats should register
+     *
+     * @param stdClass|section_info $section
+     * @param string $action
+     * @param int $sr
+     * @return null|array|stdClass any data for the Javascript post-processor (must be json-encodeable)
+     */
+    public function section_action($section, $action, $sr) {
+        global $PAGE;
+        if (!$this->uses_sections() || !$section->section) {
+            // No section actions are allowed if course format does not support sections.
+            // No actions are allowed on the 0-section by default (overwrite in course format if needed).
+            throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action));
+        }
+
+        $course = $this->get_course();
+        $coursecontext = context_course::instance($course->id);
+        switch($action) {
+            case 'hide':
+            case 'show':
+                require_capability('moodle/course:sectionvisibility', $coursecontext);
+                $visible = ($action === 'hide') ? 0 : 1;
+                course_update_section($course, $section, array('visible' => $visible));
+                break;
+            default:
+                throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action));
+        }
+
+        $modules = [];
+
+        $modinfo = get_fast_modinfo($course);
+        $coursesections = $modinfo->sections;
+        if (array_key_exists($section->section, $coursesections)) {
+            $courserenderer = $PAGE->get_renderer('core', 'course');
+            $completioninfo = new completion_info($course);
+            foreach ($coursesections[$section->section] as $cmid) {
+                $cm = $modinfo->get_cm($cmid);
+                $modules[] = $courserenderer->course_section_cm_list_item($course, $completioninfo, $cm, $sr);
+            }
+        }
+
+        return ['modules' => $modules];
+    }
 }
 
 /**
@@ -1231,4 +1293,16 @@ class format_site extends format_base {
         }
         return $courseformatoptions;
     }
+
+    /**
+     * Returns whether this course format allows the activity to
+     * have "triple visibility state" - visible always, hidden on course page but available, hidden.
+     *
+     * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module)
+     * @param stdClass|section_info $section section where this module is located or will be added to
+     * @return bool
+     */
+    public function allow_stealth_module_visibility($cm, $section) {
+        return true;
+    }
 }
index c88d14d..c9237eb 100644 (file)
@@ -38,7 +38,7 @@ defined('MOODLE_INTERNAL') || die();
  */
 abstract class format_section_renderer_base extends plugin_renderer_base {
 
-    /** @var contains instance of core course renderer */
+    /** @var core_course_renderer contains instance of core course renderer */
     protected $courserenderer;
 
     /**
@@ -127,7 +127,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 $menu->add($al);
             }
 
-            $o .= html_writer::div($this->render($menu), 'section_action_menu');
+            $o .= html_writer::div($this->render($menu), 'section_action_menu',
+                array('data-sectionid' => $section->id));
         }
 
         return $o;
@@ -194,7 +195,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             // Only in the non-general sections.
             if (!$section->visible) {
                 $sectionstyle = ' hidden';
-            } else if (course_get_format($course)->is_section_current($section)) {
+            }
+            if (course_get_format($course)->is_section_current($section)) {
                 $sectionstyle = ' current';
             }
         }
@@ -226,13 +228,11 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $sectionname = html_writer::tag('span', $this->section_title($section, $course));
         $o.= $this->output->heading($sectionname, 3, 'sectionname' . $classes);
 
-        $o.= html_writer::start_tag('div', array('class' => 'summary'));
-        $o.= $this->format_summary_text($section);
-        $o.= html_writer::end_tag('div');
+        $o .= $this->section_availability($section);
 
-        $context = context_course::instance($course->id);
-        $o .= $this->section_availability_message($section,
-                has_capability('moodle/course:viewhiddensections', $context));
+        $o .= html_writer::start_tag('div', array('class' => 'summary'));
+        $o .= $this->format_summary_text($section);
+        $o .= html_writer::end_tag('div');
 
         return $o;
     }
@@ -305,14 +305,12 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             return array();
         }
 
+        $sectionreturn = $onsectionpage ? $section->section : null;
+
         $coursecontext = context_course::instance($course->id);
         $isstealth = isset($course->numsections) && ($section->section > $course->numsections);
 
-        if ($onsectionpage) {
-            $baseurl = course_get_url($course, $section->section);
-        } else {
-            $baseurl = course_get_url($course);
-        }
+        $baseurl = course_get_url($course, $sectionreturn);
         $baseurl->param('sesskey', sesskey());
 
         $controls = array();
@@ -325,7 +323,6 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 $streditsection = get_string('editsection');
             }
 
-            $sectionreturn = $onsectionpage ? $section->section : 0;
             $controls['edit'] = array(
                 'url'   => new moodle_url('/course/editsection.php', array('id' => $section->id, 'sr' => $sectionreturn)),
                 'icon' => 'i/settings',
@@ -346,7 +343,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                             'icon' => 'i/hide',
                             'name' => $strhidefromothers,
                             'pixattr' => array('class' => '', 'alt' => $strhidefromothers),
-                            'attr' => array('class' => 'icon editing_showhide', 'title' => $strhidefromothers));
+                            'attr' => array('class' => 'icon editing_showhide', 'title' => $strhidefromothers,
+                                'data-sectionreturn' => $sectionreturn, 'data-action' => 'hide'));
                     } else {
                         $strshowfromothers = get_string('showfromothers', 'format_'.$course->format);
                         $url->param('show',  $section->section);
@@ -355,7 +353,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                             'icon' => 'i/show',
                             'name' => $strshowfromothers,
                             'pixattr' => array('class' => '', 'alt' => $strshowfromothers),
-                            'attr' => array('class' => 'icon editing_showhide', 'title' => $strshowfromothers));
+                            'attr' => array('class' => 'icon editing_showhide', 'title' => $strshowfromothers,
+                                'data-sectionreturn' => $sectionreturn, 'data-action' => 'show'));
                     }
                 }
 
@@ -398,14 +397,14 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 }
                 $url = new moodle_url('/course/editsection.php', array(
                     'id' => $section->id,
-                    'sr' => $onsectionpage ? $section->section : 0,
+                    'sr' => $sectionreturn,
                     'delete' => 1));
                 $controls['delete'] = array(
                     'url' => $url,
                     'icon' => 'i/delete',
                     'name' => $strdelete,
                     'pixattr' => array('class' => '', 'alt' => $strdelete),
-                    'attr' => array('class' => 'icon delete', 'title' => $strdelete));
+                    'attr' => array('class' => 'icon editing_delete', 'title' => $strdelete));
             }
         }
 
@@ -452,9 +451,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $o.= html_writer::end_tag('div');
         $o.= $this->section_activity_summary($section, $course, null);
 
-        $context = context_course::instance($course->id);
-        $o .= $this->section_availability_message($section,
-                has_capability('moodle/course:viewhiddensections', $context));
+        $o .= $this->section_availability($section);
 
         $o .= html_writer::end_tag('div');
         $o .= html_writer::end_tag('li');
@@ -548,31 +545,50 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
      * are going to be unavailable etc). This logic is the same as for
      * activities.
      *
-     * @param stdClass $section The course_section entry from DB
+     * @param section_info $section The course_section entry from DB
      * @param bool $canviewhidden True if user can view hidden sections
      * @return string HTML to output
      */
     protected function section_availability_message($section, $canviewhidden) {
         global $CFG;
         $o = '';
-        if (!$section->uservisible) {
-            // Note: We only get to this function if availableinfo is non-empty,
-            // so there is definitely something to print.
-            $formattedinfo = \core_availability\info::format_info(
-                    $section->availableinfo, $section->course);
-            $o .= html_writer::div($formattedinfo, 'availabilityinfo');
-        } else if ($canviewhidden && !empty($CFG->enableavailability) && $section->visible) {
+        if (!$section->visible) {
+            if ($canviewhidden) {
+                $o .= $this->courserenderer->availability_info(get_string('hiddenfromstudents'), 'ishidden');
+            }
+        } else if (!$section->uservisible) {
+            if ($section->availableinfo) {
+                // Note: We only get to this function if availableinfo is non-empty,
+                // so there is definitely something to print.
+                $formattedinfo = \core_availability\info::format_info(
+                        $section->availableinfo, $section->course);
+                $o .= $this->courserenderer->availability_info($formattedinfo);
+            }
+        } else if ($canviewhidden && !empty($CFG->enableavailability)) {
+            // Check if there is an availability restriction.
             $ci = new \core_availability\info_section($section);
             $fullinfo = $ci->get_full_information();
             if ($fullinfo) {
                 $formattedinfo = \core_availability\info::format_info(
                         $fullinfo, $section->course);
-                $o .= html_writer::div($formattedinfo, 'availabilityinfo');
+                $o .= $this->courserenderer->availability_info($formattedinfo);
             }
         }
         return $o;
     }
 
+    /**
+     * Displays availability information for the section (hidden, not available unles, etc.)
+     *
+     * @param section_info $section
+     * @return string
+     */
+    public function section_availability($section) {
+        $context = context_course::instance($section->course);
+        $canviewhidden = has_capability('moodle/course:viewhiddensections', $context);
+        return html_writer::div($this->section_availability_message($section, $canviewhidden), 'section_availability');
+    }
+
     /**
      * Show if something is on on the course clipboard (moving around)
      *
index d405e7c..c44e0f6 100644 (file)
@@ -108,4 +108,16 @@ class format_social extends format_base {
         }
         return $courseformatoptions;
     }
+
+    /**
+     * Returns whether this course format allows the activity to
+     * have "triple visibility state" - visible always, hidden on course page but available, hidden.
+     *
+     * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module)
+     * @param stdClass|section_info $section section where this module is located or will be added to
+     * @return bool
+     */
+    public function allow_stealth_module_visibility($cm, $section) {
+        return true;
+    }
 }
index f5e0488..1bc58ab 100644 (file)
@@ -410,6 +410,36 @@ class format_topics extends format_base {
     public function supports_news() {
         return true;
     }
+
+    /**
+     * Returns whether this course format allows the activity to
+     * have "triple visibility state" - visible always, hidden on course page but available, hidden.
+     *
+     * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module)
+     * @param stdClass|section_info $section section where this module is located or will be added to
+     * @return bool
+     */
+    public function allow_stealth_module_visibility($cm, $section) {
+        // Allow the third visibility state inside visible sections or in section 0, not allow in orphaned sections.
+        return !$section->section || ($section->visible && $section->section <= $this->get_course()->numsections);
+    }
+
+    public function section_action($section, $action, $sr) {
+        global $PAGE;
+
+        if ($section->section && ($action === 'setmarker' || $action === 'removemarker')) {
+            // Format 'topics' allows to set and remove markers in addition to common section actions.
+            require_capability('moodle/course:setcurrentsection', context_course::instance($this->courseid));
+            course_set_marker($this->courseid, ($action === 'setmarker') ? $section->section : 0);
+            return null;
+        }
+
+        // For show/hide actions call the parent method and return the new content for .section_availability element.
+        $rv = parent::section_action($section, $action, $sr);
+        $renderer = $PAGE->get_renderer('format_topics');
+        $rv['section_availability'] = $renderer->section_availability($this->get_section($section));
+        return $rv;
+    }
 }
 
 /**
index 9ed95bd..963e066 100644 (file)
@@ -129,7 +129,8 @@ class format_topics_renderer extends format_section_renderer_base {
                 $controls['highlight'] = array('url' => $url, "icon" => 'i/marked',
                                                'name' => $highlightoff,
                                                'pixattr' => array('class' => '', 'alt' => $markedthistopic),
-                                               'attr' => array('class' => 'editing_highlight', 'title' => $markedthistopic));
+                                               'attr' => array('class' => 'editing_highlight', 'title' => $markedthistopic,
+                                                   'data-action' => 'removemarker'));
             } else {
                 $url->param('marker', $section->section);
                 $markthistopic = get_string('markthistopic');
@@ -137,7 +138,8 @@ class format_topics_renderer extends format_section_renderer_base {
                 $controls['highlight'] = array('url' => $url, "icon" => 'i/marker',
                                                'name' => $highlight,
                                                'pixattr' => array('class' => '', 'alt' => $markthistopic),
-                                               'attr' => array('class' => 'editing_highlight', 'title' => $markthistopic));
+                                               'attr' => array('class' => 'editing_highlight', 'title' => $markthistopic,
+                                                   'data-action' => 'setmarker'));
             }
         }
 
index ccc38b6..f5cf7ca 100644 (file)
@@ -2,6 +2,15 @@ This files describes API changes for course formats
 
 Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
 
+=== 3.3 ===
+* Javascript code for editing activities and sections was moved to an AMD module, course/rest.php is no longer
+  responsible for editing actions, instead it is done in web services. Carefully test all editing actions during upgrade.
+* The new method format_base::allow_stealth_module_visibility() can indicate whether course format supports "stealth"
+  activities mode when they are available but not visible on course page. Course format that supports stealth mode
+  must check $cm->is_visible_on_course_page() when displaying activities list on the course page instead of $cm->uservisible.
+  For all other plugins except course formats the same property $cm->uservisible indicates if the activity contents
+  is actually available to student.
+
 === 3.2 ===
 * Callback delete_course is deprecated and should be replaced with observer for event \core\event\course_content_deleted
 * Course formats can overwrite get_default_course_enddate function to set the default course end date for new courses.
index af0fba4..408b590 100644 (file)
@@ -497,6 +497,29 @@ class format_weeks extends format_base {
     public function supports_news() {
         return true;
     }
+
+    /**
+     * Returns whether this course format allows the activity to
+     * have "triple visibility state" - visible always, hidden on course page but available, hidden.
+     *
+     * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module)
+     * @param stdClass|section_info $section section where this module is located or will be added to
+     * @return bool
+     */
+    public function allow_stealth_module_visibility($cm, $section) {
+        // Allow the third visibility state inside visible sections or in section 0, not allow in orphaned sections.
+        return !$section->section || ($section->visible && $section->section <= $this->get_course()->numsections);
+    }
+
+    public function section_action($section, $action, $sr) {
+        global $PAGE;
+
+        // Call the parent method and return the new content for .section_availability element.
+        $rv = parent::section_action($section, $action, $sr);
+        $renderer = $PAGE->get_renderer('format_weeks');
+        $rv['section_availability'] = $renderer->section_availability($this->get_section($section));
+        return $rv;
+    }
 }
 
 /**
index 2262711..4fdcc4d 100644 (file)
@@ -388,13 +388,16 @@ function get_array_of_activities($courseid) {
     if (empty($rawmods)) {
         return $mod; // always return array
     }
+    $courseformat = course_get_format($course);
 
-    if ($sections = $DB->get_records('course_sections', array('course' => $courseid), 'section ASC', 'id,section,sequence')) {
+    if ($sections = $DB->get_records('course_sections', array('course' => $courseid),
+            'section ASC', 'id,section,sequence,visible')) {
         // First check and correct obvious mismatches between course_sections.sequence and course_modules.section.
         if ($errormessages = course_integrity_check($courseid, $rawmods, $sections)) {
             debugging(join('<br>', $errormessages));
             $rawmods = get_course_mods($courseid);
-            $sections = $DB->get_records('course_sections', array('course' => $courseid), 'section ASC', 'id,section,sequence');
+            $sections = $DB->get_records('course_sections', array('course' => $courseid),
+                'section ASC', 'id,section,sequence,visible');
         }
         // Build array of activities.
        foreach ($sections as $section) {
@@ -404,6 +407,13 @@ function get_array_of_activities($courseid) {
                    if (empty($rawmods[$seq])) {
                        continue;
                    }
+                   // Adjust visibleoncoursepage, value in DB may not respect format availability.
+                   $rawmods[$seq]->visibleoncoursepage = (!$rawmods[$seq]->visible
+                           || $rawmods[$seq]->visibleoncoursepage
+                           || empty($CFG->allowstealth)
+                           || !$courseformat->allow_stealth_module_visibility($rawmods[$seq], $section)) ? 1 : 0;
+
+                   // Create an object that will be cached.
                    $mod[$seq] = new stdClass();
                    $mod[$seq]->id               = $rawmods[$seq]->instance;
                    $mod[$seq]->cm               = $rawmods[$seq]->id;
@@ -418,6 +428,7 @@ function get_array_of_activities($courseid) {
                    $mod[$seq]->score            = $rawmods[$seq]->score;
                    $mod[$seq]->idnumber         = $rawmods[$seq]->idnumber;
                    $mod[$seq]->visible          = $rawmods[$seq]->visible;
+                   $mod[$seq]->visibleoncoursepage = $rawmods[$seq]->visibleoncoursepage;
                    $mod[$seq]->visibleold       = $rawmods[$seq]->visibleold;
                    $mod[$seq]->groupmode        = $rawmods[$seq]->groupmode;
                    $mod[$seq]->groupingid       = $rawmods[$seq]->groupingid;
@@ -557,9 +568,15 @@ function get_module_types_names($plural = false) {
  * @return void
  */
 function course_set_marker($courseid, $marker) {
-    global $DB;
+    global $DB, $COURSE;
     $DB->set_field("course", "marker", $marker, array('id' => $courseid));
-    format_base::reset_course_cache($courseid);
+    if ($COURSE && $COURSE->id == $courseid) {
+        $COURSE->marker = $marker;
+    }
+    if (class_exists('format_base')) {
+        format_base::reset_course_cache($courseid);
+    }
+    course_modinfo::clear_instance_cache($courseid);
 }
 
 /**
@@ -950,33 +967,29 @@ function set_coursemodule_idnumber($id, $idnumber) {
  *
  * @param int $id of the module
  * @param int $visible state of the module
+ * @param int $visibleoncoursepage state of the module on the course page
  * @return bool false when the module was not found, true otherwise
  */
-function set_coursemodule_visible($id, $visible) {
+function set_coursemodule_visible($id, $visible, $visibleoncoursepage = 1) {
     global $DB, $CFG;
     require_once($CFG->libdir.'/gradelib.php');
     require_once($CFG->dirroot.'/calendar/lib.php');
 
-    // Trigger developer's attention when using the previously removed argument.
-    if (func_num_args() > 2) {
-        debugging('Wrong number of arguments passed to set_coursemodule_visible(), $prevstateoverrides
-            has been removed.', DEBUG_DEVELOPER);
-    }
-
     if (!$cm = $DB->get_record('course_modules', array('id'=>$id))) {
         return false;
     }
 
     // Create events and propagate visibility to associated grade items if the value has changed.
     // Only do this if it's changed to avoid accidently overwriting manual showing/hiding of student grades.
-    if ($cm->visible == $visible) {
+    if ($cm->visible == $visible && $cm->visibleoncoursepage == $visibleoncoursepage) {
         return true;
     }
 
     if (!$modulename = $DB->get_field('modules', 'name', array('id'=>$cm->module))) {
         return false;
     }
-    if ($events = $DB->get_records('event', array('instance'=>$cm->instance, 'modulename'=>$modulename))) {
+    if (($cm->visible != $visible) &&
+            ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename)))) {
         foreach($events as $event) {
             if ($visible) {
                 $event = new calendar_event($event);
@@ -993,17 +1006,19 @@ function set_coursemodule_visible($id, $visible) {
     $cminfo = new stdClass();
     $cminfo->id = $id;
     $cminfo->visible = $visible;
+    $cminfo->visibleoncoursepage = $visibleoncoursepage;
     $cminfo->visibleold = $visible;
     $DB->update_record('course_modules', $cminfo);
 
     // Hide the associated grade items so the teacher doesn't also have to go to the gradebook and hide them there.
     // Note that this must be done after updating the row in course_modules, in case
     // the modules grade_item_update function needs to access $cm->visible.
-    if (plugin_supports('mod', $modulename, FEATURE_CONTROLS_GRADE_VISIBILITY) &&
+    if ($cm->visible != $visible &&
+            plugin_supports('mod', $modulename, FEATURE_CONTROLS_GRADE_VISIBILITY) &&
             component_callback_exists('mod_' . $modulename, 'grade_item_update')) {
         $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST);
         component_callback('mod_' . $modulename, 'grade_item_update', array($instance));
-    } else {
+    } else if ($cm->visible != $visible) {
         $grade_items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$modulename, 'iteminstance'=>$cm->instance, 'courseid'=>$cm->course));
         if ($grade_items) {
             foreach ($grade_items as $grade_item) {
@@ -1593,10 +1608,10 @@ function course_update_section($course, $section, $data) {
             if ($cm = get_coursemodule_from_id(null, $moduleid, $courseid)) {
                 if ($data['visible']) {
                     // As we unhide the section, we use the previously saved visibility stored in visibleold.
-                    set_coursemodule_visible($moduleid, $cm->visibleold);
+                    set_coursemodule_visible($moduleid, $cm->visibleold, $cm->visibleoncoursepage);
                 } else {
                     // We hide the section, so we hide the module but we store the original state in visibleold.
-                    set_coursemodule_visible($moduleid, 0);
+                    set_coursemodule_visible($moduleid, 0, $cm->visibleoncoursepage);
                     $DB->set_field('course_modules', 'visibleold', $cm->visible, array('id' => $moduleid));
                 }
                 \core\event\course_module_updated::create_from_cm($cm)->trigger();
@@ -1765,12 +1780,13 @@ function moveto_module($mod, $section, $beforemod=NULL) {
  * @return array array of action_link or pix_icon objects
  */
 function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
-    global $COURSE, $SITE;
+    global $COURSE, $SITE, $CFG;
 
     static $str;
 
     $coursecontext = context_course::instance($mod->course);
     $modcontext = context_module::instance($mod->id);
+    $courseformat = course_get_format($mod->get_course());
 
     $editcaps = array('moodle/course:manageactivities', 'moodle/course:activityvisibility', 'moodle/role:assign');
     $dupecaps = array('moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport');
@@ -1784,7 +1800,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
 
     if (!isset($str)) {
         $str = get_strings(array('delete', 'move', 'moveright', 'moveleft',
-            'editsettings', 'duplicate', 'hide', 'show'), 'moodle');
+            'editsettings', 'duplicate', 'hide', 'makeavailable', 'makeunavailable', 'show'), 'moodle');
         $str->assign         = get_string('assignroles', 'role');
         $str->groupsnone     = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsnone"));
         $str->groupsseparate = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsseparate"));
@@ -1830,7 +1846,8 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
             new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '1')),
             new pix_icon($rightarrow, $str->moveright, 'moodle', array('class' => 'iconsmall', 'title' => '')),
             $str->moveright,
-            array('class' => 'editing_moveright ' . $enabledclass, 'data-action' => 'moveright', 'data-keepopen' => true)
+            array('class' => 'editing_moveright ' . $enabledclass, 'data-action' => 'moveright',
+                'data-keepopen' => true, 'data-sectionreturn' => $sr)
         );
 
         if ($indent <= $indentlimits->min) {
@@ -1842,21 +1859,33 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
             new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '-1')),
             new pix_icon($leftarrow, $str->moveleft, 'moodle', array('class' => 'iconsmall', 'title' => '')),
             $str->moveleft,
-            array('class' => 'editing_moveleft ' . $enabledclass, 'data-action' => 'moveleft', 'data-keepopen' => true)
+            array('class' => 'editing_moveleft ' . $enabledclass, 'data-action' => 'moveleft',
+                'data-keepopen' => true, 'data-sectionreturn' => $sr)
         );
 
     }
 
-    // Hide/Show.
+    // Hide/Show/Available/Unavailable.
     if (has_capability('moodle/course:activityvisibility', $modcontext)) {
-        if ($mod->visible) {
+        $allowstealth = !empty($CFG->allowstealth) && $courseformat->allow_stealth_module_visibility($mod, $mod->get_section_info());
+
+        $sectionvisible = $mod->get_section_info()->visible;
+        // The module on the course page may be in one of the following states:
+        // - Available and displayed on the course page ($displayedoncoursepage);
+        // - Not available and not displayed on the course page ($unavailable);
+        // - Available but not displayed on the course page ($stealth) - this can also be a visible activity in a hidden section.
+        $displayedoncoursepage = $mod->visible && $mod->visibleoncoursepage && $sectionvisible;
+        $unavailable = !$mod->visible;
+        $stealth = $mod->visible && (!$mod->visibleoncoursepage || !$sectionvisible);
+        if ($displayedoncoursepage) {
             $actions['hide'] = new action_menu_link_secondary(
                 new moodle_url($baseurl, array('hide' => $mod->id)),
                 new pix_icon('t/hide', $str->hide, 'moodle', array('class' => 'iconsmall', 'title' => '')),
                 $str->hide,
                 array('class' => 'editing_hide', 'data-action' => 'hide')
             );
-        } else {
+        } else if (!$displayedoncoursepage && $sectionvisible) {
+            // Offer to "show" only if the section is visible.
             $actions['show'] = new action_menu_link_secondary(
                 new moodle_url($baseurl, array('show' => $mod->id)),
                 new pix_icon('t/show', $str->show, 'moodle', array('class' => 'iconsmall', 'title' => '')),
@@ -1864,16 +1893,38 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
                 array('class' => 'editing_show', 'data-action' => 'show')
             );
         }
+
+        if ($stealth) {
+            // When making the "stealth" module unavailable we perform the same action as hiding the visible module.
+            $actions['hide'] = new action_menu_link_secondary(
+                new moodle_url($baseurl, array('hide' => $mod->id)),
+                new pix_icon('t/unblock', $str->makeunavailable, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+                $str->makeunavailable,
+                array('class' => 'editing_makeunavailable', 'data-action' => 'hide', 'data-sectionreturn' => $sr)
+            );
+        } else if ($unavailable && (!$sectionvisible || $allowstealth) && $mod->has_view()) {
+            // Allow to make visually hidden module available in gradebook and other reports by making it a "stealth" module.
+            // When the section is hidden it is an equivalent of "showing" the module.
+            // Activities without the link (i.e. labels) can not be made available but hidden on course page.
+            $action = $sectionvisible ? 'stealth' : 'show';
+            $actions[$action] = new action_menu_link_secondary(
+                new moodle_url($baseurl, array($action => $mod->id)),
+                new pix_icon('t/block', $str->makeavailable, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+                $str->makeavailable,
+                array('class' => 'editing_makeavailable', 'data-action' => $action, 'data-sectionreturn' => $sr)
+            );
+        }
     }
 
     // Duplicate (require both target import caps to be able to duplicate and backup2 support, see modduplicate.php)
     if (has_all_capabilities($dupecaps, $coursecontext) &&
-            plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2)) {
+            plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2) &&
+            course_allowed_module($mod->get_course(), $mod->modname)) {
         $actions['duplicate'] = new action_menu_link_secondary(
             new moodle_url($baseurl, array('duplicate' => $mod->id)),
             new pix_icon('t/copy', $str->duplicate, 'moodle', array('class' => 'iconsmall', 'title' => '')),
             $str->duplicate,
-            array('class' => 'editing_duplicate', 'data-action' => 'duplicate', 'data-sr' => $sr)
+            array('class' => 'editing_duplicate', 'data-action' => 'duplicate', 'data-sectionreturn' => $sr)
         );
     }
 
@@ -1884,16 +1935,19 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
                 $nextgroupmode = VISIBLEGROUPS;
                 $grouptitle = $str->groupsseparate;
                 $actionname = 'groupsseparate';
+                $nextactionname = 'groupsvisible';
                 $groupimage = 'i/groups';
             } else if ($mod->effectivegroupmode == VISIBLEGROUPS) {
                 $nextgroupmode = NOGROUPS;
                 $grouptitle = $str->groupsvisible;
                 $actionname = 'groupsvisible';
+                $nextactionname = 'groupsnone';
                 $groupimage = 'i/groupv';
             } else {
                 $nextgroupmode = SEPARATEGROUPS;
                 $grouptitle = $str->groupsnone;
                 $actionname = 'groupsnone';
+                $nextactionname = 'groupsseparate';
                 $groupimage = 'i/groupn';
             }
 
@@ -1901,7 +1955,8 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
                 new moodle_url($baseurl, array('id' => $mod->id, 'groupmode' => $nextgroupmode)),
                 new pix_icon($groupimage, null, 'moodle', array('class' => 'iconsmall')),
                 $grouptitle,
-                array('class' => 'editing_'. $actionname, 'data-action' => $actionname, 'data-nextgroupmode' => $nextgroupmode, 'aria-live' => 'assertive')
+                array('class' => 'editing_'. $actionname, 'data-action' => $nextactionname,
+                    'aria-live' => 'assertive', 'data-sectionreturn' => $sr)
             );
         } else {
             $actions['nogroupsupport'] = new action_menu_filler();
@@ -1914,7 +1969,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
             new moodle_url('/admin/roles/assign.php', array('contextid' => $modcontext->id)),
             new pix_icon('t/assignroles', $str->assign, 'moodle', array('class' => 'iconsmall', 'title' => '')),
             $str->assign,
-            array('class' => 'editing_assign', 'data-action' => 'assignroles')
+            array('class' => 'editing_assign', 'data-action' => 'assignroles', 'data-sectionreturn' => $sr)
         );
     }
 
@@ -1924,7 +1979,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
             new moodle_url($baseurl, array('delete' => $mod->id)),
             new pix_icon('t/delete', $str->delete, 'moodle', array('class' => 'iconsmall', 'title' => '')),
             $str->delete,
-            array('class' => 'editing_delete', 'data-action' => 'delete')
+            array('class' => 'editing_delete', 'data-action' => 'delete', 'data-sectionreturn' => $sr)
         );
     }
 
@@ -1970,7 +2025,7 @@ function course_get_cm_move(cm_info $mod, $sr = null) {
         return html_writer::link(
             new moodle_url($baseurl, array('copy' => $mod->id)),
             $OUTPUT->pix_icon($pixicon, $str->move, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-            array('class' => 'editing_move', 'data-action' => 'move')
+            array('class' => 'editing_move', 'data-action' => 'move', 'data-sectionreturn' => $sr)
         );
     }
     return '';
@@ -3007,25 +3062,6 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules =
         $config->pageparams = array();
     }
 
-    // Include toolboxes
-    $PAGE->requires->yui_module('moodle-course-toolboxes',
-            'M.course.init_resource_toolbox',
-            array(array(
-                'courseid' => $course->id,
-                'ajaxurl' => $config->resourceurl,
-                'config' => $config,
-            ))
-    );
-    $PAGE->requires->yui_module('moodle-course-toolboxes',
-            'M.course.init_section_toolbox',
-            array(array(
-                'courseid' => $course->id,
-                'format' => $course->format,
-                'ajaxurl' => $config->sectionurl,
-                'config' => $config,
-            ))
-    );
-
     // Include course dragdrop
     if (course_format_uses_sections($course->format)) {
         $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_section_dragdrop',
@@ -3088,6 +3124,8 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules =
     require_once($CFG->dirroot.'/course/dnduploadlib.php');
     dndupload_add_to_course($course, $enabledmodules);
 
+    $PAGE->requires->js_call_amd('core_course/actions', 'initCoursePage', array($course->format));
+
     return true;
 }
 
index 9c23493..54f6f9c 100644 (file)
@@ -33,6 +33,7 @@ $indent        = optional_param('indent', 0, PARAM_INT);
 $update        = optional_param('update', 0, PARAM_INT);
 $duplicate     = optional_param('duplicate', 0, PARAM_INT);
 $hide          = optional_param('hide', 0, PARAM_INT);
+$stealth       = optional_param('stealth', 0, PARAM_INT);
 $show          = optional_param('show', 0, PARAM_INT);
 $copy          = optional_param('copy', 0, PARAM_INT);
 $moveto        = optional_param('moveto', 0, PARAM_INT);
@@ -202,28 +203,30 @@ if ((!empty($movetosection) or !empty($moveto)) and confirm_sesskey()) {
     $modcontext = context_module::instance($cm->id);
     require_capability('moodle/course:activityvisibility', $modcontext);
 
-    set_coursemodule_visible($cm->id, 0);
-    \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger();
+    if (set_coursemodule_visible($cm->id, 0)) {
+        \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger();
+    }
     redirect(course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn)));
 
-} else if (!empty($show) and confirm_sesskey()) {
-    $cm     = get_coursemodule_from_id('', $show, 0, true, MUST_EXIST);
-    $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
-
+} else if (!empty($stealth) and confirm_sesskey()) {
+    list($course, $cm) = get_course_and_cm_from_cmid($stealth);
     require_login($course, false, $cm);
-    $coursecontext = context_course::instance($course->id);
-    $modcontext = context_module::instance($cm->id);
-    require_capability('moodle/course:activityvisibility', $modcontext);
+    require_capability('moodle/course:activityvisibility', $cm->context);
 
-    $section = $DB->get_record('course_sections', array('id'=>$cm->section), '*', MUST_EXIST);
+    if (set_coursemodule_visible($cm->id, 1, 0)) {
+        \core\event\course_module_updated::create_from_cm($cm)->trigger();
+    }
+    redirect(course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn)));
 
-    $module = $DB->get_record('modules', array('id'=>$cm->module), '*', MUST_EXIST);
+} else if (!empty($show) and confirm_sesskey()) {
+    list($course, $cm) = get_course_and_cm_from_cmid($show);
+    require_login($course, false, $cm);
+    require_capability('moodle/course:activityvisibility', $cm->context);
+    $section = $cm->get_section_info();
 
-    if ($module->visible and ($section->visible or (SITEID == $cm->course))) {
-        set_coursemodule_visible($cm->id, 1);
-        \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger();
+    if (set_coursemodule_visible($cm->id, 1)) {
+        \core\event\course_module_updated::create_from_cm($cm)->trigger();
     }
-
     redirect(course_get_url($course, $section->section, array('sr' => $sectionreturn)));
 
 } else if ($groupmode > -1 and confirm_sesskey()) {
index d45966d..68ed661 100644 (file)
@@ -60,6 +60,7 @@ function add_moduleinfo($moduleinfo, $course, $mform = null) {
     $newcm->module           = $moduleinfo->module;
     $newcm->instance         = 0; // Not known yet, will be updated later (this is similar to restore code).
     $newcm->visible          = $moduleinfo->visible;
+    $newcm->visibleoncoursepage = $moduleinfo->visibleoncoursepage;
     $newcm->visibleold       = $moduleinfo->visible;
     if (isset($moduleinfo->cmidnumber)) {
         $newcm->idnumber         = $moduleinfo->cmidnumber;
@@ -597,7 +598,7 @@ function update_moduleinfo($cm, $moduleinfo, $course, $mform = null) {
 
     // Make sure visibility is set correctly (in particular in calendar).
     if (has_capability('moodle/course:activityvisibility', $modcontext)) {
-        set_coursemodule_visible($moduleinfo->coursemodule, $moduleinfo->visible);
+        set_coursemodule_visible($moduleinfo->coursemodule, $moduleinfo->visible, $moduleinfo->visibleoncoursepage);
     }
 
     if (isset($moduleinfo->cmidnumber)) { // Label.
@@ -655,6 +656,7 @@ function get_moduleinfo_data($cm, $course) {
     $data->coursemodule       = $cm->id;
     $data->section            = $cw->section;  // The section number itself - relative!!! (section column in course_sections)
     $data->visible            = $cm->visible; //??  $cw->visible ? $cm->visible : 0; // section hiding overrides
+    $data->visibleoncoursepage = $cm->visibleoncoursepage;
     $data->cmidnumber         = $cm->idnumber;          // The cm IDnumber
     $data->groupmode          = groups_get_activity_groupmode($cm); // locked later if forced
     $data->groupingid         = $cm->groupingid;
index f5c76e0..f98c1fd 100644 (file)
@@ -21,7 +21,7 @@ abstract class moodleform_mod extends moodleform {
      * Section of course that module instance will be put in or is in.
      * This is always the section number itself (column 'section' from 'course_sections' table).
      *
-     * @var mixed
+     * @var int
      */
     protected $_section;
     /**
@@ -555,7 +555,10 @@ abstract class moodleform_mod extends moodleform {
 
         $mform->addElement('header', 'modstandardelshdr', get_string('modstandardels', 'form'));
 
-        $mform->addElement('modvisible', 'visible', get_string('visible'));
+        $section = get_fast_modinfo($COURSE)->get_section_info($this->_section);
+        $allowstealth = !empty($CFG->allowstealth) && $this->courseformat->allow_stealth_module_visibility($this->_cm, $section);
+        $mform->addElement('modvisible', 'visible', get_string('visible'), null,
+                array('allowstealth' => $allowstealth, 'sectionvisible' => $section->visible, 'cm' => $this->_cm));
         if (!empty($this->_cm)) {
             $context = context_module::instance($this->_cm->id);
             if (!has_capability('moodle/course:activityvisibility', $context)) {
index 90ad00f..6a6c100 100644 (file)
@@ -450,7 +450,7 @@ class core_course_renderer extends plugin_renderer_base {
     public function course_section_cm_completion($course, &$completioninfo, cm_info $mod, $displayoptions = array()) {
         global $CFG;
         $output = '';
-        if (!empty($displayoptions['hidecompletion']) || !isloggedin() || isguestuser() || !$mod->uservisible) {
+        if (!$mod->is_visible_on_course_page()) {
             return $output;
         }
         if ($completioninfo === null) {
@@ -582,15 +582,58 @@ class core_course_renderer extends plugin_renderer_base {
      * @return string
      */
     public function course_section_cm_name(cm_info $mod, $displayoptions = array()) {
-        if ((!$mod->uservisible && empty($mod->availableinfo)) || !$mod->url) {
+        if (!$mod->is_visible_on_course_page() || !$mod->url) {
             // Nothing to be displayed to the user.
             return '';
         }
 
+        list($linkclasses, $textclasses) = $this->course_section_cm_classes($mod);
+        $groupinglabel = $mod->get_grouping_label($textclasses);
+
         // Render element that allows to edit activity name inline. It calls {@link course_section_cm_name_title()}
         // to get the display title of the activity.
         $tmpl = new \core_course\output\course_module_name($mod, $this->page->user_is_editing(), $displayoptions);
-        return $this->output->render_from_template('core/inplace_editable', $tmpl->export_for_template($this->output));
+        return $this->output->render_from_template('core/inplace_editable', $tmpl->export_for_template($this->output)) .
+            $groupinglabel;
+    }
+
+    /**
+     * Returns the CSS classes for the activity name/content
+     *
+     * For items which are hidden, unavailable or stealth but should be displayed
+     * to current user ($mod->is_visible_on_course_page()), we show those as dimmed.
+     * Students will also see as dimmed activities names that are not yet available
+     * but should still be displayed (without link) with availability info.
+     *
+     * @param cm_info $mod
+     * @return array array of two elements ($linkclasses, $textclasses)
+     */
+    protected function course_section_cm_classes(cm_info $mod) {
+        $linkclasses = '';
+        $textclasses = '';
+        if ($mod->uservisible) {
+            $conditionalhidden = $this->is_cm_conditionally_hidden($mod);
+            $accessiblebutdim = (!$mod->visible || $conditionalhidden) &&
+                has_capability('moodle/course:viewhiddenactivities', $mod->context);
+            if ($accessiblebutdim) {
+                $linkclasses .= ' dimmed';
+                $textclasses .= ' dimmed_text';
+                if ($conditionalhidden) {
+                    $linkclasses .= ' conditionalhidden';
+                    $textclasses .= ' conditionalhidden';
+                }
+            }
+            if ($mod->is_stealth()) {
+                // Stealth activity is the one that is not visible on course page.
+                // It still may be displayed to the users who can manage it.
+                $linkclasses .= ' stealth';
+                $textclasses .= ' stealth';
+            }
+        } else {
+            $linkclasses .= ' dimmed';
+            $textclasses .= ' dimmed_text';
+        }
+        return array($linkclasses, $textclasses);
     }
 
     /**
@@ -608,12 +651,9 @@ class core_course_renderer extends plugin_renderer_base {
      */
     public function course_section_cm_name_title(cm_info $mod, $displayoptions = array()) {
         $output = '';
-        if (!$mod->uservisible && empty($mod->availableinfo)) {
-            // Nothing to be displayed to the user.
-            return $output;
-        }
         $url = $mod->url;
-        if (!$url) {
+        if (!$mod->is_visible_on_course_page() || !$url) {
+            // Nothing to be displayed to the user.
             return $output;
         }
 
@@ -632,51 +672,22 @@ class core_course_renderer extends plugin_renderer_base {
             $altname = get_accesshide(' '.$altname);
         }
 
-        // For items which are hidden but available to current user
-        // ($mod->uservisible), we show those as dimmed only if the user has
-        // viewhiddenactivities, so that teachers see 'items which might not
-        // be available to some students' dimmed but students do not see 'item
-        // which is actually available to current student' dimmed.
-        $linkclasses = '';
-        $accesstext = '';
-        $textclasses = '';
-        if ($mod->uservisible) {
-            $conditionalhidden = $this->is_cm_conditionally_hidden($mod);
-            $accessiblebutdim = (!$mod->visible || $conditionalhidden) &&
-                has_capability('moodle/course:viewhiddenactivities', $mod->context);
-            if ($accessiblebutdim) {
-                $linkclasses .= ' dimmed';
-                $textclasses .= ' dimmed_text';
-                if ($conditionalhidden) {
-                    $linkclasses .= ' conditionalhidden';
-                    $textclasses .= ' conditionalhidden';
-                }
-                // Show accessibility note only if user can access the module himself.
-                $accesstext = get_accesshide(get_string('hiddenfromstudents').':'. $mod->modfullname);
-            }
-        } else {
-            $linkclasses .= ' dimmed';
-            $textclasses .= ' dimmed_text';
-        }
+        list($linkclasses, $textclasses) = $this->course_section_cm_classes($mod);
 
         // Get on-click attribute value if specified and decode the onclick - it
         // has already been encoded for display (puke).
         $onclick = htmlspecialchars_decode($mod->onclick, ENT_QUOTES);
 
-        $groupinglabel = $mod->get_grouping_label($textclasses);
-
         // Display link itself.
         $activitylink = html_writer::empty_tag('img', array('src' => $mod->get_icon_url(),
-                'class' => 'iconlarge activityicon', 'alt' => ' ', 'role' => 'presentation')) . $accesstext .
+                'class' => 'iconlarge activityicon', 'alt' => ' ', 'role' => 'presentation')) .
                 html_writer::tag('span', $instancename . $altname, array('class' => 'instancename'));
         if ($mod->uservisible) {
-            $output .= html_writer::link($url, $activitylink, array('class' => $linkclasses, 'onclick' => $onclick)) .
-                    $groupinglabel;
+            $output .= html_writer::link($url, $activitylink, array('class' => $linkclasses, 'onclick' => $onclick));
         } else {
             // We may be displaying this just in order to show information
-            // about visibility, without the actual link ($mod->uservisible)
-            $output .= html_writer::tag('div', $activitylink, array('class' => $textclasses)) .
-                    $groupinglabel;
+            // about visibility, without the actual link ($mod->is_visible_on_course_page()).
+            $output .= html_writer::tag('div', $activitylink, array('class' => $textclasses));
         }
         return $output;
     }
@@ -690,29 +701,13 @@ class core_course_renderer extends plugin_renderer_base {
      */
     public function course_section_cm_text(cm_info $mod, $displayoptions = array()) {
         $output = '';
-        if (!$mod->uservisible && empty($mod->availableinfo)) {
+        if (!$mod->is_visible_on_course_page()) {
             // nothing to be displayed to the user
             return $output;
         }
         $content = $mod->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
-        $accesstext = '';
-        $textclasses = '';
-        if ($mod->uservisible) {
-            $conditionalhidden = $this->is_cm_conditionally_hidden($mod);
-            $accessiblebutdim = (!$mod->visible || $conditionalhidden) &&
-                has_capability('moodle/course:viewhiddenactivities', $mod->context);
-            if ($accessiblebutdim) {
-                $textclasses .= ' dimmed_text';
-                if ($conditionalhidden) {
-                    $textclasses .= ' conditionalhidden';
-                }
-                // Show accessibility note only if user can access the module himself.
-                $accesstext = get_accesshide(get_string('hiddenfromstudents').':'. $mod->modfullname);
-            }
-        } else {
-            $textclasses .= ' dimmed_text';
-        }
-        if ($mod->url) {
+        list($linkclasses, $textclasses) = $this->course_section_cm_classes($mod);
+        if ($mod->url && $mod->uservisible) {
             if ($content) {
                 // If specified, display extra content after link.
                 $output = html_writer::tag('div', $content, array('class' =>
@@ -722,12 +717,24 @@ class core_course_renderer extends plugin_renderer_base {
             $groupinglabel = $mod->get_grouping_label($textclasses);
 
             // No link, so display only content.
-            $output = html_writer::tag('div', $accesstext . $content . $groupinglabel,
+            $output = html_writer::tag('div', $content . $groupinglabel,
                     array('class' => 'contentwithoutlink ' . $textclasses));
         }
         return $output;
     }
 
+    /**
+     * Displays availability info for a course section or course module
+     *
+     * @param string $text
+     * @param string $additionalclasses
+     * @return string
+     */
+    public function availability_info($text, $additionalclasses = '') {
+        $data = ['text' => $text, 'classes' => $additionalclasses];
+        return $this->render_from_template('core/availability_info', $data);
+    }
+
     /**
      * Renders HTML to show course module availability information (for someone who isn't allowed
      * to see the activity itself, or for staff)
@@ -738,13 +745,17 @@ class core_course_renderer extends plugin_renderer_base {
      */
     public function course_section_cm_availability(cm_info $mod, $displayoptions = array()) {
         global $CFG;
+        $output = '';
+        if (!$mod->is_visible_on_course_page()) {
+            return $output;
+        }
         if (!$mod->uservisible) {
             // this is a student who is not allowed to see the module but might be allowed
             // to see availability info (i.e. "Available from ...")
             if (!empty($mod->availableinfo)) {
                 $formattedinfo = \core_availability\info::format_info(
                         $mod->availableinfo, $mod->get_course());
-                $output = html_writer::tag('div', $formattedinfo, array('class' => 'availabilityinfo'));
+                $output = $this->availability_info($formattedinfo);
             }
             return $output;
         }
@@ -752,7 +763,19 @@ class core_course_renderer extends plugin_renderer_base {
         // information that module is not available to all/some students
         $modcontext = context_module::instance($mod->id);
         $canviewhidden = has_capability('moodle/course:viewhiddenactivities', $modcontext);
+        if ($canviewhidden && !$mod->visible) {
+            // This module is hidden but current user has capability to see it.
+            // Do not display the availability info if the whole section is hidden.
+            if ($mod->get_section_info()->visible) {
+                $output .= $this->availability_info(get_string('hiddenfromstudents'), 'ishidden');
+            }
+        } else if ($mod->is_stealth()) {
+            // This module is available but is normally not displayed on the course page
+            // (this user can see it because they can manage it).
+            $output .= $this->availability_info(get_string('hiddenoncoursepage'), 'isstealth');
+        }
         if ($canviewhidden && !empty($CFG->enableavailability)) {
+            // Display information about conditional availability.
             // Don't add availability information if user is not editing and activity is hidden.
             if ($mod->visible || $this->page->user_is_editing()) {
                 $hidinfoclass = '';
@@ -764,11 +787,11 @@ class core_course_renderer extends plugin_renderer_base {
                 if ($fullinfo) {
                     $formattedinfo = \core_availability\info::format_info(
                             $fullinfo, $mod->get_course());
-                    return html_writer::div($formattedinfo, 'availabilityinfo ' . $hidinfoclass);
+                    $output .= $this->availability_info($formattedinfo, $hidinfoclass);
                 }
             }
         }
-        return '';
+        return $output;
     }
 
     /**
@@ -823,7 +846,7 @@ class core_course_renderer extends plugin_renderer_base {
         // 2) The 'availableinfo' is empty, i.e. the activity was
         //     hidden in a way that leaves no info, such as using the
         //     eye icon.
-        if (!$mod->uservisible && empty($mod->availableinfo)) {
+        if (!$mod->is_visible_on_course_page()) {
             return $output;
         }
 
@@ -890,15 +913,15 @@ class core_course_renderer extends plugin_renderer_base {
             $output .= html_writer::span($modicons, 'actions');
         }
 
+        // Show availability info (if module is not available).
+        $output .= $this->course_section_cm_availability($mod, $displayoptions);
+
         // If there is content AND a link, then display the content here
         // (AFTER any icons). Otherwise it was displayed before
         if (!empty($url)) {
             $output .= $contentpart;
         }
 
-        // show availability info (if module is not available)
-        $output .= $this->course_section_cm_availability($mod, $displayoptions);
-
         $output .= html_writer::end_tag('div'); // $indentclasses
 
         // End of indentation div.
index a63f6ce..e7d5b2e 100644 (file)
@@ -33,17 +33,10 @@ require_once($CFG->dirroot.'/course/lib.php');
 $courseid   = required_param('courseId', PARAM_INT);
 $class      = required_param('class', PARAM_ALPHA);
 $field      = optional_param('field', '', PARAM_ALPHA);
-$instanceid = optional_param('instanceId', 0, PARAM_INT);
 $sectionid  = optional_param('sectionId', 0, PARAM_INT);
 $beforeid   = optional_param('beforeId', 0, PARAM_INT);
 $value      = optional_param('value', 0, PARAM_INT);
-$column     = optional_param('column', 0, PARAM_ALPHA);
 $id         = optional_param('id', 0, PARAM_INT);
-$summary    = optional_param('summary', '', PARAM_RAW);
-$sequence   = optional_param('sequence', '', PARAM_SEQUENCE);
-$visible    = optional_param('visible', 0, PARAM_INT);
-$pageaction = optional_param('action', '', PARAM_ALPHA); // Used to simulate a DELETE command
-$title      = optional_param('title', '', PARAM_TEXT);
 
 $PAGE->set_url('/course/rest.php', array('courseId'=>$courseid,'class'=>$class));
 
@@ -63,113 +56,33 @@ require_sesskey();
 
 echo $OUTPUT->header(); // send headers
 
-// OK, now let's process the parameters and do stuff
-// MDL-10221 the DELETE method is not allowed on some web servers, so we simulate it with the action URL param
-$requestmethod = $_SERVER['REQUEST_METHOD'];
-if ($pageaction == 'DELETE') {
-    $requestmethod = 'DELETE';
-}
-
-switch($requestmethod) {
-    case 'POST':
-
-        switch ($class) {
-            case 'section':
-
-                if (!$DB->record_exists('course_sections', array('course'=>$course->id, 'section'=>$id))) {
-                    throw new moodle_exception('AJAX commands.php: Bad Section ID '.$id);
-                }
-
-                switch ($field) {
-                    case 'visible':
-                        require_capability('moodle/course:sectionvisibility', $coursecontext);
-                        $resourcestotoggle = set_section_visible($course->id, $id, $value);
-                        echo json_encode(array('resourcestotoggle' => $resourcestotoggle));
-                        break;
-
-                    case 'move':
-                        require_capability('moodle/course:movesections', $coursecontext);
-                        move_section_to($course, $id, $value);
-                        // See if format wants to do something about it
-                        $response = course_get_format($course)->ajax_section_move();
-                        if ($response !== null) {
-                            echo json_encode($response);
-                        }
-                        break;
-                }
-                break;
-
-            case 'resource':
-                switch ($field) {
-                    case 'visible':
-                        require_capability('moodle/course:activityvisibility', $modcontext);
-                        set_coursemodule_visible($cm->id, $value);
-                        \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger();
-                        break;
-
-                    case 'duplicate':
-                        require_capability('moodle/course:manageactivities', $coursecontext);
-                        require_capability('moodle/backup:backuptargetimport', $coursecontext);
-                        require_capability('moodle/restore:restoretargetimport', $coursecontext);
-                        if (!course_allowed_module($course, $cm->modname)) {
-                            throw new moodle_exception('No permission to create that activity');
-                        }
-                        $sr = optional_param('sr', null, PARAM_INT);
-                        $result = mod_duplicate_activity($course, $cm, $sr);
-                        echo json_encode($result);
-                        break;
-
-                    case 'groupmode':
-                        require_capability('moodle/course:manageactivities', $modcontext);
-                        set_coursemodule_groupmode($cm->id, $value);
-                        \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger();
-                        break;
-
-                    case 'indent':
-                        require_capability('moodle/course:manageactivities', $modcontext);
-                        $cm->indent = $value;
-                        if ($cm->indent >= 0) {
-                            $DB->update_record('course_modules', $cm);
-                            rebuild_course_cache($cm->course);
-                        }
-                        break;
-
-                    case 'move':
-                        require_capability('moodle/course:manageactivities', $modcontext);
-                        if (!$section = $DB->get_record('course_sections', array('course'=>$course->id, 'section'=>$sectionid))) {
-                            throw new moodle_exception('AJAX commands.php: Bad section ID '.$sectionid);
-                        }
-
-                        if ($beforeid > 0){
-                            $beforemod = get_coursemodule_from_id('', $beforeid, $course->id);
-                            $beforemod = $DB->get_record('course_modules', array('id'=>$beforeid));
-                        } else {
-                            $beforemod = NULL;
-                        }
-
-                        $isvisible = moveto_module($cm, $section, $beforemod);
-                        echo json_encode(array('visible' => (bool) $isvisible));
-                        break;
-                }
-                break;
-
-            case 'course':
-                switch($field) {
-                    case 'marker':
-                        require_capability('moodle/course:setcurrentsection', $coursecontext);
-                        course_set_marker($course->id, $value);
-                        break;
-                }
-                break;
-        }
-        break;
-
-    case 'DELETE':
-        switch ($class) {
-            case 'resource':
-                require_capability('moodle/course:manageactivities', $modcontext);
-                course_delete_module($cm->id, true);
-                break;
-        }
-        break;
+if ($class === 'section' && $field === 'move') {
+    if (!$DB->record_exists('course_sections', array('course' => $course->id, 'section' => $id))) {
+        throw new moodle_exception('AJAX commands.php: Bad Section ID ' . $id);
+    }
+
+    require_capability('moodle/course:movesections', $coursecontext);
+    move_section_to($course, $id, $value);
+    // See if format wants to do something about it.
+    $response = course_get_format($course)->ajax_section_move();
+    if ($response !== null) {
+        echo json_encode($response);
+    }
+
+} else if ($class === 'resource' && $field === 'move') {
+
+    require_capability('moodle/course:manageactivities', $modcontext);
+    if (!$section = $DB->get_record('course_sections', array('course' => $course->id, 'section' => $sectionid))) {
+        throw new moodle_exception('AJAX commands.php: Bad section ID '.$sectionid);
+    }
+
+    if ($beforeid > 0) {
+        $beforemod = get_coursemodule_from_id('', $beforeid, $course->id);
+        $beforemod = $DB->get_record('course_modules', array('id' => $beforeid));
+    } else {
+        $beforemod = null;
+    }
+
+    $isvisible = moveto_module($cm, $section, $beforemod);
+    echo json_encode(array('visible' => (bool) $isvisible));
 }
index b9af5b1..a89880c 100644 (file)
@@ -25,35 +25,62 @@ Feature: Toggle activities visibility from the course page
       | Description | Test forum description |
       | Visible | Show |
     When I open "Test forum name" actions menu
+    Then "Test forum name" actions menu should not have "Show" item
+    And "Test forum name" actions menu should not have "Make available" item
+    And "Test forum name" actions menu should not have "Make unavailable" item
     And I click on "Hide" "link" in the "Test forum name" activity
-    Then "Test forum name" activity should be hidden
+    And "Test forum name" activity should be hidden
     And I open "Test forum name" actions menu
+    And "Test forum name" actions menu should not have "Hide" item
+    # Stealth behaviour is not available by default:
+    And "Test forum name" actions menu should not have "Make available" item
+    And "Test forum name" actions menu should not have "Make unavailable" item
     And I click on "Show" "link" in the "Test forum name" activity
     And "Test forum name" activity should be visible
     And I open "Test forum name" actions menu
+    And "Test forum name" actions menu should not have "Show" item
+    And "Test forum name" actions menu should not have "Make available" item
+    And "Test forum name" actions menu should not have "Make unavailable" item
     And I click on "Hide" "link" in the "Test forum name" activity
     And "Test forum name" activity should be hidden
     And I reload the page
     And "Test forum name" activity should be hidden
+    # Make sure that "Visible" dropdown in the edit menu has two options: Show/Hide.
+    And I open "Test forum name" actions menu
+    And I click on "Edit settings" "link" in the "Test forum name" activity
+    And I expand all fieldsets
+    And the "Visible" select box should not contain "Hidden from students"
+    And the "Visible" select box should not contain "Available but not displayed on course page"
+    And the "Visible" select box should contain "Show"
+    And the field "Visible" matches value "Hide"
+    And I press "Save and return to course"
+    And "Test forum name" activity should be hidden
+    And I turn editing mode off
+    And "Test forum name" activity should be hidden
     And I log out
+    # Student should not see this activity.
     And I log in as "student1"
     And I follow "Course 1"
-    And "Test forum name" activity should be hidden
+    And I should not see "Test forum name"
+    And I log out
 
   @javascript
-  Scenario: Activities can be shown and hidden inside a hidden section
+  Scenario: Activities can be made available and unavailable inside a hidden section
     Given the following "users" exist:
       | username | firstname | lastname | email |
       | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
     And the following "courses" exist:
       | fullname | shortname | format | numsections |
       | Course 1 | C1 | topics | 2 |
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
     And I log in as "teacher1"
     And I follow "Course 1"
     And I turn editing mode on
+    And I add the "Recent activity" block
     And I add a "Forum" to section "2" and I fill the form with:
       | Forum name | Test forum name |
       | Description | Test forum description |
@@ -61,11 +88,39 @@ Feature: Toggle activities visibility from the course page
     When I hide section "2"
     Then "Test forum name" activity should be hidden
     And I open "Test forum name" actions menu
-    And I click on "Show" "link" in the "Test forum name" activity
-    And "Test forum name" activity should be visible
+    And "Test forum name" actions menu should not have "Show" item
+    And "Test forum name" actions menu should not have "Hide" item
+    And "Test forum name" actions menu should not have "Make unavailable" item
+    And I click on "Make available" "link" in the "Test forum name" activity
+    And "Test forum name" activity should be available but hidden from course page
     And I open "Test forum name" actions menu
-    And I click on "Hide" "link" in the "Test forum name" activity
+    And "Test forum name" actions menu should not have "Show" item
+    And "Test forum name" actions menu should not have "Hide" item
+    And "Test forum name" actions menu should not have "Make available" item
+    And I click on "Make unavailable" "link" in the "Test forum name" activity
+    And "Test forum name" activity should be hidden
+    # Make sure that "Visible" dropdown in the edit menu has three options.
+    And I open "Test forum name" actions menu
+    And I click on "Edit settings" "link" in the "Test forum name" activity
+    And I expand all fieldsets
+    And the "Visible" select box should contain "Hidden from students"
+    And the "Visible" select box should contain "Available but not displayed on course page"
+    And the "Visible" select box should not contain "Hide"
+    And the "Visible" select box should not contain "Show"
+    And I set the field "Visible" to "Available but not displayed on course page"
+    And I press "Save and return to course"
+    And "Test forum name" activity should be available but hidden from course page
+    And I turn editing mode off
+    And "Test forum name" activity should be available but hidden from course page
+    And I log out
+    # Student will not see the module on the course page but can access it from other reports and blocks:
+    And I log in as "student1"
+    And I follow "Course 1"
     And "Test forum name" activity should be hidden
+    And I click on "Test forum name" "link" in the "Recent activity" "block"
+    And I should see "Test forum name"
+    And I should see "(There are no discussion topics yet in this forum)"
+    And I log out
 
   @javascript
   Scenario: Activities can be shown and hidden inside an orphaned section
@@ -88,8 +143,74 @@ Feature: Toggle activities visibility from the course page
     When I click on ".reduce-sections" "css_element"
     Then "Test forum name" activity should be visible
     And I open "Test forum name" actions menu
+    And "Test forum name" actions menu should not have "Show" item
+    And "Test forum name" actions menu should not have "Make available" item
+    And "Test forum name" actions menu should not have "Make unavailable" item
     And I click on "Hide" "link" in the "Test forum name" activity
     And "Test forum name" activity should be hidden
     And I open "Test forum name" actions menu
+    And "Test forum name" actions menu should not have "Hide" item
+    And "Test forum name" actions menu should not have "Make available" item
+    And "Test forum name" actions menu should not have "Make unavailable" item
     And I click on "Show" "link" in the "Test forum name" activity
     And "Test forum name" activity should be visible
+
+  @javascript
+  Scenario: Activities can be made available but not visible on a course page
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format | numsections |
+      | Course 1 | C1        | topics | 2           |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | allowstealth | 1 |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add the "Recent activity" block
+    And I add a "Assignment" to section "2" and I fill the form with:
+      | Assignment name | Test assignment name |
+      | Description | Test assignment description |
+      | Visible | Show |
+    When I open "Test assignment name" actions menu
+    Then "Test assignment name" actions menu should not have "Show" item
+    And "Test assignment name" actions menu should have "Hide" item
+    And "Test assignment name" actions menu should not have "Make available" item
+    And "Test assignment name" actions menu should not have "Make unavailable" item
+    And I click on "Hide" "link" in the "Test assignment name" activity
+    And "Test assignment name" activity should be hidden
+    And I open "Test assignment name" actions menu
+    And "Test assignment name" actions menu should have "Show" item
+    And "Test assignment name" actions menu should not have "Hide" item
+    And "Test assignment name" actions menu should not have "Make unavailable" item
+    And I click on "Make available" "link" in the "Test assignment name" activity
+    And "Test assignment name" activity should be available but hidden from course page
+    # Make sure that "Visible" dropdown in the edit menu has three options.
+    And I open "Test assignment name" actions menu
+    And I click on "Edit settings" "link" in the "Test assignment name" activity
+    And I expand all fieldsets
+    And the "Visible" select box should contain "Show"
+    And the "Visible" select box should contain "Hidden from students"
+    And the "Visible" select box should not contain "Hide"
+    And the field "Visible" matches value "Available but not displayed on course page"
+    And I press "Save and return to course"
+    And "Test assignment name" activity should be available but hidden from course page
+    And I turn editing mode off
+    And "Test assignment name" activity should be available but hidden from course page
+    And I log out
+    # Student will not see the module on the course page but can access it from other reports and blocks:
+    And I log in as "student1"
+    And I follow "Course 1"
+    And "Test assignment name" activity should be hidden
+    And I click on "Test assignment name" "link" in the "Recent activity" "block"
+    And I should see "Test assignment name"
+    And I should see "Submission status"
+    And I log out
index df95cce..237bc7f 100644 (file)
@@ -623,9 +623,67 @@ class behat_course extends behat_base {
                 // All ok.
             }
 
-            // The 'Hide' button should be available.
-            $nohideexception = new ExpectationException('"' . $activityname . '" don\'t have a "' . get_string('hide') . '" icon', $this->getSession());
-            $this->find('named_partial', array('link', get_string('hide')), $nohideexception, $activitynode);
+            // Additional check if this is a teacher in editing mode.
+            if ($this->is_editing_on()) {
+                // The 'Hide' button should be available.
+                $nohideexception = new ExpectationException('"' . $activityname . '" doesn\'t have a "' .
+                    get_string('hide') . '" icon', $this->getSession());
+                $this->find('named_partial', array('link', get_string('hide')), $nohideexception, $activitynode);
+            }
+        }
+    }
+
+    /**
+     * Checks that the specified activity is visible. You need to be in the course page.
+     * It can be used being logged as a student and as a teacher on editing mode.
+     *
+     * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be available but hidden from course page$/
+     * @param string $activityname
+     * @throws ExpectationException
+     */
+    public function activity_should_be_available_but_hidden_from_course_page($activityname) {
+
+        if ($this->is_course_editor()) {
+
+            // The activity must exists and be visible.
+            $activitynode = $this->get_activity_node($activityname);
+
+            // The activity should not be dimmed.
+            try {
+                $xpath = "/descendant-or-self::a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')] | " .
+                    "/descendant-or-self::div[contains(concat(' ', normalize-space(@class), ' '), ' dimmed_text ')]";
+                $this->find('xpath', $xpath, false, $activitynode);
+                throw new ExpectationException('"' . $activityname . '" is hidden', $this->getSession());
+            } catch (ElementNotFoundException $e) {
+                // All ok.
+            }
+
+            // Should has "stealth" class.
+            $exception = new ExpectationException('"' . $activityname . '" does not have CSS class "stealth"', $this->getSession());
+            $xpath = "/descendant-or-self::a[contains(concat(' ', normalize-space(@class), ' '), ' stealth ')]";
+            $this->find('xpath', $xpath, $exception, $activitynode);
+
+            // Additional check if this is a teacher in editing mode.
+            if ($this->is_editing_on()) {
+                // Also has either 'Hide' or 'Make unavailable' edit control.
+                $nohideexception = new ExpectationException('"' . $activityname . '" has neither "' . get_string('hide') .
+                    '" nor "' . get_string('makeunavailable') . '" icons', $this->getSession());
+                try {
+                    $this->find('named_partial', array('link', get_string('hide')), false, $activitynode);
+                } catch (ElementNotFoundException $e) {
+                    $this->find('named_partial', array('link', get_string('makeunavailable')), $nohideexception, $activitynode);
+                }
+            }
+
+        } else {
+
+            // Student should not see the activity at all.
+            try {
+                $this->get_activity_node($activityname);
+                throw new ExpectationException('The "' . $activityname . '" should not appear', $this->getSession());
+            } catch (ElementNotFoundException $e) {
+                // This is good, the activity should not be there.
+            }
         }
     }
 
@@ -649,15 +707,23 @@ class behat_course extends behat_base {
                      "/descendant-or-self::div[contains(concat(' ', normalize-space(@class), ' '), ' dimmed_text ')]";
             $this->find('xpath', $xpath, $exception, $activitynode);
 
-            // Also 'Show' icon.
-            $noshowexception = new ExpectationException('"' . $activityname . '" don\'t have a "' . get_string('show') . '" icon', $this->getSession());
-            $this->find('named_partial', array('link', get_string('show')), $noshowexception, $activitynode);
+            // Additional check if this is a teacher in editing mode.
+            if ($this->is_editing_on()) {
+                // Also has either 'Show' or 'Make available' edit control.
+                $noshowexception = new ExpectationException('"' . $activityname . '" has neither "' . get_string('show') .
+                    '" nor "' . get_string('makeavailable') . '" icons', $this->getSession());
+                try {
+                    $this->find('named_partial', array('link', get_string('show')), false, $activitynode);
+                } catch (ElementNotFoundException $e) {
+                    $this->find('named_partial', array('link', get_string('makeavailable')), $noshowexception, $activitynode);
+                }
+            }
 
         } else {
 
             // It should not exist at all.
             try {
-                $this->find_link($activityname);
+                $this->get_activity_node($activityname);
                 throw new ExpectationException('The "' . $activityname . '" should not appear', $this->getSession());
             } catch (ElementNotFoundException $e) {
                 // This is good, the activity should not be there.
@@ -666,6 +732,26 @@ class behat_course extends behat_base {
 
     }
 
+    /**
+     * Checks that the specified activity is dimmed. You need to be in the course page.
+     *
+     * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be dimmed$/
+     * @param string $activityname
+     * @throws ExpectationException
+     */
+    public function activity_should_be_dimmed($activityname) {
+
+        // The activity should exist.
+        $activitynode = $this->get_activity_node($activityname);
+
+        // Should be hidden.
+        $exception = new ExpectationException('"' . $activityname . '" is not dimmed', $this->getSession());
+        $xpath = "/descendant-or-self::a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')] | ".
+            "/descendant-or-self::div[contains(concat(' ', normalize-space(@class), ' '), ' dimmed_text ')]";
+        $this->find('xpath', $xpath, $exception, $activitynode);
+
+    }
+
     /**
      * Moves the specified activity to the first slot of a section. This step is experimental when using it in Javascript tests. Editing mode should be on.
      *
@@ -803,6 +889,42 @@ class behat_course extends behat_base {
         }
     }
 
+    /**
+     * Checks that the specified activity's action menu contains an item.
+     *
+     * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" actions menu should have "(?P<menu_item_string>(?:[^"]|\\")*)" item$/
+     * @throws DriverException The step is not available when Javascript is disabled
+     * @param string $activityname
+     * @param string $menuitem
+     */
+    public function actions_menu_should_have_item($activityname, $menuitem) {
+        $activitynode = $this->get_activity_node($activityname);
+
+        $notfoundexception = new ExpectationException('"' . $activityname . '" doesn\'t have a "' .
+            $menuitem . '" item', $this->getSession());
+        $this->find('named_partial', array('link', $menuitem), $notfoundexception, $activitynode);
+    }
+
+    /**
+     * Checks that the specified activity's action menu does not contains an item.
+     *
+     * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" actions menu should not have "(?P<menu_item_string>(?:[^"]|\\")*)" item$/
+     * @throws DriverException The step is not available when Javascript is disabled
+     * @param string $activityname
+     * @param string $menuitem
+     */
+    public function actions_menu_should_not_have_item($activityname, $menuitem) {
+        $activitynode = $this->get_activity_node($activityname);
+
+        try {
+            $this->find('named_partial', array('link', $menuitem), false, $activitynode);
+            throw new ExpectationException('"' . $activityname . '" has a "' . $menuitem .
+                '" item when it should not', $this->getSession());
+        } catch (ElementNotFoundException $e) {
+            // This is good, the menu item should not be there.
+        }
+    }
+
     /**
      * Indents to the right the activity or resource specified by it's name. Editing mode should be on.
      *
@@ -1159,6 +1281,15 @@ class behat_course extends behat_base {
         return true;
     }
 
+    /**
+     * Returns whether the user can edit the course contents and the editing mode is on.
+     *
+     * @return bool
+     */
+    protected function is_editing_on() {
+        return $this->getSession()->getPage()->findButton(get_string('turneditingoff')) ? true : false;
+    }
+
     /**
      * Returns the id of the category with the given idnumber.
      *
index e67ec73..c726278 100644 (file)
@@ -201,6 +201,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $moduleinfo->course = $course->id;
         $moduleinfo->groupingid = $grouping->id;
         $moduleinfo->visible = true;
+        $moduleinfo->visibleoncoursepage = true;
 
         // Sometimes optional generic values for some modules.
         $moduleinfo->name = 'My test module';
@@ -436,6 +437,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $moduleinfo->course = $course->id;
         $moduleinfo->groupingid = $grouping->id;
         $moduleinfo->visible = true;
+        $moduleinfo->visibleoncoursepage = true;
 
         // Sometimes optional generic values for some modules.
         $moduleinfo->name = 'My test module';
index 7d732b0..cc232a0 100644 (file)
@@ -1680,7 +1680,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
 
         $this->assertCount(0, $result['warnings']);
         // Test we retrieve all the fields.
-        $this->assertCount(27, $result['cm']);
+        $this->assertCount(28, $result['cm']);
         $this->assertEquals($record['name'], $result['cm']['name']);
         $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
         $this->assertEquals(100, $result['cm']['grade']);
@@ -1747,7 +1747,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
 
         $this->assertCount(0, $result['warnings']);
         // Test we retrieve all the fields.
-        $this->assertCount(22, $result['cm']);
+        $this->assertCount(23, $result['cm']);
         $this->assertEquals($record['name'], $result['cm']['name']);
         $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
 
index 3d5c64c..f4b607c 100644 (file)
@@ -99,6 +99,7 @@ class core_course_modlib_testcase extends advanced_testcase {
         $expecteddata->coursemodule       = $assigncm->id;
         $expecteddata->section            = $cw->section;
         $expecteddata->visible            = $assigncm->visible;
+        $expecteddata->visibleoncoursepage = $assigncm->visibleoncoursepage;
         $expecteddata->cmidnumber         = $assigncm->idnumber;
         $expecteddata->groupmode          = groups_get_activity_groupmode($cm);
         $expecteddata->groupingid         = $assigncm->groupingid;
index b47c80f..8eed14d 100644 (file)
Binary files a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js and b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js differ
index 65266a5..1dc7922 100644 (file)
Binary files a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js and b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js differ
index bd057c8..0796cae 100644 (file)
Binary files a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js and b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js differ
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
deleted file mode 100644 (file)
index 2465ecc..0000000
Binary files a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js and /dev/null 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
deleted file mode 100644 (file)
index 9a0d330..0000000
Binary files a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js and /dev/null 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
deleted file mode 100644 (file)
index 2465ecc..0000000
Binary files a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js and /dev/null differ
index 1b0be26..f8c4e51 100644 (file)
@@ -108,7 +108,8 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, {
             // Replace move icons
             var move = resourcesnode.one('a.' + CSS.EDITINGMOVE);
             if (move) {
-                move.replace(this.resourcedraghandle.cloneNode(true));
+                var sr = move.getData('sr');
+                move.replace(this.resourcedraghandle.cloneNode(true).setAttribute('data-sectionreturn', sr));
             }
         }, this);
     },
diff --git a/course/yui/src/toolboxes/build.json b/course/yui/src/toolboxes/build.json
deleted file mode 100644 (file)
index ffd5939..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-    "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
deleted file mode 100644 (file)
index 677836f..0000000
+++ /dev/null
@@ -1,552 +0,0 @@
-/* global TOOLBOX, BODY, SELECTOR, INDENTLIMITS */
-
-/**
- * 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,
-
-    /**
-     * 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.
-     * @return {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 '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.
-                this.change_groupmode(ev, node, activity, action);
-                break;
-            case 'move':
-            case 'update':
-            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