Merge branch 'MDL-57927-master' of git://github.com/lameze/moodle
authorDavid Monllao <david.monllao@gmail.com>
Tue, 27 Jun 2017 13:55:17 +0000 (15:55 +0200)
committerDavid Monllao <david.monllao@gmail.com>
Tue, 27 Jun 2017 13:55:17 +0000 (15:55 +0200)
158 files changed:
.travis.yml
admin/cli/build_theme_css.php [new file with mode: 0644]
admin/cli/mysql_collation.php
admin/registration/forms.php
admin/roles/usersroles.php
admin/settings/appearance.php
admin/settings/subsystems.php
admin/tool/langimport/index.php
admin/tool/log/backup/moodle2/restore_tool_log_logstore_subplugin.class.php
admin/tool/lp/templates/competency_rule_config.mustache
auth/oauth2/classes/api.php
auth/oauth2/classes/auth.php
availability/classes/info_section.php
availability/condition/date/classes/condition.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
blocks/completionstatus/details.php
blocks/myoverview/amd/build/tab_preferences.min.js [new file with mode: 0644]
blocks/myoverview/amd/src/tab_preferences.js [new file with mode: 0644]
blocks/myoverview/block_myoverview.php
blocks/myoverview/classes/output/courses_view.php
blocks/myoverview/classes/output/main.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/lib.php [new file with mode: 0644]
blocks/myoverview/settings.php [new file with mode: 0644]
blocks/myoverview/templates/main.mustache
blocks/myoverview/tests/behat/block_myoverview_progress.feature
blocks/myoverview/version.php
blocks/navigation/amd/build/ajax_response_renderer.min.js
blocks/navigation/amd/src/ajax_response_renderer.js
blocks/navigation/styles.css
blocks/settings/styles.css
calendar/classes/local/event/container.php
calendar/export.php
calendar/lib.php
calendar/managesubscriptions.php
calendar/renderer.php
calendar/tests/externallib_test.php
calendar/view.php
completion/completion_completion.php
course/format/renderer.php
course/lib.php
course/renderer.php
course/tests/courselib_test.php
course/tests/externallib_test.php
dataformat/html/classes/writer.php
dataformat/json/classes/writer.php
dataformat/upgrade.txt
filter/urltolink/filter.php
filter/urltolink/tests/filter_test.php
grade/edit/tree/item_form.php
group/classes/output/index_page.php [new file with mode: 0644]
group/classes/output/renderer.php [new file with mode: 0644]
group/externallib.php
group/index.php
group/templates/index.mustache [new file with mode: 0644]
group/tests/externallib_test.php
install/lang/zh_tw/admin.php
lang/en/admin.php
lang/en/cache.php
lang/en/calendar.php
lang/en/moodle.php
lib/amd/build/ajax.min.js
lib/amd/build/notification.min.js
lib/amd/src/ajax.js
lib/amd/src/notification.js
lib/classes/dataformat/base.php
lib/classes/dataformat/spout_base.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/plugininfo/repository.php
lib/classes/user.php
lib/completionlib.php
lib/dataformatlib.php
lib/db/caches.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/filelib.php
lib/filestorage/zip_archive.php
lib/navigationlib.php
lib/outputlib.php
lib/phpunit/classes/util.php
lib/setuplib.php
lib/tablelib.php
lib/tests/filelib_test.php
lib/upgrade.txt
lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-debug.js
lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-min.js
lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js
lib/yui/src/languninstallconfirm/js/languninstallconfirm.js
lib/yui/src/notification/js/exception.js
media/player/videojs/amd/build/loader.min.js
media/player/videojs/amd/src/loader.js
media/player/videojs/classes/plugin.php
media/player/videojs/tests/player_test.php
message/amd/build/message_repository.min.js
message/amd/src/message_repository.js
message/output/popup/amd/build/notification_repository.min.js
message/output/popup/amd/src/notification_repository.js
mod/assign/db/upgrade.php
mod/assign/feedback/comments/locallib.php
mod/assign/feedback/comments/tests/behat/feedback_comments.feature [new file with mode: 0644]
mod/assign/gradingtable.php
mod/assign/locallib.php
mod/assign/override_form.php
mod/assign/submission/file/lang/en/assignsubmission_file.php
mod/assign/submission/file/locallib.php
mod/assign/submission/file/settings.php
mod/assign/tests/markerallocation_test.php [new file with mode: 0644]
mod/assign/version.php
mod/data/field/latlong/field.class.php
mod/feedback/classes/completion.php
mod/feedback/classes/external.php
mod/feedback/lib.php
mod/lesson/classes/external.php
mod/lesson/lang/en/lesson.php
mod/lesson/locallib.php
mod/lesson/pagetypes/branchtable.php
mod/lesson/tests/external_test.php
mod/lesson/upgrade.txt
mod/lti/db/services.php
mod/scorm/lib.php
mod/scorm/tests/lib_test.php
pix/i/mahara_host.gif [deleted file]
pix/i/mahara_host.png [new file with mode: 0644]
pix/i/mahara_host.svg [new file with mode: 0644]
question/type/multichoice/classes/admin_setting_answernumbering.php [new file with mode: 0644]
question/type/multichoice/edit_multichoice_form.php
question/type/multichoice/lang/en/qtype_multichoice.php
question/type/multichoice/renderer.php
question/type/multichoice/settings.php [new file with mode: 0644]
report/stats/locallib.php
report/stats/user.php
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/user.scss
theme/boost/templates/core/availability_info.mustache
theme/boost/templates/header.mustache
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/tool_usertours.less
theme/bootstrapbase/less/moodle/user.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_myoverview/main.mustache
theme/bootstrapbase/templates/core/availability_info.mustache
theme/clean/classes/core_renderer.php
theme/styles.php
user/externallib.php
user/files.php
user/tests/externallib_test.php
user/tests/userlib_test.php
version.php
webservice/externallib.php
webservice/tests/externallib_test.php
webservice/upgrade.txt

index ff2b674..d58effa 100644 (file)
@@ -2,7 +2,7 @@
 # process (which uses our internal CI system) this file is here for the benefit
 # of community developers git clones - see MDL-51458.
 
-sudo: false
+sudo: required
 
 # We currently disable Travis notifications entirely until https://github.com/travis-ci/travis-ci/issues/4976
 # is fixed.
@@ -18,6 +18,10 @@ php:
 
 addons:
   postgresql: "9.3"
+  packages:
+    - mysql-server-5.6
+    - mysql-client-core-5.6
+    - mysql-client-5.6
 
 services:
     - redis-server
@@ -73,6 +77,28 @@ cache:
       - $HOME/.npm
 
 install:
+    - sudo apt-get -y install haveged
+    - sudo service haveged start
+    - >
+        if [ "$DB" = 'mysqli' ];
+        then
+            sudo mkdir /mnt/ramdisk
+            sudo mount -t tmpfs -o size=1024m tmpfs /mnt/ramdisk
+            sudo stop mysql
+            sudo mv /var/lib/mysql /mnt/ramdisk
+            sudo ln -s /mnt/ramdisk/mysql /var/lib/mysql
+            sudo start mysql
+        fi
+    - >
+        if [ "$DB" = 'pgsql' ];
+        then
+            sudo mkdir /mnt/ramdisk
+            sudo mount -t tmpfs -o size=1024m tmpfs /mnt/ramdisk
+            sudo service postgresql stop
+            sudo mv /var/lib/postgresql /mnt/ramdisk
+            sudo ln -s /mnt/ramdisk/postgresql /var/lib/postgresql
+            sudo service postgresql start 9.3
+        fi
     - >
         if [ "$TASK" = 'PHPUNIT' ];
         then
diff --git a/admin/cli/build_theme_css.php b/admin/cli/build_theme_css.php
new file mode 100644 (file)
index 0000000..30cd996
--- /dev/null
@@ -0,0 +1,119 @@
+<?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/>.
+
+/**
+ * Build and store theme CSS.
+ *
+ * @package    core
+ * @subpackage cli
+ * @copyright  2017 Ryan Wyllie <ryan@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");
+require_once("$CFG->libdir/csslib.php");
+require_once("$CFG->libdir/outputlib.php");
+
+$longparams = [
+    'themes'    => null,
+    'direction' => null,
+    'help'      => false,
+    'verbose'   => false
+];
+
+$shortmappings = [
+    't' => 'themes',
+    'd' => 'direction',
+    'h' => 'help',
+    'v' => 'verbose'
+];
+
+// Get CLI params.
+list($options, $unrecognized) = cli_get_params($longparams, $shortmappings);
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+if ($options['help']) {
+    echo
+"Compile the CSS for one or more installed themes.
+Existing CSS caches will replaced.
+By default all themes will be recompiled unless otherwise specified.
+
+Options:
+-t, --themes    A comma separated list of themes to be compiled
+-d, --direction Only compile a single direction (either ltr or rtl)
+-v, --verbose   Print info comments to stdout
+-h, --help      Print out this help
+
+Example:
+\$ sudo -u www-data /usr/bin/php admin/cli/build_theme_css.php --themes=boost --direction=ltr
+";
+
+    die;
+}
+
+if (empty($options['verbose'])) {
+    $trace = new null_progress_trace();
+} else {
+    $trace = new text_progress_trace();
+}
+
+cli_heading('Build theme css');
+
+// Determine which themes we need to build.
+$themenames = [];
+if (is_null($options['themes'])) {
+    $trace->output('No themes specified. Finding all installed themes.');
+    $themenames = array_keys(core_component::get_plugin_list('theme'));
+} else {
+    if (is_string($options['themes'])) {
+        $themenames = explode(',', $options['themes']);
+    } else {
+        cli_error('--themes must be a comma separated list of theme names');
+    }
+}
+
+$trace->output('Checking that each theme is correctly installed...');
+$themeconfigs = [];
+foreach ($themenames as $themename) {
+    if (is_null(theme_get_config_file_path($themename))) {
+        cli_error("Unable to find theme config for {$themename}");
+    }
+
+    // Load the config for the theme.
+    $themeconfigs[] = theme_config::load($themename);
+}
+
+$directions = ['ltr', 'rtl'];
+
+if (!is_null($options['direction'])) {
+    if (!in_array($options['direction'], $directions)) {
+         cli_error("--direction must be either ltr or rtl");
+    }
+
+    $directions = [$options['direction']];
+}
+
+$trace->output('Building CSS for themes: ' . implode(', ', $themenames));
+theme_build_css_for_themes($themeconfigs, $directions);
+
+exit(0);
index adaadbb..ae3c709 100644 (file)
@@ -53,7 +53,7 @@ Options:
 -h, --help            Print out this help
 
 Example:
-\$ sudo -u www-data /usr/bin/php admin/cli/mysql_collation.php --collation=utf8_general_ci
+\$ sudo -u www-data /usr/bin/php admin/cli/mysql_collation.php --collation=utf8mb4_unicode_ci
 ";
 
 if (!empty($options['collation'])) {
@@ -145,9 +145,22 @@ if (!empty($options['collation'])) {
             $skipped++;
 
         } else {
-            $DB->change_database_structure("ALTER TABLE $table->name DEFAULT CHARACTER SET $charset DEFAULT COLLATE = $collation");
-            echo "CONVERTED\n";
-            $converted++;
+            try {
+                $DB->change_database_structure("ALTER TABLE $table->name CONVERT TO CHARACTER SET $charset COLLATE $collation");
+                echo "CONVERTED\n";
+                $converted++;
+            } catch (ddl_exception $e) {
+                $result = mysql_set_row_format($table->name, $charset, $collation, $engine);
+                if ($result) {
+                    echo "CONVERTED\n";
+                    $converted++;
+                } else {
+                    // We don't know what the problem is. Stop the conversion.
+                    cli_error("Error: Tried to convert $table->name, but there was a problem. Please check the details of this
+                            table and try again.");
+                    die();
+                }
+            }
         }
 
         $sql = "SHOW FULL COLUMNS FROM $table->name WHERE collation IS NOT NULL";
@@ -290,3 +303,26 @@ function mysql_get_column_collations($tablename) {
     $rs->close();
     return $collations;
 }
+
+function mysql_set_row_format($tablename, $charset, $collation, $engine) {
+    global $DB;
+
+    $sql = "SELECT row_format
+              FROM INFORMATION_SCHEMA.TABLES
+             WHERE table_schema = DATABASE() AND table_name = ?";
+    $rs = $DB->get_record_sql($sql, array($tablename));
+    if ($rs) {
+        if ($rs->row_format == 'Compact' || $rs->row_format == 'Redundant') {
+            $rowformat = $DB->get_row_format_sql($engine, $collation);
+            // Try to convert to compressed format and then try updating the collation again.
+            $DB->change_database_structure("ALTER TABLE $tablename $rowformat");
+            $DB->change_database_structure("ALTER TABLE $tablename CONVERT TO CHARACTER SET $charset COLLATE $collation");
+        } else {
+            // Row format may not be the problem. Can not diagnose problem. Send fail reply.
+            return false;
+        }
+    } else {
+        return false;
+    }
+    return true;
+}
index 92b15e8..6e0b8a9 100644 (file)
@@ -232,7 +232,7 @@ class site_registration_form extends moodleform {
         }
         $language = get_config('hub', 'site_language_' . $cleanhuburl);
         if ($language === false) {
-            $language = current_language();
+            $language = explode('_', current_language())[0];
         }
         $geolocation = get_config('hub', 'site_geolocation_' . $cleanhuburl);
         $contactable = get_config('hub', 'site_contactable_' . $cleanhuburl);
index 5e871e6..c455902 100644 (file)
@@ -193,8 +193,8 @@ function print_report_tree($contextid, $contexts, $systemcontext, $fullname, $al
                 $strgoto = get_string('gotoassignroles', 'core_role', $a);
                 $strcheck = get_string('checkuserspermissionshere', 'core_role', $a);
             }
-            echo ' <a title="' . $strgoto . '" href="' . $raurl . '">' . $OUTPUT->pix_icon('t/edit', 'core', $stredit) . '</a> ';
-            echo ' <a title="' . $strcheck . '" href="' . $churl . '">' . $OUTPUT->pix_icon('t/preview', 'core', $strcheckpermissions) . '</a> ';
+            echo ' <a title="' . $strgoto . '" href="' . $raurl . '">' . $OUTPUT->pix_icon('t/edit', $stredit) . '</a> ';
+            echo ' <a title="' . $strcheck . '" href="' . $churl . '">' . $OUTPUT->pix_icon('t/preview', $strcheckpermissions) . '</a> ';
             echo "</p>\n";
         }
     }
index 5a20af8..1a63f4f 100644 (file)
@@ -179,7 +179,8 @@ preferences,moodle|/user/preferences.php|preferences',
         'idnumber' => new lang_string('sort_idnumber', 'admin'),
     );
     $temp->add(new admin_setting_configselect('navsortmycoursessort', new lang_string('navsortmycoursessort', 'admin'), new lang_string('navsortmycoursessort_help', 'admin'), 'sortorder', $sortoptions));
-    $temp->add(new admin_setting_configtext('navcourselimit',new lang_string('navcourselimit','admin'),new lang_string('confignavcourselimit', 'admin'),20,PARAM_INT));
+    $temp->add(new admin_setting_configtext('navcourselimit', new lang_string('navcourselimit', 'admin'),
+        new lang_string('confignavcourselimit', 'admin'), 10, PARAM_INT));
     $temp->add(new admin_setting_configcheckbox('usesitenameforsitepages', new lang_string('usesitenameforsitepages', 'admin'), new lang_string('configusesitenameforsitepages', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('linkadmincategories', new lang_string('linkadmincategories', 'admin'), new lang_string('linkadmincategories_help', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('linkcoursesections', new lang_string('linkcoursesections', 'admin'), new lang_string('linkcoursesections_help', 'admin'), 0));
index 6f5ded5..f2b50d5 100644 (file)
@@ -55,9 +55,6 @@ 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));
+    $optionalsubsystems->add(new admin_setting_configcheckbox('allowstealth', new lang_string('allowstealthmodules'),
+        new lang_string('allowstealthmodules_help'), 0, 1, 0));
 }
index 0e98a5d..92a09da 100644 (file)
@@ -40,7 +40,7 @@ if (empty($CFG->langotherroot)) {
 $mode               = optional_param('mode', 0, PARAM_INT);              // action
 $pack               = optional_param_array('pack', array(), PARAM_SAFEDIR);    // pack to install
 $uninstalllang      = optional_param_array('uninstalllang', array(), PARAM_LANG);// installed pack to uninstall
-$confirmtounistall  = optional_param('confirmtouninstall', '', PARAM_ALPHAEXT);  // uninstallation confirmation
+$confirmtounistall  = optional_param('confirmtouninstall', '', PARAM_SAFEPATH);  // uninstallation confirmation
 $purgecaches        = optional_param('purgecaches', false, PARAM_BOOL);  // explicit caches reset
 
 if ($purgecaches) {
@@ -74,7 +74,7 @@ if ($mode == DELETION_OF_SELECTED_LANG and (!empty($uninstalllang) or !empty($co
     // Actually deleting languages, languages to delete are passed as GET parameter as string
     // ...need to populate them to array.
     if (empty($uninstalllang)) {
-        $uninstalllang = explode('-', $confirmtounistall);
+        $uninstalllang = explode('/', $confirmtounistall);
     }
 
     if (in_array('en', $uninstalllang)) {
@@ -84,8 +84,10 @@ if ($mode == DELETION_OF_SELECTED_LANG and (!empty($uninstalllang) or !empty($co
     } else if (empty($confirmtounistall) and confirm_sesskey()) { // User chose langs to be deleted, show confirmation.
         echo $OUTPUT->header();
         echo $OUTPUT->confirm(get_string('uninstallconfirm', 'tool_langimport', implode(', ', $uninstalllang)),
-                     'index.php?mode='.DELETION_OF_SELECTED_LANG.'&confirmtouninstall='.implode('-', $uninstalllang),
-                     'index.php');
+            new moodle_url($PAGE->url, array(
+                'mode' => DELETION_OF_SELECTED_LANG,
+                'confirmtouninstall' => implode('/', $uninstalllang),
+            )), $PAGE->url);
         echo $OUTPUT->footer();
         die;
 
index 0c00f00..c48c2c8 100644 (file)
@@ -84,8 +84,7 @@ abstract class restore_tool_log_logstore_subplugin extends restore_subplugin {
             }
         }
 
-        // Roll dates.
-        $data->timecreated = $this->apply_date_offset($data->timecreated);
+        // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961.
 
         // Revert other to its original php way.
         $data->other = unserialize(base64_decode($data->other));
index b0e9db8..59f4d57 100644 (file)
@@ -60,7 +60,7 @@
         <div data-region="rule-base" class="form">
             <div data-region="rule-outcome" class="form-group">
                 <label>{{#str}}outcome, tool_lp{{/str}}</label>
-                <select name="outcome" class="custom-select" ng-label="{{#str}}outcome, tool_lp{{/str}}">
+                <select name="outcome" class="custom-select">
                     {{#outcomes}}
                     <option value="{{code}}" {{#selected}}selected{{/selected}}>{{name}}</option>
                     {{/outcomes}}
@@ -68,7 +68,7 @@
             </div>
             <div data-region="rule-type" class="form-group">
                 <label>{{#str}}when, tool_lp{{/str}}</label>
-                <select name="rule" class="custom-select" ng-label="{{#str}}when, tool_lp{{/str}}">
+                <select name="rule" class="custom-select">
                     <option value="-1">{{#str}}choosedots{{/str}}</option>
                     {{#rules}}
                     <option value="{{type}}" {{#selected}}selected{{/selected}}>{{name}}</option>
         {{/config}}
     </div>
 
-    <div data-region="footer" class="pull-xs-right">
+    <div data-region="footer" class="pull-xs-right m-t-1">
         {{#config}}
         <input type="button" class="btn btn-primary" data-action="save" value="{{#str}}savechanges{{/str}}"/>
         {{/config}}
         <input type="button" class="btn btn-secondary" data-action="cancel" value="{{#str}}cancel{{/str}}"/>
     </div>
+    <div class="clearfix"></div>
 </div>
index 689ad1c..9d678a8 100644 (file)
@@ -192,10 +192,10 @@ class api {
         ];
         $confirmationurl = new moodle_url('/auth/oauth2/confirm-linkedlogin.php', $params);
 
-        // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
-        $data->link = $confirmationurl->out();
+        $data->link = $confirmationurl->out(false);
+        $message = get_string('confirmlinkedloginemail', 'auth_oauth2', $data);
 
-        $message     = get_string('confirmlinkedloginemail', 'auth_oauth2', $data);
+        $data->link = $confirmationurl->out();
         $messagehtml = text_to_html(get_string('confirmlinkedloginemail', 'auth_oauth2', $data), false, false, true);
 
         $user->mailformat = 1;  // Always send HTML version as well.
@@ -303,9 +303,10 @@ class api {
         ];
         $confirmationurl = new moodle_url('/auth/oauth2/confirm-account.php', $params);
 
-        $data->link = $confirmationurl->out();
+        $data->link = $confirmationurl->out(false);
+        $message = get_string('confirmaccountemail', 'auth_oauth2', $data);
 
-        $message     = get_string('confirmaccountemail', 'auth_oauth2', $data);
+        $data->link = $confirmationurl->out();
         $messagehtml = text_to_html(get_string('confirmaccountemail', 'auth_oauth2', $data), false, false, true);
 
         $user->mailformat = 1;  // Always send HTML version as well.
index 812cf98..aa5f70c 100644 (file)
@@ -346,7 +346,7 @@ class auth extends \auth_plugin_base {
      * Complete the login process after oauth handshake is complete.
      * @param \core\oauth2\client $client
      * @param string $redirecturl
-     * @return none Either redirects or throws an exception
+     * @return void Either redirects or throws an exception
      */
     public function complete_login(client $client, $redirecturl) {
         global $CFG, $SESSION, $PAGE;
@@ -356,7 +356,7 @@ class auth extends \auth_plugin_base {
         if (!$userinfo) {
             // Trigger login failed event.
             $failurereason = AUTH_LOGIN_NOUSER;
-            $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
+            $event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown',
                                                                         'reason' => $failurereason]]);
             $event->trigger();
 
@@ -368,7 +368,7 @@ class auth extends \auth_plugin_base {
         if (empty($userinfo['username']) || empty($userinfo['email'])) {
             // Trigger login failed event.
             $failurereason = AUTH_LOGIN_NOUSER;
-            $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
+            $event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown',
                                                                         'reason' => $failurereason]]);
             $event->trigger();
 
