Merge branch 'MDL-58898-master' of https://github.com/xow/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Fri, 12 May 2017 04:32:23 +0000 (12:32 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Fri, 12 May 2017 04:32:23 +0000 (12:32 +0800)
51 files changed:
admin/index.php
admin/tool/templatelibrary/classes/api.php
auth/classes/output/login.php
auth/oauth2/classes/api.php
auth/oauth2/classes/linked_login.php
auth/oauth2/confirm-account.php
auth/oauth2/confirm-linkedlogin.php
auth/oauth2/db/events.php [new file with mode: 0644]
auth/oauth2/lang/en/auth_oauth2.php
auth/oauth2/linkedlogins.php
auth/oauth2/version.php
backup/moodle2/restore_stepslib.php
calendar/classes/external/event_action_exporter.php
calendar/classes/external/event_exporter.php
calendar/classes/external/event_icon_exporter.php
calendar/classes/local/event/container.php
course/amd/build/actions.min.js
course/amd/src/actions.js
course/changenumsections.php
course/editsection.php
course/format/renderer.php
course/format/topics/lang/en/format_topics.php
course/format/topics/tests/behat/edit_delete_sections.feature
course/format/weeks/lang/en/format_weeks.php
course/format/weeks/tests/behat/edit_delete_sections.feature
lang/en/backup.php
lang/en/completion.php
lang/en/moodle.php
lib/amd/build/modal_save_cancel.min.js
lib/amd/src/modal_save_cancel.js
lib/classes/output/icon_system_fontawesome.php
lib/classes/task/refresh_mod_calendar_events_task.php
lib/db/upgrade.php
lib/setuplib.php
lib/templates/columns-1to1to1.mustache
lib/templates/columns-1to2.mustache
lib/templates/columns-2to1.mustache
login/signup.php
mod/assign/db/upgrade.php
mod/assign/gradingtable.php
mod/assign/lib.php
mod/lesson/lib.php
mod/lti/classes/service_exception_handler.php
mod/quiz/lib.php
mod/scorm/lang/en/scorm.php
repository/onedrive/lang/en/repository_onedrive.php
theme/boost/templates/core/columns-1to1to1.mustache [new file with mode: 0644]
theme/boost/templates/core/columns-1to2.mustache [new file with mode: 0644]
theme/boost/templates/core/columns-2to1.mustache [new file with mode: 0644]
version.php
webservice/xmlrpc/locallib.php

