Merge branch 'MDL-60963-master' of git://github.com/ryanwyllie/moodle
authorDavid Monllao <davidm@moodle.com>
Wed, 17 Jan 2018 09:49:58 +0000 (10:49 +0100)
committerDavid Monllao <davidm@moodle.com>
Wed, 17 Jan 2018 09:49:58 +0000 (10:49 +0100)
81 files changed:
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]
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_event_exporter.php
calendar/classes/external/event_exporter_base.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/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
lang/en/deprecated.txt
lang/en/error.php
lang/en/moodle.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/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/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/engine/lib.php
question/engine/questionattempt.php
question/type/essay/renderer.php
theme/styles.php
user/tests/behat/view_participants.feature

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 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 7529e1f..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);
         }
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 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 49980d5..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,74 +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);
-            $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->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);
+    }
 }
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');
     }
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 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 aa1c81d..4fbd4bd 100644 (file)
@@ -3,9 +3,14 @@ information provided here is intended especially for developers.
 
 === 3.5 ===
 
+* The core_renderer methods notify_problem(), notify_success(), notify_message() and notify_redirect() that were
+  deprecated in Moodle 3.1 have been removed. Use \core\notification::add(), or \core\output\notification as required.
 * The maximum supported precision (the total number of digits) for XMLDB_TYPE_NUMBER ("number") fields raised from 20 to
   38 digits. Additionally, the whole number part (precision minus scale) must not be longer than the maximum length of
   integer fields (20 digits). Note that PHP floats commonly support precision of roughly 15 digits only (MDL-32113).
+* Event triggering and event handlers:
+    - The following events, deprecated since moodle 2.6, have been finally removed: groups_members_removed,
+      groups_groupings_groups_removed, groups_groups_deleted, groups_groupings_deleted.
 
 === 3.4 ===
 
index 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..78a78f3 100644 (file)
@@ -3283,17 +3283,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 +5302,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);
index e5a0ccf..8f6b820 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,24 @@ 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->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']);
+
+        // 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']);
     }
 
     /**
@@ -2025,7 +2046,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 +2139,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..e298490 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 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 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 1889378..b65ce4b 100644 (file)
@@ -954,6 +954,65 @@ abstract class question_utils {
         $text = str_replace('@@PLUGINFILE@@/', 'http://example.com/', $text);
         return html_to_text(format_text($text, $format, $options), 0, false);
     }
+
+    /**
+     * Get the options required to configure the filepicker for one of the editor
+     * toolbar buttons.
+     * @param mixed $acceptedtypes array of types of '*'.
+     * @param int $draftitemid the draft area item id.
+     * @param object $context the context.
+     * @return object the required options.
+     */
+    protected static function specific_filepicker_options($acceptedtypes, $draftitemid, $context) {
+        $filepickeroptions = new stdClass();
+        $filepickeroptions->accepted_types = $acceptedtypes;
+        $filepickeroptions->return_types = FILE_INTERNAL | FILE_EXTERNAL;
+        $filepickeroptions->context = $context;
+        $filepickeroptions->env = 'filepicker';
+
+        $options = initialise_filepicker($filepickeroptions);
+        $options->context = $context;
+        $options->client_id = uniqid();
+        $options->env = 'editor';
+        $options->itemid = $draftitemid;
+
+        return $options;
+    }
+
+    /**
+     * Get filepicker options for question related text areas.
+     * @param object $context the context.
+     * @param int $draftitemid the draft area item id.
+     * @return array An array of options
+     */
+    public static function get_filepicker_options($context, $draftitemid) {
+        return [
+                'image' => self::specific_filepicker_options(['image'], $draftitemid, $context),
+                'media' => self::specific_filepicker_options(['video', 'audio'], $draftitemid, $context),
+                'link'  => self::specific_filepicker_options('*', $draftitemid, $context),
+            ];
+    }
+
+    /**
+     * Get editor options for question related text areas.
+     * @param object $context the context.
+     * @return array An array of options
+     */
+    public static function get_editor_options($context) {
+        global $CFG;
+
+        $editoroptions = [
+                'subdirs'  => 0,
+                'context'  => $context,
+                'maxfiles' => EDITOR_UNLIMITED_FILES,
+                'maxbytes' => $CFG->maxbytes,
+                'noclean' => 0,
+                'trusttext' => 0,
+                'autosave' => false
+        ];
+
+        return $editoroptions;
+    }
 }
 
 
index d295170..5473535 100644 (file)
@@ -1107,8 +1107,10 @@ class question_attempt {
             return null;
         }
 
