Merge branch 'wip-MDL-57113-master' of https://github.com/kwiliarty/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 20 Feb 2017 23:59:54 +0000 (00:59 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 20 Feb 2017 23:59:54 +0000 (00:59 +0100)
194 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/usertours/tests/manager_test.php
auth/classes/output/login.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.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
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/classes/management_renderer.php
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/behat/course_search.feature
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/lti/tests/helper_test.php
filter/mediaplugin/filter.php
install/lang/es_mx/error.php
install/lang/fa/error.php
install/lang/lv/admin.php
install/lang/lv/install.php
install/lang/sr_cr/install.php
install/lang/sr_lt/install.php
lang/en/admin.php
lang/en/deprecated.txt
lang/en/moodle.php
lib/amd/build/ajax.min.js
lib/amd/build/form-autocomplete.min.js
lib/amd/build/str.min.js
lib/amd/build/templates.min.js
lib/amd/src/ajax.js
lib/amd/src/form-autocomplete.js
lib/amd/src/str.js
lib/amd/src/templates.js
lib/classes/event/base.php
lib/classes/event/user_graded.php
lib/classes/external/exporter.php
lib/classes/files/curl_security_helper.php
lib/classes/notification.php
lib/classes/persistent.php
lib/classes/session/manager.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/db/upgradelib.php
lib/deprecatedlib.php
lib/editor/tinymce/plugins/moodlemedia/preview.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/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/exporter_test.php
lib/tests/medialib_test.php
lib/tests/persistent_test.php
lib/tests/upgradelib_test.php
lib/tests/weblib_test.php
lib/upgrade.txt
lib/weblib.php
login/lib.php
login/set_password_form.php
media/classes/manager.php
media/player/videojs/tests/behat/modules.feature [new file with mode: 0644]
media/player/videojs/tests/fixtures/test.mov [new file with mode: 0644]
media/upgrade.txt [new file with mode: 0644]
message/classes/api.php
message/lib.php
message/output/popup/db/install.xml
message/output/popup/db/upgrade.php
message/output/popup/tests/externallib_test.php
message/output/popup/version.php
message/tests/api_test.php
message/tests/externallib_test.php
message/tests/messagelib_test.php
mod/assign/amd/build/grading_panel.min.js
mod/assign/amd/src/grading_panel.js
mod/assign/feedback/editpdf/classes/document_services.php
mod/assign/feedback/editpdf/classes/pdf.php
mod/assign/feedback/editpdf/tests/editpdf_test.php
mod/assign/feedback/file/locallib.php
mod/assign/gradingtable.php
mod/assign/locallib.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/classes/output/big_search_form.php
mod/forum/externallib.php
mod/forum/search.php
mod/forum/templates/big_search_form.mustache
mod/glossary/import.php
mod/label/classes/external.php
mod/label/tests/behat/label_visibility.feature
mod/label/tests/externallib_test.php
mod/lesson/locallib.php
mod/page/classes/external.php
mod/page/tests/externallib_test.php
mod/quiz/tests/behat/editing_section_headings.feature
mod/resource/classes/external.php
mod/resource/locallib.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/locallib.php
mod/url/tests/externallib_test.php
my/lib.php
question/format/blackboard_six/formatbase.php
question/type/gapselect/questionbase.php
question/type/match/question.php
question/type/multichoice/question.php
question/type/questionbase.php
question/type/upgrade.txt
repository/lib.php
rss/file.php
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/templates/core/availability_info.mustache [new file with mode: 0644]
theme/boost/templates/core/login.mustache
theme/boost/templates/mod_forum/big_search_form.mustache
theme/boost/tests/behat/behat_theme_boost_behat_action_menu.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/tests/externallib_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..793205d 100644 (file)
@@ -117,7 +117,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 321ff06..bffecb3 100644 (file)
@@ -118,7 +118,7 @@ class tool_usertours_manager_testcase extends advanced_testcase {
         $rcm = $rc->getMethod($function);
         $rcm->setAccessible(true);
 
-        $this->setExpectedException('moodle_exception', 'A required parameter (sesskey) was missing');
+        $this->expectException('moodle_exception');
         $rcm->invokeArgs($manager, $arguments);
     }
 
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 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 e07b50f..b7e4ef7 100644 (file)
@@ -2670,7 +2670,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 +2688,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 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 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 9f0afc4..8701f0b 100644 (file)
@@ -422,7 +422,7 @@ class core_course_management_renderer extends plugin_renderer_base {
                     'resortcategoriesby',
                     'name',
                     false,
-                    array('aria-label' => get_string('selectcategorysortby'))
+                    array('aria-label' => get_string('selectcategorysortby'), 'class' => 'm-t-1')
                 )
             );
             $form .= html_writer::div(
@@ -441,15 +441,16 @@ class core_course_management_renderer extends plugin_renderer_base {
                     'resortcoursesby',
                     'fullname',
                     false,
-                    array('aria-label' => get_string('selectcoursesortby'))
+                    array('aria-label' => get_string('selectcoursesortby'), 'class' => 'm-t-1')
                 )
             );
-            $form .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'bulksort', 'value' => get_string('sort')));
+            $form .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'bulksort',
+                'value' => get_string('sort'), 'class' => 'btn btn-secondary m-y-1'));
             $form .= html_writer::end_div();
 
