Merge branch 'MDL-66818-39' of git://github.com/lameze/moodle into MOODLE_39_STABLE
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 20 Aug 2020 00:17:01 +0000 (08:17 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Thu, 20 Aug 2020 00:17:01 +0000 (08:17 +0800)
47 files changed:
.travis.yml
admin/tool/mobile/classes/output/subscription.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/settings.php
auth/db/auth.php
auth/db/lang/en/auth_db.php
auth/ldap/tests/plugin_test.php
backup/moodle2/restore_stepslib.php
backup/util/dbops/restore_dbops.class.php
badges/classes/form/badge.php
badges/classes/output/external_backpacks_page.php
badges/templates/external_backpacks_page.mustache
badges/tests/behat/backpack.feature
calendar/classes/external/week_day_exporter.php
calendar/templates/month_detailed.mustache
calendar/templates/month_mini.mustache
contentbank/amd/build/actions.min.js
contentbank/amd/build/actions.min.js.map
contentbank/amd/src/actions.js
contentbank/classes/content.php
contentbank/classes/external/rename_content.php
contentbank/templates/bankcontent.mustache
contentbank/tests/content_test.php
contentbank/tests/contenttype_test.php
contentbank/tests/external/rename_content_test.php
course/classes/editcategory_form.php
course/format/topics/backup/moodle2/restore_format_topics_plugin.class.php
course/format/weeks/backup/moodle2/restore_format_weeks_plugin.class.php
group/import.php
group/import_form.php
lang/en/calendar.php
lang/en/contentbank.php
lang/en/error.php
lang/en/group.php
lang/en/mimetypes.php
lib/classes/filetypes.php
lib/db/upgrade.php
lib/filelib.php
lib/form/tests/filetypes_util_test.php
mod/assign/locallib.php
mod/book/edit_form.php
mod/workshop/form/rubric/styles.css
question/type/ddimageortext/styles.css
user/edit.php
user/editadvanced.php
user/externallib.php
version.php

index 67d70d5..40161c2 100644 (file)
@@ -174,7 +174,9 @@ before_script:
         # Enable test external resources
         sed -i \
           -e "/require_once/i \\define('TEST_EXTERNAL_FILES_HTTP_URL', 'http://127.0.0.1:8080');" \
+          -e "/require_once/i \\define('TEST_EXTERNAL_FILES_HTTPS_URL', 'http://127.0.0.1:8080');" \
           config.php ;
+
         # Redis cache store tests
         sed -i \
           -e "/require_once/i \\define('TEST_CACHESTORE_REDIS_TESTSERVERS', '127.0.0.1');" \
@@ -254,8 +256,6 @@ script:
       if [ "$TASK" = 'PHPUNIT' ];
       then
         vendor/bin/phpunit --fail-on-risky --disallow-test-output --verbose;
-        EXTTESTS_HITS=$(docker logs exttests 2>&1 | grep -Fv -e 'AH00558' -e '[pid 1]' | wc -l)
-        echo -e "\nTest local resources number of hits: ${EXTTESTS_HITS}.\n"
       fi
 
     - >
@@ -292,3 +292,11 @@ script:
           exit 1 ;
         fi
       fi
+
+after_script:
+    - >
+      if [ "$TASK" = 'PHPUNIT' ];
+      then
+        EXTTESTS_HITS=$(docker logs exttests 2>&1 | grep -Fv -e 'AH00558' -e '[pid 1]' | wc -l)
+        echo -e "\nTest local resources number of hits: ${EXTTESTS_HITS}.\n"
+      fi
index d8deaca..572a91a 100644 (file)
@@ -184,6 +184,13 @@ class subscription implements \renderable, \templatable {
                                     'type' => 'danger', 'message' => get_string('subscriptionfeaturenotapplied', 'tool_mobile')];
                             }
                             break;
+                        // Check QR automatic login.
+                        case 'qrautomaticlogin':
+                            if ($ms->qrcodetype == \tool_mobile\api::QR_CODE_LOGIN) {
+                                $feature['message'] = [
+                                    'type' => 'danger', 'message' => get_string('subscriptionfeaturenotapplied', 'tool_mobile')];
+                            }
+                            break;
                     }
                 }
             }
index bc7fc89..952e99b 100644 (file)
@@ -114,7 +114,7 @@ $string['qrcodeformobileapploginabout'] = 'Scan the QR code with your mobile app
 $string['qrcodeformobileappurlabout'] = 'Scan the QR code with your mobile app to fill in the site URL in your app.';
 $string['qrsiteadminsnotallowed'] = 'For security reasons login via QR code is not allowed for site administrators or if you are logged in as another user.';
 $string['qrcodetype'] = 'QR code access';
-$string['qrcodetype_desc'] = 'A QR code can be provided for mobile app users to scan and either have the site URL filled in or be automatically logged in without having to enter their credentials.';
+$string['qrcodetype_desc'] = 'A QR code can be provided for mobile app users to scan. This can be used to fill in the site URL, or where the site is secured using HTTPS, to automatically log the user in without having to enter their username and password.';
 $string['qrcodetypeurl'] = 'QR code with site URL';
 $string['qrcodetypelogin'] = 'QR code with automatic login';
 $string['readingthisemailgettheapp'] = 'Reading this in an email? <a href="{$a}">Download the mobile app and receive notifications on your mobile device</a>.';