index 30ba5cb..6cf7f45 100644 (file)
@@ -101,6 +101,12 @@ if (function_exists('opcache_invalidate')) {
 // indirectly calls the protected init() method is good here.
 core_component::get_core_subsystems();
 
+if (is_major_upgrade_required() && isloggedin()) {
+    // A major upgrade is required.
+    // Terminate the session and redirect back here before anything DB-related happens.
+    redirect_if_major_upgrade_required();
+}
+
 require_once($CFG->libdir.'/adminlib.php');    // various admin-only functions
 require_once($CFG->libdir.'/upgradelib.php');  // general upgrade/install related functions
 
index b760007..7ae2b0e 100644 (file)
@@ -70,6 +70,9 @@ class api {
             // Look at all the templates dirs for subsystems.
             $subsystems = core_component::get_core_subsystems();
             foreach ($subsystems as $subsystem => $dir) {
+                if (empty($dir)) {
+                    continue;
+                }
                 $dir .= '/templates';
                 if (is_dir($dir)) {
                     $dirs = mustache_template_finder::get_template_directories_for_component('core_' . $subsystem, $themename);
index fcb42b7..8bdf91d 100644 (file)
@@ -128,7 +128,7 @@ class login implements renderable, templatable {
         $data->error = $this->error;
         $data->forgotpasswordurl = $this->forgotpasswordurl->out(false);
         $data->hasidentityproviders = !empty($this->identityproviders);
-        $data->hasinstructions = !empty($this->instructions);
+        $data->hasinstructions = !empty($this->instructions) || $this->cansignup;
         $data->identityproviders = $identityproviders;
         list($data->instructions, $data->instructionsformat) = external_format_text($this->instructions, FORMAT_MOODLE,
             context_system::instance()->id);
index 2c36d7d..d13a083 100644 (file)
@@ -115,6 +115,10 @@ class api {
             $userid = $USER->id;
         }
 
+        if (linked_login::has_existing_issuer_match($issuer, $userinfo['username'])) {
+            throw new moodle_exception('alreadylinked', 'auth_oauth2');
+        }
+
         if (\core\session\manager::is_loggedinas()) {
             throw new moodle_exception('notwhileloggedinas', 'auth_oauth2');
         }
@@ -154,9 +158,8 @@ class api {
         $record->issuerid = $issuer->get('id');
         $record->username = $userinfo['username'];
         $record->userid = $userid;
-        $existing = linked_login::get_record((array)$record);
-        if ($existing) {
-            return false;
+        if (linked_login::has_existing_issuer_match($issuer, $userinfo['username'])) {
+            throw new moodle_exception('alreadylinked', 'auth_oauth2');
         }
         $record->email = $userinfo['email'];
         $record->confirmtoken = random_string(32);
@@ -249,6 +252,10 @@ class api {
         require_once($CFG->dirroot.'/user/profile/lib.php');
         require_once($CFG->dirroot.'/user/lib.php');
 
+        if (linked_login::has_existing_issuer_match($issuer, $userinfo['username'])) {
+            throw new moodle_exception('alreadylinked', 'auth_oauth2');
+        }
+
         $user = new stdClass();
         $user->username = $userinfo['username'];
         $user->email = $userinfo['email'];
@@ -329,4 +336,18 @@ class api {
 
         $login->delete();
     }
+
+    /**
+     * Delete linked logins for a user.
+     *
+     * @param \core\event\user_deleted $event
+     * @return boolean
+     */
+    public static function user_deleted(\core\event\user_deleted $event) {
+        global $DB;
+
+        $userid = $event->objectid;
+
+        return $DB->delete_records(linked_login::TABLE, ['userid' => $userid]);
+    }
 }
index b49392c..96be2ae 100644 (file)
@@ -65,6 +65,29 @@ class linked_login extends persistent {
         );
     }
 
+    /**
+     * Check whether there are any valid linked accounts for this issuer
+     * and username combination.
+     *
+     * @param \core\oauth2\issuer $issuer The issuer
+     * @param string $username The username to check
+     */
+    public static function has_existing_issuer_match(\core\oauth2\issuer $issuer, $username) {
+        global $DB;
+
+        $where = "issuerid = :issuerid
+              AND username = :username
+              AND (confirmtokenexpires = 0 OR confirmtokenexpires > :maxexpiry)";
+
+        $count = $DB->count_records_select(static::TABLE, $where, [
+            'issuerid' => $issuer->get('id'),
+            'username' => $username,
+            'maxexpiry' => (new \DateTime('NOW'))->getTimestamp(),
+        ]);
+
+        return $count > 0;
+    }
+
     /**
      * Remove all linked logins that are using issuers that have been deleted.
      *
index 54e3383..03e1b76 100644 (file)
@@ -85,7 +85,7 @@ if ($confirmed == AUTH_CONFIRM_ALREADY) {
     echo $OUTPUT->footer();
     exit;
 } else {
-    print_error('invalidconfirmdata');
+    \core\notification::error(get_string('confirmationinvalid', 'auth_oauth2'));
 }
 
 redirect("$CFG->wwwroot/");
index 26182af..1d0b78e 100644 (file)
@@ -72,7 +72,7 @@ if ($confirmed) {
     echo $OUTPUT->footer();
     exit;
 } else {
-    print_error('invalidconfirmdata');
+    \core\notification::error(get_string('confirmationinvalid', 'auth_oauth2'));
 }
 
 redirect("$CFG->wwwroot/");
diff --git a/auth/oauth2/db/events.php b/auth/oauth2/db/events.php
new file mode 100644 (file)
index 0000000..b6f793c
--- /dev/null
@@ -0,0 +1,31 @@
+<?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/>.
+
+/**
+ * This file definies observers needed by the plugin.
+ *
+ * @package    auth_oauth2
+ * @copyright  2017 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// List of observers.
+$observers = [
+    [
+        'eventname'   => '\core\event\user_deleted',
+        'callback'    => '\auth_oauth2\api::user_deleted',
+    ],
+];
index f0715e0..c8ea396 100644 (file)
@@ -42,6 +42,7 @@ line at the top of your web browser window.
 If you need help, please contact the site administrator,
 {$a->admin}';
 $string['confirmaccountemailsubject'] = '{$a}: account confirmation';
+$string['confirmationinvalid'] = 'The confirmation link is either invalid, or has expired. Please start the login process again to generate a new confirmation email.';
 $string['confirmationpending'] = 'This account is pending email confirmation.';
 $string['confirmlinkedloginemail'] = 'Hi {$a->fullname},
 
@@ -77,9 +78,10 @@ $string['loginerror_userincomplete'] = 'The user information returned did not co
 $string['loginerror_nouserinfo'] = 'No user information was returned. The OAuth 2 service may be configured incorrectly.';
 $string['loginerror_invaliddomain'] = 'The email address is not allowed at this site.';
 $string['loginerror_authenticationfailed'] = 'The authentication process failed.';
-$string['loginerror_cannotcreateaccounts'] = 'The account does not exist and this site does not allow self-registration.';
+$string['loginerror_cannotcreateaccounts'] = 'An account with your email address could not be found.';
 $string['notloggedindebug'] = 'The login attempt failed. Reason: {$a}';
 $string['notwhileloggedinas'] = 'Linked logins cannot be managed while logged in as another user.';
 $string['oauth2:managelinkedlogins'] = 'Manage own linked login accounts';
 $string['plugindescription'] = 'This authentication plugin displays a list of the configured identity providers on the login page. Selecting an identity provider allows users to login with their credentials from an OAuth 2 provider.';
 $string['pluginname'] = 'OAuth 2';
+$string['alreadylinked'] = 'This external account is already linked to an account on this site';
index d8f68f1..ff9a89f 100644 (file)
@@ -58,8 +58,12 @@ if ($action == 'new') {
     $userinfo = $client->get_userinfo();
 
     if (!empty($userinfo)) {
-        \auth_oauth2\api::link_login($userinfo, $issuer);
-        redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
+        try {
+            \auth_oauth2\api::link_login($userinfo, $issuer);
+            redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
+        } catch (Exception $e) {
+            redirect($PAGE->url, $e->getMessage(), null, \core\output\notification::NOTIFY_ERROR);
+        }
     } else {
         redirect($PAGE->url, get_string('notloggedin', 'auth_oauth2'), null, \core\output\notification::NOTIFY_ERROR);
     }
index 8fc34eb..2758934 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2017051500;        // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2017051501;        // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2017050500;        // Requires this Moodle version.
 $plugin->component = 'auth_oauth2';       // Full name of the plugin (used for diagnostics).
index b23fda4..0a715ad 100644 (file)
@@ -2705,11 +2705,11 @@ class restore_calendarevents_structure_step extends restore_structure_step {
                 'userid'         => $data->userid,
                 'repeatid'       => $this->get_mappingid('event', $data->repeatid),
                 'modulename'     => $data->modulename,
-                'type'           => $data->type,
+                'type'           => isset($data->type) ? $data->type : 0,
                 'eventtype'      => $data->eventtype,
                 'timestart'      => $this->apply_date_offset($data->timestart),
                 'timeduration'   => $data->timeduration,
-                'timesort'       => $this->apply_date_offset($data->timesort),
+                'timesort'       => isset($data->timesort) ? $this->apply_date_offset($data->timesort) : null,
                 'visible'        => $data->visible,
                 'uuid'           => $data->uuid,
                 'sequence'       => $data->sequence,
index 2002cc3..097adbc 100644 (file)
@@ -90,6 +90,10 @@ class event_action_exporter extends exporter {
     protected function get_other_values(renderer_base $output) {
         $event = $this->related['event'];
 
+        if (!$event->get_course_module()) {
+            // TODO MDL-58866 Only activity modules currently support this callback.
+            return ['showitemcount' => false];
+        }
         $modulename = $event->get_course_module()->get('modname');
         $component = 'mod_' . $modulename;
         $showitemcountcallback = 'core_calendar_event_action_shows_item_count';
index ade78fa..baa73f6 100644 (file)
@@ -178,10 +178,17 @@ class event_exporter extends exporter {
         $values = [];
         $event = $this->event;
         $context = $this->related['context'];
-        $modulename = $event->get_course_module()->get('modname');
-        $moduleid = $event->get_course_module()->get('id');
+        if ($moduleproxy = $event->get_course_module()) {
+            $modulename = $moduleproxy->get('modname');
+            $moduleid = $moduleproxy->get('id');
+            $url = new \moodle_url(sprintf('/mod/%s/view.php', $modulename), ['id' => $moduleid]);
+        } else {
+            // TODO MDL-58866 We do not have any way to find urls for events outside of course modules.
+            global $CFG;
+            require_once($CFG->dirroot.'/course/lib.php');
+            $url = \course_get_url($this->related['course'] ?: SITEID);
+        }
         $timesort = $event->get_times()->get_sort_time()->getTimestamp();
-        $url = new \moodle_url(sprintf('/mod/%s/view.php', $modulename), ['id' => $moduleid]);
         $iconexporter = new event_icon_exporter($event, ['context' => $context]);
 
         $values['url'] = $url->out(false);
index 2895a40..5f4bd4d 100644 (file)
@@ -54,7 +54,7 @@ class event_icon_exporter extends exporter {
         $userid = $user ? $user->get('id') : null;
         $isactivityevent = !empty($coursemodule);
         $isglobalevent = ($course && $courseid == SITEID);
-        $iscourseevent = ($course && !empty($courseid) && $courseid != SITEID && $group && empty($groupid));
+        $iscourseevent = ($course && !empty($courseid) && $courseid != SITEID && empty($groupid));
         $isgroupevent = ($group && !empty($groupid));
         $isuserevent = ($user && !empty($userid));
 
index 083b19c..20b39ec 100644 (file)
@@ -132,7 +132,7 @@ class container {
                 $getcallback('action'),
                 $getcallback('visibility'),
                 function ($dbrow) {
-                    // At present we only handle callbacks in course modules.
+                    // At present we only have a bail-out check for events in course modules.
                     if (empty($dbrow->modulename)) {
                         return false;
                     }
@@ -246,14 +246,19 @@ class container {
                     // Callbacks will get supplied a "legacy" version
                     // of the event class.
                     $mapper = self::$eventmapper;
-                    $action = component_callback(
-                        'mod_' . $event->get_course_module()->get('modname'),
-                        'core_calendar_provide_event_action',
-                        [
-                            $mapper->from_event_to_legacy_event($event),
-                            self::$actionfactory
-                        ]
-                    );
+                    $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.
@@ -266,13 +271,17 @@ class container {
                 // This is enforced by the event_factory.
                 'visibility' => function (event_interface $event) {
                     $mapper = self::$eventmapper;
-                    $eventvisible = component_callback(
-                        'mod_' . $event->get_course_module()->get('modname'),
-                        'core_calendar_is_event_visible',
-                        [
-                            $mapper->from_event_to_legacy_event($event)
-                        ]
-                    );
+                    $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)
+                            ]
+                        );
+                    }
 
                     // Do not display the event if there is nothing to action.
                     if ($event instanceof action_event_interface && $event->get_action()->get_item_count() === 0) {
index d8e4d3b..ce8c49c 100644 (file)
Binary files a/course/amd/build/actions.min.js and b/course/amd/build/actions.min.js differ
index da81240..a804b45 100644 (file)
@@ -22,8 +22,9 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @since      3.3
  */
-define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/url', 'core/yui'],
-    function($, ajax, templates, notification, str, url, Y) {
+define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/url', 'core/yui',
+        'core/modal_factory', 'core/modal_events', 'core/key_codes'],
+    function($, ajax, templates, notification, str, url, Y, ModalFactory, ModalEvents, KeyCodes) {
         var CSS = {
             EDITINPROGRESS: 'editinprogress',
             SECTIONDRAGGABLE: 'sectiondraggable',
@@ -36,7 +37,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
             MENU: '.moodle-actionmenu[data-enhance=moodle-core-actionmenu]',
             TOGGLE: '.toggle-display,.dropdown-toggle',
             SECTIONLI: 'li.section',
-            SECTIONACTIONMENU: '.section_action_menu'
+            SECTIONACTIONMENU: '.section_action_menu',
+            ADDSECTIONS: '#changenumsections [data-add-sections]'
         };
 
         Y.use('moodle-course-coursebase', function() {
@@ -576,6 +578,44 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
                         editSection(sectionElement, sectionId, actionItem, courseformat);
                     }
                 });
+
+                // Add a handler for "Add sections" link to ask for a number of sections to add.
+                str.get_string('numberweeks').done(function(strNumberSections) {
+                    var trigger = $(SELECTOR.ADDSECTIONS),
+                        modalTitle = trigger.attr('data-add-sections');
+                    var modalBody = $('<div><label for="add_section_numsections"></label> ' +
+                        '<input id="add_section_numsections" type="number" min="1" value="1"></div>');
+                    modalBody.find('label').html(strNumberSections);
+                    ModalFactory.create({
+                        title: modalTitle,
+                        type: ModalFactory.types.SAVE_CANCEL,
+                        body: modalBody.html()
+                    }, trigger)
+                    .done(function(modal) {
+                        var numSections = $(modal.getBody()).find('#add_section_numsections'),
+                        addSections = function() {
+                            // Check if value of the "Number of sections" is a valid positive integer and redirect
+                            // to adding a section script.
+                            if ('' + parseInt(numSections.val()) === numSections.val() && parseInt(numSections.val()) >= 1) {
+                                document.location = trigger.attr('href') + '&numsections=' + parseInt(numSections.val());
+                            }
+                        };
+                        modal.setSaveButtonText(modalTitle);
+                        modal.getRoot().on(ModalEvents.shown, function() {
+                            // When modal is shown focus and select the input and add a listener to keypress of "Enter".
+                            numSections.focus().select().on('keydown', function(e) {
+                                if (e.keyCode === KeyCodes.enter) {
+                                    addSections();
+                                }
+                            });
+                        });
+                        modal.getRoot().on(ModalEvents.save, function(e) {
+                            // When modal "Add" button is pressed.
+                            e.preventDefault();
+                            addSections();
+                        });
+                    });
+                });
             },
 
             /**
index a7b9495..c3d2a55 100644 (file)
@@ -31,6 +31,7 @@ require_once($CFG->dirroot.'/course/lib.php');
 $courseid = required_param('courseid', PARAM_INT);
 $increase = optional_param('increase', null, PARAM_BOOL);
 $insertsection = optional_param('insertsection', null, PARAM_INT); // Insert section at position; 0 means at the end.
+$numsections = optional_param('numsections', 1, PARAM_INT);        // Number of sections to insert.
 $returnurl = optional_param('returnurl', null, PARAM_LOCALURL);    // Where to return to after the action.
 $sectionreturn = optional_param('sectionreturn', null, PARAM_INT); // Section to return to, ignored if $returnurl is specified.
 
@@ -70,9 +71,12 @@ if (isset($courseformatoptions['numsections']) && $increase !== null) {
         // Inserting sections at any position except in the very end requires capability to move sections.
         require_capability('moodle/course:movesections', context_course::instance($course->id));
     }
-    $section = course_create_section($course, $insertsection);
+    $sections = [];
+    for ($i = 0; $i < max($numsections, 1); $i ++) {
+        $sections[] = course_create_section($course, $insertsection);
+    }
     if (!$returnurl) {
-        $returnurl = course_get_url($course, $section->section,
+        $returnurl = course_get_url($course, $sections[0]->section,
             ($sectionreturn !== null) ? ['sr' => $sectionreturn] : []);
     }
 }
index 231935f..b04440b 100644 (file)
@@ -49,9 +49,14 @@ if ($deletesection) {
     $cancelurl = course_get_url($course, $sectioninfo, array('sr' => $sectionreturn));
     if (course_can_delete_section($course, $sectioninfo)) {
         $confirm = optional_param('confirm', false, PARAM_BOOL) && confirm_sesskey();
+        if (!$confirm && optional_param('sesskey', null, PARAM_RAW) !== null &&
+                empty($sectioninfo->summary) && empty($sectioninfo->sequence) && confirm_sesskey()) {
+            // Do not ask for confirmation if section is empty and sesskey is already provided.
+            $confirm = true;
+        }
         if ($confirm) {
             course_delete_section($course, $sectioninfo, true, true);
-            $courseurl = course_get_url($course, 0, array('sr' => $sectionreturn));
+            $courseurl = course_get_url($course, $sectioninfo->section - 1, array('sr' => $sectionreturn));
             redirect($courseurl);
         } else {
             if (get_string_manager()->string_exists('deletesection', 'format_' . $course->format)) {
index eeb126f..f16bdd6 100644 (file)
@@ -394,7 +394,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 $url = new moodle_url('/course/editsection.php', array(
                     'id' => $section->id,
                     'sr' => $sectionreturn,
-                    'delete' => 1));
+                    'delete' => 1,
+                    'sesskey' => sesskey()));
                 $controls['delete'] = array(
                     'url' => $url,
                     'icon' => 'i/delete',
@@ -991,18 +992,19 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             // capabilities 'moodle/course:update' and 'moodle/course:movesections'.
 
             echo html_writer::start_tag('div', array('id' => 'changenumsections', 'class' => 'mdl-right'));
-            if (get_string_manager()->string_exists('addsection', 'format_'.$course->format)) {
-                $straddsection = get_string('addsection', 'format_'.$course->format);
+            if (get_string_manager()->string_exists('addsections', 'format_'.$course->format)) {
+                $straddsections = get_string('addsections', 'format_'.$course->format);
             } else {
-                $straddsection = get_string('addsection');
+                $straddsections = get_string('addsections');
             }
             $url = new moodle_url('/course/changenumsections.php',
                 ['courseid' => $course->id, 'insertsection' => 0, 'sesskey' => sesskey()]);
             if ($sectionreturn !== null) {
                 $url->param('sectionreturn', $sectionreturn);
             }
-            $icon = $this->output->pix_icon('t/add', $straddsection);
-            echo html_writer::link($url, $icon . $straddsection, array('class' => 'add-section'));
+            $icon = $this->output->pix_icon('t/add', $straddsections);
+            echo html_writer::link($url, $icon . $straddsections,
+                array('class' => 'add-sections', 'data-add-sections' => $straddsections));
             echo html_writer::end_tag('div');
         }
     }
index 3899184..d9d9a15 100644 (file)
@@ -23,7 +23,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['addsection'] = 'Add topic';
+$string['addsections'] = 'Add topics';
 $string['currentsection'] = 'This topic';
 $string['editsection'] = 'Edit topic';
 $string['editsectionname'] = 'Edit topic name';
index 87ca326..bdb48f3 100644 (file)
@@ -79,3 +79,18 @@ Feature: Sections can be edited and deleted in topics format
     And I should not see "Test chat name"
     And I should see "Test choice name" in the "li#section-4" "css_element"
     And I should see "Topic 4"
+
+  @javascript
+  Scenario: Adding sections in topics format
+    When I follow "Add topics"
+    Then the field "Number of sections" matches value "1"
+    And I press "Add topics"
+    And I should see "Topic 6" in the "li#section-6" "css_element"
+    And "li#section-7" "css_element" should not exist
+    And I follow "Add topics"
+    And I set the field "Number of sections" to "3"
+    And I press "Add topics"
+    And I should see "Topic 7" in the "li#section-7" "css_element"
+    And I should see "Topic 8" in the "li#section-8" "css_element"
+    And I should see "Topic 9" in the "li#section-9" "css_element"
+    And "li#section-10" "css_element" should not exist
index 7a1ff4d..4f1eaa9 100644 (file)
@@ -23,7 +23,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['addsection'] = 'Add week';
+$string['addsections'] = 'Add weeks';
 $string['currentsection'] = 'This week';
 $string['editsection'] = 'Edit week';
 $string['editsectionname'] = 'Edit week name';
index db7cacd..7e1d175 100644 (file)
@@ -86,3 +86,18 @@ Feature: Sections can be edited and deleted in weeks format
     And I should not see "Test chat name"
     And I should see "Test choice name" in the "li#section-4" "css_element"
     And I should see "22 May - 28 May"
+
+  @javascript
+  Scenario: Adding sections in weeks format
+    When I follow "Add weeks"
+    Then the field "Number of sections" matches value "1"
+    And I press "Add weeks"
+    And I should see "5 June - 11 June" in the "li#section-6" "css_element"
+    And "li#section-7" "css_element" should not exist
+    And I follow "Add weeks"
+    And I set the field "Number of sections" to "3"
+    And I press "Add weeks"
+    And I should see "12 June - 18 June" in the "li#section-7" "css_element"
+    And I should see "19 June - 25 June" in the "li#section-8" "css_element"
+    And I should see "26 June - 2 July" in the "li#section-9" "css_element"
+    And "li#section-10" "css_element" should not exist
index 0b1e736..5e2e3d4 100644 (file)
@@ -83,8 +83,8 @@ $string['choosefilefromactivitybackup'] = 'Activity backup area';
 $string['choosefilefromactivitybackup_help'] = 'Activity backups made using default settings are stored here.';
 $string['choosefilefromautomatedbackup'] = 'Automated backups';
 $string['choosefilefromautomatedbackup_help'] = 'Contains automatically generated backups.';
-$string['config_keep_groups_and_groupings'] = 'By default keep current roles and enrolments';
-$string['config_keep_roles_and_enrolments'] = 'By default keep current groups and groupings';
+$string['config_keep_groups_and_groupings'] = 'By default keep current groups and groupings.';
+$string['config_keep_roles_and_enrolments'] = 'By default keep current roles and enrolments.';
 $string['config_overwrite_conf'] = 'Allows user to overwrite the current course configuration';
 $string['config_overwrite_course_fullname'] = 'By default overwrite course full name with the one from the backup file. This requires "Overwrite course configuration" to be checked and current user to have the capability to change course full name (moodle/course:changefullname)';
 $string['config_overwrite_course_shortname'] = 'By default overwrite course short name with the one from the backup file. This requires "Overwrite course configuration" to be checked and current user to have the capability to change course short name (moodle/course:changeshortname)';
index dd16d5f..bb4f0d7 100644 (file)
@@ -134,7 +134,7 @@ $string['dependenciescompleted'] = 'Completion of other courses';
 $string['hiddenrules'] = 'Some settings specific to <b>{$a}</b> have been hidden. To view unselect other activities';
 $string['editcoursecompletionsettings'] = 'Edit course completion settings';
 $string['enablecompletion'] = 'Enable completion tracking';
-$string['enablecompletion_help'] = 'If enabled, activity completion conditions may be set in the activity settings and/or course completion conditions may be set. It is recommended to have this enabled in order for the course progress dashboard to display meaningful data.';
+$string['enablecompletion_help'] = 'If enabled, activity completion conditions may be set in the activity settings and/or course completion conditions may be set. It is recommended to have this enabled so that meaningful data is displayed in the course overview on the Dashboard.';
 $string['enrolmentduration'] = 'Enrolment duration';
 $string['enrolmentdurationlength'] = 'User must remain enrolled for';
 $string['err_noactivities'] = 'Completion information is not enabled for any activity, so none can be displayed. You can enable completion information by editing the settings for an activity.';
index 9a317cf..196ebde 100644 (file)
@@ -71,7 +71,7 @@ $string['addresource'] = 'Add a resource...';
 $string['addresourceoractivity'] = 'Add an activity or resource';
 $string['addresourcetosection'] = 'Add a resource to section \'{$a}\'';
 $string['address'] = 'Address';
-$string['addsection'] = 'Add section';
+$string['addsections'] = 'Add sections';
 $string['addstudent'] = 'Add student';
 $string['addsubcategory'] = 'Add a subcategory';
 $string['addteacher'] = 'Add teacher';
index 0bdfe30..96737e8 100644 (file)
Binary files a/lib/amd/build/modal_save_cancel.min.js and b/lib/amd/build/modal_save_cancel.min.js differ
index 6aa133c..5949bb6 100644 (file)
@@ -88,5 +88,14 @@ define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/m
         }.bind(this));
     };
 
+    /**
+     * Allows to overwrite the text of "Save changes" button.
+     *
+     * @param {String} text
+     */
+    ModalSaveCancel.prototype.setSaveButtonText = function(text) {
+        this.getFooter().find(SELECTORS.SAVE_BUTTON).text(text);
+    };
+
     return ModalSaveCancel;
 });
index 8601211..d5ef0ab 100644 (file)
@@ -221,7 +221,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:i/grade_correct' => 'fa-check text-success',
             'core:i/grade_incorrect' => 'fa-remove text-danger',
             'core:i/grade_partiallycorrect' => 'fa-check-square',
-            'core:i/grades' => 'fa-graduation-cap',
+            'core:i/grades' => 'fa-table',
             'core:i/groupevent' => 'fa-group',
             'core:i/groupn' => 'fa-user',
             'core:i/group' => 'fa-users',
@@ -334,7 +334,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:t/enrolusers' => 'fa-user-plus',
             'core:t/expanded' => 'fa-caret-down',
             'core:t/go' => 'fa-play',
-            'core:t/grades' => 'fa-graduation-cap',
+            'core:t/grades' => 'fa-table',
             'core:t/groupn' => 'fa-user',
             'core:t/groups' => 'fa-user-circle',
             'core:t/groupv' => 'fa-user-circle-o',
index ca5a63e..34dd6d2 100644 (file)
@@ -59,10 +59,9 @@ class refresh_mod_calendar_events_task extends adhoc_task {
                 continue;
             }
             // Check if the plugin implements *_refresh_events() and call it when it does.
-            $refresheventsfunction = $plugin->name . '_refresh_events';
-            if (function_exists($refresheventsfunction)) {
-                mtrace('Calling ' . $refresheventsfunction);
-                call_user_func($refresheventsfunction);
+            if (component_callback_exists('mod_' . $plugin->name, 'refresh_events')) {
+                mtrace('Refreshing events for ' . $plugin->name);
+                component_callback('mod_' . $plugin->name, 'refresh_events');
             }
         }
     }
index 8e08d74..21b4ebe 100644 (file)
@@ -2566,19 +2566,6 @@ function xmldb_main_upgrade($oldversion) {
             $dbman->add_field($table, $field);
         }
 
-        // Create adhoc task for upgrading of existing calendar events.
-        $record = new \stdClass();
-        $record->classname = "\\core\\task\\refresh_mod_calendar_events_task";
-        $record->component = 'core';
-        // Next run time based from nextruntime computation in \core\task\manager::queue_adhoc_task().
-        $nextruntime = time() - 1;
-        $record->nextruntime = $nextruntime;
-        $DB->insert_record('task_adhoc', $record);
-
-        // This same task is queued again in a later step, but if we already queue it here
-        // then there is no need to queue it again. We use this flag in the second step.
-        $refresheventsadhocadded = true;
-
         // Main savepoint reached.
         upgrade_main_savepoint(true, 2017030700.00);
     }
@@ -2671,21 +2658,6 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017040402.00);
     }
 
