Merge branch 'MDL-60576-master' of git://github.com/andrewnicols/moodle
authorDamyon Wiese <damyon@moodle.com>
Thu, 2 Nov 2017 03:38:57 +0000 (11:38 +0800)
committerDamyon Wiese <damyon@moodle.com>
Thu, 2 Nov 2017 03:38:57 +0000 (11:38 +0800)
77 files changed:
.travis.yml
admin/index.php
admin/registration/renderer.php
admin/renderer.php
admin/settings/plugins.php
admin/tool/httpsreplace/classes/url_finder.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/tests/externallib_test.php
analytics/classes/local/analyser/by_course.php
analytics/classes/local/time_splitting/base.php
calendar/amd/build/calendar.min.js
calendar/amd/src/calendar.js
calendar/classes/external/calendar_day_exporter.php
calendar/classes/external/calendar_upcoming_exporter.php
calendar/classes/external/month_exporter.php
calendar/renderer.php
calendar/templates/event_summary_body.mustache
course/externallib.php
grade/grading/lib.php
group/classes/output/group_details.php [new file with mode: 0644]
group/classes/output/renderer.php
group/members.php
group/templates/group_details.mustache [new file with mode: 0644]
group/tests/behat/group_description.feature [new file with mode: 0644]
lang/en/admin.php
lang/en/calendar.php
lib/amd/build/modal.min.js
lib/amd/src/modal.js
lib/behat/classes/partial_named_selector.php
lib/classes/hub/site_registration_form.php
lib/classes/output/icon_system_fontawesome.php
lib/phpunit/classes/base_testcase.php
lib/templates/modal.mustache
lib/weblib.php
message/output/popup/templates/message_popover.mustache
message/output/popup/templates/notification_popover.mustache
mod/assign/styles.css
mod/data/classes/external.php
mod/data/tests/externallib_test.php
mod/lti/services.php
mod/page/classes/external.php
mod/quiz/report/attemptsreport.php
mod/quiz/report/overview/report.php
mod/quiz/report/overview/tests/behat/basic.feature
mod/quiz/report/responses/first_or_all_responses_table.php
mod/quiz/report/responses/report.php
mod/quiz/report/responses/responses_form.php
mod/quiz/report/responses/tests/behat/basic.feature [new file with mode: 0644]
mod/workshop/classes/external.php
mod/workshop/tests/external_test.php
package.json
pix/i/calendareventdescription.png [new file with mode: 0644]
pix/i/calendareventdescription.svg [new file with mode: 0644]
pix/i/calendareventtime.png [new file with mode: 0644]
pix/i/calendareventtime.svg [new file with mode: 0644]
question/behaviour/manualgraded/tests/walkthrough_test.php
question/behaviour/rendererbase.php
question/engine/datalib.php
question/engine/questionattempt.php
question/type/numerical/tests/helper.php
search/classes/engine.php
search/classes/manager.php
search/engine/solr/lang/en/search_solr.php
search/tests/fixtures/mock_search_engine.php
search/tests/fixtures/testable_core_search.php
search/tests/manager_test.php
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/modal.scss
theme/boost/scss/moodle/question.scss
theme/boost/scss/moodle/user.scss
theme/boost/templates/core/modal.mustache
theme/bootstrapbase/less/moodle/question.less
theme/bootstrapbase/less/moodle/user.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/core_calendar/event_summary_body.mustache [new file with mode: 0644]
user/index.php
version.php

index f9f8b76..6542585 100644 (file)
@@ -55,7 +55,7 @@ matrix:
     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.
index 8d0199e..e04c319 100644 (file)
@@ -868,6 +868,7 @@ $cachewarnings = cache_helper::warnings();
 $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'))) {
@@ -885,4 +886,4 @@ $output = $PAGE->get_renderer('core', 'admin');
 echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
                                        $maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
                                        $registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir,
-                                       $mobileconfigured, $overridetossl);
+                                       $mobileconfigured, $overridetossl, $invalidforgottenpasswordurl);
index 4832013..b7414b4 100644 (file)
@@ -36,6 +36,6 @@ class core_register_renderer extends plugin_renderer_base {
      * @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]);
     }
 }
