Merge branch 'MDL-58711-master' of git://github.com/jleyva/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 17 Jan 2018 09:55:23 +0000 (10:55 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 17 Jan 2018 09:55:23 +0000 (10:55 +0100)
135 files changed:
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/oauth2/lang/en/tool_oauth2.php
admin/tool/task/clear_fail_delay.php [new file with mode: 0644]
admin/tool/task/lang/en/tool_task.php
admin/tool/task/renderer.php
admin/tool/task/styles.css
admin/tool/task/tests/behat/behat_tool_task.php [new file with mode: 0644]
admin/tool/task/tests/behat/clear_fail_delay.feature [new file with mode: 0644]
admin/tool/uploadcourse/classes/step2_form.php
admin/tool/usertours/amd/build/tour.min.js
admin/tool/usertours/amd/src/tour.js
admin/tool/usertours/tests/behat/tour_filter.feature
admin/tool/usertours/thirdpartylibs.xml
auth/ldap/auth.php
auth/ldap/lang/en/auth_ldap.php
backup/moodle2/restore_course_task.class.php
backup/util/plan/restore_step.class.php
badges/backpack.js
blocks/mnet_hosts/block_mnet_hosts.php
calendar/amd/build/calendar_threemonth.min.js
calendar/amd/build/repository.min.js
calendar/amd/build/view_manager.min.js
calendar/amd/src/calendar_threemonth.js
calendar/amd/src/repository.js
calendar/amd/src/view_manager.js
calendar/classes/external/calendar_day_exporter.php
calendar/classes/external/calendar_event_exporter.php
calendar/classes/external/calendar_upcoming_exporter.php
calendar/classes/external/day_exporter.php
calendar/classes/external/event_exporter_base.php
calendar/classes/external/events_related_objects_cache.php
calendar/classes/external/month_exporter.php
calendar/classes/external/week_day_exporter.php
calendar/classes/local/event/container.php
calendar/externallib.php
calendar/lib.php
calendar/renderer.php
calendar/templates/calendar_mini.mustache
calendar/tests/calendar_event_exporter_test.php
calendar/tests/events_related_objects_cache_test.php [new file with mode: 0644]
calendar/tests/externallib_test.php
completion/classes/edit_base_form.php
course/dndupload.js
course/dnduploadlib.php
course/edit_form.php
course/lib.php
course/moodleform_mod.php
course/reset_form.php
enrol/category/lang/en/enrol_category.php
enrol/database/lib.php
enrol/manual/amd/build/quickenrolment.min.js
enrol/manual/amd/src/quickenrolment.js
group/lib.php
iplookup/tests/geoip_test.php
iplookup/tests/geoplugin_test.php
lang/en/admin.php
lang/en/auth.php
lang/en/badges.php
lang/en/deprecated.txt
lang/en/error.php
lang/en/hub.php
lang/en/langconfig.php
lang/en/moodle.php
lang/en/repository.php
lang/en/search.php
lang/en/table.php
lang/en/tag.php
lib/badgeslib.php
lib/classes/files/curl_security_helper.php
lib/classes/output/notification.php
lib/classes/task/manager.php
lib/form/amd/build/defaultcustom.min.js
lib/form/amd/src/defaultcustom.js
lib/form/defaultcustom.php
lib/form/form.js
lib/formslib.php
lib/grouplib.php
lib/outputlib.php
lib/outputrenderers.php
lib/tests/curl_security_helper_test.php
lib/tests/scheduled_task_test.php
lib/tests/theme_config_test.php
lib/upgrade.txt
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/quickcommentlist.js
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/submission/onlinetext/locallib.php
mod/feedback/lang/en/deprecated.txt
mod/feedback/lang/en/feedback.php
mod/forum/lang/en/deprecated.txt [deleted file]
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/rsslib.php
mod/forum/tests/behat/edit_post_student.feature
mod/forum/tests/lib_test.php
mod/lesson/lang/en/deprecated.txt [deleted file]
mod/lesson/lang/en/lesson.php
mod/lesson/locallib.php
mod/lesson/pagetypes/multichoice.php
mod/lti/amd/build/contentitem_return.min.js
mod/lti/amd/src/contentitem_return.js
mod/lti/locallib.php
mod/lti/return.php
mod/quiz/classes/external.php
mod/quiz/tests/behat/manually_mark_question.feature
mod/quiz/tests/external_test.php
mod/quiz/tests/fixtures/moodle_logo.jpg [new file with mode: 0644]
mod/workshop/form/accumulative/lang/en/deprecated.txt [deleted file]
mod/workshop/form/accumulative/lang/en/workshopform_accumulative.php
mod/workshop/form/comments/lang/en/deprecated.txt [deleted file]
mod/workshop/form/comments/lang/en/workshopform_comments.php
mod/workshop/form/numerrors/lang/en/deprecated.txt [deleted file]
mod/workshop/form/numerrors/lang/en/workshopform_numerrors.php
mod/workshop/lang/en/deprecated.txt
mod/workshop/lang/en/workshop.php
question/behaviour/behaviourbase.php
question/behaviour/rendererbase.php
question/engine/lib.php
question/engine/questionattempt.php
question/type/essay/renderer.php
report/outline/classes/filter_form.php [new file with mode: 0644]
report/outline/index.php
report/outline/tests/behat/behat_report_outline.php [new file with mode: 0644]
report/outline/tests/behat/filter.feature [new file with mode: 0644]
repository/filepicker.js
repository/lib.php
repository/manage_instances.php
repository/repository_ajax.php
search/classes/document.php
theme/styles.php
user/tests/behat/view_participants.feature
version.php

index 47def2b..f08f71c 100644 (file)
@@ -67,7 +67,7 @@ $string['invalidanalysablestable'] = 'Invalid site analysable elements table';
 $string['invalidprediction'] = 'Invalid to get predictions';
 $string['invalidtraining'] = 'Invalid to train the model';
 $string['loginfo'] = 'Log extra info';
-$string['modelid'] = 'Model id';
+$string['modelid'] = 'Model ID';
 $string['modelinvalidanalysables'] = 'Invalid analysable elements for "{$a}" model';
 $string['modelresults'] = '{$a} results';
 $string['modeltimesplitting'] = 'Time splitting';
index b8fe75b..55fce2e 100644 (file)
@@ -96,7 +96,7 @@ $string['systemaccountconnected'] = 'System account connected';
 $string['systemaccountnotconnected'] = 'System account not connected';
 $string['systemauthstatus'] = 'System account connected';
 $string['usebasicauth'] = 'Authenticate token requests via HTTP headers';
-$string['usebasicauth_help'] = 'Utilize the HTTP Basic authentication scheme when sending client ID and password with a refresh token request. Recommended by the OAuth 2 standard, but may not be available with some issuers.';
+$string['usebasicauth_help'] = 'Utilise the HTTP Basic authentication scheme when sending client ID and password with a refresh token request. Recommended by the OAuth 2 standard, but may not be available with some issuers.';
 $string['userfieldexternalfield'] = 'External field name';
 $string['userfieldexternalfield_help'] = 'Name of the field provided by the external OAuth system.';
 $string['userfieldinternalfield_help'] = 'Name of the Moodle user field that should be mapped from the external field.';
diff --git a/admin/tool/task/clear_fail_delay.php b/admin/tool/task/clear_fail_delay.php
new file mode 100644 (file)
index 0000000..d820dda
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Script clears the fail delay for a task and reschedules its next execution.
+ *
+ * @package tool_task
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('NO_OUTPUT_BUFFERING', true);
+
+require('../../../config.php');
+
+require_once($CFG->libdir.'/cronlib.php');
+
+// Basic security checks.
+require_login();
+$context = context_system::instance();
+require_capability('moodle/site:config', $context);
+
+// Get task and check the parameter is valid.
+$taskname = required_param('task', PARAM_RAW_TRIMMED);
+$task = \core\task\manager::get_scheduled_task($taskname);
+if (!$task) {
+    print_error('cannotfindinfo', 'error', $taskname);
+}
+
+// If actually doing the clear, then carry out the task and redirect to the scheduled task page.
+if (optional_param('confirm', 0, PARAM_INT)) {
+    require_sesskey();
+
+    \core\task\manager::clear_fail_delay($task);
+
+    redirect(new moodle_url('/admin/tool/task/scheduledtasks.php'));
+}
+
+// Start output.
+$PAGE->set_url(new moodle_url('/admin/tool/task/schedule_task.php'));
+$PAGE->set_context($context);
+$PAGE->navbar->add(get_string('scheduledtasks', 'tool_task'), new moodle_url('/admin/tool/task/scheduledtasks.php'));
+$PAGE->navbar->add(s($task->get_name()));
+$PAGE->navbar->add(get_string('clear'));
+echo $OUTPUT->header();
+
+// The initial request just shows the confirmation page; we don't do anything further unless
+// they confirm.
+echo $OUTPUT->confirm(get_string('clearfaildelay_confirm', 'tool_task', $task->get_name()),
+        new single_button(new moodle_url('/admin/tool/task/clear_fail_delay.php',
+                array('task' => $taskname, 'confirm' => 1, 'sesskey' => sesskey())),
+                get_string('clear')),
+        new single_button(new moodle_url('/admin/tool/task/scheduledtasks.php'),
+                get_string('cancel'), false));
+
+echo $OUTPUT->footer();
index 2f382b0..107e01b 100644 (file)
@@ -25,6 +25,7 @@
 $string['asap'] = 'ASAP';
 $string['backtoscheduledtasks'] = 'Back to scheduled tasks';
 $string['blocking'] = 'Blocking';
+$string['clearfaildelay_confirm'] = 'Are you sure you want to clear the fail delay for task \'{$a}\'? After clearing the delay, the task will run according to its normal schedule.';
 $string['component'] = 'Component';
 $string['corecomponent'] = 'Core';
 $string['default'] = 'Default';
index 575aa66..d612355 100644 (file)
@@ -112,6 +112,14 @@ class tool_task_renderer extends plugin_renderer_base {
                         get_string('runnow', 'tool_task')), 'task-runnow');
             }
 
+            $clearfail = '';
+            if ($task->get_fail_delay()) {
+                $clearfail = html_writer::div(html_writer::link(
+                        new moodle_url('/admin/tool/task/clear_fail_delay.php',
+                                array('task' => get_class($task), 'sesskey' => sesskey())),
+                        get_string('clear')), 'task-clearfaildelay');
+            }
+
             $row = new html_table_row(array(
                         $namecell,
                         $componentcell,
@@ -123,7 +131,7 @@ class tool_task_renderer extends plugin_renderer_base {
                         new html_table_cell($task->get_day()),
                         new html_table_cell($task->get_day_of_week()),
                         new html_table_cell($task->get_month()),
-                        new html_table_cell($task->get_fail_delay()),
+                        new html_table_cell($task->get_fail_delay() . $clearfail),
                         new html_table_cell($customised)));
 
             // Cron-style values must always be LTR.
index 7714674..0e846ce 100644 (file)
@@ -10,6 +10,7 @@
     direction: ltr;
 }
 
-#page-admin-tool-task-scheduledtasks .task-runnow {
+#page-admin-tool-task-scheduledtasks .task-runnow,
+#page-admin-tool-task-scheduledtasks .task-clearfaildelay {
     font-size: 0.75em;
 }
diff --git a/admin/tool/task/tests/behat/behat_tool_task.php b/admin/tool/task/tests/behat/behat_tool_task.php
new file mode 100644 (file)
index 0000000..0a0afe1
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Behat step definitions for scheduled task administration.
+ *
+ * @package tool_task
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+
+/**
+ * Behat step definitions for scheduled task administration.
+ *
+ * @package tool_task
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_tool_task extends behat_base {
+
+    /**
+     * Set a fake fail delay for a scheduled task.
+     *
+     * @Given /^the scheduled task "(?P<task_name>[^"]+)" has a fail delay of "(?P<seconds_number>\d+)" seconds$/
+     * @param string $task Task classname
+     * @param int $seconds Fail delay time in seconds
+     */
+    public function scheduled_task_has_fail_delay_seconds($task, $seconds) {
+        global $DB;
+        $id = $DB->get_field('task_scheduled', 'id', ['classname' => $task], IGNORE_MISSING);
+        if (!$id) {
+            throw new Exception('Unknown scheduled task: ' . $task);
+        }
+        $DB->set_field('task_scheduled', 'faildelay', $seconds, ['id' => $id]);
+    }
+}
diff --git a/admin/tool/task/tests/behat/clear_fail_delay.feature b/admin/tool/task/tests/behat/clear_fail_delay.feature
new file mode 100644 (file)
index 0000000..474468d
--- /dev/null
@@ -0,0 +1,25 @@
+@tool @tool_task
+Feature: Clear scheduled task fail delay
+  In order to stop failures from delaying a scheduled task run
+  As an admin
+  I need to be able to clear the fail delay on a task
+
+  Background:
+    Given the scheduled task "\core\task\send_new_user_passwords_task" has a fail delay of "60" seconds
+    And I log in as "admin"
+    And I navigate to "Scheduled tasks" node in "Site administration > Server"
+
+  Scenario: Clear fail delay
+    When I click on "Clear" "text" in the "Send new user passwords" "table_row"
+    And I should see "Are you sure you want to clear the fail delay"
+    And I press "Clear"
+
+    Then I should not see "60" in the "Send new user passwords" "table_row"
+    And I should not see "Clear" in the "Send new user passwords" "table_row"
+
+  Scenario: Cancel clearing the fail delay
+    When I click on "Clear" "text" in the "Send new user passwords" "table_row"
+    And I press "Cancel"
+
+    Then I should see "60" in the "Send new user passwords" "table_row"
+    And I should see "Clear" in the "Send new user passwords" "table_row"
index abdb88a..2e39463 100644 (file)
@@ -93,11 +93,11 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
         $mform->addHelpButton('defaults[visible]', 'coursevisibility');
         $mform->setDefault('defaults[visible]', $courseconfig->visible);
 
-        $mform->addElement('date_selector', 'defaults[startdate]', get_string('startdate'));
+        $mform->addElement('date_time_selector', 'defaults[startdate]', get_string('startdate'));
         $mform->addHelpButton('defaults[startdate]', 'startdate');
         $mform->setDefault('defaults[startdate]', time() + 3600 * 24);
 
-        $mform->addElement('date_selector', 'defaults[enddate]', get_string('enddate'), array('optional' => true));
+        $mform->addElement('date_time_selector', 'defaults[enddate]', get_string('enddate'), array('optional' => true));
         $mform->addHelpButton('defaults[enddate]', 'enddate');
 
         $courseformats = get_sorted_course_formats(true);
