Merge branch 'MDL-70298-master' of git://github.com/ilyatregubov/moodle
authorSara Arjona <sara@moodle.com>
Tue, 12 Jan 2021 14:58:34 +0000 (15:58 +0100)
committerSara Arjona <sara@moodle.com>
Tue, 12 Jan 2021 14:58:34 +0000 (15:58 +0100)
41 files changed:
admin/tasklogs.php
admin/tool/langimport/classes/output/langimport_page.php
admin/tool/task/classes/check/maxfaildelay.php
cohort/assign.php
contentbank/classes/contentbank.php
contentbank/classes/external/delete_content.php
contentbank/classes/external/rename_content.php
contentbank/view.php
course/modedit.php
course/tests/services_content_item_service_test.php
grade/grading/form/guide/lib.php
grade/tests/behat/grade_to_pass.feature
h5p/classes/api.php
lang/en/admin.php
lang/en/badges.php
lang/en/grades.php
lang/en/payment.php
lib/amd/build/modal.min.js
lib/amd/build/modal.min.js.map
lib/amd/build/modal_backdrop.min.js
lib/amd/build/modal_backdrop.min.js.map
lib/amd/build/tree.min.js
lib/amd/build/tree.min.js.map
lib/amd/src/modal.js
lib/amd/src/modal_backdrop.js
lib/amd/src/tree.js
lib/classes/task/manager.php
lib/myprofilelib.php
lib/outputlib.php
lib/templates/search_input.mustache
lib/tests/theme_config_test.php
mod/assign/lang/en/assign.php
mod/assign/tests/behat/steps_blind_marking.feature
mod/forum/lang/en/forum.php
mod/forum/tests/behat/grade_forum.feature
mod/quiz/lang/en/quiz.php
mod/quiz/override_form.php
mod/quiz/overridedelete.php
mod/quiz/overrides.php
mod/quiz/tests/behat/quiz_user_override.feature
payment/gateway/paypal/lang/en/paygw_paypal.php

index a91faa3..9d100c0 100644 (file)
@@ -32,6 +32,7 @@ $result = optional_param('result', null, PARAM_INT);
 
 $pageurl = new \moodle_url('/admin/tasklogs.php');
 $pageurl->param('filter', $filter);
+$pageurl->param('result', $result);
 
 $PAGE->set_url($pageurl);
 $PAGE->set_context(context_system::instance());
index 97ff743..1643f02 100644 (file)
@@ -22,8 +22,8 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 namespace tool_langimport\output;
-defined('MOODLE_INTERNAL') || die();
 
+use core_collator;
 use moodle_url;
 use renderable;
 use renderer_base;
