Merge branch 'MDL-50469-master' of git://github.com/junpataleta/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 22 Jan 2018 01:43:08 +0000 (02:43 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 22 Jan 2018 01:43:08 +0000 (02:43 +0100)
118 files changed:
admin/roles/classes/define_role_table_advanced.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/usertours/amd/build/usertours.min.js
admin/tool/usertours/amd/src/usertours.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/lib.php
course/moodleform_mod.php
enrol/database/lib.php
enrol/manual/amd/build/quickenrolment.min.js
enrol/manual/amd/src/quickenrolment.js
group/lib.php
install/lang/ug_lt/langconfig.php [new file with mode: 0644]
install/lang/ug_ug/admin.php [new file with mode: 0644]
install/lang/ug_ug/langconfig.php [new file with mode: 0644]
lang/en/deprecated.txt
lang/en/error.php
lang/en/moodle.php
lang/en/role.php
lang/en/table.php
lang/en/tag.php
lib/badgeslib.php
lib/classes/output/notification.php
lib/classes/task/manager.php
lib/grouplib.php
lib/outputlib.php
lib/outputrenderers.php
lib/tests/scheduled_task_test.php
lib/tests/theme_config_test.php
lib/upgrade.txt
mod/assign/backup/moodle2/restore_assign_stepslib.php
mod/assign/externallib.php
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/locallib.php
mod/assign/tests/externallib_test.php
mod/assign/upgrade.txt
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/tests/behat/edit_post_student.feature
mod/lesson/lang/en/deprecated.txt [deleted file]
mod/lesson/lang/en/lesson.php
mod/lti/amd/build/contentitem_return.min.js
mod/lti/amd/src/contentitem_return.js
mod/lti/return.php
mod/quiz/report/statistics/report.php
mod/quiz/tests/behat/manually_mark_question.feature
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/classes/statistics/questions/all_calculated_for_qubaid_condition.php
question/classes/statistics/questions/calculator.php
question/classes/statistics/responses/analysis_for_question.php
question/classes/statistics/responses/analysis_for_subpart.php
question/engine/lib.php
question/engine/questionattempt.php
question/type/calculated/pix/icon.svg [new file with mode: 0644]
question/type/calculatedmulti/pix/icon.svg [new file with mode: 0644]
question/type/calculatedsimple/pix/icon.svg [new file with mode: 0644]
question/type/ddimageortext/pix/icon.svg [new file with mode: 0644]
question/type/ddmarker/pix/icon.svg [new file with mode: 0644]
question/type/ddwtos/pix/icon.svg [new file with mode: 0644]
question/type/description/pix/icon.svg [new file with mode: 0644]
question/type/essay/pix/icon.svg [new file with mode: 0644]
question/type/essay/renderer.php
question/type/gapselect/pix/icon.svg [new file with mode: 0644]
question/type/match/pix/icon.svg [new file with mode: 0644]
question/type/missingtype/pix/icon.svg [new file with mode: 0644]
question/type/multianswer/pix/icon.svg [new file with mode: 0644]
question/type/multichoice/pix/icon.svg [new file with mode: 0644]
question/type/numerical/pix/icon.svg [new file with mode: 0644]
question/type/random/pix/icon.svg [new file with mode: 0644]
question/type/randomsamatch/pix/icon.svg [new file with mode: 0644]
question/type/shortanswer/pix/icon.svg [new file with mode: 0644]
question/type/truefalse/pix/icon.svg [new file with mode: 0644]
theme/styles.php
user/tests/behat/view_participants.feature
version.php

index 957fe85..16e188b 100644 (file)
@@ -103,6 +103,8 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
             $this->role->shortname = core_text::strtolower(clean_param($this->role->shortname, PARAM_ALPHANUMEXT));
             if (empty($this->role->shortname)) {
                 $this->errors['shortname'] = get_string('errorbadroleshortname', 'core_role');
+            } else if (core_text::strlen($this->role->shortname) > 100) { // Check if it exceeds the max of 100 characters.
+                $this->errors['shortname'] = get_string('errorroleshortnametoolong', 'core_role');
             }
         }
         if ($DB->record_exists_select('role', 'shortname = ? and id <> ?', array($this->role->shortname, $this->roleid))) {
@@ -496,7 +498,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
     }
 
     protected function get_shortname_field($id) {
-        return '<input type="text" id="' . $id . '" name="' . $id . '" maxlength="254" value="' . s($this->role->shortname) . '"' .
+        return '<input type="text" id="' . $id . '" name="' . $id . '" maxlength="100" value="' . s($this->role->shortname) . '"' .
                 ' class="form-control"/>';
     }
 
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 efc5448..3caa790 100644 (file)
Binary files a/admin/tool/usertours/amd/build/usertours.min.js and b/admin/tool/usertours/amd/build/usertours.min.js differ
index 044180b..70bd057 100644 (file)
@@ -54,6 +54,7 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
          * @param   {Number}    tourId      The ID of the tour to start.
          */
         fetchTour: function(tourId) {
+            M.util.js_pending('admin_usertour_fetchTour' + tourId);
             $.when(
                 ajax.call([
                     {
@@ -66,10 +67,16 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
                     }
                 ])[0],
                 templates.render('tool_usertours/tourstep', {})
-            ).then(function(response, template) {
-                usertours.startBootstrapTour(tourId, template[0], response.tourconfig);
+            )
+            .then(function(response, template) {
+                return usertours.startBootstrapTour(tourId, template[0], response.tourconfig);
+            })
+            .always(function() {
+                M.util.js_complete('admin_usertour_fetchTour' + tourId);
+
                 return;
-            }).fail(notification.exception);
+            })
+            .fail(notification.exception);
         },
 
         /**
@@ -79,6 +86,8 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
          */
         addResetLink: function() {
             var ele;
+            M.util.js_pending('admin_usertour_addResetLink');
+
             // Append the link to the most suitable place on the page
             // with fallback to legacy selectors and finally the body
             // if there is no better place.
@@ -92,9 +101,17 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
                 ele = $('body');
             }
             templates.render('tool_usertours/resettour', {})
-                .done(function(html, js) {
-                    templates.appendNodeContents(ele, html, js);
-                });
+            .then(function(html, js) {
+                templates.appendNodeContents(ele, html, js);
+
+                return;
+            })
+            .always(function() {
+                M.util.js_complete('admin_usertour_addResetLink');
+
+                return;
+            })
+            .fail();
         },
 
         /**
@@ -104,6 +121,7 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
          * @param   {Number}    tourId      The ID of the tour to start.
          * @param   {String}    template    The template to use.
          * @param   {Object}    tourConfig  The tour configuration.
+         * @return  {Object}
          */
         startBootstrapTour: function(tourId, template, tourConfig) {
             if (usertours.currentTour) {
@@ -147,7 +165,7 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
             });
 
             usertours.currentTour = new BootstrapTour(tourConfig);