index 9ea4ea1..dbd77f6 100644 (file)
@@ -280,6 +280,7 @@ class core_admin_renderer extends plugin_renderer_base {
      * @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.
      */
@@ -287,7 +288,7 @@ class core_admin_renderer extends plugin_renderer_base {
             $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 = '';
 
@@ -308,6 +309,7 @@ class core_admin_renderer extends plugin_renderer_base {
         $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 ///
@@ -866,6 +868,24 @@ class core_admin_renderer extends plugin_renderer_base {
         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') . '&nbsp;' . $configurebutton,
+                'error alert alert-danger');
+        }
+
+        return $output;
+    }
+
     /**
      * Helper method to render the information about the available Moodle update
      *
index 4444ff0..c55b1f1 100644 (file)
@@ -102,7 +102,7 @@ if ($hassiteconfig) {
     $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'),
index af9fc9b..8dc1416 100644 (file)
@@ -24,6 +24,9 @@
 
 namespace tool_httpsreplace;
 
+use database_column_info;
+use progress_bar;
+
 defined('MOODLE_INTERNAL') || die();
 
 /**
@@ -60,7 +63,7 @@ class url_finder {
      * 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/
@@ -174,7 +177,7 @@ class url_finder {
                             $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];
@@ -186,7 +189,7 @@ class url_finder {
                                 }
                                 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;
index bfc42ee..9b3e3b3 100644 (file)
@@ -140,7 +140,7 @@ class api {
             '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,
index b5986aa..ff1f3c1 100644 (file)
@@ -97,11 +97,13 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         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);
index 7d9f28a..bbda69a 100644 (file)
@@ -44,7 +44,11 @@ abstract class by_course extends base {
 
         // 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');
index 11532a9..1ee6dfb 100644 (file)
@@ -197,6 +197,10 @@ abstract class base {
 
         $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);
 
index b9d8d94..a21c475 100644 (file)
Binary files a/calendar/amd/build/calendar.min.js and b/calendar/amd/build/calendar.min.js differ
index 67fee60..4b25c1a 100644 (file)
@@ -86,18 +86,44 @@ define([
         });
     };
 
+    /**
+     * 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;
@@ -112,6 +138,7 @@ define([
                 templateContext: {
                     canedit: eventData.canedit,
                     candelete: eventData.candelete,
+                    headerclasses: typeClass,
                     isactionevent: eventData.isactionevent,
                     url: eventData.url
                 }
index 091dffc..5869c10 100644 (file)
@@ -74,7 +74,7 @@ class calendar_day_exporter extends exporter {
             ],
             'defaulteventcontext' => [
                 'type' => PARAM_INT,
-                'default' => null,
+                'default' => 0,
             ],
             'filter_selector' => [
                 'type' => PARAM_RAW,
@@ -238,7 +238,7 @@ class calendar_day_exporter extends exporter {
      * @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);
     }
 
index ab35779..a7759c6 100644 (file)
@@ -74,7 +74,7 @@ class calendar_upcoming_exporter extends exporter {
             ],
             'defaulteventcontext' => [
                 'type' => PARAM_INT,
-                'default' => null,
+                'default' => 0,
             ],
             'filter_selector' => [
                 'type' => PARAM_RAW,
index 9ca253e..12b910e 100644 (file)
@@ -171,7 +171,7 @@ class month_exporter extends exporter {
             ],
             'defaulteventcontext' => [
                 'type' => PARAM_INT,
-                'default' => null,
+                'default' => 0,
             ],
         ];
     }
index 767e837..7193a97 100644 (file)
@@ -64,15 +64,17 @@ class core_calendar_renderer extends plugin_renderer_base {
         $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 = '';
index 2a67233..0770cc5 100644 (file)
     }} 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>
index 73e4b80..d3aba3b 100644 (file)
@@ -268,8 +268,9 @@ class core_course_external extends external_api {
 
                         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
index 46fd52a..d7c8294 100644 (file)
@@ -489,7 +489,7 @@ class grading_manager {
      * 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;
@@ -534,7 +534,7 @@ class grading_manager {
     /**
      * 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()) {
diff --git a/group/classes/output/group_details.php b/group/classes/output/group_details.php
new file mode 100644 (file)
index 0000000..11d4a62
--- /dev/null
@@ -0,0 +1,93 @@
+<?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;
+        }
+    }
+}
index 14443cf..c3285fa 100644 (file)
@@ -47,4 +47,15 @@ class renderer extends plugin_renderer_base {
         $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);
+    }
 }
index 3f4e352..f089e32 100644 (file)
@@ -102,36 +102,11 @@ echo $OUTPUT->heading(get_string('adduserstogroup', 'group').": $groupname", 3);
 // 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
diff --git a/group/templates/group_details.mustache b/group/templates/group_details.mustache
new file mode 100644 (file)
index 0000000..bd6408f
--- /dev/null
@@ -0,0 +1,53 @@
+{{!
+    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}}
diff --git a/group/tests/behat/group_description.feature b/group/tests/behat/group_description.feature
new file mode 100644 (file)
index 0000000..015cb26
--- /dev/null
@@ -0,0 +1,104 @@
+@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
index 5d9b636..69291ba 100644 (file)
@@ -616,6 +616,7 @@ $string['installhijacked'] = 'Installation must be finished from the original IP
 $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.';
@@ -939,15 +940,7 @@ $string['quizattemptsupgradedmessage'] = 'In Moodle 2.1 there was a major upgrad
 $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.';
index 01dcd4f..206ebb2 100644 (file)
@@ -108,6 +108,7 @@ $string['eventrepeat'] = 'Repeats';
 $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';
index a280a27..ab2020f 100644 (file)
Binary files a/lib/amd/build/modal.min.js and b/lib/amd/build/modal.min.js differ
index 3f368b9..302cd8d 100644 (file)
@@ -358,7 +358,8 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes',
     };
 
     /**
-     * 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
@@ -368,6 +369,9 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes',
      * @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') {
@@ -396,6 +400,34 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes',
         }
     };
 
+    /**
+     * 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.
      *
@@ -508,6 +540,12 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes',
             return;
         }
 
+        if (this.hasFooterContent()) {
+            this.showFooter();
+        } else {
+            this.hideFooter();
+        }
+
         if (!this.isAttached) {
             this.attachToDOM();
         }
index a0599fb..1faa0db 100644 (file)
@@ -109,6 +109,7 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
         'text' => 'text',
         'xpath_element' => 'xpath_element',
         'form_row' => 'form_row',
+        'autocomplete_selection' => 'autocomplete_selection',
     );
 
     /**
@@ -178,6 +179,9 @@ XPATH
 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
     );
 
index 317862e..ee7368d 100644 (file)
@@ -173,6 +173,15 @@ class site_registration_form extends \moodleform {
         $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);
     }
 
@@ -212,26 +221,6 @@ class site_registration_form extends \moodleform {
 
     }
 
-    /**
-     * 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
      *
index 055857a..d7e858a 100644 (file)
@@ -195,6 +195,8 @@ class icon_system_fontawesome extends icon_system_font {
             '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',
index 54eb63f..8f78f4c 100644 (file)
@@ -242,7 +242,7 @@ abstract class base_testcase extends PHPUnit_Framework_TestCase {
                         }
                     } // match by exact string
                     else {
-                        if ($node->getAttribute($name) != $value) {
+                        if ($node->getAttribute($name) !== (string) $value) {
                             $invalid = true;
                         }
                     }
index d8c4b70..a860c75 100644 (file)
         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}}
index e8e2e95..4716080 100644 (file)
@@ -2423,23 +2423,56 @@ function print_group_picture($group, $courseid, $large=false, $return=false, $li
         }
     }
 
+    $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 .'&amp;group='. $group->id .'">';
-    } else {
-        $output = '';
-    }
     if ($large) {
         $file = 'f1';
     } else {
@@ -2448,18 +2481,7 @@ function print_group_picture($group, $courseid, $large=false, $return=false, $li
 
     $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;
 }
 
 
index 666242a..0355993 100644 (file)
     {{$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}}
index b0684ed..97efbff 100644 (file)
 
     {{$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}}
index aeb2e12..f1b36a1 100644 (file)
     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 **/
index b188cbe..649a633 100644 (file)
@@ -264,7 +264,6 @@ class mod_data_external extends external_api {
             'warnings' => $warnings
         );
 
-        $groupmode = groups_get_activity_groupmode($cm);
         if (!empty($params['groupid'])) {
             $groupid = $params['groupid'];
             // Determine is the group is visible to user.
@@ -273,12 +272,9 @@ class mod_data_external extends external_api {
             }
         } 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;
             }
