Merge branch 'MDL-69529-master' of git://github.com/ilyatregubov/moodle
authorJake Dallimore <jake@moodle.com>
Tue, 12 Jan 2021 08:40:26 +0000 (16:40 +0800)
committerJake Dallimore <jake@moodle.com>
Tue, 12 Jan 2021 08:40:26 +0000 (16:40 +0800)
64 files changed:
admin/tasklogs.php
admin/tool/langimport/classes/output/langimport_page.php
cohort/assign.php
course/management.php
course/tests/services_content_item_service_test.php
grade/grading/form/guide/lib.php
grade/tests/behat/grade_to_pass.feature
install/lang/ar/langconfig.php
install/lang/el/install.php
install/lang/en_us_k12/langconfig.php
install/lang/he/langconfig.php
install/lang/id/admin.php
iplookup/tests/geoplugin_test.php
lang/en/admin.php
lang/en/badges.php
lang/en/cache.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/output/mustache_pix_helper.php
lib/classes/task/manager.php
lib/db/caches.php
lib/gradelib.php
lib/myprofilelib.php
lib/outputcomponents.php
lib/outputlib.php
lib/outputrenderers.php
lib/templates/permissionmanager_role.mustache
lib/templates/search_input.mustache
lib/templates/search_input_navbar.mustache
lib/tests/outputcomponents_test.php
lib/tests/scheduled_task_test.php
lib/tests/theme_config_test.php
lib/tests/upgradelib_test.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/tests/behat/steps_blind_marking.feature
mod/assign/tests/locallib_test.php
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
question/type/multichoice/renderer.php
question/type/multichoice/styles.css
theme/boost/config.php
theme/boost/layout/embedded.php
theme/boost/scss/moodle/blocks.scss
theme/boost/style/moodle.css
theme/boost/templates/embedded.mustache
theme/classic/style/moodle.css
theme/upgrade.txt
version.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 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 5afc685..94e02aa 100644 (file)
@@ -455,7 +455,7 @@ if ($viewmode === 'default' || $viewmode === 'combined') {
     }
 }
 if ($viewmode === 'default' || $viewmode === 'combined') {
-    $class .= ' viewmode-cobmined';
+    $class .= ' viewmode-combined';
 } else {
     $class .= ' viewmode-'.$viewmode;
 }
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 3ae9a6d..ff4f645 100644 (file)
@@ -29,5 +29,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['parentlanguage'] = '';
 $string['thisdirection'] = 'rtl';
 $string['thislanguage'] = 'العربية';
index 3ca5689..efca936 100644 (file)
@@ -84,7 +84,7 @@ $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Βλέπετε αυτή τη σελίδα γιατί εγκαταστήσατε και ξεκινήσατε με επιτυχία το πακέτο <strong>{$a->packname} {$a->packversion}</strong> στον υπολογιστή σας. Συγχαρητήρια!';
 $string['welcomep30'] = 'Αυτή η έκδοση/διανομή <strong>{$a->installername}</strong> περιλαμβάνει τις εφαρμογές για τη δημιουργία ενός περιβάλλοντος μέσα στο οποίο θα λειτουργεί το <strong>Moodle</strong>, ονομαστικά:';
 $string['welcomep40'] = 'Το πακέτο περιλαμβάνει επίσης το <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