-            usertours.currentTour.startTour();
+            return usertours.currentTour.startTour();
         },
 
         /**
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 423f605..6be5dc5 100644 (file)
@@ -3490,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 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 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');
     }
diff --git a/install/lang/ug_lt/langconfig.php b/install/lang/ug_lt/langconfig.php
new file mode 100644 (file)
index 0000000..e0974cd
--- /dev/null
@@ -0,0 +1,33 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thislanguage'] = 'Uyghur - latin';
diff --git a/install/lang/ug_ug/admin.php b/install/lang/ug_ug/admin.php
new file mode 100644 (file)
index 0000000..a7d5494
--- /dev/null
@@ -0,0 +1,34 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['clianswerno'] = 'ن';
+$string['cliansweryes'] = 'ي';
diff --git a/install/lang/ug_ug/langconfig.php b/install/lang/ug_ug/langconfig.php
new file mode 100644 (file)
index 0000000..cf7351c
--- /dev/null
@@ -0,0 +1,34 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thisdirection'] = 'rtl';
+$string['thislanguage'] = 'ئۇيغۇرچە';
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 7df89eb..80b20b6 100644 (file)
@@ -2150,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 8c8072e..f35cf01 100644 (file)
@@ -218,6 +218,7 @@ $string['errorbadrolename'] = 'Incorrect role name';
 $string['errorbadroleshortname'] = 'Incorrect role short name';
 $string['errorexistsrolename'] = 'Role name already exists';
 $string['errorexistsroleshortname'] = 'Role name already exists';
+$string['errorroleshortnametoolong'] = 'The short name must not exceed 100 characters';
 $string['eventroleallowassignupdated'] = 'Allow role assignment';
 $string['eventroleallowoverrideupdated'] = 'Allow role override';
 $string['eventroleallowswitchupdated'] = 'Allow role switch';
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 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 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 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 ba5f85a..f616a89 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.
 * The following functions have been finally deprecated and can not be used any more:
   - notify()
 
index 0146519..0987c53 100644 (file)
@@ -117,6 +117,8 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
         }
         if (!isset($data->gradingduedate)) {
             $data->gradingduedate = 0;
+        } else {
+            $data->gradingduedate = $this->apply_date_offset($data->gradingduedate);
         }
         if (!isset($data->markingworkflow)) {
             $data->markingworkflow = 0;
index c4059c9..7de404c 100644 (file)
@@ -2285,6 +2285,8 @@ class mod_assign_external extends external_api {
             array(
                 'assignid' => new external_value(PARAM_INT, 'assignment instance id'),
                 'userid' => new external_value(PARAM_INT, 'user id (empty for current user)', VALUE_DEFAULT, 0),
+                'groupid' => new external_value(PARAM_INT, 'filter by users in group (used for generating the grading summary).
+                    Empty or 0 for all groups information.', VALUE_DEFAULT, 0),
             )
         );
     }
@@ -2294,11 +2296,12 @@ class mod_assign_external extends external_api {
      *
      * @param int $assignid assignment instance id
      * @param int $userid user id (empty for current user)
+     * @param int $groupid filter by users in group id (used for generating the grading summary). Use 0 for all groups information.
      * @return array of warnings and grading, status, feedback and previous attempts information
      * @since Moodle 3.1
      * @throws required_capability_exception
      */