index d65729a..6ef4fc9 100644 (file)
@@ -62,8 +62,12 @@ class info_section extends info {
 
     protected function set_in_database($availability) {
         global $DB;
-        $DB->set_field('course_sections', 'availability', $availability,
-                array('id' => $this->section->id));
+
+        $section = new \stdClass();
+        $section->id = $this->section->id;
+        $section->availability = $availability;
+        $section->timemodified = time();
+        $DB->update_record('course_sections', $section);
     }
 
     /**
index 722b024..63b61b4 100644 (file)
@@ -287,8 +287,12 @@ class condition extends \core_availability\condition {
 
             // Save the updated course module.
             if ($changed) {
-                $DB->set_field('course_sections', 'availability', json_encode($tree->save()),
-                        array('id' => $section->id));
+                $updatesection = new \stdClass();
+                $updatesection->id = $section->id;
+                $updatesection->availability = json_encode($tree->save());
+                $updatesection->timemodified = time();
+                $DB->update_record('course_sections', $updatesection);
+
                 $anychanged = true;
             }
         }
index acdf34b..2ea5248 100644 (file)
@@ -325,7 +325,7 @@ class backup_section_structure_step extends backup_structure_step {
 
         $section = new backup_nested_element('section', array('id'), array(
                 'number', 'name', 'summary', 'summaryformat', 'sequence', 'visible',
-                'availabilityjson'));
+                'availabilityjson', 'timemodified'));
 
         // attach format plugin structure to $section element, only one allowed
         $this->add_plugin_structure('format', $section, false);
index 5d607ef..f886496 100644 (file)
@@ -795,7 +795,8 @@ class restore_rebuild_course_cache extends restore_execution_step {
             if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) {
                 $sectionrec = array(
                     'course' => $this->get_courseid(),
-                    'section' => $i);
+                    'section' => $i,
+                    'timemodified' => time());
                 $DB->insert_record('course_sections', $sectionrec); // missing section created
             }
         }
@@ -1575,8 +1576,9 @@ class restore_section_structure_step extends restore_structure_step {
         $section = new stdclass();
         $section->course  = $this->get_courseid();
         $section->section = $data->number;
+        $section->timemodified = isset($data->timemodified) ? $this->apply_date_offset($data->timemodified) : 0;
         // Section doesn't exist, create it with all the info from backup
-        if (!$secrec = $DB->get_record('course_sections', (array)$section)) {
+        if (!$secrec = $DB->get_record('course_sections', ['course' => $this->get_courseid(), 'section' => $data->number])) {
             $section->name = $data->name;
             $section->summary = $data->summary;
             $section->summaryformat = $data->summaryformat;
@@ -1721,8 +1723,12 @@ class restore_section_structure_step extends restore_structure_step {
                     array('id' => $availfield->coursesectionid), MUST_EXIST);
             $newvalue = \core_availability\info::add_legacy_availability_field_condition(
                     $currentvalue, $availfield, $show);
-            $DB->set_field('course_sections', 'availability', $newvalue,
-                    array('id' => $availfield->coursesectionid));
+
+            $section = new stdClass();
+            $section->id = $availfield->coursesectionid;
+            $section->availability = $newvalue;
+            $section->timemodified = time();
+            $DB->update_record('course_sections', $section);
         }
     }
 
@@ -3083,7 +3089,8 @@ class restore_course_logs_structure_step extends restore_structure_step {
 
         $data = (object)($data);
 
-        $data->time = $this->apply_date_offset($data->time);
+        // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961.
+
         $data->userid = $this->get_mappingid('user', $data->userid);
         $data->course = $this->get_courseid();
         $data->cmid = 0;
@@ -3130,7 +3137,8 @@ class restore_activity_logs_structure_step extends restore_course_logs_structure
 
         $data = (object)($data);
 
-        $data->time = $this->apply_date_offset($data->time);
+        // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961.
+
         $data->userid = $this->get_mappingid('user', $data->userid);
         $data->course = $this->get_courseid();
         $data->cmid = $this->task->get_moduleid();
@@ -4032,11 +4040,13 @@ class restore_module_structure_step extends restore_structure_step {
         if (!$data->section) { // no sections in course, create section 0 and 1 and assign module to 1
             $sectionrec = array(
                 'course' => $this->get_courseid(),
-                'section' => 0);
+                'section' => 0,
+                'timemodified' => time());
             $DB->insert_record('course_sections', $sectionrec); // section 0
             $sectionrec = array(
                 'course' => $this->get_courseid(),
-                'section' => 1);
+                'section' => 1,
+                'timemodified' => time());
             $data->section = $DB->insert_record('course_sections', $sectionrec); // section 1
         }
         $data->groupingid= $this->get_mappingid('grouping', $data->groupingid);      // grouping
@@ -4090,7 +4100,12 @@ class restore_module_structure_step extends restore_structure_step {
         } else {
             $sequence = $newitemid;
         }
-        $DB->set_field('course_sections', 'sequence', $sequence, array('id' => $data->section));
+
+        $updatesection = new \stdClass();
+        $updatesection->id = $data->section;
+        $updatesection->sequence = $sequence;
+        $updatesection->timemodified = time();
+        $DB->update_record('course_sections', $updatesection);
 
         // If there is the legacy showavailability data, store this for later use.
         // (This data is not present when restoring 'new' backups.)
index abb479b..33bbe77 100644 (file)
@@ -90,7 +90,7 @@ echo html_writer::start_tag('tbody');
 if ($USER->id != $user->id) {
     echo html_writer::start_tag('tr');
     echo html_writer::start_tag('td', array('colspan' => '2'));
-    echo html_writer::tag('b', get_string('showinguser', 'completion'));
+    echo html_writer::tag('b', get_string('showinguser', 'completion') . ' ');
     $url = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $course->id));
     echo html_writer::link($url, fullname($user));
     echo html_writer::end_tag('td');
@@ -99,7 +99,7 @@ if ($USER->id != $user->id) {
 
 echo html_writer::start_tag('tr');
 echo html_writer::start_tag('td', array('colspan' => '2'));
-echo html_writer::tag('b', get_string('status'));
+echo html_writer::tag('b', get_string('status') . ' ');
 
 // Is course complete?
 $coursecomplete = $info->is_course_complete($user->id);
@@ -141,7 +141,7 @@ if (empty($completions)) {
 } else {
     echo html_writer::start_tag('tr');
     echo html_writer::start_tag('td', array('colspan' => '2'));
-    echo html_writer::tag('b', get_string('required'));
+    echo html_writer::tag('b', get_string('required') . ' ');
 
     // Get overall aggregation method.
     $overall = $info->get_aggregation_method();
@@ -214,7 +214,7 @@ if (empty($completions)) {
                     echo core_text::strtolower(get_string('any', 'completion'));
                 }
 
-                echo html_writer::end_tag('i') .core_text::strtolower(get_string('required')).')';
+                echo ' ' . html_writer::end_tag('i') .core_text::strtolower(get_string('required')).')';
                 $agg_type = false;
             }
         }
diff --git a/blocks/myoverview/amd/build/tab_preferences.min.js b/blocks/myoverview/amd/build/tab_preferences.min.js
new file mode 100644 (file)
index 0000000..da5bd97
Binary files /dev/null and b/blocks/myoverview/amd/build/tab_preferences.min.js differ
diff --git a/blocks/myoverview/amd/src/tab_preferences.js b/blocks/myoverview/amd/src/tab_preferences.js
new file mode 100644 (file)
index 0000000..25ac2ee
--- /dev/null
@@ -0,0 +1,61 @@
+// 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/>.
+
+/**
+ * Javascript used to save the user's tab preference.
+ *
+ * @package    block_myoverview
+ * @copyright  2017 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery', 'core/ajax', 'core/custom_interaction_events',
+    'core/notification'], function($, Ajax, CustomEvents, Notification) {
+
+    /**
+     * Registers an event that saves the user's tab preference when switching between them.
+     *
+     * @param {object} root The container element
+     */
+    var registerEventListeners = function(root) {
+        CustomEvents.define(root, [CustomEvents.events.activate]);
+        root.on(CustomEvents.events.activate, "[data-toggle='tab']", function(e) {
+            var tabname = $(e.currentTarget).data('tabname');
+            // Bootstrap does not change the URL when using BS tabs, so need to do this here.
+            // Also check to make sure the browser supports the history API.
+            if (typeof window.history.pushState === "function") {
+                window.history.pushState(null, null, '?myoverviewtab=' + tabname);
+            }
+            var request = {
+                methodname: 'core_user_update_user_preferences',
+                args: {
+                    preferences: [
+                        {
+                            type: 'block_myoverview_last_tab',
+                            value: tabname
+                        }
+                    ]
+                }
+            };
+
+            Ajax.call([request])[0]
+                .fail(Notification.exception);
+        });
+    };
+
+    return {
+        registerEventListeners: registerEventListeners
+    };
+});
index f22ce15..8afd4a1 100644 (file)
@@ -50,7 +50,16 @@ class block_myoverview extends block_base {
             return $this->content;
         }
 
-        $renderable = new \block_myoverview\output\main();
+        // Check if the tab to select wasn't passed in the URL, if so see if the user has any preference.
+        if (!$tab = optional_param('myoverviewtab', null, PARAM_ALPHA)) {
+            // Check if the user has no preference, if so get the site setting.
+            if (!$tab = get_user_preferences('block_myoverview_last_tab')) {
+                $config = get_config('block_myoverview');
+                $tab = $config->defaulttab;
+            }
+        }
+
+        $renderable = new \block_myoverview\output\main($tab);
         $renderer = $this->page->get_renderer('block_myoverview');
 
         $this->content = new stdClass();
@@ -68,4 +77,13 @@ class block_myoverview extends block_base {
     public function applicable_formats() {
         return array('my' => true);
     }
+
+    /**
+     * This block does contain a configuration settings.
+     *
+     * @return boolean
+     */
+    public function has_config() {
+        return true;
+    }
 }
