Merge branch 'MDL-59295-master' of git://github.com/lameze/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 26 Jun 2017 22:53:28 +0000 (00:53 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 26 Jun 2017 22:53:28 +0000 (00:53 +0200)
83 files changed:
admin/cli/mysql_collation.php
admin/registration/forms.php
admin/roles/usersroles.php
admin/tool/langimport/index.php
admin/tool/log/backup/moodle2/restore_tool_log_logstore_subplugin.class.php
admin/tool/lp/templates/competency_rule_config.mustache
backup/moodle2/restore_stepslib.php
blocks/myoverview/amd/build/tab_preferences.min.js
blocks/myoverview/amd/src/tab_preferences.js
blocks/myoverview/classes/output/courses_view.php
blocks/navigation/amd/build/ajax_response_renderer.min.js
blocks/navigation/amd/src/ajax_response_renderer.js
blocks/navigation/styles.css
blocks/settings/styles.css
calendar/classes/local/event/container.php
calendar/lib.php
calendar/renderer.php
calendar/tests/externallib_test.php
course/format/renderer.php
course/renderer.php
course/tests/externallib_test.php
filter/urltolink/filter.php
filter/urltolink/tests/filter_test.php
group/classes/output/index_page.php [new file with mode: 0644]
group/classes/output/renderer.php [new file with mode: 0644]
group/externallib.php
group/index.php
group/templates/index.mustache [new file with mode: 0644]
group/tests/externallib_test.php
lang/en/calendar.php
lang/en/moodle.php
lib/amd/build/ajax.min.js
lib/amd/src/ajax.js
lib/classes/output/icon_system_fontawesome.php
lib/classes/plugininfo/repository.php
lib/classes/user.php
lib/completionlib.php
lib/db/services.php
lib/filelib.php
lib/filestorage/zip_archive.php
lib/phpunit/classes/util.php
lib/tablelib.php
lib/tests/filelib_test.php
lib/upgrade.txt
lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-debug.js
lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-min.js
lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm.js
lib/yui/src/languninstallconfirm/js/languninstallconfirm.js
media/player/videojs/amd/build/loader.min.js
media/player/videojs/amd/src/loader.js
media/player/videojs/classes/plugin.php
media/player/videojs/tests/player_test.php
message/amd/build/message_repository.min.js
message/amd/src/message_repository.js
message/output/popup/amd/build/notification_repository.min.js
message/output/popup/amd/src/notification_repository.js
mod/assign/submission/file/lang/en/assignsubmission_file.php
mod/assign/submission/file/locallib.php
mod/assign/submission/file/settings.php
mod/lesson/lang/en/lesson.php
mod/lesson/locallib.php
mod/lesson/pagetypes/branchtable.php
pix/i/mahara_host.gif [deleted file]
pix/i/mahara_host.png [new file with mode: 0644]
pix/i/mahara_host.svg [new file with mode: 0644]
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/user.scss
theme/boost/templates/core/availability_info.mustache
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/tool_usertours.less
theme/bootstrapbase/less/moodle/user.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_myoverview/main.mustache
theme/bootstrapbase/templates/core/availability_info.mustache
user/externallib.php
user/files.php
user/tests/externallib_test.php
version.php
webservice/externallib.php
webservice/tests/externallib_test.php
webservice/upgrade.txt

index adaadbb..ae3c709 100644 (file)
@@ -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;
+}
index 92b15e8..6e0b8a9 100644 (file)
@@ -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);
index 5e871e6..c455902 100644 (file)
@@ -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 ' <a title="' . $strgoto . '" href="' . $raurl . '">' . $OUTPUT->pix_icon('t/edit', 'core', $stredit) . '</a> ';
-            echo ' <a title="' . $strcheck . '" href="' . $churl . '">' . $OUTPUT->pix_icon('t/preview', 'core', $strcheckpermissions) . '</a> ';
+            echo ' <a title="' . $strgoto . '" href="' . $raurl . '">' . $OUTPUT->pix_icon('t/edit', $stredit) . '</a> ';
+            echo ' <a title="' . $strcheck . '" href="' . $churl . '">' . $OUTPUT->pix_icon('t/preview', $strcheckpermissions) . '</a> ';
             echo "</p>\n";
         }
     }
index 0e98a5d..92a09da 100644 (file)
@@ -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;
 
index 0c00f00..c48c2c8 100644 (file)
@@ -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));
index edb7bcc..59f4d57 100644 (file)
         {{/config}}
     </div>
 