-    public static function get_submission_status($assignid, $userid = 0) {
+    public static function get_submission_status($assignid, $userid = 0, $groupid = 0) {
         global $USER;
 
         $warnings = array();
@@ -2306,6 +2309,7 @@ class mod_assign_external extends external_api {
         $params = array(
             'assignid' => $assignid,
             'userid' => $userid,
+            'groupid' => $groupid,
         );
         $params = self::validate_parameters(self::get_submission_status_parameters(), $params);
 
@@ -2325,8 +2329,18 @@ class mod_assign_external extends external_api {
         $gradingsummary = $lastattempt = $feedback = $previousattempts = null;
 
         // Get the renderable since it contais all the info we need.
-        if ($assign->can_view_grades()) {
-            $gradingsummary = $assign->get_assign_grading_summary_renderable();
+        if (!empty($params['groupid'])) {
+            $groupid = $params['groupid'];
+            // Determine is the group is visible to user.
+            if (!groups_group_visible($groupid, $course, $cm)) {
+                throw new moodle_exception('notingroup');
+            }
+        } else {
+            // A null gorups means that following functions will calculate the current group.
+            $groupid = null;
+        }
+        if ($assign->can_view_grades($groupid)) {
+            $gradingsummary = $assign->get_assign_grading_summary_renderable($groupid);
         }
 
         // Retrieve the rest of the renderable objects.
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 91fc26d..54adce0 100644 (file)
@@ -2133,9 +2133,10 @@ class assign {
      * This means the submission modification time is more recent than the
      * grading modification time and the status is SUBMITTED.
      *
+     * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
      * @return int number of matching submissions
      */
-    public function count_submissions_need_grading() {
+    public function count_submissions_need_grading($currentgroup = null) {
         global $DB;
 
         if ($this->get_instance()->teamsubmission) {
@@ -2143,7 +2144,9 @@ class assign {
             return 0;
         }
 
-        $currentgroup = groups_get_activity_group($this->get_course_module(), true);
+        if ($currentgroup === null) {
+            $currentgroup = groups_get_activity_group($this->get_course_module(), true);
+        }
         list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
 
         $params['assignid'] = $this->get_instance()->id;
@@ -2250,12 +2253,15 @@ class assign {
      * Load a count of submissions with a specified status.
      *
      * @param string $status The submission status - should match one of the constants
+     * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
      * @return int number of matching submissions
      */
-    public function count_submissions_with_status($status) {
+    public function count_submissions_with_status($status, $currentgroup = null) {
         global $DB;
 
-        $currentgroup = groups_get_activity_group($this->get_course_module(), true);
+        if ($currentgroup === null) {
+            $currentgroup = groups_get_activity_group($this->get_course_module(), true);
+        }
         list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
 
         $params['assignid'] = $this->get_instance()->id;
@@ -3283,17 +3289,21 @@ class assign {
     /**
      * Does this user have view grade or grade permission for this assignment?
      *
+     * @param mixed $groupid int|null when is set to a value, use this group instead calculating it
      * @return bool
      */
-    public function can_view_grades() {
+    public function can_view_grades($groupid = null) {
         // Permissions check.
         if (!has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
             return false;
         }
         // Checks for the edge case when user belongs to no groups and groupmode is sep.
         if ($this->get_course_module()->effectivegroupmode == SEPARATEGROUPS) {
+            if ($groupid === null) {
+                $groupid = groups_get_activity_allowed_groups($this->get_course_module());
+            }
             $groupflag = has_capability('moodle/site:accessallgroups', $this->get_context());
-            $groupflag = $groupflag || !empty(groups_get_activity_allowed_groups($this->get_course_module()));
+            $groupflag = $groupflag || !empty($groupid);
             return (bool)$groupflag;
         }
         return true;
@@ -5298,16 +5308,19 @@ class assign {
     /**
      * Creates an assign_grading_summary renderable.
      *
+     * @param mixed $activitygroup int|null the group for calculating the grading summary (if null the function will determine it)
      * @return assign_grading_summary renderable object
      */
-    public function get_assign_grading_summary_renderable() {
+    public function get_assign_grading_summary_renderable($activitygroup = null) {
 
         $instance = $this->get_instance();
 
         $draft = ASSIGN_SUBMISSION_STATUS_DRAFT;
         $submitted = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
 
-        $activitygroup = groups_get_activity_group($this->get_course_module());
+        if ($activitygroup === null) {
+            $activitygroup = groups_get_activity_group($this->get_course_module());
+        }
 
         if ($instance->teamsubmission) {
             $defaultteammembers = $this->get_submission_group_members(0, true);
@@ -5315,13 +5328,13 @@ class assign {
 
             $summary = new assign_grading_summary($this->count_teams($activitygroup),
                                                   $instance->submissiondrafts,
-                                                  $this->count_submissions_with_status($draft),
+                                                  $this->count_submissions_with_status($draft, $activitygroup),
                                                   $this->is_any_submission_plugin_enabled(),
-                                                  $this->count_submissions_with_status($submitted),
+                                                  $this->count_submissions_with_status($submitted, $activitygroup),
                                                   $instance->cutoffdate,
                                                   $instance->duedate,
                                                   $this->get_course_module()->id,
-                                                  $this->count_submissions_need_grading(),
+                                                  $this->count_submissions_need_grading($activitygroup),
                                                   $instance->teamsubmission,
                                                   $warnofungroupedusers,
                                                   $this->can_grade());
@@ -5330,13 +5343,13 @@ class assign {
             $countparticipants = $this->count_participants($activitygroup);
             $summary = new assign_grading_summary($countparticipants,
                                                   $instance->submissiondrafts,
-                                                  $this->count_submissions_with_status($draft),
+                                                  $this->count_submissions_with_status($draft, $activitygroup),
                                                   $this->is_any_submission_plugin_enabled(),
-                                                  $this->count_submissions_with_status($submitted),
+                                                  $this->count_submissions_with_status($submitted, $activitygroup),
                                                   $instance->cutoffdate,
                                                   $instance->duedate,
                                                   $this->get_course_module()->id,
-                                                  $this->count_submissions_need_grading(),
+                                                  $this->count_submissions_need_grading($activitygroup),
                                                   $instance->teamsubmission,
                                                   false,
                                                   $this->can_grade());
index e5a0ccf..1e9923e 100644 (file)
@@ -1829,7 +1829,10 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         require_once($CFG->dirroot . '/mod/assign/tests/base_test.php');
 
         // Create a course and assignment and users.
-        $course = self::getDataGenerator()->create_course();
+        $course = self::getDataGenerator()->create_course(array('groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1));
+
+        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
 
         $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
         $params = array(
@@ -1862,6 +1865,11 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
 
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $student1->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $teacher->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group2->id, 'userid' => $student2->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group2->id, 'userid' => $teacher->id));
+
         $this->setUser($student1);
 
         // Create a student1 with an online text submission.
@@ -1900,7 +1908,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
             $assign->submit_for_grading($data, $notices);
         }
 
-        return array($assign, $instance, $student1, $student2, $teacher);
+        return array($assign, $instance, $student1, $student2, $teacher, $group1, $group2);
     }
 
     /**
@@ -1909,7 +1917,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
     public function test_get_submission_status_in_draft_status() {
         $this->resetAfterTest(true);
 
-        list($assign, $instance, $student1, $student2, $teacher) = $this->create_submission_for_testing_status();
+        list($assign, $instance, $student1, $student2, $teacher, $g1, $g2) = $this->create_submission_for_testing_status();
         $studentsubmission = $assign->get_user_submission($student1->id, true);
 
         $result = mod_assign_external::get_submission_status($assign->get_instance()->id);
@@ -1965,7 +1973,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
     public function test_get_submission_status_in_submission_status() {
         $this->resetAfterTest(true);
 
-        list($assign, $instance, $student1, $student2, $teacher) = $this->create_submission_for_testing_status(true);
+        list($assign, $instance, $student1, $student2, $teacher, $g1, $g2) = $this->create_submission_for_testing_status(true);
 
         $result = mod_assign_external::get_submission_status($assign->get_instance()->id);
         // We expect debugging because of the $PAGE object, this won't happen in a normal WS request.
@@ -1995,11 +2003,12 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
     public function test_get_submission_status_in_submission_status_for_teacher() {
         $this->resetAfterTest(true);
 
-        list($assign, $instance, $student1, $student2, $teacher) = $this->create_submission_for_testing_status(true);
+        list($assign, $instance, $student1, $student2, $teacher, $g1, $g2) = $this->create_submission_for_testing_status(true);
 
         // Now, as teacher, see the grading summary.
         $this->setUser($teacher);
-        $result = mod_assign_external::get_submission_status($assign->get_instance()->id);
+        // First one group.
+        $result = mod_assign_external::get_submission_status($assign->get_instance()->id, 0, $g1->id);
         // We expect debugging because of the $PAGE object, this won't happen in a normal WS request.
         $this->assertDebuggingCalled();
         $result = external_api::clean_returnvalue(mod_assign_external::get_submission_status_returns(), $result);
@@ -2009,12 +2018,41 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         $this->assertFalse(isset($result['feedback']));
         $this->assertFalse(isset($result['previousattempts']));
 
-        $this->assertEquals(2, $result['gradingsummary']['participantcount']);
+        $this->assertEquals(1, $result['gradingsummary']['participantcount']);
         $this->assertEquals(0, $result['gradingsummary']['submissiondraftscount']);
         $this->assertEquals(1, $result['gradingsummary']['submissionsenabled']);
-        $this->assertEquals(1, $result['gradingsummary']['submissionssubmittedcount']);
-        $this->assertEquals(1, $result['gradingsummary']['submissionsneedgradingcount']);
+        $this->assertEquals(0, $result['gradingsummary']['submissiondraftscount']);
+        $this->assertEquals(1, $result['gradingsummary']['submissionssubmittedcount']);  // One student from G1 submitted.
+        $this->assertEquals(1, $result['gradingsummary']['submissionsneedgradingcount']);    // One student from G1 submitted.
         $this->assertFalse($result['gradingsummary']['warnofungroupedusers']);
+
+        // Second group.
+        $result = mod_assign_external::get_submission_status($assign->get_instance()->id, 0, $g2->id);
+        $result = external_api::clean_returnvalue(mod_assign_external::get_submission_status_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertEquals(1, $result['gradingsummary']['participantcount']);
+        $this->assertEquals(0, $result['gradingsummary']['submissionssubmittedcount']); // G2 students didn't submit yet.
+        $this->assertEquals(0, $result['gradingsummary']['submissionsneedgradingcount']);   // G2 students didn't submit yet.
+
+        // Should return also 1 participant if we allow the function to auto-select the group.
+        $result = mod_assign_external::get_submission_status($assign->get_instance()->id);
+        $result = external_api::clean_returnvalue(mod_assign_external::get_submission_status_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertEquals(1, $result['gradingsummary']['participantcount']);
+        $this->assertEquals(0, $result['gradingsummary']['submissiondraftscount']);
+        $this->assertEquals(1, $result['gradingsummary']['submissionssubmittedcount']); // One student from G1 submitted.
+        $this->assertEquals(1, $result['gradingsummary']['submissionsneedgradingcount']); // One student from G1 submitted.
+
+        // Now check draft submissions.
+        list($assign, $instance, $student1, $student2, $teacher, $g1, $g2) = $this->create_submission_for_testing_status(false);
+        $this->setUser($teacher);
+        $result = mod_assign_external::get_submission_status($assign->get_instance()->id, 0, $g1->id);
+        $result = external_api::clean_returnvalue(mod_assign_external::get_submission_status_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertEquals(1, $result['gradingsummary']['participantcount']);
+        $this->assertEquals(1, $result['gradingsummary']['submissiondraftscount']); // We have a draft submission.
+        $this->assertEquals(0, $result['gradingsummary']['submissionssubmittedcount']); // We have only draft submissions.
+        $this->assertEquals(0, $result['gradingsummary']['submissionsneedgradingcount']); // We have only draft submissions.
     }
 
     /**
@@ -2025,7 +2063,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
 
         $this->resetAfterTest(true);
 
-        list($assign, $instance, $student1, $student2, $teacher) = $this->create_submission_for_testing_status(true);
+        list($assign, $instance, $student1, $student2, $teacher, $g1, $g2) = $this->create_submission_for_testing_status(true);
         $studentsubmission = $assign->get_user_submission($student1->id, true);
 
         $this->setUser($teacher);
@@ -2118,7 +2156,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
     public function test_get_submission_status_access_control() {
         $this->resetAfterTest(true);
 
-        list($assign, $instance, $student1, $student2, $teacher) = $this->create_submission_for_testing_status();
+        list($assign, $instance, $student1, $student2, $teacher, $g1, $g2) = $this->create_submission_for_testing_status();
 
         $this->setUser($student2);
 
index e6cef21..2a12439 100644 (file)
@@ -1,5 +1,10 @@
 This files describes API changes in the assign code.
 
+=== 3.5 ===
+* Functions assign:get_assign_grading_summary_renderable, assign:can_view_submission, assign:count_submissions_with_status,
+  assign:count_submissions_need_grading and mod_assign_external::get_submission_status now admit an additional group parameter.
+  This parameter can be used to force those functions to retrieve data only for the given group.
+
 === 3.4 ===
 * assign::add_attempt requires that set_most_recent_team_submission() be called if attempting to use this function with a team
   submission.
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 eb1aa4e..43f5ca4 100644 (file)
@@ -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>';
         }
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
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 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 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 790be61..84cfe27 100644 (file)
@@ -201,7 +201,7 @@ class quiz_statistics_report extends quiz_default_report {
 
         } else if ($qid) {
             // Report on an individual sub-question indexed questionid.
-            if (is_null($questionstats->for_subq($qid, $variantno))) {
+            if (!$questionstats->has_subq($qid, $variantno)) {
                 print_error('questiondoesnotexist', 'question');
             }
 
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
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 bafebfe..592c126 100644 (file)
@@ -95,23 +95,41 @@ class all_calculated_for_qubaid_condition {
         }
     }
 
+    /**
+     * Do we have stats for a particular quesitonid (and optionally variant)?
+     *
+     * @param int  $questionid The id of the sub question.
+     * @param int|null $variant if not null then we want the object to store a variant of a sub-question's stats.
+     * @return bool whether those stats exist (yet).
+     */
+    public function has_subq($questionid, $variant = null) {
+        if ($variant === null) {
+            return isset($this->subquestionstats[$questionid]);
+        } else {
+            return isset($this->subquestionstats[$questionid]->variantstats[$variant]);
+        }
+    }
+
     /**
      * Reference for a item stats instance for a questionid and optional variant no.
      *
      * @param int  $questionid The id of the sub question.
      * @param int|null $variant if not null then we want the object to store a variant of a sub-question's stats.
-     * @return calculated_for_subquestion|null null if the stats object does not yet exist.
+     * @return calculated|calculated_for_subquestion stats instance for a questionid and optional variant no.
+     *     Will be a calculated_for_subquestion if no variant specified.
+     * @throws \coding_exception if there is an attempt to respond to a non-existant set of stats.
      */
     public function for_subq($questionid, $variant = null) {
         if ($variant === null) {
             if (!isset($this->subquestionstats[$questionid])) {
-                return null;
+                throw new \coding_exception('Reference to unknown question id ' . $questionid);
             } else {
                 return $this->subquestionstats[$questionid];
             }
         } else {
             if (!isset($this->subquestionstats[$questionid]->variantstats[$variant])) {
-                return null;
+                throw new \coding_exception('Reference to unknown question id ' . $questionid .
+                        ' variant ' . $variant);
             } else {
                 return $this->subquestionstats[$questionid]->variantstats[$variant];
             }
@@ -136,23 +154,39 @@ class all_calculated_for_qubaid_condition {
         return array_keys($this->questionstats);
     }
 
+    /**
+     * Do we have stats for a particular slot (and optionally variant)?
+     *
+     * @param int  $slot The slot no.
+     * @param int|null $variant if provided then we want the object which stores a variant of a position's stats.
+     * @return bool whether those stats exist (yet).
+     */
+    public function has_slot($slot, $variant = null) {
+        if ($variant === null) {
+            return isset($this->questionstats[$slot]);
+        } else {
+            return isset($this->questionstats[$slot]->variantstats[$variant]);
+        }
+    }
+
     /**
      * Get position stats instance for a slot and optional variant no.
      *
      * @param int  $slot The slot no.
-     * @param null $variant if provided then we want the object which stores a variant of a position's stats.
-     * @return calculated|null An instance of the class storing the calculated position stats.
+     * @param int|null $variant if provided then we want the object which stores a variant of a position's stats.
+     * @return calculated|calculated_for_subquestion An instance of the class storing the calculated position stats.
+     * @throws \coding_exception if there is an attempt to respond to a non-existant set of stats.
      */
     public function for_slot($slot, $variant = null) {
         if ($variant === null) {
             if (!isset($this->questionstats[$slot])) {
-                return null;
+                throw new \coding_exception('Reference to unknown slot ' . $slot);
             } else {
                 return $this->questionstats[$slot];
             }
         } else {
             if (!isset($this->questionstats[$slot]->variantstats[$variant])) {
-                return null;
+                throw new \coding_exception('Reference to unknown slot ' . $slot . ' variant ' . $variant);
             } else {
                 return $this->questionstats[$slot]->variantstats[$variant];
             }
index 4f0f285..6624912 100644 (file)
@@ -106,7 +106,7 @@ class calculator {
                 $israndomquestion = ($step->questionid != $this->stats->for_slot($step->slot)->questionid);
                 $breakdownvariants = !$israndomquestion && $this->stats->for_slot($step->slot)->break_down_by_variant();
                 // If this is a variant we have not seen before create a place to store stats calculations for this variant.
-                if ($breakdownvariants && is_null($this->stats->for_slot($step->slot , $step->variant))) {
+                if ($breakdownvariants && !$this->stats->has_slot($step->slot, $step->variant)) {
                     $question = $this->stats->for_slot($step->slot)->question;
                     $this->stats->initialise_for_slot($step->slot, $question, $step->variant);
                     $this->stats->for_slot($step->slot, $step->variant)->randomguessscore =
@@ -118,14 +118,14 @@ class calculator {
 
                 // If this is a random question do the calculations for sub question stats.
                 if ($israndomquestion) {
-                    if (is_null($this->stats->for_subq($step->questionid))) {
+                    if (!$this->stats->has_subq($step->questionid)) {
                         $this->stats->initialise_for_subq($step);
                     } else if ($this->stats->for_subq($step->questionid)->maxmark != $step->maxmark) {
                         $this->stats->for_subq($step->questionid)->differentweights = true;
                     }
 
                     // If this is a variant of this subq we have not seen before create a place to store stats calculations for it.
-                    if (is_null($this->stats->for_subq($step->questionid, $step->variant))) {
+                    if (!$this->stats->has_subq($step->questionid, $step->variant)) {
                         $this->stats->initialise_for_subq($step, $step->variant);
                     }
 
index 5be3e42..3e06f3c 100644 (file)
@@ -128,6 +128,11 @@ class analysis_for_question {
         if (!isset($this->subparts[$variantno])) {
             $this->initialise_stats_for_variant($variantno);
         }
+        if (!isset($this->subparts[$variantno][$subpartid])) {
+            debugging('Unexpected sub-part id ' . $subpartid .
+                    ' encountered.');
+            $this->subparts[$variantno][$subpartid] = new analysis_for_subpart();
+        }
         return $this->subparts[$variantno][$subpartid];
     }
 
index a9e8a86..cbb7352 100644 (file)
@@ -47,6 +47,11 @@ namespace core_question\statistics\responses;
  */
 class analysis_for_subpart {
 
+    /**
+     * @var analysis_for_class[]
+     */
+    protected $responseclasses;
+
     /**
      * Takes an array of possible_responses as returned from {@link \question_type::get_possible_responses()}.
      *
@@ -57,14 +62,11 @@ class analysis_for_subpart {
             foreach ($responseclasses as $responseclassid => $responseclass) {
                 $this->responseclasses[$responseclassid] = new analysis_for_class($responseclass, $responseclassid);
             }
+        } else {
+            $this->responseclasses = [];
         }
     }
 
-    /**
-     * @var analysis_for_class[]
-     */
-    protected $responseclasses;
-
     /**
      * Unique ids for response classes.
      *
@@ -81,7 +83,12 @@ class analysis_for_subpart {
      * @return analysis_for_class
      */
     public function get_response_class($classid) {
+        if (!isset($this->responseclasses[$classid])) {
+            debugging('Unexpected class id ' . $classid . ' encountered.');
+            $this->responseclasses[$classid] = new analysis_for_class('[Unknown]', $classid);
+        }
         return $this->responseclasses[$classid];
+
     }
 
     /**
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);
         }
     }
 
diff --git a/question/type/calculated/pix/icon.svg b/question/type/calculated/pix/icon.svg
new file mode 100644 (file)
index 0000000..2e44dfe
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M11.197 15v-1.232h1.231V15h-1.231zm1.119-1.657h-1.119v-.291a1.935 1.935 0 0 1 .179-.896 2.72 2.72 0 0 1 .716-.784c.228-.173.441-.364.639-.571a.772.772 0 0 0 .156-.47.793.793 0 0 0-.235-.616 1.114 1.114 0 0 0-.772-.257 1.121 1.121 0 0 0-.784.268 1.42 1.42 0 0 0-.437.818l-1.119-.135c.029-.518.272-1 .671-1.332a2.366 2.366 0 0 1 1.624-.549 2.454 2.454 0 0 1 1.679.549c.391.314.621.786.627 1.288a1.405 1.405 0 0 1-.235.772 5.05 5.05 0 0 1-.985.985c-.192.14-.356.315-.482.516a1.749 1.749 0 0 0-.123.705zm-10.637.022v-1.119h4.479v1.119H1.679zm0-1.937v-1.119h4.479v1.119H1.679zm13.582-5.676v1.119h-4.299c.049-.437.194-.857.425-1.231a8.204 8.204 0 0 1 1.377-1.512 8.714 8.714 0 0 0 1.008-1.052c.159-.219.249-.48.257-.751a.851.851 0 0 0-.257-.627.943.943 0 0 0-1.221 0c-.173.22-.264.493-.257.773l-1.187-.112A2.007 2.007 0 0 1 11.79.903a2.306 2.306 0 0 1 1.444-.436c.545-.03 1.08.155 1.49.515.354.328.55.793.537 1.276a2.22 2.22 0 0 1-.157.829c-.123.295-.289.57-.492.817a7.781 7.781 0 0 1-.807.817q-.582.538-.739.706a2.264 2.264 0 0 0-.246.336h2.441v-.011zm-7.703.224V4.262h-1.68V3.143h1.68V1.486h1.119v1.657h1.736v1.119H8.677v1.714H7.558zM5.05 5.752v1.119H.75c.049-.437.194-.858.426-1.231a8.192 8.192 0 0 1 1.399-1.512c.369-.321.71-.674 1.019-1.052.159-.219.249-.48.258-.751a.858.858 0 0 0-.224-.627.822.822 0 0 0-.605-.224.818.818 0 0 0-.616.236 1.186 1.186 0 0 0-.257.772L.896 2.359A2.004 2.004 0 0 1 1.579.903 2.302 2.302 0 0 1 3.023.456a2.082 2.082 0 0 1 1.489.515c.355.328.55.793.538 1.276 0 .284-.053.565-.157.829-.125.294-.29.569-.493.817a7.772 7.772 0 0 1-.806.817l-.728.672a2.195 2.195 0 0 0-.246.336h2.43v.034z"/></g></svg>
\ No newline at end of file
diff --git a/question/type/calculatedmulti/pix/icon.svg b/question/type/calculatedmulti/pix/icon.svg
new file mode 100644 (file)
index 0000000..f469484
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M11.012 15.594v-1.181h1.18v1.181h-1.18zm1.073-1.588h-1.073v-.279a1.856 1.856 0 0 1 .172-.859c.18-.281.414-.526.687-.719a5.2 5.2 0 0 0 .611-.547.737.737 0 0 0 .15-.526.758.758 0 0 0-.225-.547 1.067 1.067 0 0 0-.74-.247 1.077 1.077 0 0 0-.752.257c-.223.207-.37.483-.418.784l-1.073-.129a1.79 1.79 0 0 1 .644-1.277c.434-.364.99-.551 1.556-.526a2.348 2.348 0 0 1 1.609.526c.375.301.595.754.601 1.234a1.348 1.348 0 0 1-.225.741c-.27.356-.588.674-.944.944a1.78 1.78 0 0 0-.462.493 1.666 1.666 0 0 0-.118.677zm-10.194 0v-1.074h4.292v1.074H1.891zm0-1.857v-1.073h4.292v1.073H1.891zm13.016-6.664v1.073h-4.12a2.81 2.81 0 0 1 .408-1.18 7.79 7.79 0 0 1 1.319-1.449c.35-.309.673-.646.966-1.009a1.3 1.3 0 0 0 .247-.719.83.83 0 0 0-.247-.601.901.901 0 0 0-1.169 0c-.167.21-.255.473-.247.741l-1.17-.118a1.918 1.918 0 0 1 .655-1.395 2.212 2.212 0 0 1 1.384-.419 2 2 0 0 1 1.427.494c.34.315.528.76.515 1.223.001.272-.05.542-.15.794a3.208 3.208 0 0 1-.472.784 7.619 7.619 0 0 1-.773.783q-.558.515-.708.676a2.018 2.018 0 0 0-.236.322h2.371zm-7.383.225V4.101H5.915V3.028h1.609v-1.61h1.074v1.577h1.663v1.074H8.598V5.71H7.524zm-2.403-.225v1.073H1c.046-.419.186-.822.408-1.18a7.813 7.813 0 0 1 1.341-1.449c.342-.309.658-.647.944-1.009.153-.209.239-.46.247-.719a.817.817 0 0 0-.214-.601.784.784 0 0 0-.58-.182.786.786 0 0 0-.59.225c-.167.21-.254.473-.247.741l-1.18-.161A1.921 1.921 0 0 1 1.783.828 2.213 2.213 0 0 1 3.178.409a2.002 2.002 0 0 1 1.428.494c.339.315.527.76.515 1.223 0 .272-.051.542-.151.794a3.208 3.208 0 0 1-.472.784 7.609 7.609 0 0 1-.772.783q-.558.515-.709.676a2.13 2.13 0 0 0-.236.322h2.34z"/><path vector-effect="non-scaling-stroke" stroke-width="1.073" stroke="#000" stroke-miterlimit="10" d="M1.676 8.071h12.877"/></g></svg>
\ No newline at end of file
diff --git a/question/type/calculatedsimple/pix/icon.svg b/question/type/calculatedsimple/pix/icon.svg
new file mode 100644 (file)
index 0000000..4121df6
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M3.676 8.93l2.249-.229c.072.631.362 1.218.819 1.658a2.495 2.495 0 0 0 1.679.53 2.566 2.566 0 0 0 1.681-.471c.353-.261.562-.671.569-1.108a.996.996 0 0 0-.239-.689 1.899 1.899 0 0 0-.84-.48c-.268-.093-.881-.26-1.84-.5a6.303 6.303 0 0 1-2.608-1.129 3.002 3.002 0 0 1-1.06-2.31 2.938 2.938 0 0 1 .511-1.63c.346-.521.84-.925 1.42-1.16a5.708 5.708 0 0 1 2.249-.399 4.835 4.835 0 0 1 3.249.95 3.338 3.338 0 0 1 1.14 2.519l-2.31.101a2.014 2.014 0 0 0-.641-1.27 2.326 2.326 0 0 0-1.46-.391 2.644 2.644 0 0 0-1.579.41.827.827 0 0 0-.37.71c0 .253.109.493.301.66a6.073 6.073 0 0 0 2.13.77c.861.174 1.701.449 2.499.819a3.23 3.23 0 0 1 1.3 1.21c.32.552.479 1.183.46 1.819a3.385 3.385 0 0 1-.55 1.84 3.28 3.28 0 0 1-1.55 1.279 6.524 6.524 0 0 1-2.489.42 5.002 5.002 0 0 1-3.34-1 4.347 4.347 0 0 1-1.38-2.929z" fill="#999"/><path d="M11.197 15v-1.232h1.231V15h-1.231zm1.119-1.657h-1.119v-.291a1.935 1.935 0 0 1 .179-.896 2.72 2.72 0 0 1 .716-.784c.228-.173.441-.364.639-.571a.772.772 0 0 0 .156-.47.791.791 0 0 0-.235-.616 1.112 1.112 0 0 0-.772-.257 1.123 1.123 0 0 0-.784.268 1.42 1.42 0 0 0-.437.818l-1.119-.135c.029-.518.272-1 .671-1.332a2.366 2.366 0 0 1 1.624-.549 2.454 2.454 0 0 1 1.679.549c.391.314.621.786.627 1.288a1.405 1.405 0 0 1-.235.772 5.05 5.05 0 0 1-.985.985c-.192.14-.356.315-.482.516a1.752 1.752 0 0 0-.123.705zm-10.637.022v-1.119h4.479v1.119H1.679zm0-1.937v-1.119h4.479v1.119H1.679zm13.582-5.676v1.119h-4.299c.049-.437.194-.857.425-1.231a8.236 8.236 0 0 1 1.377-1.512 8.714 8.714 0 0 0 1.008-1.052c.159-.219.249-.48.257-.751a.848.848 0 0 0-.257-.627.943.943 0 0 0-1.221 0c-.173.22-.264.493-.257.773l-1.187-.112A2.007 2.007 0 0 1 11.79.903a2.304 2.304 0 0 1 1.444-.436c.545-.03 1.08.155 1.49.515.354.328.55.793.537 1.276a2.22 2.22 0 0 1-.157.829c-.123.295-.289.57-.492.817a7.781 7.781 0 0 1-.807.817q-.582.538-.739.706a2.317 2.317 0 0 0-.246.336h2.441v-.011zm-7.703.224V4.262h-1.68V3.143h1.68V1.486h1.119v1.657h1.736v1.119H8.677v1.714H7.558zM5.05 5.752v1.119H.75c.049-.437.194-.858.426-1.231a8.192 8.192 0 0 1 1.399-1.512c.369-.321.71-.674 1.019-1.052.159-.219.249-.48.258-.751a.856.856 0 0 0-.224-.627.822.822 0 0 0-.605-.224.818.818 0 0 0-.616.236 1.186 1.186 0 0 0-.257.772L.896 2.359A2.004 2.004 0 0 1 1.579.903 2.302 2.302 0 0 1 3.023.456a2.082 2.082 0 0 1 1.489.515c.355.328.55.793.538 1.276 0 .284-.053.565-.157.829-.125.294-.29.569-.493.817a7.772 7.772 0 0 1-.806.817l-.728.672a2.195 2.195 0 0 0-.246.336h2.43v.034z"/></g></svg>
\ No newline at end of file
diff --git a/question/type/ddimageortext/pix/icon.svg b/question/type/ddimageortext/pix/icon.svg
new file mode 100644 (file)
index 0000000..2c1686c
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M6.85 9.15v3.3h-1.6l2.8 2.8 2.6-2.8h-1.5v-3.3h3.3v1.5l2.8-2.6-2.8-2.8v1.6h-3.3v-3.3h1.5L7.95.75l-2.7 2.8h1.6v3.3h-3.3v-1.6l-2.8 2.8 2.8 2.6v-1.5h3.3zm.9-.9h.5v-.5h-.5v.5z"/></g></svg>
\ No newline at end of file
diff --git a/question/type/ddmarker/pix/icon.svg b/question/type/ddmarker/pix/icon.svg
new file mode 100644 (file)
index 0000000..610302a
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><clipPath id="b"><path d="M.35.3h15.3v15.4H.35V.3z" fill="#FFF"/></clipPath><g clip-path="url(#b)"><path d="M9.139 11.539v.85h1.5l-2.6 2.9-2.8-2.9h1.6v-.85h2.3zm-4.695-2.35h-.85v1.5l-2.9-2.6 2.9-2.8v1.6h.85v2.3zm2.406-4.65v-.85h-1.5l2.6-2.9 2.8 2.9h-1.6v.85h-2.3zm4.639 2.25h.85v-1.5l2.9 2.6-2.9 2.8v-1.6h-.85v-2.3zm-.939 4.129h-.145L8.239 9.089l.505-.512 1.879 2.194a.153.153 0 0 1-.073.147zM9.394 7.406L9.25 7.26l-.289-.146-.289.146-1.661-1.39a.37.37 0 0 0 0-.512l-.216-.146a.214.214 0 0 0-.289 0L4.917 6.821a.221.221 0 0 0 0 .293l.144.146.289.146h.217l1.372 1.61a.368.368 0 0 0 0 .512l.144.146a.214.214 0 0 0 .289 0l2.022-2.048a.221.221 0 0 0 0-.22z"/></g></g></svg>
\ No newline at end of file
diff --git a/question/type/ddwtos/pix/icon.svg b/question/type/ddwtos/pix/icon.svg
new file mode 100644 (file)
index 0000000..d8a7142
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M9.173 11.5v.85h1.5l-2.6 2.9-2.8-2.9h1.6v-.85h2.3zM4.478 9.15h-.85v1.5l-2.9-2.6 2.9-2.8v1.6h.85v2.3zM6.884 4.5v-.85h-1.5L7.983.75l2.801 2.9h-1.6v.85h-2.3zM11.522 6.75h.851v-1.5l2.899 2.6-2.899 2.8v-1.6h-.851v-2.3zM5.361 5h5.279v1.76h-.24a1.601 1.601 0 0 0-.719-1.28l-.96-.16v4.64a.876.876 0 0 0 .16.64c.215.133.468.189.72.16V11h-3.12v-.24a1.04 1.04 0 0 0 .64-.16.885.885 0 0 0 .16-.64V5.32l-.96.16A1.523 1.523 0 0 0 5.6 6.76h-.239V5z"/></g></svg>
\ No newline at end of file
diff --git a/question/type/description/pix/icon.svg b/question/type/description/pix/icon.svg
new file mode 100644 (file)
index 0000000..f376056
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M1.5 12.5h12.7M1.5 2.5h13m0 10.5V2m-13 11V2" fill="none" vector-effect="non-scaling-stroke" stroke="#000" stroke-miterlimit="10"/><path d="M2 3h12v9H2V3z" fill="#FFF"/><path d="M3 4.5h2m-2 6h2m-2-2h5m-2 2h2m3-4h2m-10 0h3m0-2h6m-5 2h3" fill="#A5B0B0" vector-effect="non-scaling-stroke" stroke="#A5B0B0" stroke-miterlimit="10"/><path d="M9 8.5h4m-3 1h1" fill="#0092FF" vector-effect="non-scaling-stroke" stroke="#0092FF" stroke-miterlimit="10"/><path vector-effect="non-scaling-stroke" stroke="#824E5D" stroke-miterlimit="10" d="M11 9.5h1"/><path vector-effect="non-scaling-stroke" stroke="#009F00" stroke-miterlimit="10" d="M9 9.5h1"/><path d="M9 10.5h4m-1-1h1" fill="#239771" vector-effect="non-scaling-stroke" stroke="#239771" stroke-miterlimit="10"/></g></svg>
\ No newline at end of file
diff --git a/question/type/essay/pix/icon.svg b/question/type/essay/pix/icon.svg
new file mode 100644 (file)
index 0000000..242f5a6
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M2 15.5h12M2 .6h12m-.5 14.9V.6m-11 14.9V.6" fill="none" vector-effect="non-scaling-stroke" stroke="#000" stroke-miterlimit="10"/><path d="M3 1h10v14H3V1z" fill="#FFF"/><path d="M4 7.5h1m6 4h1m-3-2h3m-5 2h3m-4-4h3m-1-2h4m-8 8.1h3m3-6.1h2m-4 6.1h2M4 9.5h4m-4 2h2m-2-6h3" fill="#A5B0B0" vector-effect="non-scaling-stroke" stroke="#A5B0B0" stroke-miterlimit="10"/><path d="M9 2.5h2m-6 0h3" fill="#7C7C7C" vector-effect="non-scaling-stroke" stroke="#7C7C7C" stroke-miterlimit="10"/></g></svg>
\ No newline at end of file
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/question/type/gapselect/pix/icon.svg b/question/type/gapselect/pix/icon.svg
new file mode 100644 (file)
index 0000000..43397c1
--- /dev/null
@@ -0,0 +1 @@
+<svg id="Ebene_3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.st0{fill:#fff}.st0,.st1{stroke:#000;stroke-miterlimit:10}</style><path class="st0" d="M2 6zM0 0zM14 7zM8 11z"/><path class="st1" d="M8 12L3 7h10z"/></svg>
\ No newline at end of file
diff --git a/question/type/match/pix/icon.svg b/question/type/match/pix/icon.svg
new file mode 100644 (file)
index 0000000..b70cdc9
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)" stroke="#000" stroke-miterlimit="10"><path d="M6 13.5h4m-4-5h4m-4-5h4" fill="none" vector-effect="non-scaling-stroke"/><path d="M12.5 13.5a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0-5a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0-5a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-11.2 0a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0 5a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0 5a1 1 0 1 1 2 0 1 1 0 0 1-2 0z" vector-effect="non-scaling-stroke"/></g></svg>
\ No newline at end of file
diff --git a/question/type/missingtype/pix/icon.svg b/question/type/missingtype/pix/icon.svg
new file mode 100644 (file)
index 0000000..4f06e79
--- /dev/null
@@ -0,0 +1 @@
+<svg id="Ebene_3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.st0,.st1{fill:#fff;stroke:#000;stroke-miterlimit:10}.st1{fill:none}.st2{font-family:&apos;ArialMT&apos;}.st3{font-size:18px}</style><path class="st0" d="M2 6zM0 0zM14 7zM8 11z"/><text transform="translate(2.656 14.533)" class="st2 st3">?</text></svg>
\ No newline at end of file
diff --git a/question/type/multianswer/pix/icon.svg b/question/type/multianswer/pix/icon.svg
new file mode 100644 (file)
index 0000000..405eb40
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M9 1h6v3H9V1zM8 12h3v3H8v-3zM1 6h7v3H1V6z"/><path d="M1 1h2v3H1V1zm4 0h2v3H5V1zM1 12h5v3H1v-3zm12 0h2v3h-2v-3zm-3-6h5v3h-5V6z" fill="#8E8E8E"/></g></svg>
\ No newline at end of file
diff --git a/question/type/multichoice/pix/icon.svg b/question/type/multichoice/pix/icon.svg
new file mode 100644 (file)
index 0000000..0e3655d
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M7 3h7v1H7V3zm0 10h7v1H7v-1zm0-5h7v1H7V8z"/><path d="M2 13.5a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0-10a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0 5a1 1 0 1 1 2 0 1 1 0 0 1-2 0z" vector-effect="non-scaling-stroke" stroke="#000" stroke-miterlimit="10"/></g></svg>
\ No newline at end of file
diff --git a/question/type/numerical/pix/icon.svg b/question/type/numerical/pix/icon.svg
new file mode 100644 (file)
index 0000000..8308cc9
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><clipPath id="b"><path d="M0 2.5h16v11H0v-11z" fill="#FFF"/></clipPath><g clip-path="url(#b)"><path d="M10.915 6.004v.779H8.092c0-.292.097-.584.292-.779.267-.346.56-.671.876-.973.195-.292.487-.584.681-.779a.739.739 0 0 0 .195-.487c0-.194-.097-.292-.195-.389-.194-.195-.584-.195-.778 0-.098.097-.195.195-.195.389h-.779c0-.389.195-.681.487-.973.194-.195.584-.292.876-.292.389 0 .681.097.973.292.292.292.39.584.39.876 0 .195 0 .39-.098.584-.097.195-.194.39-.292.487-.194.195-.292.389-.486.487-.39.292-.487.389-.584.486-.098.098-.098.195-.195.195l1.655.097zm-3.894.682h-.876V3.668c-.292.292-.584.487-.974.584v-.681c.195-.098.487-.195.682-.39.194-.194.389-.389.486-.681h.682v4.186zm7.788 1.947H1.18v3.894h14.602v.973H.207V7.659h15.575v4.868h-.973V8.633z"/><path d="M1.18 8.633h13.629v3.894H1.18V8.633z" fill="#FFF"/></g></g></svg>
\ No newline at end of file
diff --git a/question/type/random/pix/icon.svg b/question/type/random/pix/icon.svg
new file mode 100644 (file)
index 0000000..90153c2
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M14.7 5.5l-.096 5.97-5.008 3.23L9.5 8.143 14.7 5.5zM7.8 1.1l5.7 2.3-4.9 3-5.8-2.8 5-2.5zm-.1 7l.1 6.7-5.9-3.2-.1-6.7 5.9 3.2z" vector-effect="non-scaling-stroke" stroke="#000" stroke-miterlimit="10"/><path d="M7.9.1s-.1 0-.1.1L1.6 3.6c-.1 0 0 0 0 .1l6.9 3.5c.1.1.2.1.3 0l6.1-3.7c.1-.1.1-.1 0-.2L8.2.3C8.1.1 8 .1 7.9.1zm-.6.8h.4c.6 0 1.1.3 1.1.6 0 .2-.5.5-1.1.5-.6 0-1.1-.3-1.1-.6 0-.2.3-.4.7-.5zM3.8 2.8h.4c.6 0 1.1.3 1.1.6s-.5.6-1.1.6c-.6 0-1.1-.3-1.1-.6s.3-.5.7-.6zm5.7-.9h.4c.6 0 1.1.3 1.1.6s-.5.6-1.1.6c-.6-.1-1-.3-1-.7 0-.2.3-.4.6-.5zM1.2 4c0 .1 0 .1 0 0l.1 7.9c0 .1 0 .1.1.1l6.7 3.7c.2.1.2.1.2-.1v-8c0-.1 0-.2-.1-.2L1.2 4c.1.1 0 0 0 0zM6 3.9h.4c.6 0 1.1.3 1.1.6s-.5.6-1.1.5c-.6 0-1.1-.2-1-.5 0-.3.2-.5.6-.6zm5.8-1h.4c.6 0 1.1.3 1.1.6s-.5.6-1.1.6c-.6 0-1.1-.3-1.1-.6.1-.3.3-.5.7-.6zM1.9 5.4c0-.1 0-.1 0 0 .3-.1.7.1 1 .5.3.5.3 1.1.1 1.4-.3.2-.8.1-1.2-.4-.3-.5-.4-1.2-.1-1.4.1-.1.1-.1.2-.1zM8.3 5h.4c.6 0 1.1.3 1.1.6s-.5.6-1.1.6c-.6 0-1.1-.3-1.1-.6.1-.3.3-.5.7-.6zm7-1.3s0 .1 0 0L9 7.6c-.1.1-.1.1-.1.2v7.9c0 .1 0 .2.1.1l6.2-3.9c.1-.1.1-.1.1-.2v-8c.1 0 .1 0 0 0zm-.8 1.5c.2 0 .4 0 .5.2.2.3.1 1-.3 1.4-.4.4-.9.5-1.1.2-.2-.3-.1-1 .3-1.4.2-.2.4-.4.6-.4zM6.4 7.6c.3 0 .6.2.9.5.3.5.4 1.1.1 1.4-.3.3-.8.1-1.1-.4-.4-.5-.4-1.1-.1-1.4 0 0 .1 0 .2-.1zM4.1 9c.3 0 .7.2.9.5.3.5.4 1.1.1 1.4-.2.3-.7.1-1.1-.4-.3-.5-.4-1.1-.1-1.4.1 0 .1-.1.2-.1zm-2.2 1.4c.1 0 .1 0 0 0 .3-.1.7.1 1 .5.3.5.4 1.1.1 1.4-.3.3-.8.1-1.1-.4-.3-.5-.4-1.1-.1-1.4 0-.1.1-.1.1-.1zm10.7-1.5c.2 0 .4 0 .5.2.2.3.1 1-.3 1.4-.4.4-.9.5-1.1.2-.2-.3-.1-1 .3-1.4.2-.2.4-.3.6-.4zm-6.3 3.9c.1-.1.1-.1 0 0 .3-.1.7.1 1 .5.3.5.4 1.1.1 1.4-.3.3-.8.1-1.1-.4-.3-.5-.4-1.1-.1-1.4 0-.1.1-.1.1-.1zm4.2-.1c.2 0 .4 0 .5.2.2.3.1 1-.3 1.4-.4.4-.9.5-1.1.2-.2-.3-.1-1 .3-1.4.2-.2.4-.4.6-.4z" fill="#C4CCCB"/></g></svg>
\ No newline at end of file
diff --git a/question/type/randomsamatch/pix/icon.svg b/question/type/randomsamatch/pix/icon.svg
new file mode 100644 (file)
index 0000000..495a33f
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><clipPath id="b"><path d="M0 .571h16v14.858H0V.571z" fill="#FFF"/></clipPath><g clip-path="url(#b)"><path d="M13.183 13.632a1.127 1.127 0 1 1 2.253-.071 1.127 1.127 0 0 1-2.253.071zm0-5.633a1.129 1.129 0 1 1 1.127 1.093 1.129 1.129 0 0 1-1.127-1.093zm0-5.634a1.128 1.128 0 1 1 2.254-.073 1.128 1.128 0 0 1-2.254.073zm-12.62 0a1.13 1.13 0 0 1 1.703-1.006A1.127 1.127 0 1 1 .563 2.365zm0 5.634a1.129 1.129 0 1 1 1.128 1.093A1.129 1.129 0 0 1 .563 8v-.001zm0 5.633a1.13 1.13 0 0 1 1.703-1.006 1.127 1.127 0 0 1-.576 2.099 1.129 1.129 0 0 1-1.127-1.093z" vector-effect="non-scaling-stroke" stroke-width="1.127" stroke="#000" stroke-miterlimit="10"/><path d="M7.211 10.68v-.36a3.366 3.366 0 0 1 .191-1.194 2.79 2.79 0 0 1 .475-.779 8.98 8.98 0 0 1 .856-.833c.307-.249.577-.54.8-.867.122-.217.184-.462.18-.711-.004-.46-.2-.896-.542-1.205a1.837 1.837 0 0 0-1.329-.53 1.777 1.777 0 0 0-1.261.473c-.402.4-.657.925-.722 1.488l-1.126-.147a3.157 3.157 0 0 1 .934-2.073c.603-.5 1.37-.757 2.152-.721.823-.038 1.63.24 2.254.778.543.468.852 1.153.846 1.87a2.372 2.372 0 0 1-.294 1.126 5.517 5.517 0 0 1-1.127 1.296 5.918 5.918 0 0 0-.754.756c-.121.17-.209.361-.259.563a4.71 4.71 0 0 0-.102 1.025H7.256l-.045.045zm-.067 2.389v-1.352h1.352v1.352H7.144z"/></g></g></svg>
\ No newline at end of file
diff --git a/question/type/shortanswer/pix/icon.svg b/question/type/shortanswer/pix/icon.svg
new file mode 100644 (file)
index 0000000..12f910a
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M16 6V5H0v6h16v-1H1V6h14v4h1V6z"/><path d="M1 6h14v4H1V6z" fill="#FFF"/></g></svg>
\ No newline at end of file
diff --git a/question/type/truefalse/pix/icon.svg b/question/type/truefalse/pix/icon.svg
new file mode 100644 (file)
index 0000000..2ffcf1c
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M10 8a2 2 0 1 1 4.001.001A2 2 0 0 1 10 8zM2 8a2 2 0 1 1 4.001.001A2 2 0 0 1 2 8z"/></g></svg>
\ No newline at end of file
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 d5d5f8c..f98573f 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2018011200.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2018011800.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.5dev (Build: 20180112)'; // Human-friendly version name
+$release  = '3.5dev (Build: 20180118)'; // Human-friendly version name
 
 $branch   = '35';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.