index 31dc8c6..2949c09 100644 (file)
@@ -63,7 +63,8 @@ class courses_view implements renderable, templatable {
      * @return array
      */
     public function export_for_template(renderer_base $output) {
-        $today = time();
+        global $CFG;
+        require_once($CFG->dirroot.'/course/lib.php');
 
         // Build courses view data structure.
         $coursesview = [
@@ -73,8 +74,6 @@ class courses_view implements renderable, templatable {
         // How many courses we have per status?
         $coursesbystatus = ['past' => 0, 'inprogress' => 0, 'future' => 0];
         foreach ($this->courses as $course) {
-            $startdate = $course->startdate;
-            $enddate = $course->enddate;
             $courseid = $course->id;
             $context = \context_course::instance($courseid);
             $exporter = new course_summary_exporter($course, [
@@ -84,14 +83,17 @@ class courses_view implements renderable, templatable {
             // Convert summary to plain text.
             $exportedcourse->summary = content_to_text($exportedcourse->summary, $exportedcourse->summaryformat);
 
+            $courseprogress = null;
+
+            $classified = course_classify_for_timeline($course);
+
             if (isset($this->coursesprogress[$courseid])) {
-                $coursecompleted = $this->coursesprogress[$courseid]['completed'];
                 $courseprogress = $this->coursesprogress[$courseid]['progress'];
                 $exportedcourse->hasprogress = !is_null($courseprogress);
                 $exportedcourse->progress = $courseprogress;
             }
 
-            if ((isset($coursecompleted) && $coursecompleted) || (!empty($enddate) && $enddate < $today)) {
+            if ($classified == COURSE_TIMELINE_PAST) {
                 // Courses that have already ended.
                 $pastpages = floor($coursesbystatus['past'] / $this::COURSES_PER_PAGE);
 
@@ -100,7 +102,7 @@ class courses_view implements renderable, templatable {
                 $coursesview['past']['pages'][$pastpages]['page'] = $pastpages + 1;
                 $coursesview['past']['haspages'] = true;
                 $coursesbystatus['past']++;
-            } else if ($startdate > $today) {
+            } else if ($classified == COURSE_TIMELINE_FUTURE) {
                 // Courses that have not started yet.
                 $futurepages = floor($coursesbystatus['future'] / $this::COURSES_PER_PAGE);
 
index 6215a5a..2435f54 100644 (file)
@@ -29,6 +29,7 @@ use renderer_base;
 use templatable;
 use core_completion\progress;
 
+require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
 require_once($CFG->libdir . '/completionlib.php');
 
 /**
@@ -39,6 +40,20 @@ require_once($CFG->libdir . '/completionlib.php');
  */
 class main implements renderable, templatable {
 
+    /**
+     * @var string The tab to display.
+     */
+    public $tab;
+
+    /**
+     * Constructor.
+     *
+     * @param string $tab The tab to display.
+     */
+    public function __construct($tab) {
+        $this->tab = $tab;
+    }
+
     /**
      * Export this data so it can be used as the context for a mustache template.
      *
@@ -73,13 +88,24 @@ class main implements renderable, templatable {
         $nocoursesurl = $output->image_url('courses', 'block_myoverview')->out();
         $noeventsurl = $output->image_url('activities', 'block_myoverview')->out();
 
+        // Now, set the tab we are going to be viewing.
+        $viewingtimeline = false;
+        $viewingcourses = false;
+        if ($this->tab == BLOCK_MYOVERVIEW_TIMELINE_VIEW) {
+            $viewingtimeline = true;
+        } else {
+            $viewingcourses = true;
+        }
+
         return [
             'midnight' => usergetmidnight(time()),
             'coursesview' => $coursesview->export_for_template($output),
             'urls' => [
                 'nocourses' => $nocoursesurl,
                 'noevents' => $noeventsurl
-            ]
+            ],
+            'viewingtimeline' => $viewingtimeline,
+            'viewingcourses' => $viewingcourses
         ];
     }
 }
index 4c464f5..99fb83f 100644 (file)
@@ -22,6 +22,8 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['defaulttab'] = 'Default tab';
+$string['defaulttab_desc'] = 'This is the default tab that will be shown to a user.';
 $string['future'] = 'Future';
 $string['inprogress'] = 'In progress';
 $string['morecourses'] = 'More courses';
diff --git a/blocks/myoverview/lib.php b/blocks/myoverview/lib.php
new file mode 100644 (file)
index 0000000..a73db25
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains functions called by core.
+ *
+ * @package    block_myoverview
+ * @copyright  2017 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The timeline view.
+ */
+define('BLOCK_MYOVERVIEW_TIMELINE_VIEW', 'timeline');
+
+/**
+ * The courses view.
+ */
+define('BLOCK_MYOVERVIEW_COURSES_VIEW', 'courses');
+
+/**
+ * Returns the name of the user preferences as well as the details this plugin uses.
+ *
+ * @return array
+ */
+function block_myoverview_user_preferences() {
+    $preferences = array();
+    $preferences['block_myoverview_last_tab'] = array(
+        'type' => PARAM_ALPHA,
+        'null' => NULL_NOT_ALLOWED,
+        'default' => BLOCK_MYOVERVIEW_TIMELINE_VIEW,
+        'choices' => array(BLOCK_MYOVERVIEW_TIMELINE_VIEW, BLOCK_MYOVERVIEW_COURSES_VIEW)
+    );
+
+    return $preferences;
+}
diff --git a/blocks/myoverview/settings.php b/blocks/myoverview/settings.php
new file mode 100644 (file)
index 0000000..10f084d
--- /dev/null
@@ -0,0 +1,39 @@
+<?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/>.
+
+/**
+ * Settings for the overview block.
+ *
+ * @package    block_myoverview
+ * @copyright  2017 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
+
+if ($ADMIN->fulltree) {
+
+    $options = [
+        BLOCK_MYOVERVIEW_TIMELINE_VIEW => get_string('timeline', 'block_myoverview'),
+        BLOCK_MYOVERVIEW_COURSES_VIEW => get_string('courses')
+    ];
+
+    $settings->add(new admin_setting_configselect('block_myoverview/defaulttab',
+        get_string('defaulttab', 'block_myoverview'),
+        get_string('defaulttab_desc', 'block_myoverview'), 'timeline', $options));
+}
index 3a1a942..e9b21bd 100644 (file)
 }}
 
 <div id="block-myoverview-{{uniqid}}" class="block-myoverview" data-region="myoverview">
-    <ul class="nav nav-tabs" role="tablist">
+    <ul id="block-myoverview-view-choices-{{uniqid}}" class="nav nav-tabs" role="tablist">
         <li class="nav-item">
-            <a class="nav-link active" href="#myoverview_timeline_view" role="tab" data-toggle="tab">
+            <a class="nav-link {{#viewingtimeline}}active{{/viewingtimeline}}" href="#myoverview_timeline_view" role="tab" data-toggle="tab" data-tabname="timeline">
                 {{#str}} timeline, block_myoverview {{/str}}
             </a>
         </li>
         <li class="nav-item">
-            <a class="nav-link" href="#myoverview_courses_view" role="tab" data-toggle="tab">
+            <a class="nav-link {{#viewingcourses}}active{{/viewingcourses}}" href="#myoverview_courses_view" role="tab" data-toggle="tab" data-tabname="courses">
                 {{#str}} courses {{/str}}
             </a>
         </li>
     </ul>
     <div class="tab-content content-centred">
-        <div role="tabpanel" class="tab-pane fade in active" id="myoverview_timeline_view">
+        <div role="tabpanel" class="tab-pane fade {{#viewingtimeline}}in active{{/viewingtimeline}}" id="myoverview_timeline_view">
             {{> block_myoverview/timeline-view }}
         </div>
-        <div role="tabpanel" class="tab-pane fade" id="myoverview_courses_view">
+        <div role="tabpanel" class="tab-pane fade {{#viewingcourses}}in active{{/viewingcourses}}" id="myoverview_courses_view">
             {{#coursesview}}
                 {{> block_myoverview/courses-view }}
             {{/coursesview}}
         </div>
     </div>
 </div>
+{{#js}}
+require(['jquery', 'block_myoverview/tab_preferences'], function($, TabPreferences) {
+    var root = $('#block-myoverview-view-choices-{{uniqid}}');
+    TabPreferences.registerEventListeners(root);
+});
+{{/js}}
index 8bd3afe..e8d8692 100644 (file)
@@ -53,6 +53,7 @@ Feature: Course overview block show users their progress on courses
     And I am on "Course 1" course homepage
     And I follow "Test choice 1"
     And I follow "Dashboard" in the user menu
+    And I click on "Timeline" "link" in the "Course overview" "block"
     And I click on "Sort by courses" "link" in the "Course overview" "block"
     And I should see "100%" in the "Course overview" "block"
     And I click on "Courses" "link" in the "Course overview" "block"
index fd45f77..a637f48 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2017051500;         // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2017051502;         // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2017050500;         // Requires this Moodle version.
 $plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics).
index 4c69492..02578ec 100644 (file)
Binary files a/blocks/navigation/amd/build/ajax_response_renderer.min.js and b/blocks/navigation/amd/build/ajax_response_renderer.min.js differ
index d935742..99f2e28 100644 (file)
@@ -22,7 +22,7 @@
  * @copyright  2015 John Okely <john@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery'], function($) {
+define(['jquery', 'core/templates', 'core/notification', 'core/url'], function($, Templates, Notification, Url) {
 
     // Mappings for the different types of nodes coming from the navigation.
     // Copied from lib/navigationlib.php navigation_node constants.
@@ -75,28 +75,12 @@ define(['jquery'], function($) {
                 p.addClass('branch');
             }
 
-            if (node.icon && (!isBranch || node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE)) {
-                li.addClass('item_with_icon');
-                p.addClass('hasicon');
-
-                icon = $('<img/>');
-                icon.attr('alt', node.icon.alt);
-                icon.attr('title', node.icon.title);
-                icon.attr('src', M.util.image_url(node.icon.pix, node.icon.component));
-                $.each(node.icon.classes, function(index, className) {
-                    icon.addClass(className);
-                });
-            }
-
+            var eleToAddIcon = null;
             if (node.link) {
                 var link = $('<a title="' + node.title + '" href="' + node.link + '"></a>');
 
-                if (icon) {
-                    link.append(icon);
-                    link.append('<span class="item-content-wrap">' + node.name + '</span>');
-                } else {
-                    link.append(node.name);
-                }
+                eleToAddIcon = link;
+                link.append('<span class="item-content-wrap">' + node.name + '</span>');
 
                 if (node.hidden) {
                     link.addClass('dimmed');
@@ -106,12 +90,8 @@ define(['jquery'], function($) {
             } else {
                 var span = $('<span></span>');
 
-                if (icon) {
-                    span.append(icon);
-                    span.append('<span class="item-content-wrap">' + node.name + '</span>');
-                } else {
-                    span.append(node.name);
-                }
+                eleToAddIcon = span;
+                span.append('<span class="item-content-wrap">' + node.name + '</span>');
 
                 if (node.hidden) {
                     span.addClass('dimmed');
@@ -120,6 +100,31 @@ define(['jquery'], function($) {
                 p.append(span);
             }
 
+            if (node.icon && (!isBranch || node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE)) {
+                li.addClass('item_with_icon');
+                p.addClass('hasicon');
+
+                if (node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE) {
+                    icon = $('<img/>');
+                    icon.attr('alt', node.icon.alt);
+                    icon.attr('title', node.icon.title);
+                    icon.attr('src', Url.imageUrl(node.icon.pix, node.icon.component));
+                    $.each(node.icon.classes, function(index, className) {
+                        icon.addClass(className);
+                    });
+                    eleToAddIcon.prepend(icon);
+                } else {
+                    if (node.icon.component == 'moodle') {
+                        node.icon.component = 'core';
+                    }
+                    Templates.renderPix(node.icon.pix, node.icon.component, node.icon.title).then(function(html) {
+                        // Prepend.
+                        eleToAddIcon.prepend(html);
+                        return;
+                    }).catch(Notification.exception);
+                }
+            }
+
             li.append(p);
             ul.append(li);
 
index bb541f5..2369c1a 100644 (file)
@@ -71,6 +71,6 @@
     display: block;
 }
 
-.block_navigation .block_tree [aria-hidden="true"] {
+.block_navigation .block_tree [aria-hidden="true"]:not(.icon) {
     display: none;
 }
index e0c94b2..3d7e1d1 100644 (file)
@@ -58,7 +58,7 @@
     display: block;
 }
 
-.block_settings .block_tree  [aria-hidden="true"] {
+.block_settings .block_tree  [aria-hidden="true"]:not(.icon) {
     display: none;
 }
 
index 20b39ec..9ac8cbf 100644 (file)
@@ -71,11 +71,6 @@ class container {
      */
     protected static $eventretrievalstrategy;
 
-    /**
-     * @var array A list of callbacks to use.
-     */
-    protected static $callbacks = array();
-
     /**
      * @var \stdClass[] An array of cached courses to use with the event factory.
      */
@@ -91,16 +86,6 @@ class container {
      */
     private static function init() {
         if (empty(self::$eventfactory)) {
-            // When testing the container's components, we need to make sure
-            // the callback implementations in modules are not executed, since
-            // we cannot control their output from PHPUnit. To do this we have
-            // a set of 'testing' callbacks that the factory can use. This way
-            // we know exactly how the factory behaves when being tested.
-            $getcallback = function($which) {
-                return self::$callbacks[PHPUNIT_TEST ? 'testing' : 'production'][$which];
-            };
-
-            self::initcallbacks();
             self::$actionfactory = new action_factory();
             self::$eventmapper = new event_mapper(
                 // The event mapper we return from here needs to know how to
@@ -129,8 +114,8 @@ class container {
             );
 
             self::$eventfactory = new event_factory(
-                $getcallback('action'),
-                $getcallback('visibility'),
+                [self::class, 'apply_component_provide_event_action'],
+                [self::class, 'apply_component_is_event_visible'],
                 function ($dbrow) {
                     // At present we only have a bail-out check for events in course modules.
                     if (empty($dbrow->modulename)) {
@@ -183,6 +168,19 @@ class container {
         }
     }
 
+    /**
+     * Reset all static caches, called between tests.
+     */
+    public static function reset_caches() {
+        self::$eventfactory = null;
+        self::$eventmapper = null;
+        self::$eventvault = null;
+        self::$actionfactory = null;
+        self::$eventretrievalstrategy = null;
+        self::$coursecache = [];
+        self::$modulecache = [];
+    }
+
     /**
      * Gets the event factory.
      *
@@ -214,88 +212,74 @@ class container {
     }
 
     /**
-     * Initialises the callbacks.
+     * Calls callback 'core_calendar_provide_event_action' from the component responsible for the event
      *
-     * There are two sets here, one is used during PHPUnit runs.
-     * See the comment at the start of the init method for more
-     * detail.
+     * If no callback is present or callback returns null, there is no action on the event
+     * and it will not be displayed on the dashboard.
+     *
+     * @param event_interface $event
+     * @return action_event|event_interface
      */
-    private static function initcallbacks() {
-        self::$callbacks = array(
-            'testing' => array(
-                // Always return an action event.
-                'action' => function (event_interface $event) {
-                    return new action_event(
-                        $event,
-                        new \core_calendar\local\event\value_objects\action(
-                            'test',
-                            new \moodle_url('http://example.com'),
-                            420,
-                            true
-                        ));
-                },
-                // Always be visible.
-                'visibility' => function (event_interface $event) {
-                    return true;
-                }
-            ),
-            'production' => array(
-                // This function has type event_interface -> event_interface.
-                // This is enforced by the event_factory.
-                'action' => function (event_interface $event) {
-                    // Callbacks will get supplied a "legacy" version
-                    // of the event class.
-                    $mapper = self::$eventmapper;
-                    $action = null;
-                    if ($event->get_course_module()) {
-                        // TODO MDL-58866 Only activity modules currently support this callback.
-                        // Any other event will not be displayed on the dashboard.
-                        $action = component_callback(
-                            'mod_' . $event->get_course_module()->get('modname'),
-                            'core_calendar_provide_event_action',
-                            [
-                                $mapper->from_event_to_legacy_event($event),
-                                self::$actionfactory
-                            ]
-                        );
-                    }
+    public static function apply_component_provide_event_action(event_interface $event) {
+        // Callbacks will get supplied a "legacy" version
+        // of the event class.
+        $mapper = self::$eventmapper;
+        $action = null;
+        if ($event->get_course_module()) {
+            // TODO MDL-58866 Only activity modules currently support this callback.
+            // Any other event will not be displayed on the dashboard.
+            $action = component_callback(
+                'mod_' . $event->get_course_module()->get('modname'),
+                'core_calendar_provide_event_action',
+                [
+                    $mapper->from_event_to_legacy_event($event),
+                    self::$actionfactory
+                ]
+            );
+        }
 
-                    // If we get an action back, return an action event, otherwise
-                    // continue piping through the original event.
-                    //
-                    // If a module does not implement the callback, component_callback
-                    // returns null.
-                    return $action ? new action_event($event, $action) : $event;
-                },
-                // This function has type event_interface -> bool.
-                // This is enforced by the event_factory.
-                'visibility' => function (event_interface $event) {
-                    $mapper = self::$eventmapper;
-                    $eventvisible = null;
-                    if ($event->get_course_module()) {
-                        // TODO MDL-58866 Only activity modules currently support this callback.
-                        $eventvisible = component_callback(
-                            'mod_' . $event->get_course_module()->get('modname'),
-                            'core_calendar_is_event_visible',
-                            [
-                                $mapper->from_event_to_legacy_event($event)
-                            ]
-                        );
-                    }
+        // If we get an action back, return an action event, otherwise
+        // continue piping through the original event.
+        //
+        // If a module does not implement the callback, component_callback
+        // returns null.
+        return $action ? new action_event($event, $action) : $event;
+    }
 
-                    // Do not display the event if there is nothing to action.
-                    if ($event instanceof action_event_interface && $event->get_action()->get_item_count() === 0) {
-                        return false;
-                    }
+    /**
+     * Calls callback 'core_calendar_is_event_visible' from the component responsible for the event
+     *
+     * The visibility callback is optional, if not present it is assumed as visible.
+     * If it is an actionable event but the get_item_count() returns 0 the visibility
+     * is set to false.
+     *
+     * @param event_interface $event
+     * @return bool
+     */
+    public static function apply_component_is_event_visible(event_interface $event) {
+        $mapper = self::$eventmapper;
+        $eventvisible = null;
+        if ($event->get_course_module()) {
+            // TODO MDL-58866 Only activity modules currently support this callback.
+            $eventvisible = component_callback(
+                'mod_' . $event->get_course_module()->get('modname'),
+                'core_calendar_is_event_visible',
+                [
+                    $mapper->from_event_to_legacy_event($event)
+                ]
+            );
+        }
 
-                    // Module does not implement the callback, event should be visible.
-                    if (is_null($eventvisible)) {
-                        return true;
-                    }
+        // Do not display the event if there is nothing to action.
+        if ($event instanceof action_event_interface && $event->get_action()->get_item_count() === 0) {
+            return false;
+        }
+
+        // Module does not implement the callback, event should be visible.
+        if (is_null($eventvisible)) {
+            return true;
+        }
 
-                    return $eventvisible ? true : false;
-                }
-            ),
-        );
+        return $eventvisible ? true : false;
     }
 }
index 8c5d175..04e6cf3 100644 (file)
@@ -77,7 +77,8 @@ if (!empty($day) && !empty($mon) && !empty($year)) {
 }
 
 if ($courseid != SITEID && !empty($courseid)) {
-    $course = $DB->get_record('course', array('id' => $courseid));
+    // Course ID must be valid and existing.
+    $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
     $courses = array($course->id => $course);
     $issite = false;
 } else {
@@ -85,7 +86,7 @@ if ($courseid != SITEID && !empty($courseid)) {
     $courses = calendar_get_default_courses();
     $issite = true;
 }
-require_course_login($course);
+require_login($course, false);
 
 $url = new moodle_url('/calendar/export.php', array('time' => $time));
 
index 55ad67f..f6f1e4a 100644 (file)
@@ -1457,6 +1457,15 @@ function calendar_get_mini($courses, $groups, $users, $calmonth = false, $calyea
                         $name = format_string($event->name, true);
                     }
                 }
+                // Include course's shortname into the event name, if applicable.
+                if (!empty($event->courseid) && $event->courseid !== SITEID) {
+                    $course = get_course($event->courseid);
+                    $eventnameparams = (object)[
+                        'name' => $name,
+                        'course' => format_string($course->shortname, true, array('context' => $event->context))
+                    ];
+                    $name = get_string('eventnameandcourse', 'calendar', $eventnameparams);
+                }
                 $popupcontent .= \html_writer::link($dayhref, $name);
                 $popupcontent .= \html_writer::end_tag('div');
             }
index 3e4e0e9..f1c74d6 100644 (file)
@@ -45,13 +45,15 @@ $PAGE->set_pagelayout('admin');
 $PAGE->navbar->add(get_string('managesubscriptions', 'calendar'));
 
 if ($courseid != SITEID && !empty($courseid)) {
-    $course = $DB->get_record('course', array('id' => $courseid));
+    // Course ID must be valid and existing.
+    $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
     $courses = array($course->id => $course);
 } else {
     $course = get_site();
     $courses = calendar_get_default_courses();
 }
-require_course_login($course);
+require_login($course, false);
+
 if (!calendar_user_can_add_event($course)) {
     print_error('errorcannotimport', 'calendar');
 }
index 8abfb03..6711693 100644 (file)
@@ -502,7 +502,22 @@ class core_calendar_renderer extends plugin_renderer_base {
                         $attributes['class'] = $events[$eventindex]->class;
                     }
                     $dayhref->set_anchor('event_'.$events[$eventindex]->id);
-                    $link = html_writer::link($dayhref, format_string($events[$eventindex]->name, true));
+
+                    $eventcontext = $events[$eventindex]->context;
+                    $eventformatopts = array('context' => $eventcontext);
+                    // Get event name.
+                    $eventname = format_string($events[$eventindex]->name, true, $eventformatopts);
+                    // Include course's shortname into the event name, if applicable.
+                    $courseid = $events[$eventindex]->courseid;
+                    if (!empty($courseid) && $courseid !== SITEID) {
+                        $course = get_course($courseid);
+                        $eventnameparams = (object)[
+                            'name' => $eventname,
+                            'course' => format_string($course->shortname, true, $eventformatopts)
+                        ];
+                        $eventname = get_string('eventnameandcourse', 'calendar', $eventnameparams);
+                    }
+                    $link = html_writer::link($dayhref, $eventname);
                     $cell->text .= html_writer::tag('li', $link, $attributes);
                 }
                 $cell->text .= html_writer::end_tag('ul');
index 9912bcc..fc0ecc2 100644 (file)
@@ -674,13 +674,15 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
      */
     public function test_get_calendar_events_override() {
         $user = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
         $teacher = $this->getDataGenerator()->create_user();
         $anotheruser = $this->getDataGenerator()->create_user();
         $course = $this->getDataGenerator()->create_course();
         $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
         $moduleinstance = $generator->create_instance(['course' => $course->id]);
 
-        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student');
         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
         $this->resetAfterTest(true);
         $this->setAdminUser();
@@ -692,11 +694,12 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         ];
 
         $now = time();
+        // Create two events - one for everybody in the course and one only for the first student.
         $event1 = $this->create_calendar_event('Base event', 0, 'due', 0, $now + DAYSECS, $params + ['courseid' => $course->id]);
         $event2 = $this->create_calendar_event('User event', $user->id, 'due', 0, $now + 2*DAYSECS, $params + ['courseid' => 0]);
 
-        // Retrieve course events for teacher - only one "Base event" is returned.
-        $this->setUser($teacher);
+        // Retrieve course events for the second student - only one "Base event" is returned.
+        $this->setUser($user2);
         $paramevents = array('courseids' => array($course->id));
         $options = array ('siteevents' => true, 'userevents' => true);
         $events = core_calendar_external::get_calendar_events($paramevents, $options);
@@ -705,7 +708,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(0, count($events['warnings']));
         $this->assertEquals('Base event', $events['events'][0]['name']);
 
-        // Retrieve events for user - both events are returned.
+        // Retrieve events for the first student - both events are returned.
         $this->setUser($user);
         $events = core_calendar_external::get_calendar_events($paramevents, $options);
         $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
index a55aee6..18245a5 100644 (file)
@@ -85,7 +85,8 @@ $url->param('time', $time);
 $PAGE->set_url($url);
 
 if ($courseid != SITEID && !empty($courseid)) {
-    $course = $DB->get_record('course', array('id' => $courseid));
+    // Course ID must be valid and existing.
+    $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
     $courses = array($course->id => $course);
     $issite = false;
     navigation_node::override_active_url(new moodle_url('/course/view.php', array('id' => $course->id)));
@@ -95,7 +96,7 @@ if ($courseid != SITEID && !empty($courseid)) {
     $issite = true;
 }
 
-require_course_login($course);
+require_login($course, false);
 
 $calendar = new calendar_information(0, 0, 0, $time);
 $calendar->prepare_for_view($course, $courses);
index 4e0a33c..04b05c4 100644 (file)
@@ -74,7 +74,16 @@ class completion_completion extends data_object {
      * @return data_object instance of data_object or false if none found.
      */
     public static function fetch($params) {
-        return self::fetch_helper('course_completions', __CLASS__, $params);
+        $cache = cache::make('core', 'coursecompletion');
+
+        $key = $params['userid'] . '_' . $params['course'];
+        if ($hit = $cache->get($key)) {
+            return $hit['value'];
+        }
+
+        $tocache = self::fetch_helper('course_completions', __CLASS__, $params);
+        $cache->set($key, ['value' => $tocache]);
+        return $tocache;
     }
 
     /**
@@ -179,9 +188,10 @@ class completion_completion extends data_object {
             $this->timeenrolled = 0;
         }
 
+        $result = false;
         // Save record
         if ($this->id) {
-            return $this->update();
+            $result = $this->update();
         } else {
             // Make sure reaggregate field is not null
             if (!$this->reaggregate) {
@@ -193,7 +203,17 @@ class completion_completion extends data_object {
                 $this->timestarted = 0;
             }
 
-            return $this->insert();
+            $result = $this->insert();
+        }
+
+        if ($result) {
+            // Update the cached record.
+            $cache = cache::make('core', 'coursecompletion');
+            $data = $this->get_record_data();
+            $key = $data->userid . '_' . $data->course;
+            $cache->set($key, ['value' => $data]);
         }
+
+        return $result;
     }
 }
index f16bdd6..b630ad8 100644 (file)
@@ -559,7 +559,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 // so there is definitely something to print.
                 $formattedinfo = \core_availability\info::format_info(
                         $section->availableinfo, $section->course);
-                $o .= $this->courserenderer->availability_info($formattedinfo);
+                $o .= $this->courserenderer->availability_info($formattedinfo, 'isrestricted');
             }
         } else if ($canviewhidden && !empty($CFG->enableavailability)) {
             // Check if there is an availability restriction.
@@ -568,7 +568,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             if ($fullinfo) {
                 $formattedinfo = \core_availability\info::format_info(
                         $fullinfo, $section->course);
-                $o .= $this->courserenderer->availability_info($formattedinfo);
+                $o .= $this->courserenderer->availability_info($formattedinfo, 'isrestricted isfullinfo');
             }
         }
         return $o;
index e60bd2c..f079a74 100644 (file)
@@ -55,6 +55,10 @@ define('FIRSTUSEDEXCELROW', 3);
 define('MOD_CLASS_ACTIVITY', 0);
 define('MOD_CLASS_RESOURCE', 1);
 
+define('COURSE_TIMELINE_PAST', 'past');
+define('COURSE_TIMELINE_INPROGRESS', 'inprogress');
+define('COURSE_TIMELINE_FUTURE', 'future');
+
 function make_log_url($module, $url) {
     switch ($module) {
         case 'course':
@@ -863,6 +867,7 @@ function course_create_section($courseorid, $position = 0, $skipcheck = false) {
     $cw->name = null;
     $cw->visible = 1;
     $cw->availability = null;
+    $cw->timemodified = time();
     $cw->id = $DB->insert_record("course_sections", $cw);
 
     // Now move it to the specified position.
@@ -1611,6 +1616,7 @@ function course_update_section($course, $section, $data) {
 
     // Update record in the DB and course format options.
     $data['id'] = $section->id;
+    $data['timemodified'] = time();
     $DB->update_record('course_sections', $data);
     rebuild_course_cache($courseid, true);
     course_get_format($courseid)->update_section_format_options($data);
@@ -4001,6 +4007,46 @@ function course_check_updates($course, $tocheck, $filter = array()) {
     return array($instances, $warnings);
 }
 
+/**
+ * This function classifies a course as past, in progress or future.
+ *
+ * This function may incur a DB hit to calculate course completion.
+ * @param stdClass $course Course record
+ * @param stdClass $user User record (optional - defaults to $USER).
+ * @param completion_info $completioninfo Completion record for the user (optional - will be fetched if required).
+ * @return string (one of COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_PAST)
+ */
+function course_classify_for_timeline($course, $user = null, $completioninfo = null) {
+    global $USER;
+
+    if ($user == null) {
+        $user = $USER;
+    }
+
+    $today = time();
+    // End date past.
+    if (!empty($course->enddate) && $course->enddate < $today) {
+        return COURSE_TIMELINE_PAST;
+    }
+
+    if ($completioninfo == null) {
+        $completioninfo = new completion_info($course);
+    }
+
+    // Course was completed.
+    if ($completioninfo->is_enabled() && $completioninfo->is_course_complete($user->id)) {
+        return COURSE_TIMELINE_PAST;
+    }
+
+    // Start date not reached.
+    if (!empty($course->startdate) && $course->startdate > $today) {
+        return COURSE_TIMELINE_FUTURE;
+    }
+
+    // Everything else is in progress.
+    return COURSE_TIMELINE_INPROGRESS;
+}
+
 /**
  * Check module updates since a given time.
  * This function checks for updates in the module config, file areas, completion, grades, comments and ratings.
index bee3fe7..565f90c 100644 (file)
@@ -728,7 +728,24 @@ class core_course_renderer extends plugin_renderer_base {
      * @return string
      */
     public function availability_info($text, $additionalclasses = '') {
+
         $data = ['text' => $text, 'classes' => $additionalclasses];
+        $additionalclasses = array_filter(explode(' ', $additionalclasses));
+
+        if (in_array('ishidden', $additionalclasses)) {
+            $data['ishidden'] = 1;
+
+        } else if (in_array('isstealth', $additionalclasses)) {
+            $data['isstealth'] = 1;
+
+        } else if (in_array('isrestricted', $additionalclasses)) {
+            $data['isrestricted'] = 1;
+
+            if (in_array('isfullinfo', $additionalclasses)) {
+                $data['isfullinfo'] = 1;
+            }
+        }
+
         return $this->render_from_template('core/availability_info', $data);
     }
 
@@ -752,7 +769,7 @@ class core_course_renderer extends plugin_renderer_base {
             if (!empty($mod->availableinfo)) {
                 $formattedinfo = \core_availability\info::format_info(
                         $mod->availableinfo, $mod->get_course());
-                $output = $this->availability_info($formattedinfo);
+                $output = $this->availability_info($formattedinfo, 'isrestricted');
             }
             return $output;
         }
@@ -775,9 +792,9 @@ class core_course_renderer extends plugin_renderer_base {
             // 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 = '';
+                $hidinfoclass = 'isrestricted isfullinfo';
                 if (!$mod->visible) {
-                    $hidinfoclass = 'hide';
+                    $hidinfoclass .= ' hide';
                 }
                 $ci = new \core_availability\info_module($mod);
                 $fullinfo = $ci->get_full_information();
index 61169d5..5ce1ffa 100644 (file)
@@ -678,6 +678,29 @@ class core_course_courselib_testcase extends advanced_testcase {
         }
     }
 
+    public function test_update_course_section_time_modified() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Create the course with sections.
+        $course = $this->getDataGenerator()->create_course(array('numsections' => 10), array('createsections' => true));
+        $sections = $DB->get_records('course_sections', array('course' => $course->id));
+
+        // Get the last section's time modified value.
+        $section = array_pop($sections);
+        $oldtimemodified = $section->timemodified;
+
+        // Update the section.
+        $this->waitForSecond(); // Ensuring that the section update occurs at a different timestamp.
+        course_update_section($course, $section, array());
+
+        // Check that the time has changed.
+        $section = $DB->get_record('course_sections', array('id' => $section->id));
+        $newtimemodified = $section->timemodified;
+        $this->assertGreaterThan($oldtimemodified, $newtimemodified);
+    }
+
     public function test_course_add_cm_to_section() {
         global $DB;
         $this->resetAfterTest(true);
@@ -3684,4 +3707,52 @@ class core_course_courselib_testcase extends advanced_testcase {
         }
         $this->assertEquals(2, $count);
     }
+
+    public function test_classify_course_for_timeline() {
+        global $DB, $CFG;
+
+        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php');
+
+        set_config('enablecompletion', COMPLETION_ENABLED);
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        // Create courses for testing.
+        $generator = $this->getDataGenerator();
+        $future = time() + 3600;
+        $past = time() - 3600;
+        $futurecourse = $generator->create_course(['startdate' => $future]);
+        $pastcourse = $generator->create_course(['startdate' => $past - 60, 'enddate' => $past]);
+        $completedcourse = $generator->create_course(['enablecompletion' => COMPLETION_ENABLED]);
+        $inprogresscourse = $generator->create_course();
+
+        // Set completion rules.
+        $criteriadata = new stdClass();
+        $criteriadata->id = $completedcourse->id;
+
+        // Self completion.
+        $criteriadata->criteria_self = COMPLETION_CRITERIA_TYPE_SELF;
+        $class = 'completion_criteria_self';
+        $criterion = new $class();
+        $criterion->update_config($criteriadata);
+
+        $user = $this->getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($user->id, $futurecourse->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user->id, $pastcourse->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user->id, $completedcourse->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user->id, $inprogresscourse->id, $studentrole->id);
+
+        $this->setUser($user);
+        core_completion_external::mark_course_self_completed($completedcourse->id);
+        $ccompletion = new completion_completion(array('course' => $completedcourse->id, 'userid' => $user->id));
+        $ccompletion->mark_complete();
+
+        // Aggregate the completions.
+        $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($pastcourse));
+        $this->assertEquals(COURSE_TIMELINE_FUTURE, course_classify_for_timeline($futurecourse));
+        $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($completedcourse));
+        $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($inprogresscourse));
+    }
 }
index 8012a7f..d932dd8 100644 (file)
@@ -361,6 +361,40 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         core_course_external::update_categories($categories);
     }
 
+    /**
+     * Test create_courses numsections
+     */
+    public function test_create_course_numsections() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Set the required capabilities by the external function.
+        $contextid = context_system::instance()->id;
+        $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
+        $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
+
+        $numsections = 10;
+        $category  = self::getDataGenerator()->create_category();
+
+        // Create base categories.
+        $course1['fullname'] = 'Test course 1';
+        $course1['shortname'] = 'Testcourse1';
+        $course1['categoryid'] = $category->id;
+        $course1['courseformatoptions'][] = array('name' => 'numsections', 'value' => $numsections);
+
+        $courses = array($course1);
+
+        $createdcourses = core_course_external::create_courses($courses);
+        foreach ($createdcourses as $createdcourse) {
+            $existingsections = $DB->get_records('course_sections', array('course' => $createdcourse['id']));
+            $modinfo = get_fast_modinfo($createdcourse['id']);
+            $sections = $modinfo->get_section_info_all();
+            $this->assertEquals(count($sections), $numsections + 1); // Includes generic section.
+            $this->assertEquals(count($existingsections), $numsections + 1); // Includes generic section.
+        }
+    }
+
     /**
      * Test create_courses
      */
index 90ca368..0eaa555 100644 (file)
@@ -42,11 +42,9 @@ class writer extends \core\dataformat\base {
     public $extension = ".html";
 
     /**
-     * Write the start of the format
-     *
-     * @param array $columns
+     * Write the start of the output
      */
-    public function write_header($columns) {
+    public function start_output() {
         echo "<!DOCTYPE html><html>";
         echo \html_writer::tag('title', $this->filename);
         echo "<style>
@@ -75,9 +73,16 @@ table {
     margin: auto;
 }
 </style>
-<body>
-<table border=1 cellspacing=0 cellpadding=3>
-";
+<body>";
+    }
+
+    /**
+     * Write the start of the sheet we will be adding data to.
+     *
+     * @param array $columns
+     */
+    public function start_sheet($columns) {
+        echo "<table border=1 cellspacing=0 cellpadding=3>";
         echo \html_writer::start_tag('tr');
         foreach ($columns as $k => $v) {
             echo \html_writer::tag('th', $v);
@@ -100,12 +105,18 @@ table {
     }
 
     /**
-     * Write the end of the format
+     * Write the end of the sheet containing the data.
      *
      * @param array $columns
      */
-    public function write_footer($columns) {
-        echo "</table></body></html>";
+    public function close_sheet($columns) {
+        echo "</table>";
     }
 
+    /**
+     * Write the end of the sheet containing the data.
+     */
+    public function close_output() {
+        echo "</body></html>";
+    }
 }
index 84f3cc6..8e3cfba 100644 (file)
@@ -41,12 +41,31 @@ class writer extends \core\dataformat\base {
     /** @var $extension */
     public $extension = ".json";
 
+    /** @var $sheetstarted */
+    public $sheetstarted = false;
+
+    /** @var $sheetdatadded */
+    public $sheetdatadded = false;
+
+    /**
+     * Write the start of the file.
+     */
+    public function start_output() {
+        echo "[";
+    }
+
     /**
-     * Write the start of the format
+     * Write the start of the sheet we will be adding data to.
      *
      * @param array $columns
      */
-    public function write_header($columns) {
+    public function start_sheet($columns) {
+        if ($this->sheetstarted) {
+            echo ",";
+        } else {
+            $this->sheetstarted = true;
+        }
+        $this->sheetdatadded = false;
         echo "[";
     }
 
@@ -57,19 +76,28 @@ class writer extends \core\dataformat\base {
      * @param int $rownum
      */
     public function write_record($record, $rownum) {
-        if ($rownum) {
+        if ($this->sheetdatadded) {
             echo ",";
         }
+
         echo json_encode($record);
+
+        $this->sheetdatadded = true;
     }
 
     /**
-     * Write the end of the format
+     * Write the end of the sheet containing the data.
      *
      * @param array $columns
      */
-    public function write_footer($columns) {
+    public function close_sheet($columns) {
         echo "]";
     }
 
+    /**
+     * Write the end of the file.
+     */
+    public function close_output() {
+        echo "]";
+    }
 }
index bfd29d1..5868991 100644 (file)
@@ -1,7 +1,14 @@
 This files describes API changes in /dataformat/ download system,
 information provided here is intended especially for developers.
 
-=== 3.1 ===
-* Added new plugin system with low memory support for csv, ods, xls and json
+=== 3.4 ===
 
+* In order to allow multiple sheets in an exported file the functions write_header() and write_footer() have
+  been removed from core dataformat plugins and have been replaced.
+  - write_header() has been replaced with the two functions start_output() and start_sheet().
+  - write_footer() has been replaced with the two functions close_output() and close_sheet().
+  For backwards compatibility write_header() and write_footer() will continue to work but if used will
+  trigger the function error_log().
 
+=== 3.1 ===
+* Added new plugin system with low memory support for csv, ods, xls and json
index b232eda..211e149 100644 (file)
@@ -73,8 +73,8 @@ class filter_urltolink extends moodle_text_filter {
         //<a href="blah">
         //&lt;a href="blah"&gt;
         //&lt;a href="blah">
-        $filterignoretagsopen  = array('<a\s[^>]+?>');
-        $filterignoretagsclose = array('</a>');
+        $filterignoretagsopen  = array('<a\s[^>]+?>', '<span[^>]+?class="nolink"[^>]*?>');
+        $filterignoretagsclose = array('</a>', '</span>');
         filter_save_ignore_tags($text,$filterignoretagsopen,$filterignoretagsclose,$ignoretags);
 
         // Check if we support unicode modifiers in regular expressions. Cache it.
@@ -174,4 +174,3 @@ function filter_urltolink_img_callback($link) {
     }
     return '<img class="filter_urltolink_image" alt="" src="'.$link[1].'" />';
 }
-
index 0ae7f89..1dca5b7 100644 (file)
@@ -174,6 +174,9 @@ class filter_urltolink_filter_testcase extends basic_testcase {
             '<link rel="search" type="application/opensearchdescription+xml" href="/osd.jsp" title="Peer review - Moodle Tracker"/>' => '<link rel="search" type="application/opensearchdescription+xml" href="/osd.jsp" title="Peer review - Moodle Tracker"/>',
             '<a href="https://docs.moodle.org/dev/Main_Page"></a><span>www.google.com</span><span class="placeholder"></span>' => '<a href="https://docs.moodle.org/dev/Main_Page"></a><span><a href="http://www.google.com" class="_blanktarget">www.google.com</a></span><span class="placeholder"></span>',
             'http://nolandforzombies.com <a href="zombiesFTW.com">Zombies FTW</a> http://aliens.org' => '<a href="http://nolandforzombies.com" class="_blanktarget">http://nolandforzombies.com</a> <a href="zombiesFTW.com">Zombies FTW</a> <a href="http://aliens.org" class="_blanktarget">http://aliens.org</a>',
+            // Test 'nolink' class.
+            'URL: <span class="nolink">http://moodle.org</span>' => 'URL: <span class="nolink">http://moodle.org</span>',
+            '<span class="nolink">URL: http://moodle.org</span>' => '<span class="nolink">URL: http://moodle.org</span>',
             //URLs in Javascript. Commented out as part of MDL-21183
             //'var url="http://moodle.org";'=>'var url="http://moodle.org";',
             //'var url = "http://moodle.org";'=>'var url = "http://moodle.org";',
index 2313c63..5dfb8e9 100644 (file)
@@ -264,142 +264,143 @@ class edit_item_form extends moodleform {
         $mform =& $this->_form;
 
         if ($id = $mform->getElementValue('id')) {
-            $grade_item = grade_item::fetch(array('id'=>$id));
+            $gradeitem = grade_item::fetch(array('id' => $id));
+            $parentcategory = $gradeitem->get_parent_category();
+        } else {
+            // If we do not have an id, we are creating a new grade item.
+            $gradeitem = new grade_item(array('courseid' => $COURSE->id, 'itemtype' => 'manual'), false);
+
+            // Assign the course category to this grade item.
+            $parentcategory = grade_category::fetch_course_category($COURSE->id);
+            $gradeitem->parent_category = $parentcategory;
+        }
+
+        if (!$gradeitem->is_raw_used()) {
+            $mform->removeElement('plusfactor');
+            $mform->removeElement('multfactor');
+        }
 
-            if (!$grade_item->is_raw_used()) {
-                $mform->removeElement('plusfactor');
-                $mform->removeElement('multfactor');
+        if ($gradeitem->is_outcome_item()) {
+            // We have to prevent incompatible modifications of outcomes if outcomes disabled.
+            $mform->removeElement('grademax');
+            if ($mform->elementExists('grademin')) {
+                $mform->removeElement('grademin');
             }
+            $mform->removeElement('gradetype');
+            $mform->removeElement('display');
+            $mform->removeElement('decimals');
+            $mform->hardFreeze('scaleid');
 
-            if ($grade_item->is_outcome_item()) {
-                // we have to prevent incompatible modifications of outcomes if outcomes disabled
-                $mform->removeElement('grademax');
+        } else {
+            if ($gradeitem->is_external_item()) {
+                // Following items are set up from modules and should not be overrided by user.
                 if ($mform->elementExists('grademin')) {
-                    $mform->removeElement('grademin');
+                    // The site setting grade_report_showmin may have prevented grademin being added to the form.
+                    $mform->hardFreeze('grademin');
+                }
+                $mform->hardFreeze('itemname,gradetype,grademax,scaleid');
+                if ($gradeitem->itemnumber == 0) {
+                    // The idnumber of grade itemnumber 0 is synced with course_modules.
+                    $mform->hardFreeze('idnumber');
                 }
-                $mform->removeElement('gradetype');
-                $mform->removeElement('display');
-                $mform->removeElement('decimals');
-                $mform->hardFreeze('scaleid');
 
-            } else {
-                if ($grade_item->is_external_item()) {
-                    // following items are set up from modules and should not be overrided by user
+                // For external items we can not change the grade type, even if no grades exist, so if it is set to
+                // scale, then remove the grademax and grademin fields from the form - no point displaying them.
+                if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
+                    $mform->removeElement('grademax');
                     if ($mform->elementExists('grademin')) {
-                        // The site setting grade_report_showmin may have prevented grademin being added to the form.
-                        $mform->hardFreeze('grademin');
-                    }
-                    $mform->hardFreeze('itemname,gradetype,grademax,scaleid');
-                    if ($grade_item->itemnumber == 0) {
-                        // the idnumber of grade itemnumber 0 is synced with course_modules
-                        $mform->hardFreeze('idnumber');
+                        $mform->removeElement('grademin');
                     }
+                } else { // Not using scale, so remove it.
+                    $mform->removeElement('scaleid');
+                }
 
-                    // For external items we can not change the grade type, even if no grades exist, so if it is set to
-                    // scale, then remove the grademax and grademin fields from the form - no point displaying them.
-                    if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
-                        $mform->removeElement('grademax');
-                        if ($mform->elementExists('grademin')) {
-                            $mform->removeElement('grademin');
-                        }
-                    } else { // Not using scale, so remove it.
-                        $mform->removeElement('scaleid');
-                    }
+                // Always remove the rescale grades element if it's an external item.
+                $mform->removeElement('rescalegrades');
+            } else if ($gradeitem->has_grades()) {
+                // Can't change the grade type or the scale if there are grades.
+                $mform->hardFreeze('gradetype, scaleid');
 
-                    // Always remove the rescale grades element if it's an external item.
+                // If we are using scales then remove the unnecessary rescale and grade fields.
+                if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
                     $mform->removeElement('rescalegrades');
-                } else if ($grade_item->has_grades()) {
-                    // Can't change the grade type or the scale if there are grades.
-                    $mform->hardFreeze('gradetype, scaleid');
-
-                    // If we are using scales then remove the unnecessary rescale and grade fields.
-                    if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
-                        $mform->removeElement('rescalegrades');
-                        $mform->removeElement('grademax');
-                        if ($mform->elementExists('grademin')) {
-                            $mform->removeElement('grademin');
-                        }
-                    } else { // Remove the scale field.
-                        $mform->removeElement('scaleid');
-                        // Set the maximum grade to disabled unless a grade is chosen.
-                        $mform->disabledIf('grademax', 'rescalegrades', 'eq', '');
+                    $mform->removeElement('grademax');
+                    if ($mform->elementExists('grademin')) {
+                        $mform->removeElement('grademin');
                     }
-                } else {
-                    // Remove the rescale element if there are no grades.
-                    $mform->removeElement('rescalegrades');
+                } else { // Remove the scale field.
+                    $mform->removeElement('scaleid');
+                    // Set the maximum grade to disabled unless a grade is chosen.
+                    $mform->disabledIf('grademax', 'rescalegrades', 'eq', '');
                 }
+            } else {
+                // Remove the rescale element if there are no grades.
+                $mform->removeElement('rescalegrades');
             }
+        }
+
+        // If we wanted to change parent of existing item - we would have to verify there are no circular references in parents!!!
+        if ($id && $mform->elementExists('parentcategory')) {
+            $mform->hardFreeze('parentcategory');
+        }
+
+        $parentcategory->apply_forced_settings();
 
-            // if we wanted to change parent of existing item - we would have to verify there are no circular references in parents!!!
-            if ($mform->elementExists('parentcategory')) {
-                $mform->hardFreeze('parentcategory');
+        if (!$parentcategory->is_aggregationcoef_used()) {
+            if ($mform->elementExists('aggregationcoef')) {
+                $mform->removeElement('aggregationcoef');
             }
 
-            $parent_category = $grade_item->get_parent_category();
-            $parent_category->apply_forced_settings();
+        } else {
+            $coefstring = $gradeitem->get_coefstring();
 
-            if (!$parent_category->is_aggregationcoef_used()) {
-                if ($mform->elementExists('aggregationcoef')) {
-                    $mform->removeElement('aggregationcoef');
+            if ($coefstring !== '') {
+                if ($coefstring == 'aggregationcoefextrasum' || $coefstring == 'aggregationcoefextraweightsum') {
+                    // The advcheckbox is not compatible with disabledIf!
+                    $coefstring = 'aggregationcoefextrasum';
+                    $element =& $mform->createElement('checkbox', 'aggregationcoef', get_string($coefstring, 'grades'));
+                } else {
+                    $element =& $mform->createElement('text', 'aggregationcoef', get_string($coefstring, 'grades'));
                 }
-
-            } else {
-                $coefstring = $grade_item->get_coefstring();
-
-                if ($coefstring !== '') {
-                    if ($coefstring == 'aggregationcoefextrasum' || $coefstring == 'aggregationcoefextraweightsum') {
-                        // advcheckbox is not compatible with disabledIf!
-                        $coefstring = 'aggregationcoefextrasum';
-                        $element =& $mform->createElement('checkbox', 'aggregationcoef', get_string($coefstring, 'grades'));
-                    } else {
-                        $element =& $mform->createElement('text', 'aggregationcoef', get_string($coefstring, 'grades'));
-                    }
-                    if ($mform->elementExists('parentcategory')) {
-                        $mform->insertElementBefore($element, 'parentcategory');
-                    } else {
-                        $mform->insertElementBefore($element, 'id');
-                    }
-                    $mform->addHelpButton('aggregationcoef', $coefstring, 'grades');
+                if ($mform->elementExists('parentcategory')) {
+                    $mform->insertElementBefore($element, 'parentcategory');
+                } else {
+                    $mform->insertElementBefore($element, 'id');
                 }
-                $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_NONE);
-                $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_TEXT);
-                $mform->disabledIf('aggregationcoef', 'parentcategory', 'eq', $parent_category->id);
+                $mform->addHelpButton('aggregationcoef', $coefstring, 'grades');
             }
+            $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_NONE);
+            $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_TEXT);
+            $mform->disabledIf('aggregationcoef', 'parentcategory', 'eq', $parentcategory->id);
+        }
 
-            // Remove fields used by natural weighting if the parent category is not using natural weighting.
-            // Or if the item is a scale and scales are not used in aggregation.
-            if ($parent_category->aggregation != GRADE_AGGREGATE_SUM
-                    || (empty($CFG->grade_includescalesinaggregation) && $grade_item->gradetype == GRADE_TYPE_SCALE)) {
-                if ($mform->elementExists('weightoverride')) {
-                    $mform->removeElement('weightoverride');
-                }
-                if ($mform->elementExists('aggregationcoef2')) {
-                    $mform->removeElement('aggregationcoef2');
-                }
+        // Remove fields used by natural weighting if the parent category is not using natural weighting.
+        // Or if the item is a scale and scales are not used in aggregation.
+        if ($parentcategory->aggregation != GRADE_AGGREGATE_SUM
+                || (empty($CFG->grade_includescalesinaggregation) && $gradeitem->gradetype == GRADE_TYPE_SCALE)) {
+            if ($mform->elementExists('weightoverride')) {
+                $mform->removeElement('weightoverride');
             }
+            if ($mform->elementExists('aggregationcoef2')) {
+                $mform->removeElement('aggregationcoef2');
+            }
+        }
 
-            if ($category = $grade_item->get_item_category()) {
-                if ($category->aggregation == GRADE_AGGREGATE_SUM) {
-                    if ($mform->elementExists('gradetype')) {
-                        $mform->hardFreeze('gradetype');
-                    }
-                    if ($mform->elementExists('grademin')) {
-                        $mform->hardFreeze('grademin');
-                    }
-                    if ($mform->elementExists('grademax')) {
-                        $mform->hardFreeze('grademax');
-                    }
-                    if ($mform->elementExists('scaleid')) {
-                        $mform->removeElement('scaleid');
-                    }
+        if ($category = $gradeitem->get_item_category()) {
+            if ($category->aggregation == GRADE_AGGREGATE_SUM) {
+                if ($mform->elementExists('gradetype')) {
+                    $mform->hardFreeze('gradetype');
+                }
+                if ($mform->elementExists('grademin')) {
+                    $mform->hardFreeze('grademin');
+                }
+                if ($mform->elementExists('grademax')) {
+                    $mform->hardFreeze('grademax');
+                }
+                if ($mform->elementExists('scaleid')) {
+                    $mform->removeElement('scaleid');
                 }
             }
-
-        } else {
-            // all new items are manual, children of course category
-            $mform->removeElement('plusfactor');
-            $mform->removeElement('multfactor');
-            $mform->removeElement('rescalegrades');
         }
 
         // no parent header for course category
diff --git a/group/classes/output/index_page.php b/group/classes/output/index_page.php
new file mode 100644 (file)
index 0000000..8430aae
--- /dev/null
@@ -0,0 +1,111 @@
+<?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/>.
+
+/**
+ * Group index page.
+ *
+ * @package    core_group
+ * @copyright  2017 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_group\output;
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use renderer_base;
+use stdClass;
+use templatable;
+
+/**
+ * Group index page class.
+ *
+ * @package    core_group
+ * @copyright  2017 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class index_page implements renderable, templatable {
+
+    /** @var int $courseid The course ID. */
+    public $courseid;
+
+    /** @var array The array of groups to be rendered. */
+    public $groups;
+
+    /** @var string The name of the currently selected group. */
+    public $selectedgroupname;
+
+    /** @var array The array of group members to be rendered, if a group is selected. */
+    public $selectedgroupmembers;
+
+    /** @var bool Whether to disable the add members/edit group buttons. */
+    public $disableaddedit;
+
+    /** @var bool Whether to disable the delete group button. */
+    public $disabledelete;
+
+    /** @var array Groups that can't be deleted by the user. */
+    public $undeletablegroups;
+
+    /**
+     * index_page constructor.
+     *
+     * @param int $courseid The course ID.
+     * @param array $groups The array of groups to be rendered.
+     * @param string $selectedgroupname The name of the currently selected group.
+     * @param array $selectedgroupmembers The array of group members to be rendered, if a group is selected.
+     * @param bool $disableaddedit Whether to disable the add members/edit group buttons.
+     * @param bool $disabledelete Whether to disable the delete group button.
+     * @param array $undeletablegroups Groups that can't be deleted by the user.
+     */
+    public function __construct($courseid, $groups, $selectedgroupname, $selectedgroupmembers, $disableaddedit, $disabledelete,
+                                $undeletablegroups) {
+        $this->courseid = $courseid;
+        $this->groups = $groups;
+        $this->selectedgroupname = $selectedgroupname;
+        $this->selectedgroupmembers = $selectedgroupmembers;
+        $this->disableaddedit = $disableaddedit;
+        $this->disabledelete = $disabledelete;
+        $this->undeletablegroups = $undeletablegroups;
+    }
+
+    /**
+     * Export the data.
+     *
+     * @param renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(renderer_base $output) {
+        global $CFG;
+
+        $data = new stdClass();
+
+        // Variables that will be passed to the JS helper.
+        $data->courseid = $this->courseid;
+        $data->wwwroot = $CFG->wwwroot;
+        // To be passed to the JS init script in the template. Encode as a JSON string.
+        $data->undeletablegroups = json_encode($this->undeletablegroups);
+
+        // Some buttons are enabled if single group selected.
+        $data->addmembersdisabled = $this->disableaddedit;
+        $data->editgroupsettingsdisabled = $this->disableaddedit;
+        $data->deletegroupdisabled = $this->disabledelete;
+        $data->groups = $this->groups;
+        $data->members = $this->selectedgroupmembers;
+        $data->selectedgroup = $this->selectedgroupname;
+
+        return $data;
+    }
+}
diff --git a/group/classes/output/renderer.php b/group/classes/output/renderer.php
new file mode 100644 (file)
index 0000000..14443cf
--- /dev/null
@@ -0,0 +1,50 @@
+<?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/>.
+
+/**
+ * Renderers.
+ *
+ * @package    core_group
+ * @copyright  2017 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_group\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use plugin_renderer_base;
+
+/**
+ * Renderer class.
+ *
+ * @package    core_group
+ * @copyright  2017 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends plugin_renderer_base {
+
+    /**
+     * Defer to template.
+     *
+     * @param index_page $page
+     * @return string
+     */
+    public function render_index_page(index_page $page) {
+        $data = $page->export_for_template($this);
+        return parent::render_from_template('core_group/index', $data);
+    }
+}
index 5359a37..c3d2530 100644 (file)
@@ -1386,6 +1386,7 @@ class core_group_external extends external_api {
 
         $results = array(
             'groups' => $usergroups,
+            'canaccessallgroups' => has_capability('moodle/site:accessallgroups', $context, $user),
             'warnings' => $warnings
         );
         return $results;
@@ -1401,6 +1402,8 @@ class core_group_external extends external_api {
         return new external_single_structure(
             array(
                 'groups' => new external_multiple_structure(self::group_description()),
+                'canaccessallgroups' => new external_value(PARAM_BOOL,
+                    'Whether the user will be able to access all the activity groups.', VALUE_OPTIONAL),
                 'warnings' => new external_warnings(),
             )
         );
index 6571f54..8150e19 100644 (file)
@@ -61,6 +61,7 @@ $context = context_course::instance($course->id);
 require_capability('moodle/course:managegroups', $context);
 
 $PAGE->requires->js('/group/clientlib.js');
+$PAGE->requires->js('/group/module.js');
 
 // Check for multiple/no group errors
 if (!$singlegroup) {
@@ -152,41 +153,23 @@ echo $OUTPUT->header();
 $currenttab = 'groups';
 require('tabs.php');
 
-$disabled = 'disabled="disabled"';
-
-// Some buttons are enabled if single group selected.
-$showaddmembersform_disabled = $singlegroup ? '' : $disabled;
-$showeditgroupsettingsform_disabled = $singlegroup ? '' : $disabled;
-$deletegroup_disabled = count($groupids) > 0 ? '' : $disabled;
-
 echo $OUTPUT->heading(format_string($course->shortname, true, array('context' => $context)) .' '.$strgroups, 3);
-echo '<form id="groupeditform" action="index.php" method="post">'."\n";
-echo '<div>'."\n";
-echo '<input type="hidden" name="id" value="' . $courseid . '" />'."\n";
-
-echo html_writer::start_tag('div', array('class' => 'groupmanagementtable boxaligncenter'));
-echo html_writer::start_tag('div', array('class' => 'groups'));
-
-echo '<p><label for="groups"><span id="groupslabel">'.get_string('groups').':</span><span id="thegrouping">&nbsp;</span></label></p>'."\n";
-
-$onchange = 'M.core_group.membersCombo.refreshMembers();';
-
-echo '<select name="groups[]" multiple="multiple" id="groups" size="15" class="select" onchange="'.$onchange.'">'."\n";
 
 $groups = groups_get_all_groups($courseid);
-$selectedname = '&nbsp;';
+$selectedname = null;
 $preventgroupremoval = array();
 
+// Get list of groups to render.
+$groupoptions = array();
 if ($groups) {
-    // Print out the HTML
     foreach ($groups as $group) {
-        $select = '';
-        $usercount = $DB->count_records('groups_members', array('groupid'=>$group->id));
-        $groupname = format_string($group->name).' ('.$usercount.')';
-        if (in_array($group->id,$groupids)) {
-            $select = ' selected="selected"';
+        $selected = false;
+        $usercount = $DB->count_records('groups_members', array('groupid' => $group->id));
+        $groupname = format_string($group->name) . ' (' . $usercount . ')';
+        if (in_array($group->id, $groupids)) {
+            $selected = true;
             if ($singlegroup) {
-                // Only keep selected name if there is one group selected
+                // Only keep selected name if there is one group selected.
                 $selectedname = $groupname;
             }
         }
@@ -194,76 +177,41 @@ if ($groups) {
             $preventgroupremoval[$group->id] = true;
         }
 
-        echo "<option value=\"{$group->id}\"$select title=\"$groupname\">$groupname</option>\n";
+        $groupoptions[] = (object) [
+            'value' => $group->id,
+            'selected' => $selected,
+            'text' => $groupname
+        ];
     }
-} else {
-    // Print an empty option to avoid the XHTML error of having an empty select element
-    echo '<option>&nbsp;</option>';
 }
 
-echo '</select>'."\n";
-echo '<p><input class="btn btn-secondary" type="submit" name="act_updatemembers" id="updatemembers" value="'
-        . get_string('showmembersforgroup', 'group') . '" /></p>'."\n";
-echo '<p><input class="btn btn-secondary" type="submit" '. $showeditgroupsettingsform_disabled .
-        ' name="act_showgroupsettingsform" id="showeditgroupsettingsform" value="'
-        . get_string('editgroupsettings', 'group') . '" /></p>'."\n";
-echo '<p><input class="btn btn-secondary" type="submit" '. $deletegroup_disabled .
-        ' name="act_deletegroup" id="deletegroup" value="'
-        . get_string('deleteselectedgroup', 'group') . '" /></p>'."\n";
-
-echo '<p><input class="btn btn-secondary" type="submit" name="act_showcreateorphangroupform" id="showcreateorphangroupform" value="'
-        . get_string('creategroup', 'group') . '" /></p>'."\n";
-
-echo '<p><input class="btn btn-secondary" type="submit" name="act_showautocreategroupsform" id="showautocreategroupsform" value="'
-        . get_string('autocreategroups', 'group') . '" /></p>'."\n";
-
-echo '<p><input class="btn btn-secondary" type="submit" name="act_showimportgroups" id="showimportgroups" value="'
-        . get_string('importgroups', 'core_group') . '" /></p>'."\n";
-
-echo html_writer::end_tag('div');
-echo html_writer::start_tag('div', array('class' => 'members'));
-
-echo '<p><label for="members"><span id="memberslabel">'.
-    get_string('membersofselectedgroup', 'group').
-    ' </span><span id="thegroup">'.$selectedname.'</span></label></p>'."\n";
-//NOTE: the SELECT was, multiple="multiple" name="user[]" - not used and breaks onclick.
-echo '<select name="user" id="members" size="15" class="select"'."\n";
-echo ' onclick="window.status=this.options[this.selectedIndex].title;" onmouseout="window.status=\'\';">'."\n";
-
-$member_names = array();
-
-$atleastonemember = false;
+// Get list of group members to render if there is a single selected group.
+$members = array();
 if ($singlegroup) {
-    if ($groupmemberroles = groups_get_members_by_role($groupids[0], $courseid, 'u.id, ' . get_all_user_name_fields(true, 'u'))) {
-        foreach($groupmemberroles as $roleid=>$roledata) {
-            echo '<optgroup label="'.s($roledata->name).'">';
-            foreach($roledata->users as $member) {
-                echo '<option value="'.$member->id.'">'.fullname($member, true).'</option>';
-                $atleastonemember = true;
+    $usernamefields = get_all_user_name_fields(true, 'u');
+    if ($groupmemberroles = groups_get_members_by_role(reset($groupids), $courseid, 'u.id, ' . $usernamefields)) {
+        foreach ($groupmemberroles as $roleid => $roledata) {
+            $users = array();
+            foreach ($roledata->users as $member) {
+                $users[] = (object)[
+                    'value' => $member->id,
+                    'text' => fullname($member, true)
+                ];
             }
-            echo '</optgroup>';
+            $members[] = (object)[
+                'role' => s($roledata->name),
+                'rolemembers' => $users
+            ];
         }
     }
 }
 
-if (!$atleastonemember) {
-    // Print an empty option to avoid the XHTML error of having an empty select element
-    echo '<option>&nbsp;</option>';
-}
-
-echo '</select>'."\n";
-
-echo '<p><input class="btn btn-secondary" type="submit" ' . $showaddmembersform_disabled . ' name="act_showaddmembersform" '
-        . 'id="showaddmembersform" value="' . get_string('adduserstogroup', 'group'). '" /></p>'."\n";
-echo html_writer::end_tag('div');
-echo html_writer::end_tag('div');
-
-//<input type="hidden" name="rand" value="om" />
-echo '</div>'."\n";
-echo '</form>'."\n";
-
-$PAGE->requires->js_init_call('M.core_group.init_index', array($CFG->wwwroot, $courseid));
-$PAGE->requires->js_init_call('M.core_group.groupslist', array($preventgroupremoval));
+$disableaddedit = !$singlegroup;
+$disabledelete = !empty($groupids);
+$renderable = new \core_group\output\index_page($courseid, $groupoptions, $selectedname, $members, $disableaddedit, $disabledelete,
+        $preventgroupremoval);
+$output = $PAGE->get_renderer('core_group');
+echo $output->render($renderable);
 
 echo $OUTPUT->footer();
 
diff --git a/group/templates/index.mustache b/group/templates/index.mustache
new file mode 100644 (file)
index 0000000..e68bdaf
--- /dev/null
@@ -0,0 +1,145 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_group/index
+
+    Template for the Groups page.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * courseid int The course ID.
+    * selectedgroup string The initially selected group.
+    * editgroupsettingsdisabled bool Whether to disable the "Edit group settings" button on load.
+    * deletegroupdisabled bool Whether to disable the "Delete selected group" button on load.
+    * addmembersdisabled bool Whether to disable the "Add/remove users" button on load.
+    * groups array The list of groups.
+    * members array The list of members, grouped based on roles.
+    * undeletablegroups string A JSON string containing an array of group IDs that a user cannot delete.
+
+    Example context (json):
+    {
+        "courseid": "1",
+        "selectedgroup": "Group 1 (3)",
+        "editgroupsettingsdisabled": false,
+        "deletegroupdisabled": false,
+        "addmembersdisabled": false,
+        "groups": [
+            {
+                "value": "1",
+                "text": "Group 1 (3)",
+                "selected": true
+            },
+            {
+                "value": "2",
+                "text": "Group 2 (2)"
+            }
+        ],
+        "members": [
+            {
+                "role": "Student",
+                "rolemembers": [
+                    {
+                        "value": "1",
+                        "text": "John Doe"
+                    },
+                    {
+                        "value": "2",
+                        "text": "Jane Doe"
+                    },
+                    {
+                        "value": "3",
+                        "text": "John Smith"
+                    }
+                ]
+            }
+        ],
+        "undeletablegroups": "[1: true, 3: true]"
+    }
+}}
+<form id="groupeditform" action="index.php" method="post">
+    <div class="container-fluid groupmanagementtable">
+        <div class="row row-fluid rtl-compatible">
+            <div class="col-md-6 span6 m-b-1">
+                <input type="hidden" name="id" value="{{courseid}}">
+                <div class="form-group">
+                    <label for="groups">
+                        <span id="groupslabel">{{#str}}groups{{/str}}</span>
+                        <span id="thegrouping">&nbsp;</span>
+                    </label>
+                    <select name="groups[]" multiple="multiple" id="groups" size="15" class="form-control input-block-level">
+                        {{#groups}}
+                            <option value="{{value}}" {{#selected}}selected="selected"{{/selected}} title="{{{text}}}">{{{text}}}</option>
+                        {{/groups}}
+                    </select>
+                </div>
+                <div class="form-group">
+                    <input type="submit" name="act_updatemembers" id="updatemembers" value="{{#str}}showmembersforgroup, group{{/str}}" class="btn btn-default" />
+                </div>
+                <div class="form-group">
+                    <input type="submit" name="act_showgroupsettingsform" id="showeditgroupsettingsform" value="{{#str}}editgroupsettings, group{{/str}}" {{#editgroupsettingsdisabled}}disabled="disabled"{{/editgroupsettingsdisabled}} class="btn btn-default" />
+                </div>
+                <div class="form-group">
+                    <input type="submit" name="act_deletegroup" id="deletegroup" value="{{#str}}deleteselectedgroup, group{{/str}}" {{#deletegroupdisabled}}disabled="disabled"{{/deletegroupdisabled}} class="btn btn-default" />
+                </div>
+                <div class="form-group">
+                    <input type="submit" name="act_showcreateorphangroupform" id="showcreateorphangroupform" value="{{#str}}creategroup, group{{/str}}" class="btn btn-default" />
+                </div>
+                <div class="form-group">
+                    <input type="submit" name="act_showautocreategroupsform" id="showautocreategroupsform" value="{{#str}}autocreategroups, group{{/str}}" class="btn btn-default" />
+                </div>
+                <div class="form-group">
+                    <input type="submit" name="act_showimportgroups" id="showimportgroups" value="{{#str}}importgroups, group{{/str}}" class="btn btn-default" />
+                </div>
+            </div>
+            <div class="col-md-6 span6 m-b-1">
+                <div class="form-group">
+                    <label for="members">
+                        <span id="memberslabel">{{#str}}membersofselectedgroup, group{{/str}}</span>
+                        <span id="thegroup">{{{selectedgroup}}}</span>
+                    </label>
+                    <select size="15" multiple="multiple" class="form-control input-block-level" id="members" name="user">
+                        {{#members}}
+                            <optgroup label="{{role}}">
+                                {{#rolemembers}}
+                                    <option value="{{value}}">{{{text}}}‎</option>
+                                {{/rolemembers}}
+                            </optgroup>
+                        {{/members}}
+                    </select>
+                </div>
+                <div class="form-group">
+                    <input type="submit" value="{{#str}}adduserstogroup, group{{/str}}" class="btn btn-default" {{#addmembersdisabled}}disabled="disabled"{{/addmembersdisabled}} name="act_showaddmembersform" id="showaddmembersform"/>
+                </div>
+            </div>
+        </div>
+    </div>
+</form>
+{{#js}}
+    require(['jquery', 'core/yui'], function($) {
+        $("#groups").change(function() {
+            M.core_group.membersCombo.refreshMembers();
+        });
+        M.core_group.init_index(Y, "{{wwwroot}}", {{courseid}});
+        var undeletableGroups = JSON.parse('{{{undeletablegroups}}}');
+        M.core_group.groupslist(Y, undeletableGroups);
+    });
+{{/js}}
index 97a6cf9..dcbb0ef 100644 (file)
@@ -523,6 +523,7 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
         $groups = core_group_external::get_activity_allowed_groups($cm1->id);
         $groups = external_api::clean_returnvalue(core_group_external::get_activity_allowed_groups_returns(), $groups);
         $this->assertCount(2, $groups['groups']);
+        $this->assertFalse($groups['canaccessallgroups']);
 
         foreach ($groups['groups'] as $group) {
             if ($group['name'] == $group1data['name']) {
@@ -539,12 +540,21 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
         $groups = core_group_external::get_activity_allowed_groups($cm1->id, $student->id);
         $groups = external_api::clean_returnvalue(core_group_external::get_activity_allowed_groups_returns(), $groups);
         $this->assertCount(2, $groups['groups']);
+        // We are checking the $student passed as parameter so this will return false.
+        $this->assertFalse($groups['canaccessallgroups']);
 
         // Check warnings. Trying to get groups for a user not enrolled in course.
         $groups = core_group_external::get_activity_allowed_groups($cm1->id, $otherstudent->id);
         $groups = external_api::clean_returnvalue(core_group_external::get_activity_allowed_groups_returns(), $groups);
         $this->assertCount(1, $groups['warnings']);
+        $this->assertFalse($groups['canaccessallgroups']);
 
+        // Checking teacher groups.
+        $groups = core_group_external::get_activity_allowed_groups($cm1->id);
+        $groups = external_api::clean_returnvalue(core_group_external::get_activity_allowed_groups_returns(), $groups);
+        $this->assertCount(2, $groups['groups']);
+        // Teachers by default can access all groups.
+        $this->assertTrue($groups['canaccessallgroups']);
     }
 
     /**
index 69f7708..0eccea6 100644 (file)
@@ -35,9 +35,11 @@ $string['cliansweryes'] = 'y';
 $string['cliincorrectvalueerror'] = '錯誤,將“{$a->option}”的值設為“{$a->value}”是不正確的';
 $string['cliincorrectvalueretry'] = '不正確值,請重試';
 $string['clitypevalue'] = '輸入值';
-$string['clitypevaluedefault'] = '輸入值,按回車使用預設值({$a})';
-$string['cliunknowoption'] = '錯誤選項:{$a}請使用 --help 選項。';
+$string['clitypevaluedefault'] = '輸入值,按Enter 可使用預設值({$a})';
+$string['cliunknowoption'] = '無法辨識的選項:
+{$a}
+請使用 --help 選項。';
 $string['cliyesnoprompt'] = '輸入y(表示是)或n(表示否)';
 $string['environmentrequireinstall'] = '必須安裝並啟用';
-$string['environmentrequireversion'] = '需求版本為{$a->needed},而您目前版本為 {$a->current}';
+$string['environmentrequireversion'] = '需要得版本為{$a->needed},而您目前執行的版本為 {$a->current}';
 $string['upgradekeyset'] = '升級密鑰(若不要設定請保持空白)';
index f0d5e4f..b9bb1e5 100644 (file)
@@ -274,7 +274,7 @@ $string['configmodchooserdefault'] = 'Should the activity chooser be presented t
 $string['configmycoursesperpage'] = 'Maximum number of courses to display in any list of a user\'s own courses';
 $string['configmymoodleredirect'] = 'This setting forces redirects to /my on login for non-admins and replaces the top level site navigation with /my';
 $string['configmypagelocked'] = 'This setting prevents the default page from being edited by any non-admins';
-$string['confignavcourselimit'] = 'Limits the number of courses shown to the user when they are either not logged in or are not enrolled in any courses.';
+$string['confignavcourselimit'] = 'Limits the number of courses shown to the user in the navigation.';
 $string['confignavshowallcourses'] = 'This setting determines whether users who are enrolled in courses can see Courses (listing all courses) in the navigation, in addition to My Courses (listing courses in which they are enrolled).';
 $string['confignavshowcategories'] = 'Show course categories in the navigation bar and navigation blocks. This does not occur with courses the user is currently enrolled in, they will still be listed under mycourses without categories.';
 $string['confignoreplyaddress'] = 'Emails are sometimes sent out on behalf of a user (eg forum posts). The email address you specify here will be used as the "From" address in those cases when the recipients should not be able to reply directly to the user (eg when a user chooses to keep their address private). This setting will also be used as the envelope sender when sending email.';
index e811f03..99cc03f 100644 (file)
@@ -41,8 +41,9 @@ $string['cachedef_capabilities'] = 'System capabilities list';
 $string['cachedef_config'] = 'Config settings';
 $string['cachedef_coursecat'] = 'Course categories lists for particular user';
 $string['cachedef_coursecatrecords'] = 'Course categories records';
-$string['cachedef_coursecontacts'] = 'List of course contacts';
 $string['cachedef_coursecattree'] = 'Course categories tree';
+$string['cachedef_coursecompletion'] = 'Course completion status';
+$string['cachedef_coursecontacts'] = 'List of course contacts';
 $string['cachedef_coursemodinfo'] = 'Accumulated information about modules and sections for each course';
 $string['cachedef_completion'] = 'Activity completion status';
 $string['cachedef_databasemeta'] = 'Database meta information';
@@ -60,6 +61,7 @@ $string['cachedef_navigation_expandcourse'] = 'Navigation expandable courses';
 $string['cachedef_observers'] = 'Event observers';
 $string['cachedef_plugin_functions'] = 'Plugins available callbacks';
 $string['cachedef_plugin_manager'] = 'Plugin info manager';
+$string['cachedef_postprocessedcss'] = 'Post processed CSS';
 $string['cachedef_tagindexbuilder'] = 'Search results for tagged items';
 $string['cachedef_questiondata'] = 'Question definitions';
 $string['cachedef_repositories'] = 'Repositories instances data';
index 73d8b4a..364368c 100644 (file)
@@ -93,6 +93,7 @@ $string['eventendtime'] = 'End time';
 $string['eventinstanttime'] = 'Time';
 $string['eventkind'] = 'Type of event';
 $string['eventname'] = 'Event title';
+$string['eventnameandcourse'] = '{$a->course}: {$a->name}';
 $string['eventnone'] = 'No events';
 $string['eventrepeat'] = 'Repeats';
 $string['eventsall'] = 'All events';
index 07e154d..845118e 100644 (file)
@@ -1505,6 +1505,7 @@ $string['publicsitefileswarning3'] = 'Note: Files placed here can be accessed by
 $string['publish'] = 'Publish';
 $string['question'] = 'Question';
 $string['questionsinthequestionbank'] = 'Questions in the question bank';
+$string['quotausage'] = 'You have currently used {$a->used} of your {$a->total} limit.';
 $string['readinginfofrombackup'] = 'Reading info from backup';
 $string['readme'] = 'README';
 $string['recentactivity'] = 'Recent activity';
@@ -1741,6 +1742,7 @@ $string['showsettings'] = 'Show settings';
 $string['showtheselogs'] = 'Show these logs';
 $string['showthishelpinlanguage'] = 'Show this help in language: {$a}';
 $string['schedule'] = 'Schedule';
+$string['sidepanel'] = 'Side panel';
 $string['signoutofotherservices'] = 'Sign out everywhere';
 $string['signoutofotherservices_help'] = 'If ticked, the account will be signed out of all devices and systems which use web services, such as the mobile app.';
 $string['since'] = 'Since';
index 936d338..8247f32 100644 (file)
Binary files a/lib/amd/build/ajax.min.js and b/lib/amd/build/ajax.min.js differ
index 35b66ab..9ed5a35 100644 (file)
Binary files a/lib/amd/build/notification.min.js and b/lib/amd/build/notification.min.js differ
index 663ff25..481247c 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @since      2.9
  */
-define(['jquery', 'core/config'], function($, config) {
+define(['jquery', 'core/config', 'core/log'], function($, config, Log) {
+
+    // Keeps track of when the user leaves the page so we know not to show an error.
+    var unloading = false;
 
     /**
      * Success handler. Called when the ajax call succeeds. Checks each response and
@@ -43,6 +46,18 @@ define(['jquery', 'core/config'], function($, config) {
         var request;
         var response;
 
+        if (responses.error) {
+            // There was an error with the request as a whole.
+            // We need to reject each promise.
+            // Unfortunately this may lead to duplicate dialogues, but each Promise must be rejected.
+            for (; i < requests.length; i++) {
+                request = requests[i];
+                request.deferred.reject(responses);
+            }
+
+            return;
+        }
+
         for (i = 0; i < requests.length; i++) {
             request = requests[i];
 
@@ -78,8 +93,9 @@ define(['jquery', 'core/config'], function($, config) {
      * @private
      * @param {jqXHR} jqXHR The ajax object.
      * @param {string} textStatus The status string.
+     * @param {Error|Object} exception The error thrown.
      */
-    var requestFail = function(jqXHR, textStatus) {
+    var requestFail = function(jqXHR, textStatus, exception) {
         // Reject all the promises.
         var requests = this;
 
@@ -87,7 +103,13 @@ define(['jquery', 'core/config'], function($, config) {
         for (i = 0; i < requests.length; i++) {
             var request = requests[i];
 
-            request.deferred.reject(textStatus);
+            if (unloading) {
+                // No need to trigger an error because we are already navigating.
+                Log.error("Page unloaded.");
+                Log.error(exception);
+            } else {
+                request.deferred.reject(exception);
+            }
         }
     };
 
@@ -109,6 +131,9 @@ define(['jquery', 'core/config'], function($, config) {
          * @return {Promise[]} Array of promises that will be resolved when the ajax call returns.
          */
         call: function(requests, async, loginrequired) {
+            $(window).bind('beforeunload', function() {
+                unloading = true;
+            });
             var ajaxRequestData = [],
                 i,
                 promises = [],
index e3212ba..601e50e 100644 (file)
@@ -168,6 +168,9 @@ function(Y, $, log) {
             if (ex.debuginfo) {
                 ex.stack += ex.debuginfo + '\n';
             }
+            if (!ex.backtrace && ex.stacktrace) {
+                ex.backtrace = ex.stacktrace;
+            }
             if (ex.backtrace) {
                 ex.stack += ex.backtrace;
                 var ln = ex.backtrace.match(/line ([^ ]*) of/);
index d9fe32d..d00e46f 100644 (file)
@@ -76,8 +76,6 @@ abstract class base {
      * Output file headers to initialise the download of the file.
      */
     public function send_http_headers() {
-        global $CFG;
-
         if (defined('BEHAT_SITE_RUNNING')) {
             // For text based formats - we cannot test the output with behat if we force a file download.
             return;
@@ -98,11 +96,18 @@ abstract class base {
     }
 
     /**
-     * Write the start of the format
+     * Write the start of the file.
+     */
+    public function start_output() {
+        // Override me if needed.
+    }
+
+    /**
+     * Write the start of the sheet we will be adding data to.
      *
      * @param array $columns
      */
-    public function write_header($columns) {
+    public function start_sheet($columns) {
         // Override me if needed.
     }
 
@@ -115,12 +120,18 @@ abstract class base {
     abstract public function write_record($record, $rownum);
 
     /**
-     * Write the end of the format
+     * Write the end of the sheet containing the data.
      *
      * @param array $columns
      */
-    public function write_footer($columns) {
+    public function close_sheet($columns) {
         // Override me if needed.
     }
 
+    /**
+     * Write the end of the file.
+     */
+    public function close_output() {
+        // Override me if needed.
+    }
 }
index 9208e60..6b0c10b 100644 (file)
@@ -44,6 +44,9 @@ abstract class spout_base extends \core\dataformat\base {
     /** @var $sheettitle */
     protected $sheettitle;
 
+    /** @var $renamecurrentsheet */
+    protected $renamecurrentsheet = false;
+
     /**
      * Output file headers to initialise the download of the file.
      */
@@ -54,10 +57,9 @@ abstract class spout_base extends \core\dataformat\base {
         }
         $filename = $this->filename . $this->get_extension();
         $this->writer->openToBrowser($filename);
-        if ($this->sheettitle && $this->writer instanceof \Box\Spout\Writer\AbstractMultiSheetsWriter) {
-            $sheet = $this->writer->getCurrentSheet();
-            $sheet->setName($this->sheettitle);
-        }
+
+        // By default one sheet is always created, but we want to rename it when we call start_sheet().
+        $this->renamecurrentsheet = true;
     }
 
     /**
@@ -68,18 +70,24 @@ abstract class spout_base extends \core\dataformat\base {
      * @param string $title
      */
     public function set_sheettitle($title) {
-        if (!$title) {
-            return;
-        }
         $this->sheettitle = $title;
     }
 
     /**
-     * Write the start of the format
+     * Write the start of the sheet we will be adding data to.
      *
      * @param array $columns
      */
-    public function write_header($columns) {
+    public function start_sheet($columns) {
+        if ($this->sheettitle && $this->writer instanceof \Box\Spout\Writer\AbstractMultiSheetsWriter) {
+            if ($this->renamecurrentsheet) {
+                $sheet = $this->writer->getCurrentSheet();
+                $this->renamecurrentsheet = false;
+            } else {
+                $sheet = $this->writer->addNewSheetAndMakeItCurrent();
+            }
+            $sheet->setName($this->sheettitle);
+        }
         $this->writer->addRow(array_values((array)$columns));
     }
 
@@ -94,13 +102,10 @@ abstract class spout_base extends \core\dataformat\base {
     }
 
     /**
-     * Write the end of the format
-     *
-     * @param array $columns
+     * Write the end of the file.
      */
-    public function write_footer($columns) {
+    public function close_output() {
         $this->writer->close();
         $this->writer = null;
     }
-
 }
index d5ef0ab..89edd59 100644 (file)
@@ -243,10 +243,11 @@ class icon_system_fontawesome extends icon_system_font {
             'core:i/marker' => 'fa-circle-o',
             'core:i/mean' => 'fa-calculator',
             'core:i/menu' => 'fa-ellipsis-v',
+            'core:i/menubars' => 'fa-bars',
             'core:i/mnethost' => 'fa-external-link',
             'core:i/moodle_host' => 'fa-graduation-cap',
             'core:i/move_2d' => 'fa-arrows',
-            'core:i/navigationitem' => 'fa-angle-right',
+            'core:i/navigationitem' => 'fa-fw',
             'core:i/ne_red_mark' => 'fa-remove',
             'core:i/new' => 'fa-plus',
             'core:i/news' => 'fa-newspaper-o',
@@ -279,7 +280,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:i/scales' => 'fa-balance-scale',
             'core:i/scheduled' => 'fa-calendar-check-o',
             'core:i/search' => 'fa-search',
-            'core:i/settings' => 'fa-cogs',
+            'core:i/settings' => 'fa-cog',
             'core:i/show' => 'fa-eye-slash',
             'core:i/siteevent' => 'fa-share-alt',
             'core:i/star-rating' => 'fa-star',
@@ -308,7 +309,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:t/block' => 'fa-ban',
             'core:t/block_to_dock_rtl' => 'fa-chevron-right',
             'core:t/block_to_dock' => 'fa-chevron-left',
-            'core:t/calc_off' => 'fa-times fa-cross',
+            'core:t/calc_off' => 'fa-calculator', // TODO: Change to better icon once we have stacked icon support or more icons.
             'core:t/calc' => 'fa-calculator',
             'core:t/check' => 'fa-check',
             'core:t/cohort' => 'fa-users',
index 861b24d..689edde 100644 (file)
@@ -37,7 +37,7 @@ class repository extends base {
      */
     public static function get_enabled_plugins() {
         global $DB;
-        return $DB->get_records_menu('repository', array('visible'=>1), 'type ASC', 'type, type AS val');
+        return $DB->get_records_menu('repository', null, 'type ASC', 'type, type AS val');
     }
 
     public function get_settings_section_name() {
index 4967229..6925e02 100644 (file)
@@ -461,7 +461,7 @@ class core_user {
                 'choices' => array_merge(array('' => ''), get_string_manager()->get_list_of_countries(true, true)));
         $fields['lang'] = array('type' => PARAM_LANG, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->lang,
                 'choices' => array_merge(array('' => ''), get_string_manager()->get_list_of_translations(false)));
-        $fields['calendartype'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->calendartype,
+        $fields['calendartype'] = array('type' => PARAM_PLUGIN, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->calendartype,
                 'choices' => array_merge(array('' => ''), \core_calendar\type_factory::get_list_of_calendar_types()));
         $fields['theme'] = array('type' => PARAM_THEME, 'null' => NULL_NOT_ALLOWED,
                 'default' => theme_config::DEFAULT_THEME, 'choices' => array_merge(array('' => ''), get_list_of_themes()));
index 80c8542..c8bf817 100644 (file)
@@ -423,6 +423,21 @@ class completion_info {
             // Load criteria from database
             $records = (array)$DB->get_records('course_completion_criteria', $params);
 
+            // Order records so activities are in the same order as they appear on the course view page.
+            if ($records) {
+                $activitiesorder = array_keys(get_fast_modinfo($this->course)->get_cms());
+                usort($records, function ($a, $b) use ($activitiesorder) {
+                    $aidx = ($a->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) ?
+                        array_search($a->moduleinstance, $activitiesorder) : false;
+                    $bidx = ($b->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) ?
+                        array_search($b->moduleinstance, $activitiesorder) : false;
+                    if ($aidx === false || $bidx === false || $aidx == $bidx) {
+                        return 0;
+                    }
+                    return ($aidx < $bidx) ? -1 : 1;
+                });
+            }
+
             // Build array of criteria objects
             $this->criteria = array();
             foreach ($records as $record) {
@@ -769,6 +784,7 @@ class completion_info {
 
         // Difficult to find affected users, just purge all completion cache.
         cache::make('core', 'completion')->purge();
+        cache::make('core', 'coursecompletion')->purge();
     }
 
     /**
@@ -820,6 +836,7 @@ class completion_info {
 
         // Difficult to find affected users, just purge all completion cache.
         cache::make('core', 'completion')->purge();
+        cache::make('core', 'coursecompletion')->purge();
     }
 
     /**
index 36b5173..fad18d6 100644 (file)
@@ -56,7 +56,15 @@ function download_as_dataformat($filename, $dataformat, $columns, $iterator, $ca
 
     $format->set_filename($filename);
     $format->send_http_headers();
-    $format->write_header($columns);
+    // This exists to support all dataformats - see MDL-56046.
+    if (method_exists($format, 'write_header')) {
+        error_log('The function write_header() does not support multiple sheets. In order to support multiple sheets you ' .
+            'must implement start_output() and start_sheet() and remove write_header() in your dataformat.');
+        $format->write_header($columns);
+    } else {
+        $format->start_output();
+        $format->start_sheet($columns);
+    }
     $c = 0;
     foreach ($iterator as $row) {
         if ($callback) {
@@ -67,6 +75,14 @@ function download_as_dataformat($filename, $dataformat, $columns, $iterator, $ca
         }
         $format->write_record($row, $c++);
     }
-    $format->write_footer($columns);
+    // This exists to support all dataformats - see MDL-56046.
+    if (method_exists($format, 'write_footer')) {
+        error_log('The function write_footer() does not support multiple sheets. In order to support multiple sheets you ' .
+            'must implement close_sheet() and close_output() and remove write_footer() in your dataformat.');
+        $format->write_footer($columns);
+    } else {
+        $format->close_sheet($columns);
+        $format->close_output();
+    }
 }
 
index 42d98d4..a63ff7e 100644 (file)
@@ -229,6 +229,16 @@ $definitions = array(
         'staticaccelerationsize' => 2, // Should be current course and site course.
     ),
 
+    // Used to cache course completion status.
+    'coursecompletion' => array(
+        'mode' => cache_store::MODE_APPLICATION,
+        'simplekeys' => true,
+        'simpledata' => true,
+        'ttl' => 3600,
+        'staticacceleration' => true,
+        'staticaccelerationsize' => 30, // Will be users list of current courses in nav.
+    ),
+
     // A simple cache that stores whether a user can expand a course in the navigation.
     // The key is the course ID and the value will either be 1 or 0 (cast to bool).
     // The cache isn't always up to date, it should only ever be used to save a costly call to
@@ -328,4 +338,11 @@ $definitions = array(
         'staticaccelerationsize' => 1
     ),
 
+    // Caches processed CSS.
+    'postprocessedcss' => array(
+        'mode' => cache_store::MODE_APPLICATION,
+        'simplekeys' => true,
+        'simpledata' => true,
+        'staticacceleration' => false,
+    ),
 );
index 39ab0c6..ce5dda2 100644 (file)
         <FIELD NAME="sequence" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="visible" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
         <FIELD NAME="availability" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Availability restrictions for viewing this section, in JSON format. Null if no restrictions."/>
+        <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Time at which the course section was last changed."/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index 002640e..d608f24 100644 (file)
@@ -244,7 +244,7 @@ $functions = array(
         'classname' => 'core_course_external',
         'methodname' => 'duplicate_course',
         'classpath' => 'course/externallib.php',
-        'description' => 'Duplicate an existing course (creating a new one), without user data',
+        'description' => 'Duplicate an existing course (creating a new one).',
         'type' => 'write',
         'capabilities' => 'moodle/backup:backupcourse, moodle/restore:restorecourse, moodle/course:create'
     ),
@@ -1206,6 +1206,15 @@ $functions = array(
         'type' => 'write',
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
+    'core_user_get_private_files_info' => array(
+        'classname' => 'core_user_external',
+        'methodname' => 'get_private_files_info',
+        'classpath' => 'user/externallib.php',
+        'description' => 'Returns general information about files in the user private files area.',
+        'type' => 'read',
+        'capabilities' => 'moodle/user:manageownfiles',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
 
     // Competencies functions.
     'core_competency_create_competency_framework' => array(
index b130390..7798fec 100644 (file)
@@ -2865,5 +2865,27 @@ function xmldb_main_upgrade($oldversion) {
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2017061201.00) {
+        $table = new xmldb_table('course_sections');
+        $field = new xmldb_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'availability');
+
+        // Define a field 'timemodified' in the 'course_sections' table.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        upgrade_main_savepoint(true, 2017061201.00);
+    }
+
+    if ($oldversion < 2017061301.00) {
+        // Check if the value of 'navcourselimit' is set to the old default value, if so, change it to the new default.
+        if ($CFG->navcourselimit == 20) {
+            set_config('navcourselimit', 10);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017061301.00);
+    }
+
     return true;
 }
index b1dafb9..28faa7e 100644 (file)
@@ -503,9 +503,29 @@ function file_rewrite_pluginfile_urls($text, $file, $contextid, $component, $fil
  * (more information will be added as needed).
  */
 function file_get_draft_area_info($draftitemid, $filepath = '/') {
-    global $CFG, $USER;
+    global $USER;
 
     $usercontext = context_user::instance($USER->id);
+    return file_get_file_area_info($usercontext->id, 'user', 'draft', $draftitemid, $filepath);
+}
+
+/**
+ * Returns information about files in an area.
+ *
+ * @param int $contextid context id
+ * @param string $component component
+ * @param string $filearea file area name
+ * @param int $itemid item id or all files if not specified
+ * @param string $filepath path to the directory from which the information have to be retrieved.
+ * @return array with the following entries:
+ *      'filecount' => number of files in the area.
+ *      'filesize' => total size of the files in the area.
+ *      'foldercount' => number of folders in the area.
+ *      'filesize_without_references' => total size of the area excluding file references.
+ * @since Moodle 3.4
+ */
+function file_get_file_area_info($contextid, $component, $filearea, $itemid = 0, $filepath = '/') {
+
     $fs = get_file_storage();
 
     $results = array(
@@ -515,11 +535,8 @@ function file_get_draft_area_info($draftitemid, $filepath = '/') {
         'filesize_without_references' => 0
     );
 
-    if ($filepath != '/') {
-        $draftfiles = $fs->get_directory_files($usercontext->id, 'user', 'draft', $draftitemid, $filepath, true, true);
-    } else {
-        $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id', true);
-    }
+    $draftfiles = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath, true, true);
+
     foreach ($draftfiles as $file) {
         if ($file->is_directory()) {
             $results['foldercount'] += 1;
index b7d0fd6..2131314 100644 (file)
@@ -662,6 +662,7 @@ class zip_archive extends file_archive {
                             case 'ISO-8859-6': $encoding = 'CP720'; break;
                             case 'ISO-8859-7': $encoding = 'CP737'; break;
                             case 'ISO-8859-8': $encoding = 'CP862'; break;
+                            case 'WINDOWS-1251': $encoding = 'CP866'; break;
                             case 'EUC-JP':
                             case 'UTF-8':
                                 if ($winchar = get_string('localewincharset', 'langconfig')) {
index aa29bd9..8c4ee1d 100644 (file)
@@ -84,6 +84,8 @@ class navigation_node implements renderable {
     const COURSE_MY = 1;
     /** var int Course the current user is currently viewing */
     const COURSE_CURRENT = 2;
+    /** var string The course index page navigation node */
+    const COURSE_INDEX_PAGE = 'courseindexpage';
 
     /** @var int Parameter to aid the coder in tracking [optional] */
     public $id = null;
@@ -430,7 +432,7 @@ class navigation_node implements renderable {
     public function build_flat_navigation_list(flat_navigation $nodes, $showdivider = false) {
         if ($this->showinflatnavigation) {
             $indent = 0;
-            if ($this->type == self::TYPE_COURSE) {
+            if ($this->type == self::TYPE_COURSE || $this->key == self::COURSE_INDEX_PAGE) {
                 $indent = 1;
             }
             $flat = new flat_navigation_node($this, $indent);
@@ -2568,6 +2570,7 @@ class global_navigation extends navigation_node {
 
         $coursenode = $parent->add($coursename, $url, self::TYPE_COURSE, $shortname, $course->id);
         $coursenode->showinflatnavigation = $coursetype == self::COURSE_MY;
+
         $coursenode->hidden = (!$course->visible);
         $coursenode->title(format_string($course->fullname, true, array('context' => $coursecontext, 'escape' => false)));
         if ($canexpandcourse) {
@@ -2883,6 +2886,9 @@ class global_navigation extends navigation_node {
      */
     protected function load_courses_enrolled() {
         global $CFG;
+
+        $limit = (int) $CFG->navcourselimit;
+
         $sortorder = 'visible DESC';
         // Prevent undefined $CFG->navsortmycoursessort errors.
         if (empty($CFG->navsortmycoursessort)) {
@@ -2890,8 +2896,30 @@ class global_navigation extends navigation_node {
         }
         // Append the chosen sortorder.
         $sortorder = $sortorder . ',' . $CFG->navsortmycoursessort . ' ASC';
-        $courses = enrol_get_my_courses(null, $sortorder);
-        if (count($courses) && $this->show_my_categories()) {
+        $courses = enrol_get_my_courses('*', $sortorder);
+        $flatnavcourses = [];
+
+        // Go through the courses and see which ones we want to display in the flatnav.
+        foreach ($courses as $course) {
+            $classify = course_classify_for_timeline($course);
+
+            if ($classify == COURSE_TIMELINE_INPROGRESS) {
+                $flatnavcourses[$course->id] = $course;
+            }
+        }
+
+        // Get the number of courses that can be displayed in the nav block and in the flatnav.
+        $numtotalcourses = count($courses);
+        $numtotalflatnavcourses = count($flatnavcourses);
+
+        // Reduce the size of the arrays to abide by the 'navcourselimit' setting.
+        $courses = array_slice($courses, 0, $limit, true);
+        $flatnavcourses = array_slice($flatnavcourses, 0, $limit, true);
+
+        // Get the number of courses we are going to show for each.
+        $numshowncourses = count($courses);
+        $numshownflatnavcourses = count($flatnavcourses);
+        if ($numshowncourses && $this->show_my_categories()) {
             // Generate an array containing unique values of all the courses' categories.
             $categoryids = array();
             foreach ($courses as $course) {
@@ -2944,8 +2972,46 @@ class global_navigation extends navigation_node {
                 $this->add_category($mycategory, $parent, self::TYPE_MY_CATEGORY);
             }
         }
+
+        // Go through each course now and add it to the nav block, and the flatnav if applicable.
         foreach ($courses as $course) {
-            $this->add_course($course, false, self::COURSE_MY);
+            $node = $this->add_course($course, false, self::COURSE_MY);
+            if ($node) {
+                $node->showinflatnavigation = false;
+                // Check if we should also add this to the flat nav as well.
+                if (isset($flatnavcourses[$course->id])) {
+                    $node->showinflatnavigation = true;
+                }
+            }
+        }
+
+        // Go through each course in the flatnav now.
+        foreach ($flatnavcourses as $course) {
+            // Check if we haven't already added it.
+            if (!isset($courses[$course->id])) {
+                // Ok, add it to the flatnav only.
+                $node = $this->add_course($course, false, self::COURSE_MY);
+                $node->display = false;
+                $node->showinflatnavigation = true;
+            }
+        }
+
+        $showmorelinkinnav = $numtotalcourses > $numshowncourses;
+        $showmorelinkinflatnav = $numtotalflatnavcourses > $numshownflatnavcourses;
+        // Show a link to the course page if there are more courses the user is enrolled in.
+        if ($showmorelinkinnav || $showmorelinkinflatnav) {
+            // Adding hash to URL so the link is not highlighted in the navigation when clicked.
+            $url = new moodle_url('/course/index.php#');
+            $parent = $this->rootnodes['mycourses'];
+            $coursenode = $parent->add(get_string('morenavigationlinks'), $url, self::TYPE_CUSTOM, null, self::COURSE_INDEX_PAGE);
+
+            if ($showmorelinkinnav) {
+                $coursenode->display = true;
+            }
+
+            if ($showmorelinkinflatnav) {
+                $coursenode->showinflatnavigation = true;
+            }
         }
     }
 }
@@ -3140,7 +3206,7 @@ class global_navigation_for_ajax extends global_navigation {
         // If category is shown in MyHome then only show enrolled courses and hide empty subcategories,
         // else show all courses.
         if ($nodetype === self::TYPE_MY_CATEGORY) {
-            $courses = enrol_get_my_courses();
+            $courses = enrol_get_my_courses('*');
             $categoryids = array();
 
             // Only search for categories if basecategory was found.
index 5fe61d3..d91e27e 100644 (file)
@@ -35,15 +35,58 @@ require_once($CFG->libdir.'/outputrenderers.php');
 require_once($CFG->libdir.'/outputrequirementslib.php');
 
 /**
- * Invalidate all server and client side caches.
+ * Returns current theme revision number.
  *
- * This method deletes the physical directory that is used to cache the theme
- * files used for serving.
- * Because it deletes the main theme cache directory all themes are reset by
- * this function.
+ * @return int
  */
-function theme_reset_all_caches() {
-    global $CFG, $PAGE;
+function theme_get_revision() {
+    global $CFG;
+
+    if (empty($CFG->themedesignermode)) {
+        if (empty($CFG->themerev)) {
+            // This only happens during install. It doesn't matter what themerev we use as long as it's positive.
+            return 1;
+        } else {
+            return $CFG->themerev;
+        }
+
+    } else {
+        return -1;
+    }
+}
+
+/**
+ * Returns current theme sub revision number. This is the revision for
+ * this theme exclusively, not the global theme revision.
+ *
+ * @param string $themename The non-frankenstyle name of the theme
+ * @return int
+ */
+function theme_get_sub_revision_for_theme($themename) {
+    global $CFG;
+
+    if (empty($CFG->themedesignermode)) {
+        $pluginname = "theme_{$themename}";
+        $revision = get_config($pluginname, 'themerev');
+
+        if (empty($revision)) {
+            // This only happens during install. It doesn't matter what themerev we use as long as it's positive.
+            return 1;
+        } else {
+            return $revision;
+        }
+    } else {
+        return -1;
+    }
+}
+
+/**
+ * Calculates and returns the next theme revision number.
+ *
+ * @return int
+ */
+function theme_get_next_revision() {
+    global $CFG;
 
     $next = time();
     if (isset($CFG->themerev) and $next <= $CFG->themerev and $CFG->themerev - $next < 60*60) {
@@ -53,13 +96,164 @@ function theme_reset_all_caches() {
         $next = $CFG->themerev+1;
     }
 
-    set_config('themerev', $next); // time is unique even when you reset/switch database
+    return $next;
+}
+
+/**
+ * Calculates and returns the next theme revision number.
+ *
+ * @param string $themename The non-frankenstyle name of the theme
+ * @return int
+ */
+function theme_get_next_sub_revision_for_theme($themename) {
+    global $CFG;
+
+    $next = time();
+    $current = theme_get_sub_revision_for_theme($themename);
+    if ($next <= $current and $current - $next < 60 * 60) {
+        // This resolves problems when reset is requested repeatedly within 1s,
+        // the < 1h condition prevents accidental switching to future dates
+        // because we might not recover from it.
+        $next = $current + 1;
+    }
+
+    return $next;
+}
+
+/**
+ * Sets the current theme revision number.
+ *
+ * @param int $revision The new theme revision number
+ */
+function theme_set_revision($revision) {
+    set_config('themerev', $revision);
+}
+
+/**
+ * Sets the current theme revision number for a specific theme.
+ * This does not affect the global themerev value.
+ *
+ * @param string $themename The non-frankenstyle name of the theme
+ * @param int    $revision  The new theme revision number
+ */
+function theme_set_sub_revision_for_theme($themename, $revision) {
+    set_config('themerev', $revision, "theme_{$themename}");
+}
+
+/**
+ * Get the path to a theme config.php file.
+ *
+ * @param string $themename The non-frankenstyle name of the theme to check
+ */
+function theme_get_config_file_path($themename) {
+    global $CFG;
+
+    if (file_exists("{$CFG->dirroot}/theme/{$themename}/config.php")) {
+        return "{$CFG->dirroot}/theme/{$themename}/config.php";
+    } else if (!empty($CFG->themedir) and file_exists("{$CFG->themedir}/{$themename}/config.php")) {
+        return "{$CFG->themedir}/{$themename}/config.php";
+    } else {
+        return null;
+    }
+}
+
+/**
+ * Get the path to the local cached CSS file.
+ *
+ * @param string $themename      The non-frankenstyle theme name.
+ * @param int    $globalrevision The global theme revision.
+ * @param int    $themerevision  The theme specific revision.
+ * @param string $direction      Either 'ltr' or 'rtl' (case sensitive).
+ */
+function theme_get_css_filename($themename, $globalrevision, $themerevision, $direction) {
+    global $CFG;
+
+    $path = "{$CFG->localcachedir}/theme/{$globalrevision}/{$themename}/css";
+    $filename = $direction == 'rtl' ? "all-rtl_{$themerevision}" : "all_{$themerevision}";
+    return "{$path}/{$filename}.css";
+}
+
+/**
+ * Generates and saves the CSS files for the given theme configs.
+ *
+ * @param theme_config[] $themeconfigs An array of theme_config instances.
+ * @param array          $directions   Must be a subset of ['rtl', 'ltr'].
+ * @param bool           $cache        Should the generated files be stored in local cache.
+ */
+function theme_build_css_for_themes($themeconfigs = [], $directions = ['rtl', 'ltr'], $cache = true) {
+    global $CFG;
+
+    if (empty($themeconfigs)) {
+        return;
+    }
+
+    require_once("{$CFG->libdir}/csslib.php");
+
+    $themescss = [];
+    $themerev = theme_get_revision();
+    // Make sure the local cache directory exists.
+    make_localcache_directory('theme');
+
+    foreach ($themeconfigs as $themeconfig) {
+        $themecss = [];
+        $oldrevision = theme_get_sub_revision_for_theme($themeconfig->name);
+        $newrevision = theme_get_next_sub_revision_for_theme($themeconfig->name);
+
+        // First generate all the new css.
+        foreach ($directions as $direction) {
+            $themeconfig->set_rtl_mode(($direction === 'rtl'));
+
+            $themecss[$direction] = $themeconfig->get_css_content();
+            if ($cache) {
+                $filename = theme_get_css_filename($themeconfig->name, $themerev, $newrevision, $direction);
+                css_store_css($themeconfig, $filename, $themecss[$direction]);
+            }
+        }
+        $themescss[] = $themecss;
+
+        if ($cache) {
+            // Only update the theme revision after we've successfully created the
+            // new CSS cache.
+            theme_set_sub_revision_for_theme($themeconfig->name, $newrevision);
+
+            // Now purge old files. We must purge all old files in the local cache
+            // because we've incremented the theme sub revision. This will leave any
+            // files with the old revision inaccessbile so we might as well removed
+            // them from disk.
+            foreach (['ltr', 'rtl'] as $direction) {
+                $oldcss = theme_get_css_filename($themeconfig->name, $themerev, $oldrevision, $direction);
+                if (file_exists($oldcss)) {
+                    unlink($oldcss);
+                }
+            }
+        }
+    }
+
+    return $themescss;
+}
+
+/**
+ * Invalidate all server and client side caches.
+ *
+ * This method deletes the physical directory that is used to cache the theme
+ * files used for serving.
+ * Because it deletes the main theme cache directory all themes are reset by
+ * this function.
+ */
+function theme_reset_all_caches() {
+    global $CFG, $PAGE;
+
+    $next = theme_get_next_revision();
+    theme_set_revision($next);
 
     if (!empty($CFG->themedesignermode)) {
         $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themedesigner');
         $cache->purge();
     }
 
+    // Purge compiled post processed css.
+    cache::make('core', 'postprocessedcss')->purge();
+
     if ($PAGE) {
         $PAGE->reload_theme();
     }
@@ -76,27 +270,6 @@ function theme_set_designer_mod($state) {
     theme_reset_all_caches();
 }
 
-/**
- * Returns current theme revision number.
- *
- * @return int
- */
-function theme_get_revision() {
-    global $CFG;
-
-    if (empty($CFG->themedesignermode)) {
-        if (empty($CFG->themerev)) {
-            // This only happens during install. It doesn't matter what themerev we use as long as it's positive.
-            return 1;
-        } else {
-            return $CFG->themerev;
-        }
-
-    } else {
-        return -1;
-    }
-}
-
 /**
  * Checks if the given device has a theme defined in config.php.
  *
@@ -372,7 +545,7 @@ class theme_config {
      * @var stdClass Theme settings stored in config_plugins table.
      * This can not be set in theme config.php
      */
-    public $setting = null;
+    public $settings = null;
 
     /**
      * @var bool If set to true and the theme enables the dock then  blocks will be able
@@ -788,6 +961,14 @@ class theme_config {
         if ($rev > -1) {
             $filename = right_to_left() ? 'all-rtl' : 'all';
             $url = new moodle_url("$CFG->httpswwwroot/theme/styles.php");
+            $themesubrevision = theme_get_sub_revision_for_theme($this->name);
+
+            // Provide the sub revision to allow us to invalidate cached theme CSS
+            // on a per theme basis, rather than globally.
+            if ($themesubrevision && $themesubrevision > 0) {
+                $rev .= "_{$themesubrevision}";
+            }
+
             if (!empty($CFG->slasharguments)) {
                 $slashargs = '';
                 if (!$svg) {
@@ -907,6 +1088,44 @@ class theme_config {
 
         return $csscontent;
     }
+    /**
+     * Set post processed CSS content cache.
+     *
+     * @param string $csscontent The post processed CSS content.
+     * @return bool True if the content was successfully cached.
+     */
+    public function set_css_content_cache($csscontent) {
+
+        $cache = cache::make('core', 'postprocessedcss');
+        $key = $this->get_css_cache_key();
+
+        return $cache->set($key, $csscontent);
+    }
+
+    /**
+     * Return cached post processed CSS content.
+     *
+     * @return bool|string The cached css content or false if not found.
+     */
+    public function get_css_cached_content() {
+
+        $key = $this->get_css_cache_key();
+        $cache = cache::make('core', 'postprocessedcss');
+
+        return $cache->get($key);
+    }
+
+    /**
+     * Generate the css content cache key.
+     *
+     * @return string The post processed css cache key.
+     */
+    public function get_css_cache_key() {
+        $nosvg = (!$this->use_svg_icons()) ? 'nosvg_' : '';
+        $rtlmode = ($this->rtlmode == true) ? 'rtl' : 'ltr';
+
+        return $nosvg . $this->name . '_' . $rtlmode;
+    }
 
     /**
      * Get the theme designer css markup,
index a3997c4..57c67c0 100644 (file)
@@ -229,6 +229,11 @@ class phpunit_util extends testing_util {
         // Reset internal users.
         core_user::reset_internal_users();
 
+        // Clear static caches in calendar container.
+        if (class_exists('\core_calendar\local\event\container', false)) {
+            core_calendar\local\event\container::reset_caches();
+        }
+
         //TODO MDL-25290: add more resets here and probably refactor them to new core function
 
         // Reset course and module caches.
index 9759ced..7f2e3f8 100644 (file)
@@ -384,7 +384,14 @@ function default_exception_handler($ex) {
                 // If you enable db debugging and exception is thrown, the print footer prints a lot of rubbish
                 $DB->set_debug(0);
             }
-            echo $OUTPUT->fatal_error($info->message, $info->moreinfourl, $info->link, $info->backtrace, $info->debuginfo,
+            if (AJAX_SCRIPT) {
+                // If we are in an AJAX script we don't want to use PREFERRED_RENDERER_TARGET.
+                // Because we know we will want to use ajax format.
+                $renderer = $PAGE->get_renderer('core', null, 'ajax');
+            } else {
+                $renderer = $OUTPUT;
+            }
+            echo $renderer->fatal_error($info->message, $info->moreinfourl, $info->link, $info->backtrace, $info->debuginfo,
                 $info->errorcode);
         } catch (Exception $e) {
             $out_ex = $e;
@@ -897,7 +904,11 @@ function initialise_fullme() {
     // (That is, the Moodle server uses http, with an external box translating everything to https).
     if (empty($CFG->sslproxy)) {
         if ($rurl['scheme'] === 'http' and $wwwroot['scheme'] === 'https') {
-            print_error('sslonlyaccess', 'error');
+            if (defined('REQUIRE_CORRECT_ACCESS') && REQUIRE_CORRECT_ACCESS) {
+                print_error('sslonlyaccess', 'error');
+            } else {
+                redirect($CFG->wwwroot, get_string('wwwrootmismatch', 'error', $CFG->wwwroot), 3);
+            }
         }
     } else {
         if ($wwwroot['scheme'] !== 'https') {
index 84d077b..d1f1adc 100644 (file)
@@ -125,6 +125,12 @@ class flexible_table {
      */
     private $prefs = array();
 
+    /** @var $sheettitle */
+    protected $sheettitle;
+
+    /** @var $filename */
+    protected $filename;
+
     /**
      * Constructor
      * @param string $uniqueid all tables have to have a unique id, this is used
@@ -180,7 +186,7 @@ class flexible_table {
         } else if (is_null($this->exportclass) && !empty($this->download)) {
             $this->exportclass = new table_dataformat_export_format($this, $this->download);
             if (!$this->exportclass->document_started()) {
-                $this->exportclass->start_document($this->filename);
+                $this->exportclass->start_document($this->filename, $this->sheettitle);
             }
         }
         return $this->exportclass;
@@ -1545,7 +1551,7 @@ class table_sql extends flexible_table {
      * Of course you can use sub-queries, JOINS etc. by putting them in the
      * appropriate clause of the query.
      */
-    function set_sql($fields, $from, $where, array $params = NULL) {
+    function set_sql($fields, $from, $where, array $params = array()) {
         $this->sql = new stdClass();
         $this->sql->fields = $fields;
         $this->sql->from = $from;
@@ -1741,11 +1747,14 @@ class table_dataformat_export_format extends table_default_export_format_parent
      * Start document
      *
      * @param string $filename
+     * @param string $sheettitle
      */
-    public function start_document($filename) {
-        $this->filename = $filename;
+    public function start_document($filename, $sheettitle) {
         $this->documentstarted = true;
         $this->dataformat->set_filename($filename);
+        $this->dataformat->send_http_headers();
+        $this->dataformat->set_sheettitle($sheettitle);
+        $this->dataformat->start_output();
     }
 
     /**
@@ -1755,7 +1764,6 @@ class table_dataformat_export_format extends table_default_export_format_parent
      */
     public function start_table($sheettitle) {
         $this->dataformat->set_sheettitle($sheettitle);
-        $this->dataformat->send_http_headers();
     }
 
     /**
@@ -1765,7 +1773,13 @@ class table_dataformat_export_format extends table_default_export_format_parent
      */
     public function output_headers($headers) {
         $this->columns = $headers;
-        $this->dataformat->write_header($headers);
+        if (method_exists($this->dataformat, 'write_header')) {
+            error_log('The function write_header() does not support multiple sheets. In order to support multiple sheets you ' .
+                'must implement start_output() and start_sheet() and remove write_header() in your dataformat.');
+            $this->dataformat->write_header($headers);
+        } else {
+            $this->dataformat->start_sheet($headers);
+        }
     }
 
     /**
@@ -1782,15 +1796,21 @@ class table_dataformat_export_format extends table_default_export_format_parent
      * Finish export
      */
     public function finish_table() {
-        $this->dataformat->write_footer($this->columns);
+        if (method_exists($this->dataformat, 'write_footer')) {
+            error_log('The function write_footer() does not support multiple sheets. In order to support multiple sheets you ' .
+                'must implement close_sheet() and close_output() and remove write_footer() in your dataformat.');
+            $this->dataformat->write_footer($this->columns);
+        } else {
+            $this->dataformat->close_sheet($this->columns);
+        }
     }
 
     /**
      * Finish download
      */
     public function finish_document() {
-        exit;
+        $this->dataformat->close_output();
+        exit();
     }
-
 }
 
index ea18b51..87a37bd 100644 (file)
@@ -1229,6 +1229,117 @@ EOF;
         $file = array_shift($files);
         $this->assertTrue($file->is_directory());
     }
+
+    /**
+     * Test file_get_draft_area_info.
+     */
+    public function test_file_get_draft_area_info() {
+        global $USER;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $fs = get_file_storage();
+
+        $filerecord = array(
+            'filename'  => 'one.txt',
+        );
+        $file = self::create_draft_file($filerecord);
+        $size = $file->get_filesize();
+        $draftitemid = $file->get_itemid();
+        // Add another file.
+        $filerecord = array(
+            'itemid'  => $draftitemid,
+            'filename'  => 'second.txt',
+        );
+        $file = self::create_draft_file($filerecord);
+        $size += $file->get_filesize();
+
+        // Create directory.
+        $usercontext = context_user::instance($USER->id);
+        $dir = $fs->create_directory($usercontext->id, 'user', 'draft', $draftitemid, '/testsubdir/');
+        // Add file to directory.
+        $filerecord = array(
+            'itemid'  => $draftitemid,
+            'filename' => 'third.txt',
+            'filepath' => '/testsubdir/',
+        );
+        $file = self::create_draft_file($filerecord);
+        $size += $file->get_filesize();
+
+        $fileinfo = file_get_draft_area_info($draftitemid);
+        $this->assertEquals(3, $fileinfo['filecount']);
+        $this->assertEquals($size, $fileinfo['filesize']);
+        $this->assertEquals(1, $fileinfo['foldercount']);   // Directory created.
+        $this->assertEquals($size, $fileinfo['filesize_without_references']);
+
+        // Now get files from just one folder.
+        $fileinfo = file_get_draft_area_info($draftitemid, '/testsubdir/');
+        $this->assertEquals(1, $fileinfo['filecount']);
+        $this->assertEquals($file->get_filesize(), $fileinfo['filesize']);
+        $this->assertEquals(0, $fileinfo['foldercount']);   // No subdirectories inside the directory.
+        $this->assertEquals($file->get_filesize(), $fileinfo['filesize_without_references']);
+
+        // Check we get the same results if we call file_get_file_area_info.
+        $fileinfo = file_get_file_area_info($usercontext->id, 'user', 'draft', $draftitemid);
+        $this->assertEquals(3, $fileinfo['filecount']);
+        $this->assertEquals($size, $fileinfo['filesize']);
+        $this->assertEquals(1, $fileinfo['foldercount']);   // Directory created.
+        $this->assertEquals($size, $fileinfo['filesize_without_references']);
+    }
+
+    /**
+     * Test file_get_file_area_info.
+     */
+    public function test_file_get_file_area_info() {
+        global $USER;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $fs = get_file_storage();
+
+        $filerecord = array(
+            'filename'  => 'one.txt',
+        );
+        $file = self::create_draft_file($filerecord);
+        $size = $file->get_filesize();
+        $draftitemid = $file->get_itemid();
+        // Add another file.
+        $filerecord = array(
+            'itemid'  => $draftitemid,
+            'filename'  => 'second.txt',
+        );
+        $file = self::create_draft_file($filerecord);
+        $size += $file->get_filesize();
+
+        // Create directory.
+        $usercontext = context_user::instance($USER->id);
+        $dir = $fs->create_directory($usercontext->id, 'user', 'draft', $draftitemid, '/testsubdir/');
+        // Add file to directory.
+        $filerecord = array(
+            'itemid'  => $draftitemid,
+            'filename' => 'third.txt',
+            'filepath' => '/testsubdir/',
+        );
+        $file = self::create_draft_file($filerecord);
+        $size += $file->get_filesize();
+
+        // Add files to user private file area.
+        $options = array('subdirs' => 1, 'maxfiles' => 3);
+        file_merge_files_from_draft_area_into_filearea($draftitemid, $file->get_contextid(), 'user', 'private', 0, $options);
+
+        $fileinfo = file_get_file_area_info($usercontext->id, 'user', 'private');
+        $this->assertEquals(3, $fileinfo['filecount']);
+        $this->assertEquals($size, $fileinfo['filesize']);
+        $this->assertEquals(1, $fileinfo['foldercount']);   // Directory created.
+        $this->assertEquals($size, $fileinfo['filesize_without_references']);
+
+        // Now get files from just one folder.
+        $fileinfo = file_get_file_area_info($usercontext->id, 'user', 'private', 0, '/testsubdir/');
+        $this->assertEquals(1, $fileinfo['filecount']);
+        $this->assertEquals($file->get_filesize(), $fileinfo['filesize']);
+        $this->assertEquals(0, $fileinfo['foldercount']);   // No subdirectories inside the directory.
+        $this->assertEquals($file->get_filesize(), $fileinfo['filesize_without_references']);
+    }
 }
 
 /**
index 09c2839..fce1d3a 100644 (file)
@@ -10,6 +10,9 @@ information provided here is intended especially for developers.
 * Removed accesslib private functions: load_course_context(), load_role_access_by_context(), dedupe_user_access() (MDL-49398).
 * Internal "accessdata" structure format has changed to improve ability to perform role definition caching (MDL-49398).
 * Role definitions are no longer cached in user session (MDL-49398).
+* External function core_group_external::get_activity_allowed_groups now returns an additional field: canaccessallgroups.
+  It indicates whether the user will be able to access all the activity groups.
+* file_get_draft_area_info does not sum the root folder anymore when calculating the foldercount.
 
 === 3.3.1 ===
 
index d12bfd2..6f50f4c 100644 (file)
Binary files a/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-debug.js and b/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-debug.js differ
index fd52350..94a7844 100644 (file)
Binary files a/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-min.js and b/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-min.js differ
index d12bfd2..6f50f4c 100644 (file)
Binary files a/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm.js and b/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm.js differ
index 25f79ae..f41a06f 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js and b/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js differ
index bb94ef7..79172f7 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js and b/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js differ
index 25f79ae..f41a06f 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js and b/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js differ
index 88eead8..98d87ea 100644 (file)
@@ -96,7 +96,7 @@ Y.extend(Confirmation, Y.Base, {
         _uninstall: function(e, langCodes) {
             Y.config.win.location.href = this.get('uninstallUrl') + '?mode=4' +
                                          '&sesskey=' + M.cfg.sesskey +
-                                         '&confirmtouninstall=' + langCodes.join('-');
+                                         '&confirmtouninstall=' + langCodes.join('/');
         }
 
 });
index a1b5842..e9581f7 100644 (file)
@@ -150,7 +150,7 @@ Y.extend(EXCEPTION, M.core.notification.info, {
                             "<div class='stacktrace-file'>$3</div>" +
                             "<div class='stacktrace-call'>$1</div>");
                 }
-                return lines.join('');
+                return lines.join("\n");
             },
             value: ''
         },
index 6c77c7f..d57bd60 100644 (file)
Binary files a/media/player/videojs/amd/build/loader.min.js and b/media/player/videojs/amd/build/loader.min.js differ
index 2601cf8..427b609 100644 (file)
@@ -62,7 +62,7 @@ define(['jquery', 'core/event'], function($, Event) {
             .addBack(selector)
             .find('audio, video').each(function() {
                 var id = $(this).attr('id'),
-                    config = $(this).data('setup'),
+                    config = $(this).data('setup-lazy'),
                     modules = ['media_videojs/video-lazy'];
 
                 if (config.techOrder && config.techOrder.indexOf('youtube') !== -1) {
index f13352f..3d076b0 100644 (file)
@@ -129,8 +129,13 @@ class media_videojs_plugin extends core_media_player_native {
         }
 
         // Attributes for the video/audio tag.
+        // We use data-setup-lazy as the attribute name for the config instead of
+        // data-setup because data-setup will cause video.js to load the player as soon as the library is loaded,
+        // which is BEFORE we have a chance to load any additional libraries (youtube).
+        // The data-setup-lazy is just a tag name that video.js does not recognise so we can manually initialise
+        // it when we are sure the dependencies are loaded.
         $attributes = [
-            'data-setup' => '{' . join(', ', $datasetup) . '}',
+            'data-setup-lazy' => '{' . join(', ', $datasetup) . '}',
             'id' => 'id_videojs_' . uniqid(),
             'class' => get_config('media_videojs', $isaudio ? 'audiocssclass' : 'videocssclass')
         ];
index 43d698c..7bc50ea 100644 (file)
@@ -230,7 +230,7 @@ class media_videojs_testcase extends advanced_testcase {
 
     protected function youtube_plugin_engaged($t) {
         $this->assertContains('mediaplugin_videojs', $t);
-        $this->assertContains('data-setup="{&quot;techOrder&quot;: [&quot;youtube&quot;]', $t);
+        $this->assertContains('data-setup-lazy="{&quot;techOrder&quot;: [&quot;youtube&quot;]', $t);
     }
 
     /**
index bded037..5f20161 100644 (file)
Binary files a/message/amd/build/message_repository.min.js and b/message/amd/build/message_repository.min.js differ
index b81eae6..6c0ee94 100644 (file)
@@ -22,7 +22,7 @@
  * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/ajax', 'core/notification', 'core/log'], function($, Ajax, Notification, Log) {
+define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) {
     /**
      * Retrieve a list of messages from the server.
      *
@@ -72,9 +72,7 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/log'], function($, Aja
 
         var promise = Ajax.call([request])[0];
 
-        promise.fail(function(e) {
-            Log.error('Could not retrieve unread message count: ' + e.message);
-        });
+        promise.fail(Notification.exception);
 
         return promise;
     };
index beb592e..6d076df 100644 (file)
Binary files a/message/output/popup/amd/build/notification_repository.min.js and b/message/output/popup/amd/build/notification_repository.min.js differ
index 093238a..2fdb83e 100644 (file)
@@ -22,7 +22,7 @@
  * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['core/ajax', 'core/notification', 'core/log'], function(Ajax, Notification, Log) {
+define(['core/ajax', 'core/notification'], function(Ajax, Notification) {
     /**
      * Retrieve a list of notifications from the server.
      *
@@ -64,9 +64,7 @@ define(['core/ajax', 'core/notification', 'core/log'], function(Ajax, Notificati
 
         var promise = Ajax.call([request])[0];
 
-        promise.fail(function(e) {
-            Log.error('Could not retrieve notifications count: ' + e.message);
-        });
+        promise.fail(Notification.exception);
 
         return promise;
     };
index e197a6d..1439d45 100644 (file)
@@ -295,6 +295,28 @@ function xmldb_assign_upgrade($oldversion) {
 
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
+    if ($oldversion < 2017061200) {
+        // Data fix any assign group override event priorities which may have been accidentally nulled due to a bug on the group
+        // overrides edit form.
+
+        // First, find all assign group override events having null priority (and join their corresponding assign_overrides entry).
+        $sql = "SELECT e.id AS id, o.sortorder AS priority
+                  FROM {assign_overrides} o
+                  JOIN {event} e ON (e.modulename = 'assign' AND o.assignid = e.instance AND e.groupid = o.groupid)
+                 WHERE o.groupid IS NOT NULL AND e.priority IS NULL
+              ORDER BY o.id";
+        $affectedrs = $DB->get_recordset_sql($sql);
+
+        // Now update the event's priority based on the assign_overrides sortorder we found. This uses similar logic to
+        // assign_refresh_events(), except we've restricted the set of assignments and overrides we're dealing with here.
+        foreach ($affectedrs as $record) {
+            $DB->set_field('event', 'priority', $record->priority, ['id' => $record->id]);
+        }
+        $affectedrs->close();
+
+        // Main savepoint reached.
+        upgrade_mod_savepoint(true, 2017061200, 'assign');
+    }
 
     return true;
 }
index db2deae..e6c0f4a 100644 (file)
@@ -200,7 +200,7 @@ class assign_feedback_comments extends assign_feedback_plugin {
         global $DB;
         $feedbackcomment = $this->get_feedback_comments($grade->id);
         $quickgradecomments = optional_param('quickgrade_comments_' . $userid, null, PARAM_RAW);
-        if (!$quickgradecomments) {
+        if (!$quickgradecomments && $quickgradecomments !== '') {
             return true;
         }
         if ($feedbackcomment) {
diff --git a/mod/assign/feedback/comments/tests/behat/feedback_comments.feature b/mod/assign/feedback/comments/tests/behat/feedback_comments.feature
new file mode 100644 (file)
index 0000000..245e53a
--- /dev/null
@@ -0,0 +1,47 @@
+@mod @mod_assign @assignfeedback @assignfeedback_comments
+Feature: In an assignment, teachers can provide feedback comments on student submissions
+  In order to provide feedback to students on their assignments
+  As a teacher,
+  I need to create feedback comments against their submissions.
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 0 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | teacher |
+      | student1 | C1 | student |
+
+  @javascript
+  Scenario: Teachers should be able to add and remove feedback comments via the quick grading interface
+    Given the following "activities" exist:
+      | activity | course | idnumber | name             | assignsubmission_onlinetext_enabled | assignfeedback_comments_enabled |
+      | assign   | C1     | assign1  | Test assignment1 | 1                                   | 1                               |
+    And I log in as "student1"
+    And I am on "Course 1" course homepage
+    And I follow "Test assignment1"
+    And I press "Add submission"
+    And I set the following fields to these values:
+      | Online text | I'm the student1 submission |
+    And I press "Save changes"
+    And I log out
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I follow "Test assignment1"
+    And I navigate to "View all submissions" in current page administration
+    Then I click on "Quick grading" "checkbox"
+    And I set the field "Feedback comments" to "Feedback from teacher."
+    And I press "Save all quick grading changes"
+    And I should see "The grade changes were saved"
+    And I press "Continue"
+    And I should see "Feedback from teacher."
+    And I set the field "Feedback comments" to ""
+    And I press "Save all quick grading changes"
+    And I should see "The grade changes were saved"
+    And I press "Continue"
+    And I should not see "Feedback from teacher."
index 33caa81..47867bc 100644 (file)
@@ -646,8 +646,9 @@ class assign_grading_table extends table_sql implements renderable {
         static $markers = null;
         static $markerlist = array();
         if ($markers === null) {
-            list($sort, $params) = users_order_by_sql();
-            $markers = get_users_by_capability($this->assignment->get_context(), 'mod/assign:grade', '', $sort);
+            list($sort, $params) = users_order_by_sql('u');
+            // Only enrolled users could be assigned as potential markers.
+            $markers = get_enrolled_users($this->assignment->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
             $markerlist[0] = get_string('choosemarker', 'assign');
             $viewfullnames = has_capability('moodle/site:viewfullnames', $this->assignment->get_context());
             foreach ($markers as $marker) {
index f992d36..a763f37 100644 (file)
@@ -4017,8 +4017,9 @@ class assign {
         // Get markers to use in drop lists.
         $markingallocationoptions = array();
         if ($markingallocation) {
-            list($sort, $params) = users_order_by_sql();
-            $markers = get_users_by_capability($this->context, 'mod/assign:grade', '', $sort);
+            list($sort, $params) = users_order_by_sql('u');
+            // Only enrolled users could be assigned as potential markers.
+            $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
             $markingallocationoptions[''] = get_string('filternone', 'assign');
             $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
             $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
@@ -4644,8 +4645,9 @@ class assign {
             'usershtml' => $usershtml,
         );
 
-        list($sort, $params) = users_order_by_sql();
-        $markers = get_users_by_capability($this->get_context(), 'mod/assign:grade', '', $sort);
+        list($sort, $params) = users_order_by_sql('u');
+        // Only enrolled users could be assigned as potential markers.
+        $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
         $markerlist = array();
         foreach ($markers as $marker) {
             $markerlist[$marker->id] = fullname($marker);
@@ -6532,8 +6534,9 @@ class assign {
         if ($markingallocation) {
             $markingallocationoptions[''] = get_string('filternone', 'assign');
             $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
-            list($sort, $params) = users_order_by_sql();
-            $markers = get_users_by_capability($this->context, 'mod/assign:grade', '', $sort);
+            list($sort, $params) = users_order_by_sql('u');
+            // Only enrolled users could be assigned as potential markers.
+            $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
             foreach ($markers as $marker) {
                 $markingallocationoptions[$marker->id] = fullname($marker);
             }
@@ -7167,8 +7170,9 @@ class assign {
             $this->get_instance()->markingallocation &&
             has_capability('mod/assign:manageallocations', $this->context)) {
 
-            list($sort, $params) = users_order_by_sql();
-            $markers = get_users_by_capability($this->context, 'mod/assign:grade', '', $sort);
+            list($sort, $params) = users_order_by_sql('u');
+            // Only enrolled users could be assigned as potential markers.
+            $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
             $markerlist = array('' =>  get_string('choosemarker', 'assign'));
             $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
             foreach ($markers as $marker) {
@@ -7587,8 +7591,9 @@ class assign {
             'usershtml' => ''   // initialise these parameters with real information.
         );
 
-        list($sort, $params) = users_order_by_sql();
-        $markers = get_users_by_capability($this->get_context(), 'mod/assign:grade', '', $sort);
+        list($sort, $params) = users_order_by_sql('u');
+        // Only enrolled users could be assigned as potential markers.
+        $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
         $markerlist = array();
         foreach ($markers as $marker) {
             $markerlist[$marker->id] = fullname($marker);
index b3cf8d4..0c68b77 100644 (file)
@@ -55,6 +55,9 @@ class assign_override_form extends moodleform {
     /** @var int userid, if provided. */
     protected $userid;
 
+    /** @var int sortorder, if provided. */
+    protected $sortorder;
+
     /**
      * Constructor.
      * @param moodle_url $submiturl the form action URL.
@@ -72,6 +75,7 @@ class assign_override_form extends moodleform {
         $this->groupmode = $groupmode;
         $this->groupid = empty($override->groupid) ? 0 : $override->groupid;
         $this->userid = empty($override->userid) ? 0 : $override->userid;
+        $this->sortorder = empty($override->sortorder) ? null : $override->sortorder;
 
         parent::__construct($submiturl, null, 'post');
 
@@ -97,6 +101,10 @@ class assign_override_form extends moodleform {
                 $mform->addElement('select', 'groupid',
                         get_string('overridegroup', 'assign'), $groupchoices);
                 $mform->freeze('groupid');
+                // Add a sortorder element.
+                $mform->addElement('hidden', 'sortorder', $this->sortorder);
+                $mform->setType('sortorder', PARAM_INT);
+                $mform->freeze('sortorder');
             } else {
                 // Prepare the list of groups.
                 $groups = groups_get_all_groups($cm->course);
index 381adb2..309cd8a 100644 (file)
@@ -29,6 +29,7 @@ $string['configmaxbytes'] = 'Maximum file size';
 $string['countfiles'] = '{$a} files';
 $string['default'] = 'Enabled by default';
 $string['default_help'] = 'If set, this submission method will be enabled by default for all new assignments.';
+$string['defaultacceptedfiletypes'] = 'Default accepted file types';
 $string['enabled'] = 'File submissions';
 $string['enabled_help'] = 'If enabled, students are able to upload one or more files as their submission.';
 $string['eventassessableuploaded'] = 'A file has been uploaded.';
index e2fe32f..d96b293 100644 (file)
@@ -71,7 +71,12 @@ class assign_submission_file extends assign_submission_plugin {
 
         $defaultmaxfilesubmissions = $this->get_config('maxfilesubmissions');
         $defaultmaxsubmissionsizebytes = $this->get_config('maxsubmissionsizebytes');
-        $defaultfiletypes = (string)$this->get_config('filetypeslist');
+        if ($this->assignment->has_instance()) {
+            $defaultfiletypes = $this->get_config('filetypeslist');
+        } else {
+            $defaultfiletypes = get_config('assignsubmission_file', 'filetypes');
+        }
+        $defaultfiletypes = (string)$defaultfiletypes;
 
         $settings = array();
         $options = array();
@@ -108,7 +113,7 @@ class assign_submission_file extends assign_submission_plugin {
                            'notchecked');
 
         $name = get_string('acceptedfiletypes', 'assignsubmission_file');
-        $mform->addElement('text', 'assignsubmission_file_filetypes', $name);
+        $mform->addElement('text', 'assignsubmission_file_filetypes', $name, array('size' => '60'));
         $mform->addHelpButton('assignsubmission_file_filetypes', 'acceptedfiletypes', 'assignsubmission_file');
         $mform->setType('assignsubmission_file_filetypes', PARAM_RAW);
         $mform->setDefault('assignsubmission_file_filetypes', $defaultfiletypes);
index 4d7eb22..a895e62 100644 (file)
@@ -32,6 +32,10 @@ $settings->add(new admin_setting_configtext('assignsubmission_file/maxfiles',
                    new lang_string('maxfiles', 'assignsubmission_file'),
                    new lang_string('maxfiles_help', 'assignsubmission_file'), 20, PARAM_INT));
 
+$settings->add(new admin_setting_configtext('assignsubmission_file/filetypes',
+                   new lang_string('defaultacceptedfiletypes', 'assignsubmission_file'),
+                   new lang_string('acceptedfiletypes_help', 'assignsubmission_file'), '', PARAM_RAW, 60));
+
 if (isset($CFG->maxbytes)) {
 
     $name = new lang_string('maximumsubmissionsize', 'assignsubmission_file');
diff --git a/mod/assign/tests/markerallocation_test.php b/mod/assign/tests/markerallocation_test.php
new file mode 100644 (file)
index 0000000..09b5591
--- /dev/null
@@ -0,0 +1,148 @@
+<?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/>.
+
+/**
+ * Unit tests for (some of) mod/assign/markerallocaion_test.php.
+ *
+ * @package    mod_assign
+ * @category   test
+ * @copyright  2017 Andrés Melo <andres.torres@blackboard.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/lib/accesslib.php');
+require_once($CFG->dirroot . '/course/lib.php');
+
+/**
+ * This class tests some of marker allocation functionality.
+ *
+ * @package    mod_assign
+ * @copyright  2017 Andrés Melo <andres.torres@blackboard.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_assign_markerallocation_testcase extends advanced_testcase {
+
+    /**
+     * Create all the needed elements to test the difference between both functions.
+     */
+    public function test_markerusers() {
+        $this->resetAfterTest();
+        global $DB;
+
+        // Create a course, by default it is created with 5 sections.
+        $this->course = $this->getDataGenerator()->create_course();
+
+        // Setting assing module, markingworkflow and markingallocation set to 1 to enable marker allocation.
+        $record = new stdClass();
+        $record->course = $this->course;
+
+        $modulesettings = array(
+            'alwaysshowdescription'             => 1,
+            'submissiondrafts'                  => 1,
+            'requiresubmissionstatement'        => 0,
+            'sendnotifications'                 => 0,
+            'sendstudentnotifications'          => 1,
+            'sendlatenotifications'             => 0,
+            'duedate'                           => 0,
+            'allowsubmissionsfromdate'          => 0,
+            'grade'                             => 100,
+            'cutoffdate'                        => 0,
+            'teamsubmission'                    => 0,
+            'requireallteammemberssubmit'       => 0,
+            'teamsubmissiongroupingid'          => 0,
+            'blindmarking'                      => 0,
+            'attemptreopenmethod'               => 'none',
+            'maxattempts'                       => -1,
+            'markingworkflow'                   => 1,
+            'markingallocation'                 => 1,
+        );
+
+        $assignelement = $this->getDataGenerator()->create_module('assign', $record, $modulesettings);
+
+        $coursesectionid = course_add_cm_to_section($this->course->id, $assignelement->id, 1);
+
+        // Adding users to the course.
+        $userdata = array();
+        $userdata['firstname'] = 'teacher1';
+        $userdata['lasttname'] = 'lastname_teacher1';
+
+        $user1 = $this->getDataGenerator()->create_user($userdata);
+
+        $this->getDataGenerator()->enrol_user($user1->id, $this->course->id, 'teacher');
+
+        $userdata = array();
+        $userdata['firstname'] = 'teacher2';
+        $userdata['lasttname'] = 'lastname_teacher2';
+
+        $user2 = $this->getDataGenerator()->create_user($userdata);
+
+        $this->getDataGenerator()->enrol_user($user2->id, $this->course->id, 'teacher');
+
+        $userdata = array();
+        $userdata['firstname'] = 'student';
+        $userdata['lasttname'] = 'lastname_student';
+
+        $user3 = $this->getDataGenerator()->create_user($userdata);
+
+        $this->getDataGenerator()->enrol_user($user3->id, $this->course->id, 'student');
+
+        // Adding manager to the system.
+        $userdata = array();
+        $userdata['firstname'] = 'Manager';
+        $userdata['lasttname'] = 'lastname_Manager';
+
+        $user4 = $this->getDataGenerator()->create_user($userdata);
+
+        // Getting id of manager role.
+        $managerrole = $DB->get_record('role', array('shortname' => 'manager'));
+        if (!empty($managerrole)) {
+            // By default the context of the system is assigned.
+            $idassignment = $this->getDataGenerator()->role_assign($managerrole->id, $user4->id);
+        }
+
+        $oldusers = array($user1, $user2, $user4);
+        $newusers = array($user1, $user2);
+
+        list($sort, $params) = users_order_by_sql('u');
+
+        // Old code, it must return 3 users: teacher1, teacher2 and Manger.
+        $oldmarkers = get_users_by_capability(context_course::instance($this->course->id), 'mod/assign:grade', '', $sort);
+        // New code, it must return 2 users: teacher1 and teacher2.
+        $newmarkers = get_enrolled_users(context_course::instance($this->course->id), 'mod/assign:grade', 0, 'u.*', $sort);
+
+        // Test result quantity.
+        $this->assertEquals(count($oldusers), count($oldmarkers));
+        $this->assertEquals(count($newusers), count($newmarkers));
+        $this->assertEquals(count($oldmarkers) > count($newmarkers), true);
+
+        // Elements expected with new code.
+        foreach ($newmarkers as $key => $nm) {
+            $this->assertEquals($nm, $newusers[array_search($nm, $newusers)]);
+        }
+
+        // Elements expected with old code.
+        foreach ($oldusers as $key => $os) {
+            $this->assertEquals($os->id, $oldmarkers[$os->id]->id);
+            unset($oldmarkers[$os->id]);
+        }
+
+        $this->assertEquals(count($oldmarkers), 0);
+
+    }
+}
index d75c94c..2c7e0e0 100644 (file)
@@ -25,6 +25,6 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
-$plugin->version  = 2017051500;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version  = 2017061200;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires = 2017050500;    // Requires this Moodle version.
 $plugin->cron     = 60;
index f4edefa..634778e 100644 (file)
@@ -104,14 +104,17 @@ class data_field_latlong extends data_field_base {
         foreach ($latlongsrs as $latlong) {
             $latitude = format_float($latlong->la, 4);
             $longitude = format_float($latlong->lo, 4);
-            $options[$latlong->la . ',' . $latlong->lo] = $latitude . ' ' . $longitude;
+            if ($latitude && $longitude) {
+                $options[$latlong->la . ',' . $latlong->lo] = $latitude . ' ' . $longitude;
+            }
         }
         $latlongsrs->close();
 
         $classes = array('class' => 'accesshide');
         $return = html_writer::label(get_string('latlong', 'data'), 'menuf_'.$this->field->id, false, $classes);
         $classes = array('class' => 'custom-select');
-        $return .= html_writer::select($options, 'f_'.$this->field->id, $value, null, $classes);
+        $return .= html_writer::select($options, 'f_'.$this->field->id, $value, array('' => get_string('menuchoose', 'data')),
+            $classes);
        return $return;
     }
 
index 4cf34b1..2289840 100644 (file)
@@ -561,7 +561,8 @@ class mod_feedback_completion extends mod_feedback_structure {
 
         // Update completion state.
         $completion = new completion_info($this->cm->get_course());
-        if (isloggedin() && !isguestuser() && $completion->is_enabled($this->cm) && $this->feedback->completionsubmit) {
+        if (isloggedin() && !isguestuser() && $completion->is_enabled($this->cm) &&
+                $this->cm->completion == COMPLETION_TRACKING_AUTOMATIC && $this->feedback->completionsubmit) {
             $completion->update_state($this->cm, COMPLETION_COMPLETE);
         }
     }
index faa58c0..2d89f59 100644 (file)
@@ -1149,6 +1149,8 @@ class mod_feedback_external extends external_api {
 
         $feedbackstructure = new mod_feedback_structure($feedback, $cm, $course->id);
         $responsestable = new mod_feedback_responses_table($feedbackstructure, $groupid);
+        // Ensure responses number is correct prior returning them.
+        $feedbackstructure->shuffle_anonym_responses();
         $anonresponsestable = new mod_feedback_responses_anon_table($feedbackstructure, $groupid);
 
         $result = array(
index 5fdca5a..d242b19 100644 (file)
@@ -1372,11 +1372,12 @@ function feedback_items_from_template($feedback, $templateid, $deleteold = false
             if ($completeds = $DB->get_records('feedback_completed', $params)) {
                 $completion = new completion_info($course);
                 foreach ($completeds as $completed) {
+                    $DB->delete_records('feedback_completed', array('id' => $completed->id));
                     // Update completion state
-                    if ($completion->is_enabled($cm) && $feedback->completionsubmit) {
+                    if ($completion->is_enabled($cm) && $cm->completion == COMPLETION_TRACKING_AUTOMATIC &&
+                            $feedback->completionsubmit) {
                         $completion->update_state($cm, COMPLETION_INCOMPLETE, $completed->userid);
                     }
-                    $DB->delete_records('feedback_completed', array('id'=>$completed->id));
                 }
             }
             $DB->delete_records('feedback_completedtmp', array('feedback'=>$feedback->id));
@@ -1730,11 +1731,12 @@ function feedback_delete_all_items($feedbackid) {
     if ($completeds = $DB->get_records('feedback_completed', array('feedback'=>$feedback->id))) {
         $completion = new completion_info($course);
         foreach ($completeds as $completed) {
+            $DB->delete_records('feedback_completed', array('id' => $completed->id));
             // Update completion state
-            if ($completion->is_enabled($cm) && $feedback->completionsubmit) {
+            if ($completion->is_enabled($cm) && $cm->completion == COMPLETION_TRACKING_AUTOMATIC &&
+                    $feedback->completionsubmit) {
                 $completion->update_state($cm, COMPLETION_INCOMPLETE, $completed->userid);
             }
-            $DB->delete_records('feedback_completed', array('id'=>$completed->id));
         }
     }
 
@@ -2759,14 +2761,14 @@ function feedback_delete_completed($completed, $feedback = null, $cm = null, $co
     //first we delete all related values
     $DB->delete_records('feedback_value', array('completed' => $completed->id));
 
+    // Delete the completed record.
+    $return = $DB->delete_records('feedback_completed', array('id' => $completed->id));
+
     // Update completion state
     $completion = new completion_info($course);
-    if ($completion->is_enabled($cm) && $feedback->completionsubmit) {
+    if ($completion->is_enabled($cm) && $cm->completion == COMPLETION_TRACKING_AUTOMATIC && $feedback->completionsubmit) {
         $completion->update_state($cm, COMPLETION_INCOMPLETE, $completed->userid);
     }
-    // Last we delete the completed-record.
-    $return = $DB->delete_records('feedback_completed', array('id' => $completed->id));
-
     // Trigger event for the delete action we performed.
     $event = \mod_feedback\event\response_deleted::create_from_record($completed, $cm, $feedback);
     $event->trigger();
index 7763d6e..1e53c1c 100644 (file)
@@ -1814,6 +1814,10 @@ class mod_lesson_external extends external_api {
         }
 
         list($answerpages, $userstats) = lesson_get_user_detailed_report_data($lesson, $userid, $params['lessonattempt']);
+        // Convert page object to page record.
+        foreach ($answerpages as $answerp) {
+            $answerp->page = self::get_page_fields($answerp->page);
+        }
 
         $result = array(
             'answerpages' => $answerpages,
@@ -1835,6 +1839,7 @@ class mod_lesson_external extends external_api {
                 'answerpages' => new external_multiple_structure(
                     new external_single_structure(
                         array(
+                            'page' => self::get_page_structure(VALUE_OPTIONAL),
                             'title' => new external_value(PARAM_RAW, 'Page title.'),
                             'contents' => new external_value(PARAM_RAW, 'Page contents.'),
                             'qtype' => new external_value(PARAM_TEXT, 'Identifies the page type of this page.'),
index a331db6..bceb426 100644 (file)
@@ -42,16 +42,23 @@ $string['addaquestionpage'] = 'Add a question page';
 $string['addaquestionpagehere'] = 'Add a question page here';
 $string['addbranchtable'] = 'Add a content page';
 $string['addcluster'] = 'Add a cluster';
+$string['addessay'] = 'Create an Essay question page';
 $string['addedabranchtable'] = 'Added a content page';
 $string['addedanendofbranch'] = 'Added an end of branch';
 $string['addedaquestionpage'] = 'Added a question page';
 $string['addedcluster'] = 'Added a cluster';
 $string['addedendofcluster'] = 'Added an end of cluster';
+$string['addendofbranch'] = 'Add end of branch';
 $string['addendofcluster'] = 'Add an end of cluster';
+$string['addmatching'] = 'Create a Matching question page';
+$string['addmultichoice'] = 'Create a Multichoice question page';
 $string['addnewgroupoverride'] = 'Add group override';
 $string['addnewuseroverride'] = 'Add user override';
+$string['addnumerical'] = 'Create a Numerical question page';
 $string['additionalattemptsremaining'] = 'Completed, You can re-attempt this lesson';
 $string['addpage'] = 'Add a page';
+$string['addshortanswer'] = 'Create a Short answer question page';
+$string['addtruefalse'] = 'Create a True/false question page';
 $string['allowofflineattempts'] = 'Allow lesson to be attempted offline using the mobile app';
 $string['allowofflineattempts_help'] = 'If enabled, a mobile app user can download the lesson and attempt it offline.
 All the possible answers and correct responses will be downloaded as well.
@@ -162,11 +169,22 @@ $string['displayscorewithessays'] = '<p>You earned {$a->score} out of {$a->tempm
 $string['displayscorewithoutessays'] = 'Your score is {$a->score} (out of {$a->grade}).';
 $string['duplicatepagenamed'] = 'Duplicate page: {$a}';
 $string['edit'] = 'Edit';
+$string['editbranchtable'] = 'Editing a content page';
+$string['editcluster'] = 'Editing a cluster';
+$string['editendofcluster'] = 'Editing an end of cluster page';
+$string['editendofbranch'] = 'Editing an end of branch page';
+$string['editessay'] = 'Editing an Essay question page';
 $string['editingquestionpage'] = 'Editing {$a} question page';
 $string['editlessonsettings'] = 'Edit lesson settings';
+$string['editmatching'] = 'Editing a Matching question page';
+$string['editmultichoice'] = 'Editing a Multichoice question page';
+$string['editnumerical'] = 'Editing a Numerical question page';
 $string['editoverride'] = 'Edit override';
 $string['editpage'] = 'Edit page contents';
 $string['editpagecontent'] = 'Edit page contents';
+$string['editquestion'] = 'Editing a question page';
+$string['editshortanswer'] = 'Editing a Short answer question page';
+$string['edittruefalse'] = 'Editing a True/false question page';
 $string['email'] = 'Email';
 $string['emailallgradedessays'] = 'Email ALL graded essays';
 $string['emailgradedessays'] = 'Email graded essays';
index 8c32a2d..74f584a 100644 (file)
@@ -1078,6 +1078,8 @@ function lesson_get_user_detailed_report_data(lesson $lesson, $userid, $attempt)
     while ($pageid != 0) { // EOL
         $page = $lessonpages[$pageid];
         $answerpage = new stdClass;
+        // Keep the original page object.
+        $answerpage->page = $page;
         $data ='';
 
         $answerdata = new stdClass;
@@ -1245,7 +1247,13 @@ abstract class lesson_add_page_form_base extends moodleform {
         $mform = $this->_form;
         $editoroptions = $this->_customdata['editoroptions'];
 
-        $mform->addElement('header', 'qtypeheading', get_string('createaquestionpage', 'lesson', get_string($this->qtypestring, 'lesson')));
+        if ($this->qtypestring != 'selectaqtype') {
+            if ($this->_customdata['edit']) {
+                $mform->addElement('header', 'qtypeheading', get_string('edit'. $this->qtypestring, 'lesson'));
+            } else {
+                $mform->addElement('header', 'qtypeheading', get_string('add'. $this->qtypestring, 'lesson'));
+            }
+        }
 
         if (!empty($this->_customdata['returnto'])) {
             $mform->addElement('hidden', 'returnto', $this->_customdata['returnto']);
index c7e4342..23f9c58 100644 (file)
@@ -324,7 +324,11 @@ class lesson_add_page_form_branchtable extends lesson_add_page_form_base {
 
         $jumptooptions = lesson_page_type_branchtable::get_jumptooptions($firstpage, $lesson);
 
-        $mform->setDefault('qtypeheading', get_string('addabranchtable', 'lesson'));
+        if ($this->_customdata['edit']) {
+            $mform->setDefault('qtypeheading', get_string('editbranchtable', 'lesson'));
+        } else {
+            $mform->setDefault('qtypeheading', get_string('addabranchtable', 'lesson'));
+        }
 
         $mform->addElement('hidden', 'firstpage');
         $mform->setType('firstpage', PARAM_BOOL);
index 3131639..64c5be9 100644 (file)
@@ -1217,6 +1217,14 @@ class mod_lesson_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals(1, $result['userstats']['gradeinfo']['total']);     // Total correct answers.
         $this->assertEquals(100, $result['userstats']['gradeinfo']['grade']);   // Correct answer.
 
+        // Check page object contains the lesson pages answered.
+        $pagesanswered = array();
+        foreach ($result['answerpages'] as $answerp) {
+            $pagesanswered[] = $answerp['page']['id'];
+        }
+        sort($pagesanswered);
+        $this->assertEquals(array($this->page1->id, $this->page2->id), $pagesanswered);
+
         // Test second attempt unfinished.
         $result = mod_lesson_external::get_user_attempt($this->lesson->id, $this->student->id, 1);
         $result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_returns(), $result);
index 79cc01a..9372654 100644 (file)
@@ -1,5 +1,9 @@
 This files describes API changes in the lesson code.
 
+=== 3.4 ===
+
+* External function mod_lesson_external::get_user_attempt() now returns the full page object inside each answerpages.
+
 === 3.3 ===
 
 * lesson::callback_on_view() has an additional optional parameter $redirect default to true.
index 02d24ad..c90397a 100644 (file)
@@ -49,7 +49,7 @@ $functions = array(
         'classname'     => 'mod_lti_external',
         'methodname'    => 'view_lti',
         'description'   => 'Trigger the course module viewed event and update the module completion status.',
-        'type'          => 'write',
+        'type'          => 'read',
         'capabilities'  => 'mod/lti:view',
         'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
index c4a7c0f..d4bfce3 100644 (file)
@@ -1633,6 +1633,11 @@ function mod_scorm_core_calendar_provide_event_action(calendar_event $event,
 
     $cm = get_fast_modinfo($event->courseid)->instances['scorm'][$event->instance];
 
+    if (has_capability('mod/scorm:viewreport', $cm->context)) {
+        // Teachers do not need to be reminded to complete a scorm.
+        return null;
+    }
+
     if (!empty($cm->customdata['timeclose']) && $cm->customdata['timeclose'] < time()) {
         // The scorm has closed so the user can no longer submit anything.
         return null;
index cd33822..0fcc38e 100644 (file)
@@ -207,6 +207,9 @@ class mod_scorm_lib_testcase extends externallib_advanced_testcase {
         // Create a calendar event.
         $event = $this->create_action_event($course->id, $scorm->id, SCORM_EVENT_TYPE_OPEN);
 
+        // Only students see scorm events.
+        $this->setUser($this->student);
+
         // Create an action factory.
         $factory = new \core_calendar\action_factory();
 
@@ -261,6 +264,9 @@ class mod_scorm_lib_testcase extends externallib_advanced_testcase {
         // Create a calendar event.
         $event = $this->create_action_event($course->id, $scorm->id, SCORM_EVENT_TYPE_OPEN);
 
+        // Only students see scorm events.
+        $this->setUser($this->student);
+
         // Create an action factory.
         $factory = new \core_calendar\action_factory();
 
@@ -289,6 +295,9 @@ class mod_scorm_lib_testcase extends externallib_advanced_testcase {
         // Create a calendar event.
         $event = $this->create_action_event($course->id, $scorm->id, SCORM_EVENT_TYPE_OPEN);
 
+        // Only students see scorm events.
+        $this->setUser($this->student);
+
         // Create an action factory.
         $factory = new \core_calendar\action_factory();
 
diff --git a/pix/i/mahara_host.gif b/pix/i/mahara_host.gif
deleted file mode 100644 (file)
index 6b42253..0000000
Binary files a/pix/i/mahara_host.gif and /dev/null differ
diff --git a/pix/i/mahara_host.png b/pix/i/mahara_host.png
new file mode 100644 (file)
index 0000000..c2e655d
Binary files /dev/null and b/pix/i/mahara_host.png differ
diff --git a/pix/i/mahara_host.svg b/pix/i/mahara_host.svg
new file mode 100644 (file)
index 0000000..c5cc65e
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->\r
+<svg version="1.1" id="full_color" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="16" width="16" x="0px" y="0px"\r
+        viewBox="0 0 65.2 124.7" style="enable-background:new 0 0 65.2 124.7;" xml:space="preserve" preserveAspectRatio="xMinYMid meet">\r
+<style type="text/css">\r
+       .st0{fill:#566D31;}\r
+</style>\r
+<path class="st0" d="M63.2,92c0-17.3-14.7-36.7-16-35.8c-3.5,2.2,10.1,13.4,10.1,34.3c0,11.7-6.9,23.2-22.1,23.2\r
+       c-14.7,0-24.9-9.4-24.9-23.6c0-13.5,9.3-26.7,14.9-33.5c2.2,3.2,7.4,11.2,8.5,13c1.4,2.3,3.3,7.3-0.2,7.8c-5.1,0.8-5.3-7.3-5.5-9.7\r
+       c-0.1-0.8-1.3-0.9-1.7-0.2c-1.6,2.9-2.8,11.6,2,15.7c3.4,2.9,9.4,3,11.8-3.6c3.8,6.6,4.8,12.7,5.4,16.8c0.1,0.7,1.1,0.7,1.2,0\r
+       c2.3-13.5-4-22.9-6.1-27.1c-2.1-4.2-10.8-16.5-12.3-19c-1.5-2.5-5.6-9.7-5.6-17.4c0-11.7,9.6-22.9,11.4-22.9c1.8,0,12,9.3,12,23.3\r
+       c0,5.3-1.2,9.2-2.6,12c-0.5,0.9-1.7,1-2.3,0.1c-5.6-8.7-5.6-10.5-2.1-13c2.3-1.7-9-6.4-8.6,3.3c0.2,5.1,7.4,15.5,9.2,17.5\r
+       s12-2.2,12-20.7c0-18.5-15.9-30.8-17.9-30.8C31.8,1.7,16,16.7,16,32.5c0,7.8,2.8,14.5,6.1,19.7C21.7,52.5,1.8,68.3,2,92\r
+       c0.2,18.1,12.3,31.1,32.1,31.1C52,123.1,63.2,111.3,63.2,92z"/>\r
+</svg>\r
diff --git a/question/type/multichoice/classes/admin_setting_answernumbering.php b/question/type/multichoice/classes/admin_setting_answernumbering.php
new file mode 100644 (file)
index 0000000..80de03a
--- /dev/null
@@ -0,0 +1,57 @@
+<?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/>.
+
+/**
+ * Admin settings for the multichoice question type.
+ *
+ * @package   qtype_multichoice
+ * @copyright  2015 onwards Nadav Kavalerchik
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Admin settings class for the multichoice question type method.
+ *
+ * Just so we can lazy-load the numbering style choices.
+ *
+ * @copyright  2015 onwards Nadav Kavalerchik
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_multichoice_admin_setting_answernumbering extends admin_setting_configselect {
+
+    /**
+     * This function may be used in ancestors for lazy loading of choices
+     *
+     * Override this method if loading of choices is expensive, such
+     * as when it requires multiple db requests.
+     *
+     * @return bool true if loaded, false if error
+     */
+    public function load_choices() {
+        global $CFG;
+
+        if (is_array($this->choices)) {
+            return true;
+        }
+
+        require_once($CFG->dirroot . '/question/type/multichoice/questiontype.php');
+        $this->choices = qtype_multichoice::get_numbering_styles();
+
+        return true;
+    }
+}
index b0dc614..372dfca 100644 (file)
@@ -46,17 +46,17 @@ class qtype_multichoice_edit_form extends question_edit_form {
         );
         $mform->addElement('select', 'single',
                 get_string('answerhowmany', 'qtype_multichoice'), $menu);
-        $mform->setDefault('single', 1);
+        $mform->setDefault('single', get_config('qtype_multichoice', 'answerhowmany'));
 
         $mform->addElement('advcheckbox', 'shuffleanswers',
                 get_string('shuffleanswers', 'qtype_multichoice'), null, null, array(0, 1));
         $mform->addHelpButton('shuffleanswers', 'shuffleanswers', 'qtype_multichoice');
-        $mform->setDefault('shuffleanswers', 1);
+        $mform->setDefault('shuffleanswers', get_config('qtype_multichoice', 'shuffleanswers'));
 
         $mform->addElement('select', 'answernumbering',
                 get_string('answernumbering', 'qtype_multichoice'),
                 qtype_multichoice::get_numbering_styles());
-        $mform->setDefault('answernumbering', 'abc');
+        $mform->setDefault('answernumbering', get_config('qtype_multichoice', 'answernumbering'));
 
         $this->add_per_answer_fields($mform, get_string('choiceno', 'qtype_multichoice', '{no}'),
                 question_bank::fraction_options_full(), max(5, QUESTION_NUMANS_START));
index 7305552..18aacbd 100644 (file)
@@ -24,6 +24,7 @@
  */
 
 $string['answerhowmany'] = 'One or multiple answers?';
+$string['answerhowmany_desc'] = 'Should the default for new multichoice questions be to require single or multiple answers?';
 $string['answernumbering'] = 'Number the choices?';
 $string['answernumbering123'] = '1., 2., 3., ...';
 $string['answernumberingabc'] = 'a., b., c., ...';
@@ -31,6 +32,7 @@ $string['answernumberingABCD'] = 'A., B., C., ...';
 $string['answernumberingiii'] = 'i., ii., iii., ...';
 $string['answernumberingIIII'] = 'I., II., III., ...';
 $string['answernumberingnone'] = 'No numbering';
+$string['answernumbering_desc'] = 'Set the default numbering style for new multichoice questions.';
 $string['answersingleno'] = 'Multiple answers allowed';
 $string['answersingleyes'] = 'One answer only';
 $string['choiceno'] = 'Choice {$a}';
@@ -65,6 +67,7 @@ $string['pluginnamesummary'] = 'Allows the selection of a single or multiple res
 $string['selectmulti'] = 'Select one or more:';
 $string['selectone'] = 'Select one:';
 $string['shuffleanswers'] = 'Shuffle the choices?';
+$string['shuffleanswers_desc'] = 'Should the default for new nultichoice questions be to shuffle answers?';
 $string['shuffleanswers_help'] = 'If enabled, the order of the answers is randomly shuffled for each attempt, provided that "Shuffle within questions" in the activity settings is also enabled.';
 $string['singleanswer'] = 'Choose one answer.';
 $string['toomanyselected'] = 'You have selected too many options.';
index f333c1d..edc54a4 100644 (file)
@@ -93,11 +93,11 @@ abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedb
             }
             $radiobuttons[] = $hidden . html_writer::empty_tag('input', $inputattributes) .
                     html_writer::tag('label',
-                        $this->number_in_style($value, $question->answernumbering) .
+                        html_writer::span($this->number_in_style($value, $question->answernumbering), 'answernumber') .
                         $question->make_html_inline($question->format_text(
                                 $ans->answer, $ans->answerformat,
                                 $qa, 'question', 'answer', $ansid)),
-                    array('for' => $inputattributes['id'], 'class' => 'm-l-1'));
+                        array('for' => $inputattributes['id'], 'class' => 'm-l-1'));
 
             // Param $options->suppresschoicefeedback is a hack specific to the
             // oumultiresponse question type. It would be good to refactor to
diff --git a/question/type/multichoice/settings.php b/question/type/multichoice/settings.php
new file mode 100644 (file)
index 0000000..83697b3
--- /dev/null
@@ -0,0 +1,44 @@
+<?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/>.
+
+/**
+ * Admin settings for the multichoice question type.
+ *
+ * @package   qtype_multichoice
+ * @copyright  2015 onwards Nadav Kavalerchik
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if ($ADMIN->fulltree) {
+    $menu = array(
+        new lang_string('answersingleno', 'qtype_multichoice'),
+        new lang_string('answersingleyes', 'qtype_multichoice'),
+    );
+    $settings->add(new admin_setting_configselect('qtype_multichoice/answerhowmany',
+    new lang_string('answerhowmany', 'qtype_multichoice'),
+    new lang_string('answerhowmany_desc', 'qtype_multichoice'), '1', $menu));
+
+    $settings->add(new admin_setting_configcheckbox('qtype_multichoice/shuffleanswers',
+    new lang_string('shuffleanswers', 'qtype_multichoice'),
+    new lang_string('shuffleanswers_desc', 'qtype_multichoice'), '1'));
+
+    $settings->add(new qtype_multichoice_admin_setting_answernumbering('qtype_multichoice/answernumbering',
+    new lang_string('answernumbering', 'qtype_multichoice'),
+    new lang_string('answernumbering_desc', 'qtype_multichoice'), 'abc', null ));
+
+}
index 7403e67..75b91b6 100644 (file)
@@ -264,7 +264,8 @@ function report_stats_report($course, $report, $mode, $user, $roleid, $time) {
                 // bad luck, we can not link other report
             } else if (empty($param->crosstab)) {
                 foreach  ($stats as $stat) {
-                    $a = array(userdate($stat->timeend-(60*60*24),get_string('strftimedate'),$CFG->timezone),$stat->line1);
+                    $a = array(userdate($stat->timeend - DAYSECS, get_string('strftimedate'), $CFG->timezone),
+                            $stat->line1);
                     if (isset($stat->line2)) {
                         $a[] = $stat->line2;
                     }
@@ -302,7 +303,8 @@ function report_stats_report($course, $report, $mode, $user, $roleid, $time) {
                         }
                     }
                     if (!array_key_exists($stat->timeend,$times)) {
-                        $times[$stat->timeend] = userdate($stat->timeend,get_string('strftimedate'),$CFG->timezone);
+                        $times[$stat->timeend] = userdate($stat->timeend - DAYSECS, get_string('strftimedate'),
+                                $CFG->timezone);
                     }
                 }
 
@@ -395,7 +397,7 @@ function report_stats_print_chart($courseid, $report, $time, $mode, $userid = 0,
         foreach ($stats as $stat) {
             // Build the array of formatted times indexed by timestamp used as labels.
             if (!array_key_exists($stat->timeend, $times)) {
-                $times[$stat->timeend] = userdate($stat->timeend, get_string('strftimedate'), $CFG->timezone);
+                $times[$stat->timeend] = userdate($stat->timeend - DAYSECS, get_string('strftimedate'), $CFG->timezone);
 
                 // Just add the data if the time hasn't been added yet.
                 // The number of lines of data must match the number of labels.
@@ -436,7 +438,7 @@ function report_stats_print_chart($courseid, $report, $time, $mode, $userid = 0,
 
             // Build the array of formatted times indexed by timestamp&nbs