-    if ($oldversion < 2017040403.00) {
-        // Create adhoc task for upgrading of existing calendar events.
-        $record = new \stdClass();
-        $record->classname = "\\core\\task\\refresh_mod_calendar_events_task";
-        $record->component = 'core';
-
-        // Next run time based from nextruntime computation in \core\task\manager::queue_adhoc_task().
-        $nextruntime = time() - 1;
-        $record->nextruntime = $nextruntime;
-        $DB->insert_record('task_adhoc', $record);
-
-        // Main savepoint reached.
-        upgrade_main_savepoint(true, 2017040403.00);
-    }
-
     if ($oldversion < 2017040700.01) {
 
         // Define table oauth2_issuer to be created.
@@ -2838,25 +2810,6 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017041801.00);
     }
 
-    if ($oldversion < 2017042600.01) {
-        // If the previous step didn't execute and queue the task.
-        if (!isset($refresheventsadhocadded)) {
-            // Create adhoc task for upgrading of existing calendar events.
-            $record = new \stdClass();
-            $record->classname = "\\core\\task\\refresh_mod_calendar_events_task";
-            $record->component = 'core';
-
-            // Next run time based from nextruntime computation in \core\task\manager::queue_adhoc_task().
-            $nextruntime = time() - 1;
-            $record->nextruntime = $nextruntime;
-            $DB->insert_record('task_adhoc', $record);
-
-        }
-
-        // Main savepoint reached.
-        upgrade_main_savepoint(true, 2017042600.01);
-    }
-
     if ($oldversion < 2017050500.01) {
         // Get the list of parent event IDs.
         $sql = "SELECT DISTINCT repeatid
@@ -2894,5 +2847,20 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017050500.02);
     }
 