@@ -399,11 +395,8 @@ class mod_data_external extends external_api {
         } 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;
             }
@@ -520,7 +513,7 @@ class mod_data_external extends external_api {
         $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');
             }
@@ -728,11 +721,8 @@ class mod_data_external extends external_api {
         } 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;
             }
@@ -989,26 +979,18 @@ class mod_data_external extends external_api {
         // 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');
         }
index dece2fe..e8b8dc9 100644 (file)
@@ -416,6 +416,8 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $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);
@@ -423,9 +425,10 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         // 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];
     }
 
     /**
@@ -433,15 +436,16 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      */
     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']);
@@ -450,36 +454,44 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $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']);
@@ -491,17 +503,17 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $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);
@@ -509,14 +521,14 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $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.
@@ -525,7 +537,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $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.
@@ -543,7 +555,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         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);
@@ -566,7 +578,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      */
     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);
@@ -633,7 +645,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      * 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);
@@ -646,7 +658,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      */
     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);
@@ -676,14 +688,14 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      */
     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');
@@ -694,17 +706,17 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         // 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']);
 
@@ -712,26 +724,27 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $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);
@@ -740,9 +753,9 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         ];
         $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.
@@ -752,9 +765,9 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         ];
         $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'));
@@ -765,9 +778,9 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         ];
         $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'));