-        return new question_file_saver($draftitemid, 'question', 'response_' .
-                str_replace($this->get_field_prefix(), '', $name), $text);
+        $filearea = str_replace($this->get_field_prefix(), '', $name);
+        $filearea = str_replace('-', 'bf_', $filearea);
+        $filearea = 'response_' . $filearea;
+        return new question_file_saver($draftitemid, 'question', $filearea, $text);
     }
 
     /**
@@ -1164,6 +1166,8 @@ class question_attempt {
                 $this->behaviour->get_expected_data(), $postdata, '-');
 
         $expected = $this->behaviour->get_expected_qt_data();
+        $this->check_qt_var_name_restrictions($expected);
+
         if ($expected === self::USE_RAW_DATA) {
             $submitteddata += $this->get_all_submitted_qt_vars($postdata);
         } else {
@@ -1172,6 +1176,23 @@ class question_attempt {
         return $submitteddata;
     }
 
+    /**
+     * Ensure that no reserved prefixes are being used by installed
+     * question types.
+     * @param array $expected An array of question type variables
+     */
+    protected function check_qt_var_name_restrictions($expected) {
+        global $CFG;
+
+        if ($CFG->debugdeveloper) {
+            foreach ($expected as $key => $value) {
+                if (strpos($key, 'bf_') !== false) {
+                    debugging('The bf_ prefix is reserved and cannot be used by question types', DEBUG_DEVELOPER);
+                }
+            }
+        }
+    }
+
     /**
      * Get a set of response data for this question attempt that would get the
      * best possible mark. If it is not possible to compute a correct
@@ -1370,16 +1391,17 @@ class question_attempt {
 
     /**
      * @return array(string, int) the most recent manual comment that was added
-     * to this question, and the FORMAT_... it is.
+     * to this question, the FORMAT_... it is and the step itself.
      */
     public function get_manual_comment() {
         foreach ($this->get_reverse_step_iterator() as $step) {
             if ($step->has_behaviour_var('comment')) {
                 return array($step->get_behaviour_var('comment'),
-                        $step->get_behaviour_var('commentformat'));
+                        $step->get_behaviour_var('commentformat'),
+                        $step);
             }
         }
-        return array(null, null);
+        return array(null, null, null);
     }
 
     /**
@@ -1399,7 +1421,7 @@ class question_attempt {
             if ($commentformat === null) {
                 $commentformat = FORMAT_HTML;
             }
-            return array($comment, $commentformat);
+            return array($comment, $commentformat, null);
         }
     }
 
index a6352ef..9b04c49 100644 (file)
@@ -358,28 +358,28 @@ class qtype_essay_format_editorfilepicker_renderer extends qtype_essay_format_ed
                 $name, $context->id, $step->get_qt_var($name));
     }
 
+    /**
+     * Get editor options for question response text area.
+     * @param object $context the context the attempt belongs to.
+     * @return array options for the editor.
+     */
     protected function get_editor_options($context) {
-        // Disable the text-editor autosave because quiz has it's own auto save function.
-        return array(
-            'subdirs' => 0,
-            'maxbytes' => 0,
-            'maxfiles' => -1,
-            'context' => $context,
-            'noclean' => 0,
-            'trusttext'=> 0,
-            'autosave' => false
-        );
+        return question_utils::get_editor_options($context);
     }
 
     /**
      * Get the options required to configure the filepicker for one of the editor
      * toolbar buttons.
+     * @deprecated since 3.5
      * @param mixed $acceptedtypes array of types of '*'.
      * @param int $draftitemid the draft area item id.
      * @param object $context the context.
      * @return object the required options.
      */
     protected function specific_filepicker_options($acceptedtypes, $draftitemid, $context) {
+        debugging('qtype_essay_format_editorfilepicker_renderer::specific_filepicker_options() is deprecated, ' .
+            'use question_utils::specific_filepicker_options() instead.', DEBUG_DEVELOPER);
+
         $filepickeroptions = new stdClass();
         $filepickeroptions->accepted_types = $acceptedtypes;
         $filepickeroptions->return_types = FILE_INTERNAL | FILE_EXTERNAL;
@@ -395,17 +395,13 @@ class qtype_essay_format_editorfilepicker_renderer extends qtype_essay_format_ed
         return $options;
     }
 
+    /**
+     * @param object $context the context the attempt belongs to.
+     * @param int $draftitemid draft item id.
+     * @return array filepicker options for the editor.
+     */
     protected function get_filepicker_options($context, $draftitemid) {
-        global $CFG;
-
-        return array(
-            'image' => $this->specific_filepicker_options(array('image'),
-                            $draftitemid, $context),
-            'media' => $this->specific_filepicker_options(array('video', 'audio'),
-                            $draftitemid, $context),
-            'link'  => $this->specific_filepicker_options('*',
-                            $draftitemid, $context),
-        );
+        return question_utils::get_filepicker_options($context, $draftitemid);
     }
 
     protected function filepicker_html($inputname, $draftitemid) {
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"