-$string['welcomep50'] = 'Η χρήση όλων των εφαρμογών σε αυτό το πακέτο υπόκειται στις αντίστοιχες άδειες. Ολόκληρο το πακέτο <strong>{$a->installername}</strong> είναι <a href="https://www.opensource.org/docs/definition_plain.html">λογισμικό ανοικτού κώδικα</a> και διανέμεται με την <a href="https://www.gnu.org/copyleft/gpl.html">GPL</a> άδεια.';
+$string['welcomep50'] = 'Η χρήση όλων των εφαρμογών σε αυτό το πακέτο υπόκειται στις αντίστοιχες άδειες χρήσης. Ολόκληρο το πακέτο <strong>{$a->installername}</strong> είναι <a href="https://www.opensource.org/docs/definition_plain.html">λογισμικό ανοικτού κώδικα</a> και διανέμεται με την άδεια χρήσης <a href="https://www.gnu.org/copyleft/gpl.html">GPL</a>.';
 $string['welcomep60'] = 'Οι παρακάτω σελίδες θα σας καθοδηγήσουν με εύκολα βήματα στην εγκατάσταση και ρύθμιση του <strong>Moodle</strong> στον υπολογιστή σας. Μπορείτε να δεχθείτε τις προεπιλεγμένες ρυθμίσεις ή προαιρετικά, να τις τροποποιήσετε ανάλογα με τις ανάγκες σας.';
 $string['welcomep70'] = 'Πατήστε το κουμπί «Συνέχεια» για να συνεχίσετε με την εγκατάσταση του <strong>Moodle</strong>.';
 $string['wwwroot'] = 'Διεύθυνση ιστού';
index 87e0429..f37a63b 100644 (file)
@@ -29,5 +29,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['parentlanguage'] = 'en_us';
 $string['thisdirection'] = 'ltr';
-$string['thislanguage'] = 'American English K12';
+$string['thislanguage'] = 'English (United States) K12';
index c7b3cc6..9d06728 100644 (file)
@@ -29,5 +29,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['parentlanguage'] = 'he';
 $string['thisdirection'] = 'rtl';
 $string['thislanguage'] = 'עברית';
index 7c147ae..a888f1f 100644 (file)
@@ -39,3 +39,4 @@ $string['cliunknowoption'] = 'Opsi tidak dikenali :
  {$a}
 Silahkan gunakan opsi --help';
 $string['cliyesnoprompt'] = 'ketik y (Ya) atau t (Tidak)';