-            $html .= html_writer::start_div('detail-pair row yui3-g');
-            $html .= html_writer::div(html_writer::span(get_string('sorting')), 'pair-key span3 yui3-u-1-4');
-            $html .= html_writer::div($form, 'pair-value span9 yui3-u-3-4');
+            $html .= html_writer::start_div('detail-pair row yui3-g m-y-1');
+            $html .= html_writer::div(html_writer::span(get_string('sorting')), 'pair-key span3 col-md-3 yui3-u-1-4');
+            $html .= html_writer::div($form, 'pair-value span9 col-md-9 yui3-u-3-4');
             $html .= html_writer::end_div();
         }
         if (coursecat::can_change_parent_any()) {
@@ -463,9 +464,10 @@ class core_course_management_renderer extends plugin_renderer_base {
                 'movecategoriesto',
                 '',
                 array('' => 'choosedots'),
-                array('aria-labelledby' => 'moveselectedcategoriesto')
+                array('aria-labelledby' => 'moveselectedcategoriesto', 'class' => 'm-r-1')
             );
-            $submit = array('type' => 'submit', 'name' => 'bulkmovecategories', 'value' => get_string('move'));
+            $submit = array('type' => 'submit', 'name' => 'bulkmovecategories', 'value' => get_string('move'),
+                'class' => 'btn btn-secondary');
             $html .= $this->detail_pair(
                 html_writer::span(get_string('moveselectedcategoriesto'), '', array('id' => 'moveselectedcategoriesto')),
                 $select . html_writer::empty_tag('input', $submit)
@@ -780,9 +782,10 @@ class core_course_management_renderer extends plugin_renderer_base {
                 'movecoursesto',
                 '',
                 array('' => 'choosedots'),
-                array('aria-labelledby' => 'moveselectedcoursesto')
+                array('aria-labelledby' => 'moveselectedcoursesto', 'class' => 'm-r-1')
             );
-            $submit = array('type' => 'submit', 'name' => 'bulkmovecourses', 'value' => get_string('move'));
+            $submit = array('type' => 'submit', 'name' => 'bulkmovecourses', 'value' => get_string('move'),
+                'class' => 'btn btn-secondary');
             $html .= $this->detail_pair(
                 html_writer::span(get_string('moveselectedcoursesto'), '', array('id' => 'moveselectedcoursesto')),
                 $select . html_writer::empty_tag('input', $submit)
@@ -808,7 +811,8 @@ class core_course_management_renderer extends plugin_renderer_base {
             array('' => 'choosedots'),
             array('aria-labelledby' => 'moveselectedcoursesto')
         );
-        $submit = array('type' => 'submit', 'name' => 'bulkmovecourses', 'value' => get_string('move'));
+        $submit = array('type' => 'submit', 'name' => 'bulkmovecourses', 'value' => get_string('move'),
+            'class' => 'btn btn-secondary');
         $html .= $this->detail_pair(
             html_writer::span(get_string('moveselectedcoursesto'), '', array('id' => 'moveselectedcoursesto')),
             $select . html_writer::empty_tag('input', $submit)
@@ -847,8 +851,8 @@ class core_course_management_renderer extends plugin_renderer_base {
      */
     protected function detail_pair($key, $value, $class ='') {
         $html = html_writer::start_div('detail-pair row yui3-g '.preg_replace('#[^a-zA-Z0-9_\-]#', '-', $class));
-        $html .= html_writer::div(html_writer::span($key), 'pair-key span3 yui3-u-1-4');
-        $html .= html_writer::div(html_writer::span($value), 'pair-value span9 yui3-u-3-4');
+        $html .= html_writer::div(html_writer::span($key), 'pair-key span3 col-md-3 yui3-u-1-4');
+        $html .= html_writer::div(html_writer::span($value), 'pair-value span9 col-md-9 m-b-1 yui3-u-3-4');
         $html .= html_writer::end_div();
         return $html;
     }
@@ -946,7 +950,7 @@ class core_course_management_renderer extends plugin_renderer_base {
     public function grid_column_start($size, $id = null, $class = null) {
 
         // Calculate Bootstrap grid sizing.
-        $bootstrapclass = 'span'.$size;
+        $bootstrapclass = 'span'.$size.' col-md-'.$size;
 
         // Calculate YUI grid sizing.
         if ($size === 12) {
@@ -1322,13 +1326,14 @@ class core_course_management_renderer extends plugin_renderer_base {
         $strsearchcourses = get_string("searchcourses");
         $searchurl = new moodle_url('/course/management.php');
 
-        $output = html_writer::start_tag('form', array('id' => $formid, 'action' => $searchurl, 'method' => 'get'));
-        $output .= html_writer::start_tag('fieldset', array('class' => 'coursesearchbox invisiblefieldset'));
-        $output .= html_writer::tag('label', $strsearchcourses.': ', array('for' => $inputid));
-        $output .= html_writer::empty_tag('input', array('type' => 'text', 'id' => $inputid,
-            'size' => $inputsize, 'name' => 'search', 'value' => s($value)));
-        $output .= html_writer::empty_tag('input', array('type' => 'submit',
-            'value' => get_string('go')));
+        $output = html_writer::start_tag('form', array('id' => $formid, 'action' => $searchurl, 'method' => 'get',
+            'class' => 'form-inline'));
+        $output .= html_writer::start_tag('fieldset', array('class' => 'coursesearchbox invisiblefieldset m-y-1'));
+        $output .= html_writer::tag('label', $strsearchcourses, array('for' => $inputid));
+        $output .= html_writer::empty_tag('input', array('type' => 'text', 'id' => $inputid, 'size' => $inputsize,
+            'name' => 'search', 'value' => s($value), 'class' => 'form-control m-x-1'));
+        $output .= html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('go'),
+            'class' => 'btn btn-secondary'));
         $output .= html_writer::end_tag('fieldset');
         $output .= html_writer::end_tag('form');
 
index d509033..c97800e 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'),
@@ -2558,6 +2561,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 +3239,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 5dfda56..3ffbe6e 100644 (file)
@@ -20,7 +20,7 @@ Feature: Courses can be searched for and moved in bulk.
   Scenario: Search courses finds correct results
     Given I log in as "admin"
     And I go to the courses management page
-    When I set the field "Search courses:" to "Biology"
+    When I set the field "Search courses" to "Biology"
     And I press "Go"
     Then I should see "Biology Y1"
     And I should see "Biology Y2"
@@ -31,7 +31,7 @@ Feature: Courses can be searched for and moved in bulk.
   Scenario: Search courses and move results in bulk
     Given I log in as "admin"
     And I go to the courses management page
-    And I set the field "Search courses:" to "Biology"
+    And I set the field "Search courses" to "Biology"
     And I press "Go"
     When I select course "Biology Y1" in the management interface
     And I select course "Biology Y2" in the management interface
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 or resource.
-     *
-     * @method change_indent
-     * @protected
-     * @param {EventFacade} ev The event that was fired.
-     * @param {Node} button The button that triggered this action.
-     * @param {Node} activity The activity node that this action will be performed on.
-     * @param {String} action The action that has been requested. Will be 'moveleft' or 'moveright'.
-     */
-    change_indent: function(ev, button, activity, action) {
-        // Prevent the default button action
-        ev.preventDefault();
-
-        var direction = (action === 'moveleft') ? -1 : 1;
-
-        // And we need to determine the current and new indent level
-        var indentdiv = activity.one(SELECTOR.MODINDENTDIV),
-            indent = indentdiv.getAttribute('class').match(/mod-indent-(\d{1,})/),
-            oldindent = 0,
-            newindent;
-
-        if (indent) {
-            oldindent = parseInt(indent[1], 10);
-        }
-        newindent = oldindent + parseInt(direction, 10);
-
-        if (newindent < INDENTLIMITS.MIN || newindent > INDENTLIMITS.MAX) {
-            return;
-        }
-
-        if (indent) {
-            indentdiv.removeClass(indent[0]);
-        }
-
-        // Perform the move
-        indentdiv.addClass(CSS.MODINDENTCOUNT + newindent);
-        var data = {
-            'class': 'resource',
-            'field': 'indent',
-            'value': newindent,
-            'id': Y.Moodle.core_course.util.cm.getId(activity)
-        };
-        var spinner = this.add_spinner(activity);
-        this.send_request(data, spinner);
-
-        var remainingmove;
-
-        // Handle removal/addition of the moveleft button.
-        if (newindent === INDENTLIMITS.MIN) {
-            button.addClass('hidden');
-            remainingmove = activity.one('.editing_moveright');
-        } else if (newindent > INDENTLIMITS.MIN && oldindent === INDENTLIMITS.MIN) {
-            button.ancestor('.menu').one('[data-action=moveleft]').removeClass('hidden');
-        }
-
-        if (newindent === INDENTLIMITS.MAX) {
-            button.addClass('hidden');
-            remainingmove = activity.one('.editing_moveleft');
-        } else if (newindent < INDENTLIMITS.MAX && oldindent === INDENTLIMITS.MAX) {
-            button.ancestor('.menu').one('[data-action=moveright]').removeClass('hidden');
-        }
-
-        // Handle massive indentation to match non-ajax display
-        var hashugeclass = indentdiv.hasClass(CSS.MODINDENTHUGE);
-        if (newindent > 15 && !hashugeclass) {
-            indentdiv.addClass(CSS.MODINDENTHUGE);
-        } else if (newindent <= 15 && hashugeclass) {
-            indentdiv.removeClass(CSS.MODINDENTHUGE);
-        }
-
-        if (ev.type && ev.type === "key" && remainingmove) {
-            remainingmove.focus();
-        }
-    },
-
-    /**
-     * Deletes the given activity or resource after confirmation.
-     *
-     * @protected
-     * @method delete_with_confirmation
-     * @param {EventFacade} ev The event that was fired.
-     * @param {Node} button The button that triggered this action.
-     * @param {Node} activity The activity node that this action will be performed on.
-     * @chainable
-     */
-    delete_with_confirmation: function(ev, button, activity) {
-        // Prevent the default button action
-        ev.preventDefault();
-
-        // Get the element we're working on
-        var element = activity,
-            // Create confirm string (different if element has or does not have name)
-            confirmstring = '',
-            plugindata = {
-                type: M.util.get_string('pluginname', element.getAttribute('class').match(/modtype_([^\s]*)/)[1])
-            };
-        if (Y.Moodle.core_course.util.cm.getName(element) !== null) {
-            plugindata.name = Y.Moodle.core_course.util.cm.getName(element);
-            confirmstring = M.util.get_string('deletechecktypename', 'moodle', plugindata);
-        } else {
-            confirmstring = M.util.get_string('deletechecktype', 'moodle', plugindata);
-        }
-
-        // Create the confirmation dialogue.
-        var confirm = new M.core.confirm({
-            question: confirmstring,
-            modal: true,
-            visible: false
-        });
-        confirm.show();
-
-        // If it is confirmed.
-        confirm.on('complete-yes', function() {
-
-            // Actually remove the element.
-            element.remove();
-            var data = {
-                'class': 'resource',
-                'action': 'DELETE',
-                'id': Y.Moodle.core_course.util.cm.getId(element)
-            };
-            this.send_request(data);
-            if (M.core.actionmenu && M.core.actionmenu.instance) {
-                M.core.actionmenu.instance.hideMenu(ev);
-            }
-
-        }, this);
-
-        return this;
-    },
-
-    /**
-     * Duplicates the activity.
-     *
-     * @method duplicate
-     * @protected
-     * @param {EventFacade} ev The event that was fired.
-     * @param {Node} button The button that triggered this action.
-     * @param {Node} activity The activity node that this action will be performed on.
-     * @chainable
-     */
-    duplicate: function(ev, button, activity) {
-        // Prevent the default button action
-        ev.preventDefault();
-
-        // Get the element we're working on
-        var element = activity;
-
-        // Add the lightbox.
-        var section = activity.ancestor(M.course.format.get_section_selector(Y)),
-            lightbox = M.util.add_lightbox(Y, section).show();
-
-        // Build and send the request.
-        var data = {
-            'class': 'resource',
-            'field': 'duplicate',
-            'id': Y.Moodle.core_course.util.cm.getId(element),
-            'sr': button.getData('sr')
-        };
-        this.send_request(data, lightbox, function(response) {
-            var newcm = Y.Node.create(response.fullcontent);
-
-            // Append to the section?
-            activity.insert(newcm, 'after');
-            Y.use('moodle-course-coursebase', function() {
-                M.course.coursebase.invoke_function('setup_for_resource', newcm);
-            });
-            if (M.core.actionmenu && M.core.actionmenu.newDOMNode) {
-                M.core.actionmenu.newDOMNode(newcm);
-            }
-        });
-        return this;
-    },
-
-    /**
-     * Changes the visibility of this activity or resource.
-     *
-     * @method change_visibility
-     * @protected
-     * @param {EventFacade} ev The event that was fired.
-     * @param {Node} button The button that triggered this action.
-     * @param {Node} activity The activity node that this action will be performed on.
-     * @param {String} action The action that has been requested.
-     * @chainable
-     */
-    change_visibility: function(ev, button, activity, action) {
-        // Prevent the default button action
-        ev.preventDefault();
-
-        // Get the element we're working on
-        var element = activity;
-        var value = this.handle_resource_dim(button, activity, action);
-
-        // Send the request
-        var data = {
-            'class': 'resource',
-            'field': 'visible',
-            'value': value,
-            'id': Y.Moodle.core_course.util.cm.getId(element)
-        };
-        var spinner = this.add_spinner(element);
-        this.send_request(data, spinner);
-
-        return this;
-    },
-
-    /**
-     * Handles the UI aspect of dimming the activity or resource.
-     *
-     * @method handle_resource_dim
-     * @protected
-     * @param {Node} button The button that triggered the action.
-     * @param {Node} activity The activity node that this action will be performed on.
-     * @param {String} action 'show' or 'hide'.
-     * @return {Number} 1 if we changed to visible, 0 if we were hiding.
-     */
-    handle_resource_dim: function(button, activity, action) {
-        var toggleclass = CSS.DIMCLASS,
-            dimarea = activity.one([
-                    SELECTOR.ACTIVITYLINK,
-                    SELECTOR.CONTENTWITHOUTLINK
-                ].join(', ')),
-            availabilityinfo = activity.one(CSS.AVAILABILITYINFODIV),
-            nextaction = (action === 'hide') ? 'show' : 'hide',
-            buttontext = button.one('span'),
-            newstring = M.util.get_string(nextaction, 'moodle'),
-            buttonimg = button.one('img');
-
-        // Update button info.
-        buttonimg.setAttrs({
-            'src': M.util.image_url('t/' + nextaction)
-        });
-
-        if (Y.Lang.trim(button.getAttribute('title'))) {
-            button.setAttribute('title', newstring);
-        }
-
-        if (Y.Lang.trim(buttonimg.getAttribute('alt'))) {
-            buttonimg.setAttribute('alt', newstring);
-        }
-
-        button.replaceClass('editing_' + action, 'editing_' + nextaction);
-        button.setData('action', nextaction);
-        if (buttontext) {
-            buttontext.set('text', newstring);
-        }
-
-        if (activity.one(SELECTOR.CONTENTWITHOUTLINK)) {
-            dimarea = activity.one(SELECTOR.CONTENTWITHOUTLINK);
-            toggleclass = CSS.DIMMEDTEXT;
-        }
-
-        // If activity is conditionally hidden, then don't toggle.
-        if (!dimarea.hasClass(CSS.CONDITIONALHIDDEN)) {
-            if (action === 'hide') {
-                // Change the UI.
-                dimarea.addClass(toggleclass);
-                // We need to toggle dimming on the description too.
-                activity.all(SELECTOR.CONTENTAFTERLINK).addClass(CSS.DIMMEDTEXT);
-                activity.all(SELECTOR.GROUPINGLABEL).addClass(CSS.DIMMEDTEXT);
-            } else {
-                // Change the UI.
-                dimarea.removeClass(toggleclass);
-                // We need to toggle dimming on the description too.
-                activity.all(SELECTOR.CONTENTAFTERLINK).removeClass(CSS.DIMMEDTEXT);
-                activity.all(SELECTOR.GROUPINGLABEL).removeClass(CSS.DIMMEDTEXT);
-            }
-        }
-        // Toggle availablity info for conditional activities.
-        if (availabilityinfo) {
-            availabilityinfo.toggleClass(CSS.HIDE);
-        }
-        return (action === 'hide') ? 0 : 1;
-    },
-
-    /**
-     * Changes the groupmode of the activity to the next groupmode in the sequence.
-     *
-     * @method change_groupmode
-     * @protected
-     * @param {EventFacade} ev The event that was fired.
-     * @param {Node} button The button that triggered this action.
-     * @param {Node} activity The activity node that this action will be performed on.
-     * @chainable
-     */
-    change_groupmode: function(ev, button, activity) {
-        // Prevent the default button action.
-        ev.preventDefault();
-
-        // Current Mode
-        var groupmode = parseInt(button.getData('nextgroupmode'), 10),
-            newtitle = '',
-            iconsrc = '',
-            newtitlestr,
-            data,
-            spinner,
-            nextgroupmode = groupmode + 1,
-            buttonimg = button.one('img');
-
-        if (nextgroupmode > 2) {
-            nextgroupmode = 0;
-        }
-
-        if (groupmode === this.GROUPS_NONE) {
-            newtitle = 'groupsnone';
-            iconsrc = M.util.image_url('i/groupn', 'moodle');
-        } else if (groupmode === this.GROUPS_SEPARATE) {
-            newtitle = 'groupsseparate';
-            iconsrc = M.util.image_url('i/groups', 'moodle');
-        } else if (groupmode === this.GROUPS_VISIBLE) {
-            newtitle = 'groupsvisible';
-            iconsrc = M.util.image_url('i/groupv', 'moodle');
-        }
-        newtitlestr = M.util.get_string('clicktochangeinbrackets', 'moodle', M.util.get_string(newtitle, 'moodle'));
-
-        // Change the UI
-        var oldAction = button.getData('action');
-        button.replaceClass('editing_' + oldAction, 'editing_' + newtitle);
-        buttonimg.setAttrs({
-            'src': iconsrc
-        });
-        if (Y.Lang.trim(button.getAttribute('title'))) {
-            button.setAttribute('title', newtitlestr).setData('action', newtitle).setData('nextgroupmode', nextgroupmode);
-        }
-
-        if (Y.Lang.trim(buttonimg.getAttribute('alt'))) {
-            buttonimg.setAttribute('alt', newtitlestr);
-        }
-
-        // And send the request
-        data = {
-            'class': 'resource',
-            'field': 'groupmode',
-            'value': groupmode,
-            'id': Y.Moodle.core_course.util.cm.getId(activity)
-        };
-
-        spinner = this.add_spinner(activity);
-        this.send_request(data, spinner);
-        return this;
-    },
-
-    /**
-     * Set the visibility of the specified resource to match the visible parameter.
-     *
-     * Note: This is not a toggle function and only changes the visibility
-     * in the browser (no ajax update is performed).
-     *
-     * @method set_visibility_resource_ui
-     * @param {object} args An object containing the required information to trigger a change.
-     * @param {Node} args.element The resource to toggle
-     * @param {Boolean} args.visible The target visibility
-     */
-    set_visibility_resource_ui: function(args) {
-        var element = args.element,
-            buttonnode = element.one(SELECTOR.HIDE),
-            // By default we assume that the item is visible and we're going to hide it.
-            currentVisibility = true,
-            targetVisibility = false;
-
-        if (!buttonnode) {
-            // If the buttonnode was not found, try to find the HIDE button
-            // and change the target visibility setting to false.
-            buttonnode = element.one(SELECTOR.SHOW);
-            currentVisibility = false;
-            targetVisibility = true;
-        }
-
-        if (typeof args.visible !== 'undefined') {
-            // If we were provided with a visibility argument, use that instead.
-            targetVisibility = args.visible;
-        }
-
-        // Only trigger a change if necessary.
-        if (currentVisibility !== targetVisibility) {
-            var action = 'hide';
-            if (targetVisibility) {
-                action = 'show';
-            }
-
-            this.handle_resource_dim(buttonnode, element, action);
-        }
-    }
-}, {
-    NAME: 'course-resource-toolbox',
-    ATTRS: {
-    }
-});
-
-M.course.resource_toolbox = null;
-M.course.init_resource_toolbox = function(config) {
-    M.course.resource_toolbox = new RESOURCETOOLBOX(config);
-    return M.course.resource_toolbox;
-};
diff --git a/course/yui/src/toolboxes/js/section.js b/course/yui/src/toolboxes/js/section.js
deleted file mode 100644 (file)
index dab7a44..0000000
+++ /dev/null
@@ -1,189 +0,0 @@
-/* global SELECTOR, TOOLBOX */
-
-/**
- * Resource and activity toolbox class.
- *
- * This class is responsible for managing AJAX interactions with activities and resources
- * when viewing a course in editing mode.
- *
- * @module moodle-course-toolboxes
- * @namespace M.course.toolboxes
- */
-
-/**
- * Section toolbox class.
- *
- * This class is responsible for managing AJAX interactions with sections
- * when viewing a course in editing mode.
- *
- * @class section
- * @constructor
- * @extends M.course.toolboxes.toolbox
- */
-var SECTIONTOOLBOX = function() {
-    SECTIONTOOLBOX.superclass.constructor.apply(this, arguments);
-};
-
-Y.extend(SECTIONTOOLBOX, TOOLBOX, {
-    /**
-     * Initialize the section toolboxes module.
-     *
-     * Updates all span.commands with relevant handlers and other required changes.
-     *
-     * @method initializer
-     * @protected
-     */
-    initializer: function() {
-        M.course.coursebase.register_module(this);
-
-        // Section Highlighting.
-        Y.delegate('click', this.toggle_highlight, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.HIGHLIGHT, this);
-
-        // Section Visibility.
-        Y.delegate('click', this.toggle_hide_section, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.SHOWHIDE, this);
-    },
-
-    toggle_hide_section: function(e) {
-        // Prevent the default button action.
-        e.preventDefault();
-
-        // Get the section we're working on.
-        var section = e.target.ancestor(M.course.format.get_section_selector(Y)),
-            button = e.target.ancestor('a', true),
-            hideicon = button.one('img'),
-            buttontext = button.one('span'),
-
-        // The value to submit
-            value,
-
-        // The text for strings and images. Also determines the icon to display.
-            action,
-            nextaction;
-
-        if (!section.hasClass(CSS.SECTIONHIDDENCLASS)) {
-            section.addClass(CSS.SECTIONHIDDENCLASS);
-            value = 0;
-            action = 'hide';
-            nextaction = 'show';
-        } else {
-            section.removeClass(CSS.SECTIONHIDDENCLASS);
-            value = 1;
-            action = 'show';
-            nextaction = 'hide';
-        }
-
-        var newstring = M.util.get_string(nextaction + 'fromothers', 'format_' + this.get('format'));
-        hideicon.setAttrs({
-            'alt': newstring,
-            'src': M.util.image_url('i/' + nextaction)
-        });
-        button.set('title', newstring);
-        if (buttontext) {
-            buttontext.set('text', newstring);
-        }
-
-        // Change the show/hide status
-        var data = {
-            'class': 'section',
-            'field': 'visible',
-            'id': Y.Moodle.core_course.util.section.getId(section.ancestor(M.course.format.get_section_wrapper(Y), true)),
-            'value': value
-        };
-
-        var lightbox = M.util.add_lightbox(Y, section);
-        lightbox.show();
-
-        this.send_request(data, lightbox, function(response) {
-            var activities = section.all(SELECTOR.ACTIVITYLI);
-            activities.each(function(node) {
-                var button;
-                if (node.one(SELECTOR.SHOW)) {
-                    button = node.one(SELECTOR.SHOW);
-                } else {
-                    button = node.one(SELECTOR.HIDE);
-                }
-                var activityid = Y.Moodle.core_course.util.cm.getId(node);
-
-                // NOTE: resourcestotoggle is returned as a string instead
-                // of a Number so we must cast our activityid to a String.
-                if (Y.Array.indexOf(response.resourcestotoggle, "" + activityid) !== -1) {
-                    M.course.resource_toolbox.handle_resource_dim(button, node, action);
-                }
-            }, this);
-        });
-    },
-
-    /**
-     * Toggle highlighting the current section.
-     *
-     * @method toggle_highlight
-     * @param {EventFacade} e
-     */
-    toggle_highlight: function(e) {
-        // Prevent the default button action.
-        e.preventDefault();
-
-        // Get the section we're working on.
-        var section = e.target.ancestor(M.course.format.get_section_selector(Y));
-        var button = e.target.ancestor('a', true);
-        var buttonicon = button.one('img');
-        var buttontext = button.one('span');
-
-        // Determine whether the marker is currently set.
-        var togglestatus = section.hasClass('current');
-        var value = 0;
-
-        // Set the current highlighted item text.
-        var old_string = M.util.get_string('markthistopic', 'moodle');
-
-        var selectedpage = Y.one(SELECTOR.PAGECONTENT);
-        selectedpage
-            .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT)
-            .set('title', old_string);
-        selectedpage
-            .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT + ' span')
-            .set('text', M.util.get_string('highlight', 'moodle'));
-        selectedpage
-            .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT + ' img')
-            .set('alt', old_string)
-            .set('src', M.util.image_url('i/marker'));
-
-        // Remove the highlighting from all sections.
-        selectedpage.all(M.course.format.get_section_selector(Y))
-            .removeClass('current');
-
-        // Then add it if required to the selected section.
-        if (!togglestatus) {
-            section.addClass('current');
-            value = Y.Moodle.core_course.util.section.getId(section.ancestor(M.course.format.get_section_wrapper(Y), true));
-            var new_string = M.util.get_string('markedthistopic', 'moodle');
-            button
-                .set('title', new_string);
-            buttonicon
-                .set('alt', new_string)
-                .set('src', M.util.image_url('i/marked'));
-            if (buttontext) {
-                buttontext
-                    .set('text', M.util.get_string('highlightoff', 'moodle'));
-            }
-        }
-
-        // Change the highlight status.
-        var data = {
-            'class': 'course',
-            'field': 'marker',
-            'value': value
-        };
-        var lightbox = M.util.add_lightbox(Y, section);
-        lightbox.show();
-        this.send_request(data, lightbox);
-    }
-}, {
-    NAME: 'course-section-toolbox',
-    ATTRS: {
-    }
-});
-
-M.course.init_section_toolbox = function(config) {
-    return new SECTIONTOOLBOX(config);
-};
diff --git a/course/yui/src/toolboxes/js/shared.js b/course/yui/src/toolboxes/js/shared.js
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/course/yui/src/toolboxes/js/toolbox.js b/course/yui/src/toolboxes/js/toolbox.js
deleted file mode 100644 (file)
index 6f78e09..0000000
+++ /dev/null
@@ -1,201 +0,0 @@
-/* eslint-disable no-unused-vars */
-/**
- * Resource and activity toolbox class.
- *
- * This class is responsible for managing AJAX interactions with activities and resources
- * when viewing a course in editing mode.
- *
- * @module moodle-course-toolboxes
- * @namespace M.course.toolboxes
- */
-
-// The CSS classes we use.
-var CSS = {
-        ACTIVITYINSTANCE: 'activityinstance',
-        AVAILABILITYINFODIV: 'div.availabilityinfo',
-        CONTENTWITHOUTLINK: 'contentwithoutlink',
-        CONDITIONALHIDDEN: 'conditionalhidden',
-        DIMCLASS: 'dimmed',
-        DIMMEDTEXT: 'dimmed_text',
-        EDITINSTRUCTIONS: 'editinstructions',
-        HIDE: 'hide',
-        MODINDENTCOUNT: 'mod-indent-',
-        MODINDENTHUGE: 'mod-indent-huge',
-        MODULEIDPREFIX: 'module-',
-        SECTIONHIDDENCLASS: 'hidden',
-        SECTIONIDPREFIX: 'section-',
-        SHOW: 'editing_show'
-    },
-    // The CSS selectors we use.
-    SELECTOR = {
-        ACTIONAREA: '.actions',
-        ACTIONLINKTEXT: '.actionlinktext',
-        ACTIVITYACTION: 'a.cm-edit-action[data-action]',
-        ACTIVITYICON: 'img.activityicon',
-        ACTIVITYINSTANCE: '.' + CSS.ACTIVITYINSTANCE,
-        ACTIVITYLINK: '.' + CSS.ACTIVITYINSTANCE + ' > a, .' + CSS.ACTIVITYINSTANCE +
-            ' > span[data-inplaceeditable] > a:not([data-inplaceeditablelink])',
-        ACTIVITYLI: 'li.activity',
-        COMMANDSPAN: '.commands',
-        CONTENTAFTERLINK: 'div.contentafterlink',
-        CONTENTWITHOUTLINK: 'div.contentwithoutlink',
-        GROUPINGLABEL: '.' + CSS.ACTIVITYINSTANCE + ' .groupinglabel',
-        HIDE: 'a.editing_hide',
-        HIGHLIGHT: 'a.editing_highlight',
-        INSTANCENAME: 'span.instancename',
-        MODINDENTDIV: '.mod-indent',
-        MODINDENTOUTER: '.mod-indent-outer',
-        PAGECONTENT: 'body',
-        SECTIONLI: 'li.section',
-        SHOW: 'a.' + CSS.SHOW,
-        SHOWHIDE: 'a.editing_showhide'
-    },
-    INDENTLIMITS = {
-        MIN: 0,
-        MAX: 16
-    },
-    BODY = Y.one(document.body);
-
-// Setup the basic namespace.
-M.course = M.course || {};
-
-/**
- * The toolbox class is a generic class which should never be directly
- * instantiated. Please extend it instead.
- *
- * @class toolbox
- * @constructor
- * @protected
- * @extends Base
- */
-var TOOLBOX = function() {
-    TOOLBOX.superclass.constructor.apply(this, arguments);
-};
-
-Y.extend(TOOLBOX, Y.Base, {
-    /**
-     * Send a request using the REST API
-     *
-     * @method send_request
-     * @param {Object} data The data to submit with the AJAX request
-     * @param {Node} [statusspinner] A statusspinner which may contain a section loader
-     * @param {Function} success_callback The callback to use on success
-     * @param {Object} [optionalconfig] Any additional configuration to submit
-     * @chainable
-     */
-    send_request: function(data, statusspinner, success_callback, optionalconfig) {
-        // Default data structure
-        if (!data) {
-            data = {};
-        }
-        // Handle any variables which we must pass back through to
-        var pageparams = this.get('config').pageparams,
-            varname;
-        for (varname in pageparams) {
-            data[varname] = pageparams[varname];
-        }
-
-        data.sesskey = M.cfg.sesskey;
-        data.courseId = this.get('courseid');
-
-        var uri = M.cfg.wwwroot + this.get('ajaxurl');
-
-        // Define the configuration to send with the request
-        var responsetext = [];
-        var config = {
-            method: 'POST',
-            data: data,
-            on: {
-                success: function(tid, response) {
-                    try {
-                        responsetext = Y.JSON.parse(response.responseText);
-                        if (responsetext.error) {
-                            new M.core.ajaxException(responsetext);
-                        }
-                    } catch (e) {
-                        // Ignore.
-                    }
-
-                    // Run the callback if we have one.
-                    if (success_callback) {
-                        Y.bind(success_callback, this, responsetext)();
-                    }
-
-                    if (statusspinner) {
-                        window.setTimeout(function() {
-                            statusspinner.hide();
-                        }, 400);
-                    }
-                },
-                failure: function(tid, response) {
-                    if (statusspinner) {
-                        statusspinner.hide();
-                    }
-                    new M.core.ajaxException(response);
-                }
-            },
-            context: this
-        };
-
-        // Apply optional config
-        if (optionalconfig) {
-            for (varname in optionalconfig) {
-                config[varname] = optionalconfig[varname];
-            }
-        }
-
-        if (statusspinner) {
-            statusspinner.show();
-        }
-
-        // Send the request
-        Y.io(uri, config);
-        return this;
-    }
-},
-{
-    NAME: 'course-toolbox',
-    ATTRS: {
-        /**
-         * The ID of the Moodle Course being edited.
-         *
-         * @attribute courseid
-         * @default 0
-         * @type Number
-         */
-        courseid: {
-            'value': 0
-        },
-
-        /**
-         * The Moodle course format.
-         *
-         * @attribute format
-         * @default 'topics'
-         * @type String
-         */
-        format: {
-            'value': 'topics'
-        },
-        /**
-         * The URL to use when submitting requests.
-         * @attribute ajaxurl
-         * @default null
-         * @type String
-         */
-        ajaxurl: {
-            'value': null
-        },
-        /**
-         * Any additional configuration passed when creating the instance.
-         *
-         * @attribute config
-         * @default {}
-         * @type Object
-         */
-        config: {
-            'value': {}
-        }
-    }
-}
-);
diff --git a/course/yui/src/toolboxes/meta/toolboxes.json b/course/yui/src/toolboxes/meta/toolboxes.json
deleted file mode 100644 (file)
index ce2e74d..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-    "moodle-course-toolboxes": {
-        "requires": [
-            "node",
-            "base",
-            "event-key",
-            "node",
-            "io",
-            "moodle-course-coursebase",
-            "moodle-course-util"
-        ]
-    }
-}
index 6583665..09c5804 100644 (file)
@@ -516,7 +516,7 @@ class enrol_lti_helper_testcase extends advanced_testcase {
         $document->load(realpath(__DIR__ . '/fixtures/input.xml'));
         $xpath = new \DOMXpath($document);
 
-        $this->setExpectedException("coding_exception");
+        $this->expectException('coding_exception');
         $function->invokeArgs(null, [$xpath, $parameters]);
     }
 
index e98f6d9..726c1d1 100644 (file)
@@ -57,8 +57,8 @@ class filter_mediaplugin extends moodle_text_filter {
         }
         $jsinitialised = true;
 
-        $mediamanager = core_media_manager::instance();
-        $mediamanager->setup($page);
+        // Set up the media manager so that media plugins requiring JS are initialised.
+        $mediamanager = core_media_manager::instance($page);
     }
 
     public function filter($text, array $options = array()) {
index dc1f1c7..31cf8de 100644 (file)
@@ -44,8 +44,8 @@ $string['dmlexceptiononinstall'] = '<p>Ocurrió un error en la BasedeDatos[{$a->
 $string['downloadedfilecheckfailed'] = 'Ha fallado la comprobación del archivo descargado';
 $string['invalidmd5'] = 'La variable de verificación MD5 es incorrecta no es valida - trate nuevamente';
 $string['missingrequiredfield'] = 'Falta algún campo necesario';
-$string['remotedownloaderror'] = '<p>Falló la descarga del componente a su servidor. Se recomienda ampliamente verificar los ajustes del proxy, extensión PHP cURL.</p>
-<p>Debe descargar el<a href="{$a->url}">{$a->url}</a> archivo manualmente, copiarlo en "{$a->dest}" en su servidor y descomprimirlo allí.</p>';
+$string['remotedownloaderror'] = '<p>Falló la descarga del componente a su servidor. Por favor verifique los ajustes del proxy, la extensión PHP cURL.está altamente recomendada.</p>
+<p>Debe descargar el archivo <a href="{$a->url}">{$a->url}</a>  manualmente, copiarlo a "{$a->dest}" en su servidor y descomprimirlo allí.</p>';
 $string['wrongdestpath'] = 'Ruta de destino errónea.';
 $string['wrongsourcebase'] = 'Base de fuente de URL errónea.';
 $string['wrongzipfilename'] = 'Nombre de archivo ZIP erróneo.';
index e8e00ef..6f1c089 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['cannotcreatedboninstall'] = '<p>پایگاه داده نتوانست ساخته شود.</p>
+<p>پایگاه دادهٔ تعیین شده وجود ندارد و کاربر ارائه‌شده هم مجوز ساختن پایگاه داده را ندارد.</p>
+<p>مدیر سایت باید پیکربندی پایگاه داده را بررسی کند.</p>';
+$string['cannotcreatelangdir'] = 'دایرکتوری lang نتوانست ساخته شود';
+$string['cannotcreatetempdir'] = 'نمی‌توان دایرکتوری temp را ساخت';
+$string['cannotdownloadcomponents'] = 'اجزا (کامپوننت‌ها) را نمی‌توان دریافت کرد';
+$string['cannotdownloadzipfile'] = 'فایل ZIP نتوانست دریافت شود.';
 $string['cannotfindcomponent'] = 'کامپوننت پیدا نشد';
 $string['cannotsavemd5file'] = 'نمی‌توانم فایل md5 را ذخیره کنم';
 $string['cannotsavezipfile'] = 'نمی‌توانم فایل ZIP را ذخیره کنم';
+$string['cannotunzipfile'] = 'فایل نمی‌تواند unzip شود';
+$string['componentisuptodate'] = 'کامپوننت به‌روز است';
+$string['dmlexceptiononinstall'] = '<p>یک خطای پایگاه داده رخ داد [{$a->errorcode}].<br />{$a->debuginfo}</p>';
 $string['missingrequiredfield'] = 'بعضی از فیلدهای ضروری خالی است';
index 1cf888f..6c2d146 100644 (file)
@@ -30,5 +30,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['clianswerno'] = 'n';
+$string['cliansweryes'] = 'j';
 $string['environmentrequireinstall'] = 'nepieciešams instalēt/iespējot';
 $string['environmentrequireversion'] = 'nepieciešama versija {$a->needed}, bet jūs izmantojat versiju {$a->current}';
index 4ec5f41..c0b2405 100644 (file)
@@ -34,6 +34,7 @@ $string['admindirname'] = 'Administratora direktorijs';
 $string['availablelangs'] = 'Pieejamās valodu pakotnes';
 $string['chooselanguagehead'] = 'Valodas izvēle';
 $string['chooselanguagesub'] = 'Lūdzu izvēlieties šīs instalācijas valodu. Izvēlētā valoda tiks izmantota arī kā noklusētā šīs vietnes valoda, lai gan tā var tikt nomainīta vēlāk.';
+$string['databasename'] = 'Datubāzes nosaukums';
 $string['databasetypehead'] = 'Izvēlēties datubāzes draiveru';
 $string['dataroot'] = 'Datu direktorijs';
 $string['dbprefix'] = 'Tabulu prefikss';
@@ -56,6 +57,7 @@ $string['memorylimithelp'] = '<p>Pašlaik iestatītais PHP atmiņas apjoma ierob
     <p>Tomēr dažos serveros tas neļaus darboties <b>nevienai</b> PHP lapai
 (atverot šīs lapas, tiks parādīti kļūdas ziņojumi), un fails .htaccess būs jānoņem.</p></li>
 </ol>';
+$string['paths'] = 'Ceļi';
 $string['phpversion'] = 'PHP versija';
 $string['phpversionhelp'] = '<p>Sistēmā Moodle jāizmanto PHP, kuras versija ir vismaz 4.3.0 vai 5.1.0 (versijai 5.0.x piemīt vairākas zināmas problēmas).</p>
 <p>Jūs pašlaik lietojat versiju {$a}</p>
index 5b67cff..2305267 100644 (file)
@@ -71,7 +71,7 @@ $string['pathssubdataroot'] = '<p>Директоријум где ће Moodle ч
 <p>Овај директоријум треба да буде подешен тако да корисник веб сервера (обично \'nobody\' или \'apache\') може да га чита и у њега уписује.</p>
 <p>Директоријум не сме бити доступан директно преко веба. </p>
 <p>Уколико овај директоријум не постоји процес инсталације ће покушати да га креира.</p>';
-$string['pathssubdirroot'] = '<p>Пуна путања до директоријума који садржи код Moodlea.</p>';
+$string['pathssubdirroot'] = '<p>Пуна путања до директоријума који садржи кôд Moodlea.</p>';
 $string['pathssubwwwroot'] = '<p>Пуна адреса путем које ће се приступати Moodleu, тј. адреса коју ће корисници унети у адресну траку својих веб читача како би приступили Moodleu.</p>
 <p>Није могуће приступати Moodleu коришћењем више адреса Ако се вашем сајту може приступити са више адреса, онда изаберите најлакшу, а за све остале адресе подесите перманентну редирекцију.</p>
 <p>Ако се вашем сајту може приступити са интернета али и са унутрашње мреже (која се понекад назив интранет), онда овде употребите јавну адресу.</p>
index 2349594..cac942b 100644 (file)
@@ -71,7 +71,7 @@ $string['pathssubdataroot'] = '<p>Direktorijum gde će Moodle čuvati datoteke i
 <p>Ovaj direktorijum treba da bude podešen tako da korisnik veb servera (obično \'nobody\' ili \'apache\') može da ga čita i u njega upisuje.</p>
 <p>Direktorijum ne sme biti dostupan direktno preko veba. </p>
 <p>Ukoliko ovaj direktorijum ne postoji proces instalacije će pokušati da ga kreira.</p>';
-$string['pathssubdirroot'] = '<p>Puna putanja do direktorijuma koji sadrži kod Moodlea.</p>';
+$string['pathssubdirroot'] = '<p>Puna putanja do direktorijuma koji sadrži kôd Moodlea.</p>';
 $string['pathssubwwwroot'] = '<p>Puna adresa putem koje će se pristupati Moodleu, tj. adresa koju će korisnici uneti u adresnu traku svojih veb čitača kako bi pristupili Moodleu.</p>
 <p>Nije moguće pristupati Moodleu korišćenjem više adresa Ako se vašem sajtu može pristupiti sa više adresa, onda izaberite najlakšu, a za sve ostale adrese podesite permanentnu redirekciju.</p>
 <p>Ako se vašem sajtu može pristupiti sa interneta ali i sa unutrašnje mreže (koja se ponekad naziv intranet), onda ovde upotrebite javnu adresu.</p>
index 9bc70e3..c188ff6 100644 (file)
@@ -674,8 +674,6 @@ $string['logguests_help'] = 'This setting enables logging of actions by guest ac
 $string['loginhttps'] = 'Use HTTPS for logins';
 $string['loginpageautofocus'] = 'Autofocus login page form';
 $string['loginpageautofocus_help'] = 'Enabling this option improves usability of the login page, but automatically focusing fields may be considered an accessibility issue.';
-$string['loginpasswordautocomplete'] = 'Prevent password autocompletion on login form';
-$string['loginpasswordautocomplete_help'] = 'If enabled, users are not allowed to save their account password in their browser.';
 $string['loglifetime'] = 'Keep logs for';
 $string['logo'] = 'Logo';
 $string['logo_desc'] = 'A full logo to be used as decoration by some themes (such as core themes). This image can be quite high resolution because it will be scaled down for use (and cached for performance). Logos that are wider than they are high usually give better results. Formats accepted: PNG and JPG.';
@@ -1241,3 +1239,6 @@ $string['cacheapplication'] = 'Application cache';
 $string['cacheapplicationhelp'] = 'Cached items are shared among all users and expire by a determined time to live (ttl).';
 // Deprecated since Moodle 3.2.
 $string['mobile'] = 'Mobile';
+// Deprecated since Moodle 3.3.
+$string['loginpasswordautocomplete'] = 'Prevent password autocompletion on login form';
+$string['loginpasswordautocomplete_help'] = 'If enabled, users are not allowed to save their account password in their browser.';
\ No newline at end of file
index 12593ed..38175ef 100644 (file)
@@ -59,3 +59,5 @@ strftimedaydatetime,core_message
 timenosee,core_message
 timesent,core_message
 userssearchresults,core_message
+loginpasswordautocomplete,core_admin
+loginpasswordautocomplete_help,core_admin
\ No newline at end of file
index d89a33c..13d8bd0 100644 (file)
@@ -133,6 +133,8 @@ $string['alllogs'] = 'All logs';
 $string['allmods'] = 'All {$a}';
 $string['allow'] = 'Allow';
 $string['allowinternal'] = 'Allow internal methods as well';
+$string['allowstealthmodules'] = 'Allow hidden but available activities';
+$string['allowstealthmodules_help'] = 'If enabled, activities can be marked as hidden on the course page but available by following a link from elsewhere. These activities are listed in the gradebook and other reports';
 $string['allownone'] = 'Allow none';
 $string['allownot'] = 'Do not allow';
 $string['allparticipants'] = 'All participants';
@@ -918,6 +920,7 @@ $string['helpprefix2'] = 'Help with {$a}';
 $string['helpwiththis'] = 'Help with this';
 $string['hiddenassign'] = 'Hidden assignment';
 $string['hiddenfromstudents'] = 'Hidden from students';
+$string['hiddenoncoursepage'] = 'Available but not displayed on course page';
 $string['hiddensections'] = 'Hidden sections';
 $string['hiddensections_help'] = 'This setting determines whether hidden sections are displayed to students in collapsed form (perhaps for a course in weekly format to indicate holidays) or are completely hidden.';
 $string['hiddensectionscollapsed'] = 'Hidden sections are shown in collapsed form';
@@ -925,6 +928,7 @@ $string['hiddensectionsinvisible'] = 'Hidden sections are completely invisible';
 $string['hide'] = 'Hide';
 $string['hideadvancedsettings'] = 'Hide advanced settings';
 $string['hidechartdata'] = 'Hide chart data';
+$string['hideonfrontpage'] = 'Hide from front page';
 $string['hidepicture'] = 'Hide picture';
 $string['hidesection'] = 'Hide section {$a}';
 $string['hidesettings'] = 'Hide settings';
@@ -1084,8 +1088,10 @@ $string['mailstudents'] = 'Inform students';
 $string['mailteachers'] = 'Inform teachers';
 $string['maincoursepage'] = 'Main course page';
 $string['makeafolder'] = 'Create folder';
+$string['makeavailable'] = 'Make available';
 $string['makeeditable'] = 'If you make \'{$a}\' editable by the web server process (eg apache) then you could edit this file directly from this page';
 $string['makethismyhome'] = 'Make this my default home page';
+$string['makeunavailable'] = 'Make unavailable';
 $string['manageblocks'] = 'Blocks';
 $string['managecategorythis'] = 'Manage this category';
 $string['managecourses'] = 'Manage courses';
@@ -1640,6 +1646,7 @@ $string['secondstotime86400'] = '1 day';
 $string['secretalreadyused'] = 'Change password confirmation link was already used, password was not changed.';
 $string['secs'] = 'secs';
 $string['section'] = 'Section';
+$string['sectionactionnotsupported'] = 'Section action "{$a}" is not supported here';
 $string['sectionname'] = 'Section name';
 $string['sections'] = 'Sections';
 $string['sectionusedefaultname'] = 'Use default section name';
index 3af51d6..79cd16a 100644 (file)
Binary files a/lib/amd/build/ajax.min.js and b/lib/amd/build/ajax.min.js differ
index a29e064..17220a3 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index 30589a8..48aff14 100644 (file)
Binary files a/lib/amd/build/str.min.js and b/lib/amd/build/str.min.js differ
index 7e4fd5c..e73b2f7 100644 (file)
Binary files a/lib/amd/build/templates.min.js and b/lib/amd/build/templates.min.js differ
index 7c56b36..b2afb6b 100644 (file)
@@ -87,9 +87,7 @@ define(['jquery', 'core/config'], function($, config) {
         for (i = 0; i < requests.length; i++) {
             var request = requests[i];
 
-            if (typeof request.fail != "undefined") {
-                request.deferred.reject(textStatus);
-            }
+            request.deferred.reject(textStatus);
         }
     };
 
index ca7f034..361d0e1 100644 (file)
@@ -33,7 +33,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
         ENTER: 13,
         SPACE: 32,
         ESCAPE: 27,
-        COMMA: 188,
+        COMMA: 44,
         UP: 38
     };
 
@@ -543,14 +543,6 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                     // We handled this event, so prevent it.
                     e.preventDefault();
                     return false;
-                case KEYS.COMMA:
-                    if (options.tags) {
-                        // If we are allowing tags, comma should create a tag (or enter).
-                        createItem(options, state, originalSelect);
-                    }
-                    // We handled this event, so prevent it.
-                    e.preventDefault();
-                    return false;
                 case KEYS.UP:
                     // Choose the previous active item.
                     activatePreviousItem(state);
@@ -581,6 +573,19 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             }
             return true;
         });