index 84a70c2..78eb5cf 100644 (file)
Binary files a/admin/tool/usertours/amd/build/tour.min.js and b/admin/tool/usertours/amd/build/tour.min.js differ
index ff3a01c..f77f4d9 100644 (file)
@@ -619,26 +619,43 @@ Tour.prototype.addEventHandler = function (eventName, handler) {
  */
 Tour.prototype.processStepListeners = function (stepConfig) {
     this.listeners.push(
-    // Next/Previous buttons.
-    {
-        node: this.currentStepNode,
-        args: ['click', '[data-role="next"]', $.proxy(this.next, this)]
-    }, {
-        node: this.currentStepNode,
-        args: ['click', '[data-role="previous"]', $.proxy(this.previous, this)]
-    },
-
-    // Close and end tour buttons.
-    {
-        node: this.currentStepNode,
-        args: ['click', '[data-role="end"]', $.proxy(this.endTour, this)]
-    },
-
-    // Keypresses.
-    {
-        node: $('body'),
-        args: ['keydown', $.proxy(this.handleKeyDown, this)]
-    });
+        // Next/Previous buttons.
+        {
+            node: this.currentStepNode,
+            args: ['click', '[data-role="next"]', $.proxy(this.next, this)]
+        }, {
+            node: this.currentStepNode,
+            args: ['click', '[data-role="previous"]', $.proxy(this.previous, this)]
+        },
+
+        // Close and end tour buttons.
+        {
+            node: this.currentStepNode,
+            args: ['click', '[data-role="end"]', $.proxy(this.endTour, this)]
+        },
+
+        // Click backdrop and hide tour.
+        {
+            node: $('[data-flexitour="backdrop"]'),
+            args: ['click', $.proxy(this.hide, this)]
+        },
+
+        // Click out and hide tour without backdrop.
+        {
+            node: $('body'),
+            args: ['click', $.proxy(function (e) {
+                // Handle click in or click out tour content,
+                // if click out, hide tour.
+                if (!this.currentStepNode.is(e.target) && $(e.target).closest('[data-role="flexitour-step"]').length === 0) {
+                    this.hide();
+                }}, this)]
+        },
+
+        // Keypresses.
+        {
+            node: $('body'),
+            args: ['keydown', $.proxy(this.handleKeyDown, this)]
+        });
 
     if (stepConfig.moveOnClick) {
         var targetNode = this.getStepTarget(stepConfig);
@@ -904,7 +921,7 @@ Tour.prototype.announceStep = function (stepConfig) {
  * @param   {EventFacade} e
  */
 Tour.prototype.handleKeyDown = function (e) {
-    var tabbableSelector = 'a[href], link[href], [draggable=true], [contenteditable=true], :input:enabled, [tabindex], button';
+    var tabbableSelector = 'a[href], link[href], [draggable=true], [contenteditable=true], :input:enabled, [tabindex], button:enabled';
     switch (e.keyCode) {
         case 27:
             this.endTour();
@@ -923,8 +940,17 @@ Tour.prototype.handleKeyDown = function (e) {
                 var activeElement = $(document.activeElement);
                 var stepTarget = this.getStepTarget(this.currentStepConfig);
                 var tabbableNodes = $(tabbableSelector);
+                var dialogContainer = $('span[data-flexitour="container"]');
                 var currentIndex = void 0;
-                tabbableNodes.filter(function (index, element) {
+                // Filter out element which is not belong to target section or dialogue.
+                if (stepTarget) {
+                    tabbableNodes = tabbableNodes.filter(function (index, element) {
+                        return stepTarget != null && (stepTarget.has(element).length || dialogContainer.has(element).length || stepTarget.is(element) || dialogContainer.is(element));
+                    });
+                }
+
+                // Find index of focusing element.
+                tabbableNodes.each(function (index, element) {
                     if (activeElement.is(element)) {
                         currentIndex = index;
                         return false;
@@ -934,7 +960,7 @@ Tour.prototype.handleKeyDown = function (e) {
                 var nextIndex = void 0;
                 var nextNode = void 0;
                 var focusRelevant = void 0;
-                if (currentIndex) {
+                if (currentIndex != void 0) {
                     var direction = 1;
                     if (e.shiftKey) {
                         direction = -1;
@@ -1090,6 +1116,16 @@ Tour.prototype.hide = function (transition) {
         $(this).remove();
     });
 
+    // Remove aria-describedby and tabindex attributes.
+    if (this.currentStepNode && this.currentStepNode.length) {
+        var stepId = this.currentStepNode.attr('id');
+        if (stepId) {
+            var currentStepElement = '[aria-describedby="' + stepId + '-body"]';
+            $(currentStepElement).removeAttr('tabindex');
+            $(currentStepElement).removeAttr('aria-describedby');
+        }
+    }
+
     // Reset the listeners.
     this.resetStepListeners();
 
index fdcd28d..0bafb34 100644 (file)
@@ -157,3 +157,23 @@ Feature: Apply tour filters to a tour
     When I am on "Course 2" course homepage
     And I wait until the page is ready
     Then I should not see "Welcome to your course tour."
+
+  @javascript
+  Scenario: Aria tags should not exist
+    Given I log in as "admin"
+    And I open the User tour settings page
+    # Turn on default tour for boost theme.
+    And I click on "Enable" "link" in the "Boost - administrator" "table_row"
+    And I am on site homepage
+    When I click on "Next" "button"
+    Then "button[aria-describedby^='tour-step-tool_usertours']" "css_element" should exist
+    And "button[tabindex]" "css_element" should exist
+    When I click on "Next" "button"
+    Then "button[aria-describedby^='tour-step-tool_usertours']" "css_element" should not exist
+    And "button[tabindex]" "css_element" should not exist
+    When I click on "Previous" "button"
+    Then "button[aria-describedby^='tour-step-tool_usertours']" "css_element" should exist
+    And "button[tabindex]" "css_element" should exist
+    When I click on "End tour" "button"
+    Then "button[aria-describedby^='tour-step-tool_usertours']" "css_element" should not exist
+    And "button[tabindex]" "css_element" should not exist
index 7d9c340..f55d8b1 100644 (file)
@@ -4,7 +4,7 @@
     <location>amd/src/tour.js</location>
     <name>Flexitour</name>
     <license>GPLv3</license>
-    <version>0.10.0</version>
+    <version>0.12.0</version>
     <licenseversion>3</licenseversion>
   </library>
   <library>
index 92948c1..b923003 100644 (file)
@@ -1624,7 +1624,7 @@ class auth_plugin_ldap extends auth_plugin_base {
                     $sesskey = sesskey();
                     redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_magic.php?sesskey='.$sesskey);
                 } else if ($this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESFORM) {
-                    redirect($CFG-wwwroot.'/login/index.php?authldap_skipntlmsso=1');
+                    redirect($CFG->wwwroot.'/login/index.php?authldap_skipntlmsso=1');
                 }
             }
             redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_attempt.php');
index 843e1b0..fb61dd1 100644 (file)
@@ -41,12 +41,12 @@ $string['auth_ldapdescription'] = 'This method provides authentication against a
                                   entry in its database. This module can read user attributes from LDAP and prefill
                                   wanted fields in Moodle.  For following logins only the username and
                                   password are checked.';
-$string['auth_ldap_expiration_desc'] = 'Select \'{$a->no}\' to disable expired password checking or \'{$a->ldapserver}\' to read the password expiration time directly from the LDAP server';
-$string['auth_ldap_expiration_key'] = 'Expiration';
-$string['auth_ldap_expiration_warning_desc'] = 'Number of days before password expiration warning is issued.';
-$string['auth_ldap_expiration_warning_key'] = 'Expiration warning';
-$string['auth_ldap_expireattr_desc'] = 'Optional: Overrides the LDAP attribute that stores password expiration time.';
-$string['auth_ldap_expireattr_key'] = 'Expiration attribute';
+$string['auth_ldap_expiration_desc'] = 'Select \'{$a->no}\' to disable expired password checking or \'{$a->ldapserver}\' to read the password expiry time directly from the LDAP server.';
+$string['auth_ldap_expiration_key'] = 'Expiry';
+$string['auth_ldap_expiration_warning_desc'] = 'Number of days before password expiry warning is issued.';
+$string['auth_ldap_expiration_warning_key'] = 'Expiry warning';
+$string['auth_ldap_expireattr_desc'] = 'Optional: Overrides the LDAP attribute that stores password expiry time.';
+$string['auth_ldap_expireattr_key'] = 'Expiry attribute';
 $string['auth_ldapextrafields'] = 'These fields are optional.  You can choose to pre-fill some Moodle user fields with information from the <b>LDAP fields</b> that you specify here. <p>If you leave these fields blank, then nothing will be transferred from LDAP and Moodle defaults will be used instead.</p><p>In either case, the user will be able to edit all of these fields after they log in.</p>';
 $string['auth_ldap_graceattr_desc'] = 'Optional: Overrides  gracelogin attribute';
 $string['auth_ldap_gracelogin_key'] = 'Grace login attribute';
@@ -75,7 +75,7 @@ $string['auth_ldap_opt_deref'] = 'Determines how aliases are handled during sear
 $string['auth_ldap_opt_deref_key'] = 'Dereference aliases';
 $string['auth_ldap_passtype'] = 'Specify the format of new or changed passwords in LDAP server.';
 $string['auth_ldap_passtype_key'] = 'Password format';
-$string['auth_ldap_passwdexpire_settings'] = 'LDAP password expiration settings';
+$string['auth_ldap_passwdexpire_settings'] = 'LDAP password expiry settings';
 $string['auth_ldap_preventpassindb'] = 'Select yes to prevent passwords from being stored in Moodle\'s DB.';
 $string['auth_ldap_preventpassindb_key'] = 'Prevent password caching';
 $string['auth_ldap_rolecontext'] = '{$a->localname} context';
@@ -91,7 +91,7 @@ $string['auth_ldap_suspended_attribute'] = 'Optional: When provided this attribu
 $string['auth_ldap_suspended_attribute_key'] = 'Suspended attribute';
 $string['auth_ldap_user_exists'] = 'LDAP username already exists.';
 $string['auth_ldap_user_settings'] = 'User lookup settings';
-$string['auth_ldap_user_type'] = 'Select how users are stored in LDAP. This setting also specifies how login expiration, grace logins and user creation will work.';
+$string['auth_ldap_user_type'] = 'Select how users are stored in LDAP. This setting also specifies how login expiry, grace logins and user creation will work.';
 $string['auth_ldap_user_type_key'] = 'User type';
 $string['auth_ldap_usertypeundefined'] = 'config.user_type not defined or function ldap_expirationtime2unix does not support selected type!';
 $string['auth_ldap_usertypeundefined2'] = 'config.user_type not defined or function ldap_unixi2expirationtime does not support selected type!';
@@ -149,7 +149,7 @@ $string['start_tls_key'] = 'Use TLS';
 $string['updateremfail'] = 'Error updating LDAP record. Error code: {$a->errno}; Error string: {$a->errstring}<br/>Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
 $string['updateremfailamb'] = 'Failed to update LDAP with ambiguous field {$a->key}; old moodle value: \'{$a->ouvalue}\', new value: \'{$a->nuvalue}\'';
 $string['updatepasserror'] = 'Error in user_update_password(). Error code: {$a->errno}; Error string: {$a->errstring}';
-$string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiration time. Error code: {$a->errno}; Error string: {$a->errstring}';
+$string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiry time. Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expirationtime and/or gracelogins. Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updateusernotfound'] = 'Could not find user while updating externally. Details follow: search base: \'{$a->userdn}\'; search filter: \'(objectClass=*)\'; search attributes: {$a->attribs}';
 $string['user_activatenotsupportusertype'] = 'auth: ldap user_activate() does not support selected usertype: {$a}';
index 2121c02..6bcf826 100644 (file)
@@ -200,7 +200,7 @@ class restore_course_task extends restore_task {
         $startdatedefaultvalue = $this->get_info()->original_course_startdate;
         $startdate = new restore_course_defaultcustom_setting('course_startdate', base_setting::IS_INTEGER, $startdatedefaultvalue);
         $startdate->set_ui(new backup_setting_ui_defaultcustom($startdate, get_string('setting_course_startdate', 'backup'),
-            ['customvalue' => $startdatedefaultvalue, 'defaultvalue' => $course->startdate, 'type' => 'date_selector']));
+            ['customvalue' => $startdatedefaultvalue, 'defaultvalue' => $course->startdate, 'type' => 'date_time_selector']));
         $this->add_setting($startdate);
 
         $keep_enrols = new restore_course_generic_setting('keep_roles_and_enrolments', base_setting::IS_BOOLEAN, false);
index 18e77ef..4890b6c 100644 (file)
@@ -84,10 +84,6 @@ abstract class restore_step extends base_step {
             // Original course has not startdate or setting doesn't exist, offset = 0.
             $cache[$this->get_restoreid()] = 0;
 
-        } else if (abs($setting - $original) < 24 * 60 * 60) {
-            // Less than 24h of difference, offset = 0 (this avoids some problems with timezones).
-            $cache[$this->get_restoreid()] = 0;
-
         } else {
             // Arrived here, let's calculate the real offset.
             $cache[$this->get_restoreid()] = $setting - $original;
index e6dd196..2282907 100644 (file)
@@ -27,18 +27,22 @@ function check_site_access() {
     var callback = {
             method: "GET",
             on: {
-                success: function(id, o, args) {
-                            var data = Y.JSON.parse(o.responseText);
-                            if (data.code == 'http-unreachable') {
-                                add.setHTML(data.response);
-                                add.removeClass('hide');
-                            }
-                        },
-                failure: function(o) { }
+                success: function(id, o) {
+                    var data = Y.JSON.parse(o.responseText);
+                    if (data.code == 'http-unreachable') {
+                        add.setHTML(data.response);
+                        add.removeClass('hide');
+                    }
+                    M.util.js_complete('badge/backpack::check_site_access');
+                },
+                failure: function() {
+                    M.util.js_complete('badge/backpack::check_site_access');
+                }
             }
         };
 
     Y.use('io-base', function(Y) {
+        M.util.js_pending('badge/backpack::check_site_access');
         Y.io('ajax.php', callback);
     });
 
index 4d0431b..9e0715c 100644 (file)
@@ -139,15 +139,15 @@ class block_mnet_hosts extends block_list {
 
         if ($hosts) {
             foreach ($hosts as $host) {
-                $icon = $OUTPUT->pix_icon('i/'.$host->application.'_host', get_string('server', 'block_mnet_hosts')) . '&nbsp;';
-
                 if ($host->id == $USER->mnethostid) {
-                    $this->content->items[]="<a title=\"" .s($host->name).
-                        "\" href=\"{$host->wwwroot}\">".$icon. s($host->name) ."</a>";
+                    $url = new \moodle_url($host->wwwroot);
                 } else {
-                    $this->content->items[]="<a title=\"" .s($host->name).
-                        "\" href=\"{$CFG->wwwroot}/auth/mnet/jump.php?hostid={$host->id}\">" .$icon. s($host->name) ."</a>";
+                    $url = new \moodle_url('/auth/mnet/jump.php', array('hostid' => $host->id));
                 }
+                $this->content->items[] = html_writer::tag('a',
+                    $OUTPUT->pix_icon("i/{$host->application}_host", get_string('server', 'block_mnet_hosts')) . s($host->name),
+                    array('href' => $url->out(), 'title' => s($host->name))
+                );
             }
         }
 
index f0300b1..db4a1a3 100644 (file)
Binary files a/calendar/amd/build/calendar_threemonth.min.js and b/calendar/amd/build/calendar_threemonth.min.js differ
index 2c8f751..6b1db42 100644 (file)
Binary files a/calendar/amd/build/repository.min.js and b/calendar/amd/build/repository.min.js differ
index 7a451fb..dd946d4 100644 (file)
Binary files a/calendar/amd/build/view_manager.min.js and b/calendar/amd/build/view_manager.min.js differ
index 3743d4b..4d9f278 100644 (file)
@@ -71,6 +71,7 @@ function(
             var placeHolder = $('<span>');
             placeHolder.attr('data-template', 'core_calendar/threemonth_month');
             placeHolder.attr('data-includenavigation', false);
+            placeHolder.attr('data-mini', true);
             var placeHolderContainer = $('<div>');
             placeHolderContainer.hide();
             placeHolderContainer.append(placeHolder);
index e522bb2..049f819 100644 (file)
@@ -95,9 +95,10 @@ define(['jquery', 'core/ajax'], function($, Ajax) {
      * @param {Number} courseid The course id.
      * @param {Number} categoryid The category id.
      * @param {Bool} includenavigation Whether to include navigation.
+     * @param {Bool} mini Whether the month is in mini view.
      * @return {promise} Resolved with the month view data.
      */
-    var getCalendarMonthData = function(year, month, courseid, categoryid, includenavigation) {
+    var getCalendarMonthData = function(year, month, courseid, categoryid, includenavigation, mini) {
         var request = {
             methodname: 'core_calendar_get_calendar_monthly_view',
             args: {
@@ -106,6 +107,7 @@ define(['jquery', 'core/ajax'], function($, Ajax) {
                 courseid: courseid,
                 categoryid: categoryid,
                 includenavigation: includenavigation,
+                mini: mini
             }
         };
 
index 299aece..2c59a72 100644 (file)
@@ -121,7 +121,8 @@ define([
 
             M.util.js_pending([root.get('id'), year, month, courseid].join('-'));
             var includenavigation = root.data('includenavigation');
-            return CalendarRepository.getCalendarMonthData(year, month, courseid, categoryid, includenavigation)
+            var mini = root.data('mini');
+            return CalendarRepository.getCalendarMonthData(year, month, courseid, categoryid, includenavigation, mini)
                 .then(function(context) {
                     return Templates.render(root.attr('data-template'), context);
                 })
index 5869c10..5e0b0cd 100644 (file)
@@ -154,9 +154,11 @@ class calendar_day_exporter extends exporter {
         $return['events'] = array_map(function($event) use ($cache, $output, $url) {
             $context = $cache->get_context($event);
             $course = $cache->get_course($event);
+            $moduleinstance = $cache->get_module_instance($event);
             $exporter = new calendar_event_exporter($event, [
                 'context' => $context,
                 'course' => $course,
+                'moduleinstance' => $moduleinstance,
                 'daylink' => $url,
                 'type' => $this->related['type'],
                 'today' => $this->calendar->time,
index ad97c96..bd1d751 100644 (file)
@@ -93,6 +93,8 @@ class calendar_event_exporter extends event_exporter_base {
 
         $values = parent::get_other_values($output);
         $event = $this->event;
+        $course = $this->related['course'];
+        $hascourse = !empty($course);
 
         // By default all events that can be edited are
         // draggable.
@@ -109,12 +111,9 @@ class calendar_event_exporter extends event_exporter_base {
             $values['editurl'] = $editurl->out(false);
         } else if ($event->get_type() == 'category') {
             $url = $event->get_category()->get_proxied_instance()->get_view_link();
-        } else if ($event->get_type() == 'course') {
-            $url = course_get_url($event->get_course()->get('id') ?: SITEID);
         } else {
             // TODO MDL-58866 We do not have any way to find urls for events outside of course modules.
-            $course = $event->get_course()->get('id') ?: SITEID;
-            $url = course_get_url($course);
+            $url = course_get_url($hascourse ? $course : SITEID);
         }
 
         $values['url'] = $url->out(false);
@@ -165,13 +164,10 @@ class calendar_event_exporter extends event_exporter_base {
         }
 
         // Include course's shortname into the event name, if applicable.
-        $course = $this->event->get_course();
-        if ($course && $course->get('id') && $course->get('id') !== SITEID) {
+        if ($hascourse && $course->id !== SITEID) {
             $eventnameparams = (object) [
                 'name' => $values['popupname'],
-                'course' => format_string($course->get('shortname'), true, [
-                        'context' => $this->related['context'],
-                    ])
+                'course' => $values['course']->shortname,
             ];
             $values['popupname'] = get_string('eventnameandcourse', 'calendar', $eventnameparams);
         }
@@ -195,6 +191,7 @@ class calendar_event_exporter extends event_exporter_base {
         $related['daylink'] = \moodle_url::class;
         $related['type'] = '\core_calendar\type_base';
         $related['today'] = 'int';
+        $related['moduleinstance'] = 'stdClass?';
 
         return $related;
     }
@@ -221,14 +218,11 @@ class calendar_event_exporter extends event_exporter_base {
      * @return array
      */
     protected function get_module_timestamp_limits($event) {
-        global $DB;
-
         $values = [];
         $mapper = container::get_event_mapper();
         $starttime = $event->get_times()->get_start_time();
         $modname = $event->get_course_module()->get('modname');
-        $modid = $event->get_course_module()->get('instance');
-        $moduleinstance = $DB->get_record($modname, ['id' => $modid]);
+        $moduleinstance = $this->related['moduleinstance'];
 
         list($min, $max) = component_callback(
             'mod_' . $modname,
index aead6d1..ab05aef 100644 (file)
@@ -113,9 +113,11 @@ class calendar_upcoming_exporter extends exporter {
         $return['events'] = array_map(function($event) use ($cache, $output, $url) {
             $context = $cache->get_context($event);
             $course = $cache->get_course($event);
+            $moduleinstance = $cache->get_module_instance($event);
             $exporter = new calendar_event_exporter($event, [
                 'context' => $context,
                 'course' => $course,
+                'moduleinstance' => $moduleinstance,
                 'daylink' => $url,
                 'type' => $this->related['type'],
                 'today' => $this->calendar->time,
index 3920107..7e888b8 100644 (file)
@@ -188,9 +188,11 @@ class day_exporter extends exporter {
         $eventexporters = array_map(function($event) use ($cache, $output) {
             $context = $cache->get_context($event);
             $course = $cache->get_course($event);
+            $moduleinstance = $cache->get_module_instance($event);
             $exporter = new calendar_event_exporter($event, [
                 'context' => $context,
                 'course' => $course,
+                'moduleinstance' => $moduleinstance,
                 'daylink' => $this->url,
                 'type' => $this->related['type'],
                 'today' => $this->data[0],
index 0f17777..a6bef22 100644 (file)
@@ -243,6 +243,7 @@ class event_exporter_base extends exporter {
         $event = $this->event;
         $legacyevent = container::get_event_mapper()->from_event_to_legacy_event($event);
         $context = $this->related['context'];
+        $course = $this->related['course'];
         $values['isactionevent'] = false;
         $values['iscourseevent'] = false;
         $values['iscategoryevent'] = false;
@@ -268,10 +269,11 @@ class event_exporter_base extends exporter {
             $values['category'] = $categorysummaryexporter->export($output);
         }
 
-        if ($course = $this->related['course']) {
+        if ($course) {
             $coursesummaryexporter = new course_summary_exporter($course, ['context' => $context]);
             $values['course'] = $coursesummaryexporter->export($output);
         }
+
         $courseid = (!$course) ? SITEID : $course->id;
 
         $values['canedit'] = calendar_edit_event_allowed($legacyevent, true);
@@ -290,15 +292,11 @@ class event_exporter_base extends exporter {
         $values['formattedtime'] = calendar_format_event_time($legacyevent, time(), null, false,
                 $timesort);
 
-        if ($course = $this->related['course']) {
-            $coursesummaryexporter = new course_summary_exporter($course, ['context' => $context]);
-            $values['course'] = $coursesummaryexporter->export($output);
-        }
-
         if ($group = $event->get_group()) {
             $values['groupname'] = format_string($group->get('name'), true,
                 ['context' => \context_course::instance($event->get_course()->get('id'))]);
         }
+
         return $values;
     }
 
index b19fd60..e845b64 100644 (file)
@@ -54,15 +54,20 @@ class events_related_objects_cache {
     protected $courses = null;
 
     /**
-     * @var array $events The related groups.
+     * @var array $groups The related groups.
      */
     protected $groups = null;
 
     /**
-     * @var array $events The related course modules.
+     * @var array $coursemodules The related course modules.
      */
     protected $coursemodules = [];
 
+    /**
+     * @var array $moduleinstances The related module instances.
+     */
+    protected $moduleinstances = null;
+
     /**
      * Constructor.
      *
@@ -170,6 +175,33 @@ class events_related_objects_cache {
         return $this->coursemodules[$key];
     }
 
+    /**
+     * Get the related module instance for a given event.
+     *
+     * @param event_interface $event The event object.
+     * @return stdClass|null
+     */
+    public function get_module_instance(event_interface $event) {
+        if (!$event->get_course_module()) {
+            return null;
+        }
+
+        if (is_null($this->moduleinstances)) {
+            $this->load_module_instances();
+        }
+
+        $id = $event->get_course_module()->get('instance');
+        $name = $event->get_course_module()->get('modname');
+
+        if (isset($this->moduleinstances[$name])) {
+            if (isset($this->moduleinstances[$name][$id])) {
+                return $this->moduleinstances[$name][$id];
+            }
+        }
+
+        return null;
+    }
+
     /**
      * Load the list of all of the distinct courses required for the
      * list of provided events and save the result in memory.
@@ -221,4 +253,35 @@ class events_related_objects_cache {
 
         $this->groups = $DB->get_records_sql($sql, $params);
     }
+
+    /**
+     * Load the list of all of the distinct module instances required for the
+     * list of provided events and save the result in memory.
+     */
+    protected function load_module_instances() {
+        global $DB;
+
+        $this->moduleinstances = [];
+        $modulestoload = [];
+        foreach ($this->events as $event) {
+            if ($module = $event->get_course_module()) {
+                $id = $module->get('instance');
+                $name = $module->get('modname');
+
+                $ids = isset($modulestoload[$name]) ? $modulestoload[$name] : [];
+                $ids[$id] = true;
+                $modulestoload[$name] = $ids;
+            }
+        }
+
+        if (empty($modulestoload)) {
+            return;
+        }
+
+        foreach ($modulestoload as $modulename => $ids) {
+            list($idsql, $params) = $DB->get_in_or_equal(array_keys($ids));
+            $sql = "SELECT * FROM {" . $modulename . "} WHERE id {$idsql}";
+            $this->moduleinstances[$modulename] = $DB->get_records_sql($sql, $params);
+        }
+    }
 }
index 08acf56..d3d4c73 100644 (file)
@@ -64,6 +64,11 @@ class month_exporter extends exporter {
      */
     protected $initialeventsloaded = true;
 
+    /**
+     * @var bool $showcoursefilter Whether to render the course filter selector as well.
+     */
+    protected $showcoursefilter = false;
+
     /**
      * Constructor for month_exporter.
      *
@@ -120,6 +125,7 @@ class month_exporter extends exporter {
             ],
             'filter_selector' => [
                 'type' => PARAM_RAW,
+                'optional' => true,
             ],
             'weeks' => [
                 'type' => week_exporter::read_properties_definition(),
@@ -206,7 +212,6 @@ class month_exporter extends exporter {
 
         $return = [
             'courseid' => $this->calendar->courseid,
-            'filter_selector' => $this->get_course_filter_selector($output),
             'weeks' => $this->get_weeks($output),
             'daynames' => $this->get_day_names($output),
             'view' => 'month',
@@ -224,6 +229,10 @@ class month_exporter extends exporter {
             'initialeventsloaded' => $this->initialeventsloaded,
         ];
 
+        if ($this->showcoursefilter) {
+            $return['filter_selector'] = $this->get_course_filter_selector($output);
+        }
+
         if ($context = $this->get_default_add_context()) {
             $return['defaulteventcontext'] = $context->id;
         }
@@ -405,6 +414,18 @@ class month_exporter extends exporter {
         return $this;
     }
 
+    /**
+     * Set whether the course filter selector should be shown.
+     *
+     * @param   bool    $show
+     * @return  $this
+     */
+    public function set_showcoursefilter(bool $show) {
+        $this->showcoursefilter = $show;
+
+        return $this;
+    }
+
     /**
      * Get the default context for use when adding a new event.
      *
index 951b84e..98ad278 100644 (file)
@@ -38,6 +38,21 @@ use moodle_url;
  */
 class week_day_exporter extends day_exporter {
 
+    /**
+     * Constructor.
+     *
+     * @param \calendar_information $calendar The calendar information for the period being displayed
+     * @param mixed $data Either an stdClass or an array of values.
+     * @param array $related Related objects.
+     */
+    public function __construct(\calendar_information $calendar, $data, $related) {
+        parent::__construct($calendar, $data, $related);
+        // Fix the url for today to be based on the today timestamp
+        // rather than the calendar_information time set in the parent
+        // constructor.
+        $this->url->param('time', $this->data[0]);
+    }
+
     /**
      * Return the list of properties.
      *
@@ -83,72 +98,12 @@ class week_day_exporter extends day_exporter {
      * @return array Keys are the property names, values are their values.
      */
     protected function get_other_values(renderer_base $output) {
-        $timestamp = $this->data[0];
-        // Need to account for user's timezone.
-        $usernow = usergetdate(time());
-        $today = new \DateTimeImmutable();
-        // The start time should use the day's date but the current
-        // time of the day (adjusted for user's timezone).
-        $neweventstarttime = $today->setTimestamp($timestamp)->setTime(
-            $usernow['hours'],
-            $usernow['minutes'],
-            $usernow['seconds']
-        );
-
         $return = parent::get_other_values($output);
 
-        $url = new moodle_url('/calendar/view.php', [
-                'view' => 'day',
-                'time' => $timestamp,
-            ]);
-
-        if ($this->calendar->course && SITEID !== $this->calendar->course->id) {
-            $url->param('course', $this->calendar->course->id);
-        } else if ($this->calendar->categoryid) {
-            $url->param('category', $this->calendar->categoryid);
-        }
-
-        $return['viewdaylink'] = $url->out(false);
-
-        if ($popovertitle = $this->get_popover_title()) {
-            $return['popovertitle'] = $popovertitle;
-        }
-        $cache = $this->related['cache'];
-        $eventexporters = array_map(function($event) use ($cache, $output, $url) {
-            $context = $cache->get_context($event);
-            $course = $cache->get_course($event);
-            $exporter = new calendar_event_exporter($event, [
-                'context' => $context,
-                'course' => $course,
-                'daylink' => $url,
-                'type' => $this->related['type'],
-                'today' => $this->data[0],
-            ]);
-
-            return $exporter;
-        }, $this->related['events']);
-
-        $return['events'] = array_map(function($exporter) use ($output) {
-            return $exporter->export($output);
-        }, $eventexporters);
-
         if ($popovertitle = $this->get_popover_title()) {
             $return['popovertitle'] = $popovertitle;
         }
 
-        $return['calendareventtypes'] = array_map(function($exporter) {
-            return $exporter->get_calendar_event_type();
-        }, $eventexporters);
-        $return['calendareventtypes'] = array_values(array_unique($return['calendareventtypes']));
-
-        $return['haslastdayofevent'] = false;
-        foreach ($return['events'] as $event) {
-            if ($event->islastday) {
-                $return['haslastdayofevent'] = true;
-                break;
-            }
-        }
-
         return $return;
     }
 
index 398304e..597eb70 100644 (file)
@@ -151,11 +151,16 @@ class container {
                     // 2) Only process modules for courses a user has the capability to view OR they are enrolled in.
                     // 3) Only process modules for courses that are visible OR if the course is not visible, the user
                     //    has the capability to view hidden courses.
+                    if (!$cm->uservisible) {
+                        return true;
+                    }
+
                     $coursecontext = \context_course::instance($dbrow->courseid);
-                    $canseecourse = has_capability('moodle/course:view', $coursecontext) || is_enrolled($coursecontext);
-                    $canseecourse = $canseecourse &&
-                        ($cm->get_course()->visible || has_capability('moodle/course:viewhiddencourses', $coursecontext));
-                    if (!$cm->uservisible || !$canseecourse) {
+                    if (!$cm->get_course()->visible && !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
+                        return true;
+                    }
+
+                    if (!has_capability('moodle/course:view', $coursecontext) && !is_enrolled($coursecontext)) {
                         return true;
                     }
 
index 7592536..0aeb9a4 100644 (file)
@@ -945,9 +945,10 @@ class core_calendar_external extends external_api {
      * @param   int     $courseid The course to be included
      * @param   int     $categoryid The category to be included
      * @param   bool    $includenavigation Whether to include navigation
+     * @param   bool    $mini Whether to return the mini month view or not
      * @return  array
      */
-    public static function get_calendar_monthly_view($year, $month, $courseid, $categoryid, $includenavigation) {
+    public static function get_calendar_monthly_view($year, $month, $courseid, $categoryid, $includenavigation, $mini) {
         global $CFG, $DB, $USER, $PAGE;
         require_once($CFG->dirroot."/calendar/lib.php");
 
@@ -958,6 +959,7 @@ class core_calendar_external extends external_api {
             'courseid' => $courseid,
             'categoryid' => $categoryid,
             'includenavigation' => $includenavigation,
+            'mini' => $mini,
         ]);
 
         $context = \context_user::instance($USER->id);
@@ -970,7 +972,8 @@ class core_calendar_external extends external_api {
         $calendar = \calendar_information::create($time, $params['courseid'], $params['categoryid']);
         self::validate_context($calendar->context);
 
-        list($data, $template) = calendar_get_view($calendar, 'month', $params['includenavigation']);
+        $view = $params['mini'] ? 'mini' : 'month';
+        list($data, $template) = calendar_get_view($calendar, $view, $params['includenavigation']);
 
         return $data;
     }
@@ -994,6 +997,13 @@ class core_calendar_external extends external_api {
                     true,
                     NULL_ALLOWED
                 ),
+                'mini' => new external_value(
+                    PARAM_BOOL,
+                    'Whether to return the mini month view or not',
+                    VALUE_DEFAULT,
+                    false,
+                    NULL_ALLOWED
+                ),
             ]
         );
     }
index beac35f..6be5dc5 100644 (file)
@@ -1819,6 +1819,13 @@ function calendar_time_representation($time) {
         $timeformat = get_config(null, 'calendar_site_timeformat');
     }
 
+    // Allow language customization of selected time format.
+    if ($timeformat === CALENDAR_TF_12) {
+        $timeformat = get_string('strftimetime12', 'langconfig');
+    } else if ($timeformat === CALENDAR_TF_24) {
+        $timeformat = get_string('strftimetime24', 'langconfig');
+    }
+
     return userdate($time, empty($timeformat) ? $langtimeformat : $timeformat);
 }
 
@@ -3483,6 +3490,7 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig
         $month = new \core_calendar\external\month_exporter($calendar, $type, $related);
         $month->set_includenavigation($includenavigation);
         $month->set_initialeventsloaded(!$skipevents);
+        $month->set_showcoursefilter($view == "month");
         $data = $month->export($renderer);
     } else if ($view == "day") {
         $day = new \core_calendar\external\calendar_day_exporter($calendar, $related);
index 610ba2d..e451b2f 100644 (file)
@@ -243,19 +243,41 @@ class core_calendar_renderer extends plugin_renderer_base {
      * @return string
      */
     public function course_filter_selector(moodle_url $returnurl, $label = null, $courseid = null) {
-        global $CFG;
+        global $CFG, $DB;
 
         if (!isloggedin() or isguestuser()) {
             return '';
         }
 
+        $contextrecords = [];
         $courses = calendar_get_default_courses($courseid, 'id, shortname');
 
+        if (!empty($courses) && count($courses) > CONTEXT_CACHE_MAX_SIZE) {
+            // We need to pull the context records from the DB to preload them
+            // below. The calendar_get_default_courses code will actually preload
+            // the contexts itself however the context cache is capped to a certain
+            // amount before it starts recycling. Unfortunately that starts to happen
+            // quite a bit if a user has access to a large number of courses (e.g. admin).
+            // So in order to avoid hitting the DB for each context as we loop below we
+            // can load all of the context records and add them to the cache just in time.
+            $courseids = array_map(function($c) {
+                return $c->id;
+            }, $courses);
+            list($insql, $params) = $DB->get_in_or_equal($courseids);
+            $contextsql = "SELECT ctx.instanceid, " . context_helper::get_preload_record_columns_sql('ctx') .
+                          " FROM {context} ctx WHERE ctx.contextlevel = ? AND ctx.instanceid $insql";
+            array_unshift($params, CONTEXT_COURSE);
+            $contextrecords = $DB->get_records_sql($contextsql, $params);
+        }
+
         unset($courses[SITEID]);
 
         $courseoptions = array();
         $courseoptions[SITEID] = get_string('fulllistofcourses');
         foreach ($courses as $course) {
+            if (isset($contextrecords[$course->id])) {
+                context_helper::preload_from_record($contextrecords[$course->id]);
+            }
             $coursecontext = context_course::instance($course->id);
             $courseoptions[$course->id] = format_string($course->shortname, true, array('context' => $coursecontext));
         }
index e7929f8..53b6943 100644 (file)
@@ -35,6 +35,7 @@
     }} id="calendar-month-{{date.year}}-{{date.month}}-{{uniqid}}" {{!
     }} data-template="core_calendar/month_mini" {{!
     }} data-includenavigation="{{#includenavigation}}true{{/includenavigation}}{{^includenavigation}}false{{/includenavigation}}"{{!
+    }} data-mini="true"{{!
     }}>
     {{> core_calendar/month_mini}}
 </div>
index 5ae3edb..f5a7b9b 100644 (file)
@@ -26,6 +26,9 @@ defined('MOODLE_INTERNAL') || die();
 
 use core_calendar\external\calendar_event_exporter;
 use core_calendar\local\event\container;
+use core_calendar\type_factory;
+
+require_once(__DIR__ . '/helpers.php');
 
 /**
  * Calendar event exporter testcase.
@@ -147,4 +150,162 @@ class core_calendar_event_exporter_testcase extends advanced_testcase {
         $this->assertEquals($expected, $result['maxdaytimestamp']);
         $this->assertEquals($max[1], $result['maxdayerror']);
     }
+
+    /**
+     * Exporting a course event should generate the course URL.
+     */
+    public function test_calendar_event_exporter_course_url_course_event() {
+        global $CFG, $PAGE;
+        require_once($CFG->dirroot . '/course/lib.php');
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $now = time();
+        $mapper = container::get_event_mapper();
+        $legacyevent = create_event([
+            'courseid' => $course->id,
+            'userid' => 1,
+            'eventtype' => 'course',
+            'timestart' => $now
+        ]);
+        $event = $mapper->from_legacy_event_to_event($legacyevent);
+        $exporter = new calendar_event_exporter($event, [
+            'context' => $context,
+            'course' => $course,
+            'moduleinstance' => null,
+            'daylink' => new moodle_url(''),
+            'type' => type_factory::get_calendar_instance(),
+            'today' => $now
+        ]);
+
+        $courseurl = course_get_url($course->id);
+        $expected = $courseurl->out(false);
+        $renderer = $PAGE->get_renderer('core_calendar');
+        $exportedevent = $exporter->export($renderer);
+
+        // The exported URL should be for the course.
+        $this->assertEquals($expected, $exportedevent->url);
+    }
+
+    /**
+     * Exporting a user event should generate the site course URL.
+     */
+    public function test_calendar_event_exporter_course_url_user_event() {
+        global $CFG, $PAGE;
+        require_once($CFG->dirroot . '/course/lib.php');
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $context = context_user::instance($user->id);
+        $now = time();
+        $mapper = container::get_event_mapper();
+        $legacyevent = create_event([
+            'courseid' => 0,
+            'userid' => $user->id,
+            'eventtype' => 'user',
+            'timestart' => $now
+        ]);
+        $event = $mapper->from_legacy_event_to_event($legacyevent);
+        $exporter = new calendar_event_exporter($event, [
+            'context' => $context,
+            'course' => null,
+            'moduleinstance' => null,
+            'daylink' => new moodle_url(''),
+            'type' => type_factory::get_calendar_instance(),
+            'today' => $now
+        ]);
+
+        $courseurl = course_get_url(SITEID);
+        $expected = $courseurl->out(false);
+        $renderer = $PAGE->get_renderer('core_calendar');
+        $exportedevent = $exporter->export($renderer);
+
+        // The exported URL should be for the site course.
+        $this->assertEquals($expected, $exportedevent->url);
+    }
+
+    /**
+     * Popup name respects filters for course shortname.
+     */
+    public function test_calendar_event_exporter_popupname_course_shortname_strips_links() {
+        global $CFG, $PAGE;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $rawshortname = 'Shortname <a href="#">link</a>';
+        $nolinkshortname = strip_links($rawshortname);
+        $course = $generator->create_course(['shortname' => $rawshortname]);
+        $coursecontext = context_course::instance($course->id);
+        $now = time();
+        $mapper = container::get_event_mapper();
+        $renderer = $PAGE->get_renderer('core_calendar');
+        $legacyevent = create_event([
+            'courseid' => $course->id,
+            'userid' => 1,
+            'eventtype' => 'course',
+            'timestart' => $now
+        ]);
+        $event = $mapper->from_legacy_event_to_event($legacyevent);
+        $exporter = new calendar_event_exporter($event, [
+            'context' => $coursecontext,
+            'course' => $course,
+            'moduleinstance' => null,
+            'daylink' => new moodle_url(''),
+            'type' => type_factory::get_calendar_instance(),
+            'today' => $now
+        ]);
+
+        $exportedevent = $exporter->export($renderer);
+        // Links should always be stripped from the course short name.
+        $this->assertRegExp("/$nolinkshortname/", $exportedevent->popupname);
+    }
+
+    /**
+     * Exported event contains the exported course.
+     */
+    public function test_calendar_event_exporter_exports_course() {
+        global $CFG, $PAGE;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $rawshortname = 'Shortname <a href="#">link</a>';
+        $nolinkshortname = strip_links($rawshortname);
+        $course = $generator->create_course(['shortname' => $rawshortname]);
+        $coursecontext = context_course::instance($course->id);
+        $now = time();
+        $mapper = container::get_event_mapper();
+        $renderer = $PAGE->get_renderer('core_calendar');
+        $legacyevent = create_event([
+            'courseid' => $course->id,
+            'userid' => 1,
+            'eventtype' => 'course',
+            'timestart' => $now
+        ]);
+        $event = $mapper->from_legacy_event_to_event($legacyevent);
+        $exporter = new calendar_event_exporter($event, [
+            'context' => $coursecontext,
+            'course' => $course,
+            'moduleinstance' => null,
+            'daylink' => new moodle_url(''),
+            'type' => type_factory::get_calendar_instance(),
+            'today' => $now
+        ]);
+
+        $exportedevent = $exporter->export($renderer);
+        $courseexporter = new \core_course\external\course_summary_exporter($course, [
+            'context' => $coursecontext
+        ]);
+        $exportedcourse = $courseexporter->export($renderer);
+        $this->assertEquals($exportedevent->course, $exportedcourse);
+    }
 }
diff --git a/calendar/tests/events_related_objects_cache_test.php b/calendar/tests/events_related_objects_cache_test.php
new file mode 100644 (file)
index 0000000..f0799db
--- /dev/null
@@ -0,0 +1,140 @@
+<?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/>.
+
+/**
+ * Tests for the events_related_objects_cache.
+ *
+ * @package    core_calendar
+ * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/helpers.php');
+
+use \core_calendar\external\events_related_objects_cache;
+use \core_calendar\local\event\container;
+
+/**
+ * Tests for the events_related_objects_cache.
+ *
+ * @package    core_calendar
+ * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_calendar_events_related_objects_cache_testcase extends advanced_testcase {
+
+    /**
+     * Tests set up
+     */
+    protected function setUp() {
+        $this->resetAfterTest();
+    }
+
+    /**
+     * An event with no module should return null when trying to retrieve
+     * the module instance.
+     */
+    public function test_get_module_instance_no_module() {
+        $this->setAdminUser();
+        $mapper = container::get_event_mapper();
+        $legacyevent = create_event([
+            'modulename' => '',
+            'instance' => 0
+        ]);
+        $event = $mapper->from_legacy_event_to_event($legacyevent);
+        $cache = new events_related_objects_cache([$event]);
+
+        $this->assertNull($cache->get_module_instance($event));
+    }
+
+    /**
+     * The get_module_instance should return the correct module instances
+     * for the given set of events in the cache.
+     */
+    public function test_get_module_instance_with_modules() {
+        $this->setAdminUser();
+        $mapper = container::get_event_mapper();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $plugingenerator = $generator->get_plugin_generator('mod_assign');
+        $instance1 = $plugingenerator->create_instance(['course' => $course->id]);
+        $instance2 = $plugingenerator->create_instance(['course' => $course->id]);
+        unset($instance1->cmid);
+        unset($instance2->cmid);
+
+        $params = [
+            'type' => CALENDAR_EVENT_TYPE_ACTION,
+            'courseid' => $course->id,
+            'modulename' => 'assign',
+            'userid' => 0,
+            'eventtype' => 'due',
+            'repeats' => 0,
+            'timestart' => 1,
+        ];
+
+        $legacyevent1 = create_event(array_merge($params, ['name' => 'Event 1', 'instance' => $instance1->id]));
+        $legacyevent2 = create_event(array_merge($params, ['name' => 'Event 2', 'instance' => $instance1->id]));
+        $legacyevent3 = create_event(array_merge($params, ['name' => 'Event 3', 'instance' => $instance2->id]));
+        $event1 = $mapper->from_legacy_event_to_event($legacyevent1);
+        $event2 = $mapper->from_legacy_event_to_event($legacyevent2);
+        $event3 = $mapper->from_legacy_event_to_event($legacyevent3);
+        $cache = new events_related_objects_cache([$event1, $event2, $event3]);
+
+        $eventinstance1 = $cache->get_module_instance($event1);
+        $eventinstance2 = $cache->get_module_instance($event2);
+        $eventinstance3 = $cache->get_module_instance($event3);
+
+        $this->assertEquals($instance1, $eventinstance1);
+        $this->assertEquals($instance1, $eventinstance2);
+        $this->assertEquals($instance2, $eventinstance3);
+    }
+
+    /**
+     * Trying to load the course module of an event that isn't in
+     * the cache should return null.
+     */
+    public function test_module_instance_unknown_event() {
+        $this->setAdminUser();
+        $mapper = container::get_event_mapper();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $plugingenerator = $generator->get_plugin_generator('mod_assign');
+        $instance1 = $plugingenerator->create_instance(['course' => $course->id]);
+        $instance2 = $plugingenerator->create_instance(['course' => $course->id]);
+        unset($instance1->cmid);
+        unset($instance2->cmid);
+
+        $params = [
+            'type' => CALENDAR_EVENT_TYPE_ACTION,
+            'courseid' => $course->id,
+            'modulename' => 'assign',
+            'userid' => 0,
+            'eventtype' => 'due',
+            'repeats' => 0,
+            'timestart' => 1,
+        ];
+
+        $legacyevent1 = create_event(array_merge($params, ['name' => 'Event 1', 'instance' => $instance1->id]));
+        $legacyevent2 = create_event(array_merge($params, ['name' => 'Event 2', 'instance' => $instance2->id]));
+        $event1 = $mapper->from_legacy_event_to_event($legacyevent1);
+        $event2 = $mapper->from_legacy_event_to_event($legacyevent2);
+        $cache = new events_related_objects_cache([$event1]);
+
+        $this->assertNull($cache->get_module_instance($event2));
+    }
+}
index 178bfff..42e3b0e 100644 (file)
@@ -2294,7 +2294,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         $data = external_api::clean_returnvalue(
             core_calendar_external::get_calendar_monthly_view_returns(),
             core_calendar_external::get_calendar_monthly_view($timestart->format('Y'), $timestart->format('n'),
-                                                              $course->id, null, false)
+                                                              $course->id, null, false, true)
         );
         $this->assertEquals($data['courseid'], $course->id);
         // User enrolled in the course can load the course calendar.
@@ -2302,7 +2302,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         $data = external_api::clean_returnvalue(
             core_calendar_external::get_calendar_monthly_view_returns(),
             core_calendar_external::get_calendar_monthly_view($timestart->format('Y'), $timestart->format('n'),
-                                                              $course->id, null, false)
+                                                              $course->id, null, false, true)
         );
         $this->assertEquals($data['courseid'], $course->id);
         // User not enrolled in the course cannot load the course calendar.
@@ -2311,7 +2311,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         $data = external_api::clean_returnvalue(
             core_calendar_external::get_calendar_monthly_view_returns(),
             core_calendar_external::get_calendar_monthly_view($timestart->format('Y'), $timestart->format('n'),
-                                                              $course->id, null, false)
+                                                              $course->id, null, false, false)
         );
     }
 
index 676d712..505c789 100644 (file)
@@ -220,7 +220,7 @@ abstract class core_completion_edit_base_form extends moodleform {
         }
 
         // Completion expected at particular date? (For progress tracking).
-        $mform->addElement('date_selector', 'completionexpected',
+        $mform->addElement('date_time_selector', 'completionexpected',
             get_string('completionexpected', 'completion'), ['optional' => true]);
         $mform->addHelpButton('completionexpected', 'completionexpected', 'completion');
         $mform->disabledIf('completionexpected', 'completion', 'eq', COMPLETION_TRACKING_NONE);
index 7c60ee0..3238f05 100644 (file)
@@ -410,12 +410,12 @@ M.course_dndupload = {
      * @return false to prevent the event from continuing to be processed
      */
     drop: function(e) {
+        this.hide_preview_element();
+
         if (!(type = this.check_drag(e))) {
             return false;
         }
 
-        this.hide_preview_element();
-
         // Work out the number of the section we are on (from its id)
         var section = this.get_section(e.currentTarget);
         var sectionnumber = this.get_section_number(section);
@@ -792,16 +792,42 @@ M.course_dndupload = {
 
         // Prepare the data to send
         var formData = new FormData();
-        formData.append('repo_upload_file', file);
+        try {
+            formData.append('repo_upload_file', file);
+        } catch (e) {
+            // Edge throws an error at this point if we try to upload a folder.
+            resel.parent.removeChild(resel.li);
+            new M.core.alert({message: M.util.get_string('filereaderror', 'moodle', file.name)});
+            return;
+        }
         formData.append('sesskey', M.cfg.sesskey);
         formData.append('course', this.courseid);
         formData.append('section', sectionnumber);
         formData.append('module', module);
         formData.append('type', 'Files');
 
-        // Send the AJAX call
-        xhr.open("POST", this.url, true);
-        xhr.send(formData);
+        // Try reading the file to check it is not a folder, before sending it to the server.
+        var reader = new FileReader();
+        reader.onload = function() {
+            // File was read OK - send it to the server.
+            xhr.open("POST", self.url, true);
+            xhr.send(formData);
+        };
+        reader.onerror = function() {
+            // Unable to read the file (it is probably a folder) - display an error message.
+            resel.parent.removeChild(resel.li);
+            new M.core.alert({message: M.util.get_string('filereaderror', 'moodle', file.name)});
+        };
+        if (file.size > 0) {
+            // If this is a non-empty file, try reading the first few bytes.
+            // This will trigger reader.onerror() for folders and reader.onload() for ordinary, readable files.
+            reader.readAsText(file.slice(0, 5));
+        } else {
+            // If you call slice() on a 0-byte folder, before calling readAsText, then Firefox triggers reader.onload(),
+            // instead of reader.onerror().
+            // So, for 0-byte files, just call readAsText on the whole file (and it will trigger load/error functions as expected).
+            reader.readAsText(file);
+        }
     },
 
     /**
index 73ecfc8..1e12105 100644 (file)
@@ -64,6 +64,7 @@ function dndupload_add_to_course($course, $modnames) {
             array('namedfiletoolarge', 'moodle'),
             array('actionchoice', 'moodle'),
             array('servererror', 'moodle'),
+            array('filereaderror', 'moodle'),
             array('upload', 'moodle'),
             array('cancel', 'moodle')
         ),
index 47c8c18..4ffe172 100644 (file)
@@ -120,12 +120,13 @@ class course_edit_form extends moodleform {
                 $mform->setConstant('visible', $courseconfig->visible);
             }
         }
-
-        $mform->addElement('date_selector', 'startdate', get_string('startdate'));
+        $mform->addElement('date_time_selector', 'startdate', get_string('startdate'));
         $mform->addHelpButton('startdate', 'startdate');
-        $mform->setDefault('startdate', time() + 3600 * 24);
+        $date = (new DateTime())->setTimestamp(usergetmidnight(time()));
+        $date->modify('+1 day');
+        $mform->setDefault('startdate', $date->getTimestamp());
 
-        $mform->addElement('date_selector', 'enddate', get_string('enddate'), array('optional' => true));
+        $mform->addElement('date_time_selector', 'enddate', get_string('enddate'), array('optional' => true));
         $mform->addHelpButton('enddate', 'enddate');
 
         $mform->addElement('text','idnumber', get_string('idnumbercourse'),'maxlength="100"  size="10"');
index d0ad4f0..b2d0b1d 100644 (file)
@@ -3017,6 +3017,10 @@ class course_request {
         $data->lang               = $courseconfig->lang;
         $data->enablecompletion   = $courseconfig->enablecompletion;
         $data->numsections        = $courseconfig->numsections;
+        $data->startdate          = usergetmidnight(time());
+        if ($courseconfig->courseenddateenabled) {
+            $data->enddate        = usergetmidnight(time()) + $courseconfig->courseduration;
+        }
 
         $course = create_course($data);
         $context = context_course::instance($course->id, MUST_EXIST);
index 06cc2b1..024f159 100644 (file)
@@ -710,7 +710,8 @@ abstract class moodleform_mod extends moodleform {
             }
 
             // Completion expected at particular date? (For progress tracking)
-            $mform->addElement('date_selector', 'completionexpected', get_string('completionexpected', 'completion'), array('optional'=>true));
+            $mform->addElement('date_time_selector', 'completionexpected', get_string('completionexpected', 'completion'),
+                    array('optional' => true));
             $mform->addHelpButton('completionexpected', 'completionexpected', 'completion');
             $mform->disabledIf('completionexpected', 'completion', 'eq', COMPLETION_TRACKING_NONE);
         }
index 6314a16..1accadd 100644 (file)
@@ -41,9 +41,9 @@ class course_reset_form extends moodleform {
 
         $mform->addElement('header', 'generalheader', get_string('general'));
 
-        $mform->addElement('date_selector', 'reset_start_date', get_string('startdate'), array('optional'=>true));
+        $mform->addElement('date_time_selector', 'reset_start_date', get_string('startdate'), array('optional' => true));
         $mform->addHelpButton('reset_start_date', 'startdate');
-        $mform->addElement('date_selector', 'reset_end_date', get_string('enddate'), array('optional' => true));
+        $mform->addElement('date_time_selector', 'reset_end_date', get_string('enddate'), array('optional' => true));
         $mform->addHelpButton('reset_end_date', 'enddate');
         $mform->addElement('checkbox', 'reset_events', get_string('deleteevents', 'calendar'));
         $mform->addElement('checkbox', 'reset_notes', get_string('deletenotes', 'notes'));
index badd2a5..5857ccc 100644 (file)
@@ -25,4 +25,4 @@
 $string['category:config'] = 'Configure category enrol instances';
 $string['category:synchronised'] = 'Role assignments synchronised to course enrolment';
 $string['pluginname'] = 'Category enrolments';
-$string['pluginname_desc'] = 'Category enrolment plugin is a legacy solution for enrolments at the course category level via role assignments. It is recommended to use cohort synchronisation instead.';
+$string['pluginname_desc'] = 'The category enrolments plugin synchronises any role assignments in the category context for roles with the capability enrol/category:synchronised allowed.';
index 1ecb250..c888169 100644 (file)
@@ -783,6 +783,10 @@ class enrol_database_plugin extends enrol_plugin {
                 $template->visible        = $courseconfig->visible;
                 $template->lang           = $courseconfig->lang;
                 $template->groupmodeforce = $courseconfig->groupmodeforce;
+                $template->startdate      = usergetmidnight(time());
+                if ($courseconfig->courseenddateenabled) {
+                    $template->enddate    = usergetmidnight(time()) + $courseconfig->courseduration;
+                }
             }
 
             foreach ($createcourses as $fields) {
index 427c130..0e86534 100644 (file)
Binary files a/enrol/manual/amd/build/quickenrolment.min.js and b/enrol/manual/amd/build/quickenrolment.min.js differ
index 6f359b3..980581a 100644 (file)
@@ -137,7 +137,7 @@ define(['core/templates',
         // This hidden fields are added automatically by mforms and when it reaches the AJAX we get an error.
         var hidden = form.find(SELECTORS.UNWANTEDHIDDENFIELDS);
         hidden.each(function() {
-            this.remove();
+            $(this).remove();
         });
 
         var formData = form.serialize();
index ca774c1..f9339b9 100644 (file)
@@ -600,14 +600,6 @@ function groups_delete_group_members($courseid, $userid=0, $unused=false) {
     }
     $rs->close();
 
-    // TODO MDL-41312 Remove events_trigger_legacy('groups_members_removed').
-    // This event is kept here for backwards compatibility, because it cannot be
-    // translated to a new event as it is wrong.
-    $eventdata = new stdClass();
-    $eventdata->courseid = $courseid;
-    $eventdata->userid   = $userid;
-    events_trigger_legacy('groups_members_removed', $eventdata);
-
     return true;
 }
 
@@ -635,11 +627,6 @@ function groups_delete_groupings_groups($courseid, $showfeedback=false) {
     // Purge the group and grouping cache for users.
     cache_helper::purge_by_definition('core', 'user_group_groupings');
 
-    // TODO MDL-41312 Remove events_trigger_legacy('groups_groupings_groups_removed').
-    // This event is kept here for backwards compatibility, because it cannot be
-    // translated to a new event as it is wrong.
-    events_trigger_legacy('groups_groupings_groups_removed', $courseid);
-
     // no need to show any feedback here - we delete usually first groupings and then groups
 
     return true;
@@ -666,11 +653,6 @@ function groups_delete_groups($courseid, $showfeedback=false) {
     // Purge the group and grouping cache for users.
     cache_helper::purge_by_definition('core', 'user_group_groupings');
 
-    // TODO MDL-41312 Remove events_trigger_legacy('groups_groups_deleted').
-    // This event is kept here for backwards compatibility, because it cannot be
-    // translated to a new event as it is wrong.
-    events_trigger_legacy('groups_groups_deleted', $courseid);
-
     if ($showfeedback) {
         echo $OUTPUT->notification(get_string('deleted').' - '.get_string('groups', 'group'), 'notifysuccess');
     }
@@ -699,11 +681,6 @@ function groups_delete_groupings($courseid, $showfeedback=false) {
     // Purge the group and grouping cache for users.
     cache_helper::purge_by_definition('core', 'user_group_groupings');
 
-    // TODO MDL-41312 Remove events_trigger_legacy('groups_groupings_deleted').
-    // This event is kept here for backwards compatibility, because it cannot be
-    // translated to a new event as it is wrong.
-    events_trigger_legacy('groups_groupings_deleted', $courseid);
-
     if ($showfeedback) {
         echo $OUTPUT->notification(get_string('deleted').' - '.get_string('groupings', 'group'), 'notifysuccess');
     }
index f6970c4..6f164a7 100644 (file)
@@ -81,17 +81,16 @@ class core_iplookup_geoip_testcase extends advanced_testcase {
     }
 
     public function test_ipv4() {
-
-        $result = iplookup_find_location('131.111.150.25');
+        $result = iplookup_find_location('192.30.255.112');
 
         $this->assertEquals('array', gettype($result));
-        $this->assertEquals('Cambridge', $result['city']);
-        $this->assertEquals(0.1167, $result['longitude'], 'Coordinates are out of accepted tolerance', 0.01);
-        $this->assertEquals(52.2, $result['latitude'], 'Coordinates are out of accepted tolerance', 0.01);
+        $this->assertEquals('San Francisco', $result['city']);
+        $this->assertEquals(-122.3933, $result['longitude'], 'Coordinates are out of accepted tolerance', 0.01);
+        $this->assertEquals(37.7697, $result['latitude'], 'Coordinates are out of accepted tolerance', 0.01);
         $this->assertNull($result['error']);
         $this->assertEquals('array', gettype($result['title']));
-        $this->assertEquals('Cambridge', $result['title'][0]);
-        $this->assertEquals('United Kingdom', $result['title'][1]);
+        $this->assertEquals('San Francisco', $result['title'][0]);
+        $this->assertEquals('United States', $result['title'][1]);
     }
 
     public function test_ipv6() {
@@ -110,4 +109,3 @@ class core_iplookup_geoip_testcase extends advanced_testcase {
         $this->assertEquals('United States', $result['title'][1]);
     }
 }
-
index 8c94273..864d944 100644 (file)
@@ -46,17 +46,17 @@ class core_iplookup_geoplugin_testcase extends advanced_testcase {
         $CFG->geoipfile = '';
     }
 
-    public function test_geoip_ipv4() {
-        $result = iplookup_find_location('131.111.150.25');
+    public function test_ipv4() {
+        $result = iplookup_find_location('192.30.255.112');
 
         $this->assertEquals('array', gettype($result));
-        $this->assertEquals('Cambridge', $result['city']);
-        $this->assertEquals(0.1167, $result['longitude'], '', 0.001);
-        $this->assertEquals(52.200000000000003, $result['latitude'], '', 0.001);
+        $this->assertEquals('San Francisco', $result['city']);
+        $this->assertEquals(-122.3933, $result['longitude'], 'Coordinates are out of accepted tolerance', 0.01);
+        $this->assertEquals(37.7697, $result['latitude'], 'Coordinates are out of accepted tolerance', 0.01);
         $this->assertNull($result['error']);
         $this->assertEquals('array', gettype($result['title']));
-        $this->assertEquals('Cambridge', $result['title'][0]);
-        $this->assertEquals('United Kingdom', $result['title'][1]);
+        $this->assertEquals('San Francisco', $result['title'][0]);
+        $this->assertEquals('United States', $result['title'][1]);
     }
 
     public function test_geoip_ipv6() {
index 01ee8b7..7a48f3b 100644 (file)
@@ -197,8 +197,8 @@ $string['configdefaulthomepage'] = 'This determines the home page for logged in
 $string['configdefaultrequestcategory'] = 'Courses requested by users will be automatically placed in this category.';
 $string['configdefaultrequestedcategory'] = 'Default category to put courses that were requested into, if they\'re approved.';
 $string['configdefaultuserroleid'] = 'All logged in users will be given the capabilities of the role you specify here, at the site level, in ADDITION to any other roles they may have been given.  The default is the Authenticated user role.  Note that this will not conflict with other roles they have unless you prohibit capabilities, it just ensures that all users have capabilities that are not assignable at the course level (eg post blog entries, manage own calendar, etc).';
-$string['configdeleteincompleteusers'] = 'After this period, old not fully setup accounts are deleted.';
-$string['configdeleteunconfirmed'] = 'If you are using email authentication, this is the period within which a response will be accepted from users.  After this period, old unconfirmed accounts are deleted.';
+$string['configdeleteincompleteusers'] = 'After this period, any account without the first name, last name or email field filled in is deleted.';
+$string['configdeleteunconfirmed'] = 'For certain authentication methods, such as email-based self-registration, users must confirm their account within a certain time. After this period, any old unconfirmed accounts are deleted.';
 $string['configdenyemailaddresses'] = 'To deny email addresses from particular domains list them here in the same way.  All other domains will be accepted. To deny subdomains add the domain with a preceding \'.\'. eg <strong>hotmail.com yahoo.co.uk .live.com</strong>';
 $string['configenableblogs'] = 'This switch provides all site users with their own blog.';
 $string['configenabledevicedetection'] = 'Enables detection of mobiles, smartphones, tablets or default devices (desktop PCs, laptops, etc) for the application of themes and other features.';
@@ -294,7 +294,7 @@ $string['configpathtodu'] = 'Path to du. Probably something like /usr/bin/du. If
 $string['configperfdebug'] = 'If you turn this on, performance info will be printed in the footer of the standard theme';
 $string['configprofileroles'] = 'List of roles that are visible on user profiles and participation page.';
 $string['configprofilesforenrolledusersonly'] = 'To prevent misuse by spammers, profile descriptions of users who are not yet enrolled in any course are hidden. New users must enrol in at least one course before they can add a profile description.';
-$string['configprotectusernames'] = 'By default forget_password.php does not display any hints that would allow guessing of usernames or email addresses.';
+$string['configprotectusernames'] = 'If enabled, the forgotten password form will not display any hints allowing account usernames or email addresses to be guessed.';
 $string['configproxybypass'] = 'Comma separated list of (partial) hostnames or IPs that should bypass proxy (e.g., 192.168., .mydomain.com)';
 $string['configproxyhost'] = 'If this <b>server</b> needs to use a proxy computer (eg a firewall) to access the Internet, then provide the proxy hostname here.  Otherwise leave it blank.';
 $string['configproxypassword'] = 'Password needed to access internet through proxy if required, empty if none (PHP cURL extension required).';
@@ -836,7 +836,7 @@ $string['pathtopsql'] = 'Path to psql';
 $string['pathtopsqldesc'] = 'This is only necessary to enter if you have more than one psql on your system (for example if you have more than one version of postgresql installed)';
 $string['pathtopsqlinvalid'] = 'Invalid path to psql - either wrong path or not executable';
 $string['pathtopython'] = 'Path to Python';
-$string['pathtopythondesc'] = 'Path to your executable Python binary.';
+$string['pathtopythondesc'] = 'Path to your executable Python binary (both Python 2 and Python 3 are acceptable).';
 $string['pcreunicodewarning'] = 'It is strongly recommended to use PCRE PHP extension that is compatible with Unicode characters.';
 $string['perfdebug'] = 'Performance info';
 $string['performance'] = 'Performance';
@@ -941,7 +941,7 @@ $string['quizattemptsupgradedmessage'] = 'In Moodle 2.1 there was a major upgrad
 $string['recaptchaprivatekey'] = 'ReCAPTCHA secret key';
 $string['recaptchapublickey'] = 'ReCAPTCHA site key';
 $string['register'] = 'Register your site';
-$string['registermoodlenet'] = '<p>We\'d love to stay in touch for important things for your Moodle site!</p><p>By registering,</p><ul><li>You are contributing to our collective knowledge about the users of Moodle which helps us improve Moodle and all our community services.</li><li>You’ll be one of the first to find out about important notifications such as security alerts and new Moodle releases.</li><li>You can access and activate mobile push notifications from your Moodle site through our free <a href="https://download.moodle.org/mobile/">Moodle Mobile app</a>.</li><li>Optionally, your site can be included as a proud member and supporter of the Moodle community on the <a href="https://moodle.net/stats">list of registered sites</a>.</li></ul>';
+$string['registermoodlenet'] = '<p>We\'d love to stay in touch and provide you with important things for your Moodle site!</p><p>By registering:</p><ul><li>You\'ll be one of the first to find out about important notifications such as security alerts and new Moodle releases.</li><li>You can access and activate mobile push notifications from your Moodle site through our free <a href="https://download.moodle.org/mobile/">Moodle Mobile app</a>.</li><li>You are contributing to our <a href="https://moodle.net/stats/">Moodle statistics</a> of the worldwide community, which help us improve Moodle and our community sites.</li><li>If you wish, your site can be included in the <a href="https://moodle.net/sites/">list of registered Moodle sites</a> in your country.</li></ul>';
 $string['registermoodleorg'] = 'When you register your site';
 $string['registermoodleorgli1'] = 'You are added to a low-volume mailing list for important notifications such as security alerts and new releases of Moodle.';
 $string['registermoodleorgli2'] = 'Statistics about your site will be added to the {$a} of the worldwide Moodle community.';
@@ -1162,7 +1162,7 @@ $string['uninstallplugin'] = 'Uninstall';
 $string['unlockaccount'] = 'Unlock account';
 $string['unsettheme'] = 'Unset theme';
 $string['unsupported'] = 'Unsupported';
-$string['unsupporteddbfileformat'] = 'Your database has tables using Antelope as the file format. Full UTF-8 support in MySQL and MariaDB requires the Barracuda file format. Please convert the tables to the Barracuda file format. See the documentation <a href="https://docs.moodle.org/en/cli">Administration via command line</a> for details of a tool for converting InnoDB tables to Barracuda.';
+$string['unsupporteddbfileformat'] = 'Your database uses Antelope as the file format. Full UTF-8 support in MySQL and MariaDB requires the Barracuda file format. Please switch to the Barracuda file format. See the documentation <a href="https://docs.moodle.org/en/admin/environment/custom check/mysql full unicode support">MySQL full unicode support</a> for details.';
 $string['unsupporteddbfilepertable'] = 'For full support of UTF-8 both MySQL and MariaDB require you to change your MySQL setting \'innodb_file_per_table\' to \'ON\'. See the documentation for further details.';
 $string['unsupporteddblargeprefix'] = 'For full support of UTF-8 both MySQL and MariaDB require you to change your MySQL setting \'innodb_large_prefix\' to \'ON\'. See the documentation for further details.';
 $string['unsupporteddbstorageengine'] = 'The database storage engine being used is no longer supported.';
@@ -1257,7 +1257,7 @@ $string['warningcurrentsetting'] = 'Invalid current value: {$a}';
 $string['warningiconvbuggy'] = 'Your version of the iconv library does not support the //IGNORE modifier. You should install the mbstring extension which can be used instead for cleaning strings containing invalid UTF-8 characters.';
 $string['webproxy'] = 'Web proxy';
 $string['webproxyinfo'] = 'Fill in following options if your Moodle server can not access internet directly. Internet access is required for download of environment data, language packs, RSS feeds, timezones, etc.<br /><em>PHP cURL extension is highly recommended.</em>';
-$string['xmlrpcrecommended'] = 'The xmlrpc extension is useful for web services and Moodle networking';
+$string['xmlrpcrecommended'] = 'The XMLRPC extension is useful for web services and Moodle networking.';
 $string['yuicomboloading'] = 'YUI combo loading';
 $string['ziprequired'] = 'The Zip PHP extension is now required by Moodle, info-ZIP binaries or PclZip library are not used anymore.';
 
index 91f4f0c..d5f70cb 100644 (file)
@@ -66,7 +66,7 @@ $string['auth_user_creation'] = 'New (anonymous) users can create user accounts
 $string['auth_usernameexists'] = 'Selected username already exists. Please choose a new one.';
 $string['auth_usernotexist'] = 'Cannot update non-existent user: {$a}';
 $string['auto_add_remote_users'] = 'Auto add remote users';
-$string['cannotmapfield'] = 'Field "{$a->fieldname}" can not be mapped because its short name "{$a->shortname}" is too long. To enable mapping reduce the profile field short name down to {$a->charlimit} characters. <a href="{$a->link}">Edit user profile fields</a>';
+$string['cannotmapfield'] = 'The field "{$a->fieldname}" can\'t be mapped because its short name "{$a->shortname}" is too long. To allow it to be mapped, you need to reduce the short name to {$a->charlimit} characters. <a href="{$a->link}">Edit user profile fields</a>';
 $string['createpassword'] = 'Generate password and notify user';
 $string['createpasswordifneeded'] = 'Create password if needed and send via email';
 $string['emailchangecancel'] = 'Cancel email change';
index 2364312..309f29c 100644 (file)
@@ -27,7 +27,7 @@
 $string['actions'] = 'Actions';
 $string['activate'] = 'Enable access';
 $string['activatesuccess'] = 'Access to the badges was successfully enabled.';
-$string['addbadge'] = 'Add a badge as criteria';
+$string['addbadge'] = 'Add badges';
 $string['addbadge_help'] = 'Select all badges that should be added to this badge requirement. Hold CTRL key to select multiple items.';
 $string['addbadgecriteria'] = 'Add badge criteria';
 $string['addcriteria'] = 'Add criteria';
@@ -260,7 +260,7 @@ $string['error:invalidexpiredate'] = 'Expiry date has to be in the future.';
 $string['error:invalidexpireperiod'] = 'Expiry period cannot be negative or equal 0.';
 $string['error:invalidparambadge'] = 'Badge does not exist. ';
 $string['error:noactivities'] = 'There are no activities with completion criteria enabled in this course.';
-$string['error:nobadges'] = 'There are no course or site badges available to be added as criteria.  Make sure that your other badges are enabled ';
+$string['error:nobadges'] = 'There are no course or site badges with access enabled to be added as criteria.';
 $string['error:nocourses'] = 'Course completion is not enabled for any of the courses in this site, so none can be displayed. Course completion may be enabled in the course settings.';
 $string['error:nogroups'] = '<p>There are no public collections of badges available in your backpack. </p>
 <p>Only public collections are shown, <a href="http://backpack.openbadges.org">visit your backpack</a> to create some public collections.</p>';
index c7b2559..06ce696 100644 (file)
@@ -5,24 +5,8 @@ myfilesmanage,core
 mypreferences,core_grades
 myprofile,core
 viewallmyentries,core_blog
-taggedwith,core_tag
-officialtag,core_tag
-otags,core_tag
-othertags,core_tag
-tagtype,core_tag
-manageofficialtags,core_tag
-settypeofficial,core_tag
-filetoolarge,core
-maxbytesforfile,core
 modchooserenable,core
 modchooserdisable,core
-maxbytes,core_error
-downloadcsv,core_table
-downloadexcel,core_table
-downloadods,core_table
-downloadoptions,core_table
-downloadtsv,core_table
-downloadxhtml,core_table
 invalidpersistent,core_competency
 revealpassword,core_form
 mediasettings,core_media
index 55786b4..492de6f 100644 (file)
@@ -593,5 +593,3 @@ $string['alreadyloggedin'] = 'You are already logged in as {$a}, you need to log
 $string['youcannotdeletecategory'] = 'You cannot delete category \'{$a}\' because you can neither delete the contents, nor move them elsewhere.';
 $string['protected_cc_not_supported'] = 'Protected cartridges not supported.';
 
-// Deprecated since Moodle 3.1.
-$string['maxbytes'] = 'The file is larger than the maximum size allowed.';
index 016b1fa..fcd85b0 100644 (file)
@@ -25,7 +25,7 @@
 
 $string['addscreenshots'] = 'Add screenshots';
 $string['advertise'] = 'Share this course for people to join';
-$string['advertised'] = 'Advertised';
+$string['advertised'] = 'For people to join';
 $string['advertiseon'] = 'Share this course on {$a}';
 $string['readvertiseon'] = 'Update advertising information on {$a}';
 $string['advertisepublication_help'] = 'This course will be listed on Moodle.net as a course that people can enrol in and participate. Email-based self-registration should be enabled on the site and you need to enable self enrolment in this course.';
@@ -119,7 +119,7 @@ $string['publicationinfo'] = 'Course publication information';
 $string['publish'] = 'Share';
 $string['publishcourse'] = 'Share {$a}';
 $string['publishcourseon'] = 'Share on {$a}';
-$string['publishedon'] = 'Publications';
+$string['publishedon'] = 'Course sharing';
 $string['publisheremail'] = 'Publisher email';
 $string['publisheremail_help'] = 'The publisher email address allows the hub administrator to alert the publisher about any changes to the status of the published course.';
 $string['publishername'] = 'Publisher';
@@ -147,7 +147,7 @@ $string['sendfollowinginfo'] = 'More information';
 $string['sendfollowinginfo_help'] = 'The following information will be sent to contribute to overall statistics only.  It will not be made public on any site listing.';
 $string['sent'] = '...finished';
 $string['share'] = 'Share this course for people to download';
-$string['shared'] = 'Shared';
+$string['shared'] = 'For people to download';
 $string['shareon'] = 'Upload this course to {$a}';
 $string['sharepublication_help'] = 'A backup of this course will be available on Moodle.net for people to restore and use on their own site.';
 $string['siteadmin'] = 'Administrator';
@@ -195,10 +195,10 @@ $string['statuspublished'] = 'Listed';
 $string['statusunpublished'] = 'Not listed';
 $string['tags'] = 'Tags';
 $string['tags_help'] = 'Tags help to further categorise your course and help it to be found. Please use simple, meaningful words and separate them with a comma. Example: math, algebra, geometry';
-$string['type'] = 'Advertised / Shared';
+$string['type'] = 'Shared';
 $string['unpublish'] = 'Stop sharing';
-$string['unpublishalladvertisedcourses'] = 'Remove all courses currently being advertised on Moodle.net';
-$string['unpublishalluploadedcourses'] = 'Removed all courses that were uploaded to Moodle.net';
+$string['unpublishalladvertisedcourses'] = 'Remove all courses that were shared on Moodle.net for people to join';
+$string['unpublishalluploadedcourses'] = 'Remove all courses that were shared on Moodle.net for people to download';
 $string['unpublishconfirmation'] = 'Do you really want to remove the course "{$a->courseshortname}" from "{$a->hubname}"';
 $string['unpublishcourse'] = 'Stop sharing {$a}';
 $string['unregister'] = 'Unregister';
index 48eaf09..331cf1f 100644 (file)
@@ -48,6 +48,8 @@ $string['strftimemonthyear'] = '%B %Y';
 $string['strftimerecent'] = '%d %b, %H:%M';
 $string['strftimerecentfull'] = '%a, %d %b %Y, %I:%M %p';
 $string['strftimetime'] = '%I:%M %p';
+$string['strftimetime12'] = '%I:%M %p';
+$string['strftimetime24'] = '%H:%M';
 $string['thisdirection'] = 'ltr';
 $string['thisdirectionvertical'] = 'btt';
 $string['thislanguage'] = 'English';
index f1eb252..80b20b6 100644 (file)
@@ -799,6 +799,7 @@ $string['feedback'] = 'Feedback';
 $string['file'] = 'File';
 $string['fileexists'] = 'There is already a file called {$a}';
 $string['filemissing'] = '{$a} is missing';
+$string['filereaderror'] = 'Unable to read the file \'{$a}\' - please check this really is a file and not a folder';
 $string['files'] = 'Files';
 $string['filesanduploads'] = 'Files and uploads';
 $string['filesfolders'] = 'Files/folders';
@@ -1747,7 +1748,7 @@ $string['selectcoursesortby'] = 'Select how you would like to sort courses';
 $string['senddetails'] = 'Send my details via email';
 $string['separate'] = 'Separate';
 $string['separateandconnected'] = 'Separate and Connected ways of knowing';
-$string['separateandconnectedinfo'] = 'The scale based on the theory of separate and connected knowing. This theory describes two different ways that we can evaluate and learn about the things we see and hear.<ul><li><strong>Separate knowers</strong> remain as objective as possible without including feelings and emotions. In a discussion with other people, they like to defend their own ideas, using logic to find holes in opponent\'s ideas.</li><li><strong>Connected knowers</strong> are more sensitive to other people. They are skilled at empathy and tends to listen and ask questions until they feel they can connect and "understand things from their point of view". They learn by trying to share the experiences that led to the knowledge they find in other people.</li></ul>';
+$string['separateandconnectedinfo'] = 'The scale based on the theory of separate and connected knowing. This theory describes two different ways that we can evaluate and learn about the things we see and hear.<ul><li><strong>Separate knowers</strong> remain as objective as possible without including feelings and emotions. In a discussion with other people, they like to defend their own ideas, using logic to find holes in opponent\'s ideas.</li><li><strong>Connected knowers</strong> are more sensitive to other people. They are skilled at empathy and tend to listen and ask questions until they feel they can connect and "understand things from their point of view". They learn by trying to share the experiences that led to the knowledge they find in other people.</li></ul>';
 $string['servererror'] = 'An error occurred whilst communicating with the server';
 $string['serverlocaltime'] = 'Server\'s local time';
 $string['setcategorytheme'] = 'Set category theme';
@@ -2149,10 +2150,6 @@ $string['yourwordforx'] = 'Your word for \'{$a}\'';
 $string['zippingbackup'] = 'Zipping backup';
 $string['deprecatedeventname'] = '{$a} (no longer in use)';
 
-// Deprecated since Moodle 3.1.
-$string['filetoolarge'] = 'is too large to upload';
-$string['maxbytesforfile'] = 'The file {$a} is larger than the maximum size allowed.';
-
 // Deprecated since Moodle 3.2.
 $string['modchooserenable'] = 'Activity chooser on';
 $string['modchooserdisable'] = 'Activity chooser off';
index a978f52..3b67454 100644 (file)
@@ -166,6 +166,7 @@ $string['manage'] = 'Manage repositories';
 $string['manageinstances'] = 'Manage instances';
 $string['manageurl'] = 'Manage';
 $string['manageuserrepository'] = 'Manage individual repository';
+$string['missingsourcekey'] = 'The source key is missing. This key must also be provided to retrieve the file.';
 $string['moving'] = 'Moving';
 $string['name'] = 'Name';
 $string['newfolder'] = 'New folder';
@@ -222,6 +223,7 @@ $string['setmainfile'] = 'Set main file';
 $string['setmainfile_help'] = 'If there are multiple files in the folder, the main file is the one that appears on the view page. Other files such as images or videos may be embedded in it. In filemanager the main file is indicated with a title in bold.';
 $string['siteinstances'] = 'Repositories instances of the site';
 $string['size'] = 'Size';
+$string['sourcekeymismatch'] = 'The source url does not match the sourcekey.';
 $string['submit'] = 'Submit';
 $string['sync'] = 'Sync';
 $string['syncfiletimeout'] = 'Sync file timeout';
index cba4592..8241c69 100644 (file)
@@ -52,6 +52,7 @@ $string['enginenotinstalled'] = 'Engine {$a} is not installed.';
 $string['enginenotselected'] = 'You have not selected any search engine.';
 $string['engineserverstatus'] = 'The search engine is not available. Please contact your administrator.';
 $string['enteryoursearchquery'] = 'Enter your search query';
+$string['error_indexing'] = 'An error occurred while indexing';
 $string['errors'] = 'Errors';
 $string['errorareanotavailable'] = '{$a} search area is not available.';
 $string['everywhere'] = 'Everywhere you can access';
index 8ab01a3..78020e0 100644 (file)
 
 $string['downloadas'] = 'Download table data as';
 
-// Deprecated since Moodle 3.1.
-$string['downloadcsv'] = 'Comma separated values text file';
-$string['downloadexcel'] = 'Excel spreadsheet';
-$string['downloadods'] = 'OpenDocument spreadsheet';
-$string['downloadoptions'] = 'Select download options';
-$string['downloadtsv'] = 'Tab separated values text file';
-$string['downloadxhtml'] = 'Unpaged XHTML document';
index 83d3b5f..fea1031 100644 (file)
@@ -148,12 +148,3 @@ $string['page-tag-index'] = 'Single tag page';
 $string['page-tag-search'] = 'Tag search page';
 $string['page-tag-manage'] = 'Manage tags page';
 
-// Deprecated since 3.1 .
-
-$string['manageofficialtags'] = 'Manage official tags';
-$string['officialtag'] = 'Official';
-$string['otags'] = 'Official tags';
-$string['othertags'] = 'Other tags';
-$string['settypeofficial'] = 'Make official';
-$string['taggedwith'] = 'tagged with "{$a}"';
-$string['tagtype'] = 'Tag type';
index c9ac3ff..aa5efbb 100644 (file)
@@ -1186,6 +1186,12 @@ function badges_download($userid) {
  * @return string Code of backpack accessibility status.
  */
 function badges_check_backpack_accessibility() {
+    if (defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING) {
+        // For behat sites, do not poll the remote badge site.
+        // Behat sites should not be available, but we should pretend as though they are.
+        return 'available';
+    }
+
     global $CFG;
     include_once $CFG->libdir . '/filelib.php';
 
index 757c2a6..8c71720 100644 (file)
@@ -113,6 +113,9 @@ class curl_security_helper extends curl_security_helper_base {
      * 2. Check the host component against the list of domain names and wildcard domain names.
      *  - This will perform a DNS reverse lookup if required.
      *
+     * The behaviour of this function can be classified as strict, as it returns true for hosts which are invalid or
+     * could not be parsed, as well as those valid URLs which were found in the blacklist.
+     *
      * @param string $host the host component of the URL to check against the blacklist.
      * @return bool true if the host is both valid and blocked, false otherwise.
      */
@@ -132,7 +135,8 @@ class curl_security_helper extends curl_security_helper_base {
 
             // Only perform a reverse lookup if there is a point to it (i.e. we have rules to check against).
             if ($blacklistedhosts['domain'] || $blacklistedhosts['domainwildcard']) {
-                $hostname = gethostbyaddr($host); // DNS reverse lookup - supports both IPv4 and IPv6 address formats.
+                // DNS reverse lookup - supports both IPv4 and IPv6 address formats.
+                $hostname = gethostbyaddr($host);
                 if ($hostname !== $host && $this->host_explicitly_blocked($hostname)) {
                     return true;
                 }
@@ -144,15 +148,39 @@ class curl_security_helper extends curl_security_helper_base {
 
             // Only perform a forward lookup if there are IP rules to check against.
             if ($blacklistedhosts['ipv4'] || $blacklistedhosts['ipv6']) {
-                $hostip = gethostbyname($host); // DNS forward lookup - only returns IPv4 addresses!
-                if ($hostip !== $host && $this->address_explicitly_blocked($hostip)) {
+                // DNS forward lookup - returns a list of only IPv4 addresses!
+                $hostips = $this->get_host_list_by_name($host);
+
+                // If we don't get a valid record, bail (so cURL is never called).
+                if (!$hostips) {
                     return true;
                 }
+
+                // If any of the returned IPs are in the blacklist, block the request.
+                foreach ($hostips as $hostip) {
+                    if ($this->address_explicitly_blocked($hostip)) {
+                        return true;
+                    }
+                }
             }
+        } else {
+            // Was not something we consider to be a valid IP or domain name, block it.
+            return true;
         }
+
         return false;
     }
 
+    /**
+     * Retrieve all hosts for a domain name.
+     *
+     * @param string $param
+     * @return array An array of IPs associated with the host name.
+     */
+    protected function get_host_list_by_name($host) {
+        return ($hostips = gethostbynamel($host)) ? $hostips : [];
+    }
+
     /**
      * Checks whether the given port is blocked, as determined by its absence on the ports whitelist.
      * Ports are assumed to be blocked unless found in the whitelist.
index e071d6b..93f8b59 100644 (file)
@@ -55,24 +55,6 @@ class notification implements \renderable, \templatable {
      */
     const NOTIFY_ERROR = 'error';
 
-    /**
-     * @deprecated
-     * A generic message.
-     */
-    const NOTIFY_MESSAGE = 'message';
-
-    /**
-     * @deprecated
-     * A message notifying the user that a problem occurred.
-     */
-    const NOTIFY_PROBLEM = 'problem';
-
-    /**
-     * @deprecated
-     * A notification of level 'redirect'.
-     */
-    const NOTIFY_REDIRECT = 'redirect';
-
     /**
      * @var string Message payload.
      */
@@ -102,7 +84,7 @@ class notification implements \renderable, \templatable {
      * Notification constructor.
      *
      * @param string $message the message to print out
-     * @param string $messagetype normally NOTIFY_PROBLEM or NOTIFY_SUCCESS.
+     * @param string $messagetype one of the NOTIFY_* constants..
      */
     public function __construct($message, $messagetype = null) {
         $this->message = $message;
@@ -112,13 +94,6 @@ class notification implements \renderable, \templatable {
         }
 
         $this->messagetype = $messagetype;
-
-        switch ($messagetype) {
-            case self::NOTIFY_PROBLEM:
-            case self::NOTIFY_REDIRECT:
-            case self::NOTIFY_MESSAGE:
-                debugging('Use of ' . $messagetype . ' has been deprecated. Please switch to an alternative type.');
-        }
     }
 
     /**
index d7f4a11..fe4124a 100644 (file)
@@ -89,10 +89,7 @@ class manager {
         $validtasks = array();
 
         foreach ($tasks as $taskid => $task) {
-            $classname = get_class($task);
-            if (strpos($classname, '\\') !== 0) {
-                $classname = '\\' . $classname;
-            }
+            $classname = self::get_canonical_class_name($task);
 
             $validtasks[] = $classname;
 
@@ -188,10 +185,7 @@ class manager {
     public static function configure_scheduled_task(scheduled_task $task) {
         global $DB;
 
-        $classname = get_class($task);
-        if (strpos($classname, '\\') !== 0) {
-            $classname = '\\' . $classname;
-        }
+        $classname = self::get_canonical_class_name($task);
 
         $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST);
 
@@ -211,10 +205,7 @@ class manager {
      */
     public static function record_from_scheduled_task($task) {
         $record = new \stdClass();
-        $record->classname = get_class($task);
-        if (strpos($record->classname, '\\') !== 0) {
-            $record->classname = '\\' . $record->classname;
-        }
+        $record->classname = self::get_canonical_class_name($task);
         $record->component = $task->get_component();
         $record->blocking = $task->is_blocking();
         $record->customised = $task->is_customised();
@@ -239,10 +230,7 @@ class manager {
      */
     public static function record_from_adhoc_task($task) {
         $record = new \stdClass();
-        $record->classname = get_class($task);
-        if (strpos($record->classname, '\\') !== 0) {
-            $record->classname = '\\' . $record->classname;
-        }
+        $record->classname = self::get_canonical_class_name($task);
         $record->id = $task->get_id();
         $record->component = $task->get_component();
         $record->blocking = $task->is_blocking();
@@ -261,10 +249,7 @@ class manager {
      * @return \core\task\adhoc_task
      */
     public static function adhoc_task_from_record($record) {
-        $classname = $record->classname;
-        if (strpos($classname, '\\') !== 0) {
-            $classname = '\\' . $classname;
-        }
+        $classname = self::get_canonical_class_name($record->classname);
         if (!class_exists($classname)) {
             debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
             return false;
@@ -301,10 +286,7 @@ class manager {
      * @return \core\task\scheduled_task
      */
     public static function scheduled_task_from_record($record) {
-        $classname = $record->classname;
-        if (strpos($classname, '\\') !== 0) {
-            $classname = '\\' . $classname;
-        }
+        $classname = self::get_canonical_class_name($record->classname);
         if (!class_exists($classname)) {
             debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
             return false;
@@ -381,9 +363,7 @@ class manager {
     public static function get_scheduled_task($classname) {
         global $DB;
 
-        if (strpos($classname, '\\') !== 0) {
-            $classname = '\\' . $classname;
-        }
+        $classname = self::get_canonical_class_name($classname);
         // We are just reading - so no locks required.
         $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
         if (!$record) {
@@ -401,9 +381,7 @@ class manager {
     public static function get_adhoc_tasks($classname) {
         global $DB;
 
-        if (strpos($classname, '\\') !== 0) {
-            $classname = '\\' . $classname;
-        }
+        $classname = self::get_canonical_class_name($classname);
         // We are just reading - so no locks required.
         $records = $DB->get_records('task_adhoc', array('classname' => $classname));
 
@@ -601,10 +579,7 @@ class manager {
             $delay = 86400;
         }
 
-        $classname = get_class($task);
-        if (strpos($classname, '\\') !== 0) {
-            $classname = '\\' . $classname;
-        }
+        $classname = self::get_canonical_class_name($task);
 
         $task->set_next_run_time(time() + $delay);
         $task->set_fail_delay($delay);
@@ -657,10 +632,7 @@ class manager {
             $delay = 86400;
         }
 
-        $classname = get_class($task);
-        if (strpos($classname, '\\') !== 0) {
-            $classname = '\\' . $classname;
-        }
+        $classname = self::get_canonical_class_name($task);
 
         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
         $record->nextruntime = time() + $delay;
@@ -673,6 +645,23 @@ class manager {
         $task->get_lock()->release();
     }
 
+    /**
+     * Clears the fail delay for the given task and updates its next run time based on the schedule.
+     *
+     * @param scheduled_task $task Task to reset
+     * @throws \dml_exception If there is a database error
+     */
+    public static function clear_fail_delay(scheduled_task $task) {
+        global $DB;
+
+        $record = new \stdClass();
+        $record->id = $DB->get_field('task_scheduled', 'id',
+                ['classname' => self::get_canonical_class_name($task)]);
+        $record->nextruntime = $task->get_next_scheduled_time();
+        $record->faildelay = 0;
+        $DB->update_record('task_scheduled', $record);
+    }
+
     /**
      * This function indicates that a scheduled task was completed successfully and should be rescheduled.
      *
@@ -681,10 +670,7 @@ class manager {
     public static function scheduled_task_complete(scheduled_task $task) {
         global $DB;
 
-        $classname = get_class($task);
-        if (strpos($classname, '\\') !== 0) {
-            $classname = '\\' . $classname;
-        }
+        $classname = self::get_canonical_class_name($task);
         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
         if ($record) {
             $record->lastruntime = time();
@@ -731,4 +717,21 @@ class manager {
         $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
         return $record && (intval($record->value) > $starttime);
     }
+
+    /**
+     * Gets class name for use in database table. Always begins with a \.
+     *
+     * @param string|task_base $taskorstring Task object or a string
+     */
+    protected static function get_canonical_class_name($taskorstring) {
+        if (is_string($taskorstring)) {
+            $classname = $taskorstring;
+        } else {
+            $classname = get_class($taskorstring);
+        }
+        if (strpos($classname, '\\') !== 0) {
+            $classname = '\\' . $classname;
+        }
+        return $classname;
+    }
 }
index e1d7944..aeda1d5 100644 (file)
Binary files a/lib/form/amd/build/defaultcustom.min.js and b/lib/form/amd/build/defaultcustom.min.js differ
index 0f280d5..e2e4d11 100644 (file)
@@ -39,6 +39,12 @@ define(['jquery'], function($) {
             form.find('[name="' + elementName + '[day]"]').val(newvalue.day);
             form.find('[name="' + elementName + '[month]"]').val(newvalue.month);
             form.find('[name="' + elementName + '[year]"]').val(newvalue.year);
+        } else if (type === 'date_time_selector') {
+            form.find('[name="' + elementName + '[day]"]').val(newvalue.day);
+            form.find('[name="' + elementName + '[month]"]').val(newvalue.month);
+            form.find('[name="' + elementName + '[year]"]').val(newvalue.year);
+            form.find('[name="' + elementName + '[hour]"]').val(newvalue.hour);
+            form.find('[name="' + elementName + '[minute]"]').val(newvalue.minute);
         }
     };
 
index ab8929a..16a263d 100644 (file)
@@ -84,8 +84,8 @@ class MoodleQuickForm_defaultcustom extends MoodleQuickForm_group {
         if (is_array($options)) {
             foreach ($options as $name => $value) {
                 if (array_key_exists($name, $this->_options)) {
-                    if ($name === 'type' && !in_array($value, ['text', 'date_selector'])) {
-                        throw new coding_exception('Only text and date_selector elements are supported in ' . $this->_type);
+                    if ($name === 'type' && !in_array($value, ['text', 'date_selector', 'date_time_selector'])) {
+                        throw new coding_exception('Only text, date_selector, and date_time_selector elements are supported in ' . $this->_type);
                     }
                     if ($name === 'optional' && $value) {
                         throw new coding_exception('Date selector can not be optional in ' . $this->_type);
@@ -105,6 +105,8 @@ class MoodleQuickForm_defaultcustom extends MoodleQuickForm_group {
         $calendartype = \core_calendar\type_factory::get_calendar_instance();
         $currentdate = $calendartype->timestamp_to_date_array($value, $this->_options['timezone']);
         return array(
+            'minute' => $currentdate['minutes'],
+            'hour' => $currentdate['hours'],
             'day' => $currentdate['mday'],
             'month' => $currentdate['mon'],
             'year' => $currentdate['year']);
@@ -137,6 +139,9 @@ class MoodleQuickForm_defaultcustom extends MoodleQuickForm_group {
         } else if ($this->_options['type'] === 'date_selector') {
             $element = $this->createFormElement($this->_options['type'], 'value', '', $this->_options,
                 $this->getAttributes());
+        } else if ($this->_options['type'] === 'date_time_selector') {
+            $element = $this->createFormElement($this->_options['type'], 'value', '', $this->_options,
+                $this->getAttributes());
         }
         $this->_elements[] = $element;
     }
@@ -184,10 +189,17 @@ class MoodleQuickForm_defaultcustom extends MoodleQuickForm_group {
                 if ($this->has_customize_switch()) {
                     if ($this->_options['type'] === 'text') {
                         $caller->disabledIf($arg[0] . '[value]', $arg[0] . '[customize]', 'notchecked');
+                    } else if ($this->_options['type'] === 'date_selector') {
+                        $caller->disabledIf($arg[0] . '[value][day]', $arg[0] . '[customize]', 'notchecked');
+                        $caller->disabledIf($arg[0] . '[value][month]', $arg[0] . '[customize]', 'notchecked');
+                        $caller->disabledIf($arg[0] . '[value][year]', $arg[0] . '[customize]', 'notchecked');
                     } else {
+                        // Date / Time selector.
                         $caller->disabledIf($arg[0] . '[value][day]', $arg[0] . '[customize]', 'notchecked');
                         $caller->disabledIf($arg[0] . '[value][month]', $arg[0] . '[customize]', 'notchecked');
                         $caller->disabledIf($arg[0] . '[value][year]', $arg[0] . '[customize]', 'notchecked');
+                        $caller->disabledIf($arg[0] . '[value][hour]', $arg[0] . '[customize]', 'notchecked');
+                        $caller->disabledIf($arg[0] . '[value][minute]', $arg[0] . '[customize]', 'notchecked');
                     }
                 }
                 return $rv;
@@ -230,7 +242,7 @@ class MoodleQuickForm_defaultcustom extends MoodleQuickForm_group {
             $firstelement = reset($this->_elements);
             $defaultvalue = $this->_options['defaultvalue'];
             $customvalue = $this->_options['customvalue'];
-            if ($this->_options['type'] === 'date_selector') {
+            if ($this->_options['type'] === 'date_selector' || $this->_options['type'] === 'date_time_selector') {
                 $defaultvalue = $this->timestamp_to_date_array($defaultvalue);
                 $customvalue = $this->timestamp_to_date_array($customvalue);
             }
index 59f3ac6..50f5960 100644 (file)
@@ -282,6 +282,8 @@ if (typeof M.form.dependencyManager === 'undefined') {
             var els = this.elementsByName(name, true);
             els.each(function(node) {
                 var e = node.ancestor('.fitem', true);
+                var label = null,
+                    id = null;
                 if (e) {
                     // Cope with differences between clean and boost themes.
                     if (e.hasClass('fitem_fgroup')) {
@@ -298,6 +300,22 @@ if (typeof M.form.dependencyManager === 'undefined') {
                     e.setStyles({
                         display: (hidden) ? 'none' : ''
                     });
+
+                    // Hide/unhide the label as well.
+                    id = node.get('id');
+                    if (id) {
+                        label = Y.all('label[for="' + id + '"]');
+                        if (label) {
+                            if (hidden) {
+                                label.setAttribute('hidden', 'hidden');
+                            } else {
+                                label.removeAttribute('hidden');
+                            }
+                            label.setStyles({
+                                display: (hidden) ? 'none' : ''
+                            });
+                        }
+                    }
                 }
             });
         },
index 3a3b602..94dc22d 100644 (file)
@@ -2837,13 +2837,13 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
         // switch next two lines for ol li containers for form items.
         //        $this->_elementTemplates=array('default'=>"\n\t\t".'<li class="fitem"><label>{label}{help}<!-- BEGIN required -->{req}<!-- END required --></label><div class="qfelement<!-- BEGIN error --> error<!-- END error --> {typeclass}"><!-- BEGIN error --><span class="error">{error}</span><br /><!-- END error -->{element}</div></li>');
         $this->_elementTemplates = array(
-        'default' => "\n\t\t".'<div id="{id}" class="fitem {advanced}<!-- BEGIN required --> required<!-- END required --> fitem_{typeclass} {emptylabel} {class}" {aria-live}><div class="fitemtitle"><label>{label}<!-- BEGIN required -->{req}<!-- END required -->{advancedimg} </label>{help}</div><div class="felement {typeclass}<!-- BEGIN error --> error<!-- END error -->" data-fieldtype="{type}"><!-- BEGIN error --><span class="error" tabindex="0">{error}</span><br /><!-- END error -->{element}</div></div>',
+        'default' => "\n\t\t".'<div id="{id}" class="fitem {advanced}<!-- BEGIN required --> required<!-- END required --> fitem_{typeclass} {emptylabel} {class}" {aria-live} {groupname}><div class="fitemtitle"><label>{label}<!-- BEGIN required -->{req}<!-- END required -->{advancedimg} </label>{help}</div><div class="felement {typeclass}<!-- BEGIN error --> error<!-- END error -->" data-fieldtype="{type}"><!-- BEGIN error --><span class="error" tabindex="0">{error}</span><br /><!-- END error -->{element}</div></div>',
 
-        'actionbuttons' => "\n\t\t".'<div id="{id}" class="fitem fitem_actionbuttons fitem_{typeclass} {class}"><div class="felement {typeclass}" data-fieldtype="{type}">{element}</div></div>',
+        'actionbuttons' => "\n\t\t".'<div id="{id}" class="fitem fitem_actionbuttons fitem_{typeclass} {class}" {groupname}><div class="felement {typeclass}" data-fieldtype="{type}">{element}</div></div>',
 
-        'fieldset' => "\n\t\t".'<div id="{id}" class="fitem {advanced} {class}<!-- BEGIN required --> required<!-- END required --> fitem_{typeclass} {emptylabel}"><div class="fitemtitle"><div class="fgrouplabel"><label>{label}<!-- BEGIN required -->{req}<!-- END required -->{advancedimg} </label>{help}</div></div><fieldset class="felement {typeclass}<!-- BEGIN error --> error<!-- END error -->" data-fieldtype="{type}"><!-- BEGIN error --><span class="error" tabindex="0">{error}</span><br /><!-- END error -->{element}</fieldset></div>',
+        'fieldset' => "\n\t\t".'<div id="{id}" class="fitem {advanced} {class}<!-- BEGIN required --> required<!-- END required --> fitem_{typeclass} {emptylabel}" {groupname}><div class="fitemtitle"><div class="fgrouplabel"><label>{label}<!-- BEGIN required -->{req}<!-- END required -->{advancedimg} </label>{help}</div></div><fieldset class="felement {typeclass}<!-- BEGIN error --> error<!-- END error -->" data-fieldtype="{type}"><!-- BEGIN error --><span class="error" tabindex="0">{error}</span><br /><!-- END error -->{element}</fieldset></div>',
 
-        'static' => "\n\t\t".'<div id="{id}" class="fitem {advanced} {emptylabel} {class}"><div class="fitemtitle"><div class="fstaticlabel">{label}<!-- BEGIN required -->{req}<!-- END required -->{advancedimg} {help}</div></div><div class="felement fstatic <!-- BEGIN error --> error<!-- END error -->" data-fieldtype="static"><!-- BEGIN error --><span class="error" tabindex="0">{error}</span><br /><!-- END error -->{element}</div></div>',
+        'static' => "\n\t\t".'<div id="{id}" class="fitem {advanced} {emptylabel} {class}" {groupname}><div class="fitemtitle"><div class="fstaticlabel">{label}<!-- BEGIN required -->{req}<!-- END required -->{advancedimg} {help}</div></div><div class="felement fstatic <!-- BEGIN error --> error<!-- END error -->" data-fieldtype="static"><!-- BEGIN error --><span class="error" tabindex="0">{error}</span><br /><!-- END error -->{element}</div></div>',
 
         'warning' => "\n\t\t".'<div id="{id}" class="fitem {advanced} {emptylabel} {class}">{element}</div>',
 
@@ -2966,6 +2966,7 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
             }
             $html = str_replace('{id}', $group->getAttribute('id'), $html);
             $html = str_replace('{name}', $group->getName(), $html);
+            $html = str_replace('{groupname}', 'data-groupname="'.$group->getName().'"', $html);
             $html = str_replace('{typeclass}', 'fgroup', $html);
             $html = str_replace('{type}', 'group', $html);
             $html = str_replace('{class}', $group->getAttribute('class'), $html);
@@ -3033,6 +3034,7 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
             $html = str_replace('{typeclass}', 'f' . $element->getType(), $html);
             $html = str_replace('{type}', $element->getType(), $html);
             $html = str_replace('{name}', $element->getName(), $html);
+            $html = str_replace('{groupname}', '', $html);
             $html = str_replace('{class}', $element->getAttribute('class'), $html);
             $emptylabel = '';
             if ($element->getLabel() == '') {
index 2f36760..469a012 100644 (file)
@@ -165,7 +165,8 @@ function groups_get_grouping_by_idnumber($courseid, $idnumber) {
  * @param int $groupid ID of the group.
  * @param string $fields (default is all fields)
  * @param int $strictness (IGNORE_MISSING - default)
- * @return stdGlass group object
+ * @return bool|stdClass group object or false if not found
+ * @throws dml_exception
  */
 function groups_get_group($groupid, $fields='*', $strictness=IGNORE_MISSING) {
     global $DB;
index 7a8beff..e5a8514 100644 (file)
@@ -897,6 +897,14 @@ class theme_config {
         global $CFG;
         $rev = theme_get_revision();
         if ($rev > -1) {
+            $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}";
+            }
+
             $url = new moodle_url("/theme/styles.php");
             if (!empty($CFG->slasharguments)) {
                 $url->set_slashargument('/'.$this->name.'/'.$rev.'/editor', 'noparam', true);
index 4fbdedc..d4ad83d 100644 (file)
@@ -2890,71 +2890,35 @@ EOD;
     }
 
     /**
-     * Output a notification at a particular level - in this case, NOTIFY_PROBLEM.
-     *
-     * @param string $message the message to print out
-     * @return string HTML fragment.
      * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
-     * @todo MDL-53113 This will be removed in Moodle 3.5.
-     * @see \core\output\notification
      */
-    public function notify_problem($message) {
-        debugging(__FUNCTION__ . ' is deprecated.' .
-            'Please use \core\notification::add, or \core\output\notification as required',
-            DEBUG_DEVELOPER);
-        $n = new \core\output\notification($message, \core\output\notification::NOTIFY_ERROR);
-        return $this->render($n);
+    public function notify_problem() {
+        throw new coding_exception('core_renderer::notify_problem() can not be used any more, '.
+            'please use \core\notification::add(), or \core\output\notification as required.');
     }
 
     /**
-     * Output a notification at a particular level - in this case, NOTIFY_SUCCESS.
-     *
-     * @param string $message the message to print out
-     * @return string HTML fragment.
      * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
-     * @todo MDL-53113 This will be removed in Moodle 3.5.
-     * @see \core\output\notification
      */
-    public function notify_success($message) {
-        debugging(__FUNCTION__ . ' is deprecated.' .
-            'Please use \core\notification::add, or \core\output\notification as required',
-            DEBUG_DEVELOPER);
-        $n = new \core\output\notification($message, \core\output\notification::NOTIFY_SUCCESS);
-        return $this->render($n);
+    public function notify_success() {
+        throw new coding_exception('core_renderer::notify_success() can not be used any more, '.
+            'please use \core\notification::add(), or \core\output\notification as required.');
     }
 
     /**
-     * Output a notification at a particular level - in this case, NOTIFY_MESSAGE.
-     *
-     * @param string $message the message to print out
-     * @return string HTML fragment.
      * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
-     * @todo MDL-53113 This will be removed in Moodle 3.5.
-     * @see \core\output\notification
      */
-    public function notify_message($message) {
-        debugging(__FUNCTION__ . ' is deprecated.' .
-            'Please use \core\notification::add, or \core\output\notification as required',
-            DEBUG_DEVELOPER);
-        $n = new \core\output\notification($message, \core\output\notification::NOTIFY_INFO);
-        return $this->render($n);
+    public function notify_message() {
+        throw new coding_exception('core_renderer::notify_message() can not be used any more, '.
+            'please use \core\notification::add(), or \core\output\notification as required.');
     }
 
     /**
-     * Output a notification at a particular level - in this case, NOTIFY_REDIRECT.
-     *
-     * @param string $message the message to print out
-     * @return string HTML fragment.
      * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
-     * @todo MDL-53113 This will be removed in Moodle 3.5.
-     * @see \core\output\notification
-     */
-    public function notify_redirect($message) {
-        debugging(__FUNCTION__ . ' is deprecated.' .
-            'Please use \core\notification::add, or \core\output\notification as required',
-            DEBUG_DEVELOPER);
-        $n = new \core\output\notification($message, \core\output\notification::NOTIFY_INFO);
-        return $this->render($n);
+     */
+    public function notify_redirect() {
+        throw new coding_exception('core_renderer::notify_redirect() can not be used any more, '.
+            'please use \core\notification::add(), or \core\output\notification as required.');
     }
 
     /**
index 83b1cc4..f2691b7 100644 (file)
@@ -38,15 +38,29 @@ class core_curl_security_helper_testcase extends advanced_testcase {
     /**
      * Test for \core\files\curl_security_helper::url_is_blocked().
      *
+     * @param array $dns a mapping between hosts and IPs to be used instead of a real DNS lookup. The values must be arrays.
      * @param string $url the url to validate.
      * @param string $blockedhosts the list of blocked hosts.
      * @param string $allowedports the list of allowed ports.
      * @param bool $expected the expected result.
      * @dataProvider curl_security_url_data_provider
      */
-    public function test_curl_security_helper_url_is_blocked($url, $blockedhosts, $allowedports, $expected) {
+    public function test_curl_security_helper_url_is_blocked($dns, $url, $blockedhosts, $allowedports, $expected) {
         $this->resetAfterTest(true);
-        $helper = new \core\files\curl_security_helper();
+        $helper = $this->getMockBuilder('\core\files\curl_security_helper')
+                        ->setMethods(['get_host_list_by_name'])
+                        ->getMock();
+
+        // Override the get host list method to return hard coded values based on a mapping provided by $dns.
+        $helper->method('get_host_list_by_name')
+               ->will(
+                   $this->returnCallback(
+                       function($host) use ($dns) {
+                           return isset($dns[$host]) ? $dns[$host] : [];
+                       }
+                   )
+               );
+
         set_config('curlsecurityblockedhosts', $blockedhosts);
         set_config('curlsecurityallowedport', $allowedports);
         $this->assertEquals($expected, $helper->url_is_blocked($url));
@@ -58,67 +72,82 @@ class core_curl_security_helper_testcase extends advanced_testcase {
      * @return array
      */
     public function curl_security_url_data_provider() {
+        $simpledns = ['localhost' => ['127.0.0.1']];
+        $multiplerecorddns = [
+            'sub.example.com' => ['1.2.3.4', '5.6.7.8']
+        ];
         // Format: url, blocked hosts, allowed ports, expected result.
         return [
             // Base set without the blacklist enabled - no checking takes place.
-            ["http://localhost/x.png", "", "", false],       // IP=127.0.0.1, Port=80 (port inferred from http).
-            ["http://localhost:80/x.png", "", "", false],    // IP=127.0.0.1, Port=80 (specific port overrides http scheme).
-            ["https://localhost/x.png", "", "", false],      // IP=127.0.0.1, Port=443 (port inferred from https).
-            ["http://localhost:443/x.png", "", "", false],   // IP=127.0.0.1, Port=443 (specific port overrides http scheme).
-            ["localhost/x.png", "", "", false],              // IP=127.0.0.1, Port=80 (port inferred from http fallback).
-            ["localhost:443/x.png", "", "", false],          // IP=127.0.0.1, Port=443 (port hard specified, despite http fallback).
-            ["http://127.0.0.1/x.png", "", "", false],       // IP=127.0.0.1, Port=80 (port inferred from http).
-            ["127.0.0.1/x.png", "", "", false],              // IP=127.0.0.1, Port=80 (port inferred from http fallback).
-            ["http://localhost:8080/x.png", "", "", false],  // IP=127.0.0.1, Port=8080 (port hard specified).
-            ["http://192.168.1.10/x.png", "", "", false],    // IP=192.168.1.10, Port=80 (port inferred from http).
-            ["https://192.168.1.10/x.png", "", "", false],   // IP=192.168.1.10, Port=443 (port inferred from https).
-            ["http://sub.example.com/x.png", "", "", false], // IP=::1, Port = 80 (port inferred from http).
-            ["http://s-1.d-1.com/x.png", "", "", false],     // IP=::1, Port = 80 (port inferred from http).
+            [$simpledns, "http://localhost/x.png", "", "", false],       // IP=127.0.0.1, Port=80 (port inferred from http).
+            [$simpledns, "http://localhost:80/x.png", "", "", false],    // IP=127.0.0.1, Port=80 (specific port overrides http scheme).
+            [$simpledns, "https://localhost/x.png", "", "", false],      // IP=127.0.0.1, Port=443 (port inferred from https).
+            [$simpledns, "http://localhost:443/x.png", "", "", false],   // IP=127.0.0.1, Port=443 (specific port overrides http scheme).
+            [$simpledns, "localhost/x.png", "", "", false],              // IP=127.0.0.1, Port=80 (port inferred from http fallback).
+            [$simpledns, "localhost:443/x.png", "", "", false],          // IP=127.0.0.1, Port=443 (port hard specified, despite http fallback).
+            [$simpledns, "http://127.0.0.1/x.png", "", "", false],       // IP=127.0.0.1, Port=80 (port inferred from http).
+            [$simpledns, "127.0.0.1/x.png", "", "", false],              // IP=127.0.0.1, Port=80 (port inferred from http fallback).
+            [$simpledns, "http://localhost:8080/x.png", "", "", false],  // IP=127.0.0.1, Port=8080 (port hard specified).
+            [$simpledns, "http://192.168.1.10/x.png", "", "", false],    // IP=192.168.1.10, Port=80 (port inferred from http).
+            [$simpledns, "https://192.168.1.10/x.png", "", "", false],   // IP=192.168.1.10, Port=443 (port inferred from https).
+            [$simpledns, "http://sub.example.com/x.png", "", "", false], // IP=::1, Port = 80 (port inferred from http).
+            [$simpledns, "http://s-1.d-1.com/x.png", "", "", false],     // IP=::1, Port = 80 (port inferred from http).
 
             // Test set using domain name filters but with all ports allowed (empty).
-            ["http://localhost/x.png", "localhost", "", true],
-            ["localhost/x.png", "localhost", "", true],
-            ["localhost:0/x.png", "localhost", "", true],
-            ["ftp://localhost/x.png", "localhost", "", true],
-            ["http://sub.example.com/x.png", "localhost", "", false],
-            ["http://example.com/x.png", "example.com", "", true],
-            ["http://sub.example.com/x.png", "example.com", "", false],
+            [$simpledns, "http://localhost/x.png", "localhost", "", true],
+            [$simpledns, "localhost/x.png", "localhost", "", true],
+            [$simpledns, "localhost:0/x.png", "localhost", "", true],
+            [$simpledns, "ftp://localhost/x.png", "localhost", "", true],
+            [$simpledns, "http://sub.example.com/x.png", "localhost", "", false],
+            [$simpledns, "http://example.com/x.png", "example.com", "", true],
+            [$simpledns, "http://sub.example.com/x.png", "example.com", "", false],
 
             // Test set using wildcard domain name filters but with all ports allowed (empty).
-            ["http://sub.example.com/x.png", "*.com", "", true],
-            ["http://example.com/x.png", "*.example.com", "", false],
-            ["http://sub.example.com/x.png", "*.example.com", "", true],
-            ["http://sub.example.com/x.png", "*.sub.example.com", "", false],
-            ["http://sub.example.com/x.png", "*.example", "", false],
+            [$simpledns, "http://sub.example.com/x.png", "*.com", "", true],
+            [$simpledns, "http://example.com/x.png", "*.example.com", "", false],
+            [$simpledns, "http://sub.example.com/x.png", "*.example.com", "", true],
+            [$simpledns, "http://sub.example.com/x.png", "*.sub.example.com", "", false],
+            [$simpledns, "http://sub.example.com/x.png", "*.example", "", false],
 
             // Test set using IP address filters but with all ports allowed (empty).
-            ["http://localhost/x.png", "127.0.0.1", "", true],
-            ["http://127.0.0.1/x.png", "127.0.0.1", "", true],
-            ["http://sub.example.com", "127.0.0.1", "", false],
+            [$simpledns, "http://localhost/x.png", "127.0.0.1", "", true],
+            [$simpledns, "http://127.0.0.1/x.png", "127.0.0.1", "", true],
 
             // Test set using CIDR IP range filters but with all ports allowed (empty).
-            ["http://localhost/x.png", "127.0.0.0/24", "", true],
-            ["http://127.0.0.1/x.png", "127.0.0.0/24", "", true],
-            ["http://sub.example.com", "127.0.0.0/24", "", false],
+            [$simpledns, "http://localhost/x.png", "127.0.0.0/24", "", true],
+            [$simpledns, "http://127.0.0.1/x.png", "127.0.0.0/24", "", true],
 
             // Test set using last-group range filters but with all ports allowed (empty).
-            ["http://localhost/x.png", "127.0.0.0-30", "", true],
-            ["http://127.0.0.1/x.png", "127.0.0.0-30", "", true],
-            ["http://sub.example.com", "127.0.0.0/24", "", false],
+            [$simpledns, "http://localhost/x.png", "127.0.0.0-30", "", true],
+            [$simpledns, "http://127.0.0.1/x.png", "127.0.0.0-30", "", true],
 
             // Test set using port filters but with all hosts allowed (empty).
-            ["http://localhost/x.png", "", "80\n443", false],
-            ["http://localhost:80/x.png", "", "80\n443", false],
-            ["https://localhost/x.png", "", "80\n443", false],
-            ["http://localhost:443/x.png", "", "80\n443", false],
-            ["http://sub.example.com:8080/x.png", "", "80\n443", true],
-            ["http://sub.example.com:-80/x.png", "", "80\n443", true],
-            ["http://sub.example.com:aaa/x.png", "", "80\n443", true],
+            [$simpledns, "http://localhost/x.png", "", "80\n443", false],
+            [$simpledns, "http://localhost:80/x.png", "", "80\n443", false],
+            [$simpledns, "https://localhost/x.png", "", "80\n443", false],
+            [$simpledns, "http://localhost:443/x.png", "", "80\n443", false],
+            [$simpledns, "http://sub.example.com:8080/x.png", "", "80\n443", true],
+            [$simpledns, "http://sub.example.com:-80/x.png", "", "80\n443", true],
+            [$simpledns, "http://sub.example.com:aaa/x.png", "", "80\n443", true],
 
             // Test set using port filters and hosts filters.
-            ["http://localhost/x.png", "127.0.0.1", "80\n443", true],
-            ["http://127.0.0.1/x.png", "127.0.0.1", "80\n443", true],
-            ["http://sub.example.com", "127.0.0.1", "80\n443", false],
+            [$simpledns, "http://localhost/x.png", "127.0.0.1", "80\n443", true],
+            [$simpledns, "http://127.0.0.1/x.png", "127.0.0.1", "80\n443", true],
+
+            // Test using multiple A records.
+            // Multiple record DNS gives two IPs for the same host, we want to make
+            // sure that if we blacklist one of those (doesn't matter which one)
+            // the request is blocked.
+            [$multiplerecorddns, "http://sub.example.com", '1.2.3.4', "", true],
+            [$multiplerecorddns, "http://sub.example.com", '5.6.7.8', "", true],
+
+            // Test when DNS resolution fails.
+            [[], "http://example.com", "127.0.0.1", "", true],
+
+            // Test some freaky deaky Unicode domains. Should be blocked always.
+            [$simpledns, "http://169。254。169。254/", "127.0.0.1", "", true],
+            [$simpledns, "http://169。254。169。254/", "1.2.3.4", "", true],
+            [$simpledns, "http://169。254。169。254/", "127.0.0.1", "80\n443", true]
 
             // Note on testing URLs using IPv6 notation:
             // At present, the curl_security_helper class doesn't support IPv6 url notation.
index 2dc039c..adc9b41 100644 (file)
@@ -467,4 +467,38 @@ class core_scheduled_task_testcase extends advanced_testcase {
         // There should only be two items in the array, '.' and '..'.
         $this->assertEquals(2, count($filesarray));
     }
+
+    /**
+     * Test that the function to clear the fail delay from a task works correctly.
+     */
+    public function test_clear_fail_delay() {
+
+        $this->resetAfterTest();
+
+        // Get an example task to use for testing. Task is set to run every minute by default.
+        $taskname = '\core\task\send_new_user_passwords_task';
+
+        // Pretend task started running and then failed 3 times.
+        $before = time();
+        $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
+        for ($i = 0; $i < 3; $i ++) {
+            $task = \core\task\manager::get_scheduled_task($taskname);
+            $lock = $cronlockfactory->get_lock('\\' . get_class($task), 10);
+            $task->set_lock($lock);
+            \core\task\manager::scheduled_task_failed($task);
+        }
+
+        // Confirm task is now delayed by several minutes.
+        $task = \core\task\manager::get_scheduled_task($taskname);
+        $this->assertEquals(240, $task->get_fail_delay());
+        $this->assertGreaterThan($before + 230, $task->get_next_run_time());
+
+        // Clear the fail delay and re-get the task.
+        \core\task\manager::clear_fail_delay($task);
+        $task = \core\task\manager::get_scheduled_task($taskname);
+
+        // There should be no delay and it should run within the next minute.
+        $this->assertEquals(0, $task->get_fail_delay());
+        $this->assertLessThan($before + 70, $task->get_next_run_time());
+    }
 }
index 99ab6a9..85945c4 100644 (file)
@@ -153,4 +153,26 @@ class core_theme_config_testcase extends advanced_testcase {
         $this->assertTrue(core_useragent::set_user_device_type('tablet'));
         $this->assertTrue(core_useragent::set_user_device_type('featurephone'));
     }
+
+    /**
+     * Confirm that the editor_css_url contains the theme revision and the
+     * theme subrevision if not in theme designer mode.
+     */
+    public function test_editor_css_url_has_revision_and_subrevision() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $theme = theme_config::load(theme_config::DEFAULT_THEME);
+        $themename = $theme->name;
+        $themerevision = 1234;
+        $themesubrevision = 5678;
+
+        $CFG->themedesignermode = false;
+        $CFG->themerev = $themerevision;
+
+        theme_set_sub_revision_for_theme($themename, $themesubrevision);
+        $url = $theme->editor_css_url();
+
+        $this->assertRegExp("/{$themerevision}_{$themesubrevision}/", $url->out(false));
+    }
 }
index aa1c81d..4fbd4bd 100644 (file)
@@ -3,9 +3,14 @@ information provided here is intended especially for developers.
 
 === 3.5 ===
 
+* The core_renderer methods notify_problem(), notify_success(), notify_message() and notify_redirect() that were
+  deprecated in Moodle 3.1 have been removed. Use \core\notification::add(), or \core\output\notification as required.
 * The maximum supported precision (the total number of digits) for XMLDB_TYPE_NUMBER ("number") fields raised from 20 to
   38 digits. Additionally, the whole number part (precision minus scale) must not be longer than the maximum length of
   integer fields (20 digits). Note that PHP floats commonly support precision of roughly 15 digits only (MDL-32113).
+* Event triggering and event handlers:
+    - The following events, deprecated since moodle 2.6, have been finally removed: groups_members_removed,
+      groups_groupings_groups_removed, groups_groups_deleted, groups_groupings_deleted.
 
 === 3.4 ===
 
index 3dd767e..56f1f76 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js differ
index 36fcb5c..0111190 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js differ
index 3dd767e..56f1f76 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js differ
index a3689b4..c960ec2 100644 (file)
@@ -86,6 +86,9 @@ var QUICKCOMMENTLIST = function(editor) {
                                                                                      jsondata.width,
                                                                                      jsondata.colour);
                             this.comments.push(quickcomment);
+                            this.comments.sort(function(a, b) {
+                                return a.rawtext.localeCompare(b.rawtext);
+                            });
                         }
                     } catch (e) {
                         return new M.core.exception(e);
@@ -182,6 +185,10 @@ var QUICKCOMMENTLIST = function(editor) {
                                                                                              comment.colour);
                                 this.comments.push(quickcomment);
                             }, this);
+
+                            this.comments.sort(function(a, b) {
+                                return a.rawtext.localeCompare(b.rawtext);
+                            });
                         }
                     } catch (e) {
                         return new M.core.exception(e);
index 949ae5b..35f6a14 100644 (file)
@@ -446,7 +446,7 @@ $string['submissioncopiedhtml'] = '<p>You have made a copy of your previous
 assignment submission for \'<i>{$a->assignment}</i>\'.</p>
 <p>You can see the status of your <a href="{$a->url}">assignment submission</a>.</p>';
 $string['submissioncopiedsmall'] = 'You have copied your previous assignment submission for {$a->assignment}';
-$string['submissiondrafts'] = 'Require students click submit button';
+$string['submissiondrafts'] = 'Require students to click the submit button';
 $string['submissiondrafts_help'] = 'If enabled, students will have to click a Submit button to declare their submission as final. This allows students to keep a draft version of the submission on the system. If this setting is changed from "No" to "Yes" after students have already submitted those submissions will be regarded as final.';
 $string['submissioneditable'] = 'Student can edit this submission';
 $string['submissionlog'] = 'Student: {$a->fullname}, Status: {$a->status}';
index b1a84e7..54adce0 100644 (file)
@@ -7645,7 +7645,7 @@ class assign {
             return false;
         }
         $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
-        $this->update_submission($submission, $userid, true, $this->get_instance()->teamsubmission);
+        $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission);
 
         // Give each submission plugin a chance to process the reverting to draft.
         $plugins = $this->get_submission_plugins();
index 1a03ee5..8afd96f 100644 (file)
@@ -353,6 +353,7 @@ class assign_submission_onlinetext extends assign_submission_plugin {
                                                              'onlinetext',
                                                              'assignsubmission_onlinetext');
 
+            $onlinetext = trim($onlinetextsubmission->onlinetext);
             $shorttext = shorten_text($text, 140);
             $plagiarismlinks = '';
 
@@ -360,13 +361,13 @@ class assign_submission_onlinetext extends assign_submission_plugin {
                 require_once($CFG->libdir . '/plagiarismlib.php');
 
                 $plagiarismlinks .= plagiarism_get_links(array('userid' => $submission->userid,
-                    'content' => trim($onlinetextsubmission->onlinetext),
+                    'content' => $onlinetext,
                     'cmid' => $this->assignment->get_course_module()->id,
                     'course' => $this->assignment->get_course()->id,
                     'assignment' => $submission->assignment));
             }
             if ($text != $shorttext) {
-                $wordcount = get_string('numwords', 'assignsubmission_onlinetext', count_words($text));
+                $wordcount = get_string('numwords', 'assignsubmission_onlinetext', count_words($onlinetext));
 
                 return $plagiarismlinks . $wordcount . $shorttext;
             } else {
index 176f304..fe76fa7 100644 (file)
@@ -1,28 +1,2 @@
-cancel_moving,mod_feedback
-cannotunmap,mod_feedback
-cannotmapfeedback,mod_feedback
-line_values,mod_feedback
-mapcourses_help,mod_feedback
-max_args_exceeded,mod_feedback
-movedown_item,mod_feedback
-move_here,mod_feedback
-moveup_item,mod_feedback
-notavailable,mod_feedback
-parameters_missing,mod_feedback
-picture,mod_feedback
-picture_file_list,mod_feedback
-picture_values,mod_feedback
-preview,mod_feedback
-preview_help,mod_feedback
-radiorated,mod_feedback
-radiobutton,mod_feedback
-radiobutton_rated,mod_feedback
-relateditemsdeleted,mod_feedback
-separator_decimal,mod_feedback
-separator_thousand,mod_feedback
-saving_failed_because_missing_or_false_values,mod_feedback
 start,mod_feedback
-stop,mod_feedback
-switch_group,mod_feedback
-viewcompleted,mod_feedback
-viewcompleted_help,mod_feedback
+stop,mod_feedback
\ No newline at end of file
index 73eb880..a0ee5f1 100644 (file)
@@ -275,35 +275,6 @@ $string['use_one_line_for_each_value'] = 'Use one line for each answer!';
 $string['use_this_template'] = 'Use this template';
 $string['using_templates'] = 'Use a template';
 $string['vertical'] = 'vertical';
-// Deprecated since Moodle 3.1.
-$string['cannotmapfeedback'] = 'Database problem, unable to map feedback to course';
-$string['line_values'] = 'Rating';
-$string['mapcourses_help'] = 'Once you have selected the relevant course(s) from your search,
-you can associate them with this feedback using map course(s). Multiple courses may be selected by holding down the Apple or Ctrl key whilst clicking on the course names. A course may be disassociated from a feedback at any time.';
-$string['max_args_exceeded'] = 'Max 6 arguments can be handled, too many arguments for';
-$string['cancel_moving'] = 'Cancel moving';
-$string['movedown_item'] = 'Move this question down';
-$string['move_here'] = 'Move here';
-$string['moveup_item'] = 'Move this question up';
-$string['notavailable'] = 'this feedback is not available';
-$string['saving_failed_because_missing_or_false_values'] = 'Saving failed because missing or false values';
-$string['cannotunmap'] = 'Database problem, unable to unmap';
-$string['viewcompleted'] = 'completed feedbacks';
-$string['viewcompleted_help'] = 'You may view completed feedback forms, searchable by course and/or by question.
-Feedback responses may be exported to Excel.';
-$string['parameters_missing'] = 'Parameters missing from';
-$string['picture'] = 'Picture';
-$string['picture_file_list'] = 'List of pictures';
-$string['picture_values'] = 'Choose one or more<br />picture files from the list:';
-$string['preview'] = 'Preview';
-$string['preview_help'] = 'In the preview you can change the order of questions.';
-$string['switch_group'] = 'Switch group';
-$string['separator_decimal'] = '.';
-$string['separator_thousand'] = ',';
-$string['relateditemsdeleted'] = 'All responses for this question will also be deleted.';
-$string['radiorated'] = 'Radiobutton (rated)';
-$string['radiobutton'] = 'Multiple choice - single answer allowed (radio buttons)';
-$string['radiobutton_rated'] = 'Radiobutton (rated)';
 // Deprecated since Moodle 3.2.
 $string['start'] = 'Start';
 $string['stop'] = 'End';
diff --git a/mod/forum/lang/en/deprecated.txt b/mod/forum/lang/en/deprecated.txt
deleted file mode 100644 (file)
index caf084c..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-postmailinfo,mod_forum
-emaildigestupdated,mod_forum
-emaildigestupdated_default,mod_forum
-emaildigest_0,mod_forum
-emaildigest_1,mod_forum
-emaildigest_2,mod_forum
index 9ee8be1..abb8410 100644 (file)
@@ -555,13 +555,3 @@ $string['warnformorepost'] = 'Warning! There is more than one discussion in this
 $string['yournewquestion'] = 'Your new question';
 $string['yournewtopic'] = 'Your new discussion topic';
 $string['yourreply'] = 'Your reply';
-
-// Deprecated since Moodle 3.1.
-$string['postmailinfo'] = 'This is a copy of a message posted on the {$a} website.
-
-To reply click on this link:';
-$string['emaildigestupdated'] = 'The e-mail digest option was changed to \'{$a->maildigesttitle}\' for the forum \'{$a->forum}\'. {$a->maildigestdescription}';
-$string['emaildigestupdated_default'] = 'Your default profile setting of \'{$a->maildigesttitle}\' was used for the forum \'{$a->forum}\'. {$a->maildigestdescription}.';
-$string['emaildigest_0'] = 'You will receive one e-mail per forum post.';
-$string['emaildigest_1'] = 'You will receive one digest e-mail per day containing the complete contents of each forum post.';
-$string['emaildigest_2'] = 'You will receive one digest e-mail per day containing the subject of each forum post.';
index 7708529..43f5ca4 100644 (file)
@@ -2025,7 +2025,7 @@ function forum_search_posts($searchterms, $courseid=0, $limitfrom=0, $limitnum=5
         return false;
     }
 
-    $now = round(time(), -2); // db friendly
+    $now = floor(time() / 60) * 60; // DB Cache Friendly.
 
     $fullaccess = array();
     $where = array();
@@ -2467,7 +2467,7 @@ function forum_count_discussions($forum, $cm, $course) {
 
     static $cache = array();
 
-    $now = round(time(), -2); // db cache friendliness
+    $now = floor(time() / 60) * 60; // DB Cache Friendly.
 
     $params = array($course->id);
 
@@ -2567,7 +2567,7 @@ function forum_get_discussions($cm, $forumsort="", $fullpost=true, $unused=-1, $
 
     $timelimit = '';
 
-    $now = round(time(), -2);
+    $now = floor(time() / 60) * 60;
     $params = array($cm->instance);
 
     $modcontext = context_module::instance($cm->id);
@@ -2721,7 +2721,7 @@ function forum_get_discussion_neighbours($cm, $discussion, $forum) {
     }
 
     $neighbours = array('prev' => false, 'next' => false);
-    $now = round(time(), -2);
+    $now = floor(time() / 60) * 60;
     $params = array();
 
     $modcontext = context_module::instance($cm->id);
@@ -2889,7 +2889,7 @@ function forum_get_default_sort_order($desc = true, $compare = 'd.timemodified',
 function forum_get_discussions_unread($cm) {
     global $CFG, $DB, $USER;
 
-    $now = round(time(), -2);
+    $now = floor(time() / 60) * 60;
     $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
 
     $params = array();
@@ -2961,7 +2961,7 @@ function forum_get_discussions_unread($cm) {
 function forum_get_discussions_count($cm) {
     global $CFG, $DB, $USER;
 
-    $now = round(time(), -2);
+    $now = floor(time() / 60) * 60;
     $params = array($cm->instance);
     $groupmode    = groups_get_activity_groupmode($cm);
     $currentgroup = groups_get_activity_group($cm);
@@ -3516,10 +3516,11 @@ function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=fa
         }
         if (!empty($discussion->unread) && $discussion->unread !== '-') {
             $replystring .= ' <span class="sep">/</span> <span class="unread">';
+            $unreadlink = new moodle_url($discussionlink, null, 'unread');
             if ($discussion->unread == 1) {
-                $replystring .= get_string('unreadpostsone', 'forum');
+                $replystring .= html_writer::link($unreadlink, get_string('unreadpostsone', 'forum'));
             } else {
-                $replystring .= get_string('unreadpostsnumber', 'forum', $discussion->unread);
+                $replystring .= html_writer::link($unreadlink, get_string('unreadpostsnumber', 'forum', $discussion->unread));
             }
             $replystring .= '</span>';
         }
@@ -6378,7 +6379,7 @@ function forum_tp_is_post_old($post, $time=null) {
 function forum_tp_get_course_unread_posts($userid, $courseid) {
     global $CFG, $DB;
 
-    $now = round(time(), -2); // DB cache friendliness.
+    $now = floor(time() / 60) * 60; // DB cache friendliness.
     $cutoffdate = $now - ($CFG->forum_oldpostdays * 24 * 60 * 60);
     $params = array($userid, $userid, $courseid, $cutoffdate, $userid);
 
@@ -6477,7 +6478,7 @@ function forum_tp_count_forum_unread_posts($cm, $course, $resetreadcache = false
 
     list ($groups_sql, $groups_params) = $DB->get_in_or_equal($mygroups);
 
-    $now = round(time(), -2); // db cache friendliness
+    $now = floor(time() / 60) * 60; // DB Cache friendliness.
     $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
     $params = array($USER->id, $forumid, $cutoffdate);
 
@@ -7679,7 +7680,7 @@ function forum_get_posts_by_user($user, array $courses, $musthaveaccess = false,
     // Will record forums where the user can freely access everything
     $forumsearchfullaccess = array();
     // DB caching friendly
-    $now = round(time(), -2);
+    $now = floor(time() / 60) * 60;
     // For each course to search we want to find the forums the user has posted in
     // and providing the current user can access the forum create a search condition
     // for the forum to get the requested users posts.
index 6c301b7..4f41397 100644 (file)
@@ -146,7 +146,7 @@ function forum_rss_feed_discussions_sql($forum, $cm, $newsince=0) {
 
     $modcontext = null;
 
-    $now = round(time(), -2);
+    $now = floor(time() / 60) * 60; // DB Cache Friendly.
     $params = array();
 
     $modcontext = context_module::instance($cm->id);
index 56ae13a..bb5fbbc 100644 (file)
@@ -49,10 +49,6 @@ Feature: Students can edit or delete their forum posts within a set time limit
     And I press "Save changes"
     And I am on "Course 1" course homepage with editing mode on
     And I add the "Recent activity" block
-    And I add a "Forum" to section "1" and I fill the form with:
-      | Forum name | Test forum name |
-      | Forum type | Standard forum for general use |
-      | Description | Test forum description |
     And I log out
     And I log in as "student1"
     And I am on "Course 1" course homepage
index 69ac1fd..5aee4dc 100644 (file)
@@ -881,8 +881,8 @@ class mod_forum_lib_testcase extends advanced_testcase {
         // Adding timed discussions.
         $CFG->forum_enabletimedposts = true;
         $now = $record->timemodified;
-        $past = $now - 60;
-        $future = $now + 60;
+        $past = $now - 600;
+        $future = $now + 600;
 
         $record = new stdClass();
         $record->course = $course->id;
@@ -1125,8 +1125,8 @@ class mod_forum_lib_testcase extends advanced_testcase {
         // Adding timed discussions.
         $CFG->forum_enabletimedposts = true;
         $now = $record->timemodified;
-        $past = $now - 60;
-        $future = $now + 60;
+        $past = $now - 600;
+        $future = $now + 600;
 
         $record = new stdClass();
         $record->course = $course->id;
diff --git a/mod/lesson/lang/en/deprecated.txt b/mod/lesson/lang/en/deprecated.txt
deleted file mode 100644 (file)
index 7521da8..0000000
+++ /dev/null
@@ -1 +0,0 @@
-configactionaftercorrectanswer,mod_lesson
\ No newline at end of file
index a5cd95f..52cf786 100644 (file)
@@ -547,5 +547,3 @@ $string['yourcurrentgradeis'] = 'Your current grade is {$a}';
 $string['yourcurrentgradeisoutof'] = 'Your current grade is {$a->grade} out of {$a->total}';
 $string['youshouldview'] = 'You should answer at least: {$a}';
 
-// Deprecated since Moodle 3.1.
-$string['configactionaftercorrectanswer'] = 'The default action to take after a correct answer';
index 69e313a..c666bef 100644 (file)
@@ -4119,23 +4119,38 @@ abstract class lesson_page extends lesson_base {
                 $options->context = $context;
 
                 $result->feedback .= $OUTPUT->box(format_text($this->get_contents(), $this->properties->contentsformat, $options),
-                        'generalbox boxaligncenter');
-                $studentanswer = format_text($result->studentanswer, $result->studentanswerformat,
-                        array('context' => $context, 'para' => true));
+                        'generalbox boxaligncenter p-y-1');
                 $result->feedback .= '<div class="correctanswer generalbox"><em>'
-                        . get_string("youranswer", "lesson").'</em> : ' . $studentanswer;
-                if (isset($result->responseformat)) {
-                    $result->response = file_rewrite_pluginfile_urls($result->response, 'pluginfile.php', $context->id,
-                            'mod_lesson', 'page_responses', $result->answerid);
-                    $result->feedback .= $OUTPUT->box(format_text($result->response, $result->responseformat, $options)
-                            , $class);
-                } else {
-                    $result->feedback .= $OUTPUT->box($result->response, $class);
+                        . get_string("youranswer", "lesson").'</em> : <div class="studentanswer m-t-2 m-b-2">';
+
+                $studentanswerarray = explode('<br />', $result->studentanswer);
+                $responsearr = explode('<br />', $result->response);
+                $studentanswerresponse = array_combine($studentanswerarray, $responsearr);
+                // Create a table containing the answers and responses.
+                $table = new html_table();
+                foreach ($studentanswerresponse as $answer => $response) {
+                    // Add a table row containing the answer.
+                    $studentanswer = format_text($answer, $result->studentanswerformat,
+                            array('context' => $context, 'para' => true));
+                    $table->data[] = array($studentanswer);
+                    // If the response exists, add a table row containing the response. If not, add en empty row.
+                    if (!empty(trim($response))) {
+                        if (isset($result->responseformat)) {
+                            $convertstudentresponse = file_rewrite_pluginfile_urls($response, 'pluginfile.php',
+                                $context->id, 'mod_lesson', 'page_responses', $result->answerid);
+                            $studentresponse = format_text($convertstudentresponse, $result->responseformat, $options);
+                        } else {
+                            $studentresponse = $response;
+                        }
+                        $table->data[] = array('<em>'.get_string("response", "lesson").
+                            '</em>: <br/>'.$studentresponse);
+                    } else {
+                        $table->data[] = array('');
+                    }
                 }
-                $result->feedback .= '</div>';
+                $result->feedback .= html_writer::table($table).'</div></div>';
             }
         }
-
         return $result;
     }
 
index 38d314c..7f51e87 100644 (file)
@@ -169,13 +169,12 @@ class lesson_page_type_multichoice extends lesson_page {
             foreach ($answers as $answer) {
                 foreach ($studentanswers as $answerid) {
                     if ($answerid == $answer->id) {
-                        $result->studentanswer .= '<br />'.format_text($answer->answer, $answer->answerformat, $formattextdefoptions);
-                        if (trim(strip_tags($answer->response))) {
-                            $responses[$answerid] = format_text($answer->response, $answer->responseformat, $formattextdefoptions);
-                        }
+                        $studentanswerarray[] = format_text($answer->answer, $answer->answerformat, $formattextdefoptions);
+                        $responses[$answerid] = format_text($answer->response, $answer->responseformat, $formattextdefoptions);
                     }
                 }
             }
+            $result->studentanswer = implode('<br />', $studentanswerarray);
             $correctpageid = null;
             $wrongpageid = null;
 
index ef56c52..2903b14 100644 (file)
Binary files a/mod/lti/amd/build/contentitem_return.min.js and b/mod/lti/amd/build/contentitem_return.min.js differ
index 37d7c37..ee7ffd7 100644 (file)
@@ -36,6 +36,8 @@ define(['jquery'], function($) {
                 if (window != top) {
                     // Send return data to be processed by the parent window.
                     parent.processContentItemReturnData(returnData);
+                } else {
+                    window.processContentItemReturnData(returnData);
                 }
             });
         }
index 32a9566..3bf73d3 100644 (file)
@@ -1131,6 +1131,9 @@ EOD;
  * @return Array of enabled capabilities
  */
 function lti_get_enabled_capabilities($tool) {
+    if (!isset($tool)) {
+        return array();
+    }
     if (!empty($tool->enabledcapability)) {
         $enabledcapabilities = explode("\n", $tool->enabledcapability);
     } else {
index 31d4da9..3004702 100644 (file)
@@ -119,6 +119,8 @@ if (!empty($errormsg)) {
             //<![CDATA[
                 if(window != top){
                     top.location.href = '{$url}';
+                } else {
+                    window.location.href = '{$url}';
                 }
             //]]
             </script>
index d652b40..684dd2d 100644 (file)
@@ -865,7 +865,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes a single question structure.
      *
-     * @return external_single_structure the question structure
+     * @return external_single_structure the question data. Some fields may not be returned depending on the quiz display settings.
      * @since  Moodle 3.1
      * @since Moodle 3.2 blockedbyprevious parameter added.
      */
@@ -883,13 +883,18 @@ class mod_quiz_external extends external_api {
                                                             VALUE_OPTIONAL),
                 'flagged' => new external_value(PARAM_BOOL, 'whether the question is flagged or not'),
                 'number' => new external_value(PARAM_INT, 'question ordering number in the quiz', VALUE_OPTIONAL),
-                'state' => new external_value(PARAM_ALPHA, 'the state where the question is in', VALUE_OPTIONAL),
+                'state' => new external_value(PARAM_ALPHA, 'the state where the question is in.
+                    It will not be returned if the user cannot see it due to the quiz display correctness settings.',
+                    VALUE_OPTIONAL),
                 'status' => new external_value(PARAM_RAW, 'current formatted state of the question', VALUE_OPTIONAL),
                 'blockedbyprevious' => new external_value(PARAM_BOOL, 'whether the question is blocked by the previous question',
-                        VALUE_OPTIONAL),
-                'mark' => new external_value(PARAM_RAW, 'the mark awarded', VALUE_OPTIONAL),
-                'maxmark' => new external_value(PARAM_FLOAT, 'the maximum mark possible for this question attempt', VALUE_OPTIONAL),
-            )
+                    VALUE_OPTIONAL),
+                'mark' => new external_value(PARAM_RAW, 'the mark awarded.
+                    It will be returned only if the user is allowed to see it.', VALUE_OPTIONAL),
+                'maxmark' => new external_value(PARAM_FLOAT, 'the maximum mark possible for this question attempt.
+                    It will be returned only if the user is allowed to see it.', VALUE_OPTIONAL),
+            ),
+            'The question data. Some fields may not be returned depending on the quiz display settings.'
         );
     }
 
@@ -924,7 +929,10 @@ class mod_quiz_external extends external_api {
 
             if ($attemptobj->is_real_question($slot)) {
                 $question['number'] = $attemptobj->get_question_number($slot);
-                $question['state'] = (string) $attemptobj->get_question_state($slot);
+                $showcorrectness = $displayoptions->correctness && $attemptobj->get_question_attempt($slot)->has_marks();
+                if ($showcorrectness) {
+                    $question['state'] = (string) $attemptobj->get_question_state($slot);
+                }
                 $question['status'] = $attemptobj->get_question_status($slot, $displayoptions->correctness);
                 $question['blockedbyprevious'] = $attemptobj->is_blocked_by_previous_question($slot);
             }
index 72c2c39..f465f19 100644 (file)
@@ -36,15 +36,15 @@ Feature: Teachers can override the grade for any question
     And I press "Submit all and finish"
     And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
     And I log out
-    And I log in as "teacher1"
+
+  @javascript @_switch_window @_bug_phantomjs
+  Scenario: Validating the marking of an essay question attempt.
+    When I log in as "teacher1"
     And I am on "Course 1" course homepage
     And I follow "Quiz 1"
     And I follow "Attempts: 1"
     And I follow "Review attempt"
-
-  @javascript @_switch_window @_bug_phantomjs
-  Scenario: Validating the marking of an essay question attempt.
-    When I follow "Make comment or override mark"
+    And I follow "Make comment or override mark"
     And I switch to "commentquestion" window
     And I set the field "Mark" to "25"
     And I press "Save"
@@ -57,3 +57,35 @@ Feature: Teachers can override the grade for any question
     And I should see "Complete" in the "Manually graded 10 with comment: " "table_row"
     # This time is same as time the window is open. So wait for it to close before proceeding.
     And I wait "2" seconds
+
+  @javascript @_switch_window @_file_upload @_bug_phantomjs
+  Scenario: Comment on a response to an essay question attempt.
+    When I log in as "teacher1"
+    And I follow "Manage private files"
+    And I upload "mod/quiz/tests/fixtures/moodle_logo.jpg" file to "Files" filemanager
+    And I click on "Save changes" "button"
+    And I am on "Course 1" course homepage
+    And I follow "Quiz 1"
+    And I follow "Attempts: 1"
+    And I follow "Review attempt"
+    And I follow "Make comment or override mark"
+    And I switch to "commentquestion" window
+    And I set the field "Comment" to "Administrator's comment"
+    # Atto needs focus to add image, select empty p tag to do so.
+    And I select the text in the "Comment" Atto editor
+    And I click on "Image" "button" in the "[data-fieldtype=editor]" "css_element"
+    And I click on "Browse repositories..." "button"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "moodle_logo.jpg" "link"
+    And I click on "Select this file" "button"
+    And I set the field "Describe this image for someone who cannot see it" to "It's the logo"
+    And I click on "Save image" "button"
+    # Editor is not inserting the html for the image correctly
+    # when running under behat so line below manually inserts it.
+    And I set the field "Comment" to "<img src=\"@@PLUGINFILE@@/moodle_logo.jpg\" alt=\"It's the logo\" width=\"48\" height=\"48\" class=\"img-responsive atto_image_button_text-bottom\"><!-- File hash: a8e3ffba4ab315b3fb9187ebbf122fe9 -->"
+    And I press "Save" and switch to main window
+    And I switch to the main window
+    And I should see "It's the logo" in the "3" "table_row"
+    And "//*[contains(@class, 'comment')]//img[contains(@src, 'moodle_logo.jpg')]" "xpath_element" should exist
+    # This time is same as time the window is open. So wait for it to close before proceeding.
+    And I wait "2" seconds
index 9a03d6d..015ba42 100644 (file)
@@ -855,6 +855,9 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
         // Create a new quiz with one attempt started.
         list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
 
+        // Set correctness mask so questions state can be fetched only after finishing the attempt.
+        $DB->set_field('quiz', 'reviewcorrectness', mod_quiz_display_options::IMMEDIATELY_AFTER, array('id' => $quiz->id));
+
         $quizobj = $attemptobj->get_quizobj();
         $quizobj->preload_questions();
         $quizobj->load_questions();
@@ -873,7 +876,7 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals(1, $result['questions'][0]['slot']);
         $this->assertEquals(1, $result['questions'][0]['number']);
         $this->assertEquals('numerical', $result['questions'][0]['type']);
-        $this->assertEquals('todo', $result['questions'][0]['state']);
+        $this->assertArrayNotHasKey('state', $result['questions'][0]);  // We don't receive the state yet.
         $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']);
         $this->assertFalse($result['questions'][0]['flagged']);
         $this->assertEquals(0, $result['questions'][0]['page']);
@@ -894,7 +897,7 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals(2, $result['questions'][0]['slot']);
         $this->assertEquals(2, $result['questions'][0]['number']);
         $this->assertEquals('numerical', $result['questions'][0]['type']);
-        $this->assertEquals('todo', $result['questions'][0]['state']);
+        $this->assertArrayNotHasKey('state', $result['questions'][0]);  // We don't receive the state yet.
         $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']);
         $this->assertFalse($result['questions'][0]['flagged']);
         $this->assertEquals(1, $result['questions'][0]['page']);
@@ -905,6 +908,11 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
         // Finish previous attempt.
         $attemptobj->process_finish(time(), false);
 
+        // Now we should receive the question state.
+        $result = mod_quiz_external::get_attempt_review($attempt->id, 1);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
+        $this->assertEquals('gaveup', $result['questions'][0]['state']);
+
         // Change setting and expect two pages.
         $quiz->questionsperpage = 4;
         $DB->update_record('quiz', $quiz);
diff --git a/mod/quiz/tests/fixtures/moodle_logo.jpg b/mod/quiz/tests/fixtures/moodle_logo.jpg
new file mode 100644 (file)
index 0000000..f2d5365
Binary files /dev/null and b/mod/quiz/tests/fixtures/moodle_logo.jpg differ
diff --git a/mod/workshop/form/accumulative/lang/en/deprecated.txt b/mod/workshop/form/accumulative/lang/en/deprecated.txt
deleted file mode 100644 (file)
index a7d55aa..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-dimensioncomment,workshopform_accumulative
-dimensiongrade,workshopform_accumulative
index 31ab57f..43567c4 100644 (file)
@@ -48,6 +48,3 @@ $string['scalename5'] = 'Excellent/Very poor (5 point)';
 $string['scalename6'] = 'Excellent/Very poor (7 point)';
 $string['verypoor'] = 'Very poor';
 
-// Deprecated since Moodle 3.1.
-$string['dimensioncomment'] = 'Comment';
-$string['dimensiongrade'] = 'Grade';
diff --git a/mod/workshop/form/comments/lang/en/deprecated.txt b/mod/workshop/form/comments/lang/en/deprecated.txt
deleted file mode 100644 (file)
index a680205..0000000
+++ /dev/null
@@ -1 +0,0 @@
-dimensioncomment,workshopform_comments
index fb11fbe..7677acd 100644 (file)
@@ -29,5 +29,3 @@ $string['dimensiondescription'] = 'Description';
 $string['dimensionnumber'] = 'Aspect {$a}';
 $string['pluginname'] = 'Comments';
 
-// Deprecated since Moodle 3.1.
-$string['dimensioncomment'] = 'Comment';
diff --git a/mod/workshop/form/numerrors/lang/en/deprecated.txt b/mod/workshop/form/numerrors/lang/en/deprecated.txt
deleted file mode 100644 (file)
index c078309..0000000
+++ /dev/null
@@ -1 +0,0 @@
-dimensioncomment,workshopform_numerrors
index 55fd534..307cfed 100644 (file)
@@ -41,5 +41,3 @@ $string['mapgrade'] = 'Grade for submission';
 $string['percents'] = '{$a} %';
 $string['pluginname'] = 'Number of errors';
 
-// Deprecated since Moodle 3.1.
-$string['dimensioncomment'] = 'Comment';
index dc549a1..0be98a2 100644 (file)
@@ -1,3 +1,2 @@
 err_unknownfileextension,mod_workshop
 err_wrongfileextension,mod_workshop
-yourassessment,mod_workshop
index b09bb2a..a7b383f 100644 (file)
@@ -371,9 +371,6 @@ $string['yourassessmentfor'] = 'Your assessment for {$a}';
 $string['yourgrades'] = 'Your grades';
 $string['yoursubmission'] = 'Your submission';
 
-// Deprecated since Moodle 3.1.
-$string['yourassessment'] = 'Your assessment';
-
 // Deprecated since Moodle 3.4.
 $string['err_unknownfileextension'] = 'Unknown file extension: {$a}';
 $string['err_wrongfileextension'] = 'Some files ({$a->wrongfiles}) cannot be uploaded. Only file types {$a->whitelist} are allowed.';
index 6558785..3b9943f 100644 (file)
@@ -124,6 +124,17 @@ abstract class question_behaviour {
      */
     public function check_file_access($options, $component, $filearea, $args, $forcedownload) {
         $this->adjust_display_options($options);
+
+        if ($component == 'question' && $filearea == 'response_bf_comment') {
+            foreach ($this->qa->get_step_iterator() as $attemptstep) {
+                if ($attemptstep->get_id() == $args[0]) {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
         return $this->question->check_file_access($this->qa, $options, $component,
                 $filearea, $args, $forcedownload);
     }
@@ -202,7 +213,7 @@ abstract class question_behaviour {
             return array();
         }
 
-        $vars = array('comment' => PARAM_RAW, 'commentformat' => PARAM_INT);
+        $vars = array('comment' => question_attempt::PARAM_RAW_FILES, 'commentformat' => PARAM_INT);
         if ($this->qa->get_max_mark()) {
             $vars['mark'] = PARAM_RAW_TRIMMED;
             $vars['maxmark'] = PARAM_FLOAT;
@@ -507,15 +518,20 @@ abstract class question_behaviour {
      * @param $comment the comment text to format. If omitted,
      *      $this->qa->get_manual_comment() is used.
      * @param $commentformat the format of the comment, one of the FORMAT_... constants.
+     * @param $context the quiz context.
      * @return string the comment, ready to be output.
      */
-    public function format_comment($comment = null, $commentformat = null) {
+    public function format_comment($comment = null, $commentformat = null, $context = null) {
         $formatoptions = new stdClass();
         $formatoptions->noclean = true;
         $formatoptions->para = false;
 
         if (is_null($comment)) {
-            list($comment, $commentformat) = $this->qa->get_manual_comment();
+            list($comment, $commentformat, $commentstep) = $this->qa->get_manual_comment();
+        }
+
+        if ($context !== null) {
+            $comment = $this->qa->rewrite_response_pluginfile_urls($comment, $context->id, 'bf_comment', $commentstep);
         }
 
         return format_text($comment, $commentformat, $formatoptions);
@@ -528,8 +544,9 @@ abstract class question_behaviour {
     protected function summarise_manual_comment($step) {
         $a = new stdClass();
         if ($step->has_behaviour_var('comment')) {
-            $a->comment = shorten_text(html_to_text($this->format_comment(
-                    $step->get_behaviour_var('comment')), 0, false), 200);
+            list($comment, $commentformat, $commentstep) = $this->qa->get_manual_comment();
+            $comment = question_utils::to_plain_text($comment, $commentformat);
+            $a->comment = shorten_text($comment, 200);
         } else {
             $a->comment = '';
         }
index b474887..c791866 100644 (file)
@@ -69,9 +69,14 @@ abstract class qbehaviour_renderer extends plugin_renderer_base {
     }
 
     public function manual_comment_fields(question_attempt $qa, question_display_options $options) {
+        global $CFG;
+
+        require_once($CFG->dirroot.'/lib/filelib.php');
+        require_once($CFG->dirroot.'/repository/lib.php');
+
         $inputname = $qa->get_behaviour_field_name('comment');
         $id = $inputname . '_id';
-        list($commenttext, $commentformat) = $qa->get_current_manual_comment();
+        list($commenttext, $commentformat, $commentstep) = $qa->get_current_manual_comment();
 
         $editor = editors_get_preferred_editor($commentformat);
         $strformats = format_text_menu();
@@ -80,12 +85,27 @@ abstract class qbehaviour_renderer extends plugin_renderer_base {
             $formats[$fid] = $strformats[$fid];
         }
 
+        $draftitemareainputname = $qa->get_behaviour_field_name('comment:itemid');
+        $draftitemid = optional_param($draftitemareainputname, false, PARAM_INT);
+
+        if (!$draftitemid && $commentstep === null) {
+            $commenttext = '';
+            $draftitemid = file_get_unused_draft_itemid();
+        } else if (!$draftitemid) {
+            list($draftitemid, $commenttext) = $commentstep->prepare_response_files_draft_itemid_with_text(
+                    'bf_comment', $options->context->id, $commenttext);
+        }
+
         $editor->set_text($commenttext);
-        $editor->use_editor($id, array('context' => $options->context));
+        $editor->use_editor($id, question_utils::get_editor_options($options->context),
+                question_utils::get_filepicker_options($options->context, $draftitemid));
 
         $commenteditor = html_writer::tag('div', html_writer::tag('textarea', s($commenttext),
                 array('id' => $id, 'name' => $inputname, 'rows' => 10, 'cols' => 60)));
 
+        $attributes = ['type'  => 'hidden', 'name'  => $draftitemareainputname, 'value' => $draftitemid];
+        $commenteditor .= html_writer::empty_tag('input', $attributes);
+
         $editorformat = '';
         if (count($formats) == 1) {
             reset($formats);
@@ -105,7 +125,7 @@ abstract class qbehaviour_renderer extends plugin_renderer_base {
         $comment = html_writer::tag('div', html_writer::tag('div',
                 html_writer::tag('label', get_string('comment', 'question'),
                 array('for' => $id)), array('class' => 'fitemtitle')) .
-                html_writer::tag('div', $commenteditor, array('class' => 'felement fhtmleditor')),
+                html_writer::tag('div', $commenteditor, array('class' => 'felement fhtmleditor', 'data-fieldtype' => "editor")),
                 array('class' => 'fitem'));
         $comment .= $editorformat;
 
@@ -168,7 +188,7 @@ abstract class qbehaviour_renderer extends plugin_renderer_base {
     public function manual_comment_view(question_attempt $qa, question_display_options $options) {
         $output = '';
         if ($qa->has_manual_comment()) {
-            $output .= get_string('commentx', 'question', $qa->get_behaviour()->format_comment());
+            $output .= get_string('commentx', 'question', $qa->get_behaviour()->format_comment(null, null, $options->context));
         }
         if ($options->manualcommentlink) {
             $url = new moodle_url($options->manualcommentlink, array('slot' => $qa->get_slot()));
index 1889378..b65ce4b 100644 (file)
@@ -954,6 +954,65 @@ abstract class question_utils {
         $text = str_replace('@@PLUGINFILE@@/', 'http://example.com/', $text);
         return html_to_text(format_text($text, $format, $options), 0, false);
     }
+
+    /**
+     * Get the options required to configure the filepicker for one of the editor
+     * toolbar buttons.
+     * @param mixed $acceptedtypes array of types of '*'.
+     * @param int $draftitemid the draft area item id.
+     * @param object $context the context.
+     * @return object the required options.
+     */
+    protected static function specific_filepicker_options($acceptedtypes, $draftitemid, $context) {
+        $filepickeroptions = new stdClass();
+        $filepickeroptions->accepted_types = $acceptedtypes;
+        $filepickeroptions->return_types = FILE_INTERNAL | FILE_EXTERNAL;
+        $filepickeroptions->context = $context;
+        $filepickeroptions->env = 'filepicker';
+
+        $options = initialise_filepicker($filepickeroptions);
+        $options->context = $context;
+        $options->client_id = uniqid();
+        $options->env = 'editor';
+        $options->itemid = $draftitemid;
+
+        return $options;
+    }
+
+    /**
+     * Get filepicker options for question related text areas.
+     * @param object $context the context.
+     * @param int $draftitemid the draft area item id.
+     * @return array An array of options
+     */
+    public static function get_filepicker_options($context, $draftitemid) {
+        return [
+                'image' => self::specific_filepicker_options(['image'], $draftitemid, $context),
+                'media' => self::specific_filepicker_options(['video', 'audio'], $draftitemid, $context),
+                'link'  => self::specific_filepicker_options('*', $draftitemid, $context),
+            ];
+    }
+
+    /**
+     * Get editor options for question related text areas.
+     * @param object $context the context.
+     * @return array An array of options
+     */
+    public static function get_editor_options($context) {
+        global $CFG;
+
+        $editoroptions = [
+                'subdirs'  => 0,
+                'context'  => $context,
+                'maxfiles' => EDITOR_UNLIMITED_FILES,
+                'maxbytes' => $CFG->maxbytes,
+                'noclean' => 0,
+                'trusttext' => 0,
+                'autosave' => false
+        ];
+
+        return $editoroptions;
+    }
 }
 
 
index d295170..5473535 100644 (file)
@@ -1107,8 +1107,10 @@ class question_attempt {
             return null;
         }
 
-        return new question_file_saver($draftitemid, 'question', 'response_' .
-                str_replace($this->get_field_prefix(), '', $name), $text);
+        $filearea = str_replace($this->get_field_prefix(), '', $name);
+        $filearea = str_replace('-', 'bf_', $filearea);
+        $filearea = 'response_' . $filearea;
+        return new question_file_saver($draftitemid, 'question', $filearea, $text);
     }
 
     /**
@@ -1164,6 +1166,8 @@ class question_attempt {
                 $this->behaviour->get_expected_data(), $postdata, '-');
 
         $expected = $this->behaviour->get_expected_qt_data();
+        $this->check_qt_var_name_restrictions($expected);
+
         if ($expected === self::USE_RAW_DATA) {
             $submitteddata += $this->get_all_submitted_qt_vars($postdata);
         } else {
@@ -1172,6 +1176,23 @@ class question_attempt {
         return $submitteddata;
     }
 
+    /**
+     * Ensure that no reserved prefixes are being used by installed
+     * question types.
+     * @param array $expected An array of question type variables
+     */
+    protected function check_qt_var_name_restrictions($expected) {
+        global $CFG;
+
+        if ($CFG->debugdeveloper) {
+            foreach ($expected as $key => $value) {
+                if (strpos($key, 'bf_') !== false) {
+                    debugging('The bf_ prefix is reserved and cannot be used by question types', DEBUG_DEVELOPER);
+                }
+            }
+        }
+    }
+
     /**
      * Get a set of response data for this question attempt that would get the
      * best possible mark. If it is not possible to compute a correct
@@ -1370,16 +1391,17 @@ class question_attempt {
 
     /**
      * @return array(string, int) the most recent manual comment that was added
-     * to this question, and the FORMAT_... it is.
+     * to this question, the FORMAT_... it is and the step itself.
      */
     public function get_manual_comment() {
         foreach ($this->get_reverse_step_iterator() as $step) {
             if ($step->has_behaviour_var('comment')) {
                 return array($step->get_behaviour_var('comment'),
-                        $step->get_behaviour_var('commentformat'));
+                        $step->get_behaviour_var('commentformat'),
+                        $step);
             }
         }
-        return array(null, null);
+        return array(null, null, null);
     }
 
     /**
@@ -1399,7 +1421,7 @@ class question_attempt {
             if ($commentformat === null) {
                 $commentformat = FORMAT_HTML;
             }
-            return array($comment, $commentformat);
+            return array($comment, $commentformat, null);
         }
     }
 
index a6352ef..9b04c49 100644 (file)
@@ -358,28 +358,28 @@ class qtype_essay_format_editorfilepicker_renderer extends qtype_essay_format_ed
                 $name, $context->id, $step->get_qt_var($name));
     }
 
+    /**
+     * Get editor options for question response text area.
+     * @param object $context the context the attempt belongs to.
+     * @return array options for the editor.
+     */
     protected function get_editor_options($context) {
-        // Disable the text-editor autosave because quiz has it's own auto save function.
-        return array(
-            'subdirs' => 0,
-            'maxbytes' => 0,
-            'maxfiles' => -1,
-            'context' => $context,
-            'noclean' => 0,
-            'trusttext'=> 0,
-            'autosave' => false
-        );
+        return question_utils::get_editor_options($context);
     }
 
     /**
      * Get the options required to configure the filepicker for one of the editor
      * toolbar buttons.
+     * @deprecated since 3.5
      * @param mixed $acceptedtypes array of types of '*'.
      * @param int $draftitemid the draft area item id.
      * @param object $context the context.
      * @return object the required options.
      */
     protected function specific_filepicker_options($acceptedtypes, $draftitemid, $context) {
+        debugging('qtype_essay_format_editorfilepicker_renderer::specific_filepicker_options() is deprecated, ' .
+            'use question_utils::specific_filepicker_options() instead.', DEBUG_DEVELOPER);
+
         $filepickeroptions = new stdClass();
         $filepickeroptions->accepted_types = $acceptedtypes;
         $filepickeroptions->return_types = FILE_INTERNAL | FILE_EXTERNAL;
@@ -395,17 +395,13 @@ class qtype_essay_format_editorfilepicker_renderer extends qtype_essay_format_ed
         return $options;
     }
 
+    /**
+     * @param object $context the context the attempt belongs to.
+     * @param int $draftitemid draft item id.
+     * @return array filepicker options for the editor.
+     */
     protected function get_filepicker_options($context, $draftitemid) {
-        global $CFG;
-
-        return array(
-            'image' => $this->specific_filepicker_options(array('image'),
-                            $draftitemid, $context),
-            'media' => $this->specific_filepicker_options(array('video', 'audio'),
-                            $draftitemid, $context),
-            'link'  => $this->specific_filepicker_options('*',
-                            $draftitemid, $context),
-        );
+        return question_utils::get_filepicker_options($context, $draftitemid);
     }
 
     protected function filepicker_html($inputname, $draftitemid) {
diff --git a/report/outline/classes/filter_form.php b/report/outline/classes/filter_form.php
new file mode 100644 (file)
index 0000000..4e7dd0c
--- /dev/null
@@ -0,0 +1,74 @@
+<?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/>.
+
+/**
+ * Form to filter the outline report
+ *
+ * @package   report_outline
+ * @copyright 2017 Davo Smith, Synergy Learning
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace report_outline;
+
+defined('MOODLE_INTERNAL') || die();
+global $CFG;
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Class filter_form form to filter the results by date
+ * @package report_outline
+ */
+class filter_form extends \moodleform {
+    /**
+     * Form definition
+     * @throws \HTML_QuickForm_Error
+     * @throws \coding_exception
+     */
+    protected function definition() {
+        $mform = $this->_form;
+
+        $mform->addElement('hidden', 'id');
+        $mform->setType('id', PARAM_INT);
+
+        $mform->addElement('header', 'filterheader', get_string('filter'));
+        $opts = ['optional' => true];
+        $mform->addElement('date_selector', 'filterstartdate', get_string('from'), $opts);
+        $mform->addElement('date_selector', 'filterenddate', get_string('to'), $opts);
+
+        $mform->setExpanded('filterheader', false);
+
+        // Add the filter/cancel buttons (without 'closeHeaderBefore', so they collapse with the filter).
+        $buttonarray = [
+            $mform->createElement('submit', 'submitbutton', get_string('filter')),
+            $mform->createElement('cancel'),
+        ];
+        $mform->addGroup($buttonarray, 'buttonar', '', [' '], false);
+    }
+
+    /**
+     * Expand the form contents if the filter is in use.
+     * @throws \HTML_QuickForm_Error
+     */
+    public function definition_after_data() {
+        $mform = $this->_form;
+        $filterstartdate = $mform->getElement('filterstartdate')->getValue();
+        $filterenddate = $mform->getElement('filterenddate')->getValue();
+        if (!empty($filterstartdate['enabled']) || !empty($filterenddate['enabled'])) {
+            $mform->setExpanded('filterheader', true);
+        }
+    }
+}
index cd13403..b247098 100644 (file)
@@ -27,16 +27,45 @@ require('../../config.php');
 require_once($CFG->dirroot.'/report/outline/locallib.php');
 
 $id = required_param('id',PARAM_INT);       // course id
+$startdate = optional_param('startdate', null, PARAM_INT);
+$enddate = optional_param('enddate', null, PARAM_INT);
 
 $course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST);
 
-$PAGE->set_url('/report/outline/index.php', array('id'=>$id));
+$pageparams = array('id' => $id);
+if ($startdate) {
+    $pageparams['startdate'] = $startdate;
+}
+if ($enddate) {
+    $pageparams['enddate'] = $enddate;
+}
+
+$PAGE->set_url('/report/outline/index.php', $pageparams);
 $PAGE->set_pagelayout('report');
 
 require_login($course);
 $context = context_course::instance($course->id);
 require_capability('report/outline:view', $context);
 
+// Handle form to filter access logs by date.
+$filterform = new \report_outline\filter_form();
+$filterform->set_data(['id' => $course->id, 'filterstartdate' => $startdate, 'filterenddate' => $enddate]);
+if ($filterform->is_cancelled()) {
+    $redir = $PAGE->url;
+    $redir->remove_params(['startdate', 'enddate']);
+    redirect($redir);
+}
+if ($filter = $filterform->get_data()) {
+    $redir = $PAGE->url;
+    if ($filter->filterstartdate) {
+        $redir->param('startdate', $filter->filterstartdate);
+    }
+    if ($filter->filterenddate) {
+        $redir->param('enddate', $filter->filterenddate);
+    }
+    redirect($redir);
+}
+
 // Trigger an activity report viewed event.
 $event = \report_outline\event\activity_report_viewed::create(array('context' => $context));
 $event->trigger();
@@ -85,6 +114,8 @@ if ($useinternalreader) {
     }
 }
 
+$filterform->display();
+
 echo $OUTPUT->container(get_string('computedfromlogs', 'admin', userdate($minlog)), 'loginfo');
 
 $outlinetable = new html_table();
@@ -107,9 +138,19 @@ $modinfo = get_fast_modinfo($course);
 if ($uselegacyreader) {
     // If we are going to use the internal (not legacy) log table, we should only get records
     // from the legacy table that exist before we started adding logs to the new table.
+    $params = array('courseid' => $course->id, 'action' => 'view%', 'visible' => 1);
     $limittime = '';
     if (!empty($minloginternalreader)) {
         $limittime = ' AND time < :timeto ';
+        $params['timeto'] = $minloginternalreader;
+    }
+    if ($startdate) {
+        $limittime .= ' AND time >= :startdate ';
+        $params['startdate'] = $startdate;
+    }
+    if ($enddate) {
+        $limittime .= ' AND time < :enddate ';
+        $params['enddate'] = $enddate;
     }
     // Check if we need to show the last access.
     $sqllasttime = '';
@@ -127,10 +168,6 @@ if ($uselegacyreader) {
                AND $logactionlike
                AND m.visible = :visible $limittime
           GROUP BY cm.id";
-    $params = array('courseid' => $course->id, 'action' => 'view%', 'visible' => 1);
-    if (!empty($minloginternalreader)) {
-        $params['timeto'] = $minloginternalreader;
-    }
     $views = $DB->get_records_sql($sql, $params);
 }
 
@@ -141,14 +178,24 @@ if ($useinternalreader) {
     if ($showlastaccess) {
         $sqllasttime = ", MAX(timecreated) AS lasttime";
     }
+    $params = array('courseid' => $course->id, 'contextmodule' => CONTEXT_MODULE);
+    $limittime = '';
+    if ($startdate) {
+        $limittime .= ' AND timecreated >= :startdate ';
+        $params['startdate'] = $startdate;
+    }
+    if ($enddate) {
+        $limittime .= ' AND timecreated < :enddate ';
+        $params['enddate'] = $enddate;
+    }
     $sql = "SELECT contextinstanceid as cmid, COUNT('x') AS numviews, COUNT(DISTINCT userid) AS distinctusers $sqllasttime
               FROM {" . $logtable . "} l
              WHERE courseid = :courseid
                AND anonymous = 0
                AND crud = 'r'
                AND contextlevel = :contextmodule
+               $limittime
           GROUP BY contextinstanceid";
-    $params = array('courseid' => $course->id, 'contextmodule' => CONTEXT_MODULE);
     $v = $DB->get_records_sql($sql, $params);
 
     if (empty($views)) {
diff --git a/report/outline/tests/behat/behat_report_outline.php b/report/outline/tests/behat/behat_report_outline.php
new file mode 100644 (file)
index 0000000..01bad20
--- /dev/null
@@ -0,0 +1,68 @@
+<?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/>.
+
+/**
+ * Custom behat functions
+ *
+ * @package   report_outline
+ * @copyright 2017 Davo Smith, Synergy Learning
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
+
+/**
+ * Class behat_report_outline custom Behat steps for report_outline.
+ */
+class behat_report_outline extends behat_base {
+    /**
+     * This is a horrible, horrible hack, but it is not clear how else a range of log entries can be produced to test the
+     * filtering of the log entries.
+     *
+     * @Given /^the log timestamp for "(?P<username>(?:[^"]|\\")*)" and "(?P<activity_idnumber>(?:[^"]|\\")*)" is set to "(?P<date>(?:[^"]|\\")*)"$/
+     * @param string $username
+     * @param string $activityidnumber
+     * @param string $date
+     */
+    public function the_log_timestamp_for_and_is_set_to($username, $activityidnumber, $date) {
+        global $DB;
+
+        // Get the name of the log table.
+        $lm = get_log_manager();
+        $readers = $lm->get_readers('\\core\\log\\sql_internal_table_reader');
+        $reader = reset($readers);
+        $table = $reader->get_internal_log_table_name();
+
+        // Find the log entry.
+        $cmrec = $DB->get_record('course_modules', ['idnumber' => $activityidnumber], '*', MUST_EXIST);
+        $modname = $DB->get_field('modules', 'name', ['id' => $cmrec->module], MUST_EXIST);
+        $userid = $DB->get_field('user', 'id', ['username' => $username], MUST_EXIST);
+
+        $cond = [
+            'userid' => $userid,
+            'component' => 'mod_'.$modname,
+            'target' => 'course_module',
+            'action' => 'viewed',
+            'contextinstanceid' => $cmrec->id,
+        ];
+        $logentries = $DB->get_records($table, $cond, 'timecreated DESC', 'id', 0, 1);
+        $logentry = reset($logentries);
+
+        // Update the timecreated for the entry.
+        $timestamp = strtotime($date);
+        $DB->set_field($table, 'timecreated', $timestamp, ['id' => $logentry->id]);
+    }
+}
\ No newline at end of file
diff --git a/report/outline/tests/behat/filter.feature b/report/outline/tests/behat/filter.feature
new file mode 100644 (file)
index 0000000..7c97444
--- /dev/null
@@ -0,0 +1,94 @@
+@report @report_outline
+Feature: Filter an outline report
+  In order to ensure the outline report works as expected
+  As a teacher
+  I need to log in as a teacher and view the outline report with various filters in place
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+      | student2 | Student | 2 | student2@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+    When I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "Forum" to section "1" and I fill the form with:
+      | Forum name | Forum name |
+      | Description | Forum description |
+      | ID number| FORUM01 |
+    And I add a "Book" to section "1" and I fill the form with:
+      | Name | Book name |
+      | Description | Book description |
+      | ID number| BOOK01 |
+
+  Scenario: Filter the outline report by start date
+    Given I navigate to "Manage log stores" node in "Site administration > Plugins > Logging"
+    And "Enable" "link" should exist in the "Legacy log" "table_row"
+    And "Disable" "link" should exist in the "Standard log" "table_row"
+    And I log out
+    And I log in as "student1"
+    And I am on "Course 1" course homepage
+    And I follow "Forum name"
+    And the log timestamp for "student1" and "FORUM01" is set to "12 June 2017 12:49:00"
+    And I am on "Course 1" course homepage
+    And I follow "Book name"
+    And the log timestamp for "student1" and "BOOK01" is set to "10 June 2017 14:01:00"
+    And I log out
+    And I log in as "student2"
+    And I am on "Course 1" course homepage
+    And I follow "Book name"
+    And the log timestamp for "student2" and "BOOK01" is set to "14 June 2017 11:02:00"
+    And I log out
+    And I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I navigate to "Activity report" node in "Course administration > Reports"
+    And I should see "2 views by 2 users" in the "Book name" "table_row"
+    And I should see "1 views by 1 users" in the "Forum name" "table_row"
+    When I set the following fields to these values:
+      | filterstartdate[enabled] | 1    |
+      | filterstartdate[day]     | 12   |
+      | filterstartdate[month]   | June |
+      | filterstartdate[year]    | 2017 |
+    And I press "Filter"
+    Then I should see "1 views by 1 users" in the "Book name" "table_row"
+    And I should see "1 views by 1 users" in the "Forum name" "table_row"
+
+  Scenario: Filter the outline report by end date
+    Given I navigate to "Manage log stores" node in "Site administration > Plugins > Logging"
+    And "Enable" "link" should exist in the "Legacy log" "table_row"
+    And "Disable" "link" should exist in the "Standard log" "table_row"
+    And I log out
+    And I log in as "student1"
+    And I am on "Course 1" course homepage
+    And I follow "Forum name"
+    And the log timestamp for "student1" and "FORUM01" is set to "12 June 2017 12:49:00"
+    And I am on "Course 1" course homepage
+    And I follow "Book name"
+    And the log timestamp for "student1" and "BOOK01" is set to "10 June 2017 14:01:00"
+    And I log out
+    And I log in as "student2"
+    And I am on "Course 1" course homepage
+    And I follow "Book name"
+    And the log timestamp for "student2" and "BOOK01" is set to "14 June 2017 11:02:00"
+    And I log out
+    And I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I navigate to "Activity report" node in "Course administration > Reports"
+    And I should see "2 views by 2 users" in the "Book name" "table_row"
+    And I should see "1 views by 1 users" in the "Forum name" "table_row"
+    When I set the following fields to these values:
+      | filterenddate[enabled] | 1    |
+      | filterenddate[day]     | 11   |
+      | filterenddate[month]   | June |
+      | filterenddate[year]    | 2017 |
+    And I press "Filter"
+    Then I should see "1 views by 1 users" in the "Book name" "table_row"
+    And I should not see "views by" in the "Forum name" "table_row"
index 277fb95..48e7ea3 100644 (file)
@@ -1137,6 +1137,7 @@ M.core_filepicker.init = function(Y, options) {
             selectnode.one('.fp-setauthor input').set('value', args.author ? args.author : this.options.author);
             this.set_selected_license(selectnode.one('.fp-setlicense'), args.license);
             selectnode.one('form #filesource-'+client_id).set('value', args.source);
+            selectnode.one('form #filesourcekey-'+client_id).set('value', args.sourcekey);
 
             // display static information about a file (when known)
             var attrs = ['datemodified','datecreated','size','license','author','dimensions'];
@@ -1181,7 +1182,8 @@ M.core_filepicker.init = function(Y, options) {
                 var repository_id = this.active_repo.id;
                 var title = selectnode.one('.fp-saveas input').get('value');
                 var filesource = selectnode.one('form #filesource-'+client_id).get('value');
-                var params = {'title':title, 'source':filesource, 'savepath': this.options.savepath};
+                var filesourcekey = selectnode.one('form #filesourcekey-'+client_id).get('value');
+                var params = {'title':title, 'source':filesource, 'savepath': this.options.savepath, sourcekey: filesourcekey};
                 var license = selectnode.one('.fp-setlicense select');
                 if (license) {
                     params['license'] = license.get('value');
@@ -1243,6 +1245,8 @@ M.core_filepicker.init = function(Y, options) {
             var elform = selectnode.one('form');
             elform.appendChild(Y.Node.create('<input/>').
                 setAttrs({type:'hidden',id:'filesource-'+client_id}));
+            elform.appendChild(Y.Node.create('<input/>').
+                setAttrs({type:'hidden',id:'filesourcekey-'+client_id}));
             elform.on('keydown', function(e) {
                 if (e.keyCode == 13) {
                     getfile.simulate('click');
index 9597665..eddb0c6 100644 (file)
@@ -2231,6 +2231,11 @@ abstract class repository implements cacheable_object {
                 $file =& $list[$i];
                 $converttoobject = false;
             }
+
+            if (isset($file['source'])) {
+                $file['sourcekey'] = sha1($file['source'] . self::get_secret_key() . sesskey());
+            }
+
             if (isset($file['size'])) {
                 $file['size'] = (int)$file['size'];
                 $file['size_f'] = display_size($file['size']);
@@ -2837,6 +2842,20 @@ abstract class repository implements cacheable_object {
         debugging('The method repository::uses_post_requests() is deprecated and must not be used anymore.', DEBUG_DEVELOPER);
         return false;
     }
+
+    /**
+     * Generate a secret key to be used for passing sensitive information around.
+     *
+     * @return string repository secret key.
+     */
+    final static public function get_secret_key() {
+        global $CFG;
+
+        if (!isset($CFG->reposecretkey)) {
+            set_config('reposecretkey', time() . random_string(32));
+        }
+        return $CFG->reposecretkey;
+    }
 }
 
 /**
index 172d050..34a30f9 100644 (file)
@@ -99,10 +99,10 @@ if ($context->contextlevel == CONTEXT_COURSE) {
 if (!empty($new) && empty($edit)){
     $type = repository::get_type_by_typename($new);
 } else if (!empty($edit)){
-    $instance = repository::get_instance($edit);
+    $instance = repository::get_repository_by_id($edit, $context->id);
     $type = repository::get_type_by_id($instance->options['typeid']);
 } else if (!empty($delete)){
-    $instance = repository::get_instance($delete);
+    $instance = repository::get_repository_by_id($delete, $context->id);
     $type = repository::get_type_by_id($instance->options['typeid']);
 }
 
index 726486a..285bc4f 100644 (file)
@@ -40,6 +40,7 @@ $env       = optional_param('env', 'filepicker', PARAM_ALPHA);  // Opened in edi
 $license   = optional_param('license', $CFG->sitedefaultlicense, PARAM_TEXT);
 $author    = optional_param('author', '', PARAM_TEXT);          // File author
 $source    = optional_param('source', '', PARAM_RAW);           // File to download
+$sourcekey = optional_param('sourcekey', '', PARAM_RAW);        // Used to verify the source.
 $itemid    = optional_param('itemid', 0, PARAM_INT);            // Itemid
 $page      = optional_param('page', '', PARAM_RAW);             // Page
 $maxbytes  = optional_param('maxbytes', 0, PARAM_INT);          // Maxbytes
@@ -158,6 +159,16 @@ switch ($action) {
         // allow external links in url element all the time
         $allowexternallink = ($allowexternallink || ($env == 'url'));
 
+        // Validate the sourcekey.
+        if (empty($sourcekey)) {
+            throw new moodle_exception('missingsourcekey', 'repository');
+        }
+
+        // Check that the sourcekey matches.
+        if (sha1($source . repository::get_secret_key() . sesskey()) !== $sourcekey) {
+            throw new moodle_exception('sourcekeymismatch', 'repository');
+        }
+
         $reference = $repo->get_file_reference($source);
 
         // Use link of the files
index 97cdc7b..64831b1 100644 (file)
@@ -280,6 +280,16 @@ class document implements \renderable, \templatable {
         } else {
             // Replace all groups of line breaks and spaces by single spaces.
             $this->data[$fieldname] = preg_replace("/\s+/u", " ", $value);
+            if ($this->data[$fieldname] === null) {
+                if (isset($this->data['id'])) {
+                    $docid = $this->data['id'];
+                } else {
+                    $docid = '(unknown)';
+                }
+                throw new \moodle_exception('error_indexing', 'search', '', null, '"' . $fieldname .
+                        '" value causes preg_replace error (may be caused by unusual characters) ' .
+                        'in document with id "' . $docid . '"');
+            }
         }
 
         return $this->data[$fieldname];
index 4977095..6a4aa9b 100644 (file)
@@ -69,11 +69,7 @@ $values = explode('_', $rev);
 $rev = min_clean_param(array_shift($values), 'INT');
 $themesubrev = array_shift($values);
 
-if (is_null($themesubrev)) {
-    // Default to the current theme subrevision if one isn't
-    // provided in the URL.
-    $themesubrev = theme_get_sub_revision_for_theme($themename);
-} else {
+if (!is_null($themesubrev)) {
     $themesubrev = min_clean_param($themesubrev, 'INT');
 }
 
@@ -144,12 +140,12 @@ make_localcache_directory('theme', false);
 
 if ($type === 'editor') {
     $csscontent = $theme->get_css_content_editor();
-    css_store_css($theme, $candidatesheet, $csscontent, false);
 
     if ($cache) {
+        css_store_css($theme, $candidatesheet, $csscontent, false);
         css_send_cached_css($candidatesheet, $etag);
     } else {
-        css_send_uncached_css(file_get_contents($candidatesheet));
+        css_send_uncached_css($csscontent);
     }
 
 }
index 6d93553..5e1f44f 100644 (file)
@@ -6,57 +6,57 @@ Feature: View course participants
 
   Background:
     Given the following "users" exist:
-      | username  | firstname | lastname | email                 |
-      | teacher1  | Teacher   | 1        | teacher1@example.com  |
-      | student0  | Student   | 0        | student0@example.com  |
-      | student1  | Student   | 1        | student1@example.com  |
-      | student2  | Student   | 2        | student2@example.com  |
-      | student3  | Student   | 3        | student3@example.com  |
-      | student4  | Student   | 4        | student4@example.com  |
-      | student5  | Student   | 5        | student5@example.com  |
-      | student6  | Student   | 6        | student6@example.com  |
-      | student7  | Student   | 7        | student7@example.com  |
-      | student8  | Student   | 8        | student8@example.com  |
-      | student9  | Student   | 9        | student9@example.com  |
-      | student10 | Student   | 10       | student10@example.com |
-      | student11 | Student   | 11       | student11@example.com |
-      | student12 | Student   | 12       | student12@example.com |
-      | student13 | Student   | 13       | student13@example.com |
-      | student14 | Student   | 14       | student14@example.com |
-      | student15 | Student   | 15       | student15@example.com |
-      | student16 | Student   | 16       | student16@example.com |
-      | student17 | Student   | 17       | student17@example.com |
-      | student18 | Student   | 18       | student18@example.com |
-      | student19 | Student   | 19       | student19@example.com |
+      | username   | firstname | lastname | email                 |
+      | teacher1x  | Teacher   | 1x       | teacher1x@example.com  |
+      | student0x  | Student   | 0x       | student0x@example.com  |
+      | student1x  | Student   | 1x       | student1x@example.com  |
+      | student2x  | Student   | 2x       | student2x@example.com  |
+      | student3x  | Student   | 3x       | student3x@example.com  |
+      | student4x  | Student   | 4x       | student4x@example.com  |
+      | student5x  | Student   | 5x       | student5x@example.com  |
+      | student6x  | Student   | 6x       | student6x@example.com  |
+      | student7x  | Student   | 7x       | student7x@example.com  |
+      | student8x  | Student   | 8x       | student8x@example.com  |
+      | student9x  | Student   | 9x       | student9x@example.com  |
+      | student10x | Student   | 10x      | student10x@example.com |
+      | student11x | Student   | 11x      | student11x@example.com |
+      | student12x | Student   | 12x      | student12x@example.com |
+      | student13x | Student   | 13x      | student13x@example.com |
+      | student14x | Student   | 14x      | student14x@example.com |
+      | student15x | Student   | 15x      | student15x@example.com |
+      | student16x | Student   | 16x      | student16x@example.com |
+      | student17x | Student   | 17x      | student17x@example.com |
+      | student18x | Student   | 18x      | student18x@example.com |
+      | student19x | Student   | 19x      | student19x@example.com |
     And the following "courses" exist:
       | fullname | shortname | format |
       | Course 1 | C1        | topics |
     And the following "course enrolments" exist:
       | user      | course | role           | status | timeend |
-      | teacher1  | C1     | editingteacher |    0   |    0    |
-      | student0  | C1     | student        |    0   |    0    |
-      | student1  | C1     | student        |    0   |    0    |
-      | student2  | C1     | student        |    0   |    0    |
-      | student3  | C1     | student        |    0   |    0    |
-      | student4  | C1     | student        |    0   |    0    |
-      | student5  | C1     | student        |    0   |    0    |
-      | student6  | C1     | student        |    0   |    0    |
-      | student7  | C1     | student        |    0   |    0    |
-      | student8  | C1     | student        |    0   |    0    |
-      | student9  | C1     | student        |    0   |    0    |
-      | student10 | C1     | student        |    1   |    0    |
-      | student11 | C1     | student        |    0   |  100    |
-      | student12 | C1     | student        |    0   |    0    |
-      | student13 | C1     | student        |    0   |    0    |
-      | student14 | C1     | student        |    0   |    0    |
-      | student15 | C1     | student        |    0   |    0    |
-      | student16 | C1     | student        |    0   |    0    |
-      | student17 | C1     | student        |    0   |    0    |
-      | student18 | C1     | student        |    0   |    0    |
+      | teacher1x  | C1     | editingteacher |    0   |    0    |
+      | student0x  | C1     | student        |    0   |    0    |
+      | student1x  | C1     | student        |    0   |    0    |
+      | student2x  | C1     | student        |    0   |    0    |
+      | student3x  | C1     | student        |    0   |    0    |
+      | student4x  | C1     | student        |    0   |    0    |
+      | student5x  | C1     | student        |    0   |    0    |
+      | student6x  | C1     | student        |    0   |    0    |
+      | student7x  | C1     | student        |    0   |    0    |
+      | student8x  | C1     | student        |    0   |    0    |
+      | student9x  | C1     | student        |    0   |    0    |
+      | student10x | C1     | student        |    1   |    0    |
+      | student11x | C1     | student        |    0   |  100    |
+      | student12x | C1     | student        |    0   |    0    |
+      | student13x | C1     | student        |    0   |    0    |
+      | student14x | C1     | student        |    0   |    0    |
+      | student15x | C1     | student        |    0   |    0    |
+      | student16x | C1     | student        |    0   |    0    |
+      | student17x | C1     | student        |    0   |    0    |
+      | student18x | C1     | student        |    0   |    0    |
 
   @javascript
   Scenario: Use select and deselect all buttons
-    Given I log in as "teacher1"
+    Given I log in as "teacher1x"
     And I am on "Course 1" course homepage
     And I navigate to course participants
     When I press "Select all"
@@ -104,35 +104,35 @@ Feature: View course participants
     And the field with xpath "//tbody//tr[20]//input[@class='usercheckbox']" matches value "0"
 
   Scenario: Sort and paginate the list of users
-    Given I log in as "teacher1"
+    Given I log in as "teacher1x"
     And the following "course enrolments" exist:
       | user      | course | role           |
-      | student19 | C1     | student |
+      | student19x | C1     | student |
     And I am on "Course 1" course homepage
     And I navigate to course participants
     And I follow "Email address"
     When I follow "2"
-    Then I should not see "student0@example.com"
-    And I should not see "student19@example.com"
-    And I should see "teacher1@example.com"
+    Then I should not see "student0x@example.com"
+    And I should not see "student19x@example.com"
+    And I should see "teacher1x@example.com"
     And I follow "Email address"
     And I follow "2"
-    And I should not see "teacher1@example.com"
-    And I should not see "student19@example.com"
-    And I should not see "student1@example.com"
-    And I should see "student0@example.com"
+    And I should not see "teacher1x@example.com"
+    And I should not see "student19x@example.com"
+    And I should not see "student1x@example.com"
+    And I should see "student0x@example.com"
 
   @javascript
   Scenario: Use select all users on this page, select all n users and deselect all
     Given the following "course enrolments" exist:
       | user      | course | role    |
-      | student19 | C1     | student |
-    When I log in as "teacher1"
+      | student19x | C1     | student |
+    When I log in as "teacher1x"
     And I am on "Course 1" course homepage
     And I navigate to course participants
     And I follow "Surname"
     And I press "Select all users on this page"
-    Then I should not see "Student 9"
+    Then I should not see "Student 9x"
     And the field with xpath "//tbody//tr[1]//input[@class='usercheckbox']" matches value "1"
     And the field with xpath "//tbody//tr[2]//input[@class='usercheckbox']" matches value "1"
     And the field with xpath "//tbody//tr[3]//input[@class='usercheckbox']" matches value "1"
@@ -177,7 +177,7 @@ Feature: View course participants
     And the field with xpath "//tbody//tr[20]//input[@class='usercheckbox']" matches value "0"
 
     And I press "Select all 21 users"
-    And I should see "Student 9"
+    And I should see "Student 9x"
     And the field with xpath "//tbody//tr[1]//input[@class='usercheckbox']" matches value "1"
     And the field with xpath "//tbody//tr[2]//input[@class='usercheckbox']" matches value "1"
     And the field with xpath "//tbody//tr[3]//input[@class='usercheckbox']" matches value "1"
@@ -224,44 +224,44 @@ Feature: View course participants
     And the field with xpath "//tbody//tr[21]//input[@class='usercheckbox']" matches value "0"
 
   Scenario: View the participants page as a teacher
-    Given I log in as "teacher1"
+    Given I log in as "teacher1x"
     And I am on "Course 1" course homepage
     When I navigate to course participants
-    Then I should see "Active" in the "student0" "table_row"
-    And I should see "Active" in the "student1" "table_row"
-    And I should see "Active" in the "student2" "table_row"
-    And I should see "Active" in the "student3" "table_row"
-    And I should see "Active" in the "student4" "table_row"
-    And I should see "Active" in the "student5" "table_row"
-    And I should see "Active" in the "student6" "table_row"
-    And I should see "Active" in the "student7" "table_row"
-    And I should see "Active" in the "student8" "table_row"
-    And I should see "Active" in the "student9" "table_row"
-    And I should see "Suspended" in the "student10" "table_row"
-    And I should see "Not current" in the "student11" "table_row"
-    And I should see "Active" in the "student12" "table_row"
-    And I should see "Active" in the "student13" "table_row"
-    And I should see "Active" in the "student14" "table_row"
-    And I should see "Active" in the "student15" "table_row"
-    And I should see "Active" in the "student16" "table_row"
-    And I should see "Active" in the "student17" "table_row"
-    And I should see "Active" in the "student18" "table_row"
+    Then I should see "Active" in the "student0x" "table_row"
+    Then I should see "Active" in the "student1x" "table_row"
+    And I should see "Active" in the "student2x" "table_row"
+    And I should see "Active" in the "student3x" "table_row"
+    And I should see "Active" in the "student4x" "table_row"
+    And I should see "Active" in the "student5x" "table_row"
+    And I should see "Active" in the "student6x" "table_row"
+    And I should see "Active" in the "student7x" "table_row"
+    And I should see "Active" in the "student8x" "table_row"
+    And I should see "Active" in the "student9x" "table_row"
+    And I should see "Suspended" in the "student10x" "table_row"
+    And I should see "Not current" in the "student11x" "table_row"
+    And I should see "Active" in the "student12x" "table_row"
+    And I should see "Active" in the "student13x" "table_row"
+    And I should see "Active" in the "student14x" "table_row"
+    And I should see "Active" in the "student15x" "table_row"
+    And I should see "Active" in the "student16x" "table_row"
+    And I should see "Active" in the "student17x" "table_row"
+    And I should see "Active" in the "student18x" "table_row"
 
   Scenario: View the participants page as a student
-    Given I log in as "student1"
+    Given I log in as "student1x"
     And I am on "Course 1" course homepage
     When I navigate to course participants
     # Student should not see the status column.
     Then I should not see "Status" in the "participants" "table"
     # Student should be able to see the other actively-enrolled students.
-    And I should see "Student 1" in the "participants" "table"
-    And I should see "Student 2" in the "participants" "table"
-    And I should see "Student 3" in the "participants" "table"
-    And I should see "Student 4" in the "participants" "table"
-    And I should see "Student 5" in the "participants" "table"
-    And I should see "Student 6" in the "participants" "table"
-    And I should see "Student 7" in the "participants" "table"
-    And I should see "Student 8" in the "participants" "table"
+    And I should see "Student 1x" in the "participants" "table"
+    And I should see "Student 2x" in the "participants" "table"
+    And I should see "Student 3x" in the "participants" "table"
+    And I should see "Student 4x" in the "participants" "table"
+    And I should see "Student 5x" in the "participants" "table"
+    And I should see "Student 6x" in the "participants" "table"
+    And I should see "Student 7x" in the "participants" "table"
+    And I should see "Student 8x" in the "participants" "table"
     # Suspended and non-current students should not be rendered.
-    And I should not see "Student 10" in the "participants" "table"
-    And I should not see "Student 11" in the "participants" "table"
+    And I should not see "Student 10x" in the "participants" "table"
+    And I should not see "Student 11x" in the "participants" "table"
index 1d6be75..d5d5f8c 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2018010400.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2018011200.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.5dev (Build: 20180104)'; // Human-friendly version name
+$release  = '3.5dev (Build: 20180112)'; // Human-friendly version name
 
 $branch   = '35';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.