@@ -779,7 +792,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $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']);
     }
 
     /**
@@ -787,7 +800,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      */
     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)));
@@ -801,7 +814,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      */
     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)));
@@ -815,7 +828,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      */
     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');
@@ -827,7 +840,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      */
     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);
@@ -845,7 +858,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      */
     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);
@@ -858,7 +871,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      */
     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;
@@ -874,7 +887,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      */
     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');
@@ -887,7 +900,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
     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 = [];
@@ -1038,7 +1051,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      */
     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;
@@ -1055,7 +1068,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      */
     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);
@@ -1072,7 +1085,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
     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 = [];
@@ -1212,7 +1225,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      * 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, []);
@@ -1228,7 +1241,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      */
     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;
@@ -1245,7 +1258,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
      */
     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');
@@ -1261,7 +1274,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
 
         $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();
index 1cf58c5..547ea0e 100644 (file)
@@ -33,6 +33,7 @@ require_once($CFG->dirroot . '/mod/lti/locallib.php');
 $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'] : '');
@@ -51,7 +52,7 @@ foreach ($services as $service) {
     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();
index 260512a..8b10f1e 100644 (file)
@@ -161,8 +161,9 @@ class mod_page_external extends external_api {
                                                                 $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;
index 4545a70..06614eb 100644 (file)
@@ -138,6 +138,48 @@ abstract class quiz_attempts_report extends quiz_default_report {
         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.
index d5d47bd..e5c0e93 100644 (file)
@@ -98,36 +98,13 @@ class quiz_overview_report extends quiz_attempts_report {
         $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();
index 8612ffd..8799c79 100644 (file)
@@ -1,4 +1,4 @@
-@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
index 4a90a08..4f2600a 100644 (file)
@@ -68,6 +68,15 @@ class quiz_first_or_all_responses_table extends quiz_last_responses_table {
         // 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);
@@ -230,7 +239,7 @@ class quiz_first_or_all_responses_table extends quiz_last_responses_table {
      * @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;
@@ -244,18 +253,27 @@ class quiz_first_or_all_responses_table extends quiz_last_responses_table {
      * @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);
         }
     }
index f72fdbb..693029e 100644 (file)
@@ -111,36 +111,13 @@ class quiz_responses_report extends quiz_attempts_report {
 
         $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();
index 461dcd2..4fdbe90 100644 (file)
@@ -70,6 +70,7 @@ class quiz_responses_settings_form extends mod_quiz_attempts_report_form {
                                            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);
         }
     }
 }
diff --git a/mod/quiz/report/responses/tests/behat/basic.feature b/mod/quiz/report/responses/tests/behat/basic.feature
new file mode 100644 (file)
index 0000000..b8344ea
--- /dev/null
@@ -0,0 +1,91 @@
+@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
index 829f0d3..7657fb6 100644 (file)
@@ -1728,6 +1728,18 @@ class mod_workshop_external extends external_api {
         $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;
index 8d26175..a725af5 100644 (file)
@@ -1612,13 +1612,13 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         $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);
@@ -1627,7 +1627,23 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
 
         $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);
     }
 
     /**
@@ -1638,7 +1654,7 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         $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);
@@ -1648,8 +1664,8 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         $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);
@@ -1657,7 +1673,7 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
 
         $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']);
     }
 
     /**
@@ -1668,13 +1684,13 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         $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);
index ecc7d01..3a79c54 100644 (file)
@@ -22,6 +22,6 @@
     "xpath": "0.0.23"
   },
   "engines": {
-    "node": ">=4"
+    "node": "8.9"
   }
 }
diff --git a/pix/i/calendareventdescription.png b/pix/i/calendareventdescription.png
new file mode 100644 (file)
index 0000000..5ab349c
Binary files /dev/null and b/pix/i/calendareventdescription.png differ
diff --git a/pix/i/calendareventdescription.svg b/pix/i/calendareventdescription.svg
new file mode 100644 (file)
index 0000000..986b0a2
--- /dev/null
@@ -0,0 +1,3 @@
+<!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
diff --git a/pix/i/calendareventtime.png b/pix/i/calendareventtime.png
new file mode 100644 (file)
index 0000000..944f36b
Binary files /dev/null and b/pix/i/calendareventtime.png differ
diff --git a/pix/i/calendareventtime.svg b/pix/i/calendareventtime.svg
new file mode 100644 (file)
index 0000000..94f355b
--- /dev/null
@@ -0,0 +1,3 @@
+<!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
index d63030c..c47e205 100644 (file)
@@ -18,8 +18,7 @@
  * 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
  */