+        // Support multi lingual COMMA keycode (44).
+        inputElement.on('keypress', function(e) {
+            if (e.keyCode === KEYS.COMMA) {
+                if (options.tags) {
+                    // If we are allowing tags, comma should create a tag (or enter).
+                    createItem(options, state, originalSelect);
+                }
+                // We handled this event, so prevent it.
+                e.preventDefault();
+                return false;
+            }
+            return true;
+        });
         // Handler used to force set the value from behat.
         inputElement.on('behat:set-value', function() {
             var suggestionsElement = $(document.getElementById(state.suggestionsId));
index ad14a0a..1ef7623 100644 (file)
@@ -28,6 +28,7 @@
 /* eslint-disable no-restricted-properties */
 define(['jquery', 'core/ajax', 'core/localstorage'], function($, ajax, storage) {
 
+    var promiseCache = [];
 
     return /** @alias module:core/str */ {
         // Public variables and functions.
@@ -71,6 +72,7 @@ define(['jquery', 'core/ajax', 'core/localstorage'], function($, ajax, storage)
             var i = 0;
             var missing = false;
             var request;
+
             // Try from local storage. If it's there - put it in M.str and resolve it.
 
             for (i = 0; i < requests.length; i++) {
@@ -78,10 +80,11 @@ define(['jquery', 'core/ajax', 'core/localstorage'], function($, ajax, storage)
                 if (typeof request.lang === "undefined") {
                     request.lang = $('html').attr('lang').replace('-', '_');
                 }
+                request.cacheKey = 'core_str/' + request.key + '/' + request.component + '/' + request.lang;
                 if (typeof M.str[request.component] === "undefined" ||
                         typeof M.str[request.component][request.key] === "undefined") {
                     // Try and revive it from local storage.
-                    var cached = storage.get('core_str/' + request.key + '/' + request.component + '/' + request.lang);
+                    var cached = storage.get(request.cacheKey);
                     if (cached) {
                         if (typeof M.str[request.component] === "undefined") {
                             M.str[request.component] = [];
@@ -103,25 +106,52 @@ define(['jquery', 'core/ajax', 'core/localstorage'], function($, ajax, storage)
                 }
                 deferred.resolve(results);
             } else {
-                // Something is missing, we might as well load them all.
                 var ajaxrequests = [];
+                var fetchpromises = [];
+
+                // Done handler for ajax call. Must be bound to the current fetchpromise. We do this
+                // to avoid creating a function in a loop.
+                var doneFunc = function(str) {
+                    this.resolve(str);
+                };
+
+                var failFunc = function(reason) {
+                    this.reject(reason);
+                };
 
                 for (i = 0; i < requests.length; i++) {
                     request = requests[i];
 
-                    ajaxrequests.push({
-                        methodname: 'core_get_string',
-                        args: {
-                &nb