@@ -95,6 +95,8 @@ class langimport_page implements renderable, templatable {
 
         if (!empty($this->availablelanguages)) {
             $data->toinstalloptions = [];
+
+            core_collator::asort($this->availablelanguages);
             foreach ($this->availablelanguages as $code => $language) {
                 $option = new stdClass();
                 $option->value = $code;
index 019aa75..d6be005 100644 (file)
@@ -79,6 +79,19 @@ class maxfaildelay extends check {
             }
         }
 
+        $tasks = \core\task\manager::get_failed_adhoc_tasks();
+        foreach ($tasks as $task) {
+            $faildelay = $task->get_fail_delay();
+            if ($faildelay > $maxdelay) {
+                $maxdelay = $faildelay;
+            }
+            if ($faildelay > 0) {
+                $failures++;
+                $details .= get_string('faildelay', 'tool_task') . ': ' . format_time($faildelay);
+                $details .= ' - ' .get_class($task) . " ID = " . $task->get_id() ."<br>";
+            }
+        }
+
         if ($failures > 0) {
             // Intermittent failures are not yet a warning.
             $status = result::INFO;
index 4715517..511aac5 100644 (file)
@@ -115,11 +115,14 @@ if (optional_param('remove', false, PARAM_BOOL) && confirm_sesskey()) {
       </td>
       <td id="buttonscell">
           <div id="addcontrols">
-              <input name="add" id="add" type="submit" value="<?php echo $OUTPUT->larrow().'&nbsp;'.s(get_string('add')); ?>" title="<?php p(get_string('add')); ?>" /><br />
+              <input class="btn btn-secondary" name="add" id="add" type="submit" value="<?php echo $OUTPUT->larrow() . '&nbsp;' .
+                  s(get_string('add')); ?>" title="<?php p(get_string('add')); ?>" /><br />
           </div>
 
           <div id="removecontrols">
-              <input name="remove" id="remove" type="submit" value="<?php echo s(get_string('remove')).'&nbsp;'.$OUTPUT->rarrow(); ?>" title="<?php p(get_string('remove')); ?>" />
+              <input class="btn btn-secondary" name="remove" id="remove" type="submit"
+                     value="<?php echo s(get_string('remove')) . '&nbsp;' . $OUTPUT->rarrow(); ?>"
+                     title="<?php p(get_string('remove')); ?>" />
           </div>
       </td>
       <td id="potentialcell">
@@ -128,7 +131,7 @@ if (optional_param('remove', false, PARAM_BOOL) && confirm_sesskey()) {
       </td>
     </tr>
     <tr><td colspan="3" id='backcell'>
-      <input type="submit" name="cancel" value="<?php p(get_string('backtocohorts', 'cohort')); ?>" />
+      <input class="btn btn-secondary" type="submit" name="cancel" value="<?php p(get_string('backtocohorts', 'cohort')); ?>" />
     </td></tr>
   </table>
 </div></form>
index 9152067..6374055 100644 (file)
@@ -215,8 +215,7 @@ class contentbank {
 
         $records = $DB->get_records_select('contentbank_content', $sql, $params, 'name ASC');
         foreach ($records as $record) {
-            $contentclass = "\\$record->contenttype\\content";
-            $content = new $contentclass($record);
+            $content = $this->get_content_from_id($record->id);
             if ($content->is_view_allowed()) {
                 $contents[] = $content;
             }
@@ -267,14 +266,10 @@ class contentbank {
         $result = true;
         $records = $DB->get_records('contentbank_content', ['contextid' => $context->id]);
         foreach ($records as $record) {
-            $contenttypeclass = "\\$record->contenttype\\contenttype";
-            if (class_exists($contenttypeclass)) {
-                $contenttype = new $contenttypeclass($context);
-                $contentclass = "\\$record->contenttype\\content";
-                $content = new $contentclass($record);
-                if (!$contenttype->delete_content($content)) {
-                    $result = false;
-                }
+            $content = $this->get_content_from_id($record->id);
+            $contenttype = $content->get_content_type_instance();
+            if (!$contenttype->delete_content($content)) {
+                $result = false;
             }
         }
         return $result;
@@ -293,14 +288,10 @@ class contentbank {
         $result = true;
         $records = $DB->get_records('contentbank_content', ['contextid' => $from->id]);
         foreach ($records as $record) {
-            $contenttypeclass = "\\$record->contenttype\\contenttype";
-            if (class_exists($contenttypeclass)) {
-                $contenttype = new $contenttypeclass($from);
-                $contentclass = "\\$record->contenttype\\content";
-                $content = new $contentclass($record);
-                if (!$contenttype->move_content($content, $to)) {
-                    $result = false;
-                }
+            $content = $this->get_content_from_id($record->id);
+            $contenttype = $content->get_content_type_instance();
+            if (!$contenttype->move_content($content, $to)) {
+                $result = false;
             }
         }
         return $result;
index 65846aa..caf18c2 100644 (file)
@@ -30,6 +30,7 @@ defined('MOODLE_INTERNAL') || die();
 global $CFG;
 require_once($CFG->libdir . '/externallib.php');
 
+use core_contentbank\contentbank;
 use external_api;
 use external_function_parameters;
 use external_multiple_structure;
@@ -70,34 +71,31 @@ class delete_content extends external_api {
         $warnings = [];
 
         $params = self::validate_parameters(self::execute_parameters(), ['contentids' => $contentids]);
+        $cb = new contentbank();
         foreach ($params['contentids'] as $contentid) {
             try {
                 $record = $DB->get_record('contentbank_content', ['id' => $contentid], '*', MUST_EXIST);
-                $contenttypeclass = "\\$record->contenttype\\contenttype";
-                if (class_exists($contenttypeclass)) {
-                    $context = \context::instance_by_id($record->contextid, MUST_EXIST);
-                    self::validate_context($context);
-                    $contenttype = new $contenttypeclass($context);
-                    $contentclass = "\\$record->contenttype\\content";
-                    $content = new $contentclass($record);
-                    // Check capability.
-                    if ($contenttype->can_delete($content)) {
-                        // This content can be deleted.
-                        if (!$contenttype->delete_content($content)) {
-                            $warnings[] = [
-                                'item' => $contentid,
-                                'warningcode' => 'contentnotdeleted',
-                                'message' => get_string('contentnotdeleted', 'core_contentbank')
-                            ];
-                        }
-                    } else {
-                        // The user has no permission to delete this content.
+                $content = $cb->get_content_from_id($record->id);
+                $contenttype = $content->get_content_type_instance();
+                $context = \context::instance_by_id($record->contextid, MUST_EXIST);
+                self::validate_context($context);
+                // Check capability.
+                if ($contenttype->can_delete($content)) {
+                    // This content can be deleted.
+                    if (!$contenttype->delete_content($content)) {
                         $warnings[] = [
                             'item' => $contentid,
-                            'warningcode' => 'nopermissiontodelete',
-                            'message' => get_string('nopermissiontodelete', 'core_contentbank')
+                            'warningcode' => 'contentnotdeleted',
+                            'message' => get_string('contentnotdeleted', 'core_contentbank')
                         ];
                     }
+                } else {
+                    // The user has no permission to delete this content.
+                    $warnings[] = [
+                        'item' => $contentid,
+                        'warningcode' => 'nopermissiontodelete',
+                        'message' => get_string('nopermissiontodelete', 'core_contentbank')
+                    ];
                 }
             } catch (\moodle_exception $e) {
                 // The content or the context don't exist.
index 5cd9d75..1374aae 100644 (file)
@@ -29,6 +29,7 @@ defined('MOODLE_INTERNAL') || die();
 global $CFG;
 require_once($CFG->libdir . '/externallib.php');
 
+use core_contentbank\contentbank;
 use external_api;
 use external_function_parameters;
 use external_single_structure;
@@ -76,35 +77,33 @@ class rename_content extends external_api {
             'name' => $name,
         ]);
         $params['name'] = clean_param($params['name'], PARAM_TEXT);
-        try {
-            $record = $DB->get_record('contentbank_content', ['id' => $contentid], '*', MUST_EXIST);
-            $contenttypeclass = "\\$record->contenttype\\contenttype";
-            if (class_exists($contenttypeclass)) {
+
+        // If name is empty don't try to rename and return a more detailed message.
+        if (empty(trim($params['name']))) {
+            $warnings[] = [
+                'item' => $contentid,
+                'warningcode' => 'emptynamenotallowed',
+                'message' => get_string('emptynamenotallowed', 'core_contentbank')
+            ];
+        } else {
+            try {
+                $record = $DB->get_record('contentbank_content', ['id' => $contentid], '*', MUST_EXIST);
+                $cb = new contentbank();
+                $content = $cb->get_content_from_id($record->id);
+                $contenttype = $content->get_content_type_instance();
                 $context = \context::instance_by_id($record->contextid, MUST_EXIST);
                 self::validate_context($context);
-                $contenttype = new $contenttypeclass($context);
-                $contentclass = "\\$record->contenttype\\content";
-                $content = new $contentclass($record);
                 // Check capability.
                 if ($contenttype->can_manage($content)) {
-                    if (empty(trim($name))) {
-                        // If name is empty don't try to rename and return a more detailed message.
+                    // This content can be renamed.
+                    if ($contenttype->rename_content($content, $params['name'])) {
+                        $result = true;
+                    } else {
                         $warnings[] = [
                             'item' => $contentid,
-                            'warningcode' => 'emptynamenotallowed',
-                            'message' => get_string('emptynamenotallowed', 'core_contentbank')
+                            'warningcode' => 'contentnotrenamed',
+                            'message' => get_string('contentnotrenamed', 'core_contentbank')
                         ];
-                    } else {
-                        // This content can be renamed.
-                        if ($contenttype->rename_content($content, $params['name'])) {
-                            $result = true;
-                        } else {
-                            $warnings[] = [
-                                'item' => $contentid,
-                                'warningcode' => 'contentnotrenamed',
-                                'message' => get_string('contentnotrenamed', 'core_contentbank')
-                            ];
-                        }
                     }
                 } else {
                     // The user has no permission to manage this content.
@@ -114,14 +113,14 @@ class rename_content extends external_api {
                         'message' => get_string('nopermissiontomanage', 'core_contentbank')
                     ];
                 }
+            } catch (\moodle_exception $e) {
+                // The content or the context don't exist.
+                $warnings[] = [
+                    'item' => $contentid,
+                    'warningcode' => 'exception',
+                    'message' => $e->getMessage()
+                ];
             }
-        } catch (\moodle_exception $e) {
-            // The content or the context don't exist.
-            $warnings[] = [
-                'item' => $contentid,
-                'warningcode' => 'exception',
-                'message' => $e->getMessage()
-            ];
         }
 
         return [
index 4c84b1b..46daffb 100644 (file)
@@ -58,13 +58,9 @@ $title .= ": ".$record->name;
 $PAGE->set_title($title);
 $PAGE->set_pagetype('contentbank');
 
-$contenttypeclass = "\\$record->contenttype\\contenttype";
-$contentclass = "\\$record->contenttype\\content";
-if (!class_exists($contenttypeclass) || !class_exists($contentclass)) {
-    print_error('contenttypenotfound', 'error', $returnurl, $record->contenttype);
-}
-$contenttype = new $contenttypeclass($context);
-$content = new $contentclass($record);
+$cb = new \core_contentbank\contentbank();
+$content = $cb->get_content_from_id($record->id);
+$contenttype = $content->get_content_type_instance();
 
 // Create the cog menu with all the secondary actions, such as delete, rename...
 $actionmenu = new action_menu();
index 4fa0524..71cc310 100644 (file)
@@ -60,6 +60,15 @@ if (!empty($add)) {
     // will be the closest match we have.
     navigation_node::override_active_url(course_get_url($course, $section));
 
+    // MDL-69431 Validate that $section (url param) does not exceed the maximum for this course / format.
+    // If too high (e.g. section *id* not number) non-sequential sections inserted in course_sections table.
+    // Then on import, backup fills 'gap' with empty sections (see restore_rebuild_course_cache). Avoid this.
+    $courseformat = course_get_format($course);
+    $maxsections = $courseformat->get_max_sections();
+    if ($section > $maxsections) {
+        print_error('maxsectionslimit', 'moodle', '', $maxsections);
+    }
+
     list($module, $context, $cw, $cm, $data) = prepare_new_moduleinfo_data($course, $add, $section);
     $data->return = 0;
     $data->sr = $sectionreturn;
index f1924fb..f838265 100644 (file)
@@ -121,7 +121,8 @@ class services_content_item_service_testcase extends \advanced_testcase {
 
         // The call to get_all_content_items() should return the same items as for the course,
         // given the user in an editing teacher and can add manual lti instances.
-        $this->assertEquals(array_column($allcontentitems, 'name'), array_column($coursecontentitems, 'name'));
+        $this->assertContains('lti', array_column($coursecontentitems, 'name'));
+        $this->assertContains('lti', array_column($allcontentitems, 'name'));
 
         // Now removing the cap 'mod/lti:addinstance'. This will restrict those items returned by the course-specific method.
         $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
index f878c5e..371a5ff 100644 (file)
@@ -657,8 +657,9 @@ class gradingform_guide_controller extends gradingform_controller {
         }
         $returnvalue['maxscore'] = $maxscore;
         $returnvalue['minscore'] = 0;
-        if (!empty($this->moduleinstance->grade)) {
-            $graderange = make_grades_menu($this->moduleinstance->grade);
+        $fieldname = \core_grades\component_gradeitems::get_field_name_for_itemname($this->component, $this->area, 'grade');
+        if (!empty($this->moduleinstance->{$fieldname})) {
+            $graderange = make_grades_menu($this->moduleinstance->{$fieldname});
             $returnvalue['modulegrade'] = count($graderange) - 1;
         }
         return $returnvalue;
index 2cec4d0..0be6180 100644 (file)
@@ -209,7 +209,7 @@ Feature: We can set the grade to pass value
       | Ratings > Grade to pass  | 90                 |
     And I navigate to "View > Grader report" in the course gradebook
     And I turn editing mode on
-    And I click on "Edit  forum Rating grade for Test Forum 1" "link"
+    And I click on "Edit  forum Test Forum 1 rating" "link"
     And I expand all fieldsets
     Then the field "Grade to pass" matches value "90"
     And I set the field "Grade to pass" to "80"
index cab3476..d864e79 100644 (file)
@@ -194,7 +194,10 @@ class api {
         global $DB;
 
         // Deconstruct the URL and get the pathname associated.
-        $pathnamehash = self::get_pluginfile_hash($url, $preventredirect);
+        if (self::can_access_pluginfile_hash($url, $preventredirect)) {
+            $pathnamehash = self::get_pluginfile_hash($url);
+        }
+
         if (!$pathnamehash) {
             return [false, false];
         }
@@ -337,22 +340,28 @@ class api {
      * @param factory $factory The \core_h5p\factory object
      */
     public static function delete_content_from_pluginfile_url(string $url, factory $factory): void {
+        global $DB;
+
         // Get the H5P to delete.
-        list($file, $h5p) = self::get_content_from_pluginfile_url($url);
+        $pathnamehash = self::get_pluginfile_hash($url);
+        $h5p = $DB->get_record('h5p', ['pathnamehash' => $pathnamehash]);
         if ($h5p) {
             self::delete_content($h5p, $factory);
         }
     }
 
     /**
-     * Get the pathnamehash from an H5P internal URL.
+     * If user can access pathnamehash from an H5P internal URL.
      *
      * @param  string $url H5P pluginfile URL poiting to an H5P file.
      * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions
      *
-     * @return string|false pathnamehash for the file in the internal URL.
+     * @return bool if user can access pluginfile hash.
+     * @throws \moodle_exception
+     * @throws \coding_exception
+     * @throws \require_login_exception
      */
-    protected static function get_pluginfile_hash(string $url, bool $preventredirect = true) {
+    protected static function can_access_pluginfile_hash(string $url, bool $preventredirect = true): bool {
         global $USER, $CFG;
 
         // Decode the URL before start processing it.
@@ -365,7 +374,6 @@ class api {
         // We only need the slasharguments.
         $path = substr($path, strpos($path, '.php/') + 5);
         $parts = explode('/', $path);
-        $filename = array_pop($parts);
 
         // If the request is made by tokenpluginfile.php we need to avoid userprivateaccesskey.
         if (strpos($url, '/tokenpluginfile.php')) {
@@ -377,11 +385,6 @@ class api {
         $component = array_shift($parts);
         $filearea = array_shift($parts);
 
-        // Ignore draft files, because they are considered temporary files, so shouldn't be displayed.
-        if ($filearea == 'draft') {
-            return false;
-        }
-
         // Get the context.
         try {
             list($context, $course, $cm) = get_context_info_array($contextid);
@@ -415,7 +418,7 @@ class api {
                     $parentcontext = $context->get_parent_context();
                     if ($parentcontext->contextlevel === CONTEXT_COURSECAT) {
                         // Check if category is visible and user can view this category.
-                        if (!core_course_category::get($parentcontext->instanceid, IGNORE_MISSING)) {
+                        if (!\core_course_category::get($parentcontext->instanceid, IGNORE_MISSING)) {
                             send_file_not_found();
                         }
                     } else if ($parentcontext->contextlevel === CONTEXT_USER && $parentcontext->instanceid != $USER->id) {
@@ -431,7 +434,7 @@ class api {
             // For CONTEXT_MODULE and CONTEXT_COURSE check if the user is enrolled in the course.
             // And for CONTEXT_MODULE has permissions view this .h5p file.
             if ($context->contextlevel == CONTEXT_MODULE ||
-                    $context->contextlevel == CONTEXT_COURSE) {
+                $context->contextlevel == CONTEXT_COURSE) {
                 // Require login to the course first (without login to the module).
                 require_course_login($course, true, null, !$preventredirect, $preventredirect);
 
@@ -448,6 +451,54 @@ class api {
             }
         }
 
+        return true;
+    }
+
+    /**
+     * Get the pathnamehash from an H5P internal URL.
+     *
+     * @param  string $url H5P pluginfile URL poiting to an H5P file.
+     *
+     * @return string|false pathnamehash for the file in the internal URL.
+     *
+     * @throws \moodle_exception
+     */
+    protected static function get_pluginfile_hash(string $url) {
+
+        // Decode the URL before start processing it.
+        $url = new \moodle_url(urldecode($url));
+
+        // Remove params from the URL (such as the 'forcedownload=1'), to avoid errors.
+        $url->remove_params(array_keys($url->params()));
+        $path = $url->out_as_local_url();
+
+        // We only need the slasharguments.
+        $path = substr($path, strpos($path, '.php/') + 5);
+        $parts = explode('/', $path);
+        $filename = array_pop($parts);
+
+        // If the request is made by tokenpluginfile.php we need to avoid userprivateaccesskey.
+        if (strpos($url, '/tokenpluginfile.php')) {
+            array_shift($parts);
+        }
+
+        // Get the contextid, component and filearea.
+        $contextid = array_shift($parts);
+        $component = array_shift($parts);
+        $filearea = array_shift($parts);
+
+        // Ignore draft files, because they are considered temporary files, so shouldn't be displayed.
+        if ($filearea == 'draft') {
+            return false;
+        }
+
+        // Get the context.
+        try {
+            list($context, $course, $cm) = get_context_info_array($contextid);
+        } catch (\moodle_exception $e) {
+            throw new \moodle_exception('invalidcontextid', 'core_h5p');
+        }
+
         // Some components, such as mod_page or mod_resource, add the revision to the URL to prevent caching problems.
         // So the URL contains this revision number as itemid but a 0 is always stored in the files table.
         // In order to get the proper hash, a callback should be done (looking for those exceptions).
index 92781b5..65fa702 100644 (file)
@@ -505,7 +505,7 @@ $string['displayloginfailures'] = 'Display login failures';
 $string['divertallemails'] = 'Email diverting';
 $string['divertallemailsdetail'] = 'Used as a safeguard in development environments when testing emails and should not be used in production.';
 $string['divertallemailsexcept'] = 'Email diversion exceptions';
-$string['divertallemailsexcept_desc'] = 'A list of email exception rules separated by either commas or new lines. Each rule is interpreted as a regular expression, eg<pre>simone@acme.com
+$string['divertallemailsexcept_desc'] = 'A list of email exception rules separated by either commas or new lines. Each rule is interpreted as a regular expression e.g. <pre>simone@acme.com
 .*@acme.com
 fred(\\+.*)?@acme.com
 </pre>';
index 51b61a6..ccdfd36 100644 (file)
@@ -136,7 +136,7 @@ In this area, you can select collections of badges from your backpack that you w
 $string['backpacksettings'] = 'Backpack settings';
 $string['backpackapiurl'] = 'Backpack API URL';
 $string['backpackweburl'] = 'Backpack URL';
-$string['backpackprovider'] = 'Backpack Provider';
+$string['backpackprovider'] = 'Backpack provider';
 $string['badges'] = 'Badges';
 $string['badgedetails'] = 'Badge details';
 $string['badgeimage'] = 'Image';
index e6a73ae..9f5f867 100644 (file)
@@ -136,7 +136,7 @@ $string['coursesiamtaking'] = 'Courses I am taking';
 $string['coursesiamteaching'] = 'Courses I am teaching';
 $string['coursescales'] = 'Course scales';
 $string['coursesettings'] = 'Course settings';
-$string['coursesettingsexplanation'] = 'Course settings determine how the gradebook appears for all participants in the course.';
+$string['coursesettingsexplanation'] = 'Course grade settings determine how the gradebook appears for all participants in the course.';
 $string['coursetotal'] = 'Course total';
 $string['createcategory'] = 'Create category';
 $string['createcategoryerror'] = 'Could not create a new category';
index 1081158..f084db5 100644 (file)
@@ -28,9 +28,9 @@ $string['accountconfignote'] = 'Payment gateways for this account will be config
 $string['accountidnumber'] = 'ID number';
 $string['accountidnumber_help'] = 'The ID number is only used when matching the account against external systems and is not displayed anywhere on the site. If the account has an official code name it may be entered, otherwise the field can be left blank.';
 $string['accountname'] = 'Account name';
-$string['accountname_help'] = 'How this account will be identified for teachers or managers who set up payments (for example in the course enrolment plugin)';
+$string['accountname_help'] = 'How this account will be identified for teachers or managers who set up payments (for example in the course enrolment plugin).';
 $string['accountnotavailable'] = 'Not available';
-$string['paymentaccountsexplained'] = 'Create one or multiple payment accounts for this site. Each account includes configuration for available payment gateways. The person who configures payments on the site (for example, payment for the course enrolment) will be able to chose from the available accounts.';
+$string['paymentaccountsexplained'] = 'Create one or multiple payment accounts for this site. Each account includes configuration for available payment gateways. The person who configures payments on the site (for example, payment for the course enrolment) will be able to choose from the available accounts.';
 $string['createaccount'] = 'Create payment account';
 $string['deleteorarchive'] = 'Delete or archive';
 $string['eventaccountcreated'] = 'Payment account created';
index 91677e8..dfa48ea 100644 (file)
Binary files a/lib/amd/build/modal.min.js and b/lib/amd/build/modal.min.js differ
index e330a08..0e682d0 100644 (file)
Binary files a/lib/amd/build/modal.min.js.map and b/lib/amd/build/modal.min.js.map differ
index 1778e05..5ebc430 100644 (file)
Binary files a/lib/amd/build/modal_backdrop.min.js and b/lib/amd/build/modal_backdrop.min.js differ
index df2f559..057e7c1 100644 (file)
Binary files a/lib/amd/build/modal_backdrop.min.js.map and b/lib/amd/build/modal_backdrop.min.js.map differ
index d92786b..ea1b92d 100644 (file)
Binary files a/lib/amd/build/tree.min.js and b/lib/amd/build/tree.min.js differ
index 3eed2c1..a78d746 100644 (file)
Binary files a/lib/amd/build/tree.min.js.map and b/lib/amd/build/tree.min.js.map differ
index 118d679..ea2defa 100644 (file)
@@ -102,6 +102,8 @@ define([
         this.bodyJS = null;
         this.footerJS = null;
         this.modalCount = modalCounter++;
+        this.attachmentPoint = document.createElement('div');
+        document.body.append(this.attachmentPoint);
 
         if (!this.root.is(SELECTORS.CONTAINER)) {
             Notification.exception({message: 'Element is not a modal container'});
@@ -648,7 +650,7 @@ define([
      * @returns {jQuery}
      */
     Modal.prototype.getAttachmentPoint = function() {
-        return $(Fullscreen.getElement() || document.body);
+        return $(Fullscreen.getElement() || this.attachmentPoint);
     };
 
     /**
@@ -753,6 +755,7 @@ define([
         this.hide();
         this.root.remove();
         this.root.trigger(ModalEvents.destroyed, this);
+        this.attachmentPoint.remove();
     };
 
     /**
@@ -797,7 +800,11 @@ define([
             }
 
             if (e.keyCode == KeyCodes.escape) {
-                this.hide();
+                if (this.removeOnClose) {
+                    this.destroy();
+                } else {
+                    this.hide();
+                }
             }
         }.bind(this));
 
index 1cbc1fd..7cdd2d1 100644 (file)
@@ -37,6 +37,8 @@ define(['jquery', 'core/templates', 'core/notification', 'core/fullscreen'],
     var ModalBackdrop = function(root) {
         this.root = $(root);
         this.isAttached = false;
+        this.attachmentPoint = document.createElement('div');
+        document.body.append(this.attachmentPoint);
 
         if (!this.root.is(SELECTORS.ROOT)) {
             Notification.exception({message: 'Element is not a modal backdrop'});
@@ -59,7 +61,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core/fullscreen'],
       * @returns {jQuery}
       */
      ModalBackdrop.prototype.getAttachmentPoint = function() {
-         return $(Fullscreen.getElement() || document.body);
+         return $(Fullscreen.getElement() || this.attachmentPoint);
      };
 
     /**
@@ -155,6 +157,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core/fullscreen'],
      */
     ModalBackdrop.prototype.destroy = function() {
         this.root.remove();
+        this.attachmentPoint.remove();
     };
 
     return ModalBackdrop;
index 4ba8615..1a97cac 100644 (file)
@@ -349,12 +349,12 @@ define(['jquery'], function($) {
      * Handle a key down event - ie navigate the tree.
      *
      * @method handleKeyDown
-     * @param {Object} item is the jquery id of the parent item of the group.
      * @param {Event} e The event.
      */
      // This function should be simplified. In the meantime..
      // eslint-disable-next-line complexity
-    Tree.prototype.handleKeyDown = function(item, e) {
+    Tree.prototype.handleKeyDown = function(e) {
+        var item = $(e.target);
         var currentIndex = this.getVisibleItems().index(item);
 
         if ((e.altKey || e.ctrlKey || e.metaKey) || (e.shiftKey && e.keyCode != this.keys.tab)) {
@@ -483,16 +483,20 @@ define(['jquery'], function($) {
      * Handle a click (select).
      *
      * @method handleClick
-     * @param {Object} item The jquery id of the parent item of the group.
      * @param {Event} e The event.
      */
-    Tree.prototype.handleClick = function(item, e) {
-
+    Tree.prototype.handleClick = function(e) {
         if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
             // Do nothing.
             return;
         }
 
+        var item = $(e.target);
+
+        if (e.target !== e.currentTarget) {
+            return;
+        }
+
         // Update the active item.
         item.focus();
 
@@ -506,12 +510,10 @@ define(['jquery'], function($) {
      * Handle a focus event.
      *
      * @method handleFocus
-     * @param {Object} item The jquery id of the parent item of the group.
      * @param {Event} e The event.
      */
-    Tree.prototype.handleFocus = function(item) {
-
-        this.setActiveItem(item);
+    Tree.prototype.handleFocus = function(e) {
+        this.setActiveItem($(e.target));
     };
 
     /**
@@ -520,20 +522,12 @@ define(['jquery'], function($) {
      * @method bindEventHandlers
      */
     Tree.prototype.bindEventHandlers = function() {
-        var thisObj = this;
-
         // Bind event handlers to the tree items. Use event delegates to allow
         // for dynamically loaded parts of the tree.
         this.treeRoot.on({
-            click: function(e) {
-              return thisObj.handleClick($(this), e);
-            },
-            keydown: function(e) {
-              return thisObj.handleKeyDown($(this), e);
-            },
-            focus: function() {
-              return thisObj.handleFocus($(this));
-            },
+            click: this.handleClick.bind(this),
+            keydown: this.handleKeyDown.bind(this),
+            focus: this.handleFocus.bind(this),
         }, SELECTORS.ITEM);
     };
 
index befe2af..8a7cddd 100644 (file)
@@ -512,6 +512,27 @@ class manager {
         return $tasks;
     }
 
+    /**
+     * This function will return a list of all adhoc tasks that have a faildelay
+     *
+     * @param int $delay filter how long the task has been delayed
+     * @return \core\task\adhoc_task[]
+     */
+    public static function get_failed_adhoc_tasks(int $delay = 0): array {
+        global $DB;
+
+        $tasks = [];
+        $records = $DB->get_records_sql('SELECT * from {task_adhoc} WHERE faildelay > ?', [$delay]);
+
+        foreach ($records as $record) {
+            $task = self::adhoc_task_from_record($record);
+            if ($task) {
+                $tasks[] = $task;
+            }
+        }
+        return $tasks;
+    }
+
     /**
      * Ensure quality of service for the ad hoc task queue.
      *
index 9b99d8b..044d559 100644 (file)
@@ -339,34 +339,19 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
     }
 
     if ($user->skype && !isset($hiddenfields['skypeid'])) {
-        $imurl = 'skype:'.urlencode($user->skype).'?call';
-        $iconurl = new moodle_url('http://mystatus.skype.com/smallicon/'.urlencode($user->skype));
-        if (is_https()) {
-            // Bad luck, skype devs are lazy to set up SSL on their servers - see MDL-37233.
-            $statusicon = '';
-        } else {
-            $statusicon = html_writer::empty_tag('img',
-                array('src' => $iconurl, 'class' => 'icon icon-post', 'alt' => get_string('status')));
-        }
-
+        $imurl = 'skype:' . urlencode($user->skype) . '?call';
         $node = new core_user\output\myprofile\node('contact', 'skypeid', get_string('skypeid'), null, null,
-            html_writer::link($imurl, s($user->skype) . $statusicon));
+            html_writer::link($imurl, s($user->skype)));
         $tree->add_node($node);
     }
     if ($user->yahoo && !isset($hiddenfields['yahooid'])) {
-        $imurl = new moodle_url('https://edit.yahoo.com/config/send_webmesg', array('.target' => $user->yahoo, '.src' => 'pg'));
-        $iconurl = new moodle_url('http://opi.yahoo.com/online', array('u' => $user->yahoo, 'm' => 'g', 't' => '0'));
-        $statusicon = html_writer::tag('img', '',
-            array('src' => $iconurl, 'class' => 'iconsmall icon-post', 'alt' => get_string('status')));
-
         $node = new core_user\output\myprofile\node('contact', 'yahooid', get_string('yahooid'), null, null,
-            html_writer::link($imurl, s($user->yahoo) . $statusicon));
+            s($user->yahoo));
         $tree->add_node($node);
     }
     if ($user->aim && !isset($hiddenfields['aimid'])) {
-        $imurl = 'aim:goim?screenname='.urlencode($user->aim);
         $node = new core_user\output\myprofile\node('contact', 'aimid', get_string('aimid'), null, null,
-            html_writer::link($imurl, s($user->aim)));
+            s($user->aim));
         $tree->add_node($node);
     }
     if ($user->msn && !isset($hiddenfields['msnid'])) {
index 9f533ca..bfc93a0 100644 (file)
@@ -2451,22 +2451,22 @@ class theme_config {
      * @return string
      */
     protected function get_region_name($region, $theme) {
-        $regionstring = get_string('region-' . $region, 'theme_' . $theme);
-        // A name exists in this theme, so use it
-        if (substr($regionstring, 0, 1) != '[') {
-            return $regionstring;
+
+        $stringman = get_string_manager();
+
+        // Check if the name is defined in the theme.
+        if ($stringman->string_exists('region-' . $region, 'theme_' . $theme)) {
+            return get_string('region-' . $region, 'theme_' . $theme);
         }
 
-        // Otherwise, try to find one elsewhere
-        // Check parents, if any
+        // Check the theme parents.
         foreach ($this->parents as $parentthemename) {
-            $regionstring = get_string('region-' . $region, 'theme_' . $parentthemename);
-            if (substr($regionstring, 0, 1) != '[') {
-                return $regionstring;
+            if ($stringman->string_exists('region-' . $region, 'theme_' . $parentthemename)) {
+                return get_string('region-' . $region, 'theme_' . $parentthemename);
             }
         }
 
-        // Last resort, try the boost theme for names
+        // Last resort, try the boost theme for names.
         return get_string('region-' . $region, 'theme_boost');
     }
 
index f494a26..400a62e 100644 (file)
@@ -37,9 +37,9 @@
         ]
     }
 }}
-<div class="simplesearchform {{{ extraclasses }}}">
+<div class="simplesearchform {{ extraclasses }}">
     {{^inform}}
-    <form autocomplete="off" action="{{{ action }}}" method="get" accept-charset="utf-8" class="mform form-inline simplesearchform">
+    <form autocomplete="off" action="{{ action }}" method="get" accept-charset="utf-8" class="mform form-inline simplesearchform">
     {{/inform}}
     {{#hiddenfields}}
         <input type="hidden" name="{{ name }}" value="{{ value }}">
         <input type="text"
            id="searchinput-{{uniqid}}"
            class="form-control"
-           placeholder="{{{ searchstring }}}"
-           aria-label="{{{ searchstring }}}"
-           name="{{{ inputname }}}"
+           placeholder="{{ searchstring }}"
+           aria-label="{{ searchstring }}"
+           name="{{ inputname }}"
            data-region="input"
            autocomplete="off"
-           value="{{{ query }}}"
+           value="{{ query }}"
         >
         <div class="input-group-append">
-            <button type="submit" class="btn {{^btnclass}}btn-submit{{/btnclass}} {{{ btnclass }}} search-icon">
+            <button type="submit" class="btn {{^btnclass}}btn-submit{{/btnclass}} {{ btnclass }} search-icon">
                 {{#pix}} a/search, core {{/pix}}
-                <span class="sr-only">{{{ searchstring }}}</span>
+                <span class="sr-only">{{ searchstring }}</span>
             </button>
         </div>
 
index 35f3df7..3308d07 100644 (file)
@@ -213,4 +213,16 @@ class core_theme_config_testcase extends advanced_testcase {
 
         $this->assertEquals($cssexpected, $cssactual);
     }
+
+    /**
+     * Test that {@see theme_config::get_all_block_regions()} returns localised list of region names.
+     */
+    public function test_get_all_block_regions() {
+        $this->resetAfterTest();
+
+        $theme = theme_config::load(theme_config::DEFAULT_THEME);
+        $regions = $theme->get_all_block_regions();
+
+        $this->assertEquals('Right', $regions['side-pre']);
+    }
 }
index e420633..a9305a0 100644 (file)
@@ -55,7 +55,7 @@ $string['assign:receivegradernotifications'] = 'Receive grader submission notifi
 $string['assign:releasegrades'] = 'Release grades';
 $string['assign:revealidentities'] = 'Reveal student identities';
 $string['assign:reviewgrades'] = 'Review grades';
-$string['assign:viewblinddetails'] = 'View student identities when blind marking is enabled';
+$string['assign:viewblinddetails'] = 'View student identities when anonymous submissions are enabled';
 $string['assign:viewgrades'] = 'View grades';
 $string['assign:showhiddengrader'] = 'See the identity of a hidden grader';
 $string['assign:submit'] = 'Submit assignment';
@@ -109,9 +109,9 @@ $string['batchoperationunlock'] = 'unlock submissions';
 $string['batchoperationreverttodraft'] = 'revert submissions to draft';
 $string['batchsetallocatedmarker'] = 'Set allocated marker for {$a} selected user(s).';
 $string['batchsetmarkingworkflowstateforusers'] = 'Set marking workflow state for {$a} selected user(s).';
-$string['blindmarking'] = 'Blind marking';
-$string['blindmarkingenabledwarning'] = 'Blind marking is enabled for this activity.';
-$string['blindmarking_help'] = 'Blind marking hides the identity of students from markers. Blind marking settings will be locked once a submission or grade has been made in relation to this assignment.';
+$string['blindmarking'] = 'Anonymous submissions';
+$string['blindmarkingenabledwarning'] = 'Anonymous submissions are enabled for this activity.';
+$string['blindmarking_help'] = 'Anonymous submissions hide the identity of students from markers. Anonymous submission settings will be locked once a submission or grade has been made in relation to this assignment.';
 $string['calendardue'] = '{$a} is due';
 $string['calendargradingdue'] = '{$a} is due to be graded';
 $string['changeuser'] = 'Change user';
@@ -414,7 +414,7 @@ $string['preventsubmissions'] = 'Prevent the user from making any more submissio
 $string['preventsubmissionsshort'] = 'Prevent submission changes';
 $string['previous'] = 'Previous';
 $string['privacy:attemptpath'] = 'attempt {$a}';
-$string['privacy:blindmarkingidentifier'] = 'The identifier used for blind marking';
+$string['privacy:blindmarkingidentifier'] = 'The identifier used for anonymous submissions';
 $string['privacy:gradepath'] = 'grade';
 $string['privacy:metadata:assigndownloadasfolders'] = 'A user preference for whether multiple file submissions should be downloaded into folders';
 $string['privacy:metadata:assignfeedbackpluginsummary'] = 'Feedback data for the assignment.';
@@ -429,7 +429,7 @@ $string['privacy:metadata:assignquickgrading'] = 'A preference as to whether qui
 $string['privacy:metadata:assignsubmissiondetail'] = 'Stores user submission information';
 $string['privacy:metadata:assignsubmissionpluginsummary'] = 'Submission data for the assignment.';
 $string['privacy:metadata:assignuserflags'] = 'Stores user meta data such as extension dates';
-$string['privacy:metadata:assignusermapping'] = 'The mapping for blind marking';
+$string['privacy:metadata:assignusermapping'] = 'The mapping for anonymous submissions';
 $string['privacy:metadata:assignworkflowfilter'] = 'Filter by the different workflow stages.';
 $string['privacy:metadata:grade'] = 'The numerical grade for this assignment submission. Can be determined by scales/advancedgradingforms etc but will always be converted back to a floating point number.';
 $string['privacy:metadata:grader'] = 'The user ID of the person grading.';
@@ -448,7 +448,7 @@ $string['relativedatessubmissionduedateafter'] = '{$a->datediffstr} after course
 $string['relativedatessubmissionduedatebefore'] = '{$a->datediffstr} before course start';
 $string['removeallgroupoverrides'] = 'Delete all group overrides';
 $string['removealluseroverrides'] = 'Delete all user overrides';
-$string['reopenuntilpassincompatiblewithblindmarking'] = 'Reopen until pass option is incompatible with blind marking, because the grades are not released to the gradebook until the student identities are revealed.';
+$string['reopenuntilpassincompatiblewithblindmarking'] = 'Reopen until pass option is incompatible with anonymous submissions, because the grades are not released to the gradebook until the student identities are revealed.';
 $string['requiresubmissionstatement'] = 'Require that students accept the submission statement';
 $string['requiresubmissionstatement_help'] = 'Require that students accept the submission statement for all submissions to this assignment.';
 $string['requireallteammemberssubmit'] = 'Require all group members submit';
index 3ed5786..87f1c8d 100644 (file)
@@ -26,7 +26,7 @@ Feature: Assignments correctly add feedback to the grade report when workflow an
       | Online text | 1 |
       | File submissions | 0 |
       | Use marking workflow | Yes |
-      | Blind marking | Yes |
+      | Anonymous submissions | Yes |
     And I log out
     # Add a submission.
     And I log in as "student1"
index fd08a5d..29dcbf2 100644 (file)
@@ -762,8 +762,8 @@ $string['gradeforwholeforum'] = 'Grade for forum: {$a->str_long_grade}';
 $string['grading'] = 'Grading';
 $string['gradingstatus'] = 'Grade status:';
 $string['gradeforwholeforumhidden'] = 'Grade for forum hidden';
-$string['gradeitemnameforwholeforum'] = 'Whole forum grade for {$a->name}';
-$string['gradeitemnameforrating'] = 'Rating grade for {$a->name}';
+$string['gradeitemnameforwholeforum'] = '{$a->name} whole forum';
+$string['gradeitemnameforrating'] = '{$a->name} rating';
 $string['grades:gradesavedfor'] = 'Grade saved for {$a->fullname}';
 $string['grades:gradesavefailed'] = 'Unable to save grade for {$a->fullname}: {$a->error}';
 $string['notgraded'] = 'Not graded';
index 5d8e72a..adc1036 100644 (file)
@@ -63,11 +63,11 @@ Feature: I can grade a students interaction across a forum
     And I turn editing mode on
 
     # There shouldn't be any Ratings grade item.
-    Then I should see "Whole forum grade"
-    But I should not see "Rating grade"
+    Then I should see "Test Forum 1 whole forum"
+    But I should not see "Test Forum 1 rating"
 
     # The values saved should be reflected here.
-    Given I click on "Edit  forum Whole forum grade for Test Forum 1" "link"
+    Given I click on "Edit  forum Test Forum 1 whole forum" "link"
     When I expand all fieldsets
     Then the field "Maximum grade" matches value "10"
     Then the field "Grade to pass" matches value "4"
@@ -120,11 +120,11 @@ Feature: I can grade a students interaction across a forum
     And I turn editing mode on
 
     # There shouldn't be any Whole forum grade gradeitem.
-    Then I should see "Rating grade"
-    But I should not see "Whole forum grade"
+    Then I should see "Test Forum 1 rating"
+    But I should not see "Test Forum 1 whole forum"
 
     # The values saved should be reflected here.
-    Given I click on "Edit  forum Rating grade for Test Forum 1" "link"
+    Given I click on "Edit  forum Test Forum 1 rating" "link"
     When I expand all fieldsets
     Then the field "Maximum grade" matches value "10"
     Then the field "Grade to pass" matches value "4"
@@ -151,18 +151,18 @@ Feature: I can grade a students interaction across a forum
     And I turn editing mode on
 
     # There shouldn't be any Whole forum grade gradeitem.
-    Then I should see "Rating grade"
-    And I should see "Whole forum grade"
+    Then I should see "Test Forum 1 rating"
+    And I should see "Test Forum 1 whole forum"
 
     # The values saved should be reflected here.
-    Given I click on "Edit  forum Rating grade for Test Forum 1" "link"
+    Given I click on "Edit  forum Test Forum 1 rating" "link"
     When I expand all fieldsets
     Then the field "Maximum grade" matches value "100"
     Then the field "Grade to pass" matches value "40"
     And I should see "Peers" in the "Parent category" "fieldset"
     And I press "cancel"
 
-    Given I click on "Edit  forum Whole forum grade for Test Forum 1" "link"
+    Given I click on "Edit  forum Test Forum 1 whole forum" "link"
     When I expand all fieldsets
     Then the field "Maximum grade" matches value "10"
     Then the field "Grade to pass" matches value "4"
index 714926d..95d73f7 100644 (file)
@@ -179,7 +179,7 @@ $string['comment'] = 'Comment';
 $string['commentorgrade'] = 'Make comment or override grade';
 $string['comments'] = 'Comments';
 $string['completedon'] = 'Completed on';
-$string['completionminattempts'] = 'Student must send attempts:';
+$string['completionminattempts'] = 'Minimum number of attempts:';
 $string['completionminattemptsgroup'] = 'Require attempts';
 $string['completionminattemptserror'] = 'Minimum number of attempts must be lower or equal to attempts allowed.';
 $string['completionpass'] = 'Require passing grade';
index 7561c13..2036cae 100644 (file)
@@ -123,11 +123,12 @@ class quiz_override_form extends moodleform {
             }
         } else {
             // User override.
+            $extrauserfields = get_extra_user_fields($this->context);
             if ($this->userid) {
                 // There is already a userid, so freeze the selector.
-                $user = $DB->get_record('user', array('id'=>$this->userid));
+                $user = $DB->get_record('user', ['id' => $this->userid]);
                 $userchoices = array();
-                $userchoices[$this->userid] = fullname($user);
+                $userchoices[$this->userid] = $this->display_user_name($user, $extrauserfields);
                 $mform->addElement('select', 'userid',
                         get_string('overrideuser', 'quiz'), $userchoices);
                 $mform->freeze('userid');
@@ -142,14 +143,13 @@ class quiz_override_form extends moodleform {
                 }
 
                 // Get the list of appropriate users, depending on whether and how groups are used.
+                $userfields = user_picture::fields('u', $extrauserfields, 'userid');
                 if ($accessallgroups) {
                     $users = get_users_by_capability($this->context, 'mod/quiz:attempt',
-                            'u.id, u.email, ' . get_all_user_name_fields(true, 'u'),
-                            $sort);
+                            $userfields, $sort);
                 } else if ($groups = groups_get_activity_allowed_groups($cm)) {
                     $users = get_users_by_capability($this->context, 'mod/quiz:attempt',
-                            'u.id, u.email, ' . get_all_user_name_fields(true, 'u'),
-                            $sort, '', '', array_keys($groups));
+                            $userfields, $sort, '', '', array_keys($groups));
                 }
 
                 // Filter users based on any fixed restrictions (groups, profile).
@@ -162,17 +162,9 @@ class quiz_override_form extends moodleform {
                     print_error('usersnone', 'quiz', $link);
                 }
 
-                $userchoices = array();
-                $canviewemail = in_array('email', get_extra_user_fields($this->context));
+                $userchoices = [];
                 foreach ($users as $id => $user) {
-                    if (empty($invalidusers[$id]) || (!empty($override) &&
-                            $id == $override->userid)) {
-                        if ($canviewemail) {
-                            $userchoices[$id] = fullname($user) . ', ' . $user->email;
-                        } else {
-                            $userchoices[$id] = fullname($user);
-                        }
-                    }
+                    $userchoices[$id] = $this->display_user_name($user, $extrauserfields);
                 }
                 unset($users);
 
@@ -228,7 +220,27 @@ class quiz_override_form extends moodleform {
 
         $mform->addGroup($buttonarray, 'buttonbar', '', array(' '), false);
         $mform->closeHeaderBefore('buttonbar');
+    }
 
+    /**
+     * Get a user's name and identity ready to display.
+     *
+     * @param stdClass $user a user object.
+     * @param array $extrauserfields from get_extra_user_fields.
+     * @return string User's name, with extra info, for display.
+     */
+    protected function display_user_name(stdClass $user, array $extrauserfields) {
+        $username = fullname($user);
+        $namefields = [];
+        foreach ($extrauserfields as $field) {
+            if (isset($user->$field) && $user->$field !== '') {
+                $namefields[] = $user->$field;
+            }
+        }
+        if ($namefields) {
+            $username .= ' (' . implode(', ', $namefields) . ')';
+        }
+        return $username;
     }
 
     public function validation($data, $files) {
index c4b35c4..9da9c8e 100644 (file)
@@ -92,13 +92,24 @@ echo $OUTPUT->header();
 echo $OUTPUT->heading(format_string($quiz->name, true, array('context' => $context)));
 
 if ($override->groupid) {
-    $group = $DB->get_record('groups', array('id' => $override->groupid), 'id, name');
+    $group = $DB->get_record('groups', ['id' => $override->groupid], 'id, name');
     $confirmstr = get_string("overridedeletegroupsure", "quiz", $group->name);
 } else {
     $namefields = get_all_user_name_fields(true);
-    $user = $DB->get_record('user', array('id' => $override->userid),
-            'id, ' . $namefields);
-    $confirmstr = get_string("overridedeleteusersure", "quiz", fullname($user));
+    $user = $DB->get_record('user', ['id' => $override->userid]);
+
+    $username = fullname($user);
+    $namefields = [];
+    foreach (get_extra_user_fields($context) as $field) {
+        if (isset($user->$field) && $user->$field !== '') {
+            $namefields[] = $user->$field;
+        }
+    }
+    if ($namefields) {
+        $username .= ' (' . implode(', ', $namefields) . ')';
+    }
+
+    $confirmstr = get_string('overridedeleteusersure', 'quiz', $username);
 }
 
 echo $OUTPUT->confirm($confirmstr, $confirmurl, $cancelurl);
index 30b5fee..fafcb4f 100644 (file)
@@ -45,10 +45,10 @@ if (!$canedit) {
 }
 
 $quizgroupmode = groups_get_activity_groupmode($cm);
-$accessallgroups = ($quizgroupmode == NOGROUPS) || has_capability('moodle/site:accessallgroups', $context);
+$showallgroups = ($quizgroupmode == NOGROUPS) || has_capability('moodle/site:accessallgroups', $context);
 
 // Get the course groups that the current user can access.
-$groups = $accessallgroups ? groups_get_all_groups($cm->course) : groups_get_activity_allowed_groups($cm);
+$groups = $showallgroups ? groups_get_all_groups($cm->course) : groups_get_activity_allowed_groups($cm);
 
 // Default mode is "group", unless there are no groups.
 if ($mode != "user" and $mode != "group") {
@@ -83,6 +83,8 @@ if (!empty($orphaned)) {
 }
 
 $overrides = [];
+$colclasses = [];
+$headers = [];
 
 // Fetch all overrides.
 if ($groupmode) {
@@ -101,46 +103,60 @@ if ($groupmode) {
 
         $overrides = $DB->get_records_sql($sql, $params);
     }
+
 } else {
     // User overrides.
-    $colname = get_string('user');
+    $colclasses[] = 'colname';
+    $headers[] = get_string('user');
+    $extrauserfields = get_extra_user_fields($context);
+    foreach ($extrauserfields as $field) {
+        $colclasses[] = 'col' . $field;
+        $headers[] = get_user_field_name($field);
+    }
+
     list($sort, $params) = users_order_by_sql('u');
     $params['quizid'] = $quiz->id;
+    $userfields = user_picture::fields('u', $extrauserfields, 'userid');
 
-    if ($accessallgroups) {
-        $sql = 'SELECT o.*, ' . get_all_user_name_fields(true, 'u') . '
-                  FROM {quiz_overrides} o
-                  JOIN {user} u ON o.userid = u.id
-                 WHERE o.quiz = :quizid
-              ORDER BY ' . $sort;
+    if ($showallgroups) {
+        $groupsjoin = '';
+        $groupswhere = '';
 
-        $overrides = $DB->get_records_sql($sql, $params);
     } else if ($groups) {
         list($insql, $inparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED);
+        $groupsjoin = 'JOIN {groups_members} gm ON u.id = gm.userid';
+        $groupswhere = ' AND gm.groupid ' . $insql;
         $params += $inparams;
 
-        $sql = 'SELECT o.*, ' . get_all_user_name_fields(true, 'u') . '
-                  FROM {quiz_overrides} o
-                  JOIN {user} u ON o.userid = u.id
-                  JOIN {groups_members} gm ON u.id = gm.userid
-                 WHERE o.quiz = :quizid AND gm.groupid ' . $insql . '
-              ORDER BY ' . $sort;
-
-        $overrides = $DB->get_records_sql($sql, $params);
+    } else {
+        // User cannot see any data.
+        $groupsjoin = '';
+        $groupswhere = ' AND 1 = 2';
     }
+
+    $overrides = $DB->get_records_sql("
+            SELECT o.*, $userfields
+              FROM {quiz_overrides} o
+              JOIN {user} u ON o.userid = u.id
+              $groupsjoin
+             WHERE o.quiz = :quizid
+               $groupswhere
+             ORDER BY $sort
+            ", $params);
 }
 
 // Initialise table.
 $table = new html_table();
-$table->headspan = [1, 2, 1];
-$table->colclasses = ['colname', 'colsetting', 'colvalue', 'colaction'];
-$table->head = [
-    $colname,
-    get_string('overrides', 'quiz'),
-];
-if ($canedit) {
-    $table->head[] = get_string('action');
-}
+$table->colclasses = $colclasses;
+$table->colclasses[] = 'colsetting';
+$table->colclasses[] = 'colvalue';
+$table->colclasses[] = 'colaction';
+$table->headspan = array_fill(0, count($headers), 1);
+$table->headspan[] = 2;
+$table->headspan[] = 1;
+$table->head = $headers;
+$table->head[] = get_string('overrides', 'quiz');
+$table->head[] = get_string('action');
 
 $userurl = new moodle_url('/user/view.php', []);
 $groupurl = new moodle_url('/group/overview.php', ['id' => $cm->course]);
@@ -203,19 +219,28 @@ foreach ($overrides as $override) {
     }
 
     // Prepare the information about who this override applies to.
+    $extranamebit = $active ? '' : '*';
+    $usercells = [];
     if ($groupmode) {
-        $usergroupstr = '<a href="' . $groupurl->out(true,
-                        ['group' => $override->groupid]) . '" >' . $override->name . '</a>';
+        $groupcell = new html_table_cell();
+        $groupcell->rowspan = count($fields);
+        $groupcell->text = html_writer::link(new moodle_url($groupurl, ['group' => $override->groupid]),
+                $override->name . $extranamebit);
+        $usercells[] = $groupcell;
     } else {
-        $usergroupstr = '<a href="' . $userurl->out(true,
-                        ['id' => $override->userid]) . '" >' . fullname($override) . '</a>';
-    }
-    if (!$active) {
-        $usergroupstr .= '*';
+        $usercell = new html_table_cell();
+        $usercell->rowspan = count($fields);
+        $usercell->text = html_writer::link(new moodle_url($groupurl, ['id' => $override->userid]),
+                fullname($override) . $extranamebit);
+        $usercells[] = $usercell;
+
+        foreach ($extrauserfields as $field) {
+            $usercell = new html_table_cell();
+            $usercell->rowspan = count($fields);
+            $usercell->text = $override->$field;
+            $usercells[] = $usercell;
+        }
     }
-    $usergroupcell = new html_table_cell();
-    $usergroupcell->rowspan = count($fields);
-    $usergroupcell->text = $usergroupstr;
 
     // Prepare the actions.
     if ($canedit) {
@@ -250,7 +275,7 @@ foreach ($overrides as $override) {
         }
 
         if ($i == 0) {
-            $row->cells[] = $usergroupcell;
+            $row->cells = $usercells;
         }
 
         $labelcell = new html_table_cell();
@@ -302,7 +327,7 @@ if ($canedit) {
     } else {
         $users = [];
         // See if there are any students in the quiz.
-        if ($accessallgroups) {
+        if ($showallgroups) {
             $users = get_users_by_capability($context, 'mod/quiz:attempt', 'u.id');
             $nousermessage = get_string('usersnone', 'quiz');
         } else if ($groups) {
index 5e6ecda..db450d3 100644 (file)
@@ -30,13 +30,13 @@ Feature: Quiz user override
     And I navigate to "User overrides" in current page administration
     And I press "Add user override"
     And I set the following fields to these values:
-      | Override user        | Student1 |
-      | id_timeclose_enabled | 1        |
-      | timeclose[day]       | 1        |
-      | timeclose[month]     | January  |
-      | timeclose[year]      | 2020     |
-      | timeclose[hour]      | 08       |
-      | timeclose[minute]    | 00       |
+      | Override user        | Student One (student1@example.com) |
+      | id_timeclose_enabled | 1                                  |
+      | timeclose[day]       | 1                                  |
+      | timeclose[month]     | January                            |
+      | timeclose[year]      | 2020                               |
+      | timeclose[hour]      | 08                                 |
+      | timeclose[minute]    | 00                                 |
     And I press "Save"
     Then I should see "Wednesday, 1 January 2020, 8:00"
 
@@ -44,9 +44,11 @@ Feature: Quiz user override
     And I set the following fields to these values:
       | timeclose[year] | 2030 |
     And I press "Save"
-    And I should see "Tuesday, 1 January 2030, 8:00"
+    And I should see "Tuesday, 1 January 2030, 8:00" in the "Student One" "table_row"
+    And I should see "student1@example.com" in the "Student One" "table_row"
 
-    And I click on "Delete" "link"
+    And I click on "Delete" "link" in the "Student One" "table_row"
+    And I should see "Are you sure you want to delete the override for user Student One (student1@example.com)?"
     And I press "Continue"
     And I should not see "Student One"
 
@@ -58,14 +60,33 @@ Feature: Quiz user override
     When I am on the "Test quiz" "mod_quiz > User overrides" page logged in as "teacher"
     And I press "Add user override"
     And I set the following fields to these values:
-      | Override user    | Student1 |
-      | Attempts allowed | 1        |
+      | Override user    | Student One (student1@example.com) |
+      | Attempts allowed | 1                                  |
     And I press "Save"
     Then I should see "This override is inactive"
     And "Edit" "icon" should exist in the "Student One" "table_row"
     And "copy" "icon" should exist in the "Student One" "table_row"
     And "Delete" "icon" should exist in the "Student One" "table_row"
 
+  @javascript
+  Scenario: Teacher without 'See full user identity in lists' can see and edit overrides
+    Given the following "permission overrides" exist:
+      | capability                   | permission | role           | contextlevel | reference |
+      | moodle/site:viewuseridentity | Prevent    | editingteacher | Course       | C1        |
+    And the following "activities" exist:
+      | activity   | name      | course | idnumber | visible |
+      | quiz       | Test quiz | C1     | quiz1    | 0       |
+    When I am on the "Test quiz" "mod_quiz > User overrides" page logged in as "teacher"
+    And I press "Add user override"
+    And I set the following fields to these values:
+      | Override user    | Student One |
+      | Attempts allowed | 1           |
+    And I press "Save"
+    And I should not see "student1@example.com"
+    And "Edit" "icon" should exist in the "Student One" "table_row"
+    And "copy" "icon" should exist in the "Student One" "table_row"
+    And "Delete" "icon" should exist in the "Student One" "table_row"
+
   Scenario: A teacher without accessallgroups permission should only be able to add user override for users that he/she shares groups with,
         when the activity's group mode is to "separate groups"
     Given the following "groups" exist:
@@ -85,8 +106,8 @@ Feature: Quiz user override
       | quiz     | Test quiz | C1     | quiz1    | 1         |
     When I am on the "Test quiz" "mod_quiz > User overrides" page logged in as "teacher"
     And I press "Add user override"
-    Then the "Override user" select box should contain "Student One, student1@example.com"
-    And the "Override user" select box should not contain "Student Two, student2@example.com"
+    Then the "Override user" select box should contain "Student One (student1@example.com)"
+    And the "Override user" select box should not contain "Student Two (student2@example.com)"
 
   Scenario: Override user in an activity with group mode set to "separate groups" as a teacher who is not a member in any group, and does not have accessallgroups permission
     Given the following "groups" exist:
index e747d1e..34f9e40 100644 (file)
@@ -38,8 +38,8 @@ $string['live'] = 'Live';
 $string['paymentnotcleared'] = 'payment not cleared by PayPal.';
 $string['pluginname'] = 'PayPal';
 $string['pluginname_desc'] = 'The PayPal plugin allows you to receive payments via PayPal.';
-$string['privacy:metadata'] = 'The Analytic models plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The PayPal plugin does not store any personal data.';
 $string['repeatedorder'] = 'This order has already been processed earlier.';
 $string['sandbox'] = 'Sandbox';
 $string['secret'] = 'Secret';
-$string['secret_help'] = 'The secret thatPayPal generated for your application.';
+$string['secret_help'] = 'The secret that PayPal generated for your application.';