Merge branch 'MDL-61204-master' of git://github.com/andrewnicols/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 16 Jan 2018 01:16:40 +0000 (02:16 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 16 Jan 2018 01:16:40 +0000 (02:16 +0100)
53 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]
calendar/classes/external/calendar_event_exporter.php
calendar/classes/external/event_exporter_base.php
calendar/classes/external/week_day_exporter.php
calendar/classes/local/event/container.php
calendar/tests/calendar_event_exporter_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
lang/en/deprecated.txt
lang/en/error.php
lang/en/moodle.php
lang/en/table.php
lang/en/tag.php
lib/classes/task/manager.php
lib/grouplib.php
lib/tests/scheduled_task_test.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/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/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
version.php

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 ad97c96..fa8be8d 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 951b84e..98ad278 100644 (file)
@@ -38,6 +38,21 @@ use moodle_url;
  */
 class week_day_exporter extends day_exporter {
 
+    /**
+     * Constructor.
+     *
+     * @param \calendar_information $calendar The calendar information for the period being displayed
+     * @param mixed $data Either an stdClass or an array of values.
+     * @param array $related Related objects.
+     */
+    public function __construct(\calendar_information $calendar, $data, $related) {
+        parent::__construct($calendar, $data, $related);
+        // Fix the url for today to be based on the today timestamp
+        // rather than the calendar_information time set in the parent
+        // constructor.
+        $this->url->param('time', $this->data[0]);
+    }
+
     /**
      * Return the list of properties.
      *
@@ -83,72 +98,12 @@ class week_day_exporter extends day_exporter {
      * @return array Keys are the property names, values are their values.
      */
     protected function get_other_values(renderer_base $output) {
-        $timestamp = $this->data[0];
-        // Need to account for user's timezone.
-        $usernow = usergetdate(time());
-        $today = new \DateTimeImmutable();
-        // The start time should use the day's date but the current
-        // time of the day (adjusted for user's timezone).
-        $neweventstarttime = $today->setTimestamp($timestamp)->setTime(
-            $usernow['hours'],
-            $usernow['minutes'],
-            $usernow['seconds']
-        );
-
         $return = parent::get_other_values($output);
 
-        $url = new moodle_url('/calendar/view.php', [
-                'view' => 'day',
-                'time' => $timestamp,
-            ]);
-
-        if ($this->calendar->course && SITEID !== $this->calendar->course->id) {
-            $url->param('course', $this->calendar->course->id);
-        } else if ($this->calendar->categoryid) {
-            $url->param('category', $this->calendar->categoryid);
-        }
-
-        $return['viewdaylink'] = $url->out(false);
-
-        if ($popovertitle = $this->get_popover_title()) {
-            $return['popovertitle'] = $popovertitle;
-        }
-        $cache = $this->related['cache'];
-        $eventexporters = array_map(function($event) use ($cache, $output, $url) {
-            $context = $cache->get_context($event);
-            $course = $cache->get_course($event);
-            $exporter = new calendar_event_exporter($event, [
-                'context' => $context,
-                'course' => $course,
-                'daylink' => $url,
-                'type' => $this->related['type'],
-                'today' => $this->data[0],
-            ]);
-
-            return $exporter;
-        }, $this->related['events']);
-
-        $return['events'] = array_map(function($exporter) use ($output) {
-            return $exporter->export($output);
-        }, $eventexporters);
-
         if ($popovertitle = $this->get_popover_title()) {
             $return['popovertitle'] = $popovertitle;
         }
 
-        $return['calendareventtypes'] = array_map(function($exporter) {
-            return $exporter->get_calendar_event_type();
-        }, $eventexporters);
-        $return['calendareventtypes'] = array_values(array_unique($return['calendareventtypes']));
-
-        $return['haslastdayofevent'] = false;
-        foreach ($return['events'] as $event) {
-            if ($event->islastday) {
-                $return['haslastdayofevent'] = true;
-                break;
-            }
-        }
-
         return $return;
     }
 
index 398304e..597eb70 100644 (file)
@@ -151,11 +151,16 @@ class container {
                     // 2) Only process modules for courses a user has the capability to view OR they are enrolled in.
                     // 3) Only process modules for courses that are visible OR if the course is not visible, the user
                     //    has the capability to view hidden courses.
+                    if (!$cm->uservisible) {
+                        return true;
+                    }
+
                     $coursecontext = \context_course::instance($dbrow->courseid);
-                    $canseecourse = has_capability('moodle/course:view', $coursecontext) || is_enrolled($coursecontext);
-                    $canseecourse = $canseecourse &&
-                        ($cm->get_course()->visible || has_capability('moodle/course:viewhiddencourses', $coursecontext));
-                    if (!$cm->uservisible || !$canseecourse) {
+                    if (!$cm->get_course()->visible && !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
+                        return true;
+                    }
+
+                    if (!has_capability('moodle/course:view', $coursecontext) && !is_enrolled($coursecontext)) {
                         return true;
                     }
 
index 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 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 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 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 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 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 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 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 1d6be75..d5d5f8c 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2018010400.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2018011200.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.5dev (Build: 20180104)'; // Human-friendly version name
+$release  = '3.5dev (Build: 20180112)'; // Human-friendly version name
 
 $branch   = '35';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.