@@ -622,4 +621,40 @@ class qbehaviour_manualgraded_walkthrough_testcase extends qbehaviour_walkthroug
             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');
+    }
 }
index ea6a187..b474887 100644 (file)
@@ -126,9 +126,6 @@ abstract class qbehaviour_renderer extends plugin_renderer_base {
             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',
@@ -152,6 +149,9 @@ abstract class qbehaviour_renderer extends plugin_renderer_base {
                         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)),
index e486e46..0ef5816 100644 (file)
@@ -525,15 +525,11 @@ ORDER BY
     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();
 
index fa110dd..d295170 100644 (file)
@@ -649,17 +649,29 @@ class question_attempt {
 
     /**
      * 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);
     }
 
     /**
index 7157dfb..650674b 100644 (file)
@@ -35,7 +35,7 @@ defined('MOODLE_INTERNAL') || die();
  */
 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');
     }
 
     /**
@@ -71,6 +71,15 @@ class qtype_numerical_test_helper extends question_test_helper {
         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.';
@@ -156,6 +165,22 @@ class qtype_numerical_test_helper extends question_test_helper {
         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.';
index de5fff2..a3eed8d 100644 (file)
@@ -220,7 +220,7 @@ abstract class engine {
             // 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;
             }
index ea95108..8760bf4 100644 (file)
@@ -97,6 +97,13 @@ class manager {
      */
     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.
      *
@@ -669,7 +676,7 @@ class manager {
             });
 
             // Decide time to stop.