index c2cee8c..19a8798 100644 (file)
@@ -94,11 +94,17 @@ if ($hassiteconfig) {
     $options = [
         tool_mobile\api::QR_CODE_DISABLED => new lang_string('qrcodedisabled', 'tool_mobile'),
         tool_mobile\api::QR_CODE_URL => new lang_string('qrcodetypeurl', 'tool_mobile'),
-        tool_mobile\api::QR_CODE_LOGIN => new lang_string('qrcodetypelogin', 'tool_mobile'),
     ];
+    $qrcodetypedefault = tool_mobile\api::QR_CODE_URL;
+
+    if (is_https()) {   // Allow QR login for https sites.
+        $options[tool_mobile\api::QR_CODE_LOGIN] = new lang_string('qrcodetypelogin', 'tool_mobile');
+        $qrcodetypedefault = tool_mobile\api::QR_CODE_LOGIN;
+    }
+
     $temp->add(new admin_setting_configselect('tool_mobile/qrcodetype',
                 new lang_string('qrcodetype', 'tool_mobile'),
-                new lang_string('qrcodetype_desc', 'tool_mobile'), tool_mobile\api::QR_CODE_LOGIN, $options));
+                new lang_string('qrcodetype_desc', 'tool_mobile'), $qrcodetypedefault, $options));
 
     $temp->add(new admin_setting_configtext('tool_mobile/forcedurlscheme',
                 new lang_string('forcedurlscheme_key', 'tool_mobile'),
index 5c2fe8c..13da0f4 100644 (file)
@@ -603,9 +603,12 @@ class auth_plugin_db extends auth_plugin_base {
             }
         }
         if (!empty($update)) {
-            $authdb->Execute("UPDATE {$this->config->table}
-                                 SET ".implode(',', $update)."
-                               WHERE {$this->config->fielduser}='".$this->ext_addslashes($extusername)."'");
+            $sql = "UPDATE {$this->config->table}
+                       SET ".implode(',', $update)."
+                     WHERE {$this->config->fielduser} = ?";
+            if (!$authdb->Execute($sql, array($this->ext_addslashes($extusername)))) {
+                print_error('auth_dbupdateerror', 'auth_db');
+            }
         }
         $authdb->Close();
         return true;
index d3f8a83..75c5c26 100644 (file)
@@ -74,5 +74,6 @@ $string['auth_dbcannotconnect'] = 'Cannot connect to external database.';
 $string['auth_dbcannotreadtable'] = 'Cannot read external table.';
 $string['auth_dbtableempty'] = 'External table is empty.';
 $string['auth_dbcolumnlist'] = 'External table contains the following columns:<br />{$a}';
+$string['auth_dbupdateerror'] = 'Error updating external database.';
 $string['pluginname'] = 'External database';
 $string['privacy:metadata'] = 'The External database authentication plugin does not store any personal data.';
index 17ee9dd..dced889 100644 (file)
@@ -108,16 +108,19 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         $o['ou']          = 'users';
         ldap_add($connection, 'ou='.$o['ou'].','.$topdn, $o);
 
+        $createdusers = array();
         for ($i=1; $i<=5; $i++) {
             $this->create_ldap_user($connection, $topdn, $i);
+            $createdusers[] = 'username' . $i;
         }
 
         // Set up creators group.
+        $assignedroles = array('username1', 'username2');
         $o = array();
         $o['objectClass'] = array('posixGroup');
         $o['cn']          = 'creators';
         $o['gidNumber']   = 1;
-        $o['memberUid']   = array('username1', 'username2');
+        $o['memberUid']   = $assignedroles;
         ldap_add($connection, 'cn='.$o['cn'].','.$topdn, $o);
 
         $creatorrole = $DB->get_record('role', array('shortname'=>'coursecreator'));
@@ -174,15 +177,23 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         // Check events, 5 users created with 2 users having roles.
         $this->assertCount(7, $events);
         foreach ($events as $index => $event) {
-            $usercreatedindex = array(0, 2, 4, 5, 6);
-            $roleassignedindex = array (1, 3);
-            if (in_array($index, $usercreatedindex)) {
-                $this->assertInstanceOf('\core\event\user_created', $event);
-            }
-            if (in_array($index, $roleassignedindex)) {
-                $this->assertInstanceOf('\core\event\role_assigned', $event);
+            $username = $DB->get_field('user', 'username', array('id' => $event->relateduserid)); // Get username.
+
+            if ($event->eventname === '\core\event\user_created') {
+                $this->assertContains($username, $createdusers);
+                unset($events[$index]); // Remove matching event.
+
+            } else if ($event->eventname === '\core\event\role_assigned') {
+                $this->assertContains($username, $assignedroles);
+                unset($events[$index]); // Remove matching event.
+
+            } else {
+                $this->fail('Unexpected event found: ' . $event->eventname);
             }
         }
+        // If all the user_created and role_assigned events have matched
+        // then the $events array should be now empty.
+        $this->assertCount(0, $events);
 
         $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
         $this->assertEquals(2, $DB->count_records('role_assignments'));
index 223b849..baf3b04 100644 (file)
@@ -122,7 +122,7 @@ class restore_gradebook_structure_step extends restore_structure_step {
         }
 
         // Identify the backup we're dealing with.
-        $backuprelease = floatval($this->get_task()->get_info()->backup_release); // The major version: 2.9, 3.0, ...
+        $backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
         $backupbuild = 0;
         preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
         if (!empty($matches[1])) {
@@ -132,7 +132,7 @@ class restore_gradebook_structure_step extends restore_structure_step {
         // On older versions the freeze value has to be converted.
         // We do this from here as it is happening right before the file is read.
         // This only targets the backup files that can contain the legacy freeze.
-        if ($backupbuild > 20150618 && ($backuprelease < 3.0 || $backupbuild < 20160527)) {
+        if ($backupbuild > 20150618 && (version_compare($backuprelease, '3.0', '<') || $backupbuild < 20160527)) {
             $this->rewrite_step_backup_file_for_legacy_freeze($fullpath);
         }
 
@@ -505,8 +505,7 @@ class restore_gradebook_structure_step extends restore_structure_step {
         $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid());
         preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
         $backupbuild = (int)$matches[1];
-        // The function floatval will return a float even if there is text mixed with the release number.
-        $backuprelease = floatval($this->get_task()->get_info()->backup_release);
+        $backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
 
         // Extra credits need adjustments only for backups made between 2.8 release (20141110) and the fix release (20150619).
         if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150619) {
@@ -521,7 +520,7 @@ class restore_gradebook_structure_step extends restore_structure_step {
         // Courses from before 3.1 (20160518) may have a letter boundary problem and should be checked for this issue.
         // Backups from before and including 2.9 could have a build number that is greater than 20160518 and should
         // be checked for this problem.
-        if (!$gradebookcalculationsfreeze && ($backupbuild < 20160518 || $backuprelease <= 2.9)) {
+        if (!$gradebookcalculationsfreeze && ($backupbuild < 20160518 || version_compare($backuprelease, '2.9', '<='))) {
             require_once($CFG->libdir . '/db/upgradelib.php');
             upgrade_course_letter_boundary($this->get_courseid());
         }
@@ -4631,11 +4630,11 @@ class restore_create_categories_and_questions extends restore_structure_step {
 
         // Before 3.5, question categories could be created at top level.
         // From 3.5 onwards, all question categories should be a child of a special category called the "top" category.
-        $backuprelease = floatval($this->get_task()->get_info()->backup_release);
+        $backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
         preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
         $backupbuild = (int)$matches[1];
         $before35 = false;
-        if ($backuprelease < 3.5 || $backupbuild < 20180205) {
+        if (version_compare($backuprelease, '3.5', '<') || $backupbuild < 20180205) {
             $before35 = true;
         }
         if (empty($mapping->info->parent) && $before35) {
@@ -4892,11 +4891,11 @@ class restore_move_module_questions_categories extends restore_execution_step {
     protected function define_execution() {
         global $DB;
 
-        $backuprelease = floatval($this->task->get_info()->backup_release);
+        $backuprelease = $this->task->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
         preg_match('/(\d{8})/', $this->task->get_info()->moodle_release, $matches);
         $backupbuild = (int)$matches[1];
         $after35 = false;
-        if ($backuprelease >= 3.5 && $backupbuild > 20180205) {
+        if (version_compare($backuprelease, '3.5', '>=') && $backupbuild > 20180205) {
             $after35 = true;
         }
 
index eef73fc..a5ca451 100644 (file)
@@ -581,11 +581,11 @@ abstract class restore_dbops {
         $rc = restore_controller_dbops::load_controller($restoreid);
         $restoreinfo = $rc->get_info();
         $rc->destroy(); // Always need to destroy.
-        $backuprelease = floatval($restoreinfo->backup_release);
+        $backuprelease = $restoreinfo->backup_release; // The major version: 2.9, 3.0, 3.10...
         preg_match('/(\d{8})/', $restoreinfo->moodle_release, $matches);
         $backupbuild = (int)$matches[1];
         $after35 = false;
-        if ($backuprelease >= 3.5 && $backupbuild > 20180205) {
+        if (version_compare($backuprelease, '3.5', '>=') && $backupbuild > 20180205) {
             $after35 = true;
         }
 
index 523403b..6626b58 100644 (file)
@@ -70,7 +70,7 @@ class badge extends moodleform {
         $mform->addRule('description', null, 'required');
 
         $str = $action == 'new' ? get_string('badgeimage', 'badges') : get_string('newimage', 'badges');
-        $imageoptions = array('maxbytes' => 262144, 'accepted_types' => array('web_image'));
+        $imageoptions = array('maxbytes' => 262144, 'accepted_types' => array('optimised_image'));
         $mform->addElement('filepicker', 'image', $str, null, $imageoptions);
 
         if ($action == 'new') {
index 28da925..32c38db 100644 (file)
@@ -67,11 +67,6 @@ class external_backpacks_page implements \renderable {
         foreach ($this->backpacks as $backpack) {
             $exporter = new backpack_exporter($backpack);
             $backpack = $exporter->export($output);
-            if ($backpack->apiversion == OPEN_BADGES_V2 || $backpack->apiversion == OPEN_BADGES_V2P1) {
-                $backpack->canedit = true;
-            } else {
-                $backpack->canedit = false;
-            }
             $backpack->cantest = ($backpack->apiversion == OPEN_BADGES_V2);
             $backpack->iscurrent = ($backpack->id == $CFG->badges_site_backpack);
 
index 0f9df01..bfdcc5e 100644 (file)
@@ -25,7 +25,7 @@
     Example context (json):
     {
         "backpacks": [
-            {"backpackweburl": "http://localhost/", "sitebackpack": true, "canedit": false, "cantest": true}
+            {"backpackweburl": "http://localhost/", "sitebackpack": true, "cantest": true}
         ]
     }
 }}
@@ -50,9 +50,7 @@
             <td> {{{backpackweburl}}} </td>
             <td> {{#sitebackpack}}Yes{{/sitebackpack}} </td>
             <td>
-            {{#canedit}}
                 <a href="{{baseurl}}?id={{id}}&action=edit">{{#pix}}t/edit, core,{{#str}}editsettings{{/str}}{{/pix}}</a>
-            {{/canedit}}
             {{^iscurrent}}
                 <a href="{{baseurl}}?id={{id}}&action=delete" role="button" data-action="deletebackpack">
                     {{#pix}}t/delete, core,{{#str}}delete{{/str}}{{/pix}}
index 67376f0..ea02aa7 100644 (file)
@@ -107,7 +107,8 @@ Feature: Backpack badges
     And I set the field "backpackweburl" to "http://backpackweburl.cat"
     And I press "Save changes"
     Then I should see "http://backpackweburl.cat"
-    And "Delete" "button" should exist
+    And "Delete" "icon" should exist in the "http://backpackweburl.cat" "table_row"
+    And "Edit settings" "icon" should exist in the "http://backpackweburl.cat" "table_row"
 
   @javascript
   Scenario: Remove a site backpack
index 98ad278..80ddbf0 100644 (file)
@@ -86,6 +86,9 @@ class week_day_exporter extends day_exporter {
                 'type' => PARAM_RAW,
                 'default' => '',
             ],
+            'daytitle' => [
+                'type' => PARAM_RAW,
+            ]
         ]);
 
         return $return;
@@ -104,6 +107,8 @@ class week_day_exporter extends day_exporter {
             $return['popovertitle'] = $popovertitle;
         }
 
+        $return['daytitle'] = $this->get_day_title();
+
         return $return;
     }
 
@@ -141,4 +146,24 @@ class week_day_exporter extends day_exporter {
 
         return $title;
     }
+
+    /**
+     * Get the title for this day.
+     *
+     * @return string
+     */
+    protected function get_day_title(): string {
+        $userdate = userdate($this->data[0], get_string('strftimedayshort'));
+
+        $numevents = count($this->related['events']);
+        if ($numevents == 1) {
+            $title = get_string('dayeventsone', 'calendar', $userdate);
+        } else if ($numevents) {
+            $title = get_string('dayeventsmany', 'calendar', ['num' => $numevents, 'day' => $userdate]);
+        } else {
+            $title = get_string('dayeventsnone', 'calendar', $userdate);
+        }
+
+        return $title;
+    }
 }
index a88f65e..ba01dfe 100644 (file)
@@ -46,8 +46,9 @@
         <thead>
             <tr>
                 {{# daynames }}
-                <th class="header text-xs-center" aria-label="{{fullname}}">
-                    {{shortname}}
+                <th class="header text-xs-center">
+                    <span class="sr-only">{{fullname}}</span>
+                    <span aria-hidden="true">{{shortname}}</span>
                 </th>
                 {{/ daynames }}
             </tr>
@@ -71,6 +72,7 @@
                         data-region="day"
                         data-new-event-timestamp="{{neweventtimestamp}}">
                         <div class="d-none d-md-block hidden-phone text-xs-center">
+                            <span class="sr-only">{{daytitle}}</span>
                             {{#hasevents}}
                                 <a data-action="view-day-link" href="#" class="aalink day" aria-label="{{viewdaylinktitle}}"
                                     data-year="{{date.year}}" data-month="{{date.mon}}" data-day="{{mday}}"
@@ -78,7 +80,7 @@
                                     data-timestamp="{{timestamp}}">{{mday}}</a>
                             {{/hasevents}}
                             {{^hasevents}}
-                                {{mday}}
+                                <span aria-hidden="true">{{mday}}</span>
                             {{/hasevents}}
                             {{#hasevents}}
                                 <div data-region="day-content">
                             {{/hasevents}}
                         </div>
                         <div class="d-md-none hidden-desktop hidden-tablet">
+                            <span class="sr-only">{{daytitle}}</span>
                             {{#hasevents}}
                                 <a data-action="view-day-link" href="#" class="day aalink" aria-label="{{viewdaylinktitle}}"
                                     data-year="{{date.year}}" data-month="{{date.mon}}" data-day="{{mday}}"
                                     data-timestamp="{{timestamp}}">{{mday}}</a>
                             {{/hasevents}}
                             {{^hasevents}}
-                                    {{mday}}
+                                <span aria-hidden="true">{{mday}}</span>
                             {{/hasevents}}
                         </div>
                     </td>
index ff10a74..4ff2656 100644 (file)
@@ -78,8 +78,9 @@
         <thead>
           <tr>
                 {{# daynames }}
-                <th class="header text-xs-center" scope="col" aria-label="{{fullname}}">
-                    {{shortname}}
+                <th class="header text-xs-center">
+                    <span class="sr-only">{{fullname}}</span>
+                    <span aria-hidden="true">{{shortname}}</span>
                 </th>
                 {{/ daynames }}
             </tr>
                         This is the timestamp for this month.
                         }} data-day-timestamp="{{timestamp}}"{{!
                     }}>{{!
-                        }}{{#popovertitle}}
+                        }}<span class="sr-only">{{daytitle}}</span>
+                        {{#popovertitle}}
                             {{< core_calendar/minicalendar_day_link }}
                                 {{$day}}{{mday}}{{/day}}
                                 {{$url}}{{viewdaylink}}{{/url}}
                             {{/ core_calendar/minicalendar_day_link }}
                         {{/popovertitle}}{{!
                         }}{{^popovertitle}}
-                            {{mday}}
+                            <span aria-hidden="true">{{mday}}</span>
                         {{/popovertitle}}{{!
                     }}</td>
                 {{/days}}
index 237c664..0f2b538 100644 (file)
Binary files a/contentbank/amd/build/actions.min.js and b/contentbank/amd/build/actions.min.js differ
index 79956ea..ee0807e 100644 (file)
Binary files a/contentbank/amd/build/actions.min.js.map and b/contentbank/amd/build/actions.min.js.map differ
index 1776292..176b127 100644 (file)
@@ -139,10 +139,26 @@ function($, Ajax, Notification, Str, Templates, Url, ModalFactory, ModalEvents)
                 });
             }).then(function(modal) {
                 modal.setSaveButtonText(saveButtonText);
-                modal.getRoot().on(ModalEvents.save, function() {
+                modal.getRoot().on(ModalEvents.save, function(e) {
                     // The action is now confirmed, sending an action for it.
-                    var newname = $("#newname").val();
-                    return renameContent(contentid, newname);
+                    var newname = $("#newname").val().trim();
+                    if (newname) {
+                        renameContent(contentid, newname);
+                    } else {
+                        var errorStrings = [
+                            {
+                                key: 'error',
+                            },
+                            {
+                                key: 'emptynamenotallowed',
+                                component: 'core_contentbank',
+                            },
+                        ];
+                        Str.get_strings(errorStrings).then(function(langStrings) {
+                            Notification.alert(langStrings[0], langStrings[1]);
+                        }).catch(Notification.exception);
+                        e.preventDefault();
+                    }
                 });
 
                 // Handle hidden event.
@@ -211,11 +227,11 @@ function($, Ajax, Notification, Str, Templates, Url, ModalFactory, ModalEvents)
         };
         var requestType = 'success';
         Ajax.call([request])[0].then(function(data) {
-            if (data) {
+            if (data.result) {
                 return 'contentrenamed';
             }
             requestType = 'error';
-            return 'contentnotrenamed';
+            return data.warnings[0].message;
 
         }).then(function(message) {
             var params = null;
index b8a9d40..37e3385 100644 (file)
@@ -127,6 +127,7 @@ abstract class content {
      * @throws \coding_exception if not loaded.
      */
     public function set_name(string $name): bool {
+        $name = trim($name);
         if (empty($name)) {
             return false;
         }
index 18d36f3..5cd9d75 100644 (file)
@@ -87,15 +87,24 @@ class rename_content extends external_api {
                 $content = new $contentclass($record);
                 // Check capability.
                 if ($contenttype->can_manage($content)) {
-                    // This content can be renamed.
-                    if ($contenttype->rename_content($content, $params['name'])) {
-                        $result = true;
-                    } else {
+                    if (empty(trim($name))) {
+                        // If name is empty don't try to rename and return a more detailed message.
                         $warnings[] = [
                             'item' => $contentid,
-                            'warningcode' => 'contentnotrenamed',
-                            'message' => get_string('contentnotrenamed', 'core_contentbank')
+                            'warningcode' => 'emptynamenotallowed',
+                            'message' => get_string('emptynamenotallowed', '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.
index 4801a7f..6819447 100644 (file)
@@ -125,7 +125,7 @@ data-region="contentbank">
                     <div class="cb-type cb-column d-flex last">
                         <div class="title">{{#str}} type, contentbank {{/str}}</div>
                         <button class="btn btn-sm cb-btnsort dir-none ml-auto" data-string="type" data-action="sorttype"
-                        title="{{#str}} sortbyx, core, {{#str}} size, contentbank {{/str}} {{/str}}">
+                        title="{{#str}} sortbyx, core, {{#str}} type, contentbank {{/str}} {{/str}}">
                             <span class="default">{{#pix}} t/sort, core, {{#str}}sort, core {{/str}} {{/pix}}</span>
                             <span class="desc">{{#pix}} t/sort_desc, core, {{#str}}desc, core{{/str}} {{/pix}}</span>
                             <span class="asc">{{#pix}} t/sort_asc, core, {{#str}}asc, core{{/str}} {{/pix}}</span>
index b0bfede..ddfff94 100644 (file)
@@ -81,7 +81,9 @@ class core_contenttype_content_testcase extends \advanced_testcase {
             'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle'],
             'Name with tags' => ['This is <b>bold</b>', 'This is bold'],
             'Long name' => [str_repeat('a', 100), str_repeat('a', 100)],
-            'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)]
+            'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)],
+            'Empty name' => ['', 'Old name'],
+            'Blanks only' => ['  ', 'Old name'],
         ];
     }
 
index 1fee5be..c8ae908 100644 (file)
@@ -375,12 +375,14 @@ class core_contenttype_contenttype_testcase extends \advanced_testcase {
      */
     public function rename_content_provider() {
         return [
-            'Standard name' => ['New name', 'New name'],
-            'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017'],
-            'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle'],
-            'Name with tags' => ['This is <b>bold</b>', 'This is bold'],
-            'Long name' => [str_repeat('a', 100), str_repeat('a', 100)],
-            'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)]
+            'Standard name' => ['New name', 'New name', true],
+            'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017', true],
+            'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle', true],
+            'Name with tags' => ['This is <b>bold</b>', 'This is bold', true],
+            'Long name' => [str_repeat('a', 100), str_repeat('a', 100), true],
+            'Too long name' => [str_repeat('a', 300), str_repeat('a', 255), true],
+            'Empty name' => ['', 'Test content ', false],
+            'Blanks only' => ['  ', 'Test content ', false],
         ];
     }
 
@@ -390,10 +392,11 @@ class core_contenttype_contenttype_testcase extends \advanced_testcase {
      * @dataProvider    rename_content_provider
      * @param   string  $newname    The name to set
      * @param   string   $expected   The name result
+     * @param   bool   $result   The bolean result expected when renaming
      *
      * @covers ::rename_content
      */
-    public function test_rename_content(string $newname, string $expected) {
+    public function test_rename_content(string $newname, string $expected, bool $result) {
         global $DB;
 
         $this->resetAfterTest();
@@ -414,9 +417,8 @@ class core_contenttype_contenttype_testcase extends \advanced_testcase {
 
         // Check the content is renamed as expected by a user with permission.
         $renamed = $contenttype->rename_content($content, $newname);
-        $this->assertTrue($renamed);
+        $this->assertEquals($result, $renamed);
         $record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]);
-        $this->assertNotEquals($oldname, $record->name);
         $this->assertEquals($expected, $record->name);
     }
 
index 6a9ea67..d369bb2 100644 (file)
@@ -52,12 +52,14 @@ class rename_content_testcase extends \externallib_advanced_testcase {
      */
     public function rename_content_provider() {
         return [
-            'Standard name' => ['New name', 'New name'],
-            'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017'],
-            'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle'],
-            'Name with tags' => ['This is <b>bold</b>', 'This is bold'],
-            'Long name' => [str_repeat('a', 100), str_repeat('a', 100)],
-            'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)]
+            'Standard name' => ['New name', 'New name', true],
+            'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017', true],
+            'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle', true],
+            'Name with tags' => ['This is <b>bold</b>', 'This is bold', true],
+            'Long name' => [str_repeat('a', 100), str_repeat('a', 100), true],
+            'Too long name' => [str_repeat('a', 300), str_repeat('a', 255), true],
+            'Empty name' => ['', 'Test content ', false],
+            'Blanks only' => ['  ', 'Test content ', false],
         ];
     }
 
@@ -66,11 +68,12 @@ class rename_content_testcase extends \externallib_advanced_testcase {
      *
      * @dataProvider    rename_content_provider
      * @param   string  $newname    The name to set
-     * @param   string   $expected   The name result
+     * @param   string   $expectedname   The name result
+     * @param   bool   $expectedresult   The bolean result expected when renaming
      *
      * @covers ::execute
      */
-    public function test_rename_content_with_permission(string $newname, string $expected) {
+    public function test_rename_content_with_permission(string $newname, string $expectedname, bool $expectedresult) {
         global $DB;
         $this->resetAfterTest();
 
@@ -91,10 +94,9 @@ class rename_content_testcase extends \externallib_advanced_testcase {
         // Call the WS and check the content is renamed as expected.
         $result = rename_content::execute($content->get_id(), $newname);
         $result = external_api::clean_returnvalue(rename_content::execute_returns(), $result);
-        $this->assertTrue($result['result']);
+        $this->assertEquals($expectedresult, $result['result']);
         $record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]);
-        $this->assertNotEquals($oldname, $record->name);
-        $this->assertEquals($expected, $record->name);
+        $this->assertEquals($expectedname, $record->name);
 
         // Call the WS using an unexisting contentid and check an error is thrown.
         $this->expectException(\invalid_response_exception::class);
index 2e58af3..f11d28c 100644 (file)
@@ -75,6 +75,7 @@ class core_course_editcategory_form extends moodleform {
 
         $mform->addElement('editor', 'description_editor', get_string('description'), null,
             $this->get_description_editor_options());
+        $mform->setType('description_editor', PARAM_RAW);
 
         if (!empty($CFG->allowcategorythemes)) {
             $themes = array(''=>get_string('forceno'));
@@ -105,7 +106,8 @@ class core_course_editcategory_form extends moodleform {
         return array(
             'maxfiles'  => EDITOR_UNLIMITED_FILES,
             'maxbytes'  => $CFG->maxbytes,
-            'trusttext' => true,
+            'trusttext' => false,
+            'noclean'   => true,
             'context'   => $context,
             'subdirs'   => file_area_contains_subdirs($context, 'coursecat', 'description', $itemid),
         );
index 0e918c9..8807ed8 100644 (file)
@@ -48,8 +48,8 @@ class restore_format_topics_plugin extends restore_format_plugin {
      */
     protected function need_restore_numsections() {
         $backupinfo = $this->step->get_task()->get_info();
-        $backuprelease = $backupinfo->backup_release;
-        return version_compare($backuprelease, '3.3', 'lt');
+        $backuprelease = $backupinfo->backup_release; // The major version: 2.9, 3.0, 3.10...
+        return version_compare($backuprelease, '3.3', '<');
     }
 
     /**
index e0ec9c8..64e3b33 100644 (file)
@@ -48,8 +48,8 @@ class restore_format_weeks_plugin extends restore_format_plugin {
      */
     protected function is_pre_33_backup() {
         $backupinfo = $this->step->get_task()->get_info();
-        $backuprelease = $backupinfo->backup_release;
-        return version_compare($backuprelease, '3.3', 'lt');
+        $backuprelease = $backupinfo->backup_release; // The major version: 2.9, 3.0, 3.10...
+        return version_compare($backuprelease, '3.3', '<');
     }
 
     /**
index 2f563a2..58f405b 100644 (file)
@@ -49,30 +49,37 @@ $PAGE->set_pagelayout('admin');
 
 $returnurl = new moodle_url('/group/index.php', array('id'=>$id));
 
-$mform_post = new groups_import_form(null, array('id'=>$id));
+$importform = new groups_import_form(null, ['id' => $id]);
 
 // If a file has been uploaded, then process it
-if ($mform_post->is_cancelled()) {
+if ($importform->is_cancelled()) {
     redirect($returnurl);
 
-} else if ($mform_post->get_data()) {
+} else if ($formdata = $importform->get_data()) {
     echo $OUTPUT->header();
 
-    $csv_encode = '/\&\#44/';
-    if (isset($CFG->CSV_DELIMITER)) {
-        $csv_delimiter = $CFG->CSV_DELIMITER;
+    $text = $importform->get_file_content('userfile');
+    $text = preg_replace('!\r\n?!', "\n", $text);
 
-        if (isset($CFG->CSV_ENCODE)) {
-            $csv_encode = '/\&\#' . $CFG->CSV_ENCODE . '/';
-        }
-    } else {
-        $csv_delimiter = ",";
+    $rawlines = explode("\n", $text);
+
+    require_once($CFG->libdir . '/csvlib.class.php');
+    $importid = csv_import_reader::get_new_iid('groupimport');
+    $csvimport = new csv_import_reader($importid, 'groupimport');
+    $delimiter = $formdata->delimiter_name;
+    $encoding = $formdata->encoding;
+    $readcount = $csvimport->load_csv_content($text, $encoding, $delimiter);
+
+    if ($readcount === false) {
+        print_error('csvfileerror', 'error', $PAGE->url, $csvimport->get_error());
+    } else if ($readcount == 0) {
+        print_error('csvemptyfile', 'error', $PAGE->url, $csvimport->get_error());
+    } else if ($readcount == 1) {
+        print_error('csvnodata', 'error', $PAGE->url);
     }
 
-    $text = $mform_post->get_file_content('userfile');
-    $text = preg_replace('!\r\n?!',"\n",$text);
+    $csvimport->init();
 
-    $rawlines = explode("\n", $text);
     unset($text);
 
     // make arrays of valid fields for error checking
@@ -88,12 +95,12 @@ if ($mform_post->is_cancelled()) {
         );
 
     // --- get header (field names) ---
-    $header = explode($csv_delimiter, array_shift($rawlines));
+    $header = explode($csvimport::get_delimiter($delimiter), array_shift($rawlines));
     // check for valid field names
     foreach ($header as $i => $h) {
         $h = trim($h); $header[$i] = $h; // remove whitespace
         if (!(isset($required[$h]) or isset($optionalDefaults[$h]) or isset($optional[$h]))) {
-                print_error('invalidfieldname', 'error', 'import.php?id='.$id, $h);
+                print_error('invalidfieldname', 'error', $PAGE->url, $h);
             }
         if (isset($required[$h])) {
             $required[$h] = 2;
@@ -102,32 +109,28 @@ if ($mform_post->is_cancelled()) {
     // check for required fields
     foreach ($required as $key => $value) {
         if ($value < 2) {
-            print_error('fieldrequired', 'error', 'import.php?id='.$id, $key);
+            print_error('fieldrequired', 'error', $PAGE->url, $key);
         }
     }
     $linenum = 2; // since header is line 1
 
-    foreach ($rawlines as $rawline) {
+    while ($line = $csvimport->next()) {
 
         $newgroup = new stdClass();//to make Martin happy
         foreach ($optionalDefaults as $key => $value) {
             $newgroup->$key = current_language(); //defaults to current language
         }
-        //Note: commas within a field should be encoded as &#44 (for comma separated csv files)
-        //Note: semicolon within a field should be encoded as &#59 (for semicolon separated csv files)
-        $line = explode($csv_delimiter, $rawline);
         foreach ($line as $key => $value) {
-            //decode encoded commas
-            $record[$header[$key]] = preg_replace($csv_encode, $csv_delimiter, trim($value));
+            $record[$header[$key]] = trim($value);
         }
-        if (trim($rawline) !== '') {
+        if (trim(implode($line)) !== '') {
             // add a new group to the database
 
             // add fields to object $user
             foreach ($record as $name => $value) {
                 // check for required values
                 if (isset($required[$name]) and !$value) {
-                    print_error('missingfield', 'error', 'import.php?id='.$id, $name);
+                    print_error('missingfield', 'error', $PAGE->url, $name);
                 } else if ($name == "groupname") {
                     $newgroup->name = $value;
                 } else {
@@ -232,6 +235,7 @@ if ($mform_post->is_cancelled()) {
         }
     }
 
+    $csvimport->close();
     echo $OUTPUT->single_button($returnurl, get_string('continue'), 'get');
     echo $OUTPUT->footer();
     die;
@@ -240,5 +244,5 @@ if ($mform_post->is_cancelled()) {
 /// Print the form
 echo $OUTPUT->header();
 echo $OUTPUT->heading_with_help($strimportgroups, 'importgroups', 'core_group');
-$mform_post ->display();
+$importform->display();
 echo $OUTPUT->footer();
index e877b5f..ce13446 100644 (file)
@@ -27,6 +27,7 @@ if (!defined('MOODLE_INTERNAL')) {
 }
 
 require_once($CFG->libdir.'/formslib.php');
+require_once($CFG->libdir . '/csvlib.class.php');
 
 /**
  * Groups import form class
@@ -56,6 +57,19 @@ class groups_import_form extends moodleform {
         $mform->addElement('hidden', 'id');
         $mform->setType('id', PARAM_INT);
 
+        $choices = csv_import_reader::get_delimiter_list();
+        $mform->addElement('select', 'delimiter_name', get_string('csvdelimiter', 'group'), $choices);
+        if (array_key_exists('cfg', $choices)) {
+            $mform->setDefault('delimiter_name', 'cfg');
+        } else if (get_string('listsep', 'langconfig') == ';') {
+            $mform->setDefault('delimiter_name', 'semicolon');
+        } else {
+            $mform->setDefault('delimiter_name', 'comma');
+        }
+
+        $choices = core_text::get_encodings();
+        $mform->addElement('select', 'encoding', get_string('encoding', 'group'), $choices);
+        $mform->setDefault('encoding', 'UTF-8');
         $this->add_action_buttons(true, get_string('importgroups', 'core_group'));
 
         $this->set_data($data);
index 6f1c4f9..e10e573 100644 (file)
@@ -52,6 +52,9 @@ $string['courses'] = 'Courses';
 $string['customexport'] = 'Custom range ({$a->timestart} - {$a->timeend})';
 $string['daily'] = 'Daily';
 $string['day'] = 'Day';
+$string['dayeventsmany'] = '{$a->num} events, {$a->day}';
+$string['dayeventsnone'] = 'No events, {$a}';
+$string['dayeventsone'] = '1 event, {$a}';
 $string['daynext'] = 'Next day';
 $string['dayprev'] = 'Previous day';
 $string['dayviewfor'] = 'Day view for:';
index bac647a..6411ece 100644 (file)
@@ -33,6 +33,7 @@ $string['contentrenamed'] = 'The content has been renamed.';
 $string['contentsmoved'] = 'Content bank contents moved to {$a}.';
 $string['contenttypenoaccess'] = 'You cannot view this {$a} instance.';
 $string['contenttypenoedit'] = 'You can not edit this content';
+$string['emptynamenotallowed'] = 'Empty name is not allowed';
 $string['eventcontentcreated'] = 'Content created';
 $string['eventcontentdeleted'] = 'Content deleted';
 $string['eventcontentupdated'] = 'Content updated';
index 2a2eab5..3530052 100644 (file)
@@ -195,10 +195,12 @@ $string['coursemisconf'] = 'Course is misconfigured';
 $string['courserequestdisabled'] = 'Sorry, but course requests have been disabled by the administrator.';
 $string['csvcolumnduplicates'] = 'Duplicate columns detected';
 $string['csvemptyfile'] = 'The CSV file is empty';
+$string['csvfileerror'] = 'There is something wrong with the format of the CSV file. Please check the number of headings and columns match, and that the delimiter and file encoding are correct: {$a}';
 $string['csvfewcolumns'] = 'Not enough columns, please verify the delimiter setting';
 $string['csvinvalidcols'] = '<b>Invalid CSV file:</b> First line must include "Header Fields" and the file must be type of <br />"Expanded Fields/Comma Separated"<br />or<br /> "Expanded Fields with CAVV Result Code/Comma Separated"';
 $string['csvinvalidcolsnum'] = 'Invalid CSV file - each line must include 49 or 70 fields';
 $string['csvloaderror'] = 'An error occurred while loading the CSV file: {$a}';
+$string['csvnodata'] = 'Invalid CSV file - The CSV file has headers but does not contain any data.';
 $string['csvweirdcolumns'] = 'Invalid CSV file format - number of columns is not constant!';
 $string['dbconnectionfailed'] = '<p>Error: Database connection failed</p>
 <p>It is possible that the database is overloaded or otherwise not running properly.</p>
index 9f32f85..ab1ce3e 100644 (file)
@@ -43,6 +43,7 @@ $string['creategrouping'] = 'Create grouping';
 $string['creategroupinselectedgrouping'] = 'Create group in grouping';
 $string['createingrouping'] = 'Grouping of auto-created groups';
 $string['createorphangroup'] = 'Create orphan group';
+$string['csvdelimiter'] = 'CSV delimiter';
 $string['databaseupgradegroups'] = 'Groups version is now {$a}';
 $string['defaultgrouping'] = 'Default grouping';
 $string['defaultgroupingname'] = 'Grouping';
@@ -59,6 +60,7 @@ $string['editgroupsettings'] = 'Edit group settings';
 $string['editusersgroupsa'] = 'Edit groups for "{$a}"';
 $string['enablemessaging'] = 'Group messaging';
 $string['enablemessaging_help'] = 'If enabled, group members can send messages to the others in their group via the messaging drawer.';
+$string['encoding'] = 'Encoding';
 $string['enrolmentkey'] = 'Enrolment key';
 $string['enrolmentkey_help'] = 'An enrolment key enables access to the course to be restricted to only those who know the key. If a group enrolment key is specified, then not only will entering that key let the user into the course, but it will also automatically make them a member of this group.
 
index cbcd329..83f4257 100644 (file)
@@ -74,6 +74,7 @@ $string['group:html_track'] = 'HTML track files';
 $string['group:html_video'] = 'Video files natively supported by browsers';
 $string['group:image'] = 'Image files';
 $string['group:media_source'] = 'Streaming media';
+$string['group:optimised_image'] = 'Image files to be optimised, such as badges';
 $string['group:presentation'] = 'Presentation files';
 $string['group:sourcecode'] = 'Source code';
 $string['group:spreadsheet'] = 'Spreadsheet files';
index e1e0416..8d32b35 100644 (file)
@@ -107,7 +107,8 @@ abstract class core_filetypes {
                     'groups' => array('spreadsheet')),
             'gslides' => array('type' => 'application/vnd.google-apps.presentation', 'icon' => 'powerpoint',
                     'groups' => array('presentation')),
-            'gif' => array('type' => 'image/gif', 'icon' => 'gif', 'groups' => array('image', 'web_image'), 'string' => 'image'),
+            'gif' => array('type' => 'image/gif', 'icon' => 'gif', 'groups' => array('image', 'web_image', 'optimised_image'),
+                'string' => 'image'),
             'gtar' => array('type' => 'application/x-gtar', 'icon' => 'archive',
                     'groups' => array('archive'), 'string' => 'archive'),
             'tgz' => array('type' => 'application/g-zip', 'icon' => 'archive', 'groups' => array('archive'), 'string' => 'archive'),
@@ -136,9 +137,12 @@ abstract class core_filetypes {
             'jmt' => array('type' => 'text/xml', 'icon' => 'markup'),
             'jmx' => array('type' => 'text/xml', 'icon' => 'markup'),
             'jnlp' => array('type' => 'application/x-java-jnlp-file', 'icon' => 'markup'),
-            'jpe' => array('type' => 'image/jpeg', 'icon' => 'jpeg', 'groups' => array('image', 'web_image'), 'string' => 'image'),
-            'jpeg' => array('type' => 'image/jpeg', 'icon' => 'jpeg', 'groups' => array('image', 'web_image'), 'string' => 'image'),
-            'jpg' => array('type' => 'image/jpeg', 'icon' => 'jpeg', 'groups' => array('image', 'web_image'), 'string' => 'image'),
+            'jpe' => array('type' => 'image/jpeg', 'icon' => 'jpeg', 'groups' => array('image', 'web_image', 'optimised_image'),
+                'string' => 'image'),
+            'jpeg' => array('type' => 'image/jpeg', 'icon' => 'jpeg', 'groups' => array('image', 'web_image', 'optimised_image'),
+                'string' => 'image'),
+            'jpg' => array('type' => 'image/jpeg', 'icon' => 'jpeg', 'groups' => array('image', 'web_image', 'optimised_image'),
+                'string' => 'image'),
             'jqz' => array('type' => 'text/xml', 'icon' => 'markup'),
             'js' => array('type' => 'application/x-javascript', 'icon' => 'text', 'groups' => array('web_file')),
             'json' => array('type' => 'application/json', 'icon' => 'text'),
@@ -206,7 +210,8 @@ abstract class core_filetypes {
             'php' => array('type' => 'text/plain', 'icon' => 'sourcecode'),
             'pic' => array('type' => 'image/pict', 'icon' => 'image', 'groups' => array('image'), 'string' => 'image'),
             'pict' => array('type' => 'image/pict', 'icon' => 'image', 'groups' => array('image'), 'string' => 'image'),
-            'png' => array('type' => 'image/png', 'icon' => 'png', 'groups' => array('image', 'web_image'), 'string' => 'image'),
+            'png' => array('type' => 'image/png', 'icon' => 'png', 'groups' => array('image', 'web_image', 'optimised_image'),
+                'string' => 'image'),
             'pps' => array('type' => 'application/vnd.ms-powerpoint', 'icon' => 'powerpoint', 'groups' => array('presentation')),
             'ppt' => array('type' => 'application/vnd.ms-powerpoint', 'icon' => 'powerpoint', 'groups' => array('presentation')),
             'pptx' => array('type' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
index 32298db..7ce3c1e 100644 (file)
@@ -2194,8 +2194,11 @@ function xmldb_main_upgrade($oldversion) {
             $DB->delete_records('competency_userevidencecomp', ['userevidenceid' => $userevidence->id]);
             $DB->delete_records('competency_userevidence', ['id' => $userevidence->id]);
 
-            $context = context_user::instance($userevidence->userid);
-            $fs->delete_area_files($context->id, 'core_competency', 'userevidence', $userevidence->id);
+            if ($record = $DB->get_record('context', ['contextlevel' => CONTEXT_USER, 'instanceid' => $userevidence->userid],
+                    '*', IGNORE_MISSING)) {
+                // Delete all orphaned user evidences files.
+                $fs->delete_area_files($record->id, 'core_competency', 'userevidence', $userevidence->userid);
+            }
         }
 
         $sql = "SELECT cp.id
@@ -2527,5 +2530,24 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2020061501.04);
     }
 
+    if ($oldversion < 2020061501.08) {
+        // Delete all user evidence files from users that have been deleted.
+        $sql = "SELECT DISTINCT f.*
+                  FROM {files} f
+             LEFT JOIN {context} c ON f.contextid = c.id
+             LEFT JOIN {user} u ON c.instanceid = u.id
+                 WHERE f.component = :component
+                   AND f.filearea = :filearea
+                   AND u.deleted = 1";
+        $stalefiles = $DB->get_records_sql($sql, ['component' => 'core_competency', 'filearea' => 'userevidence']);
+
+        $fs = get_file_storage();
+        foreach ($stalefiles as $stalefile) {
+            $fs->get_file_instance($stalefile)->delete();
+        }
+
+        upgrade_main_savepoint(true, 2020061501.08);
+    }
+
     return true;
 }
index 2a42f8e..fd0c43e 100644 (file)
@@ -1639,6 +1639,7 @@ function download_file_content($url, $headers=null, $postdata=null, $fullrespons
  *     commonly used in moodle the following groups:
  *       - web_image - image that can be included as <img> in HTML
  *       - image - image that we can parse using GD to find it's dimensions, also used for portfolio format
+ *       - optimised_image - image that will be processed and optimised
  *       - video - file that can be imported as video in text editor
  *       - audio - file that can be imported as audio in text editor
  *       - archive - we can extract files from this archive
index 7fb1b5f..355fefe 100644 (file)
@@ -302,10 +302,10 @@ class filetypes_util_testcase extends advanced_testcase {
         // All these three files are in both "image" and also "web_image"
         // groups. We display both groups.
         $data = $util->data_for_browser('jpg png gif', true, '.gif');
-        $this->assertEquals(2, count($data));
+        $this->assertEquals(3, count($data));
         $this->assertTrue($data[0]->key !== $data[1]->key);
         foreach ($data as $group) {
-            $this->assertTrue(($group->key === 'image' || $group->key === 'web_image'));
+            $this->assertTrue(($group->key === 'image' || $group->key === 'web_image' || $group->key === 'optimised_image'));
             $this->assertEquals(3, count($group->types));
             $this->assertFalse($group->selectable);
             foreach ($group->types as $ext) {
@@ -317,11 +317,11 @@ class filetypes_util_testcase extends advanced_testcase {
             }
         }
 
-        // There is a group web_image which is a subset of the group image. The
-        // file extensions that fall into both groups will be displayed twice.
+        // The groups web_image and optimised_image are a subset of the group image. The
+        // file extensions that fall into these groups will be displayed thrice.
         $data = $util->data_for_browser('web_image');
         foreach ($data as $group) {
-            $this->assertTrue(($group->key === 'image' || $group->key === 'web_image'));
+            $this->assertTrue(($group->key === 'image' || $group->key === 'web_image' || $group->key === 'optimised_image'));
         }
 
         // Check that "All file types" are displayed first.
index 4b50121..825b708 100644 (file)
@@ -5129,18 +5129,33 @@ class assign {
         require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
 
         // Check that all of the submission plugins are ready for this submission.
+        // Also check whether there is something to be submitted as well against atleast one.
         $notifications = array();
         $submission = $this->get_user_submission($USER->id, false);
+        if ($this->get_instance()->teamsubmission) {
+            $submission = $this->get_group_submission($USER->id, 0, false);
+        }
+
         $plugins = $this->get_submission_plugins();
+        $hassubmission = false;
         foreach ($plugins as $plugin) {
             if ($plugin->is_enabled() && $plugin->is_visible()) {
                 $check = $plugin->precheck_submission($submission);
                 if ($check !== true) {
                     $notifications[] = $check;
                 }
+
+                if (is_object($submission) && !$plugin->is_empty($submission)) {
+                    $hassubmission = true;
+                }
             }
         }
 
+        // If there are no submissions and no existing notifications to be displayed the stop.
+        if (!$hassubmission && !$notifications) {
+            $notifications[] = get_string('addsubmission_help', 'assign');
+        }
+
         $data = new stdClass();
         $adminconfig = $this->get_admin_config();
         $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
@@ -5338,10 +5353,20 @@ class assign {
                     $gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
                 }
                 $gradeddate = $gradebookgrade->dategraded;
-                if (isset($grade->grader) && $grade->grader > 0) {
-                    $grader = $DB->get_record('user', array('id' => $grade->grader));
-                } else if (isset($gradebookgrade->usermodified) && $gradebookgrade->usermodified > 0) {
-                    $grader = $DB->get_record('user', array('id' => $gradebookgrade->usermodified));
+
+                // Only display the grader if it is in the right state.
+                if (in_array($gradingstatus, [ASSIGN_GRADING_STATUS_GRADED, ASSIGN_MARKING_WORKFLOW_STATE_RELEASED])){
+                    if (isset($grade->grader) && $grade->grader > 0) {
+                        $grader = $DB->get_record('user', array('id' => $grade->grader));
+                    } else if (isset($gradebookgrade->usermodified)
+                        && $gradebookgrade->usermodified > 0
+                        && has_capability('mod/assign:grade', $this->get_context(), $gradebookgrade->usermodified)) {
+                        // Grader not provided. Check that usermodified is a user who can grade.
+                        // Case 1: When an assignment is reopened an empty assign_grade is created so the feedback
+                        // plugin can know which attempt it's referring to. In this case, usermodifed is a student.
+                        // Case 2: When an assignment's grade is overrided via the gradebook, usermodified is a grader
+                        $grader = $DB->get_record('user', array('id' => $gradebookgrade->usermodified));
+                    }
                 }
             }
 
index 9a8cbf4..e681e56 100644 (file)
@@ -55,9 +55,11 @@ class book_chapter_edit_form extends moodleform {
             );
         }
 
-        $mform->addElement('text', 'title', get_string('chaptertitle', 'mod_book'), array('size'=>'30'));
+        $mform->addElement('text', 'title', get_string('chaptertitle', 'mod_book'),
+            ['size' => '30', 'maxlength' => '255']);
         $mform->setType('title', PARAM_RAW);
         $mform->addRule('title', null, 'required', null, 'client');
+        $mform->addRule('title', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
 
         $mform->addElement('advcheckbox', 'subchapter', get_string('subchapter', 'mod_book'), $disabledmsg);
 
index b8ff382..023bf31 100644 (file)
     display: none;
 }
 
-.path-mod-workshop #id_rubric-grid-wrapper .rubric-grid {
+.path-mod-workshop .mform.frozen #id_rubric-grid-wrapper,
+.path-mod-workshop #id_rubric-grid-wrapper {
     margin-left: auto;
     margin-right: auto;
+    width: 100%;
+}
+
+.path-mod-workshop .mform.frozen #id_rubric-grid-wrapper .checkbox,
+.path-mod-workshop .assessmentform.rubric.grid #id_rubric-grid-wrapper .checkbox {
+    max-width: 100%;
+    flex: 0 0 100%;
+    text-align: left;
+}
+
+@media all and (-ms-high-contrast: none) {  /* IE10 & IE11 hack */
+    .path-mod-workshop .mform.frozen .rubric-grid,
+    .path-mod-workshop .assessmentform .rubric-grid {
+        width: 100%;
+        table-layout: fixed;
+    }
 }
 
 .path-mod-workshop .mform.frozen #id_rubric-grid-wrapper .fitem .felement,
index 3b93673..b0df2fd 100644 (file)
@@ -26,6 +26,7 @@ form.mform fieldset#id_previewareaheader div.ddarea {
 .que.ddimageortext div.droparea .dropzones {
     position: absolute;
     top: 0;
+    /*rtl:ignore*/
     left: 0;
 }
 
index 7fc19ff..ec1a81f 100644 (file)
@@ -165,7 +165,7 @@ $filemanagercontext = $editoroptions['context'];
 $filemanageroptions = array('maxbytes'       => $CFG->maxbytes,
                              'subdirs'        => 0,
                              'maxfiles'       => 1,
-                             'accepted_types' => 'web_image');
+                             'accepted_types' => 'optimised_image');
 file_prepare_draft_area($draftitemid, $filemanagercontext->id, 'user', 'newicon', 0, $filemanageroptions);
 $user->imagefile = $draftitemid;
 // Create form.
index 07fc5e0..fea54a6 100644 (file)
@@ -145,7 +145,7 @@ $filemanagercontext = $editoroptions['context'];
 $filemanageroptions = array('maxbytes'       => $CFG->maxbytes,
                              'subdirs'        => 0,
                              'maxfiles'       => 1,
-                             'accepted_types' => 'web_image');
+                             'accepted_types' => 'optimised_image');
 file_prepare_draft_area($draftitemid, $filemanagercontext->id, 'user', 'newicon', 0, $filemanageroptions);
 $user->imagefile = $draftitemid;
 // Create form.
index 0d7daad..90c9bd4 100644 (file)
@@ -574,7 +574,7 @@ class core_user_external extends external_api {
         $filemanageroptions = array('maxbytes' => $CFG->maxbytes,
                 'subdirs'        => 0,
                 'maxfiles'       => 1,
-                'accepted_types' => 'web_image');
+                'accepted_types' => 'optimised_image');
 
         $transaction = $DB->start_delegated_transaction();
 
@@ -1707,7 +1707,12 @@ class core_user_external extends external_api {
             throw new moodle_exception('noprofileedit', 'auth');
         }
 
-        $filemanageroptions = array('maxbytes' => $CFG->maxbytes, 'subdirs' => 0, 'maxfiles' => 1, 'accepted_types' => 'web_image');
+        $filemanageroptions = array(
+            'maxbytes' => $CFG->maxbytes,
+            'subdirs' => 0,
+            'maxfiles' => 1,
+            'accepted_types' => 'optimised_image'
+        );
         $user->deletepicture = $params['delete'];
         $user->imagefile = $params['draftitemid'];
         $success = core_user::update_picture($user, $filemanageroptions);
index e2315b2..ae78361 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2020061501.07;              // 20200615      = branching date YYYYMMDD - do not modify!
+$version  = 2020061501.08;              // 20200615      = branching date YYYYMMDD - do not modify!
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 $release  = '3.9.1+ (Build: 20200814)'; // Human-friendly version name