$this->role->shortname = core_text::strtolower(clean_param($this->role->shortname, PARAM_ALPHANUMEXT));
if (empty($this->role->shortname)) {
$this->errors['shortname'] = get_string('errorbadroleshortname', 'core_role');
+ } else if (core_text::strlen($this->role->shortname) > 100) { // Check if it exceeds the max of 100 characters.
+ $this->errors['shortname'] = get_string('errorroleshortnametoolong', 'core_role');
}
}
if ($DB->record_exists_select('role', 'shortname = ? and id <> ?', array($this->role->shortname, $this->roleid))) {
}
protected function get_shortname_field($id) {
- return '<input type="text" id="' . $id . '" name="' . $id . '" maxlength="254" value="' . s($this->role->shortname) . '"' .
+ return '<input type="text" id="' . $id . '" name="' . $id . '" maxlength="100" value="' . s($this->role->shortname) . '"' .
' class="form-control"/>';
}
--- /dev/null
+<?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();
$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';
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,
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.
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;
}
--- /dev/null
+<?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]);
+ }
+}
--- /dev/null
+@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"
* @param {Number} tourId The ID of the tour to start.
*/
fetchTour: function(tourId) {
+ M.util.js_pending('admin_usertour_fetchTour' + tourId);
$.when(
ajax.call([
{
}
])[0],
templates.render('tool_usertours/tourstep', {})
- ).then(function(response, template) {
- usertours.startBootstrapTour(tourId, template[0], response.tourconfig);
+ )
+ .then(function(response, template) {
+ return usertours.startBootstrapTour(tourId, template[0], response.tourconfig);
+ })
+ .always(function() {
+ M.util.js_complete('admin_usertour_fetchTour' + tourId);
+
return;
- }).fail(notification.exception);
+ })
+ .fail(notification.exception);
},
/**
*/
addResetLink: function() {
var ele;
+ M.util.js_pending('admin_usertour_addResetLink');
+
// Append the link to the most suitable place on the page
// with fallback to legacy selectors and finally the body
// if there is no better place.
ele = $('body');
}
templates.render('tool_usertours/resettour', {})
- .done(function(html, js) {
- templates.appendNodeContents(ele, html, js);
- });
+ .then(function(html, js) {
+ templates.appendNodeContents(ele, html, js);
+
+ return;
+ })
+ .always(function() {
+ M.util.js_complete('admin_usertour_addResetLink');
+
+ return;
+ })
+ .fail();
},
/**
* @param {Number} tourId The ID of the tour to start.
* @param {String} template The template to use.
* @param {Object} tourConfig The tour configuration.
+ * @return {Object}
*/
startBootstrapTour: function(tourId, template, tourConfig) {
if (usertours.currentTour) {
});
usertours.currentTour = new BootstrapTour(tourConfig);
- usertours.currentTour.startTour();
+ return usertours.currentTour.startTour();
},
/**
--- /dev/null
+@tool @tool_usertours
+Feature: Apply accessibility to a tour
+
+ @javascript
+ Scenario: Check tabbing working correctly.
+ Given the following "courses" exist:
+ | fullname | shortname |
+ | Course 1 | C1 |
+ And I log in as "admin"
+ And I open the User tour settings page
+ And I click on "Enable" "link" in the "Boost - course view" "table_row"
+ And I am on "Course 1" course homepage
+ # First dialogue of the tour, "Welcome". It has Close, Next and End buttons.
+ # Nothing highlighted on the page. Initially whole dialogue focused.
+ And I wait "1" seconds
+ When I press tab
+ Then the focused element is ".close" "css_element" in the "Welcome" "dialogue"
+ When I press tab
+ Then the focused element is "Next" "button" in the "Welcome" "dialogue"
+ When I press tab
+ Then the focused element is "End tour" "button" in the "Welcome" "dialogue"
+ When I press tab
+ # Here the focus loops round to the whole dialogue again.
+ And I press tab
+ Then the focused element is ".close" "css_element" in the "Welcome" "dialogue"
+ # Check looping works properly going backwards too.
+ When I press shift tab
+ And I press shift tab
+ Then the focused element is "End tour" "button" in the "Welcome" "dialogue"
+
+ When I press "Next"
+ # Now we are on the "Customisation" step, so Previous is also enabled.
+ # Also, the "Course Header" section in the page is highlighted, and this
+ # section contain breadcrumb Dashboard / Course 1 / C1 and setting drop down,
+ # so the focus have to go though them and back to the dialogue.
+ And I wait "1" seconds
+ And I press tab
+ Then the focused element is ".close" "css_element" in the "Customisation" "dialogue"
+ When I press tab
+ Then the focused element is "Previous" "button" in the "Customisation" "dialogue"
+ When I press tab
+ Then the focused element is "Next" "button" in the "Customisation" "dialogue"
+ When I press tab
+ Then the focused element is "End tour" "button" in the "Customisation" "dialogue"
+ # We tab 3 times from "End Tour" button to header container, drop down then go to "Dashboard" link.
+ When I press tab
+ And I press tab
+ And I press tab
+ Then the focused element is "Dashboard" "link" in the ".breadcrumb" "css_element"
+ When I press tab
+ Then the focused element is "Courses" "link"
+ When I press tab
+ Then the focused element is "C1" "link"
+ # Standing at final element of "Course Header" section, tab twice will lead our focus back to
+ # whole dialog then to close button on dialog header.
+ When I press tab
+ And I press tab
+ Then the focused element is ".close" "css_element" in the "Customisation" "dialogue"
+ # Press shift-tab twice should lead us back to "C1" link.
+ When I press shift tab
+ And I press shift tab
+ Then the focused element is "C1" "link"
+
+ When I press "Next"
+ # Now we are on the "Navigation" step, so Previous is also enabled.
+ # Also, the "Side panel" button in the page is highlighted, and this comes
+ # in the tab order after End buttons, and before focus loops back to the popup.
+ And I wait "1" seconds
+ And I press tab
+ Then the focused element is ".close" "css_element" in the "Navigation" "dialogue"
+ When I press tab
+ Then the focused element is "Previous" "button" in the "Navigation" "dialogue"
+ When I press tab
+ Then the focused element is "Next" "button" in the "Navigation" "dialogue"
+ When I press tab
+ Then the focused element is "End tour" "button" in the "Navigation" "dialogue"
+ When I press tab
+ Then the focused element is "Side panel" "button"
+ When I press tab
+ # Here the focus loops round to the whole dialogue again.
+ And I press tab
+ Then the focused element is ".close" "css_element" in the "Navigation" "dialogue"
+ When I press shift tab
+ And I press shift tab
+ Then the focused element is "Side panel" "button"
+ When I press shift tab
+ And the focused element is "End tour" "button" in the "Navigation" "dialogue"
if ($hosts) {
foreach ($hosts as $host) {
- $icon = $OUTPUT->pix_icon('i/'.$host->application.'_host', get_string('server', 'block_mnet_hosts')) . ' ';
-
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))
+ );
}
}
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);
* @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: {
courseid: courseid,
categoryid: categoryid,
includenavigation: includenavigation,
+ mini: mini
}
};
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);
})
$return['events'] = array_map(function($event) use ($cache, $output, $url) {
$context = $cache->get_context($event);
$course = $cache->get_course($event);
+ $moduleinstance = $cache->get_module_instance($event);
$exporter = new calendar_event_exporter($event, [
'context' => $context,
'course' => $course,
+ 'moduleinstance' => $moduleinstance,
'daylink' => $url,
'type' => $this->related['type'],
'today' => $this->calendar->time,
$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.
$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);
}
// 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);
}
$related['daylink'] = \moodle_url::class;
$related['type'] = '\core_calendar\type_base';
$related['today'] = 'int';
+ $related['moduleinstance'] = 'stdClass?';
return $related;
}
* @return array
*/
protected function get_module_timestamp_limits($event) {
- global $DB;
-
$values = [];
$mapper = container::get_event_mapper();
$starttime = $event->get_times()->get_start_time();
$modname = $event->get_course_module()->get('modname');
- $modid = $event->get_course_module()->get('instance');
- $moduleinstance = $DB->get_record($modname, ['id' => $modid]);
+ $moduleinstance = $this->related['moduleinstance'];
list($min, $max) = component_callback(
'mod_' . $modname,
$return['events'] = array_map(function($event) use ($cache, $output, $url) {
$context = $cache->get_context($event);
$course = $cache->get_course($event);
+ $moduleinstance = $cache->get_module_instance($event);
$exporter = new calendar_event_exporter($event, [
'context' => $context,
'course' => $course,
+ 'moduleinstance' => $moduleinstance,
'daylink' => $url,
'type' => $this->related['type'],
'today' => $this->calendar->time,
$eventexporters = array_map(function($event) use ($cache, $output) {
$context = $cache->get_context($event);
$course = $cache->get_course($event);
+ $moduleinstance = $cache->get_module_instance($event);
$exporter = new calendar_event_exporter($event, [
'context' => $context,
'course' => $course,
+ 'moduleinstance' => $moduleinstance,
'daylink' => $this->url,
'type' => $this->related['type'],
'today' => $this->data[0],
$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;
$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);
$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;
}
protected $courses = null;
/**
- * @var array $events The related groups.
+ * @var array $groups The related groups.
*/
protected $groups = null;
/**
- * @var array $events The related course modules.
+ * @var array $coursemodules The related course modules.
*/
protected $coursemodules = [];
+ /**
+ * @var array $moduleinstances The related module instances.
+ */
+ protected $moduleinstances = null;
+
/**
* Constructor.
*
return $this->coursemodules[$key];
}
+ /**
+ * Get the related module instance for a given event.
+ *
+ * @param event_interface $event The event object.
+ * @return stdClass|null
+ */
+ public function get_module_instance(event_interface $event) {
+ if (!$event->get_course_module()) {
+ return null;
+ }
+
+ if (is_null($this->moduleinstances)) {
+ $this->load_module_instances();
+ }
+
+ $id = $event->get_course_module()->get('instance');
+ $name = $event->get_course_module()->get('modname');
+
+ if (isset($this->moduleinstances[$name])) {
+ if (isset($this->moduleinstances[$name][$id])) {
+ return $this->moduleinstances[$name][$id];
+ }
+ }
+
+ return null;
+ }
+
/**
* Load the list of all of the distinct courses required for the
* list of provided events and save the result in memory.
$this->groups = $DB->get_records_sql($sql, $params);
}
+
+ /**
+ * Load the list of all of the distinct module instances required for the
+ * list of provided events and save the result in memory.
+ */
+ protected function load_module_instances() {
+ global $DB;
+
+ $this->moduleinstances = [];
+ $modulestoload = [];
+ foreach ($this->events as $event) {
+ if ($module = $event->get_course_module()) {
+ $id = $module->get('instance');
+ $name = $module->get('modname');
+
+ $ids = isset($modulestoload[$name]) ? $modulestoload[$name] : [];
+ $ids[$id] = true;
+ $modulestoload[$name] = $ids;
+ }
+ }
+
+ if (empty($modulestoload)) {
+ return;
+ }
+
+ foreach ($modulestoload as $modulename => $ids) {
+ list($idsql, $params) = $DB->get_in_or_equal(array_keys($ids));
+ $sql = "SELECT * FROM {" . $modulename . "} WHERE id {$idsql}";
+ $this->moduleinstances[$modulename] = $DB->get_records_sql($sql, $params);
+ }
+ }
}
*/
protected $initialeventsloaded = true;
+ /**
+ * @var bool $showcoursefilter Whether to render the course filter selector as well.
+ */
+ protected $showcoursefilter = false;
+
/**
* Constructor for month_exporter.
*
],
'filter_selector' => [
'type' => PARAM_RAW,
+ 'optional' => true,
],
'weeks' => [
'type' => week_exporter::read_properties_definition(),
$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',
'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;
}
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.
*
*/
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.
*
* @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;
}
// 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;
}
* @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");
'courseid' => $courseid,
'categoryid' => $categoryid,
'includenavigation' => $includenavigation,
+ 'mini' => $mini,
]);
$context = \context_user::instance($USER->id);
$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;
}
true,
NULL_ALLOWED
),
+ 'mini' => new external_value(
+ PARAM_BOOL,
+ 'Whether to return the mini month view or not',
+ VALUE_DEFAULT,
+ false,
+ NULL_ALLOWED
+ ),
]
);
}
$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);
* @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));
}
}} 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>
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.
$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);
+ }
}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tests for the events_related_objects_cache.
+ *
+ * @package core_calendar
+ * @copyright 2017 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/helpers.php');
+
+use \core_calendar\external\events_related_objects_cache;
+use \core_calendar\local\event\container;
+
+/**
+ * Tests for the events_related_objects_cache.
+ *
+ * @package core_calendar
+ * @copyright 2017 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_calendar_events_related_objects_cache_testcase extends advanced_testcase {
+
+ /**
+ * Tests set up
+ */
+ protected function setUp() {
+ $this->resetAfterTest();
+ }
+
+ /**
+ * An event with no module should return null when trying to retrieve
+ * the module instance.
+ */
+ public function test_get_module_instance_no_module() {
+ $this->setAdminUser();
+ $mapper = container::get_event_mapper();
+ $legacyevent = create_event([
+ 'modulename' => '',
+ 'instance' => 0
+ ]);
+ $event = $mapper->from_legacy_event_to_event($legacyevent);
+ $cache = new events_related_objects_cache([$event]);
+
+ $this->assertNull($cache->get_module_instance($event));
+ }
+
+ /**
+ * The get_module_instance should return the correct module instances
+ * for the given set of events in the cache.
+ */
+ public function test_get_module_instance_with_modules() {
+ $this->setAdminUser();
+ $mapper = container::get_event_mapper();
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course();
+ $plugingenerator = $generator->get_plugin_generator('mod_assign');
+ $instance1 = $plugingenerator->create_instance(['course' => $course->id]);
+ $instance2 = $plugingenerator->create_instance(['course' => $course->id]);
+ unset($instance1->cmid);
+ unset($instance2->cmid);
+
+ $params = [
+ 'type' => CALENDAR_EVENT_TYPE_ACTION,
+ 'courseid' => $course->id,
+ 'modulename' => 'assign',
+ 'userid' => 0,
+ 'eventtype' => 'due',
+ 'repeats' => 0,
+ 'timestart' => 1,
+ ];
+
+ $legacyevent1 = create_event(array_merge($params, ['name' => 'Event 1', 'instance' => $instance1->id]));
+ $legacyevent2 = create_event(array_merge($params, ['name' => 'Event 2', 'instance' => $instance1->id]));
+ $legacyevent3 = create_event(array_merge($params, ['name' => 'Event 3', 'instance' => $instance2->id]));
+ $event1 = $mapper->from_legacy_event_to_event($legacyevent1);
+ $event2 = $mapper->from_legacy_event_to_event($legacyevent2);
+ $event3 = $mapper->from_legacy_event_to_event($legacyevent3);
+ $cache = new events_related_objects_cache([$event1, $event2, $event3]);
+
+ $eventinstance1 = $cache->get_module_instance($event1);
+ $eventinstance2 = $cache->get_module_instance($event2);
+ $eventinstance3 = $cache->get_module_instance($event3);
+
+ $this->assertEquals($instance1, $eventinstance1);
+ $this->assertEquals($instance1, $eventinstance2);
+ $this->assertEquals($instance2, $eventinstance3);
+ }
+
+ /**
+ * Trying to load the course module of an event that isn't in
+ * the cache should return null.
+ */
+ public function test_module_instance_unknown_event() {
+ $this->setAdminUser();
+ $mapper = container::get_event_mapper();
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course();
+ $plugingenerator = $generator->get_plugin_generator('mod_assign');
+ $instance1 = $plugingenerator->create_instance(['course' => $course->id]);
+ $instance2 = $plugingenerator->create_instance(['course' => $course->id]);
+ unset($instance1->cmid);
+ unset($instance2->cmid);
+
+ $params = [
+ 'type' => CALENDAR_EVENT_TYPE_ACTION,
+ 'courseid' => $course->id,
+ 'modulename' => 'assign',
+ 'userid' => 0,
+ 'eventtype' => 'due',
+ 'repeats' => 0,
+ 'timestart' => 1,
+ ];
+
+ $legacyevent1 = create_event(array_merge($params, ['name' => 'Event 1', 'instance' => $instance1->id]));
+ $legacyevent2 = create_event(array_merge($params, ['name' => 'Event 2', 'instance' => $instance2->id]));
+ $event1 = $mapper->from_legacy_event_to_event($legacyevent1);
+ $event2 = $mapper->from_legacy_event_to_event($legacyevent2);
+ $cache = new events_related_objects_cache([$event1]);
+
+ $this->assertNull($cache->get_module_instance($event2));
+ }
+}
$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.
$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.
$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)
);
}
}
// 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);
$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);
}
// 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);
}
$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) {
// 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();
}
$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;
}
// 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;
// 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');
}
// 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');
}
--- /dev/null
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thislanguage'] = 'Uyghur - latin';
--- /dev/null
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['clianswerno'] = 'Ù†';
+$string['cliansweryes'] = 'ÙŠ';
--- /dev/null
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thisdirection'] = 'rtl';
+$string['thislanguage'] = 'ئۇيغۇرچە';
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
$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.';
$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';
$string['errorbadroleshortname'] = 'Incorrect role short name';
$string['errorexistsrolename'] = 'Role name already exists';
$string['errorexistsroleshortname'] = 'Role name already exists';
+$string['errorroleshortnametoolong'] = 'The short name must not exceed 100 characters';
$string['eventroleallowassignupdated'] = 'Allow role assignment';
$string['eventroleallowoverrideupdated'] = 'Allow role override';
$string['eventroleallowswitchupdated'] = 'Allow role switch';
$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';
$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';
* @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';
.//div[contains(concat(' ', normalize-space(@class), ' '), ' yui-dialog ') and
normalize-space(descendant::div[@class='hd']) = %locator%]
|
-.//div[@data-region='modal' and descendant::*[@data-region='title'] = %locator%]
+.//div[@data-region='modal' and descendant::*[@data-region='title'] = %locator%] |
+.//div[contains(concat(' ', normalize-space(@class), ' '), ' modal-content ') and
+ normalize-space(descendant::h4[
+ contains(concat(' ', normalize-space(@class), ' '), ' modal-title ')
+ ]) = %locator%]
XPATH
, 'icon' => <<<XPATH
.//*[contains(concat(' ', normalize-space(@class), ' '), ' icon ') and ( contains(normalize-space(@title), %locator%))]
*/
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.
*/
* 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;
}
$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.');
- }
}
/**
$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;
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);
*/
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();
*/
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();
* @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;
* @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;
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) {
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));
$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);
$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;
$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.
*
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();
$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;
+ }
}
}
/**
- * Print a bold message in an optional color.
- *
* @deprecated since Moodle 2.0 MDL-19077 - use $OUTPUT->notification instead.
- * @todo MDL-50469 This will be deleted in Moodle 3.3.
- * @param string $message The message to print out
- * @param string $classes Optional style to display message text in
- * @param string $align Alignment option
- * @param bool $return whether to return an output string or echo now
- * @return string|bool Depending on $result
- */
-function notify($message, $classes = 'error', $align = 'center', $return = false) {
- global $OUTPUT;
-
- debugging('notify() is deprecated, please use $OUTPUT->notification() instead', DEBUG_DEVELOPER);
-
- if ($classes == 'green') {
- debugging('Use of deprecated class name "green" in notify. Please change to "success".', DEBUG_DEVELOPER);
- $classes = 'success'; // Backward compatible with old color system.
- }
-
- $output = $OUTPUT->notification($message, $classes);
- if ($return) {
- return $output;
- } else {
- echo $output;
- }
+ */
+function notify() {
+ throw new coding_exception('notify() is removed, please use $OUTPUT->notification() instead');
}
/**
* @return array of table names in lowercase and without prefix
*/
public function get_tables($usecache = true) {
- if ($usecache and count($this->tables) > 0) {
+ if ($usecache and $this->tables !== null) {
return $this->tables;
}
$this->tables = array ();
* @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;
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);
}
/**
- * 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.');
}
/**
throw new \Moodle\BehatExtension\Exception\SkippedException();
}
+
+ /**
+ * Checks focus is with the given element.
+ *
+ * @Then /^the focused element is( not)? "(?P<node_string>(?:[^"]|\\")*)" "(?P<node_selector_string>[^"]*)"$/
+ * @param string $not optional step verifier
+ * @param string $nodeelement Element identifier
+ * @param string $nodeselectortype Element type
+ * @throws DriverException If not using JavaScript
+ * @throws ExpectationException
+ */
+ public function the_focused_element_is($not, $nodeelement, $nodeselectortype) {
+ if (!$this->running_javascript()) {
+ throw new DriverException('Checking focus on an element requires JavaScript');
+ }
+ list($a, $b) = $this->transform_selector($nodeselectortype, $nodeelement);
+ $element = $this->find($a, $b);
+ $xpath = addslashes_js($element->getXpath());
+ $script = 'return (function() { return document.activeElement === document.evaluate("' . $xpath . '",
+ document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; })(); ';
+ $targetisfocused = $this->getSession()->evaluateScript($script);
+ if ($not == ' not') {
+ if ($targetisfocused) {
+ throw new ExpectationException("$nodeelement $nodeselectortype is focused", $this->getSession());
+ }
+ } else {
+ if (!$targetisfocused) {
+ throw new ExpectationException("$nodeelement $nodeselectortype is not focused", $this->getSession());
+ }
+ }
+ }
+
+ /**
+ * Checks focus is with the given element.
+ *
+ * @Then /^the focused element is( not)? "(?P<n>(?:[^"]|\\")*)" "(?P<ns>[^"]*)" in the "(?P<c>(?:[^"]|\\")*)" "(?P<cs>[^"]*)"$/
+ * @param string $not string optional step verifier
+ * @param string $element Element identifier
+ * @param string $selectortype Element type
+ * @param string $nodeelement Element we look in
+ * @param string $nodeselectortype The type of selector where we look in
+ * @throws DriverException If not using JavaScript
+ * @throws ExpectationException
+ */
+ public function the_focused_element_is_in_the($not, $element, $selectortype, $nodeelement, $nodeselectortype) {
+ if (!$this->running_javascript()) {
+ throw new DriverException('Checking focus on an element requires JavaScript');
+ }
+ $element = $this->get_node_in_container($selectortype, $element, $nodeselectortype, $nodeelement);
+ $xpath = addslashes_js($element->getXpath());
+ $script = 'return (function() { return document.activeElement === document.evaluate("' . $xpath . '",
+ document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; })(); ';
+ $targetisfocused = $this->getSession()->evaluateScript($script);
+ if ($not == ' not') {
+ if ($targetisfocused) {
+ throw new ExpectationException("$nodeelement $nodeselectortype is focused", $this->getSession());
+ }
+ } else {
+ if (!$targetisfocused) {
+ throw new ExpectationException("$nodeelement $nodeselectortype is not focused", $this->getSession());
+ }
+ }
+ }
+
+ /**
+ * Manually press tab key.
+ *
+ * @When /^I press( shift)? tab$/
+ * @param string $shift string optional step verifier
+ * @throws DriverException
+ */
+ public function i_manually_press_tab($shift = '') {
+ if (!$this->running_javascript()) {
+ throw new DriverException($shift . ' Tab press step is not available with Javascript disabled');
+ }
+
+ $value = ($shift == ' shift') ? [\WebDriver\Key::SHIFT . \WebDriver\Key::TAB] : [\WebDriver\Key::TAB];
+ $this->getSession()->getDriver()->getWebDriverSession()->activeElement()->postValue(['value' => $value]);
+ }
}
// 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());
+ }
}
$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));
+ }
}
=== 3.5 ===
+* The core_renderer methods notify_problem(), notify_success(), notify_message() and notify_redirect() that were
+ deprecated in Moodle 3.1 have been removed. Use \core\notification::add(), or \core\output\notification as required.
* The maximum supported precision (the total number of digits) for XMLDB_TYPE_NUMBER ("number") fields raised from 20 to
38 digits. Additionally, the whole number part (precision minus scale) must not be longer than the maximum length of
integer fields (20 digits). Note that PHP floats commonly support precision of roughly 15 digits only (MDL-32113).
+* Event triggering and event handlers:
+ - The following events, deprecated since moodle 2.6, have been finally removed: groups_members_removed,
+ groups_groupings_groups_removed, groups_groups_deleted, groups_groupings_deleted.
+* The following functions have been finally deprecated and can not be used any more:
+ - notify()
=== 3.4 ===
}
if (!isset($data->gradingduedate)) {
$data->gradingduedate = 0;
+ } else {
+ $data->gradingduedate = $this->apply_date_offset($data->gradingduedate);
}
if (!isset($data->markingworkflow)) {
$data->markingworkflow = 0;
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),
)
);
}
*
* @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();
$params = array(
'assignid' => $assignid,
'userid' => $userid,
+ 'groupid' => $groupid,
);
$params = self::validate_parameters(self::get_submission_status_parameters(), $params);
$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.
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);
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);
* This means the submission modification time is more recent than the
* grading modification time and the status is SUBMITTED.
*
+ * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
* @return int number of matching submissions
*/
- public function count_submissions_need_grading() {
+ public function count_submissions_need_grading($currentgroup = null) {
global $DB;
if ($this->get_instance()->teamsubmission) {
return 0;
}
- $currentgroup = groups_get_activity_group($this->get_course_module(), true);
+ if ($currentgroup === null) {
+ $currentgroup = groups_get_activity_group($this->get_course_module(), true);
+ }
list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
$params['assignid'] = $this->get_instance()->id;
* Load a count of submissions with a specified status.
*
* @param string $status The submission status - should match one of the constants
+ * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
* @return int number of matching submissions
*/
- public function count_submissions_with_status($status) {
+ public function count_submissions_with_status($status, $currentgroup = null) {
global $DB;
- $currentgroup = groups_get_activity_group($this->get_course_module(), true);
+ if ($currentgroup === null) {
+ $currentgroup = groups_get_activity_group($this->get_course_module(), true);
+ }
list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
$params['assignid'] = $this->get_instance()->id;
/**
* 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;
/**
* 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);
$summary = new assign_grading_summary($this->count_teams($activitygroup),
$instance->submissiondrafts,
- $this->count_submissions_with_status($draft),
+ $this->count_submissions_with_status($draft, $activitygroup),
$this->is_any_submission_plugin_enabled(),
- $this->count_submissions_with_status($submitted),
+ $this->count_submissions_with_status($submitted, $activitygroup),
$instance->cutoffdate,
$instance->duedate,
$this->get_course_module()->id,
- $this->count_submissions_need_grading(),
+ $this->count_submissions_need_grading($activitygroup),
$instance->teamsubmission,
$warnofungroupedusers,
$this->can_grade());
$countparticipants = $this->count_participants($activitygroup);
$summary = new assign_grading_summary($countparticipants,
$instance->submissiondrafts,
- $this->count_submissions_with_status($draft),
+ $this->count_submissions_with_status($draft, $activitygroup),
$this->is_any_submission_plugin_enabled(),
- $this->count_submissions_with_status($submitted),
+ $this->count_submissions_with_status($submitted, $activitygroup),
$instance->cutoffdate,
$instance->duedate,
$this->get_course_module()->id,
- $this->count_submissions_need_grading(),
+ $this->count_submissions_need_grading($activitygroup),
$instance->teamsubmission,
false,
$this->can_grade());
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(
$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.
$assign->submit_for_grading($data, $notices);
}
- return array($assign, $instance, $student1, $student2, $teacher);
+ return array($assign, $instance, $student1, $student2, $teacher, $group1, $group2);
}
/**
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);
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.
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);
$this->assertFalse(isset($result['feedback']));
$this->assertFalse(isset($result['previousattempts']));
- $this->assertEquals(2, $result['gradingsummary']['participantcount']);
+ $this->assertEquals(1, $result['gradingsummary']['participantcount']);
$this->assertEquals(0, $result['gradingsummary']['submissiondraftscount']);
$this->assertEquals(1, $result['gradingsummary']['submissionsenabled']);
- $this->assertEquals(1, $result['gradingsummary']['submissionssubmittedcount']);
- $this->assertEquals(1, $result['gradingsummary']['submissionsneedgradingcount']);
+ $this->assertEquals(0, $result['gradingsummary']['submissiondraftscount']);
+ $this->assertEquals(1, $result['gradingsummary']['submissionssubmittedcount']); // One student from G1 submitted.
+ $this->assertEquals(1, $result['gradingsummary']['submissionsneedgradingcount']); // One student from G1 submitted.
$this->assertFalse($result['gradingsummary']['warnofungroupedusers']);
+
+ // Second group.
+ $result = mod_assign_external::get_submission_status($assign->get_instance()->id, 0, $g2->id);
+ $result = external_api::clean_returnvalue(mod_assign_external::get_submission_status_returns(), $result);
+ $this->assertCount(0, $result['warnings']);
+ $this->assertEquals(1, $result['gradingsummary']['participantcount']);
+ $this->assertEquals(0, $result['gradingsummary']['submissionssubmittedcount']); // G2 students didn't submit yet.
+ $this->assertEquals(0, $result['gradingsummary']['submissionsneedgradingcount']); // G2 students didn't submit yet.
+
+ // Should return also 1 participant if we allow the function to auto-select the group.
+ $result = mod_assign_external::get_submission_status($assign->get_instance()->id);
+ $result = external_api::clean_returnvalue(mod_assign_external::get_submission_status_returns(), $result);
+ $this->assertCount(0, $result['warnings']);
+ $this->assertEquals(1, $result['gradingsummary']['participantcount']);
+ $this->assertEquals(0, $result['gradingsummary']['submissiondraftscount']);
+ $this->assertEquals(1, $result['gradingsummary']['submissionssubmittedcount']); // One student from G1 submitted.
+ $this->assertEquals(1, $result['gradingsummary']['submissionsneedgradingcount']); // One student from G1 submitted.
+
+ // Now check draft submissions.
+ list($assign, $instance, $student1, $student2, $teacher, $g1, $g2) = $this->create_submission_for_testing_status(false);
+ $this->setUser($teacher);
+ $result = mod_assign_external::get_submission_status($assign->get_instance()->id, 0, $g1->id);
+ $result = external_api::clean_returnvalue(mod_assign_external::get_submission_status_returns(), $result);
+ $this->assertCount(0, $result['warnings']);
+ $this->assertEquals(1, $result['gradingsummary']['participantcount']);
+ $this->assertEquals(1, $result['gradingsummary']['submissiondraftscount']); // We have a draft submission.
+ $this->assertEquals(0, $result['gradingsummary']['submissionssubmittedcount']); // We have only draft submissions.
+ $this->assertEquals(0, $result['gradingsummary']['submissionsneedgradingcount']); // We have only draft submissions.
}
/**
$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);
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);
This files describes API changes in the assign code.
+=== 3.5 ===
+* Functions assign:get_assign_grading_summary_renderable, assign:can_view_submission, assign:count_submissions_with_status,
+ assign:count_submissions_need_grading and mod_assign_external::get_submission_status now admit an additional group parameter.
+ This parameter can be used to force those functions to retrieve data only for the given group.
+
=== 3.4 ===
* assign::add_attempt requires that set_most_recent_team_submission() be called if attempting to use this function with a team
submission.
-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
$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';
+++ /dev/null
-postmailinfo,mod_forum
-emaildigestupdated,mod_forum
-emaildigestupdated_default,mod_forum
-emaildigest_0,mod_forum
-emaildigest_1,mod_forum
-emaildigest_2,mod_forum
$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.';
}
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>';
}
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
}
if (!empty($entry->rating)) {
echo '<br />';
- echo '<span class="ratings">';
+ echo '<span class="ratings d-block p-t-1">';
$return = glossary_print_entry_ratings($course, $entry);
echo '</span>';
}
}
echo '</td></tr>';
- echo "</table>\n";
+ echo "</table>";
+ echo "<hr>\n";
return $return;
}
$return .= '<div>'.$comment->output(true).'</div>';
$output = true;
}
- $return .= '<hr>';
//If we haven't calculated any REAL thing, delete result ($return)
if (!$output) {
echo '<tr valign="top"><td class="icons">'.$icons.'</td></tr>';
}
if (!empty($entry->rating)) {
- echo '<tr valign="top"><td class="ratings">';
+ echo '<tr valign="top"><td class="ratings p-t-1">';
glossary_print_entry_ratings($course, $entry);
echo '</td></tr>';
}
echo '</table>';
+ echo "<hr>\n";
}
}
+++ /dev/null
-configactionaftercorrectanswer,mod_lesson
\ No newline at end of file
$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';
if (window != top) {
// Send return data to be processed by the parent window.
parent.processContentItemReturnData(returnData);
+ } else {
+ window.processContentItemReturnData(returnData);
}
});
}
//<![CDATA[
if(window != top){
top.location.href = '{$url}';
+ } else {
+ window.location.href = '{$url}';
}
//]]
</script>
} else if ($qid) {
// Report on an individual sub-question indexed questionid.
- if (is_null($questionstats->for_subq($qid, $variantno))) {
+ if (!$questionstats->has_subq($qid, $variantno)) {
print_error('questiondoesnotexist', 'question');
}
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"
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
+++ /dev/null
-dimensioncomment,workshopform_accumulative
-dimensiongrade,workshopform_accumulative
$string['scalename6'] = 'Excellent/Very poor (7 point)';
$string['verypoor'] = 'Very poor';
-// Deprecated since Moodle 3.1.
-$string['dimensioncomment'] = 'Comment';
-$string['dimensiongrade'] = 'Grade';
+++ /dev/null
-dimensioncomment,workshopform_comments
$string['dimensionnumber'] = 'Aspect {$a}';
$string['pluginname'] = 'Comments';
-// Deprecated since Moodle 3.1.
-$string['dimensioncomment'] = 'Comment';
+++ /dev/null
-dimensioncomment,workshopform_numerrors
$string['percents'] = '{$a} %';
$string['pluginname'] = 'Number of errors';
-// Deprecated since Moodle 3.1.
-$string['dimensioncomment'] = 'Comment';
err_unknownfileextension,mod_workshop
err_wrongfileextension,mod_workshop
-yourassessment,mod_workshop
$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.';
*/
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);
}
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;
* @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);
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 = '';
}
}
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();
$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);
$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;
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()));
}
}
+ /**
+ * Do we have stats for a particular quesitonid (and optionally variant)?
+ *
+ * @param int $questionid The id of the sub question.
+ * @param int|null $variant if not null then we want the object to store a variant of a sub-question's stats.
+ * @return bool whether those stats exist (yet).
+ */
+ public function has_subq($questionid, $variant = null) {
+ if ($variant === null) {
+ return isset($this->subquestionstats[$questionid]);
+ } else {
+ return isset($this->subquestionstats[$questionid]->variantstats[$variant]);
+ }
+ }
+
/**
* Reference for a item stats instance for a questionid and optional variant no.
*
* @param int $questionid The id of the sub question.
* @param int|null $variant if not null then we want the object to store a variant of a sub-question's stats.
- * @return calculated_for_subquestion|null null if the stats object does not yet exist.
+ * @return calculated|calculated_for_subquestion stats instance for a questionid and optional variant no.
+ * Will be a calculated_for_subquestion if no variant specified.
+ * @throws \coding_exception if there is an attempt to respond to a non-existant set of stats.
*/
public function for_subq($questionid, $variant = null) {
if ($variant === null) {
if (!isset($this->subquestionstats[$questionid])) {
- return null;
+ throw new \coding_exception('Reference to unknown question id ' . $questionid);
} else {
return $this->subquestionstats[$questionid];
}
} else {
if (!isset($this->subquestionstats[$questionid]->variantstats[$variant])) {
- return null;
+ throw new \coding_exception('Reference to unknown question id ' . $questionid .
+ ' variant ' . $variant);
} else {
return $this->subquestionstats[$questionid]->variantstats[$variant];
}
return array_keys($this->questionstats);
}
+ /**
+ * Do we have stats for a particular slot (and optionally variant)?
+ *
+ * @param int $slot The slot no.
+ * @param int|null $variant if provided then we want the object which stores a variant of a position's stats.
+ * @return bool whether those stats exist (yet).
+ */
+ public function has_slot($slot, $variant = null) {
+ if ($variant === null) {
+ return isset($this->questionstats[$slot]);
+ } else {
+ return isset($this->questionstats[$slot]->variantstats[$variant]);
+ }
+ }
+
/**
* Get position stats instance for a slot and optional variant no.
*
* @param int $slot The slot no.
- * @param null $variant if provided then we want the object which stores a variant of a position's stats.
- * @return calculated|null An instance of the class storing the calculated position stats.
+ * @param int|null $variant if provided then we want the object which stores a variant of a position's stats.
+ * @return calculated|calculated_for_subquestion An instance of the class storing the calculated position stats.
+ * @throws \coding_exception if there is an attempt to respond to a non-existant set of stats.
*/
public function for_slot($slot, $variant = null) {
if ($variant === null) {
if (!isset($this->questionstats[$slot])) {
- return null;
+ throw new \coding_exception('Reference to unknown slot ' . $slot);
} else {
return $this->questionstats[$slot];
}
} else {
if (!isset($this->questionstats[$slot]->variantstats[$variant])) {
- return null;
+ throw new \coding_exception('Reference to unknown slot ' . $slot . ' variant ' . $variant);
} else {
return $this->questionstats[$slot]->variantstats[$variant];
}
$israndomquestion = ($step->questionid != $this->stats->for_slot($step->slot)->questionid);
$breakdownvariants = !$israndomquestion && $this->stats->for_slot($step->slot)->break_down_by_variant();
// If this is a variant we have not seen before create a place to store stats calculations for this variant.
- if ($breakdownvariants && is_null($this->stats->for_slot($step->slot , $step->variant))) {
+ if ($breakdownvariants && !$this->stats->has_slot($step->slot, $step->variant)) {
$question = $this->stats->for_slot($step->slot)->question;
$this->stats->initialise_for_slot($step->slot, $question, $step->variant);
$this->stats->for_slot($step->slot, $step->variant)->randomguessscore =
// If this is a random question do the calculations for sub question stats.
if ($israndomquestion) {
- if (is_null($this->stats->for_subq($step->questionid))) {
+ if (!$this->stats->has_subq($step->questionid)) {
$this->stats->initialise_for_subq($step);
} else if ($this->stats->for_subq($step->questionid)->maxmark != $step->maxmark) {
$this->stats->for_subq($step->questionid)->differentweights = true;
}
// If this is a variant of this subq we have not seen before create a place to store stats calculations for it.
- if (is_null($this->stats->for_subq($step->questionid, $step->variant))) {
+ if (!$this->stats->has_subq($step->questionid, $step->variant)) {
$this->stats->initialise_for_subq($step, $step->variant);
}
if (!isset($this->subparts[$variantno])) {
$this->initialise_stats_for_variant($variantno);
}
+ if (!isset($this->subparts[$variantno][$subpartid])) {
+ debugging('Unexpected sub-part id ' . $subpartid .
+ ' encountered.');
+ $this->subparts[$variantno][$subpartid] = new analysis_for_subpart();
+ }
return $this->subparts[$variantno][$subpartid];
}
*/
class analysis_for_subpart {
+ /**
+ * @var analysis_for_class[]
+ */
+ protected $responseclasses;
+
/**
* Takes an array of possible_responses as returned from {@link \question_type::get_possible_responses()}.
*
foreach ($responseclasses as $responseclassid => $responseclass) {
$this->responseclasses[$responseclassid] = new analysis_for_class($responseclass, $responseclassid);
}
+ } else {
+ $this->responseclasses = [];
}
}
- /**
- * @var analysis_for_class[]
- */
- protected $responseclasses;
-
/**
* Unique ids for response classes.
*
* @return analysis_for_class
*/
public function get_response_class($classid) {
+ if (!isset($this->responseclasses[$classid])) {
+ debugging('Unexpected class id ' . $classid . ' encountered.');
+ $this->responseclasses[$classid] = new analysis_for_class('[Unknown]', $classid);
+ }
return $this->responseclasses[$classid];
+
}
/**
$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;
+ }
}
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);
}
/**
$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 {
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
/**
* @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);
}
/**
if ($commentformat === null) {
$commentformat = FORMAT_HTML;
}
- return array($comment, $commentformat);
+ return array($comment, $commentformat, null);
}
}
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M11.197 15v-1.232h1.231V15h-1.231zm1.119-1.657h-1.119v-.291a1.935 1.935 0 0 1 .179-.896 2.72 2.72 0 0 1 .716-.784c.228-.173.441-.364.639-.571a.772.772 0 0 0 .156-.47.793.793 0 0 0-.235-.616 1.114 1.114 0 0 0-.772-.257 1.121 1.121 0 0 0-.784.268 1.42 1.42 0 0 0-.437.818l-1.119-.135c.029-.518.272-1 .671-1.332a2.366 2.366 0 0 1 1.624-.549 2.454 2.454 0 0 1 1.679.549c.391.314.621.786.627 1.288a1.405 1.405 0 0 1-.235.772 5.05 5.05 0 0 1-.985.985c-.192.14-.356.315-.482.516a1.749 1.749 0 0 0-.123.705zm-10.637.022v-1.119h4.479v1.119H1.679zm0-1.937v-1.119h4.479v1.119H1.679zm13.582-5.676v1.119h-4.299c.049-.437.194-.857.425-1.231a8.204 8.204 0 0 1 1.377-1.512 8.714 8.714 0 0 0 1.008-1.052c.159-.219.249-.48.257-.751a.851.851 0 0 0-.257-.627.943.943 0 0 0-1.221 0c-.173.22-.264.493-.257.773l-1.187-.112A2.007 2.007 0 0 1 11.79.903a2.306 2.306 0 0 1 1.444-.436c.545-.03 1.08.155 1.49.515.354.328.55.793.537 1.276a2.22 2.22 0 0 1-.157.829c-.123.295-.289.57-.492.817a7.781 7.781 0 0 1-.807.817q-.582.538-.739.706a2.264 2.264 0 0 0-.246.336h2.441v-.011zm-7.703.224V4.262h-1.68V3.143h1.68V1.486h1.119v1.657h1.736v1.119H8.677v1.714H7.558zM5.05 5.752v1.119H.75c.049-.437.194-.858.426-1.231a8.192 8.192 0 0 1 1.399-1.512c.369-.321.71-.674 1.019-1.052.159-.219.249-.48.258-.751a.858.858 0 0 0-.224-.627.822.822 0 0 0-.605-.224.818.818 0 0 0-.616.236 1.186 1.186 0 0 0-.257.772L.896 2.359A2.004 2.004 0 0 1 1.579.903 2.302 2.302 0 0 1 3.023.456a2.082 2.082 0 0 1 1.489.515c.355.328.55.793.538 1.276 0 .284-.053.565-.157.829-.125.294-.29.569-.493.817a7.772 7.772 0 0 1-.806.817l-.728.672a2.195 2.195 0 0 0-.246.336h2.43v.034z"/></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M11.012 15.594v-1.181h1.18v1.181h-1.18zm1.073-1.588h-1.073v-.279a1.856 1.856 0 0 1 .172-.859c.18-.281.414-.526.687-.719a5.2 5.2 0 0 0 .611-.547.737.737 0 0 0 .15-.526.758.758 0 0 0-.225-.547 1.067 1.067 0 0 0-.74-.247 1.077 1.077 0 0 0-.752.257c-.223.207-.37.483-.418.784l-1.073-.129a1.79 1.79 0 0 1 .644-1.277c.434-.364.99-.551 1.556-.526a2.348 2.348 0 0 1 1.609.526c.375.301.595.754.601 1.234a1.348 1.348 0 0 1-.225.741c-.27.356-.588.674-.944.944a1.78 1.78 0 0 0-.462.493 1.666 1.666 0 0 0-.118.677zm-10.194 0v-1.074h4.292v1.074H1.891zm0-1.857v-1.073h4.292v1.073H1.891zm13.016-6.664v1.073h-4.12a2.81 2.81 0 0 1 .408-1.18 7.79 7.79 0 0 1 1.319-1.449c.35-.309.673-.646.966-1.009a1.3 1.3 0 0 0 .247-.719.83.83 0 0 0-.247-.601.901.901 0 0 0-1.169 0c-.167.21-.255.473-.247.741l-1.17-.118a1.918 1.918 0 0 1 .655-1.395 2.212 2.212 0 0 1 1.384-.419 2 2 0 0 1 1.427.494c.34.315.528.76.515 1.223.001.272-.05.542-.15.794a3.208 3.208 0 0 1-.472.784 7.619 7.619 0 0 1-.773.783q-.558.515-.708.676a2.018 2.018 0 0 0-.236.322h2.371zm-7.383.225V4.101H5.915V3.028h1.609v-1.61h1.074v1.577h1.663v1.074H8.598V5.71H7.524zm-2.403-.225v1.073H1c.046-.419.186-.822.408-1.18a7.813 7.813 0 0 1 1.341-1.449c.342-.309.658-.647.944-1.009.153-.209.239-.46.247-.719a.817.817 0 0 0-.214-.601.784.784 0 0 0-.58-.182.786.786 0 0 0-.59.225c-.167.21-.254.473-.247.741l-1.18-.161A1.921 1.921 0 0 1 1.783.828 2.213 2.213 0 0 1 3.178.409a2.002 2.002 0 0 1 1.428.494c.339.315.527.76.515 1.223 0 .272-.051.542-.151.794a3.208 3.208 0 0 1-.472.784 7.609 7.609 0 0 1-.772.783q-.558.515-.709.676a2.13 2.13 0 0 0-.236.322h2.34z"/><path vector-effect="non-scaling-stroke" stroke-width="1.073" stroke="#000" stroke-miterlimit="10" d="M1.676 8.071h12.877"/></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M3.676 8.93l2.249-.229c.072.631.362 1.218.819 1.658a2.495 2.495 0 0 0 1.679.53 2.566 2.566 0 0 0 1.681-.471c.353-.261.562-.671.569-1.108a.996.996 0 0 0-.239-.689 1.899 1.899 0 0 0-.84-.48c-.268-.093-.881-.26-1.84-.5a6.303 6.303 0 0 1-2.608-1.129 3.002 3.002 0 0 1-1.06-2.31 2.938 2.938 0 0 1 .511-1.63c.346-.521.84-.925 1.42-1.16a5.708 5.708 0 0 1 2.249-.399 4.835 4.835 0 0 1 3.249.95 3.338 3.338 0 0 1 1.14 2.519l-2.31.101a2.014 2.014 0 0 0-.641-1.27 2.326 2.326 0 0 0-1.46-.391 2.644 2.644 0 0 0-1.579.41.827.827 0 0 0-.37.71c0 .253.109.493.301.66a6.073 6.073 0 0 0 2.13.77c.861.174 1.701.449 2.499.819a3.23 3.23 0 0 1 1.3 1.21c.32.552.479 1.183.46 1.819a3.385 3.385 0 0 1-.55 1.84 3.28 3.28 0 0 1-1.55 1.279 6.524 6.524 0 0 1-2.489.42 5.002 5.002 0 0 1-3.34-1 4.347 4.347 0 0 1-1.38-2.929z" fill="#999"/><path d="M11.197 15v-1.232h1.231V15h-1.231zm1.119-1.657h-1.119v-.291a1.935 1.935 0 0 1 .179-.896 2.72 2.72 0 0 1 .716-.784c.228-.173.441-.364.639-.571a.772.772 0 0 0 .156-.47.791.791 0 0 0-.235-.616 1.112 1.112 0 0 0-.772-.257 1.123 1.123 0 0 0-.784.268 1.42 1.42 0 0 0-.437.818l-1.119-.135c.029-.518.272-1 .671-1.332a2.366 2.366 0 0 1 1.624-.549 2.454 2.454 0 0 1 1.679.549c.391.314.621.786.627 1.288a1.405 1.405 0 0 1-.235.772 5.05 5.05 0 0 1-.985.985c-.192.14-.356.315-.482.516a1.752 1.752 0 0 0-.123.705zm-10.637.022v-1.119h4.479v1.119H1.679zm0-1.937v-1.119h4.479v1.119H1.679zm13.582-5.676v1.119h-4.299c.049-.437.194-.857.425-1.231a8.236 8.236 0 0 1 1.377-1.512 8.714 8.714 0 0 0 1.008-1.052c.159-.219.249-.48.257-.751a.848.848 0 0 0-.257-.627.943.943 0 0 0-1.221 0c-.173.22-.264.493-.257.773l-1.187-.112A2.007 2.007 0 0 1 11.79.903a2.304 2.304 0 0 1 1.444-.436c.545-.03 1.08.155 1.49.515.354.328.55.793.537 1.276a2.22 2.22 0 0 1-.157.829c-.123.295-.289.57-.492.817a7.781 7.781 0 0 1-.807.817q-.582.538-.739.706a2.317 2.317 0 0 0-.246.336h2.441v-.011zm-7.703.224V4.262h-1.68V3.143h1.68V1.486h1.119v1.657h1.736v1.119H8.677v1.714H7.558zM5.05 5.752v1.119H.75c.049-.437.194-.858.426-1.231a8.192 8.192 0 0 1 1.399-1.512c.369-.321.71-.674 1.019-1.052.159-.219.249-.48.258-.751a.856.856 0 0 0-.224-.627.822.822 0 0 0-.605-.224.818.818 0 0 0-.616.236 1.186 1.186 0 0 0-.257.772L.896 2.359A2.004 2.004 0 0 1 1.579.903 2.302 2.302 0 0 1 3.023.456a2.082 2.082 0 0 1 1.489.515c.355.328.55.793.538 1.276 0 .284-.053.565-.157.829-.125.294-.29.569-.493.817a7.772 7.772 0 0 1-.806.817l-.728.672a2.195 2.195 0 0 0-.246.336h2.43v.034z"/></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M6.85 9.15v3.3h-1.6l2.8 2.8 2.6-2.8h-1.5v-3.3h3.3v1.5l2.8-2.6-2.8-2.8v1.6h-3.3v-3.3h1.5L7.95.75l-2.7 2.8h1.6v3.3h-3.3v-1.6l-2.8 2.8 2.8 2.6v-1.5h3.3zm.9-.9h.5v-.5h-.5v.5z"/></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><clipPath id="b"><path d="M.35.3h15.3v15.4H.35V.3z" fill="#FFF"/></clipPath><g clip-path="url(#b)"><path d="M9.139 11.539v.85h1.5l-2.6 2.9-2.8-2.9h1.6v-.85h2.3zm-4.695-2.35h-.85v1.5l-2.9-2.6 2.9-2.8v1.6h.85v2.3zm2.406-4.65v-.85h-1.5l2.6-2.9 2.8 2.9h-1.6v.85h-2.3zm4.639 2.25h.85v-1.5l2.9 2.6-2.9 2.8v-1.6h-.85v-2.3zm-.939 4.129h-.145L8.239 9.089l.505-.512 1.879 2.194a.153.153 0 0 1-.073.147zM9.394 7.406L9.25 7.26l-.289-.146-.289.146-1.661-1.39a.37.37 0 0 0 0-.512l-.216-.146a.214.214 0 0 0-.289 0L4.917 6.821a.221.221 0 0 0 0 .293l.144.146.289.146h.217l1.372 1.61a.368.368 0 0 0 0 .512l.144.146a.214.214 0 0 0 .289 0l2.022-2.048a.221.221 0 0 0 0-.22z"/></g></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M9.173 11.5v.85h1.5l-2.6 2.9-2.8-2.9h1.6v-.85h2.3zM4.478 9.15h-.85v1.5l-2.9-2.6 2.9-2.8v1.6h.85v2.3zM6.884 4.5v-.85h-1.5L7.983.75l2.801 2.9h-1.6v.85h-2.3zM11.522 6.75h.851v-1.5l2.899 2.6-2.899 2.8v-1.6h-.851v-2.3zM5.361 5h5.279v1.76h-.24a1.601 1.601 0 0 0-.719-1.28l-.96-.16v4.64a.876.876 0 0 0 .16.64c.215.133.468.189.72.16V11h-3.12v-.24a1.04 1.04 0 0 0 .64-.16.885.885 0 0 0 .16-.64V5.32l-.96.16A1.523 1.523 0 0 0 5.6 6.76h-.239V5z"/></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M1.5 12.5h12.7M1.5 2.5h13m0 10.5V2m-13 11V2" fill="none" vector-effect="non-scaling-stroke" stroke="#000" stroke-miterlimit="10"/><path d="M2 3h12v9H2V3z" fill="#FFF"/><path d="M3 4.5h2m-2 6h2m-2-2h5m-2 2h2m3-4h2m-10 0h3m0-2h6m-5 2h3" fill="#A5B0B0" vector-effect="non-scaling-stroke" stroke="#A5B0B0" stroke-miterlimit="10"/><path d="M9 8.5h4m-3 1h1" fill="#0092FF" vector-effect="non-scaling-stroke" stroke="#0092FF" stroke-miterlimit="10"/><path vector-effect="non-scaling-stroke" stroke="#824E5D" stroke-miterlimit="10" d="M11 9.5h1"/><path vector-effect="non-scaling-stroke" stroke="#009F00" stroke-miterlimit="10" d="M9 9.5h1"/><path d="M9 10.5h4m-1-1h1" fill="#239771" vector-effect="non-scaling-stroke" stroke="#239771" stroke-miterlimit="10"/></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M2 15.5h12M2 .6h12m-.5 14.9V.6m-11 14.9V.6" fill="none" vector-effect="non-scaling-stroke" stroke="#000" stroke-miterlimit="10"/><path d="M3 1h10v14H3V1z" fill="#FFF"/><path d="M4 7.5h1m6 4h1m-3-2h3m-5 2h3m-4-4h3m-1-2h4m-8 8.1h3m3-6.1h2m-4 6.1h2M4 9.5h4m-4 2h2m-2-6h3" fill="#A5B0B0" vector-effect="non-scaling-stroke" stroke="#A5B0B0" stroke-miterlimit="10"/><path d="M9 2.5h2m-6 0h3" fill="#7C7C7C" vector-effect="non-scaling-stroke" stroke="#7C7C7C" stroke-miterlimit="10"/></g></svg>
\ No newline at end of file
$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;
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) {
--- /dev/null
+<svg id="Ebene_3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.st0{fill:#fff}.st0,.st1{stroke:#000;stroke-miterlimit:10}</style><path class="st0" d="M2 6zM0 0zM14 7zM8 11z"/><path class="st1" d="M8 12L3 7h10z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)" stroke="#000" stroke-miterlimit="10"><path d="M6 13.5h4m-4-5h4m-4-5h4" fill="none" vector-effect="non-scaling-stroke"/><path d="M12.5 13.5a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0-5a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0-5a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-11.2 0a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0 5a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0 5a1 1 0 1 1 2 0 1 1 0 0 1-2 0z" vector-effect="non-scaling-stroke"/></g></svg>
\ No newline at end of file
--- /dev/null
+<svg id="Ebene_3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.st0,.st1{fill:#fff;stroke:#000;stroke-miterlimit:10}.st1{fill:none}.st2{font-family:'ArialMT'}.st3{font-size:18px}</style><path class="st0" d="M2 6zM0 0zM14 7zM8 11z"/><text transform="translate(2.656 14.533)" class="st2 st3">?</text></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M9 1h6v3H9V1zM8 12h3v3H8v-3zM1 6h7v3H1V6z"/><path d="M1 1h2v3H1V1zm4 0h2v3H5V1zM1 12h5v3H1v-3zm12 0h2v3h-2v-3zm-3-6h5v3h-5V6z" fill="#8E8E8E"/></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M7 3h7v1H7V3zm0 10h7v1H7v-1zm0-5h7v1H7V8z"/><path d="M2 13.5a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0-10a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0 5a1 1 0 1 1 2 0 1 1 0 0 1-2 0z" vector-effect="non-scaling-stroke" stroke="#000" stroke-miterlimit="10"/></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><clipPath id="b"><path d="M0 2.5h16v11H0v-11z" fill="#FFF"/></clipPath><g clip-path="url(#b)"><path d="M10.915 6.004v.779H8.092c0-.292.097-.584.292-.779.267-.346.56-.671.876-.973.195-.292.487-.584.681-.779a.739.739 0 0 0 .195-.487c0-.194-.097-.292-.195-.389-.194-.195-.584-.195-.778 0-.098.097-.195.195-.195.389h-.779c0-.389.195-.681.487-.973.194-.195.584-.292.876-.292.389 0 .681.097.973.292.292.292.39.584.39.876 0 .195 0 .39-.098.584-.097.195-.194.39-.292.487-.194.195-.292.389-.486.487-.39.292-.487.389-.584.486-.098.098-.098.195-.195.195l1.655.097zm-3.894.682h-.876V3.668c-.292.292-.584.487-.974.584v-.681c.195-.098.487-.195.682-.39.194-.194.389-.389.486-.681h.682v4.186zm7.788 1.947H1.18v3.894h14.602v.973H.207V7.659h15.575v4.868h-.973V8.633z"/><path d="M1.18 8.633h13.629v3.894H1.18V8.633z" fill="#FFF"/></g></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M14.7 5.5l-.096 5.97-5.008 3.23L9.5 8.143 14.7 5.5zM7.8 1.1l5.7 2.3-4.9 3-5.8-2.8 5-2.5zm-.1 7l.1 6.7-5.9-3.2-.1-6.7 5.9 3.2z" vector-effect="non-scaling-stroke" stroke="#000" stroke-miterlimit="10"/><path d="M7.9.1s-.1 0-.1.1L1.6 3.6c-.1 0 0 0 0 .1l6.9 3.5c.1.1.2.1.3 0l6.1-3.7c.1-.1.1-.1 0-.2L8.2.3C8.1.1 8 .1 7.9.1zm-.6.8h.4c.6 0 1.1.3 1.1.6 0 .2-.5.5-1.1.5-.6 0-1.1-.3-1.1-.6 0-.2.3-.4.7-.5zM3.8 2.8h.4c.6 0 1.1.3 1.1.6s-.5.6-1.1.6c-.6 0-1.1-.3-1.1-.6s.3-.5.7-.6zm5.7-.9h.4c.6 0 1.1.3 1.1.6s-.5.6-1.1.6c-.6-.1-1-.3-1-.7 0-.2.3-.4.6-.5zM1.2 4c0 .1 0 .1 0 0l.1 7.9c0 .1 0 .1.1.1l6.7 3.7c.2.1.2.1.2-.1v-8c0-.1 0-.2-.1-.2L1.2 4c.1.1 0 0 0 0zM6 3.9h.4c.6 0 1.1.3 1.1.6s-.5.6-1.1.5c-.6 0-1.1-.2-1-.5 0-.3.2-.5.6-.6zm5.8-1h.4c.6 0 1.1.3 1.1.6s-.5.6-1.1.6c-.6 0-1.1-.3-1.1-.6.1-.3.3-.5.7-.6zM1.9 5.4c0-.1 0-.1 0 0 .3-.1.7.1 1 .5.3.5.3 1.1.1 1.4-.3.2-.8.1-1.2-.4-.3-.5-.4-1.2-.1-1.4.1-.1.1-.1.2-.1zM8.3 5h.4c.6 0 1.1.3 1.1.6s-.5.6-1.1.6c-.6 0-1.1-.3-1.1-.6.1-.3.3-.5.7-.6zm7-1.3s0 .1 0 0L9 7.6c-.1.1-.1.1-.1.2v7.9c0 .1 0 .2.1.1l6.2-3.9c.1-.1.1-.1.1-.2v-8c.1 0 .1 0 0 0zm-.8 1.5c.2 0 .4 0 .5.2.2.3.1 1-.3 1.4-.4.4-.9.5-1.1.2-.2-.3-.1-1 .3-1.4.2-.2.4-.4.6-.4zM6.4 7.6c.3 0 .6.2.9.5.3.5.4 1.1.1 1.4-.3.3-.8.1-1.1-.4-.4-.5-.4-1.1-.1-1.4 0 0 .1 0 .2-.1zM4.1 9c.3 0 .7.2.9.5.3.5.4 1.1.1 1.4-.2.3-.7.1-1.1-.4-.3-.5-.4-1.1-.1-1.4.1 0 .1-.1.2-.1zm-2.2 1.4c.1 0 .1 0 0 0 .3-.1.7.1 1 .5.3.5.4 1.1.1 1.4-.3.3-.8.1-1.1-.4-.3-.5-.4-1.1-.1-1.4 0-.1.1-.1.1-.1zm10.7-1.5c.2 0 .4 0 .5.2.2.3.1 1-.3 1.4-.4.4-.9.5-1.1.2-.2-.3-.1-1 .3-1.4.2-.2.4-.3.6-.4zm-6.3 3.9c.1-.1.1-.1 0 0 .3-.1.7.1 1 .5.3.5.4 1.1.1 1.4-.3.3-.8.1-1.1-.4-.3-.5-.4-1.1-.1-1.4 0-.1.1-.1.1-.1zm4.2-.1c.2 0 .4 0 .5.2.2.3.1 1-.3 1.4-.4.4-.9.5-1.1.2-.2-.3-.1-1 .3-1.4.2-.2.4-.4.6-.4z" fill="#C4CCCB"/></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><clipPath id="b"><path d="M0 .571h16v14.858H0V.571z" fill="#FFF"/></clipPath><g clip-path="url(#b)"><path d="M13.183 13.632a1.127 1.127 0 1 1 2.253-.071 1.127 1.127 0 0 1-2.253.071zm0-5.633a1.129 1.129 0 1 1 1.127 1.093 1.129 1.129 0 0 1-1.127-1.093zm0-5.634a1.128 1.128 0 1 1 2.254-.073 1.128 1.128 0 0 1-2.254.073zm-12.62 0a1.13 1.13 0 0 1 1.703-1.006A1.127 1.127 0 1 1 .563 2.365zm0 5.634a1.129 1.129 0 1 1 1.128 1.093A1.129 1.129 0 0 1 .563 8v-.001zm0 5.633a1.13 1.13 0 0 1 1.703-1.006 1.127 1.127 0 0 1-.576 2.099 1.129 1.129 0 0 1-1.127-1.093z" vector-effect="non-scaling-stroke" stroke-width="1.127" stroke="#000" stroke-miterlimit="10"/><path d="M7.211 10.68v-.36a3.366 3.366 0 0 1 .191-1.194 2.79 2.79 0 0 1 .475-.779 8.98 8.98 0 0 1 .856-.833c.307-.249.577-.54.8-.867.122-.217.184-.462.18-.711-.004-.46-.2-.896-.542-1.205a1.837 1.837 0 0 0-1.329-.53 1.777 1.777 0 0 0-1.261.473c-.402.4-.657.925-.722 1.488l-1.126-.147a3.157 3.157 0 0 1 .934-2.073c.603-.5 1.37-.757 2.152-.721.823-.038 1.63.24 2.254.778.543.468.852 1.153.846 1.87a2.372 2.372 0 0 1-.294 1.126 5.517 5.517 0 0 1-1.127 1.296 5.918 5.918 0 0 0-.754.756c-.121.17-.209.361-.259.563a4.71 4.71 0 0 0-.102 1.025H7.256l-.045.045zm-.067 2.389v-1.352h1.352v1.352H7.144z"/></g></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M16 6V5H0v6h16v-1H1V6h14v4h1V6z"/><path d="M1 6h14v4H1V6z" fill="#FFF"/></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="16" height="16"><defs><clipPath id="a"><path d="M0 0h16v16H0z"/></clipPath></defs><g clip-path="url(#a)"><path d="M10 8a2 2 0 1 1 4.001.001A2 2 0 0 1 10 8zM2 8a2 2 0 1 1 4.001.001A2 2 0 0 1 2 8z"/></g></svg>
\ No newline at end of file
$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');
}
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);
}
}
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"
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"
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"
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"
defined('MOODLE_INTERNAL') || die();
-$version = 2018011200.00; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2018011800.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
-$release = '3.5dev (Build: 20180112)'; // Human-friendly version name
+$release = '3.5dev (Build: 20180118)'; // Human-friendly version name
$branch = '35'; // This version's branch.
$maturity = MATURITY_ALPHA; // This version's maturity level.