+    if ($oldversion < 2017050900.01) {
+        // Create adhoc task for upgrading of existing calendar events.
+        $record = new \stdClass();
+        $record->classname = '\core\task\refresh_mod_calendar_events_task';
+        $record->component = 'core';
+
+        // Next run time based from nextruntime computation in \core\task\manager::queue_adhoc_task().
+        $nextruntime = time() - 1;
+        $record->nextruntime = $nextruntime;
+        $DB->insert_record('task_adhoc', $record);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017050900.01);
+    }
+
     return true;
 }
index 67ac2a4..9759ced 100644 (file)
@@ -1388,16 +1388,34 @@ function disable_output_buffering() {
 }
 
 /**
- * Check whether a major upgrade is needed. That is defined as an upgrade that
- * changes something really fundamental in the database, so nothing can possibly
- * work until the database has been updated, and that is defined by the hard-coded
- * version number in this function.
+ * Check whether a major upgrade is needed.
+ *
+ * That is defined as an upgrade that changes something really fundamental
+ * in the database, so nothing can possibly work until the database has
+ * been updated, and that is defined by the hard-coded version number in
+ * this function.
+ *
+ * @return bool
  */
-function redirect_if_major_upgrade_required() {
+function is_major_upgrade_required() {
     global $CFG;
     $lastmajordbchanges = 2017040403.00;
-    if (empty($CFG->version) or (float)$CFG->version < $lastmajordbchanges or
-            during_initial_install() or !empty($CFG->adminsetuppending)) {
+
+    $required = empty($CFG->version);
+    $required = $required || (float)$CFG->version < $lastmajordbchanges;
+    $required = $required || during_initial_install();
+    $required = $required || !empty($CFG->adminsetuppending);
+
+    return $required;
+}
+
+/**
+ * Redirect to the Notifications page if a major upgrade is required, and
+ * terminate the current user session.
+ */
+function redirect_if_major_upgrade_required() {
+    global $CFG;
+    if (is_major_upgrade_required()) {
         try {
             @\core\session\manager::terminate_current();
         } catch (Exception $e) {
index a26b39b..73718de 100644 (file)
@@ -35,9 +35,9 @@
 
     Example context (json):
     {
-        "col1content": "<div class='alert alert-error'>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam pellentesque id urna sit amet tempor.</div>",
-        "col2content": "<div class='alert alert-success'>Donec lacus nisl, molestie eget sodales non, sodales et nibh. Praesent dignissim placerat sodales.</div>",
-        "col3content": "<div class='alert alert-info'>Praesent sit amet ante odio. In mollis nisl at mi bibendum venenatis.</div>"
+        "col1content": "<div class='alert alert-error'>1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam pellentesque id urna sit amet tempor.</div>",
+        "col2content": "<div class='alert alert-success'>2. Donec lacus nisl, molestie eget sodales non, sodales et nibh. Praesent dignissim placerat sodales.</div>",
+        "col3content": "<div class='alert alert-info'>3. Praesent sit amet ante odio. In mollis nisl at mi bibendum venenatis.</div>"
     }
 
 }}
index d94a83d..2413342 100644 (file)
@@ -34,8 +34,8 @@
 
     Example context (json):
     {
-        "col1content": "<div class='alert alert-info'>Lorem ipsum dolor sit amet, consectetur adipiscing elit. In porttitor vulputate turpis, quis tempor arcu.</div>",
-        "col2content": "<div class='alert alert-success'>Vivamus ac orci in velit fringilla aliquam a a nisl. Cras luctus quam laoreet magna pulvinar aliquet.</div>"
+        "col1content": "<div class='alert alert-info'>1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In porttitor vulputate turpis, quis tempor arcu.</div>",
+        "col2content": "<div class='alert alert-success'>2. Vivamus ac orci in velit fringilla aliquam a a nisl. Cras luctus quam laoreet magna pulvinar aliquet.</div>"
     }
 
 }}
index 1978fae..d66b341 100644 (file)
@@ -34,8 +34,8 @@
 
     Example context (json):
     {
-        "col1content": "<div class='alert alert-info'>Lorem ipsum dolor sit amet, consectetur adipiscing elit. In porttitor vulputate turpis, quis tempor arcu.</div>",
-        "col2content": "<div class='alert alert-success'>Vivamus ac orci in velit fringilla aliquam a a nisl. Cras luctus quam laoreet magna pulvinar aliquet.</div>"
+        "col1content": "<div class='alert alert-info'>1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In porttitor vulputate turpis, quis tempor arcu.</div>",
+        "col2content": "<div class='alert alert-success'>2. Vivamus ac orci in velit fringilla aliquam a a nisl. Cras luctus quam laoreet magna pulvinar aliquet.</div>"
     }
 }}
 <div class="row-fluid rtl-compatible">
