Merge branch 'master' into install_master

This commit is contained in:
AMOS bot 2017-06-24 00:08:36 +08:00
commit b87f163a1b
75 changed files with 1099 additions and 341 deletions

View 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;
}

View 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);

View 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";
}
}

View 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;

View 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));

View File

@ -81,10 +81,11 @@
{{/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>

View 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();

View File

@ -1 +1 @@
define(["jquery","core/ajax","core/custom_interaction_events"],function(a,b,c){var d=function(d){c.define(d,[c.events.activate]),d.on(c.events.activate,"[data-toggle='tab']",function(c){var d=a(c.currentTarget).data("tabname");"function"==typeof window.history.pushState&&window.history.pushState(null,null,"?myoverviewtab="+d);var e={methodname:"core_user_update_user_preferences",args:{preferences:[{type:"block_myoverview_last_tab",value:d}]}};b.call([e])[0].fail(Notification.exception)})};return{registerEventListeners:d}});
define(["jquery","core/ajax","core/custom_interaction_events","core/notification"],function(a,b,c,d){var e=function(e){c.define(e,[c.events.activate]),e.on(c.events.activate,"[data-toggle='tab']",function(c){var e=a(c.currentTarget).data("tabname");"function"==typeof window.history.pushState&&window.history.pushState(null,null,"?myoverviewtab="+e);var f={methodname:"core_user_update_user_preferences",args:{preferences:[{type:"block_myoverview_last_tab",value:e}]}};b.call([f])[0].fail(d.exception)})};return{registerEventListeners:e}});

View 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.

View File

@ -1 +1 @@
define(["jquery"],function(a){function b(d,e){var f=a("<ul></ul>");f.attr("role","group"),f.attr("aria-hidden",!0),a.each(e,function(d,e){if("object"==typeof e){var g=a("<li></li>"),h=a("<p></p>"),i=e.id||e.key+"_tree_item",j=null,k=!(!e.expandable&&!e.haschildren);if(h.addClass("tree_item"),h.attr("id",i),h.attr("role","treeitem"),h.attr("tabindex","-1"),e.requiresajaxloading&&(h.attr("data-requires-ajax",!0),h.attr("data-node-id",e.id),h.attr("data-node-key",e.key),h.attr("data-node-type",e.type)),k&&(g.addClass("collapsed contains_branch"),h.attr("aria-expanded",!1),h.addClass("branch")),!e.icon||k&&e.type!==c.ACTIVITY&&e.type!==c.RESOURCE||(g.addClass("item_with_icon"),h.addClass("hasicon"),j=a("<img/>"),j.attr("alt",e.icon.alt),j.attr("title",e.icon.title),j.attr("src",M.util.image_url(e.icon.pix,e.icon.component)),a.each(e.icon.classes,function(a,b){j.addClass(b)})),e.link){var l=a('<a title="'+e.title+'" href="'+e.link+'"></a>');j?(l.append(j),l.append('<span class="item-content-wrap">'+e.name+"</span>")):l.append(e.name),e.hidden&&l.addClass("dimmed"),h.append(l)}else{var m=a("<span></span>");j?(m.append(j),m.append('<span class="item-content-wrap">'+e.name+"</span>")):m.append(e.name),e.hidden&&m.addClass("dimmed"),h.append(m)}g.append(h),f.append(g),e.children&&e.children.length?b(h,e.children):k&&!e.requiresajaxloading&&(g.removeClass("contains_branch"),h.addClass("emptybranch"))}}),d.parent().append(f);var g=d.attr("id")+"_group";f.attr("id",g),d.attr("aria-owns",g),d.attr("role","treeitem")}var c={ACTIVITY:40,RESOURCE:50};return{render:function(a,c){if(c.children&&c.children.length){b(a,c.children);var d=a.children("[role='treeitem']").first(),e=a.find("#"+d.attr("aria-owns"));d.attr("aria-expanded",!0),e.attr("aria-hidden",!1)}else a.parent().hasClass("contains_branch")&&(a.parent().removeClass("contains_branch"),a.addClass("emptybranch"))}}});
define(["jquery","core/templates","core/notification","core/url"],function(a,b,c,d){function e(g,h){var i=a("<ul></ul>");i.attr("role","group"),i.attr("aria-hidden",!0),a.each(h,function(g,h){if("object"==typeof h){var j=a("<li></li>"),k=a("<p></p>"),l=h.id||h.key+"_tree_item",m=null,n=!(!h.expandable&&!h.haschildren);k.addClass("tree_item"),k.attr("id",l),k.attr("role","treeitem"),k.attr("tabindex","-1"),h.requiresajaxloading&&(k.attr("data-requires-ajax",!0),k.attr("data-node-id",h.id),k.attr("data-node-key",h.key),k.attr("data-node-type",h.type)),n&&(j.addClass("collapsed contains_branch"),k.attr("aria-expanded",!1),k.addClass("branch"));var o=null;if(h.link){var p=a('<a title="'+h.title+'" href="'+h.link+'"></a>');o=p,p.append('<span class="item-content-wrap">'+h.name+"</span>"),h.hidden&&p.addClass("dimmed"),k.append(p)}else{var q=a("<span></span>");o=q,q.append('<span class="item-content-wrap">'+h.name+"</span>"),h.hidden&&q.addClass("dimmed"),k.append(q)}!h.icon||n&&h.type!==f.ACTIVITY&&h.type!==f.RESOURCE||(j.addClass("item_with_icon"),k.addClass("hasicon"),h.type===f.ACTIVITY||h.type===f.RESOURCE?(m=a("<img/>"),m.attr("alt",h.icon.alt),m.attr("title",h.icon.title),m.attr("src",d.imageUrl(h.icon.pix,h.icon.component)),a.each(h.icon.classes,function(a,b){m.addClass(b)}),o.prepend(m)):("moodle"==h.icon.component&&(h.icon.component="core"),b.renderPix(h.icon.pix,h.icon.component,h.icon.title).then(function(a){o.prepend(a)})["catch"](c.exception))),j.append(k),i.append(j),h.children&&h.children.length?e(k,h.children):n&&!h.requiresajaxloading&&(j.removeClass("contains_branch"),k.addClass("emptybranch"))}}),g.parent().append(i);var j=g.attr("id")+"_group";i.attr("id",j),g.attr("aria-owns",j),g.attr("role","treeitem")}var f={ACTIVITY:40,RESOURCE:50};return{render:function(a,b){if(b.children&&b.children.length){e(a,b.children);var c=a.children("[role='treeitem']").first(),d=a.find("#"+c.attr("aria-owns"));c.attr("aria-expanded",!0),d.attr("aria-hidden",!1)}else a.parent().hasClass("contains_branch")&&(a.parent().removeClass("contains_branch"),a.addClass("emptybranch"))}}});

View 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);

View 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;
}