-            $stopat = microtime(true) + $timelimit;
+            $stopat = self::get_current_time() + $timelimit;
         }
 
         foreach ($searchareas as $areaid => $searcharea) {
@@ -680,7 +687,7 @@ class manager {
             $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();
@@ -730,7 +737,7 @@ class manager {
             }
 
             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);
@@ -760,7 +767,7 @@ class manager {
                 $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;
             }
@@ -803,7 +810,7 @@ class manager {
         // 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.
@@ -840,7 +847,7 @@ class manager {
 
             $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);
@@ -881,7 +888,7 @@ class manager {
             }
 
             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);
@@ -895,7 +902,7 @@ class manager {
                 $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;
             }
@@ -1107,7 +1114,7 @@ class manager {
         }
 
         $complete = false;
-        $before = microtime(true);
+        $before = self::get_current_time();
         if ($timelimit) {
             $stopat = $before + $timelimit;
         }
@@ -1125,7 +1132,7 @@ class manager {
 
             // Calculate remaining time.
             $remainingtime = 0;
-            $beforeindex = microtime(true);
+            $beforeindex = self::get_current_time();
             if ($timelimit) {
                 $remainingtime = $stopat - $beforeindex;
             }
@@ -1143,7 +1150,7 @@ class manager {
                     $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) {
@@ -1163,4 +1170,17 @@ class manager {
         }
     }
 
+    /**
+     * 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);
+    }
 }
index b8d7e12..6addfea 100644 (file)
@@ -28,7 +28,8 @@ $string['errorcreatingschema'] = 'Error creating the Solr schema: {$a}';
 $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.';
index ad63e84..35ea923 100644 (file)
@@ -25,6 +25,8 @@ namespace mock_search;
  * @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 {
@@ -45,7 +47,7 @@ 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;
index 9414531..65ae8ce 100644 (file)
@@ -108,4 +108,14 @@ class testable_core_search extends \core_search\manager {
         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;
+    }
 }
index 8db3d7b..6ad9121 100644 (file)
@@ -46,6 +46,12 @@ class search_manager_testcase extends advanced_testcase {
         $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();
@@ -220,6 +226,9 @@ class search_manager_testcase extends advanced_testcase {
         // 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);
@@ -235,6 +244,7 @@ class search_manager_testcase extends advanced_testcase {
         // 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.
@@ -850,6 +860,7 @@ class search_manager_testcase extends advanced_testcase {
 
         // 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();
index 8b690ed..7393c00 100644 (file)
@@ -544,13 +544,20 @@ span.editinstructions {
 }
 
 .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 {
index 7399bfa..bc73697 100644 (file)
@@ -1,3 +1,16 @@
+.modal-header {
+    padding: 0;
+
+    .close {
+        padding: $modal-title-padding;
+        margin-top: 0;
+    }
+
+    .modal-title {
+        padding: $modal-title-padding;
+    }
+}
+
 .modal-body {
     & > .loading-icon {
         display: block;
index c306f04..e92f97a 100644 (file)
@@ -304,6 +304,14 @@ body.path-question-type {
     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%;
index aa7f3a6..c4674a8 100644 (file)
     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;
 }
index 7cdb760..235d2d0 100644 (file)
 <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">&times;</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">
index 024e920..d7ecf3f 100644 (file)
@@ -237,6 +237,12 @@ body.path-question-type .fitem_fgroup .accesshide {
     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;
 }
index 265d53e..2df2277 100644 (file)
     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;
index 515140e..f8ce4f4 100644 (file)
@@ -9316,6 +9316,12 @@ body.path-question-type .fitem_fgroup .accesshide {
   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;
 }
@@ -9897,16 +9903,30 @@ body.path-question-type .mform fieldset.hidden {
   -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;
diff --git a/theme/bootstrapbase/templates/core_calendar/event_summary_body.mustache b/theme/bootstrapbase/templates/core_calendar/event_summary_body.mustache
new file mode 100644 (file)
index 0000000..bcb3c56
--- /dev/null
@@ -0,0 +1,93 @@
+{{!
+    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>
index ef2de01..e89aab7 100644 (file)
@@ -44,6 +44,7 @@ $courseid     = optional_param('id', 0, PARAM_INT); // This are required.
 $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,
@@ -123,8 +124,10 @@ $groupid = false;
 $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);
@@ -183,11 +186,14 @@ foreach ($filtersapplied as $filter) {
 
 // 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;
@@ -196,6 +202,12 @@ if ($groupid !== false) {
     }
 }
 
+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();
index f9b47a4..22b7843 100644 (file)
 
 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.