index e145180..dc6774d 100644 (file)
@@ -97,5 +97,17 @@ $PAGE->set_heading($SITE->fullname);
 
 echo $OUTPUT->header();
 
-echo $OUTPUT->render($mform_signup);
+if ($mform_signup instanceof renderable) {
+    // Try and use the renderer from the auth plugin if it exists.
+    try {
+        $renderer = $PAGE->get_renderer('auth_' . $authplugin->authtype);
+    } catch (coding_exception $ce) {
+        // Fall back on the general renderer.
+        $renderer = $OUTPUT;
+    }
+    echo $renderer->render($mform_signup);
+} else {
+    // Fall back for auth plugins not using renderables.
+    $mform_signup->display();
+}
 echo $OUTPUT->footer();
index 5afc888..bc8f955 100644 (file)
@@ -253,35 +253,6 @@ function xmldb_assign_upgrade($oldversion) {
         upgrade_mod_savepoint(true, 2017021500, 'assign');
     }
 
-    if ($oldversion < 2017031000) {
-        // Set priority of assign user overrides.
-        $params = [
-            'modulename' => 'assign',
-            'courseid' => 0,
-            'groupid' => 0,
-            'repeatid' => 0
-        ];
-        // CALENDAR_EVENT_USER_OVERRIDE_PRIORITY has a value of 9999999.
-        $DB->set_field('event', 'priority', 9999999, $params);
-
-        // Set priority for group overrides for existing assign events.
-        $where = 'groupid IS NOT NULL';
-        $assignoverridesrs = $DB->get_recordset_select('assign_overrides', $where, null, '', 'id, assignid, groupid, sortorder');
-        foreach ($assignoverridesrs as $record) {
-            $params = [
-                'modulename' => 'assign',
-                'instance' => $record->assignid,
-                'groupid' => $record->groupid,
-                'repeatid' => 0
-            ];
-            $DB->set_field('event', 'priority', $record->sortorder, $params);
-        }
-        $assignoverridesrs->close();
-
-        // Assign savepoint reached.
-        upgrade_mod_savepoint(true, 2017031000, 'assign');
-    }
-
     if ($oldversion < 2017031300) {
         // Add a 'gradingduedate' field to the 'assign' table.
         $table = new xmldb_table('assign');
@@ -318,18 +289,6 @@ function xmldb_assign_upgrade($oldversion) {
         // Execute DB update for assign instances.
         $DB->execute($sql, $params);
 
-        // Create adhoc task for upgrading of existing mod_assign calendar events.
-        $task = new \stdClass();
-        $task->classname = "\\core\\task\\refresh_mod_calendar_events_task";
-        $task->component = 'core';
-
-        // Next run time based from nextruntime computation in \core\task\manager::queue_adhoc_task().
-        $nextruntime = time() - 1;
-        $task->nextruntime = $nextruntime;
-        // Indicate to the adhoc task that only the assignment module will be refreshed.
-        $task->customdata = json_encode(['plugins' => ['assign']]);
-        $DB->insert_record('task_adhoc', $task);
-
         // Assign savepoint reached.
         upgrade_mod_savepoint(true, 2017042800, 'assign');
     }
index 91ceb02..33caa81 100644 (file)
@@ -209,14 +209,13 @@ class assign_grading_table extends table_sql implements renderable {
                       JOIN {groups_members} gm ON gm.groupid = g.id
                      WHERE go.assignid = :assignmentid6
                   )
-                ) AS merged
+                ) merged
                 GROUP BY merged.userid
               ) priority ON priority.userid = u.id
 
             JOIN (
               (SELECT 9999999 AS priority,
                       u.id AS userid,
-
                       a.allowsubmissionsfromdate,
                       a.duedate,
                       a.cutoffdate
@@ -226,7 +225,6 @@ class assign_grading_table extends table_sql implements renderable {
               UNION
               (SELECT 0 AS priority,
                       uo.userid,
-
                       uo.allowsubmissionsfromdate,
                       uo.duedate,
                       uo.cutoffdate
@@ -236,7 +234,6 @@ class assign_grading_table extends table_sql implements renderable {
               UNION
               (SELECT go.sortorder AS priority,
                       gm.userid,
-
                       go.allowsubmissionsfromdate,
                       go.duedate,
                       go.cutoffdate
@@ -246,7 +243,7 @@ class assign_grading_table extends table_sql implements renderable {
                 WHERE go.assignid = :assignmentid9
               )
 
-            ) AS effective ON effective.priority = priority.priority AND effective.userid = priority.userid ';
+            ) effective ON effective.priority = priority.priority AND effective.userid = priority.userid ';
         }
 
         if (!empty($this->assignment->get_instance()->blindmarking)) {
index 30c472c..860f5ad 100644 (file)
@@ -231,14 +231,17 @@ function assign_update_events($assign, $override = null) {
             $conds['groupid'] = $override->groupid;
         }
     }
-    $oldevents = $DB->get_records('event', $conds);
+    $oldevents = $DB->get_records('event', $conds, 'id ASC');
 
     // Now make a to-do list of all that needs to be updated.
     if (empty($override)) {
-        // We are updating the primary settings for the assign, so we need to add all the overrides.
-        $overrides = $DB->get_records('assign_overrides', array('assignid' => $assigninstance->id));
-        // As well as the original assign (empty override).
-        $overrides[] = new stdClass();
+        // We are updating the primary settings for the assignment, so we need to add all the overrides.
+        $overrides = $DB->get_records('assign_overrides', array('assignid' => $assigninstance->id), 'id ASC');
+        // It is necessary to add an empty stdClass to the beginning of the array as the $oldevents
+        // list contains the original (non-override) event for the module. If this is not included
+        // the logic below will end up updating the wrong row when we try to reconcile this $overrides
+        // list against the $oldevents list.
+        array_unshift($overrides, new stdClass());
     } else {
         // Just do the one override.
         $overrides = array($override);
@@ -272,6 +275,7 @@ function assign_update_events($assign, $override = null) {
         $event->timesort    = $event->timestart + $event->timeduration;
         $event->visible     = instance_is_visible('assign', $assigninstance);
         $event->eventtype   = ASSIGN_EVENT_TYPE_DUE;
+        $event->priority    = null;
 
         // Determine the event name and priority.
         if ($groupid) {
index 7a6ab25..e838f5d 100644 (file)
@@ -121,14 +121,17 @@ function lesson_update_events($lesson, $override = null) {
             $conds['groupid'] = $override->groupid;
         }
     }
-    $oldevents = $DB->get_records('event', $conds);
+    $oldevents = $DB->get_records('event', $conds, 'id ASC');
 
     // Now make a to-do list of all that needs to be updated.
     if (empty($override)) {
         // We are updating the primary settings for the lesson, so we need to add all the overrides.
-        $overrides = $DB->get_records('lesson_overrides', array('lessonid' => $lesson->id));
-        // As well as the original lesson (empty override).
-        $overrides[] = new stdClass();
+        $overrides = $DB->get_records('lesson_overrides', array('lessonid' => $lesson->id), 'id ASC');
+        // It is necessary to add an empty stdClass to the beginning of the array as the $oldevents
+        // list contains the original (non-override) event for the module. If this is not included
+        // the logic below will end up updating the wrong row when we try to reconcile this $overrides
+        // list against the $oldevents list.
+        array_unshift($overrides, new stdClass());
     } else {
         // Just do the one override.
         $overrides = array($override);
@@ -167,6 +170,7 @@ function lesson_update_events($lesson, $override = null) {
         $event->timesort    = $available;
         $event->visible     = instance_is_visible('lesson', $lesson);
         $event->eventtype   = LESSON_EVENT_TYPE_OPEN;
+        $event->priority    = null;
 
         // Determine the event name and priority.
         if ($groupid) {
index 597a4d3..2c0e781 100644 (file)
@@ -94,9 +94,9 @@ class service_exception_handler {
     /**
      * Echo an exception message encapsulated in XML.
      *
-     * @param \Exception $exception The exception that was thrown
+     * @param \Exception|\Throwable $exception The exception that was thrown
      */
-    public function handle(\Exception $exception) {
+    public function handle($exception) {
         $message = $exception->getMessage();
 
         // Add the exception backtrace for developers.
index 7a9300a..de6d1de 100644 (file)
@@ -1205,14 +1205,17 @@ function quiz_update_events($quiz, $override = null) {
             $conds['groupid'] = $override->groupid;
         }
     }
-    $oldevents = $DB->get_records('event', $conds);
+    $oldevents = $DB->get_records('event', $conds, 'id ASC');
 
     // Now make a to-do list of all that needs to be updated.
     if (empty($override)) {
-        // We are updating the primary settings for the lesson, so we need to add all the overrides.
-        $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id));
-        // As well as the original quiz (empty override).
-        $overrides[] = new stdClass();
+        // We are updating the primary settings for the quiz, so we need to add all the overrides.
+        $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id), 'id ASC');
+        // It is necessary to add an empty stdClass to the beginning of the array as the $oldevents
+        // list contains the original (non-override) event for the module. If this is not included
+        // the logic below will end up updating the wrong row when we try to reconcile this $overrides
+        // list against the $oldevents list.
+        array_unshift($overrides, new stdClass());
     } else {
         // Just do the one override.
         $overrides = array($override);
@@ -1251,6 +1254,7 @@ function quiz_update_events($quiz, $override = null) {
         $event->timesort    = $timeopen;
         $event->visible     = instance_is_visible('quiz', $quiz);
         $event->eventtype   = QUIZ_EVENT_TYPE_OPEN;
+        $event->priority    = null;
 
         // Determine the event name and priority.
         if ($groupid) {
index e78e94d..f1684b1 100644 (file)
@@ -95,7 +95,7 @@ $string['confirmloosetracks'] = 'WARNING: The package seems to be changed or mod
 $string['contents'] = 'Contents';
 $string['coursepacket'] = 'Course package';
 $string['coursestruct'] = 'Course structure';
-$string['crontask'] = 'Background processing for Scorm';
+$string['crontask'] = 'Background processing for SCORM';
 $string['currentwindow'] = 'Current window';
 $string['datadir'] = 'Filesystem error: Can\'t create course data directory';
 $string['defaultdisplaysettings'] = 'Default display settings';
index d9dfe62..83ab88a 100644 (file)
@@ -34,7 +34,7 @@ $string['importskydrivefiles'] = 'Import files from Microsoft SkyDrive repositor
 $string['internal'] = 'Internal (files stored in Moodle)';
 $string['issuer_help'] = 'Select the OAuth 2 service that is configured to talk to the OneDrive API. If the service does not exist yet, you will need to create it.';
 $string['issuer'] = 'OAuth 2 service';
-$string['mysitenotfound'] = 'You have never logged into OneDrive before. You must login to OneDrive at least once it before it can be used with Moodle.';
+$string['mysitenotfound'] = 'You have never logged into OneDrive before. You must log in to OneDrive at least once before it can be used with Moodle.';
 $string['oauth2serviceslink'] = '<a href="{$a}" title="Link to OAuth 2 services configuration">OAuth 2 services configuration</a>';
 $string['owner'] = 'Owned by: {$a}';
 $string['pluginname'] = 'Microsoft OneDrive';
diff --git a/theme/boost/templates/core/columns-1to1to1.mustache b/theme/boost/templates/core/columns-1to1to1.mustache
new file mode 100644 (file)
index 0000000..adb1038
--- /dev/null
@@ -0,0 +1,48 @@
+{{!
+    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/columns-1to1to1
+
+    Moodle columns-1to1to1 template.
+
+    The purpose of this template is to render a template with 3 columns.
+    On mobile the columns stack underneath each other.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * col1content Column 1 contents.
+    * col2content Column 2 contents.
+    * col3content Column 3 contents.
+
+    Example context (json):
+    {
+        "col1content": "<div class='alert alert-error'>1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam pellentesque id urna sit amet tempor.</div>",
+        "col2content": "<div class='alert alert-success'>2. Donec lacus nisl, molestie eget sodales non, sodales et nibh. Praesent dignissim placerat sodales.</div>",
+        "col3content": "<div class='alert alert-info'>3. Praesent sit amet ante odio. In mollis nisl at mi bibendum venenatis.</div>"
+    }
+
+}}
+<div class="row">
+    <div class="col-md-4">{{$ column1 }}{{{ col1content }}}{{/ column1 }}</div>
+    <div class="col-md-4">{{$ column2 }}{{{ col2content }}}{{/ column2 }}</div>
+    <div class="col-md-4">{{$ column3 }}{{{ col3content }}}{{/ column3 }}</div>
+</div>
diff --git a/theme/boost/templates/core/columns-1to2.mustache b/theme/boost/templates/core/columns-1to2.mustache
new file mode 100644 (file)
index 0000000..114efcb
--- /dev/null
@@ -0,0 +1,45 @@
+{{!
+    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/columns-1to2
+
+    Moodle columns-1to2 template.
+
+    The purpose of this template is to render 2 columns where the second column has twice the width of the first one.
+    On mobile the second column collapses underneath the first.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * col1content Column 1 contents.
+    * col2content Column 2 contents.
+
+    Example context (json):
+    {
+        "col1content": "<div class='alert alert-info'>1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In porttitor vulputate turpis, quis tempor arcu.</div>",
+        "col2content": "<div class='alert alert-success'>2. Vivamus ac orci in velit fringilla aliquam a a nisl. Cras luctus quam laoreet magna pulvinar aliquet.</div>"
+    }
+
+}}
+<div class="row">
+    <div class="col-md-4">{{$ column1 }}{{{ col1content }}}{{/ column1 }}</div>
+    <div class="col-md-8">{{$ column2 }}{{{ col2content }}}{{/ column2 }}</div>
+</div>
diff --git a/theme/boost/templates/core/columns-2to1.mustache b/theme/boost/templates/core/columns-2to1.mustache
new file mode 100644 (file)
index 0000000..68f6e76
--- /dev/null
@@ -0,0 +1,44 @@
+{{!
+    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/columns-2to1
+
+    Moodle columns-2to1 template.
+
+    The purpose of this template is to render 2 columns where the first column has twice the width of the second one.
+    On mobile the second column collapses underneath the first.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * col1content Column 1 contents.
+    * col2content Column 2 contents.
+
+    Example context (json):
+    {
+        "col1content": "<div class='alert alert-info'>1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In porttitor vulputate turpis, quis tempor arcu.</div>",
+        "col2content": "<div class='alert alert-success'>2. Vivamus ac orci in velit fringilla aliquam a a nisl. Cras luctus quam laoreet magna pulvinar aliquet.</div>"
+    }
+}}
+<div class="row">
+    <div class="col-md-8">{{$ column1 }}{{{ col1content }}}{{/ column1 }}</div>
+    <div class="col-md-4">{{$ column2 }}{{{ col2content }}}{{/ column2 }}</div>
+</div>
index 0b7efa7..4f31d56 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017050900.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017050900.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
index f6334be..fb888d9 100644 (file)
@@ -161,11 +161,11 @@ class webservice_xmlrpc_server extends webservice_base_server {
     /**
      * Generate the XML-RPC fault response.
      *
-     * @param Exception $ex The exception.
+     * @param Exception|Throwable $ex The exception.
      * @param int $faultcode The faultCode to be included in the fault response
      * @return string The XML-RPC fault response xml containing the faultCode and faultString.
      */
-    protected function generate_error(Exception $ex, $faultcode = 404) {
+    protected function generate_error($ex, $faultcode = 404) {
         $error = $ex->getMessage();
 
         if (!empty($ex->errorcode)) {