include:
# Run grunt/npm install on highest version ('node' is an alias for the latest node.js version.)
- php: 7.1
- env: DB=none TASK=GRUNT NVM_VERSION='node'
+ env: DB=none TASK=GRUNT NVM_VERSION='8.9'
exclude:
# MySQL - it's just too slow.
$eventshandlers = $DB->get_records_sql('SELECT DISTINCT component FROM {events_handlers}');
$themedesignermode = !empty($CFG->themedesignermode);
$mobileconfigured = !empty($CFG->enablemobilewebservice);
+$invalidforgottenpasswordurl = !empty($CFG->forgottenpasswordurl) && empty(clean_param($CFG->forgottenpasswordurl, PARAM_URL));
// Check if a directory with development libraries exists.
if (empty($CFG->disabledevlibdirscheck) && (is_dir($CFG->dirroot.'/vendor') || is_dir($CFG->dirroot.'/node_modules'))) {
echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
$maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
$registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir,
- $mobileconfigured, $overridetossl);
+ $mobileconfigured, $overridetossl, $invalidforgottenpasswordurl);
* @return string
*/
public function moodleorg_registration_message() {
- return format_text(get_string('registermoodlenet', 'admin'), FORMAT_MARKDOWN, ['noclean' => true]);
+ return format_text(get_string('registermoodlenet', 'admin'), FORMAT_HTML, ['noclean' => true]);
}
}
* @param bool $devlibdir Warn about development libs directory presence.
* @param bool $mobileconfigured Whether the mobile web services have been enabled
* @param bool $overridetossl Whether or not ssl is being forced.
+ * @param bool $invalidforgottenpasswordurl Whether the forgotten password URL does not link to a valid URL.
*
* @return string HTML to output.
*/
$cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
$buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0,
$themedesignermode = false, $devlibdir = false, $mobileconfigured = false,
- $overridetossl = false) {
+ $overridetossl = false, $invalidforgottenpasswordurl = false) {
global $CFG;
$output = '';
$output .= $this->events_handlers($eventshandlers);
$output .= $this->registration_warning($registered);
$output .= $this->mobile_configuration_warning($mobileconfigured);
+ $output .= $this->forgotten_password_url_warning($invalidforgottenpasswordurl);
//////////////////////////////////////////////////////////////////////////////////////////////////
//// IT IS ILLEGAL AND A VIOLATION OF THE GPL TO HIDE, REMOVE OR MODIFY THIS COPYRIGHT NOTICE ///
return $output;
}
+ /**
+ * Display a warning about the forgotten password URL not linking to a valid URL.
+ *
+ * @param boolean $invalidforgottenpasswordurl true if the forgotten password URL is not valid
+ * @return string HTML to output.
+ */
+ protected function forgotten_password_url_warning($invalidforgottenpasswordurl) {
+ $output = '';
+ if ($invalidforgottenpasswordurl) {
+ $settingslink = new moodle_url('/admin/settings.php', ['section' => 'manageauths']);
+ $configurebutton = $this->single_button($settingslink, get_string('check', 'moodle'));
+ $output .= $this->warning(get_string('invalidforgottenpasswordurl', 'admin') . ' ' . $configurebutton,
+ 'error alert alert-danger');
+ }
+
+ return $output;
+ }
+
/**
* Helper method to render the information about the available Moodle update
*
$temp->add(new admin_setting_configtext('alternateloginurl', new lang_string('alternateloginurl', 'auth'),
new lang_string('alternatelogin', 'auth', htmlspecialchars(get_login_url())), ''));
$temp->add(new admin_setting_configtext('forgottenpasswordurl', new lang_string('forgottenpasswordurl', 'auth'),
- new lang_string('forgottenpassword', 'auth'), ''));
+ new lang_string('forgottenpassword', 'auth'), '', PARAM_URL));
$temp->add(new admin_setting_confightmleditor('auth_instructions', new lang_string('instructions', 'auth'),
new lang_string('authinstructions', 'auth'), ''));
$setting = new admin_setting_configtext('allowemailaddresses', new lang_string('allowemailaddresses', 'admin'),
namespace tool_httpsreplace;
+use database_column_info;
+use progress_bar;
+
defined('MOODLE_INTERNAL') || die();
/**
* for less straightforward swaps.
*
* @param string $table
- * @param string $column
+ * @param database_column_info $column
* @param string $domain
* @param string $search search string that has prefix, protocol, domain name and one extra character,
* example1: src="http://host.com/
$regex = '#((src|data)\ *=\ *[\'\"])(http://)([^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))[\'\"]#i';
preg_match_all($regex, $record->$columnname, $match);
foreach ($match[0] as $i => $fullmatch) {
- if (strpos($fullmatch, $CFG->wwwroot) !== false) {
+ if (\core_text::strpos($fullmatch, $CFG->wwwroot) !== false) {
continue;
}
$prefix = $match[1][$i];
}
if ($replacing) {
// For replace string use: prefix, protocol, host and one extra character.
- $found[$prefix . substr($url, 0, strlen($host) + 8)] = $host;
+ $found[$prefix . \core_text::substr($url, 0, \core_text::strlen($host) + 8)] = $host;
} else {
$entry["table"] = $table;
$entry["columnname"] = $columnname;
'rememberusername' => $CFG->rememberusername,
'authloginviaemail' => $CFG->authloginviaemail,
'registerauth' => $CFG->registerauth,
- 'forgottenpasswordurl' => $CFG->forgottenpasswordurl,
+ 'forgottenpasswordurl' => clean_param($CFG->forgottenpasswordurl, PARAM_URL), // We may expect a mailto: here.
'authinstructions' => $authinstructions,
'authnoneenabled' => (int) is_enabled_auth('none'),
'enablewebservices' => $CFG->enablewebservices,
set_config('typeoflogin', api::LOGIN_VIA_BROWSER, 'tool_mobile');
set_config('logo', 'mock.png', 'core_admin');
set_config('logocompact', 'mock.png', 'core_admin');
+ set_config('forgottenpasswordurl', 'mailto:fake@email.zy'); // Test old hack.
list($authinstructions, $notusedformat) = external_format_text($authinstructions, FORMAT_MOODLE, $context->id);
$expected['registerauth'] = 'email';
$expected['authinstructions'] = $authinstructions;
$expected['typeoflogin'] = api::LOGIN_VIA_BROWSER;
+ $expected['forgottenpasswordurl'] = ''; // Expect empty when it's not an URL.
if ($logourl = $OUTPUT->get_logo_url()) {
$expected['logourl'] = $logourl->out(false);
// Default to all system courses.
if (!empty($this->options['filter'])) {
- $courses = $this->options['filter'];
+ $courses = array();
+ foreach ($this->options['filter'] as $courseid) {
+ $courses[$courseid] = new \stdClass();
+ $courses[$courseid]->id = $courseid;
+ }
} else {
// Iterate through all potentially valid courses.
$courses = get_courses('all', 'c.sortorder ASC', 'c.id');
$dataset = $this->calculate_indicators($sampleids, $samplesorigin, $indicators, $ranges);
+ if (empty($dataset)) {
+ return false;
+ }
+
// Now that we have the indicators in place we can add the time range indicators (and target if provided) to each of them.
$this->fill_dataset($dataset, $calculatedtarget);
});
};
+ /**
+ * Get the CSS class to apply for the given event type.
+ *
+ * @param {String} eventType The calendar event type
+ * @return {String}
+ */
+ var getEventTypeClassFromType = function(eventType) {
+ switch (eventType) {
+ case 'user':
+ return 'calendar_event_user';
+ case 'site':
+ return 'calendar_event_site';
+ case 'group':
+ return 'calendar_event_group';
+ case 'category':
+ return 'calendar_event_category';
+ case 'course':
+ return 'calendar_event_course';
+ default:
+ return 'calendar_event_course';
+ }
+ };
+
/**
* Render the event summary modal.
*
* @param {Number} eventId The calendar event id.
*/
var renderEventSummaryModal = function(eventId) {
+ var typeClass = '';
+
// Calendar repository promise.
CalendarRepository.getEventById(eventId).then(function(getEventResponse) {
if (!getEventResponse.event) {
throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);
}
var eventData = getEventResponse.event;
+ typeClass = getEventTypeClassFromType(eventData.eventtype);
return getEventType(eventData.eventtype).then(function(eventType) {
eventData.eventtype = eventType;
templateContext: {
canedit: eventData.canedit,
candelete: eventData.candelete,
+ headerclasses: typeClass,
isactionevent: eventData.isactionevent,
url: eventData.url
}
],
'defaulteventcontext' => [
'type' => PARAM_INT,
- 'default' => null,
+ 'default' => 0,
],
'filter_selector' => [
'type' => PARAM_RAW,
* @return string The html code for the course filter selector.
*/
protected function get_course_filter_selector(renderer_base $output) {
- $langstr = get_string('upcomingeventsfor', 'calendar');
+ $langstr = get_string('dayviewfor', 'calendar');
return $output->course_filter_selector($this->url, $langstr, $this->calendar->course->id);
}
],
'defaulteventcontext' => [
'type' => PARAM_INT,
- 'default' => null,
+ 'default' => 0,
],
'filter_selector' => [
'type' => PARAM_RAW,
],
'defaulteventcontext' => [
'type' => PARAM_INT,
- 'default' => null,
+ 'default' => 0,
],
];
}
$time = $calendartype->timestamp_to_date_array($calendar->time);
$current = $calendar->time;
+ $prevmonthyear = $calendartype->get_prev_month($time['year'], $time['mon']);
$prev = $calendartype->convert_to_timestamp(
- $time['year'],
- $time['mon'] - 1,
- $time['mday']
+ $prevmonthyear[1],
+ $prevmonthyear[0],
+ 1
);
+ $nextmonthyear = $calendartype->get_next_month($time['year'], $time['mon']);
$next = $calendartype->convert_to_timestamp(
- $time['year'],
- $time['mon'] + 1,
- $time['mday']
+ $nextmonthyear[1],
+ $nextmonthyear[0],
+ 1
);
$content = '';
}} data-action-event="{{isactionevent}}"{{!
}} data-edit-url="{{editurl}}"{{!
}}>
- <h4>{{#str}} when, core_calendar {{/str}}</h4>
- {{#userdate}} {{timestart}}, {{#str}} strftimerecentfull {{/str}} {{/userdate}}
- <br>
- {{#description}}
- <h4>{{#str}} description {{/str}}</h4>
- {{{description}}}
- {{/description}}
- <h4>{{#str}} eventtype, core_calendar {{/str}}</h4>
- {{eventtype}}
- {{#iscategoryevent}}
- <div>{{{category.nestedname}}}</div>
- {{/iscategoryevent}}
- {{#iscourseevent}}
- <div><a href="{{url}}">{{course.fullname}}</a></div>
- {{/iscourseevent}}
- {{> core_calendar/event_subscription}}
- {{#groupname}}
- <div><a href="{{url}}">{{{course.fullname}}}</a></div>
- <div>{{{groupname}}}</div>
- {{/groupname}}
+ <div class="container-fluid">
+ <div class="row">
+ <div class="col-xs-1">{{#pix}} i/calendareventtime, core, {{#str}} when, core_calendar {{/str}} {{/pix}}</div>
+ <div class="col-xs-11">{{#userdate}} {{timestart}}, {{#str}} strftimerecentfull {{/str}} {{/userdate}}</div>
+ </div>
+ <div class="row m-t-1">
+ <div class="col-xs-1">{{#pix}} i/calendar, core, {{#str}} eventtype, core_calendar {{/str}} {{/pix}}</div>
+ <div class="col-xs-11">{{eventtype}}</div>
+ </div>
+ {{#description}}
+ <div class="row m-t-1">
+ <div class="col-xs-1">{{#pix}} i/calendareventdescription, core, {{#str}} description {{/str}} {{/pix}}</div>
+ <div class="col-xs-11">{{{.}}}</div>
+ </div>
+ {{/description}}
+ {{#iscategoryevent}}
+ <div class="row m-t-1">
+ <div class="col-xs-1">{{#pix}} i/categoryevent, core, {{#str}} category {{/str}} {{/pix}}</div>
+ <div class="col-xs-11">{{{category.nestedname}}}</div>
+ </div>
+ {{/iscategoryevent}}
+ {{#iscourseevent}}
+ <div class="row m-t-1">
+ <div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
+ <div class="col-xs-11"><a href="{{url}}">{{{course.fullname}}}</a></div>
+ </div>
+ {{/iscourseevent}}
+ {{#groupname}}
+ <div class="row m-t-1">
+ <div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
+ <div class="col-xs-11"><a href="{{url}}">{{{course.fullname}}}</a></div>
+ </div>
+ <div class="row m-t-1">
+ <div class="col-xs-1">{{#pix}} i/groupevent, core, {{#str}} group {{/str}} {{/pix}}</div>
+ <div class="col-xs-11">{{{groupname}}}</div>
+ </div>
+ {{/groupname}}
+ {{#subscription}}
+ {{#displayeventsource}}
+ <div class="row m-t-1">
+ <div class="col-xs-1">{{#pix}} i/rss, core, {{#str}} eventsource, core_calendar {{/str}} {{/pix}}</div>
+ <div class="col-xs-11">
+ {{#url}}
+ <a href="{{url}}">{{#str}}subscriptionsource, core_calendar, {{name}}{{/str}}</a>
+ {{/url}}
+ {{^url}}
+ <p>{{#str}}subscriptionsource, core_calendar, {{name}}{{/str}}</p>
+ {{/url}}
+ </div>
+ </div>
+ {{/displayeventsource}}
+ {{/subscription}}
+ </div>
</div>
</span>
{{> core_calendar/event_icon}}
- {{name}}
+ <span class="eventname">{{name}}</span>
</a>
</li>
{{/underway}}
if (!empty($cm->showdescription) or $cm->modname == 'label') {
// We want to use the external format. However from reading get_formatted_content(), $cm->content format is always FORMAT_HTML.
+ $options = array('noclean' => true);
list($module['description'], $descriptionformat) = external_format_text($cm->content,
- FORMAT_HTML, $modcontext->id, $cm->modname, 'intro', $cm->id);
+ FORMAT_HTML, $modcontext->id, $cm->modname, 'intro', $cm->id, $options);
}
//url of the module
* Returns the given method's controller in the gradable area
*
* @param string $method the method name, eg 'rubric' (must be available)
- * @return grading_controller
+ * @return gradingform_controller
*/
public function get_controller($method) {
global $CFG, $DB;
/**
* Returns the controller for the active method if it is available
*
- * @return null|grading_controller
+ * @return null|gradingform_controller
*/
public function get_active_controller() {
if ($gradingmethod = $this->get_active_method()) {
--- /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/>.
+
+/**
+ * Group details page.
+ *
+ * @package core_group
+ * @copyright 2017 Adrian Greeve <adrian@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_group\output;
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use renderer_base;
+use stdClass;
+use templatable;
+use context_course;
+use moodle_url;
+
+/**
+ * Group details page class.
+ *
+ * @package core_group
+ * @copyright 2017 Adrian Greeve <adrian@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class group_details implements renderable, templatable {
+
+ /** @var stdClass $group An object with the group information. */
+ protected $group;
+
+ /**
+ * group_details constructor.
+ *
+ * @param int $groupid Group ID to show details of.
+ */
+ public function __construct($groupid) {
+ $this->group = groups_get_group($groupid, '*', MUST_EXIST);
+ }
+
+ /**
+ * Export the data.
+ *
+ * @param renderer_base $output
+ * @return stdClass
+ */
+ public function export_for_template(renderer_base $output) {
+
+ if (!empty($this->group->description) || (!empty($this->group->picture) && empty($this->group->hidepicture))) {
+ $context = context_course::instance($this->group->courseid);
+ $description = file_rewrite_pluginfile_urls($this->group->description,
+ 'pluginfile.php',
+ $context->id,
+ 'group',
+ 'description',
+ $this->group->id);
+
+ $descriptionformat = $this->group->descriptionformat ?? FORMAT_MOODLE;
+ $options = [
+ 'overflowdiv' => true,
+ 'context' => $context
+ ];
+
+ $data = new stdClass();
+ $data->name = format_string($this->group->name, true, ['context' => $context]);
+ $data->pictureurl = get_group_picture_url($this->group, $this->group->courseid, true);
+ $data->description = format_text($description, $descriptionformat, $options);
+
+ if (has_capability('moodle/course:managegroups', $context)) {
+ $url = new moodle_url('/group/group.php', ['id' => $this->group->id, 'courseid' => $this->group->courseid]);
+ $data->editurl = $url->out(false);
+ }
+
+ return $data;
+ } else {
+ return;
+ }
+ }
+}
$data = $page->export_for_template($this);
return parent::render_from_template('core_group/index', $data);
}
+
+ /**
+ * Defer to template.
+ *
+ * @param group_details $page Group details page object.
+ * @return string HTML to render the group details.
+ */
+ public function group_details(group_details $page) {
+ $data = $page->export_for_template($this);
+ return parent::render_from_template('core_group/group_details', $data);
+ }
}
// Store the rows we want to display in the group info.
$groupinforow = array();
-// Check if there is a picture to display.
-if (!empty($group->picture)) {
- $picturecell = new html_table_cell();
- $picturecell->attributes['class'] = 'left side picture';
- $picturecell->text = print_group_picture($group, $course->id, true, true, false);
- $groupinforow[] = $picturecell;
-}
-
// Check if there is a description to display.
-$group->description = file_rewrite_pluginfile_urls($group->description, 'pluginfile.php', $context->id, 'group', 'description', $group->id);
if (!empty($group->description)) {
- if (!isset($group->descriptionformat)) {
- $group->descriptionformat = FORMAT_MOODLE;
- }
-
- $options = new stdClass;
- $options->overflowdiv = true;
-
- $contentcell = new html_table_cell();
- $contentcell->attributes['class'] = 'content';
- $contentcell->text = format_text($group->description, $group->descriptionformat, $options);
- $groupinforow[] = $contentcell;
-}
-
-// Check if we have something to show.
-if (!empty($groupinforow)) {
- $groupinfotable = new html_table();
- $groupinfotable->attributes['class'] = 'groupinfobox';
- $groupinfotable->data[] = new html_table_row($groupinforow);
- echo html_writer::table($groupinfotable);
+ $grouprenderer = $PAGE->get_renderer('core_group');
+ $groupdetailpage = new \core_group\output\group_details($groupid);
+ echo $grouprenderer->group_details($groupdetailpage);
}
/// Print the editing form
--- /dev/null
+{{!
+ 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/>.
+}}
+{{!
+ @template core_group/group_details
+
+ Template for the Groups page.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * name string Group Name
+ * pictureurl string Group image url
+ * description string Group description
+ * edit string edit link to edit the group
+
+ Example context (json):
+ {
+ "name": "Group Name",
+ "pictureurl": "https://raw.githubusercontent.com/moodle/moodle/master/pix/g/f1.png",
+ "description": "This is the description for Group Name",
+ "editurl": "http://www.moodle.org"
+ }
+}}
+{{#name}}
+<div class="groupinfobox container-fluid p-x-1 p-y-1">
+ {{#pictureurl}}
+ <div class="group-image"><img class="grouppicture" src="{{{pictureurl}}}" alt="{{{name}}}" title="{{{name}}}"/></div>
+ {{/pictureurl}}
+ {{#editurl}}
+ <div class="group-edit"><a href="{{editurl}}">{{#pix}}t/edit, core, {{#str}}editgroupprofile{{/str}}{{/pix}}</a></div>
+ {{/editurl}}
+ <h3 class="">{{{name}}}</h3>
+ <div class="group-description">{{{description}}}</div>
+</div>
+{{/name}}
--- /dev/null
+@core @core_group
+Feature: The description of a group can be viewed by students and teachers
+ In order to view the description of a group
+ As a teacher
+ I need to create groups and add descriptions to them.
+
+ Background:
+ Given the following "courses" exist:
+ | fullname | shortname | format |
+ | Course 1 | C1 | topics |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ | student1 | Student | 1 | student1@example.com |
+ | student2 | Student | 2 | student2@example.com |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+ | student2 | C1 | student |
+
+ @javascript
+ Scenario: A student can see the group description when visible groups are set. Teachers can see group details.
+ Given I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I navigate to "Edit settings" in current page administration
+ And I set the following fields to these values:
+ | Group mode | Visible groups |
+ And I press "Save and display"
+ And I navigate to "Users > Groups" in current page administration
+ And I press "Create group"
+ And I set the following fields to these values:
+ | Group name | Group A |
+ | Group description | Description for Group A |
+ And I press "Save changes"
+ And I press "Create group"
+ And I set the following fields to these values:
+ | Group name | Group B |
+ And I press "Save changes"
+ And I add "Student 1 (student1@example.com)" user to "Group A" group members
+ And I add "Student 2 (student2@example.com)" user to "Group B" group members
+ And I am on "Course 1" course homepage
+ And I navigate to course participants
+ And I open the autocomplete suggestions list
+ And I click on "Group: Group A" item in the autocomplete list
+ And I should see "Description for Group A"
+ And ".groupinfobox" "css_element" should exist
+ And I should see "Description for Group A"
+ And I click on "Group: Group A" "autocomplete_selection"
+ And I open the autocomplete suggestions list
+ And I click on "Group: Group B" item in the autocomplete list
+ And ".groupinfobox" "css_element" should not exist
+ And I log out
+ When I log in as "student1"
+ And I am on "Course 1" course homepage
+ And I navigate to course participants
+ Then I should see "Description for Group A"
+ And I log out
+ And I log in as "student2"
+ And I am on "Course 1" course homepage
+ And I navigate to course participants
+ And ".groupinfobox" "css_element" should not exist
+
+ @javascript
+ Scenario: A student can not see the group description when separate groups are set. Teachers can see group details.
+ Given I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I navigate to "Edit settings" in current page administration
+ And I set the following fields to these values:
+ | Group mode | Separate groups |
+ And I press "Save and display"
+ And I navigate to "Users > Groups" in current page administration
+ And I press "Create group"
+ And I set the following fields to these values:
+ | Group name | Group A |
+ | Group description | Description for Group A |
+ And I press "Save changes"
+ And I press "Create group"
+ And I set the following fields to these values:
+ | Group name | Group B |
+ And I press "Save changes"
+ And I add "Student 1 (student1@example.com)" user to "Group A" group members
+ And I add "Student 2 (student2@example.com)" user to "Group B" group members
+ And I am on "Course 1" course homepage
+ And I navigate to course participants
+ And I open the autocomplete suggestions list
+ And I click on "Group: Group A" item in the autocomplete list
+ And I should see "Description for Group A"
+ And ".groupinfobox" "css_element" should exist
+ And I click on "Group: Group A" "autocomplete_selection"
+ And I open the autocomplete suggestions list
+ And I click on "Group: Group B" item in the autocomplete list
+ And ".groupinfobox" "css_element" should not exist
+ And I log out
+ When I log in as "student1"
+ And I am on "Course 1" course homepage
+ And I navigate to course participants
+ Then I should not see "Description for Group A"
+ And ".groupinfobox" "css_element" should not exist
+ And I log out
+ And I log in as "student2"
+ And I am on "Course 1" course homepage
+ And I navigate to course participants
+ And ".groupinfobox" "css_element" should not exist
\ No newline at end of file
$string['installsessionerror'] = 'Can not initialise PHP session, please verify that your browser accepts cookies.';
$string['intlrecommended'] = 'Intl extension is used to improve internationalization support, such as locale aware sorting.';
$string['intlrequired'] = 'Intl extension is required to improve internationalization support, such as locale aware sorting and international domain names.';
+$string['invalidforgottenpasswordurl'] = 'The forgotten password URL is not a valid URL.';
$string['invalidsection'] = 'Invalid section.';
$string['invaliduserchangeme'] = 'Username "changeme" is reserved -- you cannot create an account with it.';
$string['ipblocked'] = 'This site is not available currently.';
$string['recaptchaprivatekey'] = 'ReCAPTCHA secret key';
$string['recaptchapublickey'] = 'ReCAPTCHA site key';
$string['register'] = 'Register your site';
-$string['registermoodlenet'] = 'We\'d love to stay in touch for important things for your Moodle site!
-
-By registering,
-
-* You are contributing to our collective knowledge about the users of Moodle which helps us improve Moodle and all our community services
-* You’ll be one of the first to find out about important notifications such as security alerts and new Moodle releases.
-* You can access and activate mobile push notifications from your Moodle site through our free [Moodle Mobile app](https://download.moodle.org/mobile/)
-* Optionally, your site can be included as a proud member and supporter of the Moodle community on the [list of registered sites](https://moodle.net/stats).
-';
+$string['registermoodlenet'] = '<p>We\'d love to stay in touch for important things for your Moodle site!</p><p>By registering,</p><ul><li>You are contributing to our collective knowledge about the users of Moodle which helps us improve Moodle and all our community services.</li><li>You’ll be one of the first to find out about important notifications such as security alerts and new Moodle releases.</li><li>You can access and activate mobile push notifications from your Moodle site through our free <a href="https://download.moodle.org/mobile/">Moodle Mobile app</a>.</li><li>Optionally, your site can be included as a proud member and supporter of the Moodle community on the <a href="https://moodle.net/stats">list of registered sites</a>.</li></ul>';
$string['registermoodleorg'] = 'When you register your site';
$string['registermoodleorgli1'] = 'You are added to a low-volume mailing list for important notifications such as security alerts and new releases of Moodle.';
$string['registermoodleorgli2'] = 'Statistics about your site will be added to the {$a} of the worldwide Moodle community.';
$string['eventsall'] = 'All events';
$string['eventsdeleted'] = 'Events deleted';
$string['eventsimported'] = 'Events imported: {$a}';
+$string['eventsource'] = 'Event source';
$string['eventsupdated'] = 'Events updated: {$a}';
$string['eventsfor'] = '{$a} events';
$string['eventskey'] = 'Events key';
};
/**
- * Set the modal footer element.
+ * Set the modal footer element. The footer element is made visible, if it
+ * isn't already.
*
* This method is overloaded to take either a string
* value for the body or a jQuery promise that is resolved with HTML and Javascript
* @param {(string|object)} value The footer string or jQuery promise
*/
Modal.prototype.setFooter = function(value) {
+ // Make sure the footer is visible.
+ this.showFooter();
+
var footer = this.getFooter();
if (typeof value === 'string') {
}
};
+ /**
+ * Check if the footer has any content in it.
+ *
+ * @method hasFooterContent
+ * @return {bool}
+ */
+ Modal.prototype.hasFooterContent = function() {
+ return this.getFooter().children().length ? true : false;
+ };
+
+ /**
+ * Hide the footer element.
+ *
+ * @method hideFooter
+ */
+ Modal.prototype.hideFooter = function() {
+ this.getFooter().addClass('hidden');
+ };
+
+ /**
+ * Show the footer element.
+ *
+ * @method showFooter
+ */
+ Modal.prototype.showFooter = function() {
+ this.getFooter().removeClass('hidden');
+ };
+
/**
* Mark the modal as a large modal.
*
return;
}
+ if (this.hasFooterContent()) {
+ this.showFooter();
+ } else {
+ this.hideFooter();
+ }
+
if (!this.isAttached) {
this.attachToDOM();
}
'text' => 'text',
'xpath_element' => 'xpath_element',
'form_row' => 'form_row',
+ 'autocomplete_selection' => 'autocomplete_selection',
);
/**
XPATH
, 'message_area_action' => <<<XPATH
.//div[@data-region='messaging-area']/descendant::*[@data-action = %locator%]
+XPATH
+ , 'autocomplete_selection' => <<<XPATH
+.//div[contains(concat(' ', normalize-space(@class), ' '), concat(' ', 'form-autocomplete-selection', ' '))]/span[@role='listitem'][contains(normalize-space(.), %locator%)]
XPATH
);
$mform->addElement('hidden', 'returnurl');
$mform->setType('returnurl', PARAM_LOCALURL);
+ // Prepare and set data.
+ $siteinfo['emailalertnewemail'] = !empty($siteinfo['emailalert']) && !empty($siteinfo['emailalertemail']);
+ if (empty($siteinfo['emailalertnewemail'])) {
+ $siteinfo['emailalertemail'] = '';
+ }
+ $siteinfo['commnewsnewemail'] = !empty($siteinfo['commnews']) && !empty($siteinfo['commnewsemail']);
+ if (empty($siteinfo['commnewsnewemail'])) {
+ $siteinfo['commnewsemail'] = '';
+ }
$this->set_data($siteinfo);
}
}
- /**
- * Load in existing data as form defaults
- *
- * @param stdClass|array $defaultvalues object or array of default values
- */
- public function set_data($defaultvalues) {
- if (is_object($defaultvalues)) {
- $defaultvalues = (array)$defaultvalues;
- }
- $defaultvalues['emailalertnewemail'] = !empty($defaultvalues['emailalert']) && !empty($defaultvalues['emailalertemail']);
- if (empty($defaultvalues['emailalertnewemail'])) {
- $defaultvalues['emailalertemail'] = '';
- }
- $defaultvalues['commnewsnewemail'] = !empty($defaultvalues['commnews']) && !empty($defaultvalues['commnewsemail']);
- if (empty($defaultvalues['commnewsnewemail'])) {
- $defaultvalues['commnewsemail'] = '';
- }
- parent::set_data($defaultvalues);
- }
-
/**
* Validation of the form data
*
'core:i/badge' => 'fa-shield',
'core:i/calc' => 'fa-calculator',
'core:i/calendar' => 'fa-calendar',
+ 'core:i/calendareventdescription' => 'fa-align-left',
+ 'core:i/calendareventtime' => 'fa-clock-o',
'core:i/caution' => 'fa-exclamation text-warning',
'core:i/checked' => 'fa-check',
'core:i/checkpermissions' => 'fa-unlock-alt',
}
} // match by exact string
else {
- if ($node->getAttribute($name) != $value) {
+ if ($node->getAttribute($name) !== (string) $value) {
$invalid = true;
}
}
data-region="modal"
aria-labelledby="{{uniqid}}-modal-title"
role="document">
- <div class="modal-header" data-region="header">
+ <div class="modal-header {{$headerclasses}}{{headerclasses}}{{/headerclasses}}" data-region="header">
<button type="button" class="close" data-action="hide" title="{{#str}} closebuttontitle {{/str}}"></button>
{{$header}}
- <h3 id="{{uniqid}}-modal-title" data-region="title" tabindex="0">
+ <h3 id="{{uniqid}}-modal-title" class="modal-title" data-region="title" tabindex="0">
{{$title}}{{title}}{{/title}}
</h3>
{{/header}}
}
}
+ $pictureurl = get_group_picture_url($group, $courseid, $large);
+
+ // If there is no picture, do nothing.
+ if (!isset($pictureurl)) {
+ return;
+ }
+
+ $context = context_course::instance($courseid);
+
+ $groupname = s($group->name);
+ $pictureimage = html_writer::img($pictureurl, $groupname, ['title' => $groupname]);
+
+ $output = '';
+ if ($link or has_capability('moodle/site:accessallgroups', $context)) {
+ $linkurl = new moodle_url('/user/index.php', ['id' => $courseid, 'group' => $group->id]);
+ $output .= html_writer::link($linkurl, $pictureimage);
+ } else {
+ $output .= $pictureimage;
+ }
+
+ if ($return) {
+ return $output;
+ } else {
+ echo $output;
+ }
+}
+
+/**
+ * Return the url to the group picture.
+ *
+ * @param stdClass $group A group object.
+ * @param int $courseid The course ID for the group.
+ * @param bool $large A large or small group picture? Default is small.
+ * @return moodle_url Returns the url for the group picture.
+ */
+function get_group_picture_url($group, $courseid, $large = false) {
+ global $CFG;
+
$context = context_course::instance($courseid);
// If there is no picture, do nothing.
if (!$group->picture) {
- return '';
+ return;
}
// If picture is hidden, only show to those with course:managegroups.
if ($group->hidepicture and !has_capability('moodle/course:managegroups', $context)) {
- return '';
+ return;
}
- if ($link or has_capability('moodle/site:accessallgroups', $context)) {
- $output = '<a href="'. $CFG->wwwroot .'/user/index.php?id='. $courseid .'&group='. $group->id .'">';
- } else {
- $output = '';
- }
if ($large) {
$file = 'f1';
} else {
$grouppictureurl = moodle_url::make_pluginfile_url($context->id, 'group', 'icon', $group->id, '/', $file);
$grouppictureurl->param('rev', $group->picture);
- $output .= '<img class="grouppicture" src="'.$grouppictureurl.'"'.
- ' alt="'.s(get_string('group').' '.$group->name).'" title="'.s($group->name).'"/>';
-
- if ($link or has_capability('moodle/site:accessallgroups', $context)) {
- $output .= '</a>';
- }
-
- if ($return) {
- return $output;
- } else {
- echo $output;
- }
+ return $grouppictureurl;
}
{{$headertext}}{{#str}} messages, message {{/str}}{{/headertext}}
{{$headeractions}}
<div class="newmessage-link">
- {{$anchor}}
- <a href="{{{urls.writeamessage}}}">{{#str}} newmessage, message {{/str}}
- </a>
- {{/anchor}}
+ <a href="{{{urls.writeamessage}}}">{{#str}} newmessage, message {{/str}}
+ </a>
</div>
- {{< core/hover_tooltip }}
- {{$anchor}}
- <a class="mark-all-read-button"
- href="#"
- role="button"
- title="{{#str}} markallread {{/str}}"
- data-action="mark-all-read">
- <span class="normal-icon">{{#pix}} t/markasread, core, {{#str}} markallread {{/str}} {{/pix}}</span>
- {{> core/loading }}
- </a>
- {{/anchor}}
- {{$tooltip}}{{#str}} markallread {{/str}}{{/tooltip}}
- {{/ core/hover_tooltip }}
- {{< core/hover_tooltip }}
- {{$anchor}}
- <a href="{{{urls.preferences}}}"
- title="{{#str}} messagepreferences, message {{/str}}">
- {{#pix}} i/settings, core, {{#str}} messagepreferences, message {{/str}} {{/pix}}
- </a>
- {{/anchor}}
- {{$tooltip}}{{#str}} messagepreferences, message {{/str}}{{/tooltip}}
- {{/ core/hover_tooltip }}
+ <a class="mark-all-read-button"
+ href="#"
+ role="button"
+ title="{{#str}} markallread {{/str}}"
+ data-action="mark-all-read">
+ <span class="normal-icon">{{#pix}} t/markasread, core, {{#str}} markallread {{/str}} {{/pix}}</span>
+ {{> core/loading }}
+ </a>
+ <a href="{{{urls.preferences}}}"
+ title="{{#str}} messagepreferences, message {{/str}}">
+ {{#pix}} i/settings, core, {{#str}} messagepreferences, message {{/str}} {{/pix}}
+ </a>
{{/headeractions}}
{{$content}}
{{$headertext}}{{#str}} notifications, message {{/str}}{{/headertext}}
{{$headeractions}}
- {{< core/hover_tooltip }}
- {{$anchor}}
- <a class="mark-all-read-button"
- href="#"
- title="{{#str}} markallread {{/str}}"
- data-action="mark-all-read"
- role="button">
- <span class="normal-icon">{{#pix}} t/markasread, core, {{#str}} markallread {{/str}} {{/pix}}</span>
- {{> core/loading }}
- </a>
- {{/anchor}}
- {{$tooltip}}{{#str}} markallread {{/str}}{{/tooltip}}
- {{/ core/hover_tooltip }}
- {{< core/hover_tooltip }}
- {{$anchor}}
- <a href="{{{urls.preferences}}}"
- title="{{#str}} notificationpreferences, message {{/str}}">
- {{#pix}} i/settings, core, {{#str}} notificationpreferences, message {{/str}} {{/pix}}
- </a>
- {{/anchor}}
- {{$tooltip}}{{#str}} notificationpreferences, message {{/str}}{{/tooltip}}
- {{/ core/hover_tooltip }}
+ <a class="mark-all-read-button"
+ href="#"
+ title="{{#str}} markallread {{/str}}"
+ data-action="mark-all-read"
+ role="button">
+ <span class="normal-icon">{{#pix}} t/markasread, core, {{#str}} markallread {{/str}} {{/pix}}</span>
+ {{> core/loading }}
+ </a>
+ <a href="{{{urls.preferences}}}"
+ title="{{#str}} notificationpreferences, message {{/str}}">
+ {{#pix}} i/settings, core, {{#str}} notificationpreferences, message {{/str}} {{/pix}}
+ </a>
{{/headeractions}}
{{$content}}
position: inherit;
}
/** End of base fixes **/
+
+/** Fix to YUI tree (which is a table) when displayed within grading table. **/
+.path-mod-assign table.generaltable table td.ygtvcell {
+ border: 0;
+ padding: 0;
+}
+/** End of YUI tree fix **/
'warnings' => $warnings
);
- $groupmode = groups_get_activity_groupmode($cm);
if (!empty($params['groupid'])) {
$groupid = $params['groupid'];
// Determine is the group is visible to user.
}
} else {
// Check to see if groups are being used here.
+ $groupmode = groups_get_activity_groupmode($cm);
if ($groupmode) {
$groupid = groups_get_activity_group($cm);
- // Determine is the group is visible to user (this is particullary for the group 0 -> all groups).
- if (!groups_group_visible($groupid, $course, $cm)) {
- throw new moodle_exception('notingroup');
- }
} else {
$groupid = 0;
}
} else {
// Check to see if groups are being used here.
if ($groupmode = groups_get_activity_groupmode($cm)) {
+ // We don't need to validate a possible groupid = 0 since it would be handled by data_search_entries.
$groupid = groups_get_activity_group($cm);
- // Determine is the group is visible to user (this is particullary for the group 0 -> all groups).
- if (!groups_group_visible($groupid, $course, $cm)) {
- throw new moodle_exception('notingroup');
- }
} else {
$groupid = 0;
}
$canmanageentries = has_capability('mod/data:manageentries', $context);
data_require_time_available($database, $canmanageentries);
- if ($record->groupid !== 0) {
+ if ($record->groupid != 0) {
if (!groups_group_visible($record->groupid, $course, $cm)) {
throw new moodle_exception('notingroup');
}
} else {
// Check to see if groups are being used here.
if ($groupmode = groups_get_activity_groupmode($cm)) {
+ // We don't need to validate a possible groupid = 0 since it would be handled by data_search_entries.
$groupid = groups_get_activity_group($cm);
- // Determine is the group is visible to user (this is particullary for the group 0 -> all groups).
- if (!groups_group_visible($groupid, $course, $cm)) {
- throw new moodle_exception('notingroup');
- }
} else {
$groupid = 0;
}
// Check database is open in time.
data_require_time_available($database, null, $context);
- $groupmode = groups_get_activity_groupmode($cm);
- 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 {
+ // Determine default group.
+ if (empty($params['groupid'])) {
// Check to see if groups are being used here.
+ $groupmode = groups_get_activity_groupmode($cm);
if ($groupmode) {
$groupid = groups_get_activity_group($cm);
- // Determine is the group is visible to user (this is particullary for the group 0 -> all groups).
- if (!groups_group_visible($groupid, $course, $cm)) {
- throw new moodle_exception('notingroup');
- }
} else {
$groupid = 0;
}
}
+ // Group is validated inside the function.
if (!data_user_can_add_entry($database, $groupid, $groupmode, $context)) {
throw new moodle_exception('noaccess', 'data');
}
$this->setUser($this->student2);
$entry12 = $generator->create_entry($this->database, $fieldcontents, $this->group1->id);
$entry13 = $generator->create_entry($this->database, $fieldcontents, $this->group1->id);
+ // Entry not in group.
+ $entry14 = $generator->create_entry($this->database, $fieldcontents, 0);
$this->setUser($this->student3);
$entry21 = $generator->create_entry($this->database, $fieldcontents, $this->group2->id);
// Approve all except $entry13.
$DB->set_field('data_records', 'approved', 1, ['id' => $entry11]);
$DB->set_field('data_records', 'approved', 1, ['id' => $entry12]);
+ $DB->set_field('data_records', 'approved', 1, ['id' => $entry14]);
$DB->set_field('data_records', 'approved', 1, ['id' => $entry21]);
- return [$entry11, $entry12, $entry13, $entry21];
+ return [$entry11, $entry12, $entry13, $entry14, $entry21];
}
/**
*/
public function test_get_entries() {
global $DB;
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
// First of all, expect to see only my group entries (not other users in other groups ones).
+ // We may expect entries without group also.
$this->setUser($this->student1);
$result = mod_data_external::get_entries($this->database->id);
$result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
$this->assertCount(0, $result['warnings']);
- $this->assertCount(2, $result['entries']);
- $this->assertEquals(2, $result['totalcount']);
+ $this->assertCount(3, $result['entries']);
+ $this->assertEquals(3, $result['totalcount']);
$this->assertEquals($entry11, $result['entries'][0]['id']);
$this->assertEquals($this->student1->id, $result['entries'][0]['userid']);
$this->assertEquals($this->group1->id, $result['entries'][0]['groupid']);
$this->assertEquals($this->student2->id, $result['entries'][1]['userid']);
$this->assertEquals($this->group1->id, $result['entries'][1]['groupid']);
$this->assertEquals($this->database->id, $result['entries'][1]['dataid']);
+ $this->assertEquals($entry14, $result['entries'][2]['id']);
+ $this->assertEquals($this->student2->id, $result['entries'][2]['userid']);
+ $this->assertEquals(0, $result['entries'][2]['groupid']);
+ $this->assertEquals($this->database->id, $result['entries'][2]['dataid']);
// Other user in same group.
$this->setUser($this->student2);
$result = mod_data_external::get_entries($this->database->id);
$result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
$this->assertCount(0, $result['warnings']);
- $this->assertCount(3, $result['entries']); // I can see my entry not approved yet.
- $this->assertEquals(3, $result['totalcount']);
+ $this->assertCount(4, $result['entries']); // I can see my entry not approved yet.
+ $this->assertEquals(4, $result['totalcount']);
- // Now try with the user in the second group that must see only one entry.
+ // Now try with the user in the second group that must see only two entries (his group entry and the one without group).
$this->setUser($this->student3);
$result = mod_data_external::get_entries($this->database->id);
$result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
$this->assertCount(0, $result['warnings']);
- $this->assertCount(1, $result['entries']);
- $this->assertEquals(1, $result['totalcount']);
- $this->assertEquals($entry21, $result['entries'][0]['id']);
- $this->assertEquals($this->student3->id, $result['entries'][0]['userid']);
- $this->assertEquals($this->group2->id, $result['entries'][0]['groupid']);
+ $this->assertCount(2, $result['entries']);
+ $this->assertEquals(2, $result['totalcount']);
+ $this->assertEquals($entry14, $result['entries'][0]['id']);
+ $this->assertEquals($this->student2->id, $result['entries'][0]['userid']);
+ $this->assertEquals(0, $result['entries'][0]['groupid']);
$this->assertEquals($this->database->id, $result['entries'][0]['dataid']);
+ $this->assertEquals($entry21, $result['entries'][1]['id']);
+ $this->assertEquals($this->student3->id, $result['entries'][1]['userid']);
+ $this->assertEquals($this->group2->id, $result['entries'][1]['groupid']);
+ $this->assertEquals($this->database->id, $result['entries'][1]['dataid']);
// Now, as teacher we should see all (we have permissions to view all groups).
$this->setUser($this->teacher);
$result = mod_data_external::get_entries($this->database->id);
$result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
$this->assertCount(0, $result['warnings']);
- $this->assertCount(4, $result['entries']); // I can see the not approved one.
- $this->assertEquals(4, $result['totalcount']);
+ $this->assertCount(5, $result['entries']); // I can see the not approved one.
+ $this->assertEquals(5, $result['totalcount']);
$entries = $DB->get_records('data_records', array('dataid' => $this->database->id), 'id');
- $this->assertCount(4, $entries);
+ $this->assertCount(5, $entries);
$count = 0;
foreach ($entries as $entry) {
$this->assertEquals($entry->id, $result['entries'][$count]['id']);
$result = mod_data_external::get_entries($this->database->id, $this->group1->id);
$result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
$this->assertCount(0, $result['warnings']);
- $this->assertCount(2, $result['entries']);
- $this->assertEquals(2, $result['totalcount']);
+ $this->assertCount(3, $result['entries']);
+ $this->assertEquals(3, $result['totalcount']);
// Test ordering (reverse).
$this->setUser($this->student1);
$result = mod_data_external::get_entries($this->database->id, $this->group1->id, false, null, 'DESC');
$result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
$this->assertCount(0, $result['warnings']);
- $this->assertCount(2, $result['entries']);
- $this->assertEquals(2, $result['totalcount']);
- $this->assertEquals($entry12, $result['entries'][0]['id']);
+ $this->assertCount(3, $result['entries']);
+ $this->assertEquals(3, $result['totalcount']);
+ $this->assertEquals($entry14, $result['entries'][0]['id']);
// Test pagination.
$this->setUser($this->student1);
$result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
$this->assertCount(0, $result['warnings']);
$this->assertCount(1, $result['entries']);
- $this->assertEquals(2, $result['totalcount']);
+ $this->assertEquals(3, $result['totalcount']);
$this->assertEquals($entry11, $result['entries'][0]['id']);
$result = mod_data_external::get_entries($this->database->id, $this->group1->id, false, null, null, 1, 1);
$result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
$this->assertCount(0, $result['warnings']);
$this->assertCount(1, $result['entries']);
- $this->assertEquals(2, $result['totalcount']);
+ $this->assertEquals(3, $result['totalcount']);
$this->assertEquals($entry12, $result['entries'][0]['id']);
// Now test the return contents.
$result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
$this->assertCount(0, $result['warnings']);
$this->assertCount(2, $result['entries']);
- $this->assertEquals(2, $result['totalcount']);
+ $this->assertEquals(3, $result['totalcount']);
$this->assertCount(9, $result['entries'][0]['contents']);
$this->assertCount(9, $result['entries'][1]['contents']);
// Search for some content.
global $DB;
$DB->set_field('course', 'groupmode', VISIBLEGROUPS, ['id' => $this->course->id]);
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
// Check I can see my approved group entries.
$this->setUser($this->student1);
*/
public function test_get_entry_separated_groups() {
global $DB;
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
// Check I can see my approved group entries.
$this->setUser($this->student1);
* Test get_entry from other group in separated groups.
*/
public function test_get_entry_other_group_separated_groups() {
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
// We should not be able to view other gropu entries (in separated groups).
$this->setUser($this->student1);
*/
public function test_get_fields() {
global $DB;
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
$this->setUser($this->student1);
$result = mod_data_external::get_fields($this->database->id);
*/
public function test_search_entries() {
global $DB;
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
$this->setUser($this->student1);
// Empty search, it should return all the visible entries.
$result = mod_data_external::search_entries($this->database->id, 0, false);
$result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
- $this->assertCount(2, $result['entries']);
- $this->assertEquals(2, $result['totalcount']);
+ $this->assertCount(3, $result['entries']);
+ $this->assertEquals(3, $result['totalcount']);
// Search for something that does not exists.
$result = mod_data_external::search_entries($this->database->id, 0, false, 'abc');
// Search by text matching all the entries.
$result = mod_data_external::search_entries($this->database->id, 0, false, 'text');
$result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
- $this->assertCount(2, $result['entries']);
- $this->assertEquals(2, $result['totalcount']);
- $this->assertEquals(2, $result['maxcount']);
+ $this->assertCount(3, $result['entries']);
+ $this->assertEquals(3, $result['totalcount']);
+ $this->assertEquals(3, $result['maxcount']);
// Now as the other student I should receive my not approved entry. Apply ordering here.
$this->setUser($this->student2);
$result = mod_data_external::search_entries($this->database->id, 0, false, 'text', [], DATA_APPROVED, 'ASC');
$result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
- $this->assertCount(3, $result['entries']);
- $this->assertEquals(3, $result['totalcount']);
- $this->assertEquals(3, $result['maxcount']);
+ $this->assertCount(4, $result['entries']);
+ $this->assertEquals(4, $result['totalcount']);
+ $this->assertEquals(4, $result['maxcount']);
// The not approved one should be the first.
$this->assertEquals($entry13, $result['entries'][0]['id']);
$this->setUser($this->student3);
$result = mod_data_external::search_entries($this->database->id, 0, false, 'text');
$result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
- $this->assertCount(1, $result['entries']);
- $this->assertEquals(1, $result['totalcount']);
- $this->assertEquals(1, $result['maxcount']);
- $this->assertEquals($this->student3->id, $result['entries'][0]['userid']);
+ $this->assertCount(2, $result['entries']);
+ $this->assertEquals(2, $result['totalcount']);
+ $this->assertEquals(2, $result['maxcount']);
+ $this->assertEquals($this->student2->id, $result['entries'][0]['userid']);
+ $this->assertEquals($this->student3->id, $result['entries'][1]['userid']);
// Same normal text search as teacher.
$this->setUser($this->teacher);
$result = mod_data_external::search_entries($this->database->id, 0, false, 'text');
$result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
- $this->assertCount(4, $result['entries']); // I can see all groups and non approved.
- $this->assertEquals(4, $result['totalcount']);
- $this->assertEquals(4, $result['maxcount']);
+ $this->assertCount(5, $result['entries']); // I can see all groups and non approved.
+ $this->assertEquals(5, $result['totalcount']);
+ $this->assertEquals(5, $result['maxcount']);
// Pagination.
$this->setUser($this->teacher);
$result = mod_data_external::search_entries($this->database->id, 0, false, 'text', [], DATA_TIMEADDED, 'ASC', 0, 2);
$result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
$this->assertCount(2, $result['entries']); // Only 2 per page.
- $this->assertEquals(4, $result['totalcount']);
- $this->assertEquals(4, $result['maxcount']);
+ $this->assertEquals(5, $result['totalcount']);
+ $this->assertEquals(5, $result['maxcount']);
// Now advanced search or not dinamic fields (user firstname for example).
$this->setUser($this->student1);
];
$result = mod_data_external::search_entries($this->database->id, 0, false, '', $advsearch);
$result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
- $this->assertCount(1, $result['entries']);
- $this->assertEquals(1, $result['totalcount']);
- $this->assertEquals(2, $result['maxcount']);
+ $this->assertCount(2, $result['entries']);
+ $this->assertEquals(2, $result['totalcount']);
+ $this->assertEquals(3, $result['maxcount']);
$this->assertEquals($this->student2->id, $result['entries'][0]['userid']); // I only found mine!
// Advanced search for fields.
];
$result = mod_data_external::search_entries($this->database->id, 0, false, '', $advsearch);
$result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
- $this->assertCount(2, $result['entries']); // Found two entries matching this.
- $this->assertEquals(2, $result['totalcount']);
- $this->assertEquals(2, $result['maxcount']);
+ $this->assertCount(3, $result['entries']); // Found two entries matching this.
+ $this->assertEquals(3, $result['totalcount']);
+ $this->assertEquals(3, $result['maxcount']);
// Combined search.
$field2 = $DB->get_record('data_fields', array('type' => 'number'));
];
$result = mod_data_external::search_entries($this->database->id, 0, false, '', $advsearch);
$result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
- $this->assertCount(1, $result['entries']); // Only one matching everything.
- $this->assertEquals(1, $result['totalcount']);
- $this->assertEquals(2, $result['maxcount']);
+ $this->assertCount(2, $result['entries']); // Only one matching everything.
+ $this->assertEquals(2, $result['totalcount']);
+ $this->assertEquals(3, $result['maxcount']);
// Combined search (no results).
$field2 = $DB->get_record('data_fields', array('type' => 'number'));
$result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
$this->assertCount(0, $result['entries']); // Only one matching everything.
$this->assertEquals(0, $result['totalcount']);
- $this->assertEquals(2, $result['maxcount']);
+ $this->assertEquals(3, $result['maxcount']);
}
/**
*/
public function test_approve_entry() {
global $DB;
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
$this->setUser($this->teacher);
$this->assertEquals(0, $DB->get_field('data_records', 'approved', array('id' => $entry13)));
*/
public function test_unapprove_entry() {
global $DB;
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
$this->setUser($this->teacher);
$this->assertEquals(1, $DB->get_field('data_records', 'approved', array('id' => $entry11)));
*/
public function test_approve_entry_missing_permissions() {
global $DB;
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
$this->setUser($this->student1);
$this->expectException('moodle_exception');
*/
public function test_delete_entry_as_teacher() {
global $DB;
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
$this->setUser($this->teacher);
$result = mod_data_external::delete_entry($entry11);
*/
public function test_delete_entry_as_student() {
global $DB;
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
$this->setUser($this->student1);
$result = mod_data_external::delete_entry($entry11);
*/
public function test_delete_entry_as_student_in_read_only_period() {
global $DB;
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
// Set a time period.
$this->database->timeviewfrom = time() - HOURSECS;
$this->database->timeviewto = time() + HOURSECS;
*/
public function test_delete_entry_missing_permissions() {
global $DB;
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
$this->setUser($this->student1);
$this->expectException('moodle_exception');
public function test_add_entry() {
global $DB;
// First create the record structure and add some entries.
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
$this->setUser($this->student1);
$newentrydata = [];
*/
public function test_add_entry_read_only_period() {
global $DB;
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
// Set a time period.
$this->database->timeviewfrom = time() - HOURSECS;
$this->database->timeviewto = time() + HOURSECS;
*/
public function test_add_entry_max_num_entries() {
global $DB;
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
// Set a time period.
$this->database->maxentries = 1;
$DB->update_record('data', $this->database);
public function test_update_entry() {
global $DB;
// First create the record structure and add some entries.
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
$this->setUser($this->student1);
$newentrydata = [];
* Test update_entry sending empty data.
*/
public function test_update_entry_empty_data() {
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
$this->setUser($this->student1);
$result = mod_data_external::update_entry($entry11, []);
*/
public function test_update_entry_read_only_period() {
global $DB;
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
// Set a time period.
$this->database->timeviewfrom = time() - HOURSECS;
$this->database->timeviewto = time() + HOURSECS;
*/
public function test_update_entry_other_user() {
// Try to update other user entry.
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
$this->setUser($this->student2);
$this->expectExceptionMessage(get_string('noaccess', 'data'));
$this->expectException('moodle_exception');
$DB->set_field('data', 'assessed', RATING_AGGREGATE_SUM, array('id' => $this->database->id));
$DB->set_field('data', 'scale', 100, array('id' => $this->database->id));
- list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+ list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries();
$user1 = self::getDataGenerator()->create_user();
$user2 = self::getDataGenerator()->create_user();
$response = new \mod_lti\local\ltiservice\response();
$isget = $response->get_request_method() == 'GET';
+$isdelete = $response->get_request_method() == 'DELETE';
if ($isget) {
$response->set_accept(isset($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : '');
foreach ($resources as $resource) {
if (($isget && !empty($accept) && (strpos($accept, '*/*') === false) &&
!in_array($accept, $resource->get_formats())) ||
- (!$isget && !in_array($response->get_content_type(), $resource->get_formats()))) {
+ ((!$isget && !$isdelete) && !in_array($response->get_content_type(), $resource->get_formats()))) {
continue;
}
$template = $resource->get_template();
$page->introformat, $context->id, 'mod_page', 'intro', null);
$page->introfiles = external_util::get_area_files($context->id, 'mod_page', 'intro', false, false);
+ $options = array('noclean' => true);
list($page->content, $page->contentformat) = external_format_text($page->content, $page->contentformat,
- $context->id, 'mod_page', 'content', $page->revision);
+ $context->id, 'mod_page', 'content', $page->revision, $options);
$page->contentfiles = external_util::get_area_files($context->id, 'mod_page', 'content');
$returnedpages[] = $page;
return array($currentgroup, $studentsjoins, $groupstudentsjoins, $groupstudentsjoins);
}
+ /**
+ * Outputs the things you commonly want at the top of a quiz report.
+ *
+ * Calls through to {@link print_header_and_tabs()} and then
+ * outputs the standard group selector, number of attempts summary,
+ * and messages to cover common cases when the report can't be shown.
+ *
+ * @param stdClass $cm the course_module information.
+ * @param stdClass $course the course settings.
+ * @param stdClass $quiz the quiz settings.
+ * @param mod_quiz_attempts_report_options $options the current report settings.
+ * @param int $currentgroup the current group.
+ * @param bool $hasquestions whether there are any questions in the quiz.
+ * @param bool $hasstudents whether there are any relevant students.
+ */
+ protected function print_standard_header_and_messages($cm, $course, $quiz,
+ $options, $currentgroup, $hasquestions, $hasstudents) {
+ global $OUTPUT;
+
+ $this->print_header_and_tabs($cm, $course, $quiz, $this->mode);
+
+ if (groups_get_activity_groupmode($cm)) {
+ // Groups are being used, so output the group selector if we are not downloading.
+ groups_print_activity_menu($cm, $options->get_url());
+ }
+
+ // Print information on the number of existing attempts.
+ if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) {
+ echo '<div class="quizattemptcounts">' . $strattemptnum . '</div>';
+ }
+
+ if (!$hasquestions) {
+ echo quiz_no_questions_message($quiz, $cm, $this->context);
+ } else if ($currentgroup == self::NO_GROUPS_ALLOWED) {
+ echo $OUTPUT->notification(get_string('notingroup'));
+ } else if (!$hasstudents) {
+ echo $OUTPUT->notification(get_string('nostudentsyet'));
+ } else if ($currentgroup && !$this->hasgroupstudents) {
+ echo $OUTPUT->notification(get_string('nostudentsingroup'));
+ }
+ }
+
/**
* Add all the user-related columns to the $columns and $headers arrays.
* @param table_sql $table the table being constructed.
$this->course = $course; // Hack to make this available in process_actions.
$this->process_actions($quiz, $cm, $currentgroup, $groupstudentsjoins, $allowedjoins, $options->get_url());
+ $hasquestions = quiz_has_questions($quiz->id);
+
// Start output.
if (!$table->is_downloading()) {
// Only print headers if not asked to download data.
- $this->print_header_and_tabs($cm, $course, $quiz, $this->mode);
- }
-
- if ($groupmode = groups_get_activity_groupmode($cm)) {
- // Groups are being used, so output the group selector if we are not downloading.
- if (!$table->is_downloading()) {
- groups_print_activity_menu($cm, $options->get_url());
- }
- }
-
- // Print information on the number of existing attempts.
- if (!$table->is_downloading()) {
- // Do not print notices when downloading.
- if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) {
- echo '<div class="quizattemptcounts">' . $strattemptnum . '</div>';
- }
- }
-
- $hasquestions = quiz_has_questions($quiz->id);
- if (!$table->is_downloading()) {
- if (!$hasquestions) {
- echo quiz_no_questions_message($quiz, $cm, $this->context);
- } else if (!$hasstudents) {
- echo $OUTPUT->notification(get_string('nostudentsyet'));
- } else if ($currentgroup && !$this->hasgroupstudents) {
- echo $OUTPUT->notification(get_string('nostudentsingroup'));
- }
+ $this->print_standard_header_and_messages($cm, $course, $quiz,
+ $options, $currentgroup, $hasquestions, $hasstudents);
// Print the display options.
$this->form->display();
-@mod @mod_quiz
+@mod @mod_quiz @quiz @quiz_overview
Feature: Basic use of the Grades report
In order to easily get an overview of quiz attempts
As a teacher
// Insert an extra field in attempt data and extra rows where necessary.
$newrawdata = array();
foreach ($this->rawdata as $attempt) {
+ if (!isset($this->questionusagesbyactivity[$attempt->usageid])) {
+ // This is a user without attempts.
+ $attempt->try = 0;
+ $attempt->lasttryforallparts = true;
+ $newrawdata[] = $attempt;
+ continue;
+ }
+
+ // We have an attempt, which may require several rows.
$maxtriesinanyslot = 1;
foreach ($this->questionusagesbyactivity[$attempt->usageid]->get_slots() as $slot) {
$tries = $this->get_no_of_tries($attempt, $slot);
* @return string What to put in the cell for this column, for this row data.
*/
public function col_email($tablerow) {
- if ($tablerow->try != 1) {
+ if ($tablerow->try > 1) {
return '';
} else {
return $tablerow->email;
* @return string What to put in the cell for this column, for this row data.
*/
public function col_sumgrades($tablerow) {
- if (!$tablerow->lasttryforallparts) {
+ if ($tablerow->try == 0) {
+ // We are showing a user without a quiz attempt.
+ return '-';
+ } else if (!$tablerow->lasttryforallparts) {
+ // There are more rows to come for this quiz attempt, so we will show this later.
return '';
} else {
+ // Last row for this attempt. Now is the time to show attempt-related data.
return parent::col_sumgrades($tablerow);
}
}
-
public function col_state($tablerow) {
- if (!$tablerow->lasttryforallparts) {
+ if ($tablerow->try == 0) {
+ // We are showing a user without a quiz attempt.
+ return '-';
+ } else if (!$tablerow->lasttryforallparts) {
+ // There are more rows to come for this quiz attempt, so we will show this later.
return '';
} else {
+ // Last row for this attempt. Now is the time to show attempt-related data.
return parent::col_state($tablerow);
}
}
$this->process_actions($quiz, $cm, $currentgroup, $groupstudentsjoins, $allowedjoins, $options->get_url());
+ $hasquestions = quiz_has_questions($quiz->id);
+
// Start output.
if (!$table->is_downloading()) {
// Only print headers if not asked to download data.
- $this->print_header_and_tabs($cm, $course, $quiz, $this->mode);
- }
-
- if ($groupmode = groups_get_activity_groupmode($cm)) {
- // Groups are being used, so output the group selector if we are not downloading.
- if (!$table->is_downloading()) {
- groups_print_activity_menu($cm, $options->get_url());
- }
- }
-
- // Print information on the number of existing attempts.
- if (!$table->is_downloading()) {
- // Do not print notices when downloading.
- if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) {
- echo '<div class="quizattemptcounts">' . $strattemptnum . '</div>';
- }
- }
-
- $hasquestions = quiz_has_questions($quiz->id);
- if (!$table->is_downloading()) {
- if (!$hasquestions) {
- echo quiz_no_questions_message($quiz, $cm, $this->context);
- } else if (!$hasstudents) {
- echo $OUTPUT->notification(get_string('nostudentsyet'));
- } else if ($currentgroup && !$this->hasgroupstudents) {
- echo $OUTPUT->notification(get_string('nostudentsingroup'));
- }
+ $this->print_standard_header_and_messages($cm, $course, $quiz,
+ $options, $currentgroup, $hasquestions, $hasstudents);
// Print the display options.
$this->form->display();
question_attempt::ALL_TRIES => get_string('alltries', 'question'))
);
$mform->setDefault('whichtries', question_attempt::LAST_TRY);
+ $mform->disabledIf('whichtries', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
}
}
}
--- /dev/null
+@mod @mod_quiz @quiz @quiz_reponses
+Feature: Basic use of the Responses report
+ In order to see how my students are progressing
+ As a teacher
+ I need to see all their quiz responses
+
+ Background: Using the Responses report
+ Given the following "users" exist:
+ | username | firstname | lastname |
+ | teacher | The | Teacher |
+ | student1 | Student | One |
+ | student2 | Student | Two |
+ And the following "courses" exist:
+ | fullname | shortname |
+ | Course 1 | C1 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher | C1 | editingteacher |
+ | student1 | C1 | student |
+ | student2 | C1 | student |
+ And the following "question categories" exist:
+ | contextlevel | reference | name |
+ | Course | C1 | Test questions |
+ And the following "activities" exist:
+ | activity | name | intro | course | idnumber | preferredbehaviour |
+ | quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | interactive |
+ And the following "questions" exist:
+ | questioncategory | qtype | name | template |
+ | Test questions | numerical | NQ | pi3tries |
+ And quiz "Quiz 1" contains the following questions:
+ | question | page | maxmark |
+ | NQ | 1 | 3.0 |
+
+ @javascript
+ Scenario: Report works when there are no attempts
+ When I log in as "teacher"
+ And I am on "Course 1" course homepage
+ And I follow "Quiz 1"
+ And I navigate to "Results > Responses" in current page administration
+ Then I should see "Attempts: 0"
+ And I should see "Nothing to display"
+ And I set the field "Attempts from" to "enrolled users who have not attempted the quiz"
+ And I press "Show report"
+ And "Student One" row "State" column of "responses" table should contain "-"
+
+ @javascript
+ Scenario: Report works when there are attempts
+ # Add an attempt
+ Given I log in as "student1"
+ And I am on "Course 1" course homepage
+ And I follow "Quiz 1"
+ And I press "Attempt quiz now"
+ And I set the field "Answer" to "1.0"
+ And I press "Check"
+ And I press "Try again"
+ And I set the field "Answer" to "3.0"
+ And I press "Check"
+ And I press "Try again"
+ And I set the field "Answer" to "3.14"
+ And I press "Check"
+ And I press "Finish attempt ..."
+ And I press "Submit all and finish"
+ And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
+ And I log out
+
+ When I log in as "teacher"
+ And I am on "Course 1" course homepage
+ And I follow "Quiz 1"
+ And I navigate to "Results > Responses" in current page administration
+ Then I should see "Attempts: 1"
+ And I should see "Student One"
+ And I should not see "Student Two"
+ And I set the field "Attempts from" to "enrolled users who have, or have not, attempted the quiz"
+ And I set the field "Which tries" to "All tries"
+ And I press "Show report"
+ And "Student OneReview attempt" row "Response 1Sort by Response 1 Ascending" column of "responses" table should contain "1.0"
+ And "Student OneReview attempt" row "State" column of "responses" table should contain ""
+ And "Finished" row "Grade/100.00Sort by Grade/100.00 Ascending" column of "responses" table should contain "33.33"
+ And "Finished" row "Response 1Sort by Response 1 Ascending" column of "responses" table should contain "3.14"
+ And "Student Two" row "State" column of "responses" table should contain "-"
+ And "Student Two" row "Response 1Sort by Response 1 Ascending" column of "responses" table should contain "-"
+
+ @javascript
+ Scenario: Report does not allow strange combinations of options
+ When I log in as "teacher"
+ And I am on "Course 1" course homepage
+ And I follow "Quiz 1"
+ And I navigate to "Results > Responses" in current page administration
+ And the "Which tries" "select" should be enabled
+ And I set the field "Attempts from" to "enrolled users who have not attempted the quiz"
+ Then the "Which tries" "select" should be disabled
$feedbackform = $workshop->get_feedbackreviewer_form(null, $assessment, $options);
$errors = $feedbackform->validation((array) $data, array());
+ // Extra checks for the new grade and weight.
+ $possibleweights = workshop::available_assessment_weights_list();
+ if ($data->weight < 0 || $data->weight > max(array_keys($possibleweights))) {
+ $errors['weight'] = 'The new weight must be higher or equal to 0 and cannot be higher than the maximum weight for
+ assessment.';
+ }
+ if (is_numeric($data->gradinggradeover) &&
+ ($data->gradinggradeover < 0 || $data->gradinggradeover > $workshop->gradinggrade)) {
+ $errors['gradinggradeover'] = 'The new grade must be higher or equal to 0 and cannot be higher than the maximum grade
+ for assessment.';
+ }
+
// We can get several errors, return them in warnings.
if (!empty($errors)) {
$status = false;
$submissionid = $workshopgenerator->create_submission($this->workshop->id, $this->student->id);
$assessmentid = $workshopgenerator->create_assessment($submissionid, $this->anotherstudentg1->id, array(
'weight' => 3,
- 'grade' => 95,
+ 'grade' => 20,
));
$this->setUser($this->teacher);
$feedbacktext = 'The feedback';
$feedbackformat = FORMAT_MOODLE;
- $weight = 25;
+ $weight = 10;
$gradinggradeover = 10;
$result = mod_workshop_external::evaluate_assessment($assessmentid, $feedbacktext, $feedbackformat, $weight,
$gradinggradeover);
$assessment = $DB->get_record('workshop_assessments', array('id' => $assessmentid));
$this->assertEquals('The feedback', $assessment->feedbackreviewer);
- $this->assertEquals(25, $assessment->weight);
+ $this->assertEquals(10, $assessment->weight);
+
+ // Now test passing incorrect weight and grade values.
+ $weight = 17;
+ $gradinggradeover = 100;
+ $result = mod_workshop_external::evaluate_assessment($assessmentid, $feedbacktext, $feedbackformat, $weight,
+ $gradinggradeover);
+ $result = external_api::clean_returnvalue(mod_workshop_external::evaluate_assessment_returns(), $result);
+ $this->assertFalse($result['status']);
+ $this->assertCount(2, $result['warnings']);
+ $found = 0;
+ foreach ($result['warnings'] as $warning) {
+ if ($warning['item'] == 'weight' || $warning['item'] == 'gradinggradeover') {
+ $found++;
+ }
+ }
+ $this->assertEquals(2, $found);
}
/**
$submissionid = $workshopgenerator->create_submission($this->workshop->id, $this->student->id);
$assessmentid = $workshopgenerator->create_assessment($submissionid, $this->anotherstudentg1->id, array(
'weight' => 3,
- 'grade' => 95,
+ 'grade' => 20,
));
assign_capability('mod/workshop:allocate', CAP_PROHIBIT, $this->teacherrole->id, $this->context->id);
$this->setUser($this->teacher);
$feedbacktext = 'The feedback';
$feedbackformat = FORMAT_MOODLE;
- $weight = 25;
- $gradinggradeover = 1000;
+ $weight = 10;
+ $gradinggradeover = 19;
$result = mod_workshop_external::evaluate_assessment($assessmentid, $feedbacktext, $feedbackformat, $weight,
$gradinggradeover);
$result = external_api::clean_returnvalue(mod_workshop_external::evaluate_assessment_returns(), $result);
$result = mod_workshop_external::get_assessment($assessmentid);
$result = external_api::clean_returnvalue(mod_workshop_external::get_assessment_returns(), $result);
- $this->assertNotEquals(25, $result['assessment']['weight']);
+ $this->assertNotEquals(10, $result['assessment']['weight']);
}
/**
$submissionid = $workshopgenerator->create_submission($this->workshop->id, $this->student->id);
$assessmentid = $workshopgenerator->create_assessment($submissionid, $this->anotherstudentg1->id, array(
'weight' => 3,
- 'grade' => 95,
+ 'grade' => 20,
));
$this->setUser($this->student);
$feedbacktext = 'The feedback';
$feedbackformat = FORMAT_MOODLE;
- $weight = 25;
+ $weight = 10;
$gradinggradeover = 50;
$this->expectException('moodle_exception');
mod_workshop_external::evaluate_assessment($assessmentid, $feedbacktext, $feedbackformat, $weight, $gradinggradeover);
"xpath": "0.0.23"
},
"engines": {
- "node": ">=4"
+ "node": "8.9"
}
}
--- /dev/null
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+ <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 -1 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M0 12h16v2H0v-2zm10-3H0v2h10V9zM0 8h16V6H0v2zm10-5H0v2h10V3zm6-1V0H0v2h16z" fill="#999"/></svg>
\ No newline at end of file
--- /dev/null
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+ <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M11.9 13.4c.3-.2.5-.4.8-.7 1.3-1.3 2-2.9 2-4.8s-.7-3.4-2-4.8c-1.3-1.3-2.9-2-4.8-2s-3.4.7-4.8 2c-1.3 1.3-2 2.9-2 4.8s.7 3.4 2 4.8c.3.3.5.5.8.7l-.8 1.8c-.1.2 0 .5.2.7.2.1.5 0 .7-.2l.9-1.7c.9.5 2 .7 3.1.7s2.2-.2 3.1-.7l.8 1.7c.1.2.4.4.7.2.2-.1.4-.4.2-.7l-.9-1.8zm-3.9.3c-1.6 0-2.9-.5-4-1.7-1.1-1.1-1.7-2.4-1.7-4S2.8 5.1 4 4c1.1-1.1 2.4-1.7 4-1.7s2.9.5 4 1.7c1.1 1.1 1.7 2.4 1.7 4s-.5 2.9-1.7 4c-1.1 1.2-2.4 1.7-4 1.7zm0-6l2.6 2.1-.7.8-2.8-2.2c-.1-.1-.2-.2-.2-.4V4.6H8v3.1zm3.2-6.2L12 0c1.8.9 3.1 2.2 4 4l-1.5.8c-.7-1.5-1.8-2.6-3.3-3.3zM1.5 4.8L0 4C.9 2.2 2.2.9 4 0l.8 1.5c-1.5.7-2.6 1.8-3.3 3.3z" fill="#999"/></svg>
\ No newline at end of file
* This file contains tests that walks a question through the manual graded
* behaviour.
*
- * @package qbehaviour
- * @subpackage manualgraded
+ * @package qbehaviour_manualgraded
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
new question_pattern_expectation($preg)
);
}
+
+ public function test_manual_grading_reshows_exactly_the_mark_input() {
+ global $PAGE;
+
+ // The current text editor depends on the users profile setting - so it needs a valid user.
+ $this->setAdminUser();
+ // Required to init a text editor.
+ $PAGE->set_url('/');
+
+ // Create an essay question graded out of 15 and attempt it.
+ $essay = test_question_maker::make_an_essay_question();
+ $this->start_attempt_at_question($essay, 'deferredfeedback', 15);
+ $this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$needsgrading);
+ $this->check_current_mark(null);
+ $this->assertEquals('This is my wonderful essay!',
+ $this->quba->get_response_summary($this->slot));
+
+ // Try to process a grade where the score will be stored rounded.
+ $this->manual_grade('Comment', '5.0', FORMAT_HTML);
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(5);
+ $this->displayoptions->manualcomment = question_display_options::EDITABLE;
+ $this->render();
+ $this->check_output_contains_text_input('-mark', '5.0');
+
+ // Rescale what the question is worth, and verify the display.
+ $this->get_question_attempt()->set_max_mark(1);
+ $this->render();
+ $this->check_output_contains_text_input('-mark', '0.3333333');
+ }
}
if (!is_null($currentmark)) {
$attributes['value'] = $currentmark;
}
- $a = new stdClass();
- $a->max = $qa->format_max_mark($options->markdp);
- $a->mark = html_writer::empty_tag('input', $attributes);
$markrange = html_writer::empty_tag('input', array(
'type' => 'hidden',
array('class' => 'error')) . html_writer::empty_tag('br');
}
+ $a = new stdClass();
+ $a->max = $qa->format_max_mark($options->markdp);
+ $a->mark = html_writer::empty_tag('input', $attributes);
$mark = html_writer::tag('div', html_writer::tag('div',
html_writer::tag('label', get_string('mark', 'question'),
array('for' => $markfield)),
qas.sequencenumber
", $qubaids->usage_id_in_params());
- if (!$records->valid()) {
- throw new coding_exception('Failed to load questions_usages_by_activity for qubaid_condition :' . $qubaids);
- }
-
$qubas = array();
- do {
+ while ($records->valid()) {
$record = $records->current();
$qubas[$record->qubaid] = question_usage_by_activity::load_from_records($records, $record->qubaid);
- } while ($records->valid());
+ }
$records->close();
/**
* This is used by the manual grading code, particularly in association with
- * validation. If there is a mark submitted in the request, then use that,
- * otherwise use the latest mark for this question.
- * @return number the current manual mark for this question, formatted for display.
+ * validation. It gets the current manual mark for a question, in exactly the string
+ * form that the teacher entered it, if possible. This may come from the current
+ * POST request, if there is one, otherwise from the database.
+ *
+ * @return string the current manual mark for this question, in the format the teacher typed,
+ * if possible.
*/
public function get_current_manual_mark() {
+ // Is there a current value in the current POST data? If so, use that.
$mark = $this->get_submitted_var($this->get_behaviour_field_name('mark'), PARAM_RAW_TRIMMED);
- if (is_null($mark)) {
- return format_float($this->get_mark(), 7, true, true);
- } else {
+ if ($mark !== null) {
return $mark;
}
+
+ // Otherwise, use the stored value.
+ // If the question max mark has not changed, use the stored value that was input.
+ $storedmaxmark = $this->get_last_behaviour_var('maxmark');
+ if ($storedmaxmark !== null && ($storedmaxmark - $this->get_max_mark()) < 0.0000005) {
+ return $this->get_last_behaviour_var('mark');
+ }
+
+ // The max mark for this question has changed so we must re-scale the current mark.
+ return format_float($this->get_mark(), 7, true, true);
}
/**
*/
class qtype_numerical_test_helper extends question_test_helper {
public function get_test_questions() {
- return array('pi', 'unit', 'currency');
+ return array('pi', 'unit', 'currency', 'pi3tries');
}
/**
return $num;
}
+ /**
+ * Get the form data that corresponds to saving a numerical question.
+ *
+ * This question asks for Pi to two decimal places. It has feedback
+ * for various wrong responses. There is hint data there, but
+ * it is all blank, so no hints are created if this question is saved.
+ *
+ * @return stdClass simulated question form data.
+ */
public function get_numerical_question_form_data_pi() {
$form = new stdClass();
$form->name = 'Pi to two d.p.';
return $form;
}
+ /**
+ * Get the form data that corresponds to saving a numerical question.
+ *
+ * Like {@link get_numerical_question_form_data_pi()}, but
+ * this time with two hints, making this suitable for use
+ * with the Interactive with multiple tries behaviour.
+ *
+ * @return stdClass simulated question form data.
+ */
+ public function get_numerical_question_form_data_pi3tries() {
+ $form = $this->get_numerical_question_form_data_pi();
+ $form->hint[0]['text'] = 'First hint';
+ $form->hint[1]['text'] = 'Second hint';
+ return $form;
+ }
+
public function get_numerical_question_data_pi() {
$q = new stdClass();
$q->name = 'Pi to two d.p.';
// Stop if we have exceeded the time limit (and there are still more items). Always
// do at least one second's worth of documents otherwise it will never make progress.
if ($lastindexeddoc !== $firstindexeddoc &&
- !empty($options['stopat']) && microtime(true) >= $options['stopat']) {
+ !empty($options['stopat']) && manager::get_current_time() >= $options['stopat']) {
$partial = true;
break;
}
*/
protected $engine = null;
+ /**
+ * Note: This should be removed once possible (see MDL-60644).
+ *
+ * @var float Fake current time for use in PHPunit tests
+ */
+ protected static $phpunitfaketime = 0;
+
/**
* Constructor, use \core_search\manager::instance instead to get a class instance.
*
});
// Decide time to stop.
- $stopat = microtime(true) + $timelimit;
+ $stopat = self::get_current_time() + $timelimit;
}
foreach ($searchareas as $areaid => $searcharea) {
$this->engine->area_index_starting($searcharea, $fullindex);
$indexingstart = time();
- $elapsed = microtime(true);
+ $elapsed = self::get_current_time();
// This is used to store this component config.
list($componentconfigname, $varname) = $searcharea->get_config_var_name();
}
if ($numdocs > 0) {
- $elapsed = round((microtime(true) - $elapsed), 3);
+ $elapsed = round((self::get_current_time() - $elapsed), 3);
$progress->output('Processed ' . $numrecords . ' records containing ' . $numdocs .
' documents, in ' . $elapsed . ' seconds' .
($partial ? ' (not complete)' : '') . '.', 1);
$progress->output('Engine reported error.');
}
- if ($timelimit && (microtime(true) >= $stopat)) {
+ if ($timelimit && (self::get_current_time() >= $stopat)) {
$progress->output('Stopping indexing due to time limit.');
break;
}
// Work out time to stop, if limited.
if ($timelimit) {
// Decide time to stop.
- $stopat = microtime(true) + $timelimit;
+ $stopat = self::get_current_time() + $timelimit;
}
// No PHP time limit.
$progress->output('Processing area: ' . $searcharea->get_visible_name());
- $elapsed = microtime(true);
+ $elapsed = self::get_current_time();
// Get the recordset of all documents from the area for this context.
$recordset = $searcharea->get_document_recordset($referencestarttime, $context);
}
if ($numdocs > 0) {
- $elapsed = round((microtime(true) - $elapsed), 3);
+ $elapsed = round((self::get_current_time() - $elapsed), 3);
$progress->output('Processed ' . $numrecords . ' records containing ' . $numdocs .
' documents, in ' . $elapsed . ' seconds' .
($partial ? ' (not complete)' : '') . '.', 1);
$progress->output('Engine reported error.', 1);
}
- if ($partial && $timelimit && (microtime(true) >= $stopat)) {
+ if ($partial && $timelimit && (self::get_current_time() >= $stopat)) {
$progress->output('Stopping indexing due to time limit.');
break;
}
}
$complete = false;
- $before = microtime(true);
+ $before = self::get_current_time();
if ($timelimit) {
$stopat = $before + $timelimit;
}
// Calculate remaining time.
$remainingtime = 0;
- $beforeindex = microtime(true);
+ $beforeindex = self::get_current_time();
if ($timelimit) {
$remainingtime = $stopat - $beforeindex;
}
$progress, $request->partialarea, $request->partialtime);
// Work out shared part of message.
- $endmessage = $contextname . ' (' . round(microtime(true) - $beforeindex, 1) . 's)';
+ $endmessage = $contextname . ' (' . round(self::get_current_time() - $beforeindex, 1) . 's)';
// Update database table and continue/stop as appropriate.
if ($result->complete) {
}
}
+ /**
+ * Gets current time for use in search system.
+ *
+ * Note: This should be replaced with generic core functionality once possible (see MDL-60644).
+ *
+ * @return float Current time in seconds (with decimals)
+ */
+ public static function get_current_time() {
+ if (PHPUNIT_TEST && self::$phpunitfaketime) {
+ return self::$phpunitfaketime;
+ }
+ return microtime(true);
+ }
}
$string['errorvalidatingschema'] = 'Error validating Solr schema: field {$a->fieldname} does not exist. Please <a href="{$a->setupurl}">follow this link</a> to set up the required fields.';
$string['extensionerror'] = 'The Apache Solr PHP extension is not installed. Please check the documentation.';
$string['fileindexing'] = 'Enable file indexing';
-$string['fileindexing_help'] = 'If your Solr install supports it, this feature allows Moodle to send files to be indexed.';
+$string['fileindexing_help'] = 'If your Solr install supports it, this feature allows Moodle to send files to be indexed.<br/>
+You will need to reindex all site contents after enabling this option for all files to be added.';
$string['fileindexsettings'] = 'File indexing settings';
$string['maxindexfilekb'] = 'Maximum file size to index (kB)';
$string['maxindexfilekb_help'] = 'Files larger than this number of kilobytes will not be included in search indexing. If set to zero, files of any size will be indexed.';
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+use core_search\manager;
+
defined('MOODLE_INTERNAL') || die;
class engine extends \core_search\engine {
public function add_document($document, $fileindexing = false) {
if ($this->adddelay) {
- usleep($this->adddelay);
+ \testable_core_search::fake_current_time(manager::get_current_time() + $this->adddelay);
}
$this->added[] = $document;
return true;
return parent::is_search_area($classname);
}
+ /**
+ * Fakes the current time for PHPunit. Turns off faking time if called with default parameter.
+ *
+ * Note: This should be replaced with core functionality once possible (see MDL-60644).
+ *
+ * @param float $faketime Current time
+ */
+ public static function fake_current_time($faketime = 0.0) {
+ static::$phpunitfaketime = $faketime;
+ }
}
$this->mycoursesareaid = \core_search\manager::generate_areaid('core_course', 'mycourse');
}
+ protected function tearDown() {
+ // Stop it from faking time in the search manager (if set by test).
+ testable_core_search::fake_current_time();
+ parent::tearDown();
+ }
+
public function test_search_enabled() {
$this->resetAfterTest();
// Make the search engine delay while indexing each document.
$search->get_engine()->set_add_delay(1.2);
+ // Use fake time, starting from now.
+ testable_core_search::fake_current_time(time());
+
// Index with a limit of 2 seconds - it should index 2 of the documents (after the second
// one, it will have taken 2.4 seconds so it will stop).
$search->index(false, 2);
// Wait to next second (so as to not reindex the label more than once, as it will now
// be timed before the indexing run).
$this->waitForSecond();
+ testable_core_search::fake_current_time(time());
// Next index with 1 second limit should do the label and not the forum - the logic is,
// if it spent ages indexing an area last time, do that one last on next run.
// Do the processing again with a time limit and indexing delay. The time limit is too
// small; because of the way the logic works, this means it will index 2 activities.
+ testable_core_search::fake_current_time(time());
$search->get_engine()->set_add_delay(0.2);
$search->process_index_requests(0.1, $progress);
$out = $progress->get_buffer();
.calendarmonth {
width: 98%;
margin: 10px auto;
- }
- .calendarmonth {
ul {
margin: 0;
padding: 0;
@include text-truncate;
max-width: 100%;
display: inline-block;
+
+ &:hover {
+ text-decoration: $link-decoration;
+
+ .eventname {
+ text-decoration: $link-hover-decoration;
+ }
+ }
}
.icon {
.icon {
vertical-align: initial;
}
+
+ > a {
+ &:hover {
+ text-decoration: $link-decoration;
+
+ .eventname {
+ text-decoration: $link-hover-decoration;
+ }
+ }
+ }
}
}
}
.dndupload-progress-outer {
- @extend progress;
+ background-color: $progress-bg;
+ @include border-radius($progress-border-radius);
+ @include box-shadow($progress-box-shadow);
+ display: block;
+ width: 100%;
+ height: $spacer-y;
+ margin-bottom: $spacer-y;
}
.dndupload-progress-inner {
- .progress {
- @extend .progress-bar;
- }
+ background-color: $progress-bar-color;
+ @include border-left-radius($progress-border-radius);
+ display: inline-block;
+ height: $spacer-y;
}
.dndupload-hidden {
+.modal-header {
+ padding: 0;
+
+ .close {
+ padding: $modal-title-padding;
+ margin-top: 0;
+ }
+
+ .modal-title {
+ padding: $modal-title-padding;
+ }
+}
+
.modal-body {
& > .loading-icon {
display: block;
vertical-align: baseline;
}
+.que.multianswer .formulation .yui3-widget-positioned {
+ box-sizing: content-box;
+ .feedbackspan {
+ width: inherit;
+ max-width: inherit;
+ }
+}
+
.path-mod-quiz input[size] {
width: auto;
max-width: 100%;
font-weight: bold;
}
+.group-edit {
+ position: absolute;
+ right: 0;
+ margin-right: 0.6em;
+}
+
+.group-image {
+ display: block;
+ float: left;
+ margin-right: 1em;
+
+ .grouppicture {
+ border-radius: 50%;
+ }
+}
+
.groupinfobox {
@extend .card;
}
<div class="modal moodle-has-zindex" data-region="modal-container" aria-hidden="true" role="dialog">
<div class="modal-dialog {{$classes}}{{/classes}}" role="document" data-region="modal" aria-labelledby="{{uniqid}}-modal-title">
<div class="modal-content">
- <div class="modal-header" data-region="header">
+ <div class="modal-header {{$headerclasses}}{{headerclasses}}{{/headerclasses}}" data-region="header">
<button type="button" class="close" data-action="hide" aria-label={{#quote}}{{#str}}closebuttontitle{{/str}}{{/quote}}>
<span aria-hidden="true">×</span>
</button>
{{$header}}
- <h4 id="{{uniqid}}-modal-title" data-region="title" tabindex="0">{{$title}}{{title}}{{/title}}</h4>
+ <h4 id="{{uniqid}}-modal-title" class="modal-title" data-region="title" tabindex="0">{{$title}}{{title}}{{/title}}</h4>
{{/header}}
</div>
<div class="modal-body" data-region="body">
.calendarmonth {
width: 98%;
margin: 10px auto;
- }
- .calendarmonth {
+
ul {
margin: 0;
padding: 0;
.text-truncate;
max-width: 100%;
display: inline-block;
+
+ &:hover {
+ text-decoration: none;
+
+ .eventname {
+ text-decoration: underline;
+ }
+ }
}
.icon {
padding: 0;
margin-left: 0.25em;
}
+
+ > a {
+ &:hover {
+ text-decoration: none;
+
+ .eventname {
+ text-decoration: underline;
+ }
+ }
+ }
}
}
.content {
width: auto;
vertical-align: baseline;
}
+.que.multianswer .formulation .yui3-widget-positioned .feedbackspan {
+ box-sizing: content-box;
+ padding-bottom: 0;
+ max-width: inherit;
+ width: inherit;
+}
.path-mod-quiz input[size] {
width: auto;
}
font-weight: bold;
}
.groupinfobox {
- .well
+ .well;
+ position: relative;
+
+ h3 {
+ margin-top: 0px;
+ }
}
+
.groupinfobox .left {
padding: 10px;
width: 100px;
vertical-align: top;
}
+
+.group-image {
+ display: block;
+ float: left;
+ margin-right: 1em;
+}
+
+.group-edit {
+ position: absolute;
+ right: 0;
+ margin-right: 0.6em;
+}
+
.course-participation #showall {
text-align: center;
margin: 10px 0;
max-width: 100%;
display: inline-block;
}
+.path-calendar .maincalendar .calendarmonth ul li > a:hover {
+ text-decoration: none;
+}
+.path-calendar .maincalendar .calendarmonth ul li > a:hover .eventname {
+ text-decoration: underline;
+}
.path-calendar .maincalendar .calendarmonth ul li .icon {
margin-left: 0.25em;
margin-right: 0.25em;
padding: 0;
margin-left: 0.25em;
}
+.block .calendar_filters li > a:hover {
+ text-decoration: none;
+}
+.block .calendar_filters li > a:hover .eventname {
+ text-decoration: underline;
+}
.block .content h3.eventskey {
margin-top: 0.5em;
}
width: auto;
vertical-align: baseline;
}
+.que.multianswer .formulation .yui3-widget-positioned .feedbackspan {
+ box-sizing: content-box;
+ padding-bottom: 0;
+ max-width: inherit;
+ width: inherit;
+}
.path-mod-quiz input[size] {
width: auto;
}
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
border-color: #e3e3e3;
+ position: relative;
}
.groupinfobox blockquote {
border-color: #ddd;
border-color: rgba(0, 0, 0, 0.15);
}
+.groupinfobox h3 {
+ margin-top: 0px;
+}
.groupinfobox .left {
padding: 10px;
width: 100px;
vertical-align: top;
}
+.group-image {
+ display: block;
+ float: left;
+ margin-right: 1em;
+}
+.group-edit {
+ position: absolute;
+ right: 0;
+ margin-right: 0.6em;
+}
.course-participation #showall {
text-align: center;
margin: 10px 0;
--- /dev/null
+{{!
+ 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/>.
+}}
+{{!
+ @template core_calendar/event_summary_body
+
+ This template renders the body of calendar events summary modal.
+
+ Example context (json):
+ {
+ "timestart": 1490320388,
+ "description": "An random event description",
+ "eventtype": "User",
+ "source": "Ical imported",
+ "groupname": "Group 1"
+ }
+}}
+<div{{!
+ }} data-region="summary-modal-container"{{!
+ }} data-event-id="{{id}}"{{!
+ }} data-event-title="{{name}}"{{!
+ }} data-event-count="{{eventcount}}"{{!
+ }} data-event-="{{repeatid}}"{{!
+ }} data-action-event="{{isactionevent}}"{{!
+ }} data-edit-url="{{editurl}}"{{!
+ }}>
+ <div class="container-fluid">
+ <div class="row-fluid">
+ <div class="span1">{{#pix}} i/calendareventtime, core, {{#str}} when, core_calendar {{/str}} {{/pix}}</div>
+ <div class="span11">{{#userdate}} {{timestart}}, {{#str}} strftimerecentfull {{/str}} {{/userdate}}</div>
+ </div>
+ <div class="row-fluid m-t-1">
+ <div class="span1">{{#pix}} i/calendar, core, {{#str}} eventtype, core_calendar {{/str}} {{/pix}}</div>
+ <div class="span11">{{eventtype}}</div>
+ </div>
+ {{#description}}
+ <div class="row-fluid m-t-1">
+ <div class="span1">{{#pix}} i/calendareventdescription, core, {{#str}} description {{/str}} {{/pix}}</div>
+ <div class="span11">{{{.}}}</div>
+ </div>
+ {{/description}}
+ {{#iscategoryevent}}
+ <div class="row-fluid m-t-1">
+ <div class="span1">{{#pix}} i/categoryevent, core, {{#str}} category {{/str}} {{/pix}}</div>
+ <div class="span11">{{{category.nestedname}}}</div>
+ </div>
+ {{/iscategoryevent}}
+ {{#iscourseevent}}
+ <div class="row-fluid m-t-1">
+ <div class="span1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
+ <div class="span11"><a href="{{url}}">{{{course.fullname}}}</a></div>
+ </div>
+ {{/iscourseevent}}
+ {{#groupname}}
+ <div class="row-fluid m-t-1">
+ <div class="span1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
+ <div class="span11"><a href="{{url}}">{{{course.fullname}}}</a></div>
+ </div>
+ <div class="row-fluid m-t-1">
+ <div class="span1">{{#pix}} i/groupevent, core, {{#str}} group {{/str}} {{/pix}}</div>
+ <div class="span11">{{{groupname}}}</div>
+ </div>
+ {{/groupname}}
+ {{#subscription}}
+ {{#displayeventsource}}
+ <div class="row-fluid m-t-1">
+ <div class="span1">{{#pix}} i/rss, core, {{#str}} eventsource, core_calendar {{/str}} {{/pix}}</div>
+ <div class="span11">
+ {{#url}}
+ <a href="{{url}}">{{#str}}subscriptionsource, core_calendar, {{name}}{{/str}}</a>
+ {{/url}}
+ {{^url}}
+ <p>{{#str}}subscriptionsource, core_calendar, {{name}}{{/str}}</p>
+ {{/url}}
+ </div>
+ </div>
+ {{/displayeventsource}}
+ {{/subscription}}
+ </div>
+</div>
$newcourse = optional_param('newcourse', false, PARAM_BOOL);
$selectall = optional_param('selectall', false, PARAM_BOOL); // When rendering checkboxes against users mark them all checked.
$roleid = optional_param('roleid', 0, PARAM_INT);
+$groupparam = optional_param('group', 0, PARAM_INT);
$PAGE->set_url('/user/index.php', array(
'page' => $page,
$canaccessallgroups = has_capability('moodle/site:accessallgroups', $context);
if ($course->groupmode != NOGROUPS) {
if ($canaccessallgroups) {
- // If the user can see all groups, set default to 0.
- $groupid = 0;
+ // Change the group if the user can access all groups and has specified group in the URL.
+ if ($groupparam) {
+ $groupid = $groupparam;
+ }
} else {
// Otherwise, get the user's default group.
$groupid = groups_get_course_group($course, true);
// If course supports groups we may need to set a default.
if ($groupid !== false) {
- // If we are in a course with visible groups and the user has not submitted anything and does not have
- // access to all groups, then set a default group. This is the same behaviour in 3.3.
- if (!$canaccessallgroups && !$filterwassubmitted && $course->groupmode == VISIBLEGROUPS) {
+ if ($canaccessallgroups) {
+ // User can access all groups, let them filter by whatever was selected.
+ $filtersapplied[] = USER_FILTER_GROUP . ':' . $groupid;
+ } else if (!$filterwassubmitted && $course->groupmode == VISIBLEGROUPS) {
+ // If we are in a course with visible groups and the user has not submitted anything and does not have
+ // access to all groups, then set a default group.
$filtersapplied[] = USER_FILTER_GROUP . ':' . $groupid;
- } else if (!$canaccessallgroups && !$hasgroupfilter && $course->groupmode != VISIBLEGROUPS) {
+ } else if (!$hasgroupfilter && $course->groupmode != VISIBLEGROUPS) {
// The user can't access all groups and has not set a group filter in a course where the groups are not visible
// then apply a default group filter.
$filtersapplied[] = USER_FILTER_GROUP . ':' . $groupid;
}
}
+if ($groupid && ($course->groupmode != SEPARATEGROUPS || $canaccessallgroups)) {
+ $grouprenderer = $PAGE->get_renderer('core_group');
+ $groupdetailpage = new \core_group\output\group_details($groupid);
+ echo $grouprenderer->group_details($groupdetailpage);
+}
+
// Manage enrolments.
$manager = new course_enrolment_manager($PAGE, $course);
$enrolbuttons = $manager->get_manual_enrol_buttons();
defined('MOODLE_INTERNAL') || die();
-$version = 2017102700.00; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2017110100.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
-$release = '3.4beta+ (Build: 20171027)'; // Human-friendly version name
+$release = '3.4beta+ (Build: 20171101)'; // Human-friendly version name
$branch = '34'; // This version's branch.
$maturity = MATURITY_BETA; // This version's maturity level.