-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'])) {
$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";
$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;
+}
}
}
- // 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));
{{/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>
$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;
$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();
* @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.
* @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.
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');
} 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');
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);
display: block;
}
-.block_navigation .block_tree [aria-hidden="true"] {
+.block_navigation .block_tree [aria-hidden="true"]:not(.icon) {
display: none;
}
display: block;
}
-.block_settings .block_tree [aria-hidden="true"] {
+.block_settings .block_tree [aria-hidden="true"]:not(.icon) {
display: none;
}
*/
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.
*/
*/
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
);
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)) {
}
}
+ /**
+ * 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.
*
}
/**
- * 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;
}
}
*/
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();
];
$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);
$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);
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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);
+ }
+}
$results = array(
'groups' => $usergroups,
+ 'canaccessallgroups' => has_capability('moodle/site:accessallgroups', $context, $user),
'warnings' => $warnings
);
return $results;
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(),
)
);
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) {
$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"> </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 = ' ';
+$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;
}
}
$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> </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> </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();
--- /dev/null
+{{!
+ 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">
+ <input type="hidden" name="id" value="{{courseid}}">
+ <div class="col-md-6 span6 m-b-1 groups">
+ <div class="form-group">
+ <label for="groups">
+ <span id="groupslabel">{{#str}}groups{{/str}}</span>
+ <span id="thegrouping"> </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 members">
+ <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}}
$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']) {
$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']);
}
/**
$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';
* @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
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);
+ }
}
};
* @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 = [],
'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',
'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',
*/
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() {
// 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) {
'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(
* (more information will be added as needed).
*/
function file_get_draft_area_info($draftitemid, $filepath = '/') {
- global $CFG, $USER;
+ global $USER;
$usercontext = context_user::instance($USER->id);
+ return file_get_file_area_info($usercontext->id, 'user', 'draft', $draftitemid, $filepath);
+}
+
+/**
+ * Returns information about files in an area.
+ *
+ * @param int $contextid context id
+ * @param string $component component
+ * @param string $filearea file area name
+ * @param int $itemid item id or all files if not specified
+ * @param string $filepath path to the directory from which the information have to be retrieved.
+ * @return array with the following entries:
+ * 'filecount' => number of files in the area.
+ * 'filesize' => total size of the files in the area.
+ * 'foldercount' => number of folders in the area.
+ * 'filesize_without_references' => total size of the area excluding file references.
+ * @since Moodle 3.4
+ */
+function file_get_file_area_info($contextid, $component, $filearea, $itemid = false, $filepath = '/') {
+
$fs = get_file_storage();
$results = array(
);
if ($filepath != '/') {
- $draftfiles = $fs->get_directory_files($usercontext->id, 'user', 'draft', $draftitemid, $filepath, true, true);
+ $draftfiles = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath, true, true);
} else {
- $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id', true);
+ $draftfiles = $fs->get_area_files($contextid, $component, $filearea, $itemid, 'id', true);
}
foreach ($draftfiles as $file) {
if ($file->is_directory()) {
// 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.
* 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;
$file = array_shift($files);
$this->assertTrue($file->is_directory());
}
+
+ /**
+ * Test file_get_draft_area_info.
+ */
+ public function test_file_get_draft_area_info() {
+ global $USER;
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $fs = get_file_storage();
+
+ $filerecord = array(
+ 'filename' => 'one.txt',
+ );
+ $file = self::create_draft_file($filerecord);
+ $size = $file->get_filesize();
+ $draftitemid = $file->get_itemid();
+ // Add another file.
+ $filerecord = array(
+ 'itemid' => $draftitemid,
+ 'filename' => 'second.txt',
+ );
+ $file = self::create_draft_file($filerecord);
+ $size += $file->get_filesize();
+
+ // Create directory.
+ $usercontext = context_user::instance($USER->id);
+ $dir = $fs->create_directory($usercontext->id, 'user', 'draft', $draftitemid, '/testsubdir/');
+ // Add file to directory.
+ $filerecord = array(
+ 'itemid' => $draftitemid,
+ 'filename' => 'third.txt',
+ 'filepath' => '/testsubdir/',
+ );
+ $file = self::create_draft_file($filerecord);
+ $size += $file->get_filesize();
+
+ $fileinfo = file_get_draft_area_info($draftitemid);
+ $this->assertEquals(3, $fileinfo['filecount']);
+ $this->assertEquals($size, $fileinfo['filesize']);
+ $this->assertEquals(2, $fileinfo['foldercount']); // Base and directory created.
+ $this->assertEquals($size, $fileinfo['filesize_without_references']);
+
+ // Now get files from just one folder.
+ $fileinfo = file_get_draft_area_info($draftitemid, '/testsubdir/');
+ $this->assertEquals(1, $fileinfo['filecount']);
+ $this->assertEquals($file->get_filesize(), $fileinfo['filesize']);
+ $this->assertEquals(0, $fileinfo['foldercount']); // No subdirectories inside the directory.
+ $this->assertEquals($file->get_filesize(), $fileinfo['filesize_without_references']);
+
+ // Check we get the same results if we call file_get_file_area_info.
+ $fileinfo = file_get_file_area_info($usercontext->id, 'user', 'draft', $draftitemid);
+ $this->assertEquals(3, $fileinfo['filecount']);
+ $this->assertEquals($size, $fileinfo['filesize']);
+ $this->assertEquals(2, $fileinfo['foldercount']); // Base and directory created.
+ $this->assertEquals($size, $fileinfo['filesize_without_references']);
+ }
+
+ /**
+ * Test file_get_file_area_info.
+ */
+ public function test_file_get_file_area_info() {
+ global $USER;
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $fs = get_file_storage();
+
+ $filerecord = array(
+ 'filename' => 'one.txt',
+ );
+ $file = self::create_draft_file($filerecord);
+ $size = $file->get_filesize();
+ $draftitemid = $file->get_itemid();
+ // Add another file.
+ $filerecord = array(
+ 'itemid' => $draftitemid,
+ 'filename' => 'second.txt',
+ );
+ $file = self::create_draft_file($filerecord);
+ $size += $file->get_filesize();
+
+ // Create directory.
+ $usercontext = context_user::instance($USER->id);
+ $dir = $fs->create_directory($usercontext->id, 'user', 'draft', $draftitemid, '/testsubdir/');
+ // Add file to directory.
+ $filerecord = array(
+ 'itemid' => $draftitemid,
+ 'filename' => 'third.txt',
+ 'filepath' => '/testsubdir/',
+ );
+ $file = self::create_draft_file($filerecord);
+ $size += $file->get_filesize();
+
+ // Add files to user private file area.
+ $options = array('subdirs' => 1, 'maxfiles' => 3);
+ file_merge_files_from_draft_area_into_filearea($draftitemid, $file->get_contextid(), 'user', 'private', 0, $options);
+
+ $fileinfo = file_get_file_area_info($usercontext->id, 'user', 'private');
+ $this->assertEquals(3, $fileinfo['filecount']);
+ $this->assertEquals($size, $fileinfo['filesize']);
+ $this->assertEquals(2, $fileinfo['foldercount']); // Base and directory created.
+ $this->assertEquals($size, $fileinfo['filesize_without_references']);
+
+ // Now get files from just one folder.
+ $fileinfo = file_get_file_area_info($usercontext->id, 'user', 'private', 0, '/testsubdir/');
+ $this->assertEquals(1, $fileinfo['filecount']);
+ $this->assertEquals($file->get_filesize(), $fileinfo['filesize']);
+ $this->assertEquals(0, $fileinfo['foldercount']); // No subdirectories inside the directory.
+ $this->assertEquals($file->get_filesize(), $fileinfo['filesize_without_references']);
+ }
}
/**
* Removed accesslib private functions: load_course_context(), load_role_access_by_context(), dedupe_user_access() (MDL-49398).
* Internal "accessdata" structure format has changed to improve ability to perform role definition caching (MDL-49398).
* Role definitions are no longer cached in user session (MDL-49398).
+* External function core_group_external::get_activity_allowed_groups now returns an additional field: canaccessallgroups.
+ It indicates whether the user will be able to access all the activity groups.
=== 3.3.1 ===
.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) {
}
// 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')
];
protected function youtube_plugin_engaged($t) {
$this->assertContains('mediaplugin_videojs', $t);
- $this->assertContains('data-setup="{"techOrder": ["youtube"]', $t);
+ $this->assertContains('data-setup-lazy="{"techOrder": ["youtube"]', $t);
}
/**
* @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.
*
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;
};
* @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.
*
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;
};
}
}
}
+
+// 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;
+ }
+}
display: inline;
}
}
+ p {
+ &.hasicon {
+ img {
+ &.icon {
+ padding-right: 0;
+ }
+ }
+ }
+ }
}
.footer {
margin-bottom: 4px;
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);
margin-left: 5px;
display: inline;
}
+.block .content p.hasicon img.icon {
+ padding-right: 0;
+}
.block .footer {
margin-bottom: 4px;
display: block;
}
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);
}}
<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}}
)
);
}
+
+ /**
+ * 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()
+ )
+ );
+ }
}
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();
} catch (Exception $e) {
$this->fail('Expecting \'usernotfullysetup\' moodle_exception to be thrown.');
}
+ }
+
+ /**
+ * Test get_private_files_info
+ */
+ public function test_get_private_files_info() {
+
+ $this->resetAfterTest(true);
+ $user = self::getDataGenerator()->create_user();
+ $this->setUser($user);
+ $usercontext = context_user::instance($user->id);
+
+ $filerecord = array(
+ 'contextid' => $usercontext->id,
+ 'component' => 'user',
+ 'filearea' => 'private',
+ 'itemid' => 0,
+ 'filepath' => '/',
+ 'filename' => 'thefile',
+ );
+
+ $fs = get_file_storage();
+ $file = $fs->create_file_from_string($filerecord, 'abc');
+
+ // Get my private files information.
+ $result = core_user_external::get_private_files_info();
+ $result = external_api::clean_returnvalue(core_user_external::get_private_files_info_returns(), $result);
+ $this->assertEquals(1, $result['filecount']);
+ $this->assertEquals($file->get_filesize(), $result['filesize']);
+ $this->assertEquals(1, $result['foldercount']); // Base directory.
+ $this->assertEquals($file->get_filesize(), $result['filesizewithoutreferences']);
+
+ // As admin, get user information.
+ $this->setAdminUser();
+ $result = core_user_external::get_private_files_info($user->id);
+ $result = external_api::clean_returnvalue(core_user_external::get_private_files_info_returns(), $result);
+ $this->assertEquals(1, $result['filecount']);
+ $this->assertEquals($file->get_filesize(), $result['filesize']);
+ $this->assertEquals(1, $result['foldercount']); // Base directory.
+ $this->assertEquals($file->get_filesize(), $result['filesizewithoutreferences']);
+ }
+
+ /**
+ * Test get_private_files_info missing permissions.
+ */
+ public function test_get_private_files_info_missing_permissions() {
+
+ $this->resetAfterTest(true);
+ $user1 = self::getDataGenerator()->create_user();
+ $user2 = self::getDataGenerator()->create_user();
+ $this->setUser($user1);
+ $this->setExpectedException('required_capability_exception');
+ // Try to retrieve other user private files info.
+ core_user_external::get_private_files_info($user2->id);
}
}
defined('MOODLE_INTERNAL') || die();
-$version = 2017061600.00; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2017061900.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.