View 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;
}

View 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;
}
return $eventvisible ? true : false;
}
),
);
// Module does not implement the callback, event should be visible.
if (is_null($eventvisible)) {
return true;
}
return $eventvisible ? true : false;
}
}

View 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);

View 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;

View 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();

View 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].'" />';
}

View 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";',

View File

@ -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;
}
}

View File

@ -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);
}
}

View 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(),
)
);

View 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();

View File

@ -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}}

View 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']);
}
/**

View 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';

View File

@ -1 +1 @@
define(["jquery","core/config"],function(a,b){var c=function(a){var b,c,d=this,e=null,f=0;for(f=0;f<d.length;f++){if(b=d[f],c=a[f],"undefined"==typeof c){e=new Error("missing response");break}if(c.error!==!1){e=c.exception;break}b.deferred.resolve(c.data)}if(null!==e)for(;f<d.length;f++)b=d[f],b.deferred.reject(e)},d=function(a,b){var c=this,d=0;for(d=0;d<c.length;d++){var e=c[d];e.deferred.reject(b)}};return{call:function(e,f,g){var h,i=[],j=[],k=[],l="";for("undefined"==typeof g&&(g=!0),"undefined"==typeof f&&(f=!0),h=0;h<e.length;h++){var m=e[h];i.push({index:h,methodname:m.methodname,args:m.args}),m.deferred=a.Deferred(),j.push(m.deferred.promise()),"undefined"!=typeof m.done&&m.deferred.done(m.done),"undefined"!=typeof m.fail&&m.deferred.fail(m.fail),m.index=h,k.push(m.methodname)}l=k.length<=5?k.sort().join():k.length+"-method-calls",i=JSON.stringify(i);var n={type:"POST",data:i,context:e,dataType:"json",processData:!1,async:f,contentType:"application/json"},o="service.php";g||(o="service-nologin.php");var p=b.wwwroot+"/lib/ajax/"+o+"?sesskey="+b.sesskey+"&info="+l;return f?a.ajax(p,n).done(c).fail(d):(n.success=c,n.error=d,a.ajax(p,n)),j}}});
define(["jquery","core/config","core/log"],function(a,b,c){var d=!1,e=function(a){var b,c,d=this,e=null,f=0;for(f=0;f<d.length;f++){if(b=d[f],c=a[f],"undefined"==typeof c){e=new Error("missing response");break}if(c.error!==!1){e=c.exception;break}b.deferred.resolve(c.data)}if(null!==e)for(;f<d.length;f++)b=d[f],b.deferred.reject(e)},f=function(a,b){var e=this,f=0;for(f=0;f<e.length;f++){var g=e[f];d?c.error("Page unload: "+b):g.deferred.reject(b)}};return{call:function(c,g,h){a(window).bind("beforeunload",function(){d=!0});var i,j=[],k=[],l=[],m="";for("undefined"==typeof h&&(h=!0),"undefined"==typeof g&&(g=!0),i=0;i<c.length;i++){var n=c[i];j.push({index:i,methodname:n.methodname,args:n.args}),n.deferred=a.Deferred(),k.push(n.deferred.promise()),"undefined"!=typeof n.done&&n.deferred.done(n.done),"undefined"!=typeof n.fail&&n.deferred.fail(n.fail),n.index=i,l.push(n.methodname)}m=l.length<=5?l.sort().join():l.length+"-method-calls",j=JSON.stringify(j);var o={type:"POST",data:j,context:c,dataType:"json",processData:!1,async:g,contentType:"application/json"},p="service.php";h||(p="service-nologin.php");var q=b.wwwroot+"/lib/ajax/"+p+"?sesskey="+b.sesskey+"&info="+m;return g?a.ajax(q,o).done(e).fail(f):(o.success=e,o.error=f,a.ajax(q,o)),k}}});

View File

@ -25,7 +25,10 @@
* @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 = [],

View 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',

View 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() {

View 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()));

View 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) {

View 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(

View 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;

View 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')) {

View 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.

View 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;

View 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']);
}
}
/**

View 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 ===

View File

@ -98,7 +98,7 @@ Y.extend(Confirmation, Y.Base, {
_uninstall: function(e, langCodes) {
Y.config.win.location.href = this.get('uninstallUrl') + '?mode=4' +
'&sesskey=' + M.cfg.sesskey +
'&confirmtouninstall=' + langCodes.join('-');
'&confirmtouninstall=' + langCodes.join('/');
}
});

View File

@ -1 +1 @@
YUI.add("moodle-core-languninstallconfirm",function(e,t){function n(){n.superclass.constructor.apply(this,arguments)}var r={UNINSTALLBUTTON:"#languninstallbutton",UNINSTALLSELECT:"#menuuninstalllang option",ENGLISHOPTION:"#menuuninstalllang option[value='en']"};n.NAME=t,n.ATTRS={uninstallUrl:{validator:e.Lang.isString}},e.extend(n,e.Base,{initializer:function(){e.one(r.UNINSTALLBUTTON).on("click",this._confirm,this)},_confirm:function(t){t.preventDefault();var n=[],i=[];e.all(r.UNINSTALLSELECT).each(function(e){e.get("selected")&&(n.push(e.getAttribute("value")),i.push(e.get("text")))});if(n.length===0){(new M.core.alert({message:M.util.get_string("selectlangs","tool_langimport")})).show();return}if(n.indexOf("en")>-1){e.one(r.ENGLISHOPTION).set("selected",!1),(new M.core.alert({message:M.util.get_string("noenglishuninstall","tool_langimport")})).show();return}var s={modal:!0,visible:!1,centered:!0,title:M.util.get_string("uninstall","tool_langimport"),question:M.util.get_string("uninstallconfirm","tool_langimport",i.join(", "))};(new M.core.confirm(s)).show().on("complete-yes",this._uninstall,this,n)},_uninstall:function(t,n){e.config.win.location.href=this.get("uninstallUrl")+"?mode=4"+"&sesskey="+M.cfg.sesskey+"&confirmtouninstall="+n.join("-")}}),e.namespace("M.core.languninstallconfirm").Confirmation=n,e.namespace("M.core.languninstallconfirm").init=function(e){return new n(e)}},"@VERSION@",{requires:["base","node","moodle-core-notification-confirm","moodle-core-notification-alert"]});
YUI.add("moodle-core-languninstallconfirm",function(e,t){function n(){n.superclass.constructor.apply(this,arguments)}var r={UNINSTALLBUTTON:"#languninstallbutton",UNINSTALLSELECT:"#menuuninstalllang option",ENGLISHOPTION:"#menuuninstalllang option[value='en']"};n.NAME=t,n.ATTRS={uninstallUrl:{validator:e.Lang.isString}},e.extend(n,e.Base,{initializer:function(){e.one(r.UNINSTALLBUTTON).on("click",this._confirm,this)},_confirm:function(t){t.preventDefault();var n=[],i=[];e.all(r.UNINSTALLSELECT).each(function(e){e.get("selected")&&(n.push(e.getAttribute("value")),i.push(e.get("text")))});if(n.length===0){(new M.core.alert({message:M.util.get_string("selectlangs","tool_langimport")})).show();return}if(n.indexOf("en")>-1){e.one(r.ENGLISHOPTION).set("selected",!1),(new M.core.alert({message:M.util.get_string("noenglishuninstall","tool_langimport")})).show();return}var s={modal:!0,visible:!1,centered:!0,title:M.util.get_string("uninstall","tool_langimport"),question:M.util.get_string("uninstallconfirm","tool_langimport",i.join(", "))};(new M.core.confirm(s)).show().on("complete-yes",this._uninstall,this,n)},_uninstall:function(t,n){e.config.win.location.href=this.get("uninstallUrl")+"?mode=4"+"&sesskey="+M.cfg.sesskey+"&confirmtouninstall="+n.join("/")}}),e.namespace("M.core.languninstallconfirm").Confirmation=n,e.namespace("M.core.languninstallconfirm").init=function(e){return new n(e)}},"@VERSION@",{requires:["base","node","moodle-core-notification-confirm","moodle-core-notification-alert"]});

View File

@ -98,7 +98,7 @@ Y.extend(Confirmation, Y.Base, {
_uninstall: function(e, langCodes) {
Y.config.win.location.href = this.get('uninstallUrl') + '?mode=4' +
'&sesskey=' + M.cfg.sesskey +
'&confirmtouninstall=' + langCodes.join('-');
'&confirmtouninstall=' + langCodes.join('/');
}
});

View 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('/');
}
});

View File

@ -1 +1 @@
define(["jquery","core/event"],function(a,b){var c,d=function(d){c=d,e(null,a("body")),b.getLegacyEvents().done(function(b){a(document).on(b.FILTER_CONTENT_UPDATED,e)})},e=function(b,d){var e=".mediaplugin_videojs";d.find(e).addBack(e).find("audio, video").each(function(){var b=a(this).attr("id"),d=a(this).data("setup"),e=["media_videojs/video-lazy"];d.techOrder&&d.techOrder.indexOf("youtube")!==-1&&e.push("media_videojs/Youtube-lazy"),require(e,function(a){c&&(c(a),c=null),a(b,d)})})};return{setUp:d}});
define(["jquery","core/event"],function(a,b){var c,d=function(d){c=d,e(null,a("body")),b.getLegacyEvents().done(function(b){a(document).on(b.FILTER_CONTENT_UPDATED,e)})},e=function(b,d){var e=".mediaplugin_videojs";d.find(e).addBack(e).find("audio, video").each(function(){var b=a(this).attr("id"),d=a(this).data("setup-lazy"),e=["media_videojs/video-lazy"];d.techOrder&&d.techOrder.indexOf("youtube")!==-1&&e.push("media_videojs/Youtube-lazy"),require(e,function(a){c&&(c(a),c=null),a(b,d)})})};return{setUp:d}});

View 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) {

View 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')
];

View 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);
}
/**

View File

@ -1 +1 @@
define(["jquery","core/ajax","core/notification","core/log"],function(a,b,c,d){var e=function(a){"undefined"==typeof a.limit&&(a.limit=0),"undefined"==typeof a.offset&&(a.offset=0),a.limitfrom=a.offset,a.limitnum=a.limit,delete a.limit,delete a.offset;var d={methodname:"core_message_data_for_messagearea_conversations",args:a},e=b.call([d])[0];return e.fail(c.exception),e},f=function(a){var c={methodname:"core_message_get_unread_conversations_count",args:a},e=b.call([c])[0];return e.fail(function(a){d.error("Could not retrieve unread message count: "+a.message)}),e},g=function(a){var d={methodname:"core_message_mark_all_messages_as_read",args:a},e=b.call([d])[0];return e.fail(c.exception),e};return{query:e,countUnreadConversations:f,markAllAsRead:g}});
define(["jquery","core/ajax","core/notification"],function(a,b,c){var d=function(a){"undefined"==typeof a.limit&&(a.limit=0),"undefined"==typeof a.offset&&(a.offset=0),a.limitfrom=a.offset,a.limitnum=a.limit,delete a.limit,delete a.offset;var d={methodname:"core_message_data_for_messagearea_conversations",args:a},e=b.call([d])[0];return e.fail(c.exception),e},e=function(a){var d={methodname:"core_message_get_unread_conversations_count",args:a},e=b.call([d])[0];return e.fail(c.exception),e},f=function(a){var d={methodname:"core_message_mark_all_messages_as_read",args:a},e=b.call([d])[0];return e.fail(c.exception),e};return{query:d,countUnreadConversations:e,markAllAsRead:f}});

View 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;
};

View File

@ -1 +1 @@
define(["core/ajax","core/notification","core/log"],function(a,b,c){var d=function(c){"undefined"==typeof c.limit&&(c.limit=20),"undefined"==typeof c.offset&&(c.offset=0);var d={methodname:"message_popup_get_popup_notifications",args:c},e=a.call([d])[0];return e.fail(b.exception),e},e=function(b){var d={methodname:"message_popup_get_unread_popup_notification_count",args:b},e=a.call([d])[0];return e.fail(function(a){c.error("Could not retrieve notifications count: "+a.message)}),e},f=function(c){var d={methodname:"core_message_mark_all_notifications_as_read",args:c},e=a.call([d])[0];return e.fail(b.exception),e},g=function(c,d){var e={messageid:c};d&&(e.timeread=d);var f={methodname:"core_message_mark_message_read",args:e},g=a.call([f])[0];return g.fail(b.exception),g};return{query:d,countUnread:e,markAllAsRead:f,markAsRead:g}});
define(["core/ajax","core/notification"],function(a,b){var c=function(c){"undefined"==typeof c.limit&&(c.limit=20),"undefined"==typeof c.offset&&(c.offset=0);var d={methodname:"message_popup_get_popup_notifications",args:c},e=a.call([d])[0];return e.fail(b.exception),e},d=function(c){var d={methodname:"message_popup_get_unread_popup_notification_count",args:c},e=a.call([d])[0];return e.fail(b.exception),e},e=function(c){var d={methodname:"core_message_mark_all_notifications_as_read",args:c},e=a.call([d])[0];return e.fail(b.exception),e},f=function(c,d){var e={messageid:c};d&&(e.timeread=d);var f={methodname:"core_message_mark_message_read",args:e},g=a.call([f])[0];return g.fail(b.exception),g};return{query:c,countUnread:d,markAllAsRead:e,markAsRead:f}});

View 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;
};

View 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.';

View 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);

View 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');

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 B

BIN
pix/i/mahara_host.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

15
pix/i/mahara_host.svg Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<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"
viewBox="0 0 65.2 124.7" style="enable-background:new 0 0 65.2 124.7;" xml:space="preserve" preserveAspectRatio="xMinYMid meet">
<style type="text/css">
.st0{fill:#566D31;}
</style>
<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
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
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
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
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
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
c0.2,18.1,12.3,31.1,32.1,31.1C52,123.1,63.2,111.3,63.2,92z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View 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;
}
}

View 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;

View File

@ -277,21 +277,6 @@
}
}
#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;

View File

@ -15,13 +15,47 @@
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}}

View File

@ -53,6 +53,15 @@
display: inline;
}
}
p {
&.hasicon {
img {
&.icon {
padding-right: 0;
}
}
}
}
}
.footer {
margin-bottom: 4px;

View 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;
}

View 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);

View File

@ -264,21 +264,6 @@
}
}
#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 {

View 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);

View File

@ -24,27 +24,33 @@
}}
<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}}

View File

@ -15,13 +15,47 @@
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}}

View 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()
)
);
}
}

View 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();

View 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);
}
}

View File

@ -29,11 +29,11 @@
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.

View 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),
)
);
}

View 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();

View 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.