-    <div data-region="footer" class="pull-xs-right">
+    <div data-region="footer" class="pull-xs-right m-t-1">
         {{#config}}
         <input type="button" class="btn btn-primary" data-action="save" value="{{#str}}savechanges{{/str}}"/>
         {{/config}}
         <input type="button" class="btn btn-secondary" data-action="cancel" value="{{#str}}cancel{{/str}}"/>
     </div>
+    <div class="clearfix"></div>
 </div>
index c788ac1..f886496 100644 (file)
@@ -3089,7 +3089,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;
@@ -3136,7 +3137,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();
index 2bfb8fe..da5bd97 100644 (file)
Binary files a/blocks/myoverview/amd/build/tab_preferences.min.js and b/blocks/myoverview/amd/build/tab_preferences.min.js differ
index f6afc1b..25ac2ee 100644 (file)
@@ -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.
index 798eb7b..2949c09 100644 (file)
@@ -63,6 +63,9 @@ class courses_view implements renderable, templatable {
      * @return array
      */
     public function export_for_template(renderer_base $output) {
+        global $CFG;
+        require_once($CFG->dirroot.'/course/lib.php');
+
         // Build courses view data structure.
         $coursesview = [
             'hascourses' => !empty($this->courses)
index 4c69492..02578ec 100644 (file)
Binary files a/blocks/navigation/amd/build/ajax_response_renderer.min.js and b/blocks/navigation/amd/build/ajax_response_renderer.min.js differ
index d935742..99f2e28 100644 (file)
@@ -22,7 +22,7 @@
  * @copyright  2015 John Okely <john@moodle.com>
  * @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 = $('<img/>');
-                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 = $('<a title="' + node.title + '" href="' + node.link + '"></a>');
 
-                if (icon) {
-                    link.append(icon);
-                    link.append('<span class="item-content-wrap">' + node.name + '</span>');
-                } else {
-                    link.append(node.name);
-                }
+                eleToAddIcon = link;
+                link.append('<span class="item-content-wrap">' + node.name + '</span>');
 
                 if (node.hidden) {
                     link.addClass('dimmed');
@@ -106,12 +90,8 @@ define(['jquery'], function($) {
             } else {
                 var span = $('<span></span>');
 
-                if (icon) {
-                    span.append(icon);
-                    span.append('<span class="item-content-wrap">' + node.name + '</span>');
-                } else {
-                    span.append(node.name);
-                }
+                eleToAddIcon = span;
+                span.append('<span class="item-content-wrap">' + node.name + '</span>');
 
                 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 = $('<img/>');
+                    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);
 
index bb541f5..2369c1a 100644 (file)
@@ -71,6 +71,6 @@
     display: block;
 }
 
-.block_navigation .block_tree [aria-hidden="true"] {
+.block_navigation .block_tree [aria-hidden="true"]:not(.icon) {
     display: none;
 }
index e0c94b2..3d7e1d1 100644 (file)
@@ -58,7 +58,7 @@
     display: block;
 }
 
-.block_settings .block_tree  [aria-hidden="true"] {
+.block_settings .block_tree  [aria-hidden="true"]:not(.icon) {
     display: none;
 }
 
index 20b39ec..9ac8cbf 100644 (file)
@@ -71,11 +71,6 @@ class container {
      */
     protected static $eventretrievalstrategy;
 
-    /**
-     * @var array A list of callbacks to use.
-     */
-    protected static $callbacks = array();
-
     /**
      * @var \stdClass[] An array of cached courses to use with the event factory.
      */
@@ -91,16 +86,6 @@ class container {
      */
     private static function init() {
         if (empty(self::$eventfactory)) {
-            // When testing the container's components, we need to make sure
-            // the callback implementations in modules are not executed, since
-            // we cannot control their output from PHPUnit. To do this we have
-            // a set of 'testing' callbacks that the factory can use. This way
-            // we know exactly how the factory behaves when being tested.
-            $getcallback = function($which) {
-                return self::$callbacks[PHPUNIT_TEST ? 'testing' : 'production'][$which];
-            };
-
-            self::initcallbacks();
             self::$actionfactory = new action_factory();
             self::$eventmapper = new event_mapper(
                 // The event mapper we return from here needs to know how to
@@ -129,8 +114,8 @@ class container {
             );
 
             self::$eventfactory = new event_factory(
-                $getcallback('action'),
-                $getcallback('visibility'),
+                [self::class, 'apply_component_provide_event_action'],
+                [self::class, 'apply_component_is_event_visible'],
                 function ($dbrow) {
                     // At present we only have a bail-out check for events in course modules.
                     if (empty($dbrow->modulename)) {
@@ -183,6 +168,19 @@ class container {
         }
     }
 
+    /**
+     * Reset all static caches, called between tests.
+     */
+    public static function reset_caches() {
+        self::$eventfactory = null;
+        self::$eventmapper = null;
+        self::$eventvault = null;
+        self::$actionfactory = null;
+        self::$eventretrievalstrategy = null;
+        self::$coursecache = [];
+        self::$modulecache = [];
+    }
+
     /**
      * Gets the event factory.
      *
@@ -214,88 +212,74 @@ class container {
     }
 
     /**
-     * Initialises the callbacks.
+     * Calls callback 'core_calendar_provide_event_action' from the component responsible for the event
      *
-     * There are two sets here, one is used during PHPUnit runs.
-     * See the comment at the start of the init method for more
-     * detail.
+     * If no callback is present or callback returns null, there is no action on the event
+     * and it will not be displayed on the dashboard.
+     *
+     * @param event_interface $event
+     * @return action_event|event_interface
      */
-    private static function initcallbacks() {
-        self::$callbacks = array(
-            'testing' => array(
-                // Always return an action event.
-                'action' => function (event_interface $event) {
-                    return new action_event(
-                        $event,
-                        new \core_calendar\local\event\value_objects\action(
-                            'test',
-                            new \moodle_url('http://example.com'),
-                            420,
-                            true
-                        ));
-                },
-                // Always be visible.
-                'visibility' => function (event_interface $event) {
-                    return true;
-                }
-            ),
-            'production' => array(
-                // This function has type event_interface -> event_interface.
-                // This is enforced by the event_factory.
-                'action' => function (event_interface $event) {
-                    // Callbacks will get supplied a "legacy" version
-                    // of the event class.
-                    $mapper = self::$eventmapper;
-                    $action = null;
-                    if ($event->get_course_module()) {
-                        // TODO MDL-58866 Only activity modules currently support this callback.
-                        // Any other event will not be displayed on the dashboard.
-                        $action = component_callback(
-                            'mod_' . $event->get_course_module()->get('modname'),
-                            'core_calendar_provide_event_action',
-                            [
-                                $mapper->from_event_to_legacy_event($event),
-                                self::$actionfactory
-                            ]
-                        );
-                    }
+    public static function apply_component_provide_event_action(event_interface $event) {
+        // Callbacks will get supplied a "legacy" version
+        // of the event class.
+        $mapper = self::$eventmapper;
+        $action = null;
+        if ($event->get_course_module()) {
+            // TODO MDL-58866 Only activity modules currently support this callback.
+            // Any other event will not be displayed on the dashboard.
+            $action = component_callback(
+                'mod_' . $event->get_course_module()->get('modname'),
+                'core_calendar_provide_event_action',
+                [
+                    $mapper->from_event_to_legacy_event($event),
+                    self::$actionfactory
+                ]
+            );
+        }
 
-                    // If we get an action back, return an action event, otherwise
-                    // continue piping through the original event.
-                    //
-                    // If a module does not implement the callback, component_callback
-                    // returns null.
-                    return $action ? new action_event($event, $action) : $event;
-                },
-                // This function has type event_interface -> bool.
-                // This is enforced by the event_factory.
-                'visibility' => function (event_interface $event) {
-                    $mapper = self::$eventmapper;
-                    $eventvisible = null;
-                    if ($event->get_course_module()) {
-                        // TODO MDL-58866 Only activity modules currently support this callback.
-                        $eventvisible = component_callback(
-                            'mod_' . $event->get_course_module()->get('modname'),
-                            'core_calendar_is_event_visible',
-                            [
-                                $mapper->from_event_to_legacy_event($event)
-                            ]
-                        );
-                    }
+        // If we get an action back, return an action event, otherwise
+        // continue piping through the original event.
+        //
+        // If a module does not implement the callback, component_callback
+        // returns null.
+        return $action ? new action_event($event, $action) : $event;
+    }
 
-                    // Do not display the event if there is nothing to action.
-                    if ($event instanceof action_event_interface && $event->get_action()->get_item_count() === 0) {
-                        return false;
-                    }
+    /**
+     * Calls callback 'core_calendar_is_event_visible' from the component responsible for the event
+     *
+     * The visibility callback is optional, if not present it is assumed as visible.
+     * If it is an actionable event but the get_item_count() returns 0 the visibility
+     * is set to false.
+     *
+     * @param event_interface $event
+     * @return bool
+     */
+    public static function apply_component_is_event_visible(event_interface $event) {
+        $mapper = self::$eventmapper;
+        $eventvisible = null;
+        if ($event->get_course_module()) {
+            // TODO MDL-58866 Only activity modules currently support this callback.
+            $eventvisible = component_callback(
+                'mod_' . $event->get_course_module()->get('modname'),
+                'core_calendar_is_event_visible',
+                [
+                    $mapper->from_event_to_legacy_event($event)
+                ]
+            );
+        }
 
-                    // Module does not implement the callback, event should be visible.
-                    if (is_null($eventvisible)) {
-                        return true;
-                    }
+        // Do not display the event if there is nothing to action.
+        if ($event instanceof action_event_interface && $event->get_action()->get_item_count() === 0) {
+            return false;
+        }
+
+        // Module does not implement the callback, event should be visible.
+        if (is_null($eventvisible)) {
+            return true;
+        }
 
-                    return $eventvisible ? true : false;
-                }
-            ),
-        );
+        return $eventvisible ? true : false;
     }
 }
index 55ad67f..f6f1e4a 100644 (file)
@@ -1457,6 +1457,15 @@ function calendar_get_mini($courses, $groups, $users, $calmonth = false, $calyea
                         $name = format_string($event->name, true);
                     }
                 }
+                // Include course's shortname into the event name, if applicable.
+                if (!empty($event->courseid) && $event->courseid !== SITEID) {
+                    $course = get_course($event->courseid);
+                    $eventnameparams = (object)[
+                        'name' => $name,
+                        'course' => format_string($course->shortname, true, array('context' => $event->context))
+                    ];
+                    $name = get_string('eventnameandcourse', 'calendar', $eventnameparams);
+                }
                 $popupcontent .= \html_writer::link($dayhref, $name);
                 $popupcontent .= \html_writer::end_tag('div');
             }
index 8abfb03..6711693 100644 (file)
@@ -502,7 +502,22 @@ class core_calendar_renderer extends plugin_renderer_base {
                         $attributes['class'] = $events[$eventindex]->class;
                     }
                     $dayhref->set_anchor('event_'.$events[$eventindex]->id);
-                    $link = html_writer::link($dayhref, format_string($events[$eventindex]->name, true));
+
+                    $eventcontext = $events[$eventindex]->context;
+                    $eventformatopts = array('context' => $eventcontext);
+                    // Get event name.
+                    $eventname = format_string($events[$eventindex]->name, true, $eventformatopts);
+                    // Include course's shortname into the event name, if applicable.
+                    $courseid = $events[$eventindex]->courseid;
+                    if (!empty($courseid) && $courseid !== SITEID) {
+                        $course = get_course($courseid);
+                        $eventnameparams = (object)[
+                            'name' => $eventname,
+                            'course' => format_string($course->shortname, true, $eventformatopts)
+                        ];
+                        $eventname = get_string('eventnameandcourse', 'calendar', $eventnameparams);
+                    }
+                    $link = html_writer::link($dayhref, $eventname);
                     $cell->text .= html_writer::tag('li', $link, $attributes);
                 }
                 $cell->text .= html_writer::end_tag('ul');
index 9912bcc..fc0ecc2 100644 (file)
@@ -674,13 +674,15 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
      */
     public function test_get_calendar_events_override() {
         $user = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
         $teacher = $this->getDataGenerator()->create_user();
         $anotheruser = $this->getDataGenerator()->create_user();
         $course = $this->getDataGenerator()->create_course();
         $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
         $moduleinstance = $generator->create_instance(['course' => $course->id]);
 
-        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student');
         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
         $this->resetAfterTest(true);
         $this->setAdminUser();
@@ -692,11 +694,12 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         ];
 
         $now = time();
+        // Create two events - one for everybody in the course and one only for the first student.
         $event1 = $this->create_calendar_event('Base event', 0, 'due', 0, $now + DAYSECS, $params + ['courseid' => $course->id]);
         $event2 = $this->create_calendar_event('User event', $user->id, 'due', 0, $now + 2*DAYSECS, $params + ['courseid' => 0]);
 
-        // Retrieve course events for teacher - only one "Base event" is returned.
-        $this->setUser($teacher);
+        // Retrieve course events for the second student - only one "Base event" is returned.
+        $this->setUser($user2);
         $paramevents = array('courseids' => array($course->id));
         $options = array ('siteevents' => true, 'userevents' => true);
         $events = core_calendar_external::get_calendar_events($paramevents, $options);
@@ -705,7 +708,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(0, count($events['warnings']));
         $this->assertEquals('Base event', $events['events'][0]['name']);
 
-        // Retrieve events for user - both events are returned.
+        // Retrieve events for the first student - both events are returned.
         $this->setUser($user);
         $events = core_calendar_external::get_calendar_events($paramevents, $options);
         $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
index f16bdd6..b630ad8 100644 (file)
@@ -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;
index bee3fe7..565f90c 100644 (file)
@@ -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();
index 8012a7f..d932dd8 100644 (file)
@@ -361,6 +361,40 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         core_course_external::update_categories($categories);
     }
 
+    /**
+     * Test create_courses numsections
+     */
+    public function test_create_course_numsections() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Set the required capabilities by the external function.
+        $contextid = context_system::instance()->id;
+        $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
+        $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
+
+        $numsections = 10;
+        $category  = self::getDataGenerator()->create_category();
+
+        // Create base categories.
+        $course1['fullname'] = 'Test course 1';
+        $course1['shortname'] = 'Testcourse1';
+        $course1['categoryid'] = $category->id;
+        $course1['courseformatoptions'][] = array('name' => 'numsections', 'value' => $numsections);
+
+        $courses = array($course1);
+
+        $createdcourses = core_course_external::create_courses($courses);
+        foreach ($createdcourses as $createdcourse) {
+            $existingsections = $DB->get_records('course_sections', array('course' => $createdcourse['id']));
+            $modinfo = get_fast_modinfo($createdcourse['id']);
+            $sections = $modinfo->get_section_info_all();
+            $this->assertEquals(count($sections), $numsections + 1); // Includes generic section.
+            $this->assertEquals(count($existingsections), $numsections + 1); // Includes generic section.
+        }
+    }
+
     /**
      * Test create_courses
      */
index b232eda..211e149 100644 (file)
@@ -73,8 +73,8 @@ class filter_urltolink extends moodle_text_filter {
         //<a href="blah">
         //&lt;a href="blah"&gt;
         //&lt;a href="blah">
-        $filterignoretagsopen  = array('<a\s[^>]+?>');
-        $filterignoretagsclose = array('</a>');
+        $filterignoretagsopen  = array('<a\s[^>]+?>', '<span[^>]+?class="nolink"[^>]*?>');
+        $filterignoretagsclose = array('</a>', '</span>');
         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 '<img class="filter_urltolink_image" alt="" src="'.$link[1].'" />';
 }
-
index 0ae7f89..1dca5b7 100644 (file)
@@ -174,6 +174,9 @@ class filter_urltolink_filter_testcase extends basic_testcase {
             '<link rel="search" type="application/opensearchdescription+xml" href="/osd.jsp" title="Peer review - Moodle Tracker"/>' => '<link rel="search" type="application/opensearchdescription+xml" href="/osd.jsp" title="Peer review - Moodle Tracker"/>',
             '<a href="https://docs.moodle.org/dev/Main_Page"></a><span>www.google.com</span><span class="placeholder"></span>' => '<a href="https://docs.moodle.org/dev/Main_Page"></a><span><a href="http://www.google.com" class="_blanktarget">www.google.com</a></span><span class="placeholder"></span>',
             'http://nolandforzombies.com <a href="zombiesFTW.com">Zombies FTW</a> http://aliens.org' => '<a href="http://nolandforzombies.com" class="_blanktarget">http://nolandforzombies.com</a> <a href="zombiesFTW.com">Zombies FTW</a> <a href="http://aliens.org" class="_blanktarget">http://aliens.org</a>',
+            // Test 'nolink' class.
+            'URL: <span class="nolink">http://moodle.org</span>' => 'URL: <span class="nolink">http://moodle.org</span>',
+            '<span class="nolink">URL: http://moodle.org</span>' => '<span class="nolink">URL: http://moodle.org</span>',
             //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";',
diff --git a/group/classes/output/index_page.php b/group/classes/output/index_page.php
new file mode 100644 (file)
index 0000000..8430aae
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Group 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 (file)
index 0000000..14443cf
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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);
+    }
+}
index 5359a37..c3d2530 100644 (file)
@@ -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(),
             )
         );
index 6571f54..8150e19 100644 (file)
@@ -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 '<form id="groupeditform" action="index.php" method="post">'."\n";
-echo '<div>'."\n";
-echo '<input type="hidden" name="id" value="' . $courseid . '" />'."\n";
-
-echo html_writer::start_tag('div', array('class' => 'groupmanagementtable boxaligncenter'));
-echo html_writer::start_tag('div', array('class' => 'groups'));
-
-echo '<p><label for="groups"><span id="groupslabel">'.get_string('groups').':</span><span id="thegrouping">&nbsp;</span></label></p>'."\n";
-
-$onchange = 'M.core_group.membersCombo.refreshMembers();';
-
-echo '<select name="groups[]" multiple="multiple" id="groups" size="15" class="select" onchange="'.$onchange.'">'."\n";
 
 $groups = groups_get_all_groups($courseid);
-$selectedname = '&nbsp;';
+$selectedname = null;
 $preventgroupremoval = array();
 
+// Get list of groups to render.
+$groupoptions = array();
 if ($groups) {
-    // Print out the HTML
     foreach ($groups as $group) {
-        $select = '';
-        $usercount = $DB->count_records('groups_members', array('groupid'=>$group->id));
-        $groupname = format_string($group->name).' ('.$usercount.')';
-        if (in_array($group->id,$groupids)) {
-            $select = ' selected="selected"';
+        $selected = false;
+        $usercount = $DB->count_records('groups_members', array('groupid' => $group->id));
+        $groupname = format_string($group->name) . ' (' . $usercount . ')';
+        if (in_array($group->id, $groupids)) {
+            $selected = true;
             if ($singlegroup) {
-                // Only keep selected name if there is one group selected
+                // Only keep selected name if there is one group selected.
                 $selectedname = $groupname;
             }
         }
@@ -194,76 +177,41 @@ if ($groups) {
             $preventgroupremoval[$group->id] = true;
         }
 
-        echo "<option value=\"{$group->id}\"$select title=\"$groupname\">$groupname</option>\n";
+        $groupoptions[] = (object) [
+            'value' => $group->id,
+            'selected' => $selected,
+            'text' => $groupname
+        ];
     }
-} else {
-    // Print an empty option to avoid the XHTML error of having an empty select element
-    echo '<option>&nbsp;</option>';
 }
 
-echo '</select>'."\n";
-echo '<p><input class="btn btn-secondary" type="submit" name="act_updatemembers" id="updatemembers" value="'
-        . get_string('showmembersforgroup', 'group') . '" /></p>'."\n";
-echo '<p><input class="btn btn-secondary" type="submit" '. $showeditgroupsettingsform_disabled .
-        ' name="act_showgroupsettingsform" id="showeditgroupsettingsform" value="'
-        . get_string('editgroupsettings', 'group') . '" /></p>'."\n";
-echo '<p><input class="btn btn-secondary" type="submit" '. $deletegroup_disabled .
-        ' name="act_deletegroup" id="deletegroup" value="'
-        . get_string('deleteselectedgroup', 'group') . '" /></p>'."\n";
-
-echo '<p><input class="btn btn-secondary" type="submit" name="act_showcreateorphangroupform" id="showcreateorphangroupform" value="'
-        . get_string('creategroup', 'group') . '" /></p>'."\n";
-
-echo '<p><input class="btn btn-secondary" type="submit" name="act_showautocreategroupsform" id="showautocreategroupsform" value="'
-        . get_string('autocreategroups', 'group') . '" /></p>'."\n";
-
-echo '<p><input class="btn btn-secondary" type="submit" name="act_showimportgroups" id="showimportgroups" value="'
-        . get_string('importgroups', 'core_group') . '" /></p>'."\n";
-
-echo html_writer::end_tag('div');
-echo html_writer::start_tag('div', array('class' => 'members'));
-
-echo '<p><label for="members"><span id="memberslabel">'.
-    get_string('membersofselectedgroup', 'group').
-    ' </span><span id="thegroup">'.$selectedname.'</span></label></p>'."\n";
-//NOTE: the SELECT was, multiple="multiple" name="user[]" - not used and breaks onclick.
-echo '<select name="user" id="members" size="15" class="select"'."\n";
-echo ' onclick="window.status=this.options[this.selectedIndex].title;" onmouseout="window.status=\'\';">'."\n";
-
-$member_names = array();
-
-$atleastonemember = false;
+// Get list of group members to render if there is a single selected group.
+$members = array();
 if ($singlegroup) {
-    if ($groupmemberroles = groups_get_members_by_role($groupids[0], $courseid, 'u.id, ' . get_all_user_name_fields(true, 'u'))) {
-        foreach($groupmemberroles as $roleid=>$roledata) {
-            echo '<optgroup label="'.s($roledata->name).'">';
-            foreach($roledata->users as $member) {
-                echo '<option value="'.$member->id.'">'.fullname($member, true).'</option>';
-                $atleastonemember = true;
+    $usernamefields = get_all_user_name_fields(true, 'u');
+    if ($groupmemberroles = groups_get_members_by_role(reset($groupids), $courseid, 'u.id, ' . $usernamefields)) {
+        foreach ($groupmemberroles as $roleid => $roledata) {
+            $users = array();
+            foreach ($roledata->users as $member) {
+                $users[] = (object)[
+                    'value' => $member->id,
+                    'text' => fullname($member, true)
+                ];
             }
-            echo '</optgroup>';
+            $members[] = (object)[
+                'role' => s($roledata->name),
+                'rolemembers' => $users
+            ];
         }
     }
 }
 
-if (!$atleastonemember) {
-    // Print an empty option to avoid the XHTML error of having an empty select element
-    echo '<option>&nbsp;</option>';
-}
-
-echo '</select>'."\n";
-
-echo '<p><input class="btn btn-secondary" type="submit" ' . $showaddmembersform_disabled . ' name="act_showaddmembersform" '
-        . 'id="showaddmembersform" value="' . get_string('adduserstogroup', 'group'). '" /></p>'."\n";
-echo html_writer::end_tag('div');
-echo html_writer::end_tag('div');
-
-//<input type="hidden" name="rand" value="om" />
-echo '</div>'."\n";
-echo '</form>'."\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 (file)
index 0000000..e68bdaf
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @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]"
+    }
+}}
+<form id="groupeditform" action="index.php" method="post">
+    <div class="container-fluid groupmanagementtable">
+        <div class="row row-fluid rtl-compatible">
+            <div class="col-md-6 span6 m-b-1">
+                <input type="hidden" name="id" value="{{courseid}}">
+                <div class="form-group">
+                    <label for="groups">
+                        <span id="groupslabel">{{#str}}groups{{/str}}</span>
+                        <span id="thegrouping">&nbsp;</span>
+                    </label>
+                    <select name="groups[]" multiple="multiple" id="groups" size="15" class="form-control input-block-level">
+                        {{#groups}}
+                            <option value="{{value}}" {{#selected}}selected="selected"{{/selected}} title="{{{text}}}">{{{text}}}</option>
+                        {{/groups}}
+                    </select>
+                </div>
+                <div class="form-group">
+                    <input type="submit" name="act_updatemembers" id="updatemembers" value="{{#str}}showmembersforgroup, group{{/str}}" class="btn btn-default" />
+                </div>
+                <div class="form-group">
+                    <input type="submit" name="act_showgroupsettingsform" id="showeditgroupsettingsform" value="{{#str}}editgroupsettings, group{{/str}}" {{#editgroupsettingsdisabled}}disabled="disabled"{{/editgroupsettingsdisabled}} class="btn btn-default" />
+                </div>
+                <div class="form-group">
+                    <input type="submit" name="act_deletegroup" id="deletegroup" value="{{#str}}deleteselectedgroup, group{{/str}}" {{#deletegroupdisabled}}disabled="disabled"{{/deletegroupdisabled}} class="btn btn-default" />
+                </div>
+                <div class="form-group">
+                    <input type="submit" name="act_showcreateorphangroupform" id="showcreateorphangroupform" value="{{#str}}creategroup, group{{/str}}" class="btn btn-default" />
+                </div>
+                <div class="form-group">
+                    <input type="submit" name="act_showautocreategroupsform" id="showautocreategroupsform" value="{{#str}}autocreategroups, group{{/str}}" class="btn btn-default" />
+                </div>
+                <div class="form-group">
+                    <input type="submit" name="act_showimportgroups" id="showimportgroups" value="{{#str}}importgroups, group{{/str}}" class="btn btn-default" />
+                </div>
+            </div>
+            <div class="col-md-6 span6 m-b-1">
+                <div class="form-group">
+                    <label for="members">
+                        <span id="memberslabel">{{#str}}membersofselectedgroup, group{{/str}}</span>
+                        <span id="thegroup">{{{selectedgroup}}}</span>
+                    </label>
+                    <select size="15" multiple="multiple" class="form-control input-block-level" id="members" name="user">
+                        {{#members}}
+                            <optgroup label="{{role}}">
+                                {{#rolemembers}}
+                                    <option value="{{value}}">{{{text}}}‎</option>
+                                {{/rolemembers}}
+                            </optgroup>
+                        {{/members}}
+                    </select>
+                </div>
+                <div class="form-group">
+                    <input type="submit" value="{{#str}}adduserstogroup, group{{/str}}" class="btn btn-default" {{#addmembersdisabled}}disabled="disabled"{{/addmembersdisabled}} name="act_showaddmembersform" id="showaddmembersform"/>
+                </div>
+            </div>
+        </div>
+    </div>
+</form>
+{{#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}}
index 97a6cf9..dcbb0ef 100644 (file)
@@ -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']);
     }
 
     /**
index 73d8b4a..364368c 100644 (file)
@@ -93,6 +93,7 @@ $string['eventendtime'] = 'End time';
 $string['eventinstanttime'] = 'Time';
 $string['eventkind'] = 'Type of event';
 $string['eventname'] = 'Event title';
+$string['eventnameandcourse'] = '{$a->course}: {$a->name}';
 $string['eventnone'] = 'No events';
 $string['eventrepeat'] = 'Repeats';
 $string['eventsall'] = 'All events';
index 3387da5..845118e 100644 (file)
@@ -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';
index 936d338..96fcee8 100644 (file)
Binary files a/lib/amd/build/ajax.min.js and b/lib/amd/build/ajax.min.js differ
index 663ff25..b0267a8 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @since      2.9
  */
-define(['jquery', 'core/config'], function($, config) {
+define(['jquery', 'core/config', 'core/log'], function($, config, Log) {
+
+    // Keeps track of when the user leaves the page so we know not to show an error.
+    var unloading = false;
 
     /**
      * Success handler. Called when the ajax call succeeds. Checks each response and
@@ -87,7 +90,12 @@ define(['jquery', 'core/config'], function($, config) {
         for (i = 0; i < requests.length; i++) {
             var request = requests[i];
 
-            request.deferred.reject(textStatus);
+            if (unloading) {
+                // No need to trigger an error because we are already navigating.
+                Log.error("Page unload: " + textStatus);
+            } else {
+                request.deferred.reject(textStatus);
+            }
         }
     };
 
@@ -109,6 +117,9 @@ define(['jquery', 'core/config'], function($, config) {
          * @return {Promise[]} Array of promises that will be resolved when the ajax call returns.
          */
         call: function(requests, async, loginrequired) {
+            $(window).bind('beforeunload', function() {
+                unloading = true;
+            });
             var ajaxRequestData = [],
                 i,
                 promises = [],
index b047d63..89edd59 100644 (file)
@@ -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',
@@ -309,7 +309,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:t/block' => 'fa-ban',
             'core:t/block_to_dock_rtl' => 'fa-chevron-right',
             'core:t/block_to_dock' => 'fa-chevron-left',
-            'core:t/calc_off' => 'fa-times fa-cross',
+            'core:t/calc_off' => 'fa-calculator', // TODO: Change to better icon once we have stacked icon support or more icons.
             'core:t/calc' => 'fa-calculator',
             'core:t/check' => 'fa-check',
             'core:t/cohort' => 'fa-users',
index 861b24d..689edde 100644 (file)
@@ -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() {
index 4967229..6925e02 100644 (file)
@@ -461,7 +461,7 @@ class core_user {
                 'choices' => array_merge(array('' => ''), get_string_manager()->get_list_of_countries(true, true)));
         $fields['lang'] = array('type' => PARAM_LANG, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->lang,
                 'choices' => array_merge(array('' => ''), get_string_manager()->get_list_of_translations(false)));
-        $fields['calendartype'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->calendartype,
+        $fields['calendartype'] = array('type' => PARAM_PLUGIN, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->calendartype,
                 'choices' => array_merge(array('' => ''), \core_calendar\type_factory::get_list_of_calendar_types()));
         $fields['theme'] = array('type' => PARAM_THEME, 'null' => NULL_NOT_ALLOWED,
                 'default' => theme_config::DEFAULT_THEME, 'choices' => array_merge(array('' => ''), get_list_of_themes()));
index c32ad4c..c8bf817 100644 (file)
@@ -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) {
index bdc4019..d608f24 100644 (file)
@@ -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(
index b1dafb9..28faa7e 100644 (file)
@@ -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 = 0, $filepath = '/') {
+
     $fs = get_file_storage();
 
     $results = array(
@@ -515,11 +535,8 @@ function file_get_draft_area_info($draftitemid, $filepath = '/') {
         'filesize_without_references' => 0
     );
 
-    if ($filepath != '/') {
-        $draftfiles = $fs->get_directory_files($usercontext->id, 'user', 'draft', $draftitemid, $filepath, true, true);
-    } else {
-        $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id', true);
-    }
+    $draftfiles = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath, true, true);
+
     foreach ($draftfiles as $file) {
         if ($file->is_directory()) {
             $results['foldercount'] += 1;
index b7d0fd6..2131314 100644 (file)
@@ -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')) {
index a3997c4..57c67c0 100644 (file)
@@ -229,6 +229,11 @@ class phpunit_util extends testing_util {
         // Reset internal users.
         core_user::reset_internal_users();
 
+        // Clear static caches in calendar container.
+        if (class_exists('\core_calendar\local\event\container', false)) {
+            core_calendar\local\event\container::reset_caches();
+        }
+
         //TODO MDL-25290: add more resets here and probably refactor them to new core function
 
         // Reset course and module caches.
index 84d077b..4dd84f8 100644 (file)
@@ -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;
index ea18b51..87a37bd 100644 (file)
@@ -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(1, $fileinfo['foldercount']);   // 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(1, $fileinfo['foldercount']);   // 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(1, $fileinfo['foldercount']);   // 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']);
+    }
 }
 
 /**
index 09c2839..fce1d3a 100644 (file)
@@ -10,6 +10,9 @@ 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.
+* file_get_draft_area_info does not sum the root folder anymore when calculating the foldercount.
 
 === 3.3.1 ===
 
index d12bfd2..6f50f4c 100644 (file)
Binary files a/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-debug.js and b/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-debug.js differ
index fd52350..94a7844 100644 (file)
Binary files a/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-min.js and b/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-min.js differ
index d12bfd2..6f50f4c 100644 (file)
Binary files a/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm.js and b/lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm.js differ
index 88eead8..98d87ea 100644 (file)
@@ -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('/');
         }
 
 });
index 6c77c7f..d57bd60 100644 (file)
Binary files a/media/player/videojs/amd/build/loader.min.js and b/media/player/videojs/amd/build/loader.min.js differ
index 2601cf8..427b609 100644 (file)
@@ -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) {
index f13352f..3d076b0 100644 (file)
@@ -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')
         ];
index 43d698c..7bc50ea 100644 (file)
@@ -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="{&quot;techOrder&quot;: [&quot;youtube&quot;]', $t);
+        $this->assertContains('data-setup-lazy="{&quot;techOrder&quot;: [&quot;youtube&quot;]', $t);
     }
 
     /**
index bded037..5f20161 100644 (file)
Binary files a/message/amd/build/message_repository.min.js and b/message/amd/build/message_repository.min.js differ
index b81eae6..6c0ee94 100644 (file)
@@ -22,7 +22,7 @@
  * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
  * @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;
     };
index beb592e..6d076df 100644 (file)
Binary files a/message/output/popup/amd/build/notification_repository.min.js and b/message/output/popup/amd/build/notification_repository.min.js differ
index 093238a..2fdb83e 100644 (file)
@@ -22,7 +22,7 @@
  * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
  * @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;
     };
index 381adb2..309cd8a 100644 (file)
@@ -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.';
index e2fe32f..d96b293 100644 (file)
@@ -71,7 +71,12 @@ 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 ($this->assignment->has_instance()) {
+            $defaultfiletypes = $this->get_config('filetypeslist');
+        } else {
+            $defaultfiletypes = get_config('assignsubmission_file', 'filetypes');
+        }
+        $defaultfiletypes = (string)$defaultfiletypes;
 
         $settings = array();
         $options = array();
@@ -108,7 +113,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);
index 4d7eb22..a895e62 100644 (file)
@@ -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_RAW, 60));
+
 if (isset($CFG->maxbytes)) {
 
     $name = new lang_string('maximumsubmissionsize', 'assignsubmission_file');
index a331db6..bceb426 100644 (file)
@@ -42,16 +42,23 @@ $string['addaquestionpage'] = 'Add a question page';
 $string['addaquestionpagehere'] = 'Add a question page here';
 $string['addbranchtable'] = 'Add a content page';
 $string['addcluster'] = 'Add a cluster';
+$string['addessay'] = 'Create an Essay question page';
 $string['addedabranchtable'] = 'Added a content page';
 $string['addedanendofbranch'] = 'Added an end of branch';
 $string['addedaquestionpage'] = 'Added a question page';
 $string['addedcluster'] = 'Added a cluster';
 $string['addedendofcluster'] = 'Added an end of cluster';
+$string['addendofbranch'] = 'Add end of branch';
 $string['addendofcluster'] = 'Add an end of cluster';
+$string['addmatching'] = 'Create a Matching question page';
+$string['addmultichoice'] = 'Create a Multichoice question page';
 $string['addnewgroupoverride'] = 'Add group override';
 $string['addnewuseroverride'] = 'Add user override';
+$string['addnumerical'] = 'Create a Numerical question page';
 $string['additionalattemptsremaining'] = 'Completed, You can re-attempt this lesson';
 $string['addpage'] = 'Add a page';
+$string['addshortanswer'] = 'Create a Short answer question page';
+$string['addtruefalse'] = 'Create a True/false question page';
 $string['allowofflineattempts'] = 'Allow lesson to be attempted offline using the mobile app';
 $string['allowofflineattempts_help'] = 'If enabled, a mobile app user can download the lesson and attempt it offline.
 All the possible answers and correct responses will be downloaded as well.
@@ -162,11 +169,22 @@ $string['displayscorewithessays'] = '<p>You earned {$a->score} out of {$a->tempm
 $string['displayscorewithoutessays'] = 'Your score is {$a->score} (out of {$a->grade}).';
 $string['duplicatepagenamed'] = 'Duplicate page: {$a}';
 $string['edit'] = 'Edit';
+$string['editbranchtable'] = 'Editing a content page';
+$string['editcluster'] = 'Editing a cluster';
+$string['editendofcluster'] = 'Editing an end of cluster page';
+$string['editendofbranch'] = 'Editing an end of branch page';
+$string['editessay'] = 'Editing an Essay question page';
 $string['editingquestionpage'] = 'Editing {$a} question page';
 $string['editlessonsettings'] = 'Edit lesson settings';
+$string['editmatching'] = 'Editing a Matching question page';
+$string['editmultichoice'] = 'Editing a Multichoice question page';
+$string['editnumerical'] = 'Editing a Numerical question page';
 $string['editoverride'] = 'Edit override';
 $string['editpage'] = 'Edit page contents';
 $string['editpagecontent'] = 'Edit page contents';
+$string['editquestion'] = 'Editing a question page';
+$string['editshortanswer'] = 'Editing a Short answer question page';
+$string['edittruefalse'] = 'Editing a True/false question page';
 $string['email'] = 'Email';
 $string['emailallgradedessays'] = 'Email ALL graded essays';
 $string['emailgradedessays'] = 'Email graded essays';
index e0b143b..74f584a 100644 (file)
@@ -1247,7 +1247,13 @@ abstract class lesson_add_page_form_base extends moodleform {
         $mform = $this->_form;
         $editoroptions = $this->_customdata['editoroptions'];
 
-        $mform->addElement('header', 'qtypeheading', get_string('createaquestionpage', 'lesson', get_string($this->qtypestring, 'lesson')));
+        if ($this->qtypestring != 'selectaqtype') {
+            if ($this->_customdata['edit']) {
+                $mform->addElement('header', 'qtypeheading', get_string('edit'. $this->qtypestring, 'lesson'));
+            } else {
+                $mform->addElement('header', 'qtypeheading', get_string('add'. $this->qtypestring, 'lesson'));
+            }
+        }
 
         if (!empty($this->_customdata['returnto'])) {
             $mform->addElement('hidden', 'returnto', $this->_customdata['returnto']);
index c7e4342..23f9c58 100644 (file)
@@ -324,7 +324,11 @@ class lesson_add_page_form_branchtable extends lesson_add_page_form_base {
 
         $jumptooptions = lesson_page_type_branchtable::get_jumptooptions($firstpage, $lesson);
 
-        $mform->setDefault('qtypeheading', get_string('addabranchtable', 'lesson'));
+        if ($this->_customdata['edit']) {
+            $mform->setDefault('qtypeheading', get_string('editbranchtable', 'lesson'));
+        } else {
+            $mform->setDefault('qtypeheading', get_string('addabranchtable', 'lesson'));
+        }
 
         $mform->addElement('hidden', 'firstpage');
         $mform->setType('firstpage', PARAM_BOOL);
diff --git a/pix/i/mahara_host.gif b/pix/i/mahara_host.gif
deleted file mode 100644 (file)
index 6b42253..0000000
Binary files a/pix/i/mahara_host.gif and /dev/null differ
diff --git a/pix/i/mahara_host.png b/pix/i/mahara_host.png
new file mode 100644 (file)
index 0000000..c2e655d
Binary files /dev/null and b/pix/i/mahara_host.png differ
diff --git a/pix/i/mahara_host.svg b/pix/i/mahara_host.svg
new file mode 100644 (file)
index 0000000..c5cc65e
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->\r
+<svg version="1.1" id="full_color" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="16" width="16" x="0px" y="0px"\r
+        viewBox="0 0 65.2 124.7" style="enable-background:new 0 0 65.2 124.7;" xml:space="preserve" preserveAspectRatio="xMinYMid meet">\r
+<style type="text/css">\r
+       .st0{fill:#566D31;}\r
+</style>\r
+<path class="st0" d="M63.2,92c0-17.3-14.7-36.7-16-35.8c-3.5,2.2,10.1,13.4,10.1,34.3c0,11.7-6.9,23.2-22.1,23.2\r
+       c-14.7,0-24.9-9.4-24.9-23.6c0-13.5,9.3-26.7,14.9-33.5c2.2,3.2,7.4,11.2,8.5,13c1.4,2.3,3.3,7.3-0.2,7.8c-5.1,0.8-5.3-7.3-5.5-9.7\r
+       c-0.1-0.8-1.3-0.9-1.7-0.2c-1.6,2.9-2.8,11.6,2,15.7c3.4,2.9,9.4,3,11.8-3.6c3.8,6.6,4.8,12.7,5.4,16.8c0.1,0.7,1.1,0.7,1.2,0\r
+       c2.3-13.5-4-22.9-6.1-27.1c-2.1-4.2-10.8-16.5-12.3-19c-1.5-2.5-5.6-9.7-5.6-17.4c0-11.7,9.6-22.9,11.4-22.9c1.8,0,12,9.3,12,23.3\r
+       c0,5.3-1.2,9.2-2.6,12c-0.5,0.9-1.7,1-2.3,0.1c-5.6-8.7-5.6-10.5-2.1-13c2.3-1.7-9-6.4-8.6,3.3c0.2,5.1,7.4,15.5,9.2,17.5\r
+       s12-2.2,12-20.7c0-18.5-15.9-30.8-17.9-30.8C31.8,1.7,16,16.7,16,32.5c0,7.8,2.8,14.5,6.1,19.7C21.7,52.5,1.8,68.3,2,92\r
+       c0.2,18.1,12.3,31.1,32.1,31.1C52,123.1,63.2,111.3,63.2,92z"/>\r
+</svg>\r
index bfa7815..c5df38a 100644 (file)
@@ -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;
+    }
+}
index 7f757a1..cb149d1 100644 (file)
@@ -346,9 +346,6 @@ a.skip:active {
     margin-left: 43px;
 }
 // Group
-#page-group-index #groupeditform {
-    text-align: center;
-}
 
 #doc-contents h1 {
     margin: 1em 0 0 0;
index 701d834..af83503 100644 (file)
     }
 }
 
-#groupeditform {
-    .groups,
-    .members {
-        min-width: 175px;
-        width: 49%;
-        float: left;
-        text-align: left;
-
-        select {
-            min-width: 175px;
-            max-width: 90%;
-        }
-    }
-}
-
 // Remove the little cog from participants page because we are putting a cog menu there.
 .userlist h3 .action-icon {
     display: none;
index 3cc6434..56cf9c3 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    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: <ul><li>It is on or after <strong>8 June 2012</strong></li></ul>",
+        "ishidden": 0,
+        "isstealth": 0,
+        "isrestricted": 1,
+        "isfullinfo": 1
+    }
 }}
 {{#text}}
 <div class="availabilityinfo {{classes}}">
+    {{^isrestricted}}
     <span class="tag tag-info">{{{text}}}</span>
+    {{/isrestricted}}
+    {{#isrestricted}}
+    <span class="tag tag-info">{{#str}}restricted, core{{/str}}</span> {{{text}}}
+    {{/isrestricted}}
 </div>
 {{/text}}
index c42131d..68c70da 100644 (file)
                 display: inline;
             }
         }
+        p {
+            &.hasicon {
+                img {
+                    &.icon {
+                        padding-right: 0;
+                    }
+                }
+            }
+        }
     }
     .footer {
         margin-bottom: 4px;
index 03e0b8b..6a60df6 100644 (file)
@@ -429,9 +429,6 @@ a.skip:active {
     margin-left: 43px;
 }
 // Group
-#page-group-index #groupeditform {
-    text-align: center;
-}
 #doc-contents h1 {
     margin: 1em 0 0 0;
 }
index 3ea1e6f..88fbcca 100644 (file)
@@ -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);
index c4df6c0..24a4802 100644 (file)
     }
 }
 
-#groupeditform {
-    .groups,
-    .members {
-        min-width: 175px;
-        width: 49%;
-        float: left;
-        text-align: left;
-
-        select {
-            min-width: 175px;
-            max-width: 90%;
-        }
-    }
-}
-
 /** Preferences page */
 .preferences-group {
     ul {
index fa74dbd..147abba 100644 (file)
@@ -2723,9 +2723,6 @@ a.skip:active {
 .blog_entry .content {
   margin-left: 43px;
 }
-#page-group-index #groupeditform {
-  text-align: center;
-}
 #doc-contents h1 {
   margin: 1em 0 0 0;
 }
@@ -9891,18 +9888,6 @@ body.path-question-type .mform fieldset.hidden {
 .profileeditor > .singlebutton input {
   margin: 0;
 }
-#groupeditform .groups,
-#groupeditform .members {
-  min-width: 175px;
-  width: 49%;
-  float: left;
-  text-align: left;
-}
-#groupeditform .groups select,
-#groupeditform .members select {
-  min-width: 175px;
-  max-width: 90%;
-}
 /** Preferences page */
 .preferences-group ul {
   list-style: none;
@@ -15873,6 +15858,9 @@ body {
   margin-left: 5px;
   display: inline;
 }
+.block .content p.hasicon img.icon {
+  padding-right: 0;
+}
 .block .footer {
   margin-bottom: 4px;
   display: block;
@@ -19339,6 +19327,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);
index 650eb9b..42577fe 100644 (file)
 }}
 
 <div id="block-myoverview-{{uniqid}}" class="block-myoverview" data-region="myoverview">
-    <ul class="nav nav-tabs" role="tablist">
-        <li class="nav-item active">
-            <a class="nav-link" href="#myoverview_timeline_view" role="tab" data-toggle="tab">
+    <ul id="block-myoverview-view-choices-{{uniqid}}" class="nav nav-tabs" role="tablist">
+        <li class="nav-item {{#viewingtimeline}}active{{/viewingtimeline}}">
+            <a class="nav-link" href="#myoverview_timeline_view" role="tab" data-toggle="tab" data-tabname="timeline">
                 {{#str}} timeline, block_myoverview {{/str}}
             </a>
         </li>
-        <li class="nav-item">
-            <a class="nav-link" href="#myoverview_courses_view" role="tab" data-toggle="tab">
+        <li class="nav-item {{#viewingcourses}}active{{/viewingcourses}}">
+            <a class="nav-link" href="#myoverview_courses_view" role="tab" data-toggle="tab" data-tabname="courses">
                 {{#str}} courses {{/str}}
             </a>
         </li>
     </ul>
 
     <div class="tab-content">
-        <div role="tabpanel" class="tab-pane fade in active" id="myoverview_timeline_view">
+        <div role="tabpanel" class="tab-pane fade {{#viewingtimeline}}in active{{/viewingtimeline}}" id="myoverview_timeline_view">
             {{> block_myoverview/timeline-view }}
         </div>
-        <div role="tabpanel" class="tab-pane fade" id="myoverview_courses_view">
+        <div role="tabpanel" class="tab-pane fade {{#viewingcourses}}in active{{/viewingcourses}}" id="myoverview_courses_view">
             {{#coursesview}}
                 {{> block_myoverview/courses-view }}
             {{/coursesview}}
         </div>
     </div>
 </div>
+{{#js}}
+require(['jquery', 'block_myoverview/tab_preferences'], function($, TabPreferences) {
+    var root = $('#block-myoverview-view-choices-{{uniqid}}');
+    TabPreferences.registerEventListeners(root);
+});
+{{/js}}
index fd81eb5..ee90c95 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    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: <ul><li>It is on or after <strong>8 June 2012</strong></li></ul>",
+        "ishidden": 0,
+        "isstealth": 0,
+        "isrestricted": 1,
+        "isfullinfo": 1
+    }
 }}
 {{#text}}
 <div class="availabilityinfo {{classes}}">
+    {{^isrestricted}}
     <span class="label label-info">{{{text}}}</span>
+    {{/isrestricted}}
+    {{#isrestricted}}
+    <span class="label label-info">{{#str}}restricted, core{{/str}}</span> {{{text}}}
+    {{/isrestricted}}
 </div>
 {{/text}}
index 5ead62c..2eea9f3 100644 (file)
@@ -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()
+            )
+        );
+    }
 }
index 32c99f4..dadaf61 100644 (file)
@@ -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();
index be52830..cf04d56 100644 (file)
@@ -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(0, $result['foldercount']);
+        $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(0, $result['foldercount']);
+        $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);
     }
 }
index 03d7d5b..acf34f5 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017061600.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017062200.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.4dev (Build: 20170616)'; // Human-friendly version name
+$release  = '3.4dev (Build: 20170622)'; // Human-friendly version name
 
 $branch   = '34';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.
index 84b0fd4..b091147 100644 (file)
@@ -197,6 +197,14 @@ class core_webservice_external extends external_api {
         // User home page.
         $siteinfo['userhomepage'] = get_home_page();
 
+        // Calendar.
+        $siteinfo['sitecalendartype'] = $CFG->calendartype;
+        if (empty($USER->calendartype)) {
+            $siteinfo['usercalendartype'] = $CFG->calendartype;
+        } else {
+            $siteinfo['usercalendartype'] = $USER->calendartype;
+        }
+
         return $siteinfo;
     }
 
@@ -259,7 +267,9 @@ class core_webservice_external extends external_api {
                 'userhomepage' => new external_value(PARAM_INT,
                                                         'the default home page for the user: 0 for the site home, 1 for dashboard',
                                                         VALUE_OPTIONAL),
-                'siteid'  => new external_value(PARAM_INT, 'Site course ID', VALUE_OPTIONAL)
+                'siteid'  => new external_value(PARAM_INT, 'Site course ID', VALUE_OPTIONAL),
+                'sitecalendartype'  => new external_value(PARAM_PLUGIN, 'Calendar type set in the site.', VALUE_OPTIONAL),
+                'usercalendartype'  => new external_value(PARAM_PLUGIN, 'Calendar typed used by the user.', VALUE_OPTIONAL),
             )
         );
     }
index f845b62..b5bea8d 100644 (file)
@@ -122,6 +122,12 @@ class core_webservice_externallib_testcase extends externallib_advanced_testcase
         $this->assertEquals(true, $siteinfo['usercanmanageownfiles']);
 
         $this->assertEquals(HOMEPAGE_MY, $siteinfo['userhomepage']);
+        $this->assertEquals($CFG->calendartype, $siteinfo['sitecalendartype']);
+        if (!empty($USER->calendartype)) {
+            $this->assertEquals($USER->calendartype, $siteinfo['usercalendartype']);
+        } else {
+            $this->assertEquals($CFG->calendartype, $siteinfo['usercalendartype']);
+        }
 
         // Now as admin.
         $this->setAdminUser();
index 895a5f9..2301a7c 100644 (file)
@@ -3,6 +3,11 @@ information provided here is intended especially for developers.
 
 This information is intended for authors of webservices, not people writing webservice clients.
 
+=== 3.4 ===
+
+* External function core_webservice_external::get_site_info() now returns the calendar type used in the site and
+  by the user in the sitecalendartype and usercalendartype fields.
+
 === 3.2 ===
 
 * webservice->get_external_functions now returns the external function list ordered by name ASC.