+$string['environmentrequireinstall'] = 'harus dipasang dan diaktifkan';
index 491308e..e558939 100644 (file)
@@ -51,8 +51,8 @@ class core_iplookup_geoplugin_testcase extends advanced_testcase {
 
         $this->assertEquals('array', gettype($result));
         $this->assertEquals('San Francisco', $result['city']);
-        $this->assertEqualsWithDelta(-122.3933, $result['longitude'], 0.01, 'Coordinates are out of accepted tolerance');
-        $this->assertEqualsWithDelta(37.7697, $result['latitude'], 0.01, 'Coordinates are out of accepted tolerance');
+        $this->assertEqualsWithDelta(-122.3933, $result['longitude'], 0.1, 'Coordinates are out of accepted tolerance');
+        $this->assertEqualsWithDelta(37.7697, $result['latitude'], 0.1, 'Coordinates are out of accepted tolerance');
         $this->assertNull($result['error']);
         $this->assertEquals('array', gettype($result['title']));
         $this->assertEquals('San Francisco', $result['title'][0]);
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 5a374dd..a0ff0c3 100644 (file)
@@ -89,6 +89,7 @@ $string['cachedef_user_favourite_course_content_items'] = 'User\'s starred items
 $string['cachedef_user_group_groupings'] = 'User\'s groupings and groups per course';
 $string['cachedef_user_course_content_items'] = 'User\'s content items (activities, resources and their subtypes) per course';
 $string['cachedef_yuimodules'] = 'YUI Module definitions';
+$string['cachedef_gradesetting'] = 'Course grade setting';
 $string['cachelock_file_default'] = 'Default file locking';
 $string['cachestores'] = 'Cache stores';
 $string['canuselocalstore'] = 'Can use local store';
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 a6a330c..112d4da 100644 (file)
@@ -72,6 +72,11 @@ class mustache_pix_helper {
         $text = strtok("");
         // Allow mustache tags in the last argument.
         $text = trim($helper->render($text));
+        // The $text has come from a template, so HTML special
+        // chars have been escaped. However, render_pix_icon
+        // assumes the alt arrives with no escaping. So we need
+        // ot un-escape here.
+        $text = htmlspecialchars_decode($text);
 
         return trim($this->renderer->pix_icon($key, $text, $component));
     }
index b9ef74e..befe2af 100644 (file)
@@ -229,6 +229,7 @@ class manager {
         $record = self::record_from_scheduled_task($task);
         $record->id = $original->id;
         $record->nextruntime = $task->get_next_scheduled_time();
+        unset($record->lastruntime);
         $result = $DB->update_record('task_scheduled', $record);
 
         return $result;
index 2861806..e7b352d 100644 (file)
@@ -476,4 +476,12 @@ $definitions = array(
         'simplekeys' => true,
         'simpledata' => false,
     ],
+
+    // Cache the grade setting for faster retrieval.
+    'gradesetting' => [
+        'mode'                   => cache_store::MODE_REQUEST,
+        'simplekeys'             => true,
+        'staticacceleration'     => true,
+        'staticaccelerationsize' => 100
+    ],
 );
index 02919f1..05aee9f 100644 (file)
@@ -676,16 +676,18 @@ function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $use
 function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
     global $DB;
 
-    static $cache = array();
+    $cache = cache::make('core', 'gradesetting');
+    $gradesetting = $cache->get($courseid) ?: array();
 
-    if ($resetcache or !array_key_exists($courseid, $cache)) {
-        $cache[$courseid] = array();
+    if ($resetcache or empty($gradesetting)) {
+        $gradesetting = array();
+        $cache->set($courseid, $gradesetting);
 
     } else if (is_null($name)) {
         return null;
 
-    } else if (array_key_exists($name, $cache[$courseid])) {
-        return $cache[$courseid][$name];
+    } else if (array_key_exists($name, $gradesetting)) {
+        return $gradesetting[$name];
     }
 
     if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
@@ -698,7 +700,8 @@ function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
         $result = $default;
     }
 
-    $cache[$courseid][$name] = $result;
+    $gradesetting[$name] = $result;
+    $cache->set($courseid, $gradesetting);
     return $result;
 }
 
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 b6277cb..ff5428b 100644 (file)
@@ -3407,6 +3407,15 @@ class block_contents {
     public function add_class($class) {
         $this->attributes['class'] .= ' '.$class;
     }
+
+    /**
+     * Check if the block is a fake block.
+     *
+     * @return boolean
+     */
+    public function is_fake() {
+        return isset($this->attributes['data-block']) && $this->attributes['data-block'] == '_fake';
+    }
 }
 
 
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 6b61cab..abfb603 100644 (file)
@@ -1817,9 +1817,10 @@ class core_renderer extends renderer_base {
      * Output all the blocks in a particular region.
      *
      * @param string $region the name of a region on this page.
+     * @param boolean $fakeblocksonly Output fake block only.
      * @return string the HTML to be output.
      */
-    public function blocks_for_region($region) {
+    public function blocks_for_region($region, $fakeblocksonly = false) {
         $blockcontents = $this->page->blocks->get_content_for_region($region, $this);
         $lastblock = null;
         $zones = array();
@@ -1832,10 +1833,16 @@ class core_renderer extends renderer_base {
 
         foreach ($blockcontents as $bc) {
             if ($bc instanceof block_contents) {
+                if ($fakeblocksonly && !$bc->is_fake()) {
+                    // Skip rendering real blocks if we only want to show fake blocks.
+                    continue;
+                }
                 $output .= $this->block($bc, $region);
                 $lastblock = $bc->title;
             } else if ($bc instanceof block_move_target) {
-                $output .= $this->block_move_target($bc, $zones, $lastblock, $region);
+                if (!$fakeblocksonly) {
+                    $output .= $this->block_move_target($bc, $zones, $lastblock, $region);
+                }
             } else {
                 throw new coding_exception('Unexpected type of thing (' . get_class($bc) . ') found in list of block contents.');
             }
@@ -3939,9 +3946,12 @@ EOD;
      *
      * @since Moodle 2.5.1 2.6
      * @param string $region The region to get HTML for.
+     * @param array $classes Wrapping tag classes.
+     * @param string $tag Wrapping tag.
+     * @param boolean $fakeblocksonly Include fake blocks only.
      * @return string HTML.
      */
-    public function blocks($region, $classes = array(), $tag = 'aside') {
+    public function blocks($region, $classes = array(), $tag = 'aside', $fakeblocksonly = false) {
         $displayregion = $this->page->apply_theme_region_manipulations($region);
         $classes = (array)$classes;
         $classes[] = 'block-region';
@@ -3952,7 +3962,7 @@ EOD;
             'data-droptarget' => '1'
         );
         if ($this->page->blocks->region_has_content($displayregion, $this)) {
-            $content = $this->blocks_for_region($displayregion);
+            $content = $this->blocks_for_region($displayregion, $fakeblocksonly);
         } else {
             $content = '';
         }
@@ -5084,9 +5094,10 @@ class core_renderer_maintenance extends core_renderer {
      * @param string $region
      * @param array $classes
      * @param string $tag
+     * @param boolean $fakeblocksonly
      * @return string
      */
-    public function blocks($region, $classes = array(), $tag = 'aside') {
+    public function blocks($region, $classes = array(), $tag = 'aside', $fakeblocksonly = false) {
         return '';
     }
 
@@ -5094,9 +5105,10 @@ class core_renderer_maintenance extends core_renderer {
      * Does nothing. The maintenance renderer cannot produce blocks.
      *
      * @param string $region
+     * @param boolean $fakeblocksonly Output fake block only.
      * @return string
      */
-    public function blocks_for_region($region) {
+    public function blocks_for_region($region, $fakeblocksonly = false) {
         return '';
     }
 
index b308653..d7ea395 100644 (file)
     * rolename Name of the role rendered - must have been prepared for output with format_string,
                                            or more likely one of the role API functions like role_fix_names.
     * roleid Id of the role
-    * action WEhich action is done on click
+    * action Which action is done on click
     * spanclass class attribute of span
     * linkclass class attribute of link
     * adminurl moodle admin url
-    * imageurl moodle url for delete(x) image
+    * icon moodle icon for delete(x)
+    * iconalt alt text for the icon. Must have been HTML escaped.
 
     Example context (json):
-    {"rolename" : "Manager",
-     "roleid" : 1,
-     "action": "prevent",
-     "spanclass": "allowed",
-     "linkclass": "preventlink",
-     "adminurl" : "http://localhost/moodle/admin/"}
+    {
+        "rolename": "Manager",
+        "roleid": 1,
+        "action": "prevent",
+        "spanclass": "allowed",
+        "linkclass": "preventlink",
+        "adminurl": "http://localhost/moodle/admin/",
+        "icon": "t/delete",
+        "iconalt": "Delete Student role"
+    }
 }}
 <span style="display:inline-block;" class="{{spanclass}}">&nbsp;{{{rolename}}}&nbsp;
     <a href="{{adminurl}}roles/permissions.php" class="{{linkclass}}" data-role-id="{{roleid}}" data-action="{{action}}">
         {{#icon}}
-            {{#pix}}{{icon}}, core, {{iconalt}}{{/pix}}
+            {{#pix}}{{icon}}, core, {{{iconalt}}}{{/pix}}
         {{/icon}}
     </a>
 </span>
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 6f561ec..73953d8 100644 (file)
@@ -95,7 +95,7 @@ function(
     var container = $('#searchinput-navbar-' + uniqid);
     var opensearch = container.find('[data-action="opensearch"]');
     var input = container.find('[data-region="input"]');
-    var submit = container.find('[data-action="submit"');
+    var submit = container.find('[data-action="submit"]');
 
     submit.on('click', function(e) {
         if (input.val() === '') {
@@ -113,4 +113,4 @@ function(
         input.focus();
     });
 });
-{{/js}}
\ No newline at end of file
+{{/js}}
index c46383b..0ff1c55 100644 (file)
@@ -650,4 +650,33 @@ EOF;
         $this->assertTrue(in_array(['name' => 'class', 'value' => $labelclass], $data->labelattributes));
         $this->assertTrue(in_array(['name' => 'style', 'value' => $labelstyle], $data->labelattributes));
     }
+
+    /**
+     * Data provider for test_block_contents_is_fake().
+     *
+     * @return array
+     */
+    public function block_contents_is_fake_provider() {
+        return [
+            'Null' => [null, false],
+            'Not set' => [false, false],
+            'Fake' => ['_fake', true],
+            'Real block' => ['activity_modules', false],
+        ];
+    }
+
+    /**
+     * Test block_contents is_fake() method.
+     *
+     * @dataProvider block_contents_is_fake_provider
+     * @param mixed $value Value for the data-block attribute
+     * @param boolean $expected The expected result
+     */
+    public function test_block_contents_is_fake($value, $expected) {
+        $bc = new block_contents(array());
+        if ($value !== false) {
+            $bc->attributes['data-block'] = $value;
+        }
+        $this->assertEquals($expected, $bc->is_fake());
+    }
 }
index 3246b6d..d57c352 100644 (file)
@@ -732,4 +732,24 @@ class core_scheduled_task_testcase extends advanced_testcase {
 
         call_user_func_array([$this, 'assertNotEquals'], $args);
     }
+
+    /**
+     * Assert that the lastruntime column holds an original value after a scheduled task is reset.
+     */
+    public function test_reset_scheduled_tasks_for_component_keeps_original_lastruntime(): void {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        // Set lastruntime for the scheduled task.
+        $DB->set_field('task_scheduled', 'lastruntime', 123456789, ['classname' => '\core\task\session_cleanup_task']);
+
+        // Reset the task.
+        \core\task\manager::reset_scheduled_tasks_for_component('moodle');
+
+        // Fetch the task again.
+        $taskafterreset = \core\task\manager::get_scheduled_task(core\task\session_cleanup_task::class);
+
+        // Confirm, that lastruntime is still in place.
+        $this->assertEquals(123456789, $taskafterreset->get_last_run_time());
+    }
 }
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 a0e3b48..5298ad5 100644 (file)
@@ -275,6 +275,55 @@ class core_upgradelib_testcase extends advanced_testcase {
         $this->assertEquals(20150627, $CFG->{'gradebook_calculations_freeze_' . $course2->id});
     }
 
+    /**
+     * Test the upgrade function for final grade after setting grade max for category and grade item.
+     */
+    public function test_upgrade_update_category_grademax_regrade_final_grades() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+
+        // Create a new course.
+        $course = $generator->create_course();
+
+        // Set the course aggregation to weighted mean of grades.
+        $unitcategory = \grade_category::fetch_course_category($course->id);
+        $unitcategory->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN;
+        $unitcategory->update();
+
+        // Set grade max for category.
+        $gradecategoryitem = grade_item::fetch(array('iteminstance' => $unitcategory->id));
+        $gradecategoryitem->grademax = 50;
+        $gradecategoryitem->update();
+
+        // Make new grade item.
+        $gradeitem = new \grade_item($generator->create_grade_item([
+            'itemname'        => 'Grade item',
+            'idnumber'        => 'git1',
+            'courseid'        => $course->id,
+            'grademin'        => 0,
+            'grademax'        => 50,
+            'aggregationcoef' => 100.0,
+        ]));
+
+        // Set final grade.
+        $grade = $gradeitem->get_grade($user->id, true);
+        $grade->finalgrade = 20;
+        $grade->update();
+
+        $courseitem = \grade_item::fetch(['courseid' => $course->id, 'itemtype' => 'course']);
+        $gradeitem->force_regrading();
+
+        // Trigger regrade because the grade items needs to be updated.
+        grade_regrade_final_grades($course->id);
+
+        $coursegrade = new \grade_grade($courseitem->get_final($user->id), false);
+        $this->assertEquals(20, $coursegrade->finalgrade);
+    }
+
     function test_upgrade_calculated_grade_items_regrade() {
         global $DB, $CFG;
 
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 ce652aa..0697d54 100644 (file)
@@ -2518,7 +2518,8 @@ class assign {
         // Only ever send a max of one days worth of updates.
         $yesterday = time() - (24 * 3600);
         $timenow   = time();
-        $lastruntime = $DB->get_field('task_scheduled', 'lastruntime', array('component' => 'mod_assign'));
+        $task = \core\task\manager::get_scheduled_task(mod_assign\task\cron_task::class);
+        $lastruntime = $task->get_last_run_time();
 
         // Collect all submissions that require mailing.
         // Submissions are included if all are true:
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 8c51acb..0d5af67 100644 (file)
@@ -4206,4 +4206,54 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         $output3 .= $assign->get_renderer()->render($summary);
         $this->assertStringContainsStringIgnoringCase('Friday, 7 June 2019, 5:37 PM', $output3);
     }
+
+    /**
+     * Test that cron task uses task API to get its last run time.
+     */
+    public function test_cron_use_task_api_to_get_lastruntime() {
+        global $DB;
+        $this->resetAfterTest();
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create an assignment which allows submissions from 3 days ago.
+        $assign1 = $this->create_instance($course, [
+            'duedate' => time() + DAYSECS,
+            'alwaysshowdescription' => 0,
+            'allowsubmissionsfromdate' => time() - 3 * DAYSECS,
+            'intro' => 'This one should not be re-created',
+        ]);
+
+        // Create an assignment which allows submissions from 1 day ago.
+        $assign2 = $this->create_instance($course, [
+            'duedate' => time() + DAYSECS,
+            'alwaysshowdescription' => 0,
+            'allowsubmissionsfromdate' => time() - DAYSECS,
+            'intro' => 'This one should be re-created',
+        ]);
+
+        // Set last run time 2 days ago.
+        $DB->set_field('task_scheduled', 'lastruntime', time() - 2 * DAYSECS, ['classname' => '\mod_assign\task\cron_task']);
+
+        // Remove events to make sure cron will update calendar and re-create one of them.
+        $params = array('modulename' => 'assign', 'instance' => $assign1->get_instance()->id);
+        $DB->delete_records('event', $params);
+        $params = array('modulename' => 'assign', 'instance' => $assign2->get_instance()->id);
+        $DB->delete_records('event', $params);
+
+        // Run cron.
+        assign::cron();
+
+        // Assert that calendar hasn't been updated for the first assignment as it's supposed to be
+        // updated as part of previous cron runs (allowsubmissionsfromdate is less than lastruntime).
+        $params = array('modulename' => 'assign', 'instance' => $assign1->get_instance()->id);
+        $event1 = $DB->get_record('event', $params);
+        $this->assertEmpty($event1);
+
+        // Assert that calendar has been updated for the second assignment
+        // because its allowsubmissionsfromdate is greater than lastruntime.
+        $params = array('modulename' => 'assign', 'instance' => $assign2->get_instance()->id);
+        $event2 = $DB->get_record('event', $params);
+        $this->assertNotEmpty($event2);
+        $this->assertSame('This one should be re-created', $event2->description);
+    }
 }
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.';
index 89563b2..e9cec51 100644 (file)
@@ -104,12 +104,16 @@ abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedb
                 ));
             }
 
-            $questionnumber = html_writer::span($this->number_in_style($value, $question->answernumbering), 'answernumber');
-            $answertext = $question->format_text($ans->answer, $ans->answerformat, $qa, 'question', 'answer', $ansid);
-            $questionanswer = html_writer::div($answertext, 'flex-fill ml-1');
+            $choicenumber = '';
+            if ($question->answernumbering !== 'none') {
+                $choicenumber = html_writer::span(
+                        $this->number_in_style($value, $question->answernumbering), 'answernumber');
+            }
+            $choicetext = $question->format_text($ans->answer, $ans->answerformat, $qa, 'question', 'answer', $ansid);
+            $choice = html_writer::div($choicetext, 'flex-fill ml-1');
 
             $radiobuttons[] = $hidden . html_writer::empty_tag('input', $inputattributes) .
-                    html_writer::div($questionnumber . $questionanswer, 'd-flex w-100', [
+                    html_writer::div($choicenumber . $choice, 'd-flex w-100', [
                         'id' => $inputattributes['id'] . '_label',
                         'data-region' => 'answer-label',
                     ]);
index df8ed86..bf2e736 100644 (file)
@@ -1,9 +1,3 @@
-.que.multichoice .answer .specificfeedback {
-    display: inline;
-    padding: 0 0.7em;
-    background: #fff3bf;
-}
-
 .que.multichoice .answer div.r0,
 .que.multichoice .answer div.r1 {
     display: flex;
     align-items: flex-start;
 }
 
-.que.multichoice .answer div.r0 label,
-.que.multichoice .answer div.r1 label,
-.que.multichoice .answer div.r0 div.specificfeedback,
-.que.multichoice .answer div.r1 div.specificfeedback {
-    /* In Chrome and IE, the text-indent above is applied to any embedded table
-       cells or <li>s, which screws up the intended layout. This fixes it again. */
-    text-indent: 0;
-}
-
 .que.multichoice .answer div.r0 input,
 .que.multichoice .answer div.r1 input {
     margin: 0.3rem 0.5rem;
     width: 14px;
 }
 
+.que.multichoice .answer .answernumber {
+    min-width: 1.5em;
+}
+
+.que.multichoice .answer .specificfeedback {
+    display: inline;
+    padding: 0 0.7em;
+    background: #fff3bf;
+}
+
 /* Editing form. */
 body#page-question-type-multichoice div[id^=fitem_id_][id*=answer_] {
     background: #eee;
index 613162f..8952583 100644 (file)
@@ -112,7 +112,8 @@ $THEME->layouts = [
     // Embeded pages, like iframe/object embeded in moodleform - it needs as much space as possible.
     'embedded' => array(
         'file' => 'embedded.php',
-        'regions' => array()
+        'regions' => array('side-pre'),
+        'defaultregion' => 'side-pre',
     ),
     // Used during upgrade and install, and for the 'This site is undergoing maintenance' message.
     // This must not have any blocks, links, or API calls that would lead to database or cache interaction.
index 5469e1b..762d7f3 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+$fakeblockshtml = $OUTPUT->blocks('side-pre', array(), 'aside', true);
+$hasfakeblocks = strpos($fakeblockshtml, 'data-block="_fake"') !== false;
+
 $templatecontext = [
-    'sitename' => format_string($SITE->shortname, true, ['context' => context_course::instance(SITEID), "escape" => false]),
-    'output' => $OUTPUT
+    'output' => $OUTPUT,
+    'hasfakeblocks' => $hasfakeblocks,
+    'fakeblocks' => $fakeblockshtml,
 ];
 
 echo $OUTPUT->render_from_template('theme_boost/embedded', $templatecontext);
index 7a9d4bf..95ac0b7 100644 (file)
@@ -374,3 +374,33 @@ body.drawer-open-left #region-main.has-blocks {
     border: 2px dashed $gray-800;
     margin: 4px 0;
 }
+
+.pagelayout-embedded {
+    .has-fake-blocks {
+        padding: 1rem;
+        display: flex;
+    }
+
+    .has-fake-blocks .embedded-main {
+        order: 0;
+        width: calc(100% - #{$blocks-column-width});
+        margin-right: 1rem;
+    }
+
+    .embedded-blocks {
+        order: 1;
+        width: $blocks-column-width;
+    }
+
+    @media (max-width: 767.98px) {
+        .has-fake-blocks {
+            display: block;
+        }
+        .has-fake-blocks .embedded-main {
+            width: 100%;
+        }
+        .embedded-blocks {
+            width: 100%;
+        }
+    }
+}
index c82e085..045bdff 100644 (file)
@@ -12770,6 +12770,27 @@ input[disabled] {
   border: 2px dashed #343a40;
   margin: 4px 0; }
 
+.pagelayout-embedded .has-fake-blocks {
+  padding: 1rem;
+  display: flex; }
+
+.pagelayout-embedded .has-fake-blocks .embedded-main {
+  order: 0;
+  width: calc(100% - 360px);
+  margin-right: 1rem; }
+
+.pagelayout-embedded .embedded-blocks {
+  order: 1;
+  width: 360px; }
+
+@media (max-width: 767.98px) {
+  .pagelayout-embedded .has-fake-blocks {
+    display: block; }
+  .pagelayout-embedded .has-fake-blocks .embedded-main {
+    width: 100%; }
+  .pagelayout-embedded .embedded-blocks {
+    width: 100%; } }
+
 .navbar {
   max-height: 50px; }
 
index 5d7a26d..a9a419f 100644 (file)
 
     Context variables required for this template:
     * output - The core renderer for the page
+    * hasfakeblocks - true if there are fake blocks on this page
+    * fakeblocks - HTML for the fake blocks
 
     Example context (json):
     {
         "output": {
             "doctype": "<!DOCTYPE html>",
+            "htmlattributes": "The attributes that should be added to the <html> tag",
             "page_title": "Test page",
             "favicon": "favicon.ico",
-            "main_content": "<h1>Headings make html validators happier</h1>"
-         }
+            "standard_head_html": "The standard tags that should be included in the <head> tag",
+            "body_attributes": "The attributes to use within the body tag",
+            "standard_top_of_body_html": "The standard tags that should be output just inside the start of the <body> tag",
+            "main_content": "<h1>Headings make html validators happier</h1>",
+            "standard_end_of_body_html": "The standard tags that should be output after everything else"
+         },
+         "hasfakeblocks": true,
+         "fakeblocks": "<h2>Fake blocks html goes here</h2>"
     }
 }}
 {{{ output.doctype }}}
 {{> core/local/toast/wrapper}}
 
 {{{ output.standard_top_of_body_html }}}
-<div id="page">
-    <div id="page-content" class="d-block">
+<div id="page" {{#hasfakeblocks}}class="has-fake-blocks"{{/hasfakeblocks}}>
+    {{#hasfakeblocks}}
+        <section class="embedded-blocks" aria-label="{{#str}}blocks{{/str}}">
+            {{{ fakeblocks }}}
+        </section>
+    {{/hasfakeblocks}}
+    <section class="embedded-main">
         {{{ output.main_content }}}
-    </div>
+    </section>
 </div>
 {{{ output.standard_end_of_body_html }}}
 </body>
index 9ff29bb..c104721 100644 (file)
@@ -12984,6 +12984,27 @@ input[disabled] {
   border: 2px dashed #343a40;
   margin: 4px 0; }
 
+.pagelayout-embedded .has-fake-blocks {
+  padding: 1rem;
+  display: flex; }
+
+.pagelayout-embedded .has-fake-blocks .embedded-main {
+  order: 0;
+  width: calc(100% - 360px);
+  margin-right: 1rem; }
+
+.pagelayout-embedded .embedded-blocks {
+  order: 1;
+  width: 360px; }
+
+@media (max-width: 767.98px) {
+  .pagelayout-embedded .has-fake-blocks {
+    display: block; }
+  .pagelayout-embedded .has-fake-blocks .embedded-main {
+    width: 100%; }
+  .pagelayout-embedded .embedded-blocks {
+    width: 100%; } }
+
 .navbar {
   max-height: 50px; }
 
index a67d8a8..b52dbc4 100644 (file)
@@ -1,6 +1,9 @@
 This files describes API changes in /theme/* themes,
 information provided here is intended especially for theme designer.
 
+=== 3.11 ===
+* The classname 'viewmode-cobmined' in course/management.php has been changed to 'viewmode-combined'
+
 === 3.10 ===
 * The Bootstrap legacy css utilities from Bootstrap 2 and 4alpha have been removed.
 The syntax for the new Bootstrap 4.5 utility classes is {property}{sides}-{breakpoint}-{size} for sm, md, lg, and xl.
index 846a31c..e7a8e51 100644 (file)
@@ -29,9 +29,9 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2021052500.49;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2021052500.50;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
-$release  = '4.0dev (Build: 20201224)'; // Human-friendly version name
+$release  = '4.0dev (Build: 20210108)'; // Human-friendly version name
 $branch   = '400';                      // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.