From c14e697ee6f226422232f7b6e19e6118f265098f Mon Sep 17 00:00:00 2001 From: Mathieu Petit-Clair Date: Tue, 16 May 2017 15:00:51 +0800 Subject: [PATCH 01/38] MDL-58952 admin: use valid default for language selection dropdown. --- admin/registration/forms.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/registration/forms.php b/admin/registration/forms.php index ddf2db65bd8..c4373d107d6 100644 --- a/admin/registration/forms.php +++ b/admin/registration/forms.php @@ -232,7 +232,7 @@ class site_registration_form extends moodleform { } $language = get_config('hub', 'site_language_' . $cleanhuburl); if ($language === false) { - $language = current_language(); + $language = explode('_', current_language())[0]; } $geolocation = get_config('hub', 'site_geolocation_' . $cleanhuburl); $contactable = get_config('hub', 'site_contactable_' . $cleanhuburl); From de4f0f25eac9558bd97d67be5e9b4598d4b8d8a3 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Wed, 24 May 2017 15:05:32 +0100 Subject: [PATCH 02/38] MDL-59048 user tours: avoid white-on-white text in Clean --- theme/bootstrapbase/less/moodle/tool_usertours.less | 1 + theme/bootstrapbase/style/moodle.css | 1 + 2 files changed, 2 insertions(+) diff --git a/theme/bootstrapbase/less/moodle/tool_usertours.less b/theme/bootstrapbase/less/moodle/tool_usertours.less index 3ea1e6f4f95..88fbccaa915 100644 --- a/theme/bootstrapbase/less/moodle/tool_usertours.less +++ b/theme/bootstrapbase/less/moodle/tool_usertours.less @@ -37,6 +37,7 @@ div[data-flexitour="step-background-fader"], span[data-flexitour="container"] { div[data-role="flexitour-step"] { background-color: #fff; + color: #333; border-radius: 6px; border: 1px solid rgba(0, 0, 0, .2); box-shadow: 0 5px 10px rgba(0, 0, 0, .2); diff --git a/theme/bootstrapbase/style/moodle.css b/theme/bootstrapbase/style/moodle.css index d8496ebbaed..0907f8ecb50 100644 --- a/theme/bootstrapbase/style/moodle.css +++ b/theme/bootstrapbase/style/moodle.css @@ -19334,6 +19334,7 @@ div[data-flexitour="step-background-fader"], } span[data-flexitour="container"] div[data-role="flexitour-step"] { background-color: #fff; + color: #333; border-radius: 6px; border: 1px solid rgba(0, 0, 0, 0.2); box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); From 797a70c0ecfd58c7e3f323eb688103f9e6e3941e Mon Sep 17 00:00:00 2001 From: David Balch Date: Wed, 17 May 2017 17:52:39 +0100 Subject: [PATCH 03/38] MDL-58919 assignsubmission_file: Add accepted file types default. New setting "defaultacceptedfiletypes" to define default accepted file types in File submissions. --- mod/assign/submission/file/lang/en/assignsubmission_file.php | 1 + mod/assign/submission/file/locallib.php | 3 +++ mod/assign/submission/file/settings.php | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/mod/assign/submission/file/lang/en/assignsubmission_file.php b/mod/assign/submission/file/lang/en/assignsubmission_file.php index 381adb28b0f..309cd8a5ac1 100644 --- a/mod/assign/submission/file/lang/en/assignsubmission_file.php +++ b/mod/assign/submission/file/lang/en/assignsubmission_file.php @@ -29,6 +29,7 @@ $string['configmaxbytes'] = 'Maximum file size'; $string['countfiles'] = '{$a} files'; $string['default'] = 'Enabled by default'; $string['default_help'] = 'If set, this submission method will be enabled by default for all new assignments.'; +$string['defaultacceptedfiletypes'] = 'Default accepted file types'; $string['enabled'] = 'File submissions'; $string['enabled_help'] = 'If enabled, students are able to upload one or more files as their submission.'; $string['eventassessableuploaded'] = 'A file has been uploaded.'; diff --git a/mod/assign/submission/file/locallib.php b/mod/assign/submission/file/locallib.php index e2fe32f698b..5b60450ffa1 100644 --- a/mod/assign/submission/file/locallib.php +++ b/mod/assign/submission/file/locallib.php @@ -72,6 +72,9 @@ class assign_submission_file extends assign_submission_plugin { $defaultmaxfilesubmissions = $this->get_config('maxfilesubmissions'); $defaultmaxsubmissionsizebytes = $this->get_config('maxsubmissionsizebytes'); $defaultfiletypes = (string)$this->get_config('filetypeslist'); + if ($defaultfiletypes == '') { + $defaultfiletypes = (string)get_config('assignsubmission_file', 'filetypes'); + } $settings = array(); $options = array(); diff --git a/mod/assign/submission/file/settings.php b/mod/assign/submission/file/settings.php index 4d7eb220e1f..f96217fd2c9 100644 --- a/mod/assign/submission/file/settings.php +++ b/mod/assign/submission/file/settings.php @@ -32,6 +32,10 @@ $settings->add(new admin_setting_configtext('assignsubmission_file/maxfiles', new lang_string('maxfiles', 'assignsubmission_file'), new lang_string('maxfiles_help', 'assignsubmission_file'), 20, PARAM_INT)); +$settings->add(new admin_setting_configtext('assignsubmission_file/filetypes', + new lang_string('defaultacceptedfiletypes', 'assignsubmission_file'), + new lang_string('acceptedfiletypes_help', 'assignsubmission_file'), '', PARAM_TEXT)); + if (isset($CFG->maxbytes)) { $name = new lang_string('maximumsubmissionsize', 'assignsubmission_file'); From 053c55a76c5b658ae0866f3a3fde3d6bb636c6f2 Mon Sep 17 00:00:00 2001 From: David Balch Date: Thu, 18 May 2017 09:15:42 +0100 Subject: [PATCH 04/38] MDL-58919 assignsubmission_file: Wider accepted file types input field --- mod/assign/submission/file/locallib.php | 2 +- mod/assign/submission/file/settings.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mod/assign/submission/file/locallib.php b/mod/assign/submission/file/locallib.php index 5b60450ffa1..8df609bb7b7 100644 --- a/mod/assign/submission/file/locallib.php +++ b/mod/assign/submission/file/locallib.php @@ -111,7 +111,7 @@ class assign_submission_file extends assign_submission_plugin { 'notchecked'); $name = get_string('acceptedfiletypes', 'assignsubmission_file'); - $mform->addElement('text', 'assignsubmission_file_filetypes', $name); + $mform->addElement('text', 'assignsubmission_file_filetypes', $name, array('size' => '60')); $mform->addHelpButton('assignsubmission_file_filetypes', 'acceptedfiletypes', 'assignsubmission_file'); $mform->setType('assignsubmission_file_filetypes', PARAM_RAW); $mform->setDefault('assignsubmission_file_filetypes', $defaultfiletypes); diff --git a/mod/assign/submission/file/settings.php b/mod/assign/submission/file/settings.php index f96217fd2c9..c90509e5672 100644 --- a/mod/assign/submission/file/settings.php +++ b/mod/assign/submission/file/settings.php @@ -34,7 +34,7 @@ $settings->add(new admin_setting_configtext('assignsubmission_file/maxfiles', $settings->add(new admin_setting_configtext('assignsubmission_file/filetypes', new lang_string('defaultacceptedfiletypes', 'assignsubmission_file'), - new lang_string('acceptedfiletypes_help', 'assignsubmission_file'), '', PARAM_TEXT)); + new lang_string('acceptedfiletypes_help', 'assignsubmission_file'), '', PARAM_TEXT, 60)); if (isset($CFG->maxbytes)) { From c05dc071b0a1bbd0c98bcbfe8c507d21443bc6d8 Mon Sep 17 00:00:00 2001 From: Adrian Greeve Date: Mon, 12 Jun 2017 12:02:08 +0800 Subject: [PATCH 05/38] MDL-58729 database: Improve performance of CLI tool. Imporve the speed with which mysql_collation converts to a different collation. --- admin/cli/mysql_collation.php | 44 +++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/admin/cli/mysql_collation.php b/admin/cli/mysql_collation.php index adaadbba6c7..ae3c709816b 100644 --- a/admin/cli/mysql_collation.php +++ b/admin/cli/mysql_collation.php @@ -53,7 +53,7 @@ Options: -h, --help Print out this help Example: -\$ sudo -u www-data /usr/bin/php admin/cli/mysql_collation.php --collation=utf8_general_ci +\$ sudo -u www-data /usr/bin/php admin/cli/mysql_collation.php --collation=utf8mb4_unicode_ci "; if (!empty($options['collation'])) { @@ -145,9 +145,22 @@ if (!empty($options['collation'])) { $skipped++; } else { - $DB->change_database_structure("ALTER TABLE $table->name DEFAULT CHARACTER SET $charset DEFAULT COLLATE = $collation"); - echo "CONVERTED\n"; - $converted++; + try { + $DB->change_database_structure("ALTER TABLE $table->name CONVERT TO CHARACTER SET $charset COLLATE $collation"); + echo "CONVERTED\n"; + $converted++; + } catch (ddl_exception $e) { + $result = mysql_set_row_format($table->name, $charset, $collation, $engine); + if ($result) { + echo "CONVERTED\n"; + $converted++; + } else { + // We don't know what the problem is. Stop the conversion. + cli_error("Error: Tried to convert $table->name, but there was a problem. Please check the details of this + table and try again."); + die(); + } + } } $sql = "SHOW FULL COLUMNS FROM $table->name WHERE collation IS NOT NULL"; @@ -290,3 +303,26 @@ function mysql_get_column_collations($tablename) { $rs->close(); return $collations; } + +function mysql_set_row_format($tablename, $charset, $collation, $engine) { + global $DB; + + $sql = "SELECT row_format + FROM INFORMATION_SCHEMA.TABLES + WHERE table_schema = DATABASE() AND table_name = ?"; + $rs = $DB->get_record_sql($sql, array($tablename)); + if ($rs) { + if ($rs->row_format == 'Compact' || $rs->row_format == 'Redundant') { + $rowformat = $DB->get_row_format_sql($engine, $collation); + // Try to convert to compressed format and then try updating the collation again. + $DB->change_database_structure("ALTER TABLE $tablename $rowformat"); + $DB->change_database_structure("ALTER TABLE $tablename CONVERT TO CHARACTER SET $charset COLLATE $collation"); + } else { + // Row format may not be the problem. Can not diagnose problem. Send fail reply. + return false; + } + } else { + return false; + } + return true; +} From 7352805df1eea27340c576c31aee79f246fddcb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 9 Jun 2017 10:37:29 +0200 Subject: [PATCH 06/38] MDL-58985 course: Fix display of availability information The availability info text can be either a one line short text such as "Hidden from students" but it can also be a long HTML formatted text with the list of all restricted access conditions. For the latter, using bootstrap labels was not appropriate. This extends the template context data with some boolean flags that allow to better distinguish the source and meaning of the availability information and display it accordingly. Credit goes to Marina Glancy for the solution idea. --- course/format/renderer.php | 4 +- course/renderer.php | 23 +++++++++-- .../templates/core/availability_info.mustache | 38 ++++++++++++++++++- .../templates/core/availability_info.mustache | 38 ++++++++++++++++++- 4 files changed, 94 insertions(+), 9 deletions(-) diff --git a/course/format/renderer.php b/course/format/renderer.php index f16bdd61ca3..b630ad8bef4 100644 --- a/course/format/renderer.php +++ b/course/format/renderer.php @@ -559,7 +559,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base { // so there is definitely something to print. $formattedinfo = \core_availability\info::format_info( $section->availableinfo, $section->course); - $o .= $this->courserenderer->availability_info($formattedinfo); + $o .= $this->courserenderer->availability_info($formattedinfo, 'isrestricted'); } } else if ($canviewhidden && !empty($CFG->enableavailability)) { // Check if there is an availability restriction. @@ -568,7 +568,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base { if ($fullinfo) { $formattedinfo = \core_availability\info::format_info( $fullinfo, $section->course); - $o .= $this->courserenderer->availability_info($formattedinfo); + $o .= $this->courserenderer->availability_info($formattedinfo, 'isrestricted isfullinfo'); } } return $o; diff --git a/course/renderer.php b/course/renderer.php index bee3fe78f70..565f90c3f80 100644 --- a/course/renderer.php +++ b/course/renderer.php @@ -728,7 +728,24 @@ class core_course_renderer extends plugin_renderer_base { * @return string */ public function availability_info($text, $additionalclasses = '') { + $data = ['text' => $text, 'classes' => $additionalclasses]; + $additionalclasses = array_filter(explode(' ', $additionalclasses)); + + if (in_array('ishidden', $additionalclasses)) { + $data['ishidden'] = 1; + + } else if (in_array('isstealth', $additionalclasses)) { + $data['isstealth'] = 1; + + } else if (in_array('isrestricted', $additionalclasses)) { + $data['isrestricted'] = 1; + + if (in_array('isfullinfo', $additionalclasses)) { + $data['isfullinfo'] = 1; + } + } + return $this->render_from_template('core/availability_info', $data); } @@ -752,7 +769,7 @@ class core_course_renderer extends plugin_renderer_base { if (!empty($mod->availableinfo)) { $formattedinfo = \core_availability\info::format_info( $mod->availableinfo, $mod->get_course()); - $output = $this->availability_info($formattedinfo); + $output = $this->availability_info($formattedinfo, 'isrestricted'); } return $output; } @@ -775,9 +792,9 @@ class core_course_renderer extends plugin_renderer_base { // Display information about conditional availability. // Don't add availability information if user is not editing and activity is hidden. if ($mod->visible || $this->page->user_is_editing()) { - $hidinfoclass = ''; + $hidinfoclass = 'isrestricted isfullinfo'; if (!$mod->visible) { - $hidinfoclass = 'hide'; + $hidinfoclass .= ' hide'; } $ci = new \core_availability\info_module($mod); $fullinfo = $ci->get_full_information(); diff --git a/theme/boost/templates/core/availability_info.mustache b/theme/boost/templates/core/availability_info.mustache index 3cc64343b67..56cf9c37528 100644 --- a/theme/boost/templates/core/availability_info.mustache +++ b/theme/boost/templates/core/availability_info.mustache @@ -15,13 +15,47 @@ along with Moodle. If not, see . }} {{! - Availability info. + @template core/availability_info + + Renders the availability info on the course outline page. + + Availability info can be displayed for activity modules or whole course + sections. Activity modules can be either hidden from students, or available + but not shown on course page (stealth), or the access can be restricted by + configured conditions. Sections can be hidden. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * classes String list of CSS classes for the wrapping element + * text HTML formatted text with the actual availability information + * ishidden Boolean flag indiciating that the item is hidden from students + * isstealth Boolean flag indicating that the item is in stealth mode + * isrestricted Boolean flag indicating that restricted access conditions apply + * isfullinfo Boolean flag indicating that the full list of restricted + access conditions is displayed (aka teacher's view). Example context (json): - { "classes": "", "text": "This activity is not available" } + { + "classes": "", + "text": "Not available unless:
  • It is on or after 8 June 2012
", + "ishidden": 0, + "isstealth": 0, + "isrestricted": 1, + "isfullinfo": 1 + } }} {{#text}}
+ {{^isrestricted}} {{{text}}} + {{/isrestricted}} + {{#isrestricted}} + {{#str}}restricted, core{{/str}} {{{text}}} + {{/isrestricted}}
{{/text}} diff --git a/theme/bootstrapbase/templates/core/availability_info.mustache b/theme/bootstrapbase/templates/core/availability_info.mustache index fd81eb50ea1..ee90c956307 100644 --- a/theme/bootstrapbase/templates/core/availability_info.mustache +++ b/theme/bootstrapbase/templates/core/availability_info.mustache @@ -15,13 +15,47 @@ along with Moodle. If not, see . }} {{! - Availability info. + @template core/availability_info + + Renders the availability info on the course outline page. + + Availability info can be displayed for activity modules or whole course + sections. Activity modules can be either hidden from students, or available + but not shown on course page (stealth), or the access can be restricted by + configured conditions. Sections can be hidden. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * classes String list of CSS classes for the wrapping element + * text HTML formatted text with the actual availability information + * ishidden Boolean flag indiciating that the item is hidden from students + * isstealth Boolean flag indicating that the item is in stealth mode + * isrestricted Boolean flag indicating that restricted access conditions apply + * isfullinfo Boolean flag indicating that the full list of restricted + access conditions is displayed (aka teacher's view). Example context (json): - { "classes": "", "text": "This activity is not available" } + { + "classes": "", + "text": "Not available unless:
  • It is on or after 8 June 2012
", + "ishidden": 0, + "isstealth": 0, + "isrestricted": 1, + "isfullinfo": 1 + } }} {{#text}}
+ {{^isrestricted}} {{{text}}} + {{/isrestricted}} + {{#isrestricted}} + {{#str}}restricted, core{{/str}} {{{text}}} + {{/isrestricted}}
{{/text}} From 4a0a23e98fa9af3ed6f12480131a9a6b34ebbebf Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 9 Jun 2017 16:22:36 +0800 Subject: [PATCH 07/38] MDL-58472 videojs: Force manual initialise If we let videojs auto-init the video/audio tags present in the page - we cannot guarantee that the youtube library will be loaded by the time video.js is processing the tag. Video.js uses the presence of a data-setup attibute to signal that it should auto-init this tag, so we have to rename our config attribute to something else. --- media/player/videojs/amd/build/loader.min.js | 2 +- media/player/videojs/amd/src/loader.js | 2 +- media/player/videojs/classes/plugin.php | 7 ++++++- media/player/videojs/tests/player_test.php | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/media/player/videojs/amd/build/loader.min.js b/media/player/videojs/amd/build/loader.min.js index 6c77c7f187d..d57bd6098af 100644 --- a/media/player/videojs/amd/build/loader.min.js +++ b/media/player/videojs/amd/build/loader.min.js @@ -1 +1 @@ -define(["jquery","core/event"],function(a,b){var c,d=function(d){c=d,e(null,a("body")),b.getLegacyEvents().done(function(b){a(document).on(b.FILTER_CONTENT_UPDATED,e)})},e=function(b,d){var e=".mediaplugin_videojs";d.find(e).addBack(e).find("audio, video").each(function(){var b=a(this).attr("id"),d=a(this).data("setup"),e=["media_videojs/video-lazy"];d.techOrder&&d.techOrder.indexOf("youtube")!==-1&&e.push("media_videojs/Youtube-lazy"),require(e,function(a){c&&(c(a),c=null),a(b,d)})})};return{setUp:d}}); \ No newline at end of file +define(["jquery","core/event"],function(a,b){var c,d=function(d){c=d,e(null,a("body")),b.getLegacyEvents().done(function(b){a(document).on(b.FILTER_CONTENT_UPDATED,e)})},e=function(b,d){var e=".mediaplugin_videojs";d.find(e).addBack(e).find("audio, video").each(function(){var b=a(this).attr("id"),d=a(this).data("setup-lazy"),e=["media_videojs/video-lazy"];d.techOrder&&d.techOrder.indexOf("youtube")!==-1&&e.push("media_videojs/Youtube-lazy"),require(e,function(a){c&&(c(a),c=null),a(b,d)})})};return{setUp:d}}); \ No newline at end of file diff --git a/media/player/videojs/amd/src/loader.js b/media/player/videojs/amd/src/loader.js index 2601cf888fa..427b609eea4 100644 --- a/media/player/videojs/amd/src/loader.js +++ b/media/player/videojs/amd/src/loader.js @@ -62,7 +62,7 @@ define(['jquery', 'core/event'], function($, Event) { .addBack(selector) .find('audio, video').each(function() { var id = $(this).attr('id'), - config = $(this).data('setup'), + config = $(this).data('setup-lazy'), modules = ['media_videojs/video-lazy']; if (config.techOrder && config.techOrder.indexOf('youtube') !== -1) { diff --git a/media/player/videojs/classes/plugin.php b/media/player/videojs/classes/plugin.php index f13352fe577..3d076b046f1 100644 --- a/media/player/videojs/classes/plugin.php +++ b/media/player/videojs/classes/plugin.php @@ -129,8 +129,13 @@ class media_videojs_plugin extends core_media_player_native { } // Attributes for the video/audio tag. + // We use data-setup-lazy as the attribute name for the config instead of + // data-setup because data-setup will cause video.js to load the player as soon as the library is loaded, + // which is BEFORE we have a chance to load any additional libraries (youtube). + // The data-setup-lazy is just a tag name that video.js does not recognise so we can manually initialise + // it when we are sure the dependencies are loaded. $attributes = [ - 'data-setup' => '{' . join(', ', $datasetup) . '}', + 'data-setup-lazy' => '{' . join(', ', $datasetup) . '}', 'id' => 'id_videojs_' . uniqid(), 'class' => get_config('media_videojs', $isaudio ? 'audiocssclass' : 'videocssclass') ]; diff --git a/media/player/videojs/tests/player_test.php b/media/player/videojs/tests/player_test.php index 43d698c2745..7bc50ead4b5 100644 --- a/media/player/videojs/tests/player_test.php +++ b/media/player/videojs/tests/player_test.php @@ -230,7 +230,7 @@ class media_videojs_testcase extends advanced_testcase { protected function youtube_plugin_engaged($t) { $this->assertContains('mediaplugin_videojs', $t); - $this->assertContains('data-setup="{"techOrder": ["youtube"]', $t); + $this->assertContains('data-setup-lazy="{"techOrder": ["youtube"]', $t); } /** From fe81ef397cec413abc71a0953a0e67be02db0aba Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Mon, 21 Mar 2016 10:20:11 +0800 Subject: [PATCH 08/38] MDL-51917 course completion: order activities in reports Thanks to Nick Phillips for initial patch --- lib/completionlib.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/completionlib.php b/lib/completionlib.php index 80c854242db..816c7d1ca2e 100644 --- a/lib/completionlib.php +++ b/lib/completionlib.php @@ -423,6 +423,21 @@ class completion_info { // Load criteria from database $records = (array)$DB->get_records('course_completion_criteria', $params); + // Order records so activities are in the same order as they appear on the course view page. + if ($records) { + $activitiesorder = array_keys(get_fast_modinfo($this->course)->get_cms()); + usort($records, function ($a, $b) use ($activitiesorder) { + $aidx = ($a->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) ? + array_search($a->moduleinstance, $activitiesorder) : false; + $bidx = ($b->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) ? + array_search($b->moduleinstance, $activitiesorder) : false; + if ($aidx === false || $bidx === false || $aidx == $bidx) { + return 0; + } + return ($aidx < $bidx) ? -1 : 1; + }); + } + // Build array of criteria objects $this->criteria = array(); foreach ($records as $record) { From 02375710049d31c6cdd21f2cbca16ed1b40440b8 Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Tue, 13 Jun 2017 11:22:33 +0100 Subject: [PATCH 09/38] MDL-58712 core_user: New function for retrieving file area info --- lib/filelib.php | 26 ++++++++- lib/tests/filelib_test.php | 111 +++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 3 deletions(-) diff --git a/lib/filelib.php b/lib/filelib.php index b1dafb9e3a2..08123829a38 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -503,9 +503,29 @@ function file_rewrite_pluginfile_urls($text, $file, $contextid, $component, $fil * (more information will be added as needed). */ function file_get_draft_area_info($draftitemid, $filepath = '/') { - global $CFG, $USER; + global $USER; $usercontext = context_user::instance($USER->id); + return file_get_file_area_info($usercontext->id, 'user', 'draft', $draftitemid, $filepath); +} + +/** + * Returns information about files in an area. + * + * @param int $contextid context id + * @param string $component component + * @param string $filearea file area name + * @param int $itemid item id or all files if not specified + * @param string $filepath path to the directory from which the information have to be retrieved. + * @return array with the following entries: + * 'filecount' => number of files in the area. + * 'filesize' => total size of the files in the area. + * 'foldercount' => number of folders in the area. + * 'filesize_without_references' => total size of the area excluding file references. + * @since Moodle 3.4 + */ +function file_get_file_area_info($contextid, $component, $filearea, $itemid = false, $filepath = '/') { + $fs = get_file_storage(); $results = array( @@ -516,9 +536,9 @@ function file_get_draft_area_info($draftitemid, $filepath = '/') { ); if ($filepath != '/') { - $draftfiles = $fs->get_directory_files($usercontext->id, 'user', 'draft', $draftitemid, $filepath, true, true); + $draftfiles = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath, true, true); } else { - $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id', true); + $draftfiles = $fs->get_area_files($contextid, $component, $filearea, $itemid, 'id', true); } foreach ($draftfiles as $file) { if ($file->is_directory()) { diff --git a/lib/tests/filelib_test.php b/lib/tests/filelib_test.php index ea18b514a2f..30a384918ac 100644 --- a/lib/tests/filelib_test.php +++ b/lib/tests/filelib_test.php @@ -1229,6 +1229,117 @@ EOF; $file = array_shift($files); $this->assertTrue($file->is_directory()); } + + /** + * Test file_get_draft_area_info. + */ + public function test_file_get_draft_area_info() { + global $USER; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $fs = get_file_storage(); + + $filerecord = array( + 'filename' => 'one.txt', + ); + $file = self::create_draft_file($filerecord); + $size = $file->get_filesize(); + $draftitemid = $file->get_itemid(); + // Add another file. + $filerecord = array( + 'itemid' => $draftitemid, + 'filename' => 'second.txt', + ); + $file = self::create_draft_file($filerecord); + $size += $file->get_filesize(); + + // Create directory. + $usercontext = context_user::instance($USER->id); + $dir = $fs->create_directory($usercontext->id, 'user', 'draft', $draftitemid, '/testsubdir/'); + // Add file to directory. + $filerecord = array( + 'itemid' => $draftitemid, + 'filename' => 'third.txt', + 'filepath' => '/testsubdir/', + ); + $file = self::create_draft_file($filerecord); + $size += $file->get_filesize(); + + $fileinfo = file_get_draft_area_info($draftitemid); + $this->assertEquals(3, $fileinfo['filecount']); + $this->assertEquals($size, $fileinfo['filesize']); + $this->assertEquals(2, $fileinfo['foldercount']); // Base and directory created. + $this->assertEquals($size, $fileinfo['filesize_without_references']); + + // Now get files from just one folder. + $fileinfo = file_get_draft_area_info($draftitemid, '/testsubdir/'); + $this->assertEquals(1, $fileinfo['filecount']); + $this->assertEquals($file->get_filesize(), $fileinfo['filesize']); + $this->assertEquals(0, $fileinfo['foldercount']); // No subdirectories inside the directory. + $this->assertEquals($file->get_filesize(), $fileinfo['filesize_without_references']); + + // Check we get the same results if we call file_get_file_area_info. + $fileinfo = file_get_file_area_info($usercontext->id, 'user', 'draft', $draftitemid); + $this->assertEquals(3, $fileinfo['filecount']); + $this->assertEquals($size, $fileinfo['filesize']); + $this->assertEquals(2, $fileinfo['foldercount']); // Base and directory created. + $this->assertEquals($size, $fileinfo['filesize_without_references']); + } + + /** + * Test file_get_file_area_info. + */ + public function test_file_get_file_area_info() { + global $USER; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $fs = get_file_storage(); + + $filerecord = array( + 'filename' => 'one.txt', + ); + $file = self::create_draft_file($filerecord); + $size = $file->get_filesize(); + $draftitemid = $file->get_itemid(); + // Add another file. + $filerecord = array( + 'itemid' => $draftitemid, + 'filename' => 'second.txt', + ); + $file = self::create_draft_file($filerecord); + $size += $file->get_filesize(); + + // Create directory. + $usercontext = context_user::instance($USER->id); + $dir = $fs->create_directory($usercontext->id, 'user', 'draft', $draftitemid, '/testsubdir/'); + // Add file to directory. + $filerecord = array( + 'itemid' => $draftitemid, + 'filename' => 'third.txt', + 'filepath' => '/testsubdir/', + ); + $file = self::create_draft_file($filerecord); + $size += $file->get_filesize(); + + // Add files to user private file area. + $options = array('subdirs' => 1, 'maxfiles' => 3); + file_merge_files_from_draft_area_into_filearea($draftitemid, $file->get_contextid(), 'user', 'private', 0, $options); + + $fileinfo = file_get_file_area_info($usercontext->id, 'user', 'private'); + $this->assertEquals(3, $fileinfo['filecount']); + $this->assertEquals($size, $fileinfo['filesize']); + $this->assertEquals(2, $fileinfo['foldercount']); // Base and directory created. + $this->assertEquals($size, $fileinfo['filesize_without_references']); + + // Now get files from just one folder. + $fileinfo = file_get_file_area_info($usercontext->id, 'user', 'private', 0, '/testsubdir/'); + $this->assertEquals(1, $fileinfo['filecount']); + $this->assertEquals($file->get_filesize(), $fileinfo['filesize']); + $this->assertEquals(0, $fileinfo['foldercount']); // No subdirectories inside the directory. + $this->assertEquals($file->get_filesize(), $fileinfo['filesize_without_references']); + } } /** From a4beef4e3137c3c0b35a4888df555d0ff2504abd Mon Sep 17 00:00:00 2001 From: Ankit Agarwal Date: Wed, 14 Jun 2017 14:32:53 +0530 Subject: [PATCH 10/38] MDL-44961 logs: Keep log dates constant during restore --- .../moodle2/restore_tool_log_logstore_subplugin.class.php | 3 +-- backup/moodle2/restore_stepslib.php | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/admin/tool/log/backup/moodle2/restore_tool_log_logstore_subplugin.class.php b/admin/tool/log/backup/moodle2/restore_tool_log_logstore_subplugin.class.php index 0c00f00880a..c48c2c86c24 100644 --- a/admin/tool/log/backup/moodle2/restore_tool_log_logstore_subplugin.class.php +++ b/admin/tool/log/backup/moodle2/restore_tool_log_logstore_subplugin.class.php @@ -84,8 +84,7 @@ abstract class restore_tool_log_logstore_subplugin extends restore_subplugin { } } - // Roll dates. - $data->timecreated = $this->apply_date_offset($data->timecreated); + // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961. // Revert other to its original php way. $data->other = unserialize(base64_decode($data->other)); diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 5d607ef780e..b539a4e60f4 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -3083,7 +3083,8 @@ class restore_course_logs_structure_step extends restore_structure_step { $data = (object)($data); - $data->time = $this->apply_date_offset($data->time); + // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961. + $data->userid = $this->get_mappingid('user', $data->userid); $data->course = $this->get_courseid(); $data->cmid = 0; @@ -3130,7 +3131,8 @@ class restore_activity_logs_structure_step extends restore_course_logs_structure $data = (object)($data); - $data->time = $this->apply_date_offset($data->time); + // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961. + $data->userid = $this->get_mappingid('user', $data->userid); $data->course = $this->get_courseid(); $data->cmid = $this->task->get_moduleid(); From 7af8b56ab4644cb4688a6d1d3c78cfac8aeba20b Mon Sep 17 00:00:00 2001 From: Simey Lameze Date: Wed, 7 Jun 2017 13:00:48 +0800 Subject: [PATCH 11/38] MDL-57770 tool_lp: style fixes for the competency rule dialog --- admin/tool/lp/templates/competency_rule_config.mustache | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/admin/tool/lp/templates/competency_rule_config.mustache b/admin/tool/lp/templates/competency_rule_config.mustache index b0e9db8440b..167a4237e8e 100644 --- a/admin/tool/lp/templates/competency_rule_config.mustache +++ b/admin/tool/lp/templates/competency_rule_config.mustache @@ -81,10 +81,11 @@ {{/config}} -
+
{{#config}} {{/config}}
+
From f74ac6e704869d5a09613d534314a3e5918c3d8a Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Tue, 13 Jun 2017 11:56:19 +0100 Subject: [PATCH 12/38] MDL-58712 core_user: New WS core_user_get_private_files_info --- lib/db/services.php | 9 +++++ lib/tests/filelib_test.php | 2 +- user/externallib.php | 72 +++++++++++++++++++++++++++++++++ user/tests/externallib_test.php | 54 +++++++++++++++++++++++++ version.php | 2 +- 5 files changed, 137 insertions(+), 2 deletions(-) diff --git a/lib/db/services.php b/lib/db/services.php index 002640edb58..7bb91d138be 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1206,6 +1206,15 @@ $functions = array( 'type' => 'write', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), + 'core_user_get_private_files_info' => array( + 'classname' => 'core_user_external', + 'methodname' => 'get_private_files_info', + 'classpath' => 'user/externallib.php', + 'description' => 'Returns general information about files in the user private files area.', + 'type' => 'read', + 'capabilities' => 'moodle/user:manageownfiles', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ), // Competencies functions. 'core_competency_create_competency_framework' => array( diff --git a/lib/tests/filelib_test.php b/lib/tests/filelib_test.php index 30a384918ac..a367aaaeaa1 100644 --- a/lib/tests/filelib_test.php +++ b/lib/tests/filelib_test.php @@ -1287,7 +1287,7 @@ EOF; $this->assertEquals($size, $fileinfo['filesize_without_references']); } - /** + /** * Test file_get_file_area_info. */ public function test_file_get_file_area_info() { diff --git a/user/externallib.php b/user/externallib.php index 5ead62ce4e9..2eea9f32a06 100644 --- a/user/externallib.php +++ b/user/externallib.php @@ -1860,4 +1860,76 @@ class core_user_external extends external_api { ) ); } + + /** + * Returns description of method parameters. + * + * @return external_function_parameters + * @since Moodle 3.4 + */ + public static function get_private_files_info_parameters() { + return new external_function_parameters( + array( + 'userid' => new external_value(PARAM_INT, 'Id of the user, default to current user.', VALUE_DEFAULT, 0) + ) + ); + } + + /** + * Returns general information about files in the user private files area. + * + * @param int $userid Id of the user, default to current user. + * @return array of warnings and file area information + * @since Moodle 3.4 + * @throws moodle_exception + */ + public static function get_private_files_info($userid = 0) { + global $CFG, $USER; + require_once($CFG->libdir . '/filelib.php'); + + $params = self::validate_parameters(self::get_private_files_info_parameters(), array('userid' => $userid)); + $warnings = array(); + + $context = context_system::instance(); + self::validate_context($context); + + if (empty($params['userid']) || $params['userid'] == $USER->id) { + $usercontext = context_user::instance($USER->id); + require_capability('moodle/user:manageownfiles', $usercontext); + } else { + $user = core_user::get_user($params['userid'], '*', MUST_EXIST); + core_user::require_active_user($user); + // Only admins can retrieve other users information. + require_capability('moodle/site:config', $context); + $usercontext = context_user::instance($user->id); + } + + $fileareainfo = file_get_file_area_info($usercontext->id, 'user', 'private'); + + $result = array(); + $result['filecount'] = $fileareainfo['filecount']; + $result['foldercount'] = $fileareainfo['foldercount']; + $result['filesize'] = $fileareainfo['filesize']; + $result['filesizewithoutreferences'] = $fileareainfo['filesize_without_references']; + $result['warnings'] = $warnings; + return $result; + } + + /** + * Returns description of method result value. + * + * @return external_description + * @since Moodle 3.4 + */ + public static function get_private_files_info_returns() { + return new external_single_structure( + array( + 'filecount' => new external_value(PARAM_INT, 'Number of files in the area.'), + 'foldercount' => new external_value(PARAM_INT, 'Number of folders in the area.'), + 'filesize' => new external_value(PARAM_INT, 'Total size of the files in the area.'), + 'filesizewithoutreferences' => new external_value(PARAM_INT, 'Total size of the area excluding file references'), + 'warnings' => new external_warnings() + ) + ); + } } diff --git a/user/tests/externallib_test.php b/user/tests/externallib_test.php index be52830487f..bf9edd81fdc 100644 --- a/user/tests/externallib_test.php +++ b/user/tests/externallib_test.php @@ -1148,6 +1148,60 @@ class core_user_externallib_testcase extends externallib_advanced_testcase { } catch (Exception $e) { $this->fail('Expecting \'usernotfullysetup\' moodle_exception to be thrown.'); } + } + /** + * Test get_private_files_info + */ + public function test_get_private_files_info() { + + $this->resetAfterTest(true); + $user = self::getDataGenerator()->create_user(); + $this->setUser($user); + $usercontext = context_user::instance($user->id); + + $filerecord = array( + 'contextid' => $usercontext->id, + 'component' => 'user', + 'filearea' => 'private', + 'itemid' => 0, + 'filepath' => '/', + 'filename' => 'thefile', + ); + + $fs = get_file_storage(); + $file = $fs->create_file_from_string($filerecord, 'abc'); + + // Get my private files information. + $result = core_user_external::get_private_files_info(); + $result = external_api::clean_returnvalue(core_user_external::get_private_files_info_returns(), $result); + $this->assertEquals(1, $result['filecount']); + $this->assertEquals($file->get_filesize(), $result['filesize']); + $this->assertEquals(1, $result['foldercount']); // Base directory. + $this->assertEquals($file->get_filesize(), $result['filesizewithoutreferences']); + + // As admin, get user information. + $this->setAdminUser(); + $result = core_user_external::get_private_files_info($user->id); + $result = external_api::clean_returnvalue(core_user_external::get_private_files_info_returns(), $result); + $this->assertEquals(1, $result['filecount']); + $this->assertEquals($file->get_filesize(), $result['filesize']); + $this->assertEquals(1, $result['foldercount']); // Base directory. + $this->assertEquals($file->get_filesize(), $result['filesizewithoutreferences']); + } + + /** + * Test get_private_files_info missing permissions. + */ + public function test_get_private_files_info_missing_permissions() { + + $this->resetAfterTest(true); + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $this->setUser($user1); + + $this->setExpectedException('required_capability_exception'); + // Try to retrieve other user private files info. + core_user_external::get_private_files_info($user2->id); } } diff --git a/version.php b/version.php index e90db3493dd..597192ca5a9 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2017060800.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2017060800.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. From 30e891e08fcdf0bca4982e037bedc4672484f19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Thu, 15 Jun 2017 20:51:58 +0200 Subject: [PATCH 13/38] MDL-46418 repositories: Fix how enabled repositories are populated Repository instances are stored in the 'repository' table. Repositories in the table are either 'Enabled and visible' or 'Enabled but hidden'. Hidden repositories still serve their files, but are not visible in the filepicker UI. Disabling a repository instance removes its record from the table. In the original implementation of the plugin manager (see b9934a17), the method plugintype_repository::get_enabled_repositories() correctly returned all records from the repository table. Then as a part of the bigger refactoring in MDL-41437, the commit bde002b8 replaced the original method with the new get_enabled_plugins() one which started to return visible repositories only. As a consequence, the admin tree stopped populating setting page nodes for hidden repository instances. So attempting to visit their setting page threw a section error. Credit goes to Ike Quigley for debugging and tracing this down. This patch fixes the way how the list of enabled repositories is populated by the plugin manager so that both visible and hidden repositories are returned again. This does not affect the filepicker itself as it is using its own methods for obtaining the list. --- lib/classes/plugininfo/repository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/classes/plugininfo/repository.php b/lib/classes/plugininfo/repository.php index 861b24d2547..689edde3c91 100644 --- a/lib/classes/plugininfo/repository.php +++ b/lib/classes/plugininfo/repository.php @@ -37,7 +37,7 @@ class repository extends base { */ public static function get_enabled_plugins() { global $DB; - return $DB->get_records_menu('repository', array('visible'=>1), 'type ASC', 'type, type AS val'); + return $DB->get_records_menu('repository', null, 'type ASC', 'type, type AS val'); } public function get_settings_section_name() { From 509679e203815b710a24650ea9f08d5d9778e690 Mon Sep 17 00:00:00 2001 From: Jean-Roch Meurisse - University of Namur - Belgium Date: Fri, 16 Jun 2017 09:58:41 +0800 Subject: [PATCH 14/38] MDL-59263 roles: Wrong order of parameters in calls to pix_icon function --- admin/roles/usersroles.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/roles/usersroles.php b/admin/roles/usersroles.php index 5e871e6198b..c455902c03e 100644 --- a/admin/roles/usersroles.php +++ b/admin/roles/usersroles.php @@ -193,8 +193,8 @@ function print_report_tree($contextid, $contexts, $systemcontext, $fullname, $al $strgoto = get_string('gotoassignroles', 'core_role', $a); $strcheck = get_string('checkuserspermissionshere', 'core_role', $a); } - echo ' ' . $OUTPUT->pix_icon('t/edit', 'core', $stredit) . ' '; - echo ' ' . $OUTPUT->pix_icon('t/preview', 'core', $strcheckpermissions) . ' '; + echo ' ' . $OUTPUT->pix_icon('t/edit', $stredit) . ' '; + echo ' ' . $OUTPUT->pix_icon('t/preview', $strcheckpermissions) . ' '; echo "

\n"; } } From 3b5865396ed8aa88aab311959e4109b59a115daa Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Wed, 17 May 2017 16:45:51 +0800 Subject: [PATCH 15/38] MDL-56979 group: Make group index page Boost-y --- group/classes/output/index_page.php | 111 +++++++++++++++++++++ group/classes/output/renderer.php | 50 ++++++++++ group/index.php | 124 +++++++----------------- group/templates/index.mustache | 145 ++++++++++++++++++++++++++++ 4 files changed, 342 insertions(+), 88 deletions(-) create mode 100644 group/classes/output/index_page.php create mode 100644 group/classes/output/renderer.php create mode 100644 group/templates/index.mustache diff --git a/group/classes/output/index_page.php b/group/classes/output/index_page.php new file mode 100644 index 00000000000..8430aae74b9 --- /dev/null +++ b/group/classes/output/index_page.php @@ -0,0 +1,111 @@ +. + +/** + * Group index page. + * + * @package core_group + * @copyright 2017 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_group\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use renderer_base; +use stdClass; +use templatable; + +/** + * Group index page class. + * + * @package core_group + * @copyright 2017 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class index_page implements renderable, templatable { + + /** @var int $courseid The course ID. */ + public $courseid; + + /** @var array The array of groups to be rendered. */ + public $groups; + + /** @var string The name of the currently selected group. */ + public $selectedgroupname; + + /** @var array The array of group members to be rendered, if a group is selected. */ + public $selectedgroupmembers; + + /** @var bool Whether to disable the add members/edit group buttons. */ + public $disableaddedit; + + /** @var bool Whether to disable the delete group button. */ + public $disabledelete; + + /** @var array Groups that can't be deleted by the user. */ + public $undeletablegroups; + + /** + * index_page constructor. + * + * @param int $courseid The course ID. + * @param array $groups The array of groups to be rendered. + * @param string $selectedgroupname The name of the currently selected group. + * @param array $selectedgroupmembers The array of group members to be rendered, if a group is selected. + * @param bool $disableaddedit Whether to disable the add members/edit group buttons. + * @param bool $disabledelete Whether to disable the delete group button. + * @param array $undeletablegroups Groups that can't be deleted by the user. + */ + public function __construct($courseid, $groups, $selectedgroupname, $selectedgroupmembers, $disableaddedit, $disabledelete, + $undeletablegroups) { + $this->courseid = $courseid; + $this->groups = $groups; + $this->selectedgroupname = $selectedgroupname; + $this->selectedgroupmembers = $selectedgroupmembers; + $this->disableaddedit = $disableaddedit; + $this->disabledelete = $disabledelete; + $this->undeletablegroups = $undeletablegroups; + } + + /** + * Export the data. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + global $CFG; + + $data = new stdClass(); + + // Variables that will be passed to the JS helper. + $data->courseid = $this->courseid; + $data->wwwroot = $CFG->wwwroot; + // To be passed to the JS init script in the template. Encode as a JSON string. + $data->undeletablegroups = json_encode($this->undeletablegroups); + + // Some buttons are enabled if single group selected. + $data->addmembersdisabled = $this->disableaddedit; + $data->editgroupsettingsdisabled = $this->disableaddedit; + $data->deletegroupdisabled = $this->disabledelete; + $data->groups = $this->groups; + $data->members = $this->selectedgroupmembers; + $data->selectedgroup = $this->selectedgroupname; + + return $data; + } +} diff --git a/group/classes/output/renderer.php b/group/classes/output/renderer.php new file mode 100644 index 00000000000..14443cf7891 --- /dev/null +++ b/group/classes/output/renderer.php @@ -0,0 +1,50 @@ +. + +/** + * Renderers. + * + * @package core_group + * @copyright 2017 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_group\output; + +defined('MOODLE_INTERNAL') || die(); + +use plugin_renderer_base; + +/** + * Renderer class. + * + * @package core_group + * @copyright 2017 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + + /** + * Defer to template. + * + * @param index_page $page + * @return string + */ + public function render_index_page(index_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('core_group/index', $data); + } +} diff --git a/group/index.php b/group/index.php index 6571f545d91..8150e196f00 100644 --- a/group/index.php +++ b/group/index.php @@ -61,6 +61,7 @@ $context = context_course::instance($course->id); require_capability('moodle/course:managegroups', $context); $PAGE->requires->js('/group/clientlib.js'); +$PAGE->requires->js('/group/module.js'); // Check for multiple/no group errors if (!$singlegroup) { @@ -152,41 +153,23 @@ echo $OUTPUT->header(); $currenttab = 'groups'; require('tabs.php'); -$disabled = 'disabled="disabled"'; - -// Some buttons are enabled if single group selected. -$showaddmembersform_disabled = $singlegroup ? '' : $disabled; -$showeditgroupsettingsform_disabled = $singlegroup ? '' : $disabled; -$deletegroup_disabled = count($groupids) > 0 ? '' : $disabled; - echo $OUTPUT->heading(format_string($course->shortname, true, array('context' => $context)) .' '.$strgroups, 3); -echo '
'."\n"; -echo '
'."\n"; -echo ''."\n"; - -echo html_writer::start_tag('div', array('class' => 'groupmanagementtable boxaligncenter')); -echo html_writer::start_tag('div', array('class' => 'groups')); - -echo '

'."\n"; - -$onchange = 'M.core_group.membersCombo.refreshMembers();'; - -echo ''."\n"; -echo '

'."\n"; -echo '

'."\n"; -echo '

'."\n"; - -echo '

'."\n"; - -echo '

'."\n"; - -echo '

'."\n"; - -echo html_writer::end_tag('div'); -echo html_writer::start_tag('div', array('class' => 'members')); - -echo '

'."\n"; -//NOTE: the SELECT was, multiple="multiple" name="user[]" - not used and breaks onclick. -echo ''."\n"; - -echo '

'."\n"; -echo html_writer::end_tag('div'); -echo html_writer::end_tag('div'); - -// -echo '
'."\n"; -echo '
'."\n"; - -$PAGE->requires->js_init_call('M.core_group.init_index', array($CFG->wwwroot, $courseid)); -$PAGE->requires->js_init_call('M.core_group.groupslist', array($preventgroupremoval)); +$disableaddedit = !$singlegroup; +$disabledelete = !empty($groupids); +$renderable = new \core_group\output\index_page($courseid, $groupoptions, $selectedname, $members, $disableaddedit, $disabledelete, + $preventgroupremoval); +$output = $PAGE->get_renderer('core_group'); +echo $output->render($renderable); echo $OUTPUT->footer(); diff --git a/group/templates/index.mustache b/group/templates/index.mustache new file mode 100644 index 00000000000..fb8be7f75eb --- /dev/null +++ b/group/templates/index.mustache @@ -0,0 +1,145 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_group/index + + Template for the Groups page. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * courseid int The course ID. + * selectedgroup string The initially selected group. + * editgroupsettingsdisabled bool Whether to disable the "Edit group settings" button on load. + * deletegroupdisabled bool Whether to disable the "Delete selected group" button on load. + * addmembersdisabled bool Whether to disable the "Add/remove users" button on load. + * groups array The list of groups. + * members array The list of members, grouped based on roles. + * undeletablegroups string A JSON string containing an array of group IDs that a user cannot delete. + + Example context (json): + { + "courseid": "1", + "selectedgroup": "Group 1 (3)", + "editgroupsettingsdisabled": false, + "deletegroupdisabled": false, + "addmembersdisabled": false, + "groups": [ + { + "value": "1", + "text": "Group 1 (3)", + "selected": true + }, + { + "value": "2", + "text": "Group 2 (2)" + } + ], + "members": [ + { + "role": "Student", + "rolemembers": [ + { + "value": "1", + "text": "John Doe" + }, + { + "value": "2", + "text": "Jane Doe" + }, + { + "value": "3", + "text": "John Smith" + } + ] + } + ], + "undeletablegroups": "[1: true, 3: true]" + } +}} +
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ + +
+
+ +
+
+
+
+
+{{#js}} + require(['jquery', 'core/yui'], function($) { + $("#groups").change(function() { + M.core_group.membersCombo.refreshMembers(); + }); + M.core_group.init_index(Y, "{{wwwroot}}", {{courseid}}); + var undeletableGroups = JSON.parse('{{{undeletablegroups}}}'); + M.core_group.groupslist(Y, undeletableGroups); + }); +{{/js}} From 2a477bf08e5eab1615775cc9b76846b6cece86ae Mon Sep 17 00:00:00 2001 From: Mark Sharp Date: Mon, 5 Jun 2017 09:41:42 +0100 Subject: [PATCH 16/38] MDL-59125 filter_urltolink: ignoretags on nolink span --- filter/urltolink/filter.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/filter/urltolink/filter.php b/filter/urltolink/filter.php index b232edae8b8..211e149e837 100644 --- a/filter/urltolink/filter.php +++ b/filter/urltolink/filter.php @@ -73,8 +73,8 @@ class filter_urltolink extends moodle_text_filter { // //<a href="blah"> //<a href="blah"> - $filterignoretagsopen = array(']+?>'); - $filterignoretagsclose = array(''); + $filterignoretagsopen = array(']+?>', ']+?class="nolink"[^>]*?>'); + $filterignoretagsclose = array('', ''); filter_save_ignore_tags($text,$filterignoretagsopen,$filterignoretagsclose,$ignoretags); // Check if we support unicode modifiers in regular expressions. Cache it. @@ -174,4 +174,3 @@ function filter_urltolink_img_callback($link) { } return ''; } - From 450d15d159a2c13d860ca9ffcd9c0df6f74e7119 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Fri, 16 Jun 2017 15:52:42 +0800 Subject: [PATCH 17/38] MDL-59125 filter_urltolink: respect nolink span, unittest --- filter/urltolink/tests/filter_test.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/filter/urltolink/tests/filter_test.php b/filter/urltolink/tests/filter_test.php index 0ae7f891cff..1dca5b7f74a 100644 --- a/filter/urltolink/tests/filter_test.php +++ b/filter/urltolink/tests/filter_test.php @@ -174,6 +174,9 @@ class filter_urltolink_filter_testcase extends basic_testcase { '' => '', 'www.google.com' => 'www.google.com', 'http://nolandforzombies.com Zombies FTW http://aliens.org' => 'http://nolandforzombies.com Zombies FTW http://aliens.org', + // Test 'nolink' class. + 'URL: http://moodle.org' => 'URL: http://moodle.org', + 'URL: http://moodle.org' => 'URL: http://moodle.org', //URLs in Javascript. Commented out as part of MDL-21183 //'var url="http://moodle.org";'=>'var url="http://moodle.org";', //'var url = "http://moodle.org";'=>'var url = "http://moodle.org";', From 4acb33f67b64af0e875598eca60133d6801c5ba9 Mon Sep 17 00:00:00 2001 From: Tobias Reischmann Date: Wed, 7 Jun 2017 12:43:22 +0200 Subject: [PATCH 18/38] MDL-59173 tablelib.php: changed default of $params in set_sql to array() Default of $params = null leads to a problem in query_db(). There this value is used in array_merge, which can not handle null values. If null is passed for one of the params the outcome will be null independent of the value of the second param! --- lib/tablelib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tablelib.php b/lib/tablelib.php index 84d077b5011..4dd84f8de56 100644 --- a/lib/tablelib.php +++ b/lib/tablelib.php @@ -1545,7 +1545,7 @@ class table_sql extends flexible_table { * Of course you can use sub-queries, JOINS etc. by putting them in the * appropriate clause of the query. */ - function set_sql($fields, $from, $where, array $params = NULL) { + function set_sql($fields, $from, $where, array $params = array()) { $this->sql = new stdClass(); $this->sql->fields = $fields; $this->sql->from = $from; From e00e67319598bb955e48cf55fecbcfc0bce87f80 Mon Sep 17 00:00:00 2001 From: Simey Lameze Date: Wed, 14 Jun 2017 12:26:35 +0800 Subject: [PATCH 19/38] MDL-59230 block_myoverview: add missing core notification module --- blocks/myoverview/amd/build/tab_preferences.min.js | 2 +- blocks/myoverview/amd/src/tab_preferences.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/blocks/myoverview/amd/build/tab_preferences.min.js b/blocks/myoverview/amd/build/tab_preferences.min.js index 2bfb8fe06b0..da5bd970f13 100644 --- a/blocks/myoverview/amd/build/tab_preferences.min.js +++ b/blocks/myoverview/amd/build/tab_preferences.min.js @@ -1 +1 @@ -define(["jquery","core/ajax","core/custom_interaction_events"],function(a,b,c){var d=function(d){c.define(d,[c.events.activate]),d.on(c.events.activate,"[data-toggle='tab']",function(c){var d=a(c.currentTarget).data("tabname");"function"==typeof window.history.pushState&&window.history.pushState(null,null,"?myoverviewtab="+d);var e={methodname:"core_user_update_user_preferences",args:{preferences:[{type:"block_myoverview_last_tab",value:d}]}};b.call([e])[0].fail(Notification.exception)})};return{registerEventListeners:d}}); \ No newline at end of file +define(["jquery","core/ajax","core/custom_interaction_events","core/notification"],function(a,b,c,d){var e=function(e){c.define(e,[c.events.activate]),e.on(c.events.activate,"[data-toggle='tab']",function(c){var e=a(c.currentTarget).data("tabname");"function"==typeof window.history.pushState&&window.history.pushState(null,null,"?myoverviewtab="+e);var f={methodname:"core_user_update_user_preferences",args:{preferences:[{type:"block_myoverview_last_tab",value:e}]}};b.call([f])[0].fail(d.exception)})};return{registerEventListeners:e}}); \ No newline at end of file diff --git a/blocks/myoverview/amd/src/tab_preferences.js b/blocks/myoverview/amd/src/tab_preferences.js index f6afc1b6193..25ac2eefa3d 100644 --- a/blocks/myoverview/amd/src/tab_preferences.js +++ b/blocks/myoverview/amd/src/tab_preferences.js @@ -21,7 +21,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define(['jquery', 'core/ajax', 'core/custom_interaction_events'], function($, Ajax, CustomEvents) { +define(['jquery', 'core/ajax', 'core/custom_interaction_events', + 'core/notification'], function($, Ajax, CustomEvents, Notification) { /** * Registers an event that saves the user's tab preference when switching between them. From 64b6651cfa2a8d6086f7edb76d7749c2bf9b8e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 16 Jun 2017 13:36:32 +0200 Subject: [PATCH 20/38] MDL-59269 lang: Fix ability to uninstall certain language packs In the issue MDL-39319 (6ddf92c77), the ability to uninstall multiple language packs at once was added. By a mistake, the PARAM_ALPHAEXT was used as paramater type for the dash-separated list of language packs to be uninstalled. Language packs with a number in the name (such as en_us_k12) do not pass the ALPHAEXT cleaning. This patch changes the parameter cleaning to PARAM_SAFEPATH which is more appropriate for the given scenario as language code themselves must be SAFEDIRs. --- admin/tool/langimport/index.php | 10 ++++++---- .../moodle-core-languninstallconfirm-debug.js | 2 +- .../moodle-core-languninstallconfirm-min.js | 2 +- .../moodle-core-languninstallconfirm.js | 2 +- .../languninstallconfirm/js/languninstallconfirm.js | 2 +- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/admin/tool/langimport/index.php b/admin/tool/langimport/index.php index 0e98a5d2de4..92a09dad241 100644 --- a/admin/tool/langimport/index.php +++ b/admin/tool/langimport/index.php @@ -40,7 +40,7 @@ if (empty($CFG->langotherroot)) { $mode = optional_param('mode', 0, PARAM_INT); // action $pack = optional_param_array('pack', array(), PARAM_SAFEDIR); // pack to install $uninstalllang = optional_param_array('uninstalllang', array(), PARAM_LANG);// installed pack to uninstall -$confirmtounistall = optional_param('confirmtouninstall', '', PARAM_ALPHAEXT); // uninstallation confirmation +$confirmtounistall = optional_param('confirmtouninstall', '', PARAM_SAFEPATH); // uninstallation confirmation $purgecaches = optional_param('purgecaches', false, PARAM_BOOL); // explicit caches reset if ($purgecaches) { @@ -74,7 +74,7 @@ if ($mode == DELETION_OF_SELECTED_LANG and (!empty($uninstalllang) or !empty($co // Actually deleting languages, languages to delete are passed as GET parameter as string // ...need to populate them to array. if (empty($uninstalllang)) { - $uninstalllang = explode('-', $confirmtounistall); + $uninstalllang = explode('/', $confirmtounistall); } if (in_array('en', $uninstalllang)) { @@ -84,8 +84,10 @@ if ($mode == DELETION_OF_SELECTED_LANG and (!empty($uninstalllang) or !empty($co } else if (empty($confirmtounistall) and confirm_sesskey()) { // User chose langs to be deleted, show confirmation. echo $OUTPUT->header(); echo $OUTPUT->confirm(get_string('uninstallconfirm', 'tool_langimport', implode(', ', $uninstalllang)), - 'index.php?mode='.DELETION_OF_SELECTED_LANG.'&confirmtouninstall='.implode('-', $uninstalllang), - 'index.php'); + new moodle_url($PAGE->url, array( + 'mode' => DELETION_OF_SELECTED_LANG, + 'confirmtouninstall' => implode('/', $uninstalllang), + )), $PAGE->url); echo $OUTPUT->footer(); die; diff --git a/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-debug.js b/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-debug.js index d12bfd2320e..6f50f4c2c34 100644 --- a/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-debug.js +++ b/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-debug.js @@ -98,7 +98,7 @@ Y.extend(Confirmation, Y.Base, { _uninstall: function(e, langCodes) { Y.config.win.location.href = this.get('uninstallUrl') + '?mode=4' + '&sesskey=' + M.cfg.sesskey + - '&confirmtouninstall=' + langCodes.join('-'); + '&confirmtouninstall=' + langCodes.join('/'); } }); diff --git a/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-min.js b/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-min.js index fd52350bcd1..94a7844dd2a 100644 --- a/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-min.js +++ b/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-min.js @@ -1 +1 @@ -YUI.add("moodle-core-languninstallconfirm",function(e,t){function n(){n.superclass.constructor.apply(this,arguments)}var r={UNINSTALLBUTTON:"#languninstallbutton",UNINSTALLSELECT:"#menuuninstalllang option",ENGLISHOPTION:"#menuuninstalllang option[value='en']"};n.NAME=t,n.ATTRS={uninstallUrl:{validator:e.Lang.isString}},e.extend(n,e.Base,{initializer:function(){e.one(r.UNINSTALLBUTTON).on("click",this._confirm,this)},_confirm:function(t){t.preventDefault();var n=[],i=[];e.all(r.UNINSTALLSELECT).each(function(e){e.get("selected")&&(n.push(e.getAttribute("value")),i.push(e.get("text")))});if(n.length===0){(new M.core.alert({message:M.util.get_string("selectlangs","tool_langimport")})).show();return}if(n.indexOf("en")>-1){e.one(r.ENGLISHOPTION).set("selected",!1),(new M.core.alert({message:M.util.get_string("noenglishuninstall","tool_langimport")})).show();return}var s={modal:!0,visible:!1,centered:!0,title:M.util.get_string("uninstall","tool_langimport"),question:M.util.get_string("uninstallconfirm","tool_langimport",i.join(", "))};(new M.core.confirm(s)).show().on("complete-yes",this._uninstall,this,n)},_uninstall:function(t,n){e.config.win.location.href=this.get("uninstallUrl")+"?mode=4"+"&sesskey="+M.cfg.sesskey+"&confirmtouninstall="+n.join("-")}}),e.namespace("M.core.languninstallconfirm").Confirmation=n,e.namespace("M.core.languninstallconfirm").init=function(e){return new n(e)}},"@VERSION@",{requires:["base","node","moodle-core-notification-confirm","moodle-core-notification-alert"]}); +YUI.add("moodle-core-languninstallconfirm",function(e,t){function n(){n.superclass.constructor.apply(this,arguments)}var r={UNINSTALLBUTTON:"#languninstallbutton",UNINSTALLSELECT:"#menuuninstalllang option",ENGLISHOPTION:"#menuuninstalllang option[value='en']"};n.NAME=t,n.ATTRS={uninstallUrl:{validator:e.Lang.isString}},e.extend(n,e.Base,{initializer:function(){e.one(r.UNINSTALLBUTTON).on("click",this._confirm,this)},_confirm:function(t){t.preventDefault();var n=[],i=[];e.all(r.UNINSTALLSELECT).each(function(e){e.get("selected")&&(n.push(e.getAttribute("value")),i.push(e.get("text")))});if(n.length===0){(new M.core.alert({message:M.util.get_string("selectlangs","tool_langimport")})).show();return}if(n.indexOf("en")>-1){e.one(r.ENGLISHOPTION).set("selected",!1),(new M.core.alert({message:M.util.get_string("noenglishuninstall","tool_langimport")})).show();return}var s={modal:!0,visible:!1,centered:!0,title:M.util.get_string("uninstall","tool_langimport"),question:M.util.get_string("uninstallconfirm","tool_langimport",i.join(", "))};(new M.core.confirm(s)).show().on("complete-yes",this._uninstall,this,n)},_uninstall:function(t,n){e.config.win.location.href=this.get("uninstallUrl")+"?mode=4"+"&sesskey="+M.cfg.sesskey+"&confirmtouninstall="+n.join("/")}}),e.namespace("M.core.languninstallconfirm").Confirmation=n,e.namespace("M.core.languninstallconfirm").init=function(e){return new n(e)}},"@VERSION@",{requires:["base","node","moodle-core-notification-confirm","moodle-core-notification-alert"]}); diff --git a/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm.js b/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm.js index d12bfd2320e..6f50f4c2c34 100644 --- a/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm.js +++ b/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm.js @@ -98,7 +98,7 @@ Y.extend(Confirmation, Y.Base, { _uninstall: function(e, langCodes) { Y.config.win.location.href = this.get('uninstallUrl') + '?mode=4' + '&sesskey=' + M.cfg.sesskey + - '&confirmtouninstall=' + langCodes.join('-'); + '&confirmtouninstall=' + langCodes.join('/'); } }); diff --git a/lib/yui/src/languninstallconfirm/js/languninstallconfirm.js b/lib/yui/src/languninstallconfirm/js/languninstallconfirm.js index 88eead86e08..98d87ea2019 100644 --- a/lib/yui/src/languninstallconfirm/js/languninstallconfirm.js +++ b/lib/yui/src/languninstallconfirm/js/languninstallconfirm.js @@ -96,7 +96,7 @@ Y.extend(Confirmation, Y.Base, { _uninstall: function(e, langCodes) { Y.config.win.location.href = this.get('uninstallUrl') + '?mode=4' + '&sesskey=' + M.cfg.sesskey + - '&confirmtouninstall=' + langCodes.join('-'); + '&confirmtouninstall=' + langCodes.join('/'); } }); From fc684bce4b6de3aed3fc435994095abbaf0a2f50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luca=20B=C3=B6sch?= Date: Fri, 19 May 2017 13:13:31 +0200 Subject: [PATCH 21/38] MDL-58766 css: clean theme block alignment --- theme/bootstrapbase/less/moodle/blocks.less | 9 +++++++++ theme/bootstrapbase/style/moodle.css | 3 +++ 2 files changed, 12 insertions(+) diff --git a/theme/bootstrapbase/less/moodle/blocks.less b/theme/bootstrapbase/less/moodle/blocks.less index c42131dba54..68c70daa83b 100644 --- a/theme/bootstrapbase/less/moodle/blocks.less +++ b/theme/bootstrapbase/less/moodle/blocks.less @@ -53,6 +53,15 @@ display: inline; } } + p { + &.hasicon { + img { + &.icon { + padding-right: 0; + } + } + } + } } .footer { margin-bottom: 4px; diff --git a/theme/bootstrapbase/style/moodle.css b/theme/bootstrapbase/style/moodle.css index fa74dbd2c8a..d205064187f 100644 --- a/theme/bootstrapbase/style/moodle.css +++ b/theme/bootstrapbase/style/moodle.css @@ -15873,6 +15873,9 @@ body { margin-left: 5px; display: inline; } +.block .content p.hasicon img.icon { + padding-right: 0; +} .block .footer { margin-bottom: 4px; display: block; From ff2dab456642284743c960778589a80f364819d3 Mon Sep 17 00:00:00 2001 From: Vladimir Zhirov Date: Sat, 20 May 2017 20:43:51 +0400 Subject: [PATCH 22/38] MDL-59005 resource: fixed zip extraction for filenames in CP-1251/CP-866 --- lib/filestorage/zip_archive.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/filestorage/zip_archive.php b/lib/filestorage/zip_archive.php index b7d0fd68dde..21313143642 100644 --- a/lib/filestorage/zip_archive.php +++ b/lib/filestorage/zip_archive.php @@ -662,6 +662,7 @@ class zip_archive extends file_archive { case 'ISO-8859-6': $encoding = 'CP720'; break; case 'ISO-8859-7': $encoding = 'CP737'; break; case 'ISO-8859-8': $encoding = 'CP862'; break; + case 'WINDOWS-1251': $encoding = 'CP866'; break; case 'EUC-JP': case 'UTF-8': if ($winchar = get_string('localewincharset', 'langconfig')) { From 46f68f026edb614ae4cf6adfe80cdd45e77cfa7e Mon Sep 17 00:00:00 2001 From: John Okely Date: Mon, 12 Jun 2017 11:21:38 +0800 Subject: [PATCH 23/38] MDL-58310 message: No longer need to supress errors from messaging --- message/amd/build/message_repository.min.js | 2 +- message/amd/src/message_repository.js | 6 ++---- .../output/popup/amd/build/notification_repository.min.js | 2 +- message/output/popup/amd/src/notification_repository.js | 6 ++---- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/message/amd/build/message_repository.min.js b/message/amd/build/message_repository.min.js index bded03758d5..5f20161d194 100644 --- a/message/amd/build/message_repository.min.js +++ b/message/amd/build/message_repository.min.js @@ -1 +1 @@ -define(["jquery","core/ajax","core/notification","core/log"],function(a,b,c,d){var e=function(a){"undefined"==typeof a.limit&&(a.limit=0),"undefined"==typeof a.offset&&(a.offset=0),a.limitfrom=a.offset,a.limitnum=a.limit,delete a.limit,delete a.offset;var d={methodname:"core_message_data_for_messagearea_conversations",args:a},e=b.call([d])[0];return e.fail(c.exception),e},f=function(a){var c={methodname:"core_message_get_unread_conversations_count",args:a},e=b.call([c])[0];return e.fail(function(a){d.error("Could not retrieve unread message count: "+a.message)}),e},g=function(a){var d={methodname:"core_message_mark_all_messages_as_read",args:a},e=b.call([d])[0];return e.fail(c.exception),e};return{query:e,countUnreadConversations:f,markAllAsRead:g}}); \ No newline at end of file +define(["jquery","core/ajax","core/notification"],function(a,b,c){var d=function(a){"undefined"==typeof a.limit&&(a.limit=0),"undefined"==typeof a.offset&&(a.offset=0),a.limitfrom=a.offset,a.limitnum=a.limit,delete a.limit,delete a.offset;var d={methodname:"core_message_data_for_messagearea_conversations",args:a},e=b.call([d])[0];return e.fail(c.exception),e},e=function(a){var d={methodname:"core_message_get_unread_conversations_count",args:a},e=b.call([d])[0];return e.fail(c.exception),e},f=function(a){var d={methodname:"core_message_mark_all_messages_as_read",args:a},e=b.call([d])[0];return e.fail(c.exception),e};return{query:d,countUnreadConversations:e,markAllAsRead:f}}); \ No newline at end of file diff --git a/message/amd/src/message_repository.js b/message/amd/src/message_repository.js index b81eae6043e..6c0ee94f039 100644 --- a/message/amd/src/message_repository.js +++ b/message/amd/src/message_repository.js @@ -22,7 +22,7 @@ * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define(['jquery', 'core/ajax', 'core/notification', 'core/log'], function($, Ajax, Notification, Log) { +define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) { /** * Retrieve a list of messages from the server. * @@ -72,9 +72,7 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/log'], function($, Aja var promise = Ajax.call([request])[0]; - promise.fail(function(e) { - Log.error('Could not retrieve unread message count: ' + e.message); - }); + promise.fail(Notification.exception); return promise; }; diff --git a/message/output/popup/amd/build/notification_repository.min.js b/message/output/popup/amd/build/notification_repository.min.js index beb592ec1cc..6d076df7bf4 100644 --- a/message/output/popup/amd/build/notification_repository.min.js +++ b/message/output/popup/amd/build/notification_repository.min.js @@ -1 +1 @@ -define(["core/ajax","core/notification","core/log"],function(a,b,c){var d=function(c){"undefined"==typeof c.limit&&(c.limit=20),"undefined"==typeof c.offset&&(c.offset=0);var d={methodname:"message_popup_get_popup_notifications",args:c},e=a.call([d])[0];return e.fail(b.exception),e},e=function(b){var d={methodname:"message_popup_get_unread_popup_notification_count",args:b},e=a.call([d])[0];return e.fail(function(a){c.error("Could not retrieve notifications count: "+a.message)}),e},f=function(c){var d={methodname:"core_message_mark_all_notifications_as_read",args:c},e=a.call([d])[0];return e.fail(b.exception),e},g=function(c,d){var e={messageid:c};d&&(e.timeread=d);var f={methodname:"core_message_mark_message_read",args:e},g=a.call([f])[0];return g.fail(b.exception),g};return{query:d,countUnread:e,markAllAsRead:f,markAsRead:g}}); \ No newline at end of file +define(["core/ajax","core/notification"],function(a,b){var c=function(c){"undefined"==typeof c.limit&&(c.limit=20),"undefined"==typeof c.offset&&(c.offset=0);var d={methodname:"message_popup_get_popup_notifications",args:c},e=a.call([d])[0];return e.fail(b.exception),e},d=function(c){var d={methodname:"message_popup_get_unread_popup_notification_count",args:c},e=a.call([d])[0];return e.fail(b.exception),e},e=function(c){var d={methodname:"core_message_mark_all_notifications_as_read",args:c},e=a.call([d])[0];return e.fail(b.exception),e},f=function(c,d){var e={messageid:c};d&&(e.timeread=d);var f={methodname:"core_message_mark_message_read",args:e},g=a.call([f])[0];return g.fail(b.exception),g};return{query:c,countUnread:d,markAllAsRead:e,markAsRead:f}}); \ No newline at end of file diff --git a/message/output/popup/amd/src/notification_repository.js b/message/output/popup/amd/src/notification_repository.js index 093238a9249..2fdb83ea6d0 100644 --- a/message/output/popup/amd/src/notification_repository.js +++ b/message/output/popup/amd/src/notification_repository.js @@ -22,7 +22,7 @@ * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define(['core/ajax', 'core/notification', 'core/log'], function(Ajax, Notification, Log) { +define(['core/ajax', 'core/notification'], function(Ajax, Notification) { /** * Retrieve a list of notifications from the server. * @@ -64,9 +64,7 @@ define(['core/ajax', 'core/notification', 'core/log'], function(Ajax, Notificati var promise = Ajax.call([request])[0]; - promise.fail(function(e) { - Log.error('Could not retrieve notifications count: ' + e.message); - }); + promise.fail(Notification.exception); return promise; }; From 671be2bedba7393123a8a3422865612e40b8c6f2 Mon Sep 17 00:00:00 2001 From: John Okely Date: Mon, 29 May 2017 08:30:24 +0800 Subject: [PATCH 24/38] MDL-58310 ajax: suppress errors upon page unload --- lib/amd/build/ajax.min.js | 2 +- lib/amd/src/ajax.js | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/amd/build/ajax.min.js b/lib/amd/build/ajax.min.js index 936d338c377..96fcee8fbe6 100644 --- a/lib/amd/build/ajax.min.js +++ b/lib/amd/build/ajax.min.js @@ -1 +1 @@ -define(["jquery","core/config"],function(a,b){var c=function(a){var b,c,d=this,e=null,f=0;for(f=0;f Date: Mon, 8 May 2017 09:49:34 +0800 Subject: [PATCH 25/38] MDL-58808 fontawesome: nav and settings blocks --- .../amd/build/ajax_response_renderer.min.js | 2 +- .../amd/src/ajax_response_renderer.js | 57 ++++++++++--------- blocks/navigation/styles.css | 2 +- blocks/settings/styles.css | 2 +- .../output/icon_system_fontawesome.php | 4 +- theme/boost/scss/moodle/blocks.scss | 42 ++++++++++++++ 6 files changed, 78 insertions(+), 31 deletions(-) diff --git a/blocks/navigation/amd/build/ajax_response_renderer.min.js b/blocks/navigation/amd/build/ajax_response_renderer.min.js index 4c6949207c8..02578ec2d3c 100644 --- a/blocks/navigation/amd/build/ajax_response_renderer.min.js +++ b/blocks/navigation/amd/build/ajax_response_renderer.min.js @@ -1 +1 @@ -define(["jquery"],function(a){function b(d,e){var f=a("
    ");f.attr("role","group"),f.attr("aria-hidden",!0),a.each(e,function(d,e){if("object"==typeof e){var g=a("
  • "),h=a("

    "),i=e.id||e.key+"_tree_item",j=null,k=!(!e.expandable&&!e.haschildren);if(h.addClass("tree_item"),h.attr("id",i),h.attr("role","treeitem"),h.attr("tabindex","-1"),e.requiresajaxloading&&(h.attr("data-requires-ajax",!0),h.attr("data-node-id",e.id),h.attr("data-node-key",e.key),h.attr("data-node-type",e.type)),k&&(g.addClass("collapsed contains_branch"),h.attr("aria-expanded",!1),h.addClass("branch")),!e.icon||k&&e.type!==c.ACTIVITY&&e.type!==c.RESOURCE||(g.addClass("item_with_icon"),h.addClass("hasicon"),j=a(""),j.attr("alt",e.icon.alt),j.attr("title",e.icon.title),j.attr("src",M.util.image_url(e.icon.pix,e.icon.component)),a.each(e.icon.classes,function(a,b){j.addClass(b)})),e.link){var l=a('');j?(l.append(j),l.append(''+e.name+"")):l.append(e.name),e.hidden&&l.addClass("dimmed"),h.append(l)}else{var m=a("");j?(m.append(j),m.append(''+e.name+"")):m.append(e.name),e.hidden&&m.addClass("dimmed"),h.append(m)}g.append(h),f.append(g),e.children&&e.children.length?b(h,e.children):k&&!e.requiresajaxloading&&(g.removeClass("contains_branch"),h.addClass("emptybranch"))}}),d.parent().append(f);var g=d.attr("id")+"_group";f.attr("id",g),d.attr("aria-owns",g),d.attr("role","treeitem")}var c={ACTIVITY:40,RESOURCE:50};return{render:function(a,c){if(c.children&&c.children.length){b(a,c.children);var d=a.children("[role='treeitem']").first(),e=a.find("#"+d.attr("aria-owns"));d.attr("aria-expanded",!0),e.attr("aria-hidden",!1)}else a.parent().hasClass("contains_branch")&&(a.parent().removeClass("contains_branch"),a.addClass("emptybranch"))}}}); \ No newline at end of file +define(["jquery","core/templates","core/notification","core/url"],function(a,b,c,d){function e(g,h){var i=a("
      ");i.attr("role","group"),i.attr("aria-hidden",!0),a.each(h,function(g,h){if("object"==typeof h){var j=a("
    • "),k=a("

      "),l=h.id||h.key+"_tree_item",m=null,n=!(!h.expandable&&!h.haschildren);k.addClass("tree_item"),k.attr("id",l),k.attr("role","treeitem"),k.attr("tabindex","-1"),h.requiresajaxloading&&(k.attr("data-requires-ajax",!0),k.attr("data-node-id",h.id),k.attr("data-node-key",h.key),k.attr("data-node-type",h.type)),n&&(j.addClass("collapsed contains_branch"),k.attr("aria-expanded",!1),k.addClass("branch"));var o=null;if(h.link){var p=a('');o=p,p.append(''+h.name+""),h.hidden&&p.addClass("dimmed"),k.append(p)}else{var q=a("");o=q,q.append(''+h.name+""),h.hidden&&q.addClass("dimmed"),k.append(q)}!h.icon||n&&h.type!==f.ACTIVITY&&h.type!==f.RESOURCE||(j.addClass("item_with_icon"),k.addClass("hasicon"),h.type===f.ACTIVITY||h.type===f.RESOURCE?(m=a(""),m.attr("alt",h.icon.alt),m.attr("title",h.icon.title),m.attr("src",d.imageUrl(h.icon.pix,h.icon.component)),a.each(h.icon.classes,function(a,b){m.addClass(b)}),o.prepend(m)):("moodle"==h.icon.component&&(h.icon.component="core"),b.renderPix(h.icon.pix,h.icon.component,h.icon.title).then(function(a){o.prepend(a)})["catch"](c.exception))),j.append(k),i.append(j),h.children&&h.children.length?e(k,h.children):n&&!h.requiresajaxloading&&(j.removeClass("contains_branch"),k.addClass("emptybranch"))}}),g.parent().append(i);var j=g.attr("id")+"_group";i.attr("id",j),g.attr("aria-owns",j),g.attr("role","treeitem")}var f={ACTIVITY:40,RESOURCE:50};return{render:function(a,b){if(b.children&&b.children.length){e(a,b.children);var c=a.children("[role='treeitem']").first(),d=a.find("#"+c.attr("aria-owns"));c.attr("aria-expanded",!0),d.attr("aria-hidden",!1)}else a.parent().hasClass("contains_branch")&&(a.parent().removeClass("contains_branch"),a.addClass("emptybranch"))}}}); \ No newline at end of file diff --git a/blocks/navigation/amd/src/ajax_response_renderer.js b/blocks/navigation/amd/src/ajax_response_renderer.js index d9357424d1f..99f2e28f1fe 100644 --- a/blocks/navigation/amd/src/ajax_response_renderer.js +++ b/blocks/navigation/amd/src/ajax_response_renderer.js @@ -22,7 +22,7 @@ * @copyright 2015 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define(['jquery'], function($) { +define(['jquery', 'core/templates', 'core/notification', 'core/url'], function($, Templates, Notification, Url) { // Mappings for the different types of nodes coming from the navigation. // Copied from lib/navigationlib.php navigation_node constants. @@ -75,28 +75,12 @@ define(['jquery'], function($) { p.addClass('branch'); } - if (node.icon && (!isBranch || node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE)) { - li.addClass('item_with_icon'); - p.addClass('hasicon'); - - icon = $(''); - icon.attr('alt', node.icon.alt); - icon.attr('title', node.icon.title); - icon.attr('src', M.util.image_url(node.icon.pix, node.icon.component)); - $.each(node.icon.classes, function(index, className) { - icon.addClass(className); - }); - } - + var eleToAddIcon = null; if (node.link) { var link = $(''); - if (icon) { - link.append(icon); - link.append('' + node.name + ''); - } else { - link.append(node.name); - } + eleToAddIcon = link; + link.append('' + node.name + ''); if (node.hidden) { link.addClass('dimmed'); @@ -106,12 +90,8 @@ define(['jquery'], function($) { } else { var span = $(''); - if (icon) { - span.append(icon); - span.append('' + node.name + ''); - } else { - span.append(node.name); - } + eleToAddIcon = span; + span.append('' + node.name + ''); if (node.hidden) { span.addClass('dimmed'); @@ -120,6 +100,31 @@ define(['jquery'], function($) { p.append(span); } + if (node.icon && (!isBranch || node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE)) { + li.addClass('item_with_icon'); + p.addClass('hasicon'); + + if (node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE) { + icon = $(''); + icon.attr('alt', node.icon.alt); + icon.attr('title', node.icon.title); + icon.attr('src', Url.imageUrl(node.icon.pix, node.icon.component)); + $.each(node.icon.classes, function(index, className) { + icon.addClass(className); + }); + eleToAddIcon.prepend(icon); + } else { + if (node.icon.component == 'moodle') { + node.icon.component = 'core'; + } + Templates.renderPix(node.icon.pix, node.icon.component, node.icon.title).then(function(html) { + // Prepend. + eleToAddIcon.prepend(html); + return; + }).catch(Notification.exception); + } + } + li.append(p); ul.append(li); diff --git a/blocks/navigation/styles.css b/blocks/navigation/styles.css index bb541f55926..2369c1af0d6 100644 --- a/blocks/navigation/styles.css +++ b/blocks/navigation/styles.css @@ -71,6 +71,6 @@ display: block; } -.block_navigation .block_tree [aria-hidden="true"] { +.block_navigation .block_tree [aria-hidden="true"]:not(.icon) { display: none; } diff --git a/blocks/settings/styles.css b/blocks/settings/styles.css index e0c94b2bad6..3d7e1d1a3e2 100644 --- a/blocks/settings/styles.css +++ b/blocks/settings/styles.css @@ -58,7 +58,7 @@ display: block; } -.block_settings .block_tree [aria-hidden="true"] { +.block_settings .block_tree [aria-hidden="true"]:not(.icon) { display: none; } diff --git a/lib/classes/output/icon_system_fontawesome.php b/lib/classes/output/icon_system_fontawesome.php index b047d63c47f..3d9593a1abb 100644 --- a/lib/classes/output/icon_system_fontawesome.php +++ b/lib/classes/output/icon_system_fontawesome.php @@ -247,7 +247,7 @@ class icon_system_fontawesome extends icon_system_font { 'core:i/mnethost' => 'fa-external-link', 'core:i/moodle_host' => 'fa-graduation-cap', 'core:i/move_2d' => 'fa-arrows', - 'core:i/navigationitem' => 'fa-angle-right', + 'core:i/navigationitem' => 'fa-fw', 'core:i/ne_red_mark' => 'fa-remove', 'core:i/new' => 'fa-plus', 'core:i/news' => 'fa-newspaper-o', @@ -280,7 +280,7 @@ class icon_system_fontawesome extends icon_system_font { 'core:i/scales' => 'fa-balance-scale', 'core:i/scheduled' => 'fa-calendar-check-o', 'core:i/search' => 'fa-search', - 'core:i/settings' => 'fa-cogs', + 'core:i/settings' => 'fa-cog', 'core:i/show' => 'fa-eye-slash', 'core:i/siteevent' => 'fa-share-alt', 'core:i/star-rating' => 'fa-star', diff --git a/theme/boost/scss/moodle/blocks.scss b/theme/boost/scss/moodle/blocks.scss index bfa78154978..c5df38a326e 100644 --- a/theme/boost/scss/moodle/blocks.scss +++ b/theme/boost/scss/moodle/blocks.scss @@ -206,3 +206,45 @@ $doughnut-fill-colour: $brand-warning; } } } + +// Show expand collapse with font-awesome. +.block_settings .block_tree [aria-expanded="true"], +.block_settings .block_tree [aria-expanded="true"].emptybranch, +.block_settings .block_tree [aria-expanded="false"], +.block_navigation .block_tree [aria-expanded="true"], +.block_navigation .block_tree [aria-expanded="true"].emptybranch, +.block_navigation .block_tree [aria-expanded="false"] { + background-image: none; +} +.block_settings .block_tree [aria-expanded="true"]:before, +.block_navigation .block_tree [aria-expanded="true"]:before { + content: $fa-var-angle-down; + margin-right: 0; + font-size: 16px; + @extend .fa; + width: 16px; +} + +.block_settings .block_tree [aria-expanded="false"]:before, +.block_navigation .block_tree [aria-expanded="false"]:before { + content: $fa-var-angle-right; + font-size: 16px; + margin-right: 0; + @extend .fa; + width: 16px; +} +.dir-rtl { + .block_settings .block_tree [aria-expanded="false"]:before, + .block_navigation .block_tree [aria-expanded="false"]:before { + content: $fa-var-angle-left; + } +} + +.block_navigation .block_tree p.hasicon, +.block_settings .block_tree p.hasicon { + text-indent: -3px; + + .icon { + margin-right: 2px; + } +} From b5ef326c03a3252f4b47184042e4acdf947ebd0f Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Tue, 13 Jun 2017 13:01:33 +0100 Subject: [PATCH 26/38] MDL-26976 files: Display space usage message in private files --- lang/en/moodle.php | 1 + user/files.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 07e154d24c6..cb129061a1a 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1505,6 +1505,7 @@ $string['publicsitefileswarning3'] = 'Note: Files placed here can be accessed by $string['publish'] = 'Publish'; $string['question'] = 'Question'; $string['questionsinthequestionbank'] = 'Questions in the question bank'; +$string['quotausage'] = 'You have currently used {$a->used} of your {$a->total} limit.'; $string['readinginfofrombackup'] = 'Reading info from backup'; $string['readme'] = 'README'; $string['recentactivity'] = 'Recent activity'; diff --git a/user/files.php b/user/files.php index 32c99f4f244..dadaf619d94 100644 --- a/user/files.php +++ b/user/files.php @@ -82,6 +82,20 @@ if ($mform->is_cancelled()) { echo $OUTPUT->header(); echo $OUTPUT->box_start('generalbox'); +// Show file area space usage. +if ($maxareabytes != FILE_AREA_MAX_BYTES_UNLIMITED) { + $fileareainfo = file_get_file_area_info($context->id, 'user', 'private'); + // Display message only if we have files. + if ($fileareainfo['filecount']) { + $a = (object) [ + 'used' => display_size($fileareainfo['filesize_without_references']), + 'total' => display_size($maxareabytes) + ]; + $quotamsg = get_string('quotausage', 'moodle', $a); + $notification = new \core\output\notification($quotamsg, \core\output\notification::NOTIFY_INFO); + echo $OUTPUT->render($notification); + } +} $mform->display(); echo $OUTPUT->box_end(); From 0f398b500d5ad4bd1eae20bc050b1fb8bcf9a13d Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Thu, 8 Jun 2017 13:04:10 +0100 Subject: [PATCH 27/38] MDL-59160 core_group: Return if the user can access all groups in WS The core_group_get_activity_allowed_groups WS should be returning not only the groups but also if the user is allowed to access all the activity groups. --- group/externallib.php | 3 +++ group/tests/externallib_test.php | 10 ++++++++++ lib/upgrade.txt | 2 ++ 3 files changed, 15 insertions(+) diff --git a/group/externallib.php b/group/externallib.php index 5359a373f17..c3d25304e1e 100644 --- a/group/externallib.php +++ b/group/externallib.php @@ -1386,6 +1386,7 @@ class core_group_external extends external_api { $results = array( 'groups' => $usergroups, + 'canaccessallgroups' => has_capability('moodle/site:accessallgroups', $context, $user), 'warnings' => $warnings ); return $results; @@ -1401,6 +1402,8 @@ class core_group_external extends external_api { return new external_single_structure( array( 'groups' => new external_multiple_structure(self::group_description()), + 'canaccessallgroups' => new external_value(PARAM_BOOL, + 'Whether the user will be able to access all the activity groups.', VALUE_OPTIONAL), 'warnings' => new external_warnings(), ) ); diff --git a/group/tests/externallib_test.php b/group/tests/externallib_test.php index 97a6cf93184..dcbb0ef2742 100644 --- a/group/tests/externallib_test.php +++ b/group/tests/externallib_test.php @@ -523,6 +523,7 @@ class core_group_externallib_testcase extends externallib_advanced_testcase { $groups = core_group_external::get_activity_allowed_groups($cm1->id); $groups = external_api::clean_returnvalue(core_group_external::get_activity_allowed_groups_returns(), $groups); $this->assertCount(2, $groups['groups']); + $this->assertFalse($groups['canaccessallgroups']); foreach ($groups['groups'] as $group) { if ($group['name'] == $group1data['name']) { @@ -539,12 +540,21 @@ class core_group_externallib_testcase extends externallib_advanced_testcase { $groups = core_group_external::get_activity_allowed_groups($cm1->id, $student->id); $groups = external_api::clean_returnvalue(core_group_external::get_activity_allowed_groups_returns(), $groups); $this->assertCount(2, $groups['groups']); + // We are checking the $student passed as parameter so this will return false. + $this->assertFalse($groups['canaccessallgroups']); // Check warnings. Trying to get groups for a user not enrolled in course. $groups = core_group_external::get_activity_allowed_groups($cm1->id, $otherstudent->id); $groups = external_api::clean_returnvalue(core_group_external::get_activity_allowed_groups_returns(), $groups); $this->assertCount(1, $groups['warnings']); + $this->assertFalse($groups['canaccessallgroups']); + // Checking teacher groups. + $groups = core_group_external::get_activity_allowed_groups($cm1->id); + $groups = external_api::clean_returnvalue(core_group_external::get_activity_allowed_groups_returns(), $groups); + $this->assertCount(2, $groups['groups']); + // Teachers by default can access all groups. + $this->assertTrue($groups['canaccessallgroups']); } /** diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 09c2839f65e..0044bd34e54 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -10,6 +10,8 @@ information provided here is intended especially for developers. * Removed accesslib private functions: load_course_context(), load_role_access_by_context(), dedupe_user_access() (MDL-49398). * Internal "accessdata" structure format has changed to improve ability to perform role definition caching (MDL-49398). * Role definitions are no longer cached in user session (MDL-49398). +* External function core_group_external::get_activity_allowed_groups now returns an additional field: canaccessallgroups. + It indicates whether the user will be able to access all the activity groups. === 3.3.1 === From 07782e10f12d5f6db4fa633fd697c9473e52bde7 Mon Sep 17 00:00:00 2001 From: Simey Lameze Date: Fri, 16 Jun 2017 15:01:30 +0800 Subject: [PATCH 28/38] MDL-59270 block_myoverview: tab setting logic to main template on clean --- .../templates/block_myoverview/main.mustache | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/theme/bootstrapbase/templates/block_myoverview/main.mustache b/theme/bootstrapbase/templates/block_myoverview/main.mustache index 650eb9bc060..42577fed2eb 100644 --- a/theme/bootstrapbase/templates/block_myoverview/main.mustache +++ b/theme/bootstrapbase/templates/block_myoverview/main.mustache @@ -24,27 +24,33 @@ }}
      -