# Enable test external resources
sed -i \
-e "/require_once/i \\define('TEST_EXTERNAL_FILES_HTTP_URL', 'http://127.0.0.1:8080');" \
+ -e "/require_once/i \\define('TEST_EXTERNAL_FILES_HTTPS_URL', 'http://127.0.0.1:8080');" \
config.php ;
+
# Redis cache store tests
sed -i \
-e "/require_once/i \\define('TEST_CACHESTORE_REDIS_TESTSERVERS', '127.0.0.1');" \
if [ "$TASK" = 'PHPUNIT' ];
then
vendor/bin/phpunit --fail-on-risky --disallow-test-output --verbose;
- EXTTESTS_HITS=$(docker logs exttests 2>&1 | grep -Fv -e 'AH00558' -e '[pid 1]' | wc -l)
- echo -e "\nTest local resources number of hits: ${EXTTESTS_HITS}.\n"
fi
- >
exit 1 ;
fi
fi
+
+after_script:
+ - >
+ if [ "$TASK" = 'PHPUNIT' ];
+ then
+ EXTTESTS_HITS=$(docker logs exttests 2>&1 | grep -Fv -e 'AH00558' -e '[pid 1]' | wc -l)
+ echo -e "\nTest local resources number of hits: ${EXTTESTS_HITS}.\n"
+ fi
// Always verify plugin dependencies!
$failed = array();
if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed, $CFG->branch)) {
- echo $output->unsatisfied_dependencies_page($version, $failed, $PAGE->url);
+ echo $output->unsatisfied_dependencies_page($version, $failed, new moodle_url($PAGE->url,
+ array('confirmplugincheck' => 0)));
die();
}
unset($failed);
$failed = array();
if (!$pluginman->all_plugins_ok($version, $failed, $CFG->branch)) {
$output = $PAGE->get_renderer('core', 'admin');
- echo $output->unsatisfied_dependencies_page($version, $failed, $PAGE->url);
+ echo $output->unsatisfied_dependencies_page($version, $failed, new moodle_url($PAGE->url,
+ array('confirmplugincheck' => 0)));
die();
}
unset($failed);
if (!empty($installabortable[$plugin->component])) {
$status .= $this->output->single_button(
- new moodle_url($this->page->url, array('abortinstall' => $plugin->component)),
+ new moodle_url($this->page->url, array('abortinstall' => $plugin->component, 'confirmplugincheck' => 0)),
get_string('cancelinstallone', 'core_plugin'),
'post',
array('class' => 'actionbutton cancelinstallone d-block mt-1')
if ($installabortable) {
$out .= $this->output->single_button(
- new moodle_url($this->page->url, array('abortinstallx' => 1)),
+ new moodle_url($this->page->url, array('abortinstallx' => 1, 'confirmplugincheck' => 0)),
get_string('cancelinstallall', 'core_plugin', count($installabortable)),
'post',
array('class' => 'singlebutton cancelinstallall mr-1')
$showsummary = true;
}
- if ($showsummary) {
+ if ($showsummary && $iscurrentuser) {
$summaryurl = new moodle_url('/admin/tool/dataprivacy/summary.php');
$summarynode = new core_user\output\myprofile\node('privacyandpolicies', 'retentionsummary',
get_string('dataretentionsummary', 'tool_dataprivacy'), null, $summaryurl);
'type' => 'danger', 'message' => get_string('subscriptionfeaturenotapplied', 'tool_mobile')];
}
break;
+ // Check QR automatic login.
+ case 'qrautomaticlogin':
+ if ($ms->qrcodetype == \tool_mobile\api::QR_CODE_LOGIN) {
+ $feature['message'] = [
+ 'type' => 'danger', 'message' => get_string('subscriptionfeaturenotapplied', 'tool_mobile')];
+ }
+ break;
}
}
}
$string['qrcodeformobileappurlabout'] = 'Scan the QR code with your mobile app to fill in the site URL in your app.';
$string['qrsiteadminsnotallowed'] = 'For security reasons login via QR code is not allowed for site administrators or if you are logged in as another user.';
$string['qrcodetype'] = 'QR code access';
-$string['qrcodetype_desc'] = 'A QR code can be provided for mobile app users to scan and either have the site URL filled in or be automatically logged in without having to enter their credentials.';
+$string['qrcodetype_desc'] = 'A QR code can be provided for mobile app users to scan. This can be used to fill in the site URL, or where the site is secured using HTTPS, to automatically log the user in without having to enter their username and password.';
$string['qrcodetypeurl'] = 'QR code with site URL';
$string['qrcodetypelogin'] = 'QR code with automatic login';
$string['readingthisemailgettheapp'] = 'Reading this in an email? <a href="{$a}">Download the mobile app and receive notifications on your mobile device</a>.';
$options = [
tool_mobile\api::QR_CODE_DISABLED => new lang_string('qrcodedisabled', 'tool_mobile'),
tool_mobile\api::QR_CODE_URL => new lang_string('qrcodetypeurl', 'tool_mobile'),
- tool_mobile\api::QR_CODE_LOGIN => new lang_string('qrcodetypelogin', 'tool_mobile'),
];
+ $qrcodetypedefault = tool_mobile\api::QR_CODE_URL;
+
+ if (is_https()) { // Allow QR login for https sites.
+ $options[tool_mobile\api::QR_CODE_LOGIN] = new lang_string('qrcodetypelogin', 'tool_mobile');
+ $qrcodetypedefault = tool_mobile\api::QR_CODE_LOGIN;
+ }
+
$temp->add(new admin_setting_configselect('tool_mobile/qrcodetype',
new lang_string('qrcodetype', 'tool_mobile'),
- new lang_string('qrcodetype_desc', 'tool_mobile'), tool_mobile\api::QR_CODE_LOGIN, $options));
+ new lang_string('qrcodetype_desc', 'tool_mobile'), $qrcodetypedefault, $options));
$temp->add(new admin_setting_configtext('tool_mobile/forcedurlscheme',
new lang_string('forcedurlscheme_key', 'tool_mobile'),
}
}
if (!empty($update)) {
- $authdb->Execute("UPDATE {$this->config->table}
- SET ".implode(',', $update)."
- WHERE {$this->config->fielduser}='".$this->ext_addslashes($extusername)."'");
+ $sql = "UPDATE {$this->config->table}
+ SET ".implode(',', $update)."
+ WHERE {$this->config->fielduser} = ?";
+ if (!$authdb->Execute($sql, array($this->ext_addslashes($extusername)))) {
+ print_error('auth_dbupdateerror', 'auth_db');
+ }
}
$authdb->Close();
return true;
$string['auth_dbcannotreadtable'] = 'Cannot read external table.';
$string['auth_dbtableempty'] = 'External table is empty.';
$string['auth_dbcolumnlist'] = 'External table contains the following columns:<br />{$a}';
+$string['auth_dbupdateerror'] = 'Error updating external database.';
$string['pluginname'] = 'External database';
$string['privacy:metadata'] = 'The External database authentication plugin does not store any personal data.';
$o['ou'] = 'users';
ldap_add($connection, 'ou='.$o['ou'].','.$topdn, $o);
+ $createdusers = array();
for ($i=1; $i<=5; $i++) {
$this->create_ldap_user($connection, $topdn, $i);
+ $createdusers[] = 'username' . $i;
}
// Set up creators group.
+ $assignedroles = array('username1', 'username2');
$o = array();
$o['objectClass'] = array('posixGroup');
$o['cn'] = 'creators';
$o['gidNumber'] = 1;
- $o['memberUid'] = array('username1', 'username2');
+ $o['memberUid'] = $assignedroles;
ldap_add($connection, 'cn='.$o['cn'].','.$topdn, $o);
$creatorrole = $DB->get_record('role', array('shortname'=>'coursecreator'));
// Check events, 5 users created with 2 users having roles.
$this->assertCount(7, $events);
foreach ($events as $index => $event) {
- $usercreatedindex = array(0, 2, 4, 5, 6);
- $roleassignedindex = array (1, 3);
- if (in_array($index, $usercreatedindex)) {
- $this->assertInstanceOf('\core\event\user_created', $event);
- }
- if (in_array($index, $roleassignedindex)) {
- $this->assertInstanceOf('\core\event\role_assigned', $event);
+ $username = $DB->get_field('user', 'username', array('id' => $event->relateduserid)); // Get username.
+
+ if ($event->eventname === '\core\event\user_created') {
+ $this->assertContains($username, $createdusers);
+ unset($events[$index]); // Remove matching event.
+
+ } else if ($event->eventname === '\core\event\role_assigned') {
+ $this->assertContains($username, $assignedroles);
+ unset($events[$index]); // Remove matching event.
+
+ } else {
+ $this->fail('Unexpected event found: ' . $event->eventname);
}
}
+ // If all the user_created and role_assigned events have matched
+ // then the $events array should be now empty.
+ $this->assertCount(0, $events);
$this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
$this->assertEquals(2, $DB->count_records('role_assignments'));
noneNode.appendChild(deleteIcon.span);
// Also if it's not the root, none is actually invalid, so add a label.
- noneNode.appendChild(Y.Node.create('<span class="mt-1 label label-warning">' +
+ noneNode.appendChild(Y.Node.create('<span class="mt-1 badge badge-warning">' +
M.util.get_string('invalid', 'availability') + '</span>'));
}
// Add the invalid marker (empty).
this.node.appendChild(document.createTextNode(' '));
- this.node.appendChild(Y.Node.create('<span class="label label-warning"/>'));
+ this.node.appendChild(Y.Node.create('<span class="badge badge-warning"/>'));
};
/**
errors.push('core_availability:item_unknowntype');
}
// If any errors were added, add the marker to this item.
- var errorLabel = this.node.one('> .label-warning');
+ var errorLabel = this.node.one('> .badge-warning');
if (errors.length !== before && !errorLabel.get('firstChild')) {
errorLabel.appendChild(document.createTextNode(M.util.get_string('invalid', 'availability')));
} else if (errors.length === before && errorLabel.get('firstChild')) {
}
// Identify the backup we're dealing with.
- $backuprelease = floatval($this->get_task()->get_info()->backup_release); // The major version: 2.9, 3.0, ...
+ $backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
$backupbuild = 0;
preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
if (!empty($matches[1])) {
// On older versions the freeze value has to be converted.
// We do this from here as it is happening right before the file is read.
// This only targets the backup files that can contain the legacy freeze.
- if ($backupbuild > 20150618 && ($backuprelease < 3.0 || $backupbuild < 20160527)) {
+ if ($backupbuild > 20150618 && (version_compare($backuprelease, '3.0', '<') || $backupbuild < 20160527)) {
$this->rewrite_step_backup_file_for_legacy_freeze($fullpath);
}
$gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid());
preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
$backupbuild = (int)$matches[1];
- // The function floatval will return a float even if there is text mixed with the release number.
- $backuprelease = floatval($this->get_task()->get_info()->backup_release);
+ $backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
// Extra credits need adjustments only for backups made between 2.8 release (20141110) and the fix release (20150619).
if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150619) {
// Courses from before 3.1 (20160518) may have a letter boundary problem and should be checked for this issue.
// Backups from before and including 2.9 could have a build number that is greater than 20160518 and should
// be checked for this problem.
- if (!$gradebookcalculationsfreeze && ($backupbuild < 20160518 || $backuprelease <= 2.9)) {
+ if (!$gradebookcalculationsfreeze && ($backupbuild < 20160518 || version_compare($backuprelease, '2.9', '<='))) {
require_once($CFG->libdir . '/db/upgradelib.php');
upgrade_course_letter_boundary($this->get_courseid());
}
// Before 3.5, question categories could be created at top level.
// From 3.5 onwards, all question categories should be a child of a special category called the "top" category.
- $backuprelease = floatval($this->get_task()->get_info()->backup_release);
+ $backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
$backupbuild = (int)$matches[1];
$before35 = false;
- if ($backuprelease < 3.5 || $backupbuild < 20180205) {
+ if (version_compare($backuprelease, '3.5', '<') || $backupbuild < 20180205) {
$before35 = true;
}
if (empty($mapping->info->parent) && $before35) {
protected function define_execution() {
global $DB;
- $backuprelease = floatval($this->task->get_info()->backup_release);
+ $backuprelease = $this->task->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
preg_match('/(\d{8})/', $this->task->get_info()->moodle_release, $matches);
$backupbuild = (int)$matches[1];
$after35 = false;
- if ($backuprelease >= 3.5 && $backupbuild > 20180205) {
+ if (version_compare($backuprelease, '3.5', '>=') && $backupbuild > 20180205) {
$after35 = true;
}
$rc = restore_controller_dbops::load_controller($restoreid);
$restoreinfo = $rc->get_info();
$rc->destroy(); // Always need to destroy.
- $backuprelease = floatval($restoreinfo->backup_release);
+ $backuprelease = $restoreinfo->backup_release; // The major version: 2.9, 3.0, 3.10...
preg_match('/(\d{8})/', $restoreinfo->moodle_release, $matches);
$backupbuild = (int)$matches[1];
$after35 = false;
- if ($backuprelease >= 3.5 && $backupbuild > 20180205) {
+ if (version_compare($backuprelease, '3.5', '>=') && $backupbuild > 20180205) {
$after35 = true;
}
*/
function getAllCopyProgress() {
var copyids = [];
- var progressbars = $('.progress').find('.progress-bar').not('.complete');
+ var progressbars = $('.progress').find('.progress-bar[data-operation][data-backupid][data-restoreid]').not('.complete');
progressbars.each(function() {
let progressvars = {
$mform->addRule('description', null, 'required');
$str = $action == 'new' ? get_string('badgeimage', 'badges') : get_string('newimage', 'badges');
- $imageoptions = array('maxbytes' => 262144, 'accepted_types' => array('web_image'));
+ $imageoptions = array('maxbytes' => 262144, 'accepted_types' => array('optimised_image'));
$mform->addElement('filepicker', 'image', $str, null, $imageoptions);
if ($action == 'new') {
foreach ($this->backpacks as $backpack) {
$exporter = new backpack_exporter($backpack);
$backpack = $exporter->export($output);
- if ($backpack->apiversion == OPEN_BADGES_V2 || $backpack->apiversion == OPEN_BADGES_V2P1) {
- $backpack->canedit = true;
- } else {
- $backpack->canedit = false;
- }
$backpack->cantest = ($backpack->apiversion == OPEN_BADGES_V2);
$backpack->iscurrent = ($backpack->id == $CFG->badges_site_backpack);
Example context (json):
{
"backpacks": [
- {"backpackweburl": "http://localhost/", "sitebackpack": true, "canedit": false, "cantest": true}
+ {"backpackweburl": "http://localhost/", "sitebackpack": true, "cantest": true}
]
}
}}
<td> {{{backpackweburl}}} </td>
<td> {{#sitebackpack}}Yes{{/sitebackpack}} </td>
<td>
- {{#canedit}}
<a href="{{baseurl}}?id={{id}}&action=edit">{{#pix}}t/edit, core,{{#str}}editsettings{{/str}}{{/pix}}</a>
- {{/canedit}}
{{^iscurrent}}
<a href="{{baseurl}}?id={{id}}&action=delete" role="button" data-action="deletebackpack">
{{#pix}}t/delete, core,{{#str}}delete{{/str}}{{/pix}}
And I set the field "backpackweburl" to "http://backpackweburl.cat"
And I press "Save changes"
Then I should see "http://backpackweburl.cat"
- And "Delete" "button" should exist
+ And "Delete" "icon" should exist in the "http://backpackweburl.cat" "table_row"
+ And "Edit settings" "icon" should exist in the "http://backpackweburl.cat" "table_row"
@javascript
Scenario: Remove a site backpack
* @return array Additional properties with values
*/
protected function get_other_values(renderer_base $output) {
- global $OUTPUT;
+ global $CFG;
+ require_once($CFG->libdir.'/modinfolib.php');
return array(
- 'viewurl' => (new moodle_url('/mod/'.$this->data->modname.'/view.php',
- array('id' => $this->data->cmid)))->out(false),
- 'courseviewurl' => (new moodle_url('/course/view.php', array('id' => $this->data->courseid)))->out(false),
- 'icon' => $OUTPUT->image_icon('icon', get_string('pluginname', $this->data->modname), $this->data->modname)
+ 'viewurl' => (new moodle_url('/mod/'.$this->data->modname.'/view.php',
+ array('id' => $this->data->cmid)))->out(false),
+ 'courseviewurl' => (new moodle_url('/course/view.php', array('id' => $this->data->courseid)))->out(false),
+ 'icon' => \html_writer::img(
+ get_fast_modinfo($this->data->courseid)->cms[$this->data->cmid]->get_icon_url(),
+ get_string('pluginname', $this->data->modname),
+ ['title' => get_string('pluginname', $this->data->modname), 'class' => 'icon']
+ )
);
}
)
);
}
-}
\ No newline at end of file
+}
'type' => PARAM_RAW,
'default' => '',
],
+ 'daytitle' => [
+ 'type' => PARAM_RAW,
+ ]
]);
return $return;
$return['popovertitle'] = $popovertitle;
}
+ $return['daytitle'] = $this->get_day_title();
+
return $return;
}
return $title;
}
+
+ /**
+ * Get the title for this day.
+ *
+ * @return string
+ */
+ protected function get_day_title(): string {
+ $userdate = userdate($this->data[0], get_string('strftimedayshort'));
+
+ $numevents = count($this->related['events']);
+ if ($numevents == 1) {
+ $title = get_string('dayeventsone', 'calendar', $userdate);
+ } else if ($numevents) {
+ $title = get_string('dayeventsmany', 'calendar', ['num' => $numevents, 'day' => $userdate]);
+ } else {
+ $title = get_string('dayeventsnone', 'calendar', $userdate);
+ }
+
+ return $title;
+ }
}
<thead>
<tr>
{{# daynames }}
- <th class="header text-xs-center" aria-label="{{fullname}}">
- {{shortname}}
+ <th class="header text-xs-center">
+ <span class="sr-only">{{fullname}}</span>
+ <span aria-hidden="true">{{shortname}}</span>
</th>
{{/ daynames }}
</tr>
data-region="day"
data-new-event-timestamp="{{neweventtimestamp}}">
<div class="d-none d-md-block hidden-phone text-xs-center">
+ <span class="sr-only">{{daytitle}}</span>
{{#hasevents}}
<a data-action="view-day-link" href="#" class="aalink day" aria-label="{{viewdaylinktitle}}"
data-year="{{date.year}}" data-month="{{date.mon}}" data-day="{{mday}}"
data-timestamp="{{timestamp}}">{{mday}}</a>
{{/hasevents}}
{{^hasevents}}
- {{mday}}
+ <span aria-hidden="true">{{mday}}</span>
{{/hasevents}}
{{#hasevents}}
<div data-region="day-content">
{{/hasevents}}
</div>
<div class="d-md-none hidden-desktop hidden-tablet">
+ <span class="sr-only">{{daytitle}}</span>
{{#hasevents}}
<a data-action="view-day-link" href="#" class="day aalink" aria-label="{{viewdaylinktitle}}"
data-year="{{date.year}}" data-month="{{date.mon}}" data-day="{{mday}}"
data-timestamp="{{timestamp}}">{{mday}}</a>
{{/hasevents}}
{{^hasevents}}
- {{mday}}
+ <span aria-hidden="true">{{mday}}</span>
{{/hasevents}}
</div>
</td>
<thead>
<tr>
{{# daynames }}
- <th class="header text-xs-center" scope="col" aria-label="{{fullname}}">
- {{shortname}}
+ <th class="header text-xs-center">
+ <span class="sr-only">{{fullname}}</span>
+ <span aria-hidden="true">{{shortname}}</span>
</th>
{{/ daynames }}
</tr>
This is the timestamp for this month.
}} data-day-timestamp="{{timestamp}}"{{!
}}>{{!
- }}{{#popovertitle}}
+ }}<span class="sr-only">{{daytitle}}</span>
+ {{#popovertitle}}
{{< core_calendar/minicalendar_day_link }}
{{$day}}{{mday}}{{/day}}
{{$url}}{{viewdaylink}}{{/url}}
{{/ core_calendar/minicalendar_day_link }}
{{/popovertitle}}{{!
}}{{^popovertitle}}
- {{mday}}
+ <span aria-hidden="true">{{mday}}</span>
{{/popovertitle}}{{!
}}</td>
{{/days}}
});
}).then(function(modal) {
modal.setSaveButtonText(saveButtonText);
- modal.getRoot().on(ModalEvents.save, function() {
+ modal.getRoot().on(ModalEvents.save, function(e) {
// The action is now confirmed, sending an action for it.
- var newname = $("#newname").val();
- return renameContent(contentid, newname);
+ var newname = $("#newname").val().trim();
+ if (newname) {
+ renameContent(contentid, newname);
+ } else {
+ var errorStrings = [
+ {
+ key: 'error',
+ },
+ {
+ key: 'emptynamenotallowed',
+ component: 'core_contentbank',
+ },
+ ];
+ Str.get_strings(errorStrings).then(function(langStrings) {
+ Notification.alert(langStrings[0], langStrings[1]);
+ }).catch(Notification.exception);
+ e.preventDefault();
+ }
});
// Handle hidden event.
};
var requestType = 'success';
Ajax.call([request])[0].then(function(data) {
- if (data) {
+ if (data.result) {
return 'contentrenamed';
}
requestType = 'error';
- return 'contentnotrenamed';
+ return data.warnings[0].message;
}).then(function(message) {
var params = null;
* @throws \coding_exception if not loaded.
*/
public function set_name(string $name): bool {
+ $name = trim($name);
if (empty($name)) {
return false;
}
return $this->content->configdata;
}
+ /**
+ * Import a file as a valid content.
+ *
+ * By default, all content has a public file area to interact with the content bank
+ * repository. This method should be overridden by contentypes which does not simply
+ * upload to the public file area.
+ *
+ * If any, the method will return the final stored_file. This way it can be invoked
+ * as parent::import_file in case any plugin want to store the file in the public area
+ * and also parse it.
+ *
+ * @throws file_exception If file operations fail
+ * @param stored_file $file File to store in the content file area.
+ * @return stored_file|null the stored content file or null if the file is discarted.
+ */
+ public function import_file(stored_file $file): ?stored_file {
+ $originalfile = $this->get_file();
+ if ($originalfile) {
+ $originalfile->replace_file_with($file);
+ return $originalfile;
+ } else {
+ $itemid = $this->get_id();
+ $fs = get_file_storage();
+ $filerecord = [
+ 'contextid' => $this->get_contextid(),
+ 'component' => 'contentbank',
+ 'filearea' => 'public',
+ 'itemid' => $this->get_id(),
+ 'filepath' => '/',
+ 'filename' => $file->get_filename(),
+ 'timecreated' => time(),
+ ];
+ return $fs->create_file_from_storedfile($filerecord, $file);
+ }
+ }
+
/**
* Returns the $file related to this content.
*
/**
* Create content from a file information.
*
+ * @throws file_exception If file operations fail
+ * @throws dml_exception if the content creation fails
* @param \context $context Context where to upload the file and content.
* @param int $userid Id of the user uploading the file.
* @param stored_file $file The file to get information from
$record->name = $filename;
$record->usercreated = $userid;
$contentype = new $classname($context);
- $content = $contentype->create_content($record);
+ $content = $contentype->upload_content($file, $record);
$event = \core\event\contentbank_content_uploaded::create_from_record($content->get_content());
$event->trigger();
return $content;
use core\event\contentbank_content_created;
use core\event\contentbank_content_deleted;
use core\event\contentbank_content_viewed;
+use stored_file;
+use Exception;
use moodle_url;
/**
/**
* Fills content_bank table with appropiate information.
*
+ * @throws dml_exception A DML specific exception is thrown for any creation error.
* @param \stdClass $record An optional content record compatible object (default null)
* @return content Object with content bank information.
*/
- public function create_content(\stdClass $record = null): ?content {
+ public function create_content(\stdClass $record = null): content {
global $USER, $DB;
$entry = new \stdClass();
$entry->configdata = $record->configdata ?? '';
$entry->instanceid = $record->instanceid ?? 0;
$entry->id = $DB->insert_record('contentbank_content', $entry);
- if ($entry->id) {
- $classname = '\\'.$entry->contenttype.'\\content';
- $content = new $classname($entry);
- // Trigger an event for creating the content.
- $event = contentbank_content_created::create_from_record($content->get_content());
- $event->trigger();
- return $content;
+ $classname = '\\'.$entry->contenttype.'\\content';
+ $content = new $classname($entry);
+ // Trigger an event for creating the content.
+ $event = contentbank_content_created::create_from_record($content->get_content());
+ $event->trigger();
+ return $content;
+ }
+
+ /**
+ * Create a new content from an uploaded file.
+ *
+ * @throws file_exception If file operations fail
+ * @throws dml_exception if the content creation fails
+ * @param stored_file $file the uploaded file
+ * @param \stdClass|null $record an optional content record
+ * @return content Object with content bank information.
+ */
+ public function upload_content(stored_file $file, \stdClass $record = null): content {
+ if (empty($record)) {
+ $record = new \stdClass();
+ $record->name = $file->get_filename();
}
- return null;
+ $content = $this->create_content($record);
+ try {
+ $content->import_file($file);
+ } catch (Exception $e) {
+ $this->delete_content($content);
+ throw $e;
+ }
+
+ return $content;
}
/**
$content = new $contentclass($record);
// Check capability.
if ($contenttype->can_manage($content)) {
- // This content can be renamed.
- if ($contenttype->rename_content($content, $params['name'])) {
- $result = true;
- } else {
+ if (empty(trim($name))) {
+ // If name is empty don't try to rename and return a more detailed message.
$warnings[] = [
'item' => $contentid,
- 'warningcode' => 'contentnotrenamed',
- 'message' => get_string('contentnotrenamed', 'core_contentbank')
+ 'warningcode' => 'emptynamenotallowed',
+ 'message' => get_string('emptynamenotallowed', 'core_contentbank')
];
+ } else {
+ // This content can be renamed.
+ if ($contenttype->rename_content($content, $params['name'])) {
+ $result = true;
+ } else {
+ $warnings[] = [
+ 'item' => $contentid,
+ 'warningcode' => 'contentnotrenamed',
+ 'message' => get_string('contentnotrenamed', 'core_contentbank')
+ ];
+ }
}
} else {
// The user has no permission to manage this content.
'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle'],
'Name with tags' => ['This is <b>bold</b>', 'This is bold'],
'Long name' => [str_repeat('a', 100), str_repeat('a', 100)],
- 'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)]
+ 'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)],
+ 'Empty name' => ['', 'Old name'],
+ 'Blanks only' => [' ', 'Old name'],
];
}
$this->assertEquals($newcontext->id, $content->get_contextid());
$this->assertEquals($newcontext->id, $file->get_contextid());
}
+
+ /**
+ * Tests for 'import_file' behaviour when replacing a file.
+ *
+ * @covers ::import_file
+ */
+ public function test_import_file_replace(): void {
+ global $USER;
+ $this->resetAfterTest();
+ $this->setAdminUser();
+ $context = context_system::instance();
+
+ // Add some content to the content bank.
+ $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+ $contents = $generator->generate_contentbank_data('contenttype_testable', 3, 0, $context);
+ $content = reset($contents);
+
+ $originalfile = $content->get_file();
+
+ // Create a dummy file.
+ $filerecord = array(
+ 'contextid' => $context->id,
+ 'component' => 'contentbank',
+ 'filearea' => 'draft',
+ 'itemid' => $content->get_id(),
+ 'filepath' => '/',
+ 'filename' => 'example.txt'
+ );
+ $fs = get_file_storage();
+ $file = $fs->create_file_from_string($filerecord, 'Dummy content ');
+
+ $importedfile = $content->import_file($file);
+
+ $this->assertEquals($originalfile->get_filename(), $importedfile->get_filename());
+ $this->assertEquals($originalfile->get_filearea(), $importedfile->get_filearea());
+ $this->assertEquals($originalfile->get_filepath(), $importedfile->get_filepath());
+ $this->assertEquals($originalfile->get_mimetype(), $importedfile->get_mimetype());
+
+ $this->assertEquals($file->get_userid(), $importedfile->get_userid());
+ $this->assertEquals($file->get_contenthash(), $importedfile->get_contenthash());
+ }
+
+ /**
+ * Tests for 'import_file' behaviour when uploading a new file.
+ *
+ * @covers ::import_file
+ */
+ public function test_import_file_upload(): void {
+ global $USER;
+ $this->resetAfterTest();
+ $this->setAdminUser();
+ $context = context_system::instance();
+
+ $type = new contenttype($context);
+ $record = (object)[
+ 'name' => 'content name',
+ 'usercreated' => $USER->id,
+ ];
+ $content = $type->create_content($record);
+
+ // Create a dummy file.
+ $filerecord = array(
+ 'contextid' => $context->id,
+ 'component' => 'contentbank',
+ 'filearea' => 'draft',
+ 'itemid' => $content->get_id(),
+ 'filepath' => '/',
+ 'filename' => 'example.txt'
+ );
+ $fs = get_file_storage();
+ $file = $fs->create_file_from_string($filerecord, 'Dummy content ');
+
+ $importedfile = $content->import_file($file);
+
+ $this->assertEquals($file->get_filename(), $importedfile->get_filename());
+ $this->assertEquals($file->get_userid(), $importedfile->get_userid());
+ $this->assertEquals($file->get_mimetype(), $importedfile->get_mimetype());
+ $this->assertEquals($file->get_contenthash(), $importedfile->get_contenthash());
+ $this->assertEquals('public', $importedfile->get_filearea());
+ $this->assertEquals('/', $importedfile->get_filepath());
+
+ $contentfile = $content->get_file($file);
+ $this->assertEquals($importedfile->get_id(), $contentfile->get_id());
+ }
}
$this->resetAfterTest();
$cb = new contentbank();
- $expectedsupporters = [$extension => $expected];
$systemcontext = context_system::instance();
// All contexts allowed for admins.
$this->setAdminUser();
$contextsupporters = $cb->load_context_supported_extensions($systemcontext);
- $this->assertEquals($expectedsupporters, $contextsupporters);
+ $this->assertArrayHasKey($extension, $contextsupporters);
+ $this->assertEquals($expected, $contextsupporters[$extension]);
}
/**
$this->resetAfterTest();
$cb = new contentbank();
- $expectedsupporters = [$extension => $expected];
$course = $this->getDataGenerator()->create_course();
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
// Teachers has permission in their context to upload supported by H5P content type.
$contextsupporters = $cb->load_context_supported_extensions($coursecontext);
- $this->assertEquals($expectedsupporters, $contextsupporters);
+ $this->assertArrayHasKey($extension, $contextsupporters);
+ $this->assertEquals($expected, $contextsupporters[$extension]);
}
/**
use stdClass;
use context_system;
+use context_user;
+use Exception;
use contenttype_testable\contenttype as contenttype;
/**
* Test for content bank contenttype class.
$this->assertInstanceOf('\\contenttype_testable\\content', $content);
}
+ /**
+ * Tests for behaviour of upload_content() with a file and a record.
+ *
+ * @dataProvider upload_content_provider
+ * @param bool $userecord if a predefined record has to be used.
+ *
+ * @covers ::upload_content
+ */
+ public function test_upload_content(bool $userecord): void {
+ global $USER;
+
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ $dummy = [
+ 'contextid' => context_user::instance($USER->id)->id,
+ 'component' => 'user',
+ 'filearea' => 'draft',
+ 'itemid' => 1,
+ 'filepath' => '/',
+ 'filename' => 'file.h5p',
+ 'userid' => $USER->id,
+ ];
+ $fs = get_file_storage();
+ $dummyfile = $fs->create_file_from_string($dummy, 'Dummy content');
+
+ // Create content.
+ if ($userecord) {
+ $record = new stdClass();
+ $record->name = 'Test content';
+ $record->configdata = '';
+ $record->contenttype = '';
+ $checkname = $record->name;
+ } else {
+ $record = null;
+ $checkname = $dummyfile->get_filename();
+ }
+
+ $contenttype = new contenttype(context_system::instance());
+ $content = $contenttype->upload_content($dummyfile, $record);
+
+ $this->assertEquals('contenttype_testable', $content->get_content_type());
+ $this->assertEquals($checkname, $content->get_name());
+ $this->assertInstanceOf('\\contenttype_testable\\content', $content);
+
+ $file = $content->get_file();
+ $this->assertEquals($dummyfile->get_filename(), $file->get_filename());
+ $this->assertEquals($dummyfile->get_userid(), $file->get_userid());
+ $this->assertEquals($dummyfile->get_mimetype(), $file->get_mimetype());
+ $this->assertEquals($dummyfile->get_contenthash(), $file->get_contenthash());
+ $this->assertEquals('contentbank', $file->get_component());
+ $this->assertEquals('public', $file->get_filearea());
+ $this->assertEquals('/', $file->get_filepath());
+ }
+
+ /**
+ * Data provider for test_rename_content.
+ *
+ * @return array
+ */
+ public function upload_content_provider() {
+ return [
+ 'With record' => [true],
+ 'Without record' => [false],
+ ];
+ }
+
+ /**
+ * Tests for behaviour of upload_content() with a file wrong file.
+ *
+ * @covers ::upload_content
+ */
+ public function test_upload_content_exception(): void {
+ global $USER, $DB;
+
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ // The testing contenttype thows exception if filename is "error.*".
+ $dummy = [
+ 'contextid' => context_user::instance($USER->id)->id,
+ 'component' => 'user',
+ 'filearea' => 'draft',
+ 'itemid' => 1,
+ 'filepath' => '/',
+ 'filename' => 'error.txt',
+ 'userid' => $USER->id,
+ ];
+ $fs = get_file_storage();
+ $dummyfile = $fs->create_file_from_string($dummy, 'Dummy content');
+
+ $contenttype = new contenttype(context_system::instance());
+ $cbcontents = $DB->count_records('contentbank_content');
+
+ // We need to capture the exception to check no content is created.
+ try {
+ $content = $contenttype->upload_content($dummyfile);
+ $this->assertTrue(false);
+ } catch (Exception $e) {
+ $this->assertTrue(true);
+ }
+ $this->assertEquals($cbcontents, $DB->count_records('contentbank_content'));
+ $this->assertEquals(1, $DB->count_records('files', ['contenthash' => $dummyfile->get_contenthash()]));
+ }
+
/**
* Test the behaviour of can_delete().
*/
*/
public function rename_content_provider() {
return [
- 'Standard name' => ['New name', 'New name'],
- 'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017'],
- 'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle'],
- 'Name with tags' => ['This is <b>bold</b>', 'This is bold'],
- 'Long name' => [str_repeat('a', 100), str_repeat('a', 100)],
- 'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)]
+ 'Standard name' => ['New name', 'New name', true],
+ 'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017', true],
+ 'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle', true],
+ 'Name with tags' => ['This is <b>bold</b>', 'This is bold', true],
+ 'Long name' => [str_repeat('a', 100), str_repeat('a', 100), true],
+ 'Too long name' => [str_repeat('a', 300), str_repeat('a', 255), true],
+ 'Empty name' => ['', 'Test content ', false],
+ 'Blanks only' => [' ', 'Test content ', false],
];
}
* @dataProvider rename_content_provider
* @param string $newname The name to set
* @param string $expected The name result
+ * @param bool $result The bolean result expected when renaming
*
* @covers ::rename_content
*/
- public function test_rename_content(string $newname, string $expected) {
+ public function test_rename_content(string $newname, string $expected, bool $result) {
global $DB;
$this->resetAfterTest();
// Check the content is renamed as expected by a user with permission.
$renamed = $contenttype->rename_content($content, $newname);
- $this->assertTrue($renamed);
+ $this->assertEquals($result, $renamed);
$record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]);
- $this->assertNotEquals($oldname, $record->name);
$this->assertEquals($expected, $record->name);
}
*/
public function rename_content_provider() {
return [
- 'Standard name' => ['New name', 'New name'],
- 'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017'],
- 'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle'],
- 'Name with tags' => ['This is <b>bold</b>', 'This is bold'],
- 'Long name' => [str_repeat('a', 100), str_repeat('a', 100)],
- 'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)]
+ 'Standard name' => ['New name', 'New name', true],
+ 'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017', true],
+ 'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle', true],
+ 'Name with tags' => ['This is <b>bold</b>', 'This is bold', true],
+ 'Long name' => [str_repeat('a', 100), str_repeat('a', 100), true],
+ 'Too long name' => [str_repeat('a', 300), str_repeat('a', 255), true],
+ 'Empty name' => ['', 'Test content ', false],
+ 'Blanks only' => [' ', 'Test content ', false],
];
}
*
* @dataProvider rename_content_provider
* @param string $newname The name to set
- * @param string $expected The name result
+ * @param string $expectedname The name result
+ * @param bool $expectedresult The bolean result expected when renaming
*
* @covers ::execute
*/
- public function test_rename_content_with_permission(string $newname, string $expected) {
+ public function test_rename_content_with_permission(string $newname, string $expectedname, bool $expectedresult) {
global $DB;
$this->resetAfterTest();
// Call the WS and check the content is renamed as expected.
$result = rename_content::execute($content->get_id(), $newname);
$result = external_api::clean_returnvalue(rename_content::execute_returns(), $result);
- $this->assertTrue($result['result']);
+ $this->assertEquals($expectedresult, $result['result']);
$record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]);
- $this->assertNotEquals($oldname, $record->name);
- $this->assertEquals($expected, $record->name);
+ $this->assertEquals($expectedname, $record->name);
// Call the WS using an unexisting contentid and check an error is thrown.
$this->expectException(\invalid_response_exception::class);
namespace contenttype_testable;
+use file_exception;
+use stored_file;
+
/**
* Testable content plugin class.
*
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class content extends \core_contentbank\content {
+
+ /**
+ * Import a file as a valid content.
+ *
+ * This method will thow an error if the filename is "error.*"
+ *
+ * @param stored_file $file File to store in the content file area.
+ * @return stored_file|null the stored content file or null if the file is discarted.
+ * @throws file_exception if the filename contains the word "error"
+ */
+ public function import_file(stored_file $file): ?stored_file {
+ $filename = $file->get_filename();
+ if (strrpos($filename, 'error') !== false) {
+ throw new file_exception('yourerrorthanks', 'contenttype_test');
+ }
+ return parent::import_file($file);
+ }
}
require('../config.php');
require_once("$CFG->dirroot/contentbank/files_form.php");
+use core\output\notification;
+
require_login();
$contextid = optional_param('contextid', \context_system::instance()->id, PARAM_INT);
$mform = new contentbank_files_form(null, ['contextid' => $contextid, 'data' => $data, 'options' => $options]);
+$error = '';
+
if ($mform->is_cancelled()) {
redirect($returnurl);
} else if ($formdata = $mform->get_data()) {
if (!empty($files)) {
$file = reset($files);
$content = $cb->create_content_from_file($context, $USER->id, $file);
- file_save_draft_area_files($formdata->file, $contextid, 'contentbank', 'public', $content->get_id());
$viewurl = new \moodle_url('/contentbank/view.php', ['id' => $content->get_id(), 'contextid' => $contextid]);
redirect($viewurl);
+ } else {
+ $error = get_string('errornofile', 'contentbank');
}
- redirect($returnurl);
}
echo $OUTPUT->header();
echo $OUTPUT->box_start('generalbox');
+if (!empty($error)) {
+ echo $OUTPUT->notification($error, notification::NOTIFY_ERROR);
+}
+
$mform->display();
echo $OUTPUT->box_end();
}
require_capability('moodle/course:create', $context);
+ // Fullname and short name are required to be non-empty.
+ if (trim($course['fullname']) === '') {
+ throw new moodle_exception('errorinvalidparam', 'webservice', '', 'fullname');
+ } else if (trim($course['shortname']) === '') {
+ throw new moodle_exception('errorinvalidparam', 'webservice', '', 'shortname');
+ }
+
// Make sure lang is valid
if (array_key_exists('lang', $course)) {
if (empty($availablelangs[$course['lang']])) {
$course['category'] = $course['categoryid'];
}
- // Check if the user can change fullname.
+ // Check if the user can change fullname, and the new value is non-empty.
if (array_key_exists('fullname', $course) && ($oldcourse->fullname != $course['fullname'])) {
require_capability('moodle/course:changefullname', $context);
+ if (trim($course['fullname']) === '') {
+ throw new moodle_exception('errorinvalidparam', 'webservice', '', 'fullname');
+ }
}
- // Check if the user can change shortname.
+ // Check if the user can change shortname, and the new value is non-empty.
if (array_key_exists('shortname', $course) && ($oldcourse->shortname != $course['shortname'])) {
require_capability('moodle/course:changeshortname', $context);
+ if (trim($course['shortname']) === '') {
+ throw new moodle_exception('errorinvalidparam', 'webservice', '', 'shortname');
+ }
}
// Check if the user can change the idnumber.
*/
protected function need_restore_numsections() {
$backupinfo = $this->step->get_task()->get_info();
- $backuprelease = $backupinfo->backup_release;
- return version_compare($backuprelease, '3.3', 'lt');
+ $backuprelease = $backupinfo->backup_release; // The major version: 2.9, 3.0, 3.10...
+ return version_compare($backuprelease, '3.3', '<');
}
/**
*/
protected function is_pre_33_backup() {
$backupinfo = $this->step->get_task()->get_info();
- $backuprelease = $backupinfo->backup_release;
- return version_compare($backuprelease, '3.3', 'lt');
+ $backuprelease = $backupinfo->backup_release; // The major version: 2.9, 3.0, 3.10...
+ return version_compare($backuprelease, '3.3', '<');
}
/**
if ($mform->is_cancelled()) {
if ($return && !empty($cm->id)) {
- redirect("$CFG->wwwroot/mod/$module->name/view.php?id=$cm->id");
+ $urlparams = [
+ 'id' => $cm->id, // We always need the activity id.
+ 'forceview' => 1, // Stop file downloads in resources.
+ ];
+ $activityurl = new moodle_url("/mod/$module->name/view.php", $urlparams);
+ redirect($activityurl);
} else {
redirect(course_get_url($course, $cw->section, array('sr' => $sectionreturn)));
}
$createdsubcats = core_course_external::create_courses($courses);
}
+ /**
+ * Data provider for testing empty fields produce expected exceptions
+ *
+ * @see test_create_courses_empty_field
+ * @see test_update_courses_empty_field
+ *
+ * @return array
+ */
+ public function course_empty_field_provider(): array {
+ return [
+ [[
+ 'fullname' => '',
+ 'shortname' => 'ws101',
+ ], 'fullname'],
+ [[
+ 'fullname' => ' ',
+ 'shortname' => 'ws101',
+ ], 'fullname'],
+ [[
+ 'fullname' => 'Web Services',
+ 'shortname' => '',
+ ], 'shortname'],
+ [[
+ 'fullname' => 'Web Services',
+ 'shortname' => ' ',
+ ], 'shortname'],
+ ];
+ }
+
+ /**
+ * Test creating courses with empty fields throws an exception
+ *
+ * @param array $course
+ * @param string $expectedemptyfield
+ *
+ * @dataProvider course_empty_field_provider
+ */
+ public function test_create_courses_empty_field(array $course, string $expectedemptyfield): void {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ // Create a category for the new course.
+ $course['categoryid'] = $this->getDataGenerator()->create_category()->id;
+
+ $this->expectException(moodle_exception::class);
+ $this->expectExceptionMessageRegExp("/{$expectedemptyfield}/");
+ core_course_external::create_courses([$course]);
+ }
+
+ /**
+ * Test updating courses with empty fields returns warnings
+ *
+ * @param array $course
+ * @param string $expectedemptyfield
+ *
+ * @dataProvider course_empty_field_provider
+ */
+ public function test_update_courses_empty_field(array $course, string $expectedemptyfield): void {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ // Create a course to update.
+ $course['id'] = $this->getDataGenerator()->create_course()->id;
+
+ $result = core_course_external::update_courses([$course]);
+ $result = core_course_external::clean_returnvalue(core_course_external::update_courses_returns(), $result);
+
+ $this->assertCount(1, $result['warnings']);
+
+ $warning = reset($result['warnings']);
+ $this->assertEquals('errorinvalidparam', $warning['warningcode']);
+ $this->assertContains($expectedemptyfield, $warning['message']);
+ }
+
/**
* Test delete_courses
*/
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" preserveAspectRatio="xMinYMid meet">
+<g id="surface1">
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(60%,60%,60%);fill-opacity:1;" d="M 13.144531 8.304688 L 13.144531 11.144531 C 13.144531 11.851562 12.890625 12.457031 12.386719 12.960938 C 11.886719 13.460938 11.28125 13.714844 10.570312 13.714844 L 3.144531 13.714844 C 2.433594 13.714844 1.828125 13.460938 1.324219 12.960938 C 0.824219 12.457031 0.570312 11.851562 0.570312 11.144531 L 0.570312 3.714844 C 0.570312 3.007812 0.824219 2.398438 1.324219 1.898438 C 1.828125 1.394531 2.433594 1.144531 3.144531 1.144531 L 10.570312 1.144531 C 10.945312 1.144531 11.292969 1.21875 11.617188 1.367188 C 11.707031 1.40625 11.757812 1.476562 11.777344 1.570312 C 11.792969 1.671875 11.769531 1.757812 11.695312 1.832031 L 11.257812 2.269531 C 11.199219 2.328125 11.132812 2.355469 11.054688 2.355469 C 11.035156 2.355469 11.007812 2.351562 10.972656 2.339844 C 10.835938 2.304688 10.703125 2.285156 10.570312 2.285156 L 3.144531 2.285156 C 2.75 2.285156 2.414062 2.425781 2.132812 2.707031 C 1.855469 2.984375 1.714844 3.320312 1.714844 3.714844 L 1.714844 11.144531 C 1.714844 11.535156 1.855469 11.871094 2.132812 12.152344 C 2.414062 12.429688 2.75 12.570312 3.144531 12.570312 L 10.570312 12.570312 C 10.964844 12.570312 11.300781 12.429688 11.582031 12.152344 C 11.859375 11.871094 12 11.535156 12 11.144531 L 12 8.875 C 12 8.796875 12.027344 8.730469 12.082031 8.679688 L 12.652344 8.105469 C 12.710938 8.046875 12.78125 8.019531 12.855469 8.019531 C 12.894531 8.019531 12.929688 8.027344 12.964844 8.042969 C 13.082031 8.09375 13.144531 8.179688 13.144531 8.304688 Z M 15.207031 3.9375 L 7.9375 11.207031 C 7.792969 11.347656 7.625 11.417969 7.429688 11.417969 C 7.230469 11.417969 7.0625 11.347656 6.917969 11.207031 L 3.082031 7.367188 C 2.9375 7.222656 2.867188 7.054688 2.867188 6.855469 C 2.867188 6.660156 2.9375 6.492188 3.082031 6.347656 L 4.0625 5.367188 C 4.207031 5.222656 4.375 5.152344 4.570312 5.152344 C 4.769531 5.152344 4.9375 5.222656 5.082031 5.367188 L 7.429688 7.714844 L 13.207031 1.9375 C 13.347656 1.792969 13.519531 1.722656 13.714844 1.722656 C 13.910156 1.722656 14.082031 1.792969 14.222656 1.9375 L 15.207031 2.917969 C 15.347656 3.0625 15.417969 3.230469 15.417969 3.429688 C 15.417969 3.625 15.347656 3.792969 15.207031 3.9375 Z M 15.207031 3.9375 "/>
+</g>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" preserveAspectRatio="xMinYMid meet">
+<g id="surface1">
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(60%,60%,60%);fill-opacity:1;" d="M 11.714844 2.285156 L 4.285156 2.285156 C 3.894531 2.285156 3.554688 2.425781 3.277344 2.707031 C 2.996094 2.984375 2.855469 3.320312 2.855469 3.714844 L 2.855469 11.144531 C 2.855469 11.535156 2.996094 11.871094 3.277344 12.152344 C 3.554688 12.429688 3.894531 12.570312 4.285156 12.570312 L 11.714844 12.570312 C 12.105469 12.570312 12.445312 12.429688 12.722656 12.152344 C 13.003906 11.871094 13.144531 11.535156 13.144531 11.144531 L 13.144531 3.714844 C 13.144531 3.320312 13.003906 2.984375 12.722656 2.707031 C 12.445312 2.425781 12.105469 2.285156 11.714844 2.285156 Z M 14.285156 3.714844 L 14.285156 11.144531 C 14.285156 11.851562 14.035156 12.457031 13.53125 12.960938 C 13.027344 13.460938 12.421875 13.714844 11.714844 13.714844 L 4.285156 13.714844 C 3.578125 13.714844 2.972656 13.460938 2.46875 12.960938 C 1.964844 12.457031 1.714844 11.851562 1.714844 11.144531 L 1.714844 3.714844 C 1.714844 3.007812 1.964844 2.398438 2.46875 1.898438 C 2.972656 1.394531 3.578125 1.144531 4.285156 1.144531 L 11.714844 1.144531 C 12.421875 1.144531 13.027344 1.394531 13.53125 1.898438 C 14.035156 2.398438 14.285156 3.007812 14.285156 3.714844 Z M 14.285156 3.714844 "/>
+</g>
+</svg>
$returnurl = new moodle_url('/group/index.php', array('id'=>$id));
-$mform_post = new groups_import_form(null, array('id'=>$id));
+$importform = new groups_import_form(null, ['id' => $id]);
// If a file has been uploaded, then process it
-if ($mform_post->is_cancelled()) {
+if ($importform->is_cancelled()) {
redirect($returnurl);
-} else if ($mform_post->get_data()) {
+} else if ($formdata = $importform->get_data()) {
echo $OUTPUT->header();
- $csv_encode = '/\&\#44/';
- if (isset($CFG->CSV_DELIMITER)) {
- $csv_delimiter = $CFG->CSV_DELIMITER;
+ $text = $importform->get_file_content('userfile');
+ $text = preg_replace('!\r\n?!', "\n", $text);
- if (isset($CFG->CSV_ENCODE)) {
- $csv_encode = '/\&\#' . $CFG->CSV_ENCODE . '/';
- }
- } else {
- $csv_delimiter = ",";
+ $rawlines = explode("\n", $text);
+
+ require_once($CFG->libdir . '/csvlib.class.php');
+ $importid = csv_import_reader::get_new_iid('groupimport');
+ $csvimport = new csv_import_reader($importid, 'groupimport');
+ $delimiter = $formdata->delimiter_name;
+ $encoding = $formdata->encoding;
+ $readcount = $csvimport->load_csv_content($text, $encoding, $delimiter);
+
+ if ($readcount === false) {
+ print_error('csvfileerror', 'error', $PAGE->url, $csvimport->get_error());
+ } else if ($readcount == 0) {
+ print_error('csvemptyfile', 'error', $PAGE->url, $csvimport->get_error());
+ } else if ($readcount == 1) {
+ print_error('csvnodata', 'error', $PAGE->url);
}
- $text = $mform_post->get_file_content('userfile');
- $text = preg_replace('!\r\n?!',"\n",$text);
+ $csvimport->init();
- $rawlines = explode("\n", $text);
unset($text);
// make arrays of valid fields for error checking
);
// --- get header (field names) ---
- $header = explode($csv_delimiter, array_shift($rawlines));
+ $header = explode($csvimport::get_delimiter($delimiter), array_shift($rawlines));
// check for valid field names
foreach ($header as $i => $h) {
$h = trim($h); $header[$i] = $h; // remove whitespace
if (!(isset($required[$h]) or isset($optionalDefaults[$h]) or isset($optional[$h]))) {
- print_error('invalidfieldname', 'error', 'import.php?id='.$id, $h);
+ print_error('invalidfieldname', 'error', $PAGE->url, $h);
}
if (isset($required[$h])) {
$required[$h] = 2;
// check for required fields
foreach ($required as $key => $value) {
if ($value < 2) {
- print_error('fieldrequired', 'error', 'import.php?id='.$id, $key);
+ print_error('fieldrequired', 'error', $PAGE->url, $key);
}
}
$linenum = 2; // since header is line 1
- foreach ($rawlines as $rawline) {
+ while ($line = $csvimport->next()) {
$newgroup = new stdClass();//to make Martin happy
foreach ($optionalDefaults as $key => $value) {
$newgroup->$key = current_language(); //defaults to current language
}
- //Note: commas within a field should be encoded as , (for comma separated csv files)
- //Note: semicolon within a field should be encoded as ; (for semicolon separated csv files)
- $line = explode($csv_delimiter, $rawline);
foreach ($line as $key => $value) {
- //decode encoded commas
- $record[$header[$key]] = preg_replace($csv_encode, $csv_delimiter, trim($value));
+ $record[$header[$key]] = trim($value);
}
- if (trim($rawline) !== '') {
+ if (trim(implode($line)) !== '') {
// add a new group to the database
// add fields to object $user
foreach ($record as $name => $value) {
// check for required values
if (isset($required[$name]) and !$value) {
- print_error('missingfield', 'error', 'import.php?id='.$id, $name);
+ print_error('missingfield', 'error', $PAGE->url, $name);
} else if ($name == "groupname") {
$newgroup->name = $value;
} else {
}
}
+ $csvimport->close();
echo $OUTPUT->single_button($returnurl, get_string('continue'), 'get');
echo $OUTPUT->footer();
die;
/// Print the form
echo $OUTPUT->header();
echo $OUTPUT->heading_with_help($strimportgroups, 'importgroups', 'core_group');
-$mform_post ->display();
+$importform->display();
echo $OUTPUT->footer();
}
require_once($CFG->libdir.'/formslib.php');
+require_once($CFG->libdir . '/csvlib.class.php');
/**
* Groups import form class
$mform->addElement('hidden', 'id');
$mform->setType('id', PARAM_INT);
+ $choices = csv_import_reader::get_delimiter_list();
+ $mform->addElement('select', 'delimiter_name', get_string('csvdelimiter', 'group'), $choices);
+ if (array_key_exists('cfg', $choices)) {
+ $mform->setDefault('delimiter_name', 'cfg');
+ } else if (get_string('listsep', 'langconfig') == ';') {
+ $mform->setDefault('delimiter_name', 'semicolon');
+ } else {
+ $mform->setDefault('delimiter_name', 'comma');
+ }
+
+ $choices = core_text::get_encodings();
+ $mform->addElement('select', 'encoding', get_string('encoding', 'group'), $choices);
+ $mform->setDefault('encoding', 'UTF-8');
$this->add_action_buttons(true, get_string('importgroups', 'core_group'));
$this->set_data($data);
$string['customexport'] = 'Custom range ({$a->timestart} - {$a->timeend})';
$string['daily'] = 'Daily';
$string['day'] = 'Day';
+$string['dayeventsmany'] = '{$a->num} events, {$a->day}';
+$string['dayeventsnone'] = 'No events, {$a}';
+$string['dayeventsone'] = '1 event, {$a}';
$string['daynext'] = 'Next day';
$string['dayprev'] = 'Previous day';
$string['dayviewfor'] = 'Day view for:';
$string['contentsmoved'] = 'Content bank contents moved to {$a}.';
$string['contenttypenoaccess'] = 'You cannot view this {$a} instance.';
$string['contenttypenoedit'] = 'You can not edit this content';
+$string['emptynamenotallowed'] = 'Empty name is not allowed';
$string['eventcontentcreated'] = 'Content created';
$string['eventcontentdeleted'] = 'Content deleted';
$string['eventcontentupdated'] = 'Content updated';
$string['eventcontentuploaded'] = 'Content uploaded';
$string['eventcontentviewed'] = 'Content viewed';
$string['errordeletingcontentfromcategory'] = 'Error deleting content from category {$a}.';
+$string['errornofile'] = 'A compatible file is needed to create a content';
$string['deletecontent'] = 'Delete content';
$string['deletecontentconfirm'] = 'Are you sure you want to delete the content <em>\'{$a->name}\'</em> and all associated files? This action cannot be undone.';
$string['displaydetails'] = 'Display content bank with file details';
$string['courserequestdisabled'] = 'Sorry, but course requests have been disabled by the administrator.';
$string['csvcolumnduplicates'] = 'Duplicate columns detected';
$string['csvemptyfile'] = 'The CSV file is empty';
+$string['csvfileerror'] = 'There is something wrong with the format of the CSV file. Please check the number of headings and columns match, and that the delimiter and file encoding are correct: {$a}';
$string['csvfewcolumns'] = 'Not enough columns, please verify the delimiter setting';
$string['csvinvalidcols'] = '<b>Invalid CSV file:</b> First line must include "Header Fields" and the file must be type of <br />"Expanded Fields/Comma Separated"<br />or<br /> "Expanded Fields with CAVV Result Code/Comma Separated"';
$string['csvinvalidcolsnum'] = 'Invalid CSV file - each line must include 49 or 70 fields';
$string['csvloaderror'] = 'An error occurred while loading the CSV file: {$a}';
+$string['csvnodata'] = 'Invalid CSV file - The CSV file has headers but does not contain any data.';
$string['csvweirdcolumns'] = 'Invalid CSV file format - number of columns is not constant!';
$string['dbconnectionfailed'] = '<p>Error: Database connection failed</p>
<p>It is possible that the database is overloaded or otherwise not running properly.</p>
$string['destinationcmnotexit'] = 'The destination course module does not exist';
$string['detectedbrokenplugin'] = 'Plugin "{$a}" is defective or outdated, can not continue, sorry.';
$string['dmlexceptiononinstall'] = '<p>A database error has occurred [{$a->errorcode}].<br />{$a->debuginfo}</p>';
+$string['dmlparseexception'] = 'Error parsing SQL query';
$string['dmlreadexception'] = 'Error reading from database';
$string['dmltransactionexception'] = 'Database transaction error';
$string['dmlwriteexception'] = 'Error writing to database';
$string['creategroupinselectedgrouping'] = 'Create group in grouping';
$string['createingrouping'] = 'Grouping of auto-created groups';
$string['createorphangroup'] = 'Create orphan group';
+$string['csvdelimiter'] = 'CSV delimiter';
$string['databaseupgradegroups'] = 'Groups version is now {$a}';
$string['defaultgrouping'] = 'Default grouping';
$string['defaultgroupingname'] = 'Grouping';
$string['editusersgroupsa'] = 'Edit groups for "{$a}"';
$string['enablemessaging'] = 'Group messaging';
$string['enablemessaging_help'] = 'If enabled, group members can send messages to the others in their group via the messaging drawer.';
+$string['encoding'] = 'Encoding';
$string['enrolmentkey'] = 'Enrolment key';
$string['enrolmentkey_help'] = 'An enrolment key enables access to the course to be restricted to only those who know the key. If a group enrolment key is specified, then not only will entering that key let the user into the course, but it will also automatically make them a member of this group.
$string['group:html_video'] = 'Video files natively supported by browsers';
$string['group:image'] = 'Image files';
$string['group:media_source'] = 'Streaming media';
+$string['group:optimised_image'] = 'Image files to be optimised, such as badges';
$string['group:presentation'] = 'Presentation files';
$string['group:sourcecode'] = 'Source code';
$string['group:spreadsheet'] = 'Spreadsheet files';
return $('[name="' + name + '"],[name="' + name + '[]"]');
}
- /**
- * Find the name of the given element
- * @param {EventTarget} el
- * @returns {String}
- */
- function getElementName(el) {
- return $(el).attr('name').replace(/\[]/, '');
- }
-
/**
* Check to see whether a particular condition is met
* @param {*|jQuery|HTMLElement} $dependon
}
/**
- * Show / hide the elements that depend on the element(s) with the given name
- * OR (if no dependonname given) the element(s) with the same name as the element that
- * triggered the event e.
- * @param {Event} e
- * @param {String} dependonname (optional)
+ * Show / hide the elements that depend on some elements.
*/
- function updateDependencies(e, dependonname) {
- dependonname = dependonname || getElementName(e.currentTarget);
- var $dependon = getElementsByName(dependonname);
- if (!dependencies.hasOwnProperty(dependonname)) {
- return;
- }
- // Process all dependency conditions related to the updated element.
+ function updateDependencies() {
+ // Process all dependency conditions.
var toHide = {};
- $.each(dependencies[dependonname], function(condition, values) {
- $.each(values, function(value, elements) {
- var hide = checkDependency($dependon, condition, value);
- $.each(elements, function(idx, elToHide) {
- if (toHide.hasOwnProperty(elToHide)) {
- toHide[elToHide] = toHide[elToHide] || hide;
- } else {
- toHide[elToHide] = hide;
- }
+ $.each(dependencies, function(dependonname) {
+ var dependon = getElementsByName(dependonname);
+ $.each(dependencies[dependonname], function(condition, values) {
+ $.each(values, function(value, elements) {
+ var hide = checkDependency(dependon, condition, value);
+ $.each(elements, function(idx, elToHide) {
+ if (toHide.hasOwnProperty(elToHide)) {
+ toHide[elToHide] = toHide[elToHide] || hide;
+ } else {
+ toHide[elToHide] = hide;
+ }
+ });
});
});
});
var $el = getElementsByName(depname);
if ($el.length) {
$el.on('change', updateDependencies);
- updateDependencies(null, depname);
}
});
+ updateDependencies();
}
/**
'groups' => array('spreadsheet')),
'gslides' => array('type' => 'application/vnd.google-apps.presentation', 'icon' => 'powerpoint',
'groups' => array('presentation')),
- 'gif' => array('type' => 'image/gif', 'icon' => 'gif', 'groups' => array('image', 'web_image'), 'string' => 'image'),
+ 'gif' => array('type' => 'image/gif', 'icon' => 'gif', 'groups' => array('image', 'web_image', 'optimised_image'),
+ 'string' => 'image'),
'gtar' => array('type' => 'application/x-gtar', 'icon' => 'archive',
'groups' => array('archive'), 'string' => 'archive'),
'tgz' => array('type' => 'application/g-zip', 'icon' => 'archive', 'groups' => array('archive'), 'string' => 'archive'),
'jmt' => array('type' => 'text/xml', 'icon' => 'markup'),
'jmx' => array('type' => 'text/xml', 'icon' => 'markup'),
'jnlp' => array('type' => 'application/x-java-jnlp-file', 'icon' => 'markup'),
- 'jpe' => array('type' => 'image/jpeg', 'icon' => 'jpeg', 'groups' => array('image', 'web_image'), 'string' => 'image'),
- 'jpeg' => array('type' => 'image/jpeg', 'icon' => 'jpeg', 'groups' => array('image', 'web_image'), 'string' => 'image'),
- 'jpg' => array('type' => 'image/jpeg', 'icon' => 'jpeg', 'groups' => array('image', 'web_image'), 'string' => 'image'),
+ 'jpe' => array('type' => 'image/jpeg', 'icon' => 'jpeg', 'groups' => array('image', 'web_image', 'optimised_image'),
+ 'string' => 'image'),
+ 'jpeg' => array('type' => 'image/jpeg', 'icon' => 'jpeg', 'groups' => array('image', 'web_image', 'optimised_image'),
+ 'string' => 'image'),
+ 'jpg' => array('type' => 'image/jpeg', 'icon' => 'jpeg', 'groups' => array('image', 'web_image', 'optimised_image'),
+ 'string' => 'image'),
'jqz' => array('type' => 'text/xml', 'icon' => 'markup'),
'js' => array('type' => 'application/x-javascript', 'icon' => 'text', 'groups' => array('web_file')),
'json' => array('type' => 'application/json', 'icon' => 'text'),
'php' => array('type' => 'text/plain', 'icon' => 'sourcecode'),
'pic' => array('type' => 'image/pict', 'icon' => 'image', 'groups' => array('image'), 'string' => 'image'),
'pict' => array('type' => 'image/pict', 'icon' => 'image', 'groups' => array('image'), 'string' => 'image'),
- 'png' => array('type' => 'image/png', 'icon' => 'png', 'groups' => array('image', 'web_image'), 'string' => 'image'),
+ 'png' => array('type' => 'image/png', 'icon' => 'png', 'groups' => array('image', 'web_image', 'optimised_image'),
+ 'string' => 'image'),
'pps' => array('type' => 'application/vnd.ms-powerpoint', 'icon' => 'powerpoint', 'groups' => array('presentation')),
'ppt' => array('type' => 'application/vnd.ms-powerpoint', 'icon' => 'powerpoint', 'groups' => array('presentation')),
'pptx' => array('type' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
$DB->delete_records('competency_userevidencecomp', ['userevidenceid' => $userevidence->id]);
$DB->delete_records('competency_userevidence', ['id' => $userevidence->id]);
- $context = context_user::instance($userevidence->userid);
- $fs->delete_area_files($context->id, 'core_competency', 'userevidence', $userevidence->id);
+ if ($record = $DB->get_record('context', ['contextlevel' => CONTEXT_USER, 'instanceid' => $userevidence->userid],
+ '*', IGNORE_MISSING)) {
+ // Delete all orphaned user evidences files.
+ $fs->delete_area_files($record->id, 'core_competency', 'userevidence', $userevidence->userid);
+ }
}
$sql = "SELECT cp.id
upgrade_main_savepoint(true, 2020061501.04);
}
+ if ($oldversion < 2020061501.08) {
+ // Delete all user evidence files from users that have been deleted.
+ $sql = "SELECT DISTINCT f.*
+ FROM {files} f
+ LEFT JOIN {context} c ON f.contextid = c.id
+ LEFT JOIN {user} u ON c.instanceid = u.id
+ WHERE f.component = :component
+ AND f.filearea = :filearea
+ AND u.deleted = 1";
+ $stalefiles = $DB->get_records_sql($sql, ['component' => 'core_competency', 'filearea' => 'userevidence']);
+
+ $fs = get_file_storage();
+ foreach ($stalefiles as $stalefile) {
+ $fs->get_file_instance($stalefile)->delete();
+ }
+
+ upgrade_main_savepoint(true, 2020061501.08);
+ }
+
return true;
}
/**
* Prepare the statement for execution
- * @throws dml_connection_exception
+ *
* @param string $sql
* @return resource
+ *
+ * @throws dml_exception
*/
protected function parse_query($sql) {
$stmt = oci_parse($this->oci, $sql);
if ($stmt == false) {
- throw new dml_connection_exception('Can not parse sql query'); //TODO: maybe add better info
+ throw new dml_exception('dmlparseexception', null, $this->get_last_error());
}
return $stmt;
}
* commonly used in moodle the following groups:
* - web_image - image that can be included as <img> in HTML
* - image - image that we can parse using GD to find it's dimensions, also used for portfolio format
+ * - optimised_image - image that will be processed and optimised
* - video - file that can be imported as video in text editor
* - audio - file that can be imported as audio in text editor
* - archive - we can extract files from this archive
// All these three files are in both "image" and also "web_image"
// groups. We display both groups.
$data = $util->data_for_browser('jpg png gif', true, '.gif');
- $this->assertEquals(2, count($data));
+ $this->assertEquals(3, count($data));
$this->assertTrue($data[0]->key !== $data[1]->key);
foreach ($data as $group) {
- $this->assertTrue(($group->key === 'image' || $group->key === 'web_image'));
+ $this->assertTrue(($group->key === 'image' || $group->key === 'web_image' || $group->key === 'optimised_image'));
$this->assertEquals(3, count($group->types));
$this->assertFalse($group->selectable);
foreach ($group->types as $ext) {
}
}
- // There is a group web_image which is a subset of the group image. The
- // file extensions that fall into both groups will be displayed twice.
+ // The groups web_image and optimised_image are a subset of the group image. The
+ // file extensions that fall into these groups will be displayed thrice.
$data = $util->data_for_browser('web_image');
foreach ($data as $group) {
- $this->assertTrue(($group->key === 'image' || $group->key === 'web_image'));
+ $this->assertTrue(($group->key === 'image' || $group->key === 'web_image' || $group->key === 'optimised_image'));
}
// Check that "All file types" are displayed first.
$course = $this->page->course;
if (\core\session\manager::is_loggedinas()) {
$realuser = \core\session\manager::get_realuser();
- $fullname = fullname($realuser, true);
+ $fullname = fullname($realuser);
if ($withlinks) {
$loginastitle = get_string('loginas');
$realuserinfo = " [<a href=\"$CFG->wwwroot/course/loginas.php?id=$course->id&sesskey=".sesskey()."\"";
} else if (isloggedin()) {
$context = context_course::instance($course->id);
- $fullname = fullname($USER, true);
+ $fullname = fullname($USER);
// Since Moodle 2.0 this link always goes to the public profile page (not the course profile page)
if ($withlinks) {
$linktitle = get_string('viewprofile');
tableRoot.dataset.tableLastInitial = lastInitial;
}
- if (pageNumber !== null) {
- if (tableRoot.dataset.tablePageNumber != pageNumber) {
- tableConfigChanged = true;
- }
-
- tableRoot.dataset.tablePageNumber = pageNumber;
- }
-
if (pageSize !== null) {
if (tableRoot.dataset.tablePageSize != pageSize) {
tableConfigChanged = true;
tableRoot.dataset.tableFilters = filterJson;
}
+ // Reset to page 1 when table content is being altered by filtering or sorting.
+ // This ensures the table page being loaded always exists, and gives a consistent experience.
+ if (tableConfigChanged) {
+ pageNumber = 1;
+ }
+
// Update hidden columns.
if (hiddenColumns) {
const columnJson = JSON.stringify(hiddenColumns);
tableRoot.dataset.tableHiddenColumns = columnJson;
}
+ if (pageNumber !== null) {
+ if (tableRoot.dataset.tablePageNumber != pageNumber) {
+ tableConfigChanged = true;
+ }
+
+ tableRoot.dataset.tablePageNumber = pageNumber;
+ }
+
// Refresh.
if (refreshContent && tableConfigChanged) {
return refreshTableContent(tableRoot)
global $CFG;
// Visit the Ionic URL and wait for it to load.
- $this->execute('behat_general::i_visit', [$url]);
+ $this->getSession()->visit($url);
$this->spin(
function($context, $args) {
$title = $context->getSession()->getPage()->find('xpath', '//title');
if (isset($CFG->config_php_settings['upgradekey'])) {
if ($upgradekeyhash === null or $upgradekeyhash !== sha1($CFG->config_php_settings['upgradekey'])) {
if (!$PAGE->headerprinted) {
+ $PAGE->set_title(get_string('upgradekeyreq', 'admin'));
$output = $PAGE->get_renderer('core', 'admin');
echo $output->upgradekey_form_page(new moodle_url('/admin/index.php', array('cache' => 0)));
die();
});
};
+ /**
+ * Create a plain version of an HTML text.
+ *
+ * This texts is used as a message preview while is sent to the server. This way
+ * it is possible to prevent self-xss.
+ *
+ * @param {String} text Text to send.
+ * @return {String} The plain text version of the text.
+ */
+ const previewText = function(text) {
+ // Remove all script and styles from text (we don't want it there).
+ let plaintext = text.replace(/<style([\s\S]*?)<\/style>/gi, '');
+ plaintext = plaintext.replace(/<script([\s\S]*?)<\/script>/gi, '');
+ // Beautify a bit the output adding some line breaks.
+ plaintext = plaintext.replace(/<\/div>/ig, '\n');
+ plaintext = plaintext.replace(/<\/li>/ig, '\n');
+ plaintext = plaintext.replace(/<li>/ig, ' * ');
+ plaintext = plaintext.replace(/<\/ul>/ig, '\n');
+ plaintext = plaintext.replace(/<\/p>/ig, '\n');
+ plaintext = plaintext.replace(/<br[^>]*>/gi, '\n');
+ // Remove all remaining tags and convert line breaks into html.
+ plaintext = plaintext.replace(/<[^>]+>/ig, '');
+ plaintext = plaintext.replace(/\n+/ig, '\n');
+ return plaintext.replace(/\n/ig, '<br>');
+ };
+
/**
* Buffers messages to be sent to the server. We use a buffer here to allow the
* user to freely input messages without blocking the interface for them.
*/
var sendMessage = function(text) {
var id = 'temp' + Date.now();
+ // Render a preview version of the message while sending.
+ let loadingmessage = {
+ id: id,
+ useridfrom: viewState.loggedInUserId,
+ text: previewText(text),
+ timecreated: null
+ };
+ var newState = StateManager.addMessages(viewState, [loadingmessage]);
+ render(newState);
+ // Send the real message.
var message = {
id: id,
useridfrom: viewState.loggedInUserId,
text: text,
timecreated: null
};
- var newState = StateManager.addMessages(viewState, [message]);
- render(newState);
sendMessageBuffer.push(message);
processSendMessageBuffer();
};
require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
// Check that all of the submission plugins are ready for this submission.
+ // Also check whether there is something to be submitted as well against atleast one.
$notifications = array();
$submission = $this->get_user_submission($USER->id, false);
+ if ($this->get_instance()->teamsubmission) {
+ $submission = $this->get_group_submission($USER->id, 0, false);
+ }
+
$plugins = $this->get_submission_plugins();
+ $hassubmission = false;
foreach ($plugins as $plugin) {
if ($plugin->is_enabled() && $plugin->is_visible()) {
$check = $plugin->precheck_submission($submission);
if ($check !== true) {
$notifications[] = $check;
}
+
+ if (is_object($submission) && !$plugin->is_empty($submission)) {
+ $hassubmission = true;
+ }
}
}
+ // If there are no submissions and no existing notifications to be displayed the stop.
+ if (!$hassubmission && !$notifications) {
+ $notifications[] = get_string('addsubmission_help', 'assign');
+ }
+
$data = new stdClass();
$adminconfig = $this->get_admin_config();
$requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
$gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
}
$gradeddate = $gradebookgrade->dategraded;
- if (isset($grade->grader) && $grade->grader > 0) {
- $grader = $DB->get_record('user', array('id' => $grade->grader));
- } else if (isset($gradebookgrade->usermodified) && $gradebookgrade->usermodified > 0) {
- $grader = $DB->get_record('user', array('id' => $gradebookgrade->usermodified));
+
+ // Only display the grader if it is in the right state.
+ if (in_array($gradingstatus, [ASSIGN_GRADING_STATUS_GRADED, ASSIGN_MARKING_WORKFLOW_STATE_RELEASED])){
+ if (isset($grade->grader) && $grade->grader > 0) {
+ $grader = $DB->get_record('user', array('id' => $grade->grader));
+ } else if (isset($gradebookgrade->usermodified)
+ && $gradebookgrade->usermodified > 0
+ && has_capability('mod/assign:grade', $this->get_context(), $gradebookgrade->usermodified)) {
+ // Grader not provided. Check that usermodified is a user who can grade.
+ // Case 1: When an assignment is reopened an empty assign_grade is created so the feedback
+ // plugin can know which attempt it's referring to. In this case, usermodifed is a student.
+ // Case 2: When an assignment's grade is overrided via the gradebook, usermodified is a grader
+ $grader = $DB->get_record('user', array('id' => $gradebookgrade->usermodified));
+ }
}
}
{{/duedate}}
</div>
-</span>
</div>
{{!
</div>
{{#js}}
require(['mod_assign/grading_navigation', 'core/tooltip'], function(GradingNavigation, ToolTip) {
- var nav = new GradingNavigation('[data-region="user-selector"]');
- var tooltip = new ToolTip('[data-region="assignment-tooltip"]');
+ new GradingNavigation('[data-region="user-selector"]');
+ new ToolTip('[data-region="assignment-tooltip"]');
});
{{/js}}
</small>
</span>
-<span data-region="configure-filters" id="filter-configuration-{{uniqid}}" class="card card-large p-2">
+<div data-region="configure-filters" id="filter-configuration-{{uniqid}}" class="card card-large p-2">
<form>
<span class="row px-3 py-1">
<label class="text-right w-25 p-2 m-0" for="filter-general-{{uniqid}}">
</span>
{{/hasmarkingworkflow}}
</form>
-</span>
+</div>
<a href="#" data-region="user-filters" title="{{#str}}changefilters, mod_assign{{/str}}" aria-expanded="false" aria-controls="filter-configuration-{{uniqid}}">
<span class="accesshide">
);
}
- $mform->addElement('text', 'title', get_string('chaptertitle', 'mod_book'), array('size'=>'30'));
+ $mform->addElement('text', 'title', get_string('chaptertitle', 'mod_book'),
+ ['size' => '30', 'maxlength' => '255']);
$mform->setType('title', PARAM_RAW);
$mform->addRule('title', null, 'required', null, 'client');
+ $mform->addRule('title', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
$mform->addElement('advcheckbox', 'subchapter', get_string('subchapter', 'mod_book'), $disabledmsg);
$DB->update_record($this->get_table_name(), $grade);
- // Update in the gradebook.
+ // Update in the gradebook (note that 'cmidnumber' is required in order to update grades).
$mapper = forum_container::get_legacy_data_mapper_factory()->get_forum_data_mapper();
- forum_update_grades($mapper->to_legacy_object($this->forum), $grade->userid);
+ $forumrecord = $mapper->to_legacy_object($this->forum);
+ $forumrecord->cmidnumber = $this->forum->get_course_module_record()->idnumber;
+
+ forum_update_grades($forumrecord, $grade->userid);
return true;
}
$this->log("Adding post {$post->id} in format {$maildigest} without HTML", 2);
}
- if ($maildigest == 1 && $CFG->forum_usermarksread) {
+ if ($maildigest == 1 && !$CFG->forum_usermarksread) {
// Create an array of postid's for this user to mark as read.
$this->markpostsasread[] = $post->id;
}
function forum_update_grades($forum, $userid = 0): void {
global $CFG, $DB;
require_once($CFG->libdir.'/gradelib.php');
- $cm = get_coursemodule_from_instance('forum', $forum->id);
- $forum->cmidnumber = $cm->idnumber;
$ratings = null;
if ($forum->assessed) {
require_once($CFG->dirroot.'/rating/lib.php');
+ $cm = get_coursemodule_from_instance('forum', $forum->id);
+
$rm = new rating_manager();
$ratings = $rm->get_user_grades((object) [
'component' => 'mod_forum',
$digesttime = usergetmidnight(time(), \core_date::get_server_timezone()) + ($CFG->digestmailtime * 3600);
$this->assertLessThanOrEqual($digesttime, $task->nextruntime);
}
+
+ /**
+ * The sending of a digest marks posts as read if automatic message read marking is set.
+ */
+ public function test_cron_digest_marks_posts_read() {
+ global $DB, $CFG;
+
+ $this->resetAfterTest(true);
+
+ // Disable the 'Manual message read marking' option.
+ $CFG->forum_usermarksread = false;
+
+ // Set up a basic user enrolled in a course.
+ $userhelper = $this->helper_setup_user_in_course();
+ $user = $userhelper->user;
+ $course1 = $userhelper->courses->course1;
+ $forum1 = $userhelper->forums->forum1;
+ $posts = [];
+
+ // Set the tested user's default maildigest, trackforums, read tracking settings.
+ $DB->set_field('user', 'maildigest', 1, ['id' => $user->id]);
+ $DB->set_field('user', 'trackforums', 1, ['id' => $user->id]);
+ set_user_preference('forum_markasreadonnotification', 1, $user->id);
+
+ // Set the maildigest preference for forum1 to default.
+ forum_set_user_maildigest($forum1, -1, $user);
+
+ // Add 5 discussions to forum 1.
+ for ($i = 0; $i < 5; $i++) {
+ list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]);
+ $posts[] = $post;
+ }
+
+ // There should be unread posts for the forum.
+ $expectedposts = [
+ $forum1->id => (object) [
+ 'id' => $forum1->id,
+ 'unread' => count($posts),
+ ],
+ ];
+ $this->assertEquals($expectedposts, forum_tp_get_course_unread_posts($user->id, $course1->id));
+
+ // One digest mail should be sent and no other messages.
+ $expect = [
+ (object) [
+ 'userid' => $user->id,
+ 'messages' => 0,
+ 'digests' => 1,
+ ],
+ ];
+ $this->queue_tasks_and_assert($expect);
+
+ $this->send_digests_and_assert($user, $posts);
+
+ // Verify that there are no unread posts for any forums.
+ $this->assertEmpty(forum_tp_get_course_unread_posts($user->id, $course1->id));
+ }
+
+ /**
+ * The sending of a digest does not mark posts as read when manual message read marking is set.
+ */
+ public function test_cron_digest_leaves_posts_unread() {
+ global $DB, $CFG;
+
+ $this->resetAfterTest(true);
+
+ // Enable the 'Manual message read marking' option.
+ $CFG->forum_usermarksread = true;
+
+ // Set up a basic user enrolled in a course.
+ $userhelper = $this->helper_setup_user_in_course();
+ $user = $userhelper->user;
+ $course1 = $userhelper->courses->course1;
+ $forum1 = $userhelper->forums->forum1;
+ $posts = [];
+
+ // Set the tested user's default maildigest, trackforums, read tracking settings.
+ $DB->set_field('user', 'maildigest', 1, ['id' => $user->id]);
+ $DB->set_field('user', 'trackforums', 1, ['id' => $user->id]);
+ set_user_preference('forum_markasreadonnotification', 1, $user->id);
+
+ // Set the maildigest preference for forum1 to default.
+ forum_set_user_maildigest($forum1, -1, $user);
+
+ // Add 5 discussions to forum 1.
+ for ($i = 0; $i < 5; $i++) {
+ list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]);
+ $posts[] = $post;
+ }
+
+ // There should be unread posts for the forum.
+ $expectedposts = [
+ $forum1->id => (object) [
+ 'id' => $forum1->id,
+ 'unread' => count($posts),
+ ],
+ ];
+ $this->assertEquals($expectedposts, forum_tp_get_course_unread_posts($user->id, $course1->id));
+
+ // One digest mail should be sent and no other messages.
+ $expect = [
+ (object) [
+ 'userid' => $user->id,
+ 'messages' => 0,
+ 'digests' => 1,
+ ],
+ ];
+ $this->queue_tasks_and_assert($expect);
+
+ $this->send_digests_and_assert($user, $posts);
+
+ // Verify that there are still the same unread posts for the forum.
+ $this->assertEquals($expectedposts, forum_tp_get_course_unread_posts($user->id, $course1->id));
+ }
}
// Look up the shared secret for the specified key in both the types_config table (for configured tools)
// And in the lti resource table for ad-hoc tools.
$lti13 = LTI_VERSION_1P3;
- $query = "SELECT t2.value
+ $query = "SELECT " . $DB->sql_compare_text('t2.value', 256) . " AS value
FROM {lti_types_config} t1
JOIN {lti_types_config} t2 ON t1.typeid = t2.typeid
JOIN {lti_types} type ON t2.typeid = type.id
WHERE t1.name = 'resourcekey'
- AND t1.value = :key1
+ AND " . $DB->sql_compare_text('t1.value', 256) . " = :key1
AND t2.name = 'password'
AND type.state = :configured1
AND type.ltiversion <> :ltiversion
$cmid = $quiz->coursemodule;
// Process the options from the form.
- $quiz->created = time();
+ $quiz->timecreated = time();
$result = quiz_process_options($quiz);
if ($result && is_string($result)) {
return $result;
/**
* Get any fields that might be needed when sorting on date for a particular slot.
+ *
+ * Note: these values are only used for sorting. The values displayed are taken
+ * from $this->lateststeps loaded in load_extra_data().
+ *
* @param int $slot the slot for the column we want.
* @param string $alias the table alias for latest state information relating to that slot.
+ * @return string definitions of extra fields to add to the SELECT list of the query.
*/
protected function get_required_latest_state_fields($slot, $alias) {
return '';
if (!isset($this->lateststeps[$attempt->usageid][$slot])) {
return '-';
}
- $stepdata = $this->lateststeps[$attempt->usageid][$slot];
-
- if (property_exists($stepdata, $field . 'full')) {
- $value = $stepdata->{$field . 'full'};
- } else {
- $value = $stepdata->$field;
- }
- return $value;
+ return $this->lateststeps[$attempt->usageid][$slot]->$field;
}
public function other_cols($colname, $attempt) {
*/
protected function get_required_latest_state_fields($slot, $alias) {
global $DB;
- $sortableresponse = $DB->sql_order_by_text("{$alias}.questionsummary");
- if ($sortableresponse === "{$alias}.questionsummary") {
- // Can just order by text columns. No complexity needed.
- return "{$alias}.questionsummary AS question{$slot},
- {$alias}.rightanswer AS right{$slot},
- {$alias}.responsesummary AS response{$slot}";
- } else {
- // Work-around required.
- return $DB->sql_order_by_text("{$alias}.questionsummary") . " AS question{$slot},
- {$alias}.questionsummary AS question{$slot}full,
- " . $DB->sql_order_by_text("{$alias}.rightanswer") . " AS right{$slot},
- {$alias}.rightanswer AS right{$slot}full,
- " . $DB->sql_order_by_text("{$alias}.responsesummary") . " AS response{$slot},
- {$alias}.responsesummary AS response{$slot}full";
- }
+ return $DB->sql_order_by_text("{$alias}.questionsummary") . " AS question{$slot},
+ " . $DB->sql_order_by_text("{$alias}.rightanswer") . " AS right{$slot},
+ " . $DB->sql_order_by_text("{$alias}.responsesummary") . " AS response{$slot}";
}
}
$generator->create_instance(array('course'=>$SITE->id));
$generator->create_instance(array('course'=>$SITE->id));
- $quiz = $generator->create_instance(array('course'=>$SITE->id));
+ $createtime = time();
+ $quiz = $generator->create_instance(array('course' => $SITE->id, 'timecreated' => 0));
$this->assertEquals(3, $DB->count_records('quiz'));
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
$context = context_module::instance($cm->id);
$this->assertEquals($quiz->cmid, $context->instanceid);
+
+ $this->assertEqualsWithDelta($createtime,
+ $DB->get_field('quiz', 'timecreated', ['id' => $cm->instance]), 2);
}
}
display: none;
}
-.path-mod-workshop #id_rubric-grid-wrapper .rubric-grid {
+.path-mod-workshop .mform.frozen #id_rubric-grid-wrapper,
+.path-mod-workshop #id_rubric-grid-wrapper {
margin-left: auto;
margin-right: auto;
+ width: 100%;
+}
+
+.path-mod-workshop .mform.frozen #id_rubric-grid-wrapper .checkbox,
+.path-mod-workshop .assessmentform.rubric.grid #id_rubric-grid-wrapper .checkbox {
+ max-width: 100%;
+ flex: 0 0 100%;
+ text-align: left;
+}
+
+@media all and (-ms-high-contrast: none) { /* IE10 & IE11 hack */
+ .path-mod-workshop .mform.frozen .rubric-grid,
+ .path-mod-workshop .assessmentform .rubric-grid {
+ width: 100%;
+ table-layout: fixed;
+ }
}
.path-mod-workshop .mform.frozen #id_rubric-grid-wrapper .fitem .felement,
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" preserveAspectRatio="xMinYMid meet">
+<g id="surface1">
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(60%,60%,60%);fill-opacity:1;" d="M 11.402344 10.019531 C 11.402344 9.863281 11.34375 9.730469 11.230469 9.617188 L 9.617188 8 L 11.230469 6.382812 C 11.34375 6.269531 11.402344 6.136719 11.402344 5.980469 C 11.402344 5.820312 11.34375 5.683594 11.230469 5.570312 L 10.429688 4.769531 C 10.316406 4.65625 10.179688 4.597656 10.019531 4.597656 C 9.863281 4.597656 9.730469 4.65625 9.617188 4.769531 L 8 6.382812 L 6.382812 4.769531 C 6.269531 4.65625 6.136719 4.597656 5.980469 4.597656 C 5.820312 4.597656 5.683594 4.65625 5.570312 4.769531 L 4.769531 5.570312 C 4.65625 5.683594 4.597656 5.820312 4.597656 5.980469 C 4.597656 6.136719 4.65625 6.269531 4.769531 6.382812 L 6.382812 8 L 4.769531 9.617188 C 4.65625 9.730469 4.597656 9.863281 4.597656 10.019531 C 4.597656 10.179688 4.65625 10.316406 4.769531 10.429688 L 5.570312 11.230469 C 5.683594 11.34375 5.820312 11.402344 5.980469 11.402344 C 6.136719 11.402344 6.269531 11.34375 6.382812 11.230469 L 8 9.617188 L 9.617188 11.230469 C 9.730469 11.34375 9.863281 11.402344 10.019531 11.402344 C 10.179688 11.402344 10.316406 11.34375 10.429688 11.230469 L 11.230469 10.429688 C 11.34375 10.316406 11.402344 10.179688 11.402344 10.019531 Z M 14.855469 8 C 14.855469 9.242188 14.550781 10.390625 13.9375 11.441406 C 13.324219 12.492188 12.492188 13.324219 11.441406 13.9375 C 10.390625 14.550781 9.242188 14.855469 8 14.855469 C 6.757812 14.855469 5.609375 14.550781 4.558594 13.9375 C 3.507812 13.324219 2.675781 12.492188 2.0625 11.441406 C 1.449219 10.390625 1.144531 9.242188 1.144531 8 C 1.144531 6.757812 1.449219 5.609375 2.0625 4.558594 C 2.675781 3.507812 3.507812 2.675781 4.558594 2.0625 C 5.609375 1.449219 6.757812 1.144531 8 1.144531 C 9.242188 1.144531 10.390625 1.449219 11.441406 2.0625 C 12.492188 2.675781 13.324219 3.507812 13.9375 4.558594 C 14.550781 5.609375 14.855469 6.757812 14.855469 8 Z M 14.855469 8 "/>
+</g>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" preserveAspectRatio="xMinYMid meet">
+<g id="surface1">
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(60%,60%,60%);fill-opacity:1;" d="M 10.457031 8.570312 C 10.457031 8.648438 10.425781 8.71875 10.367188 8.777344 L 6.207031 12.9375 C 6.144531 12.996094 6.078125 13.027344 6 13.027344 C 5.921875 13.027344 5.855469 12.996094 5.792969 12.9375 L 5.347656 12.492188 C 5.289062 12.429688 5.257812 12.363281 5.257812 12.285156 C 5.257812 12.207031 5.289062 12.140625 5.347656 12.082031 L 8.855469 8.570312 L 5.347656 5.0625 C 5.289062 5.003906 5.257812 4.933594 5.257812 4.855469 C 5.257812 4.78125 5.289062 4.710938 5.347656 4.652344 L 5.792969 4.207031 C 5.855469 4.144531 5.921875 4.117188 6 4.117188 C 6.078125 4.117188 6.144531 4.144531 6.207031 4.207031 L 10.367188 8.367188 C 10.425781 8.425781 10.457031 8.492188 10.457031 8.570312 Z M 10.457031 8.570312 "/>
+</g>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" preserveAspectRatio="xMinYMid meet">
+<g id="surface1">
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(60%,60%,60%);fill-opacity:1;" d="M 14.855469 12 L 14.855469 13.144531 C 14.855469 13.296875 14.800781 13.429688 14.6875 13.542969 C 14.574219 13.65625 14.441406 13.714844 14.285156 13.714844 L 1.714844 13.714844 C 1.558594 13.714844 1.425781 13.65625 1.3125 13.542969 C 1.199219 13.429688 1.144531 13.296875 1.144531 13.144531 L 1.144531 12 C 1.144531 11.84375 1.199219 11.710938 1.3125 11.597656 C 1.425781 11.484375 1.558594 11.429688 1.714844 11.429688 L 14.285156 11.429688 C 14.441406 11.429688 14.574219 11.484375 14.6875 11.597656 C 14.800781 11.710938 14.855469 11.84375 14.855469 12 Z M 14.855469 7.429688 L 14.855469 8.570312 C 14.855469 8.726562 14.800781 8.859375 14.6875 8.972656 C 14.574219 9.085938 14.441406 9.144531 14.285156 9.144531 L 1.714844 9.144531 C 1.558594 9.144531 1.425781 9.085938 1.3125 8.972656 C 1.199219 8.859375 1.144531 8.726562 1.144531 8.570312 L 1.144531 7.429688 C 1.144531 7.273438 1.199219 7.140625 1.3125 7.027344 C 1.425781 6.914062 1.558594 6.855469 1.714844 6.855469 L 14.285156 6.855469 C 14.441406 6.855469 14.574219 6.914062 14.6875 7.027344 C 14.800781 7.140625 14.855469 7.273438 14.855469 7.429688 Z M 14.855469 2.855469 L 14.855469 4 C 14.855469 4.15625 14.800781 4.289062 14.6875 4.402344 C 14.574219 4.515625 14.441406 4.570312 14.285156 4.570312 L 1.714844 4.570312 C 1.558594 4.570312 1.425781 4.515625 1.3125 4.402344 C 1.199219 4.289062 1.144531 4.15625 1.144531 4 L 1.144531 2.855469 C 1.144531 2.703125 1.199219 2.570312 1.3125 2.457031 C 1.425781 2.34375 1.558594 2.285156 1.714844 2.285156 L 14.285156 2.285156 C 14.441406 2.285156 14.574219 2.34375 14.6875 2.457031 C 14.800781 2.570312 14.855469 2.703125 14.855469 2.855469 Z M 14.855469 2.855469 "/>
+</g>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" preserveAspectRatio="xMinYMid meet">
+<g id="surface1">
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(60%,60%,60%);fill-opacity:1;" d="M 12.167969 7.832031 L 5.542969 14.457031 C 5.429688 14.570312 5.296875 14.625 5.144531 14.625 C 4.988281 14.625 4.855469 14.570312 4.742188 14.457031 L 3.257812 12.972656 C 3.144531 12.859375 3.089844 12.726562 3.089844 12.570312 C 3.089844 12.417969 3.144531 12.28125 3.257812 12.167969 L 8 7.429688 L 3.257812 2.6875 C 3.144531 2.574219 3.089844 2.441406 3.089844 2.285156 C 3.089844 2.132812 3.144531 1.996094 3.257812 1.882812 L 4.742188 0.402344 C 4.855469 0.289062 4.988281 0.230469 5.144531 0.230469 C 5.296875 0.230469 5.429688 0.289062 5.542969 0.402344 L 12.167969 7.027344 C 12.28125 7.140625 12.339844 7.273438 12.339844 7.429688 C 12.339844 7.582031 12.28125 7.71875 12.167969 7.832031 Z M 12.167969 7.832031 "/>
+</g>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" preserveAspectRatio="xMinYMid meet">
+<g id="surface1">
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(60%,60%,60%);fill-opacity:1;" d="M 12.742188 2.6875 L 8 7.429688 L 12.742188 12.167969 C 12.855469 12.28125 12.910156 12.417969 12.910156 12.570312 C 12.910156 12.726562 12.855469 12.859375 12.742188 12.972656 L 11.257812 14.457031 C 11.144531 14.570312 11.011719 14.625 10.855469 14.625 C 10.703125 14.625 10.570312 14.570312 10.457031 14.457031 L 3.832031 7.832031 C 3.71875 7.71875 3.660156 7.582031 3.660156 7.429688 C 3.660156 7.273438 3.71875 7.140625 3.832031 7.027344 L 10.457031 0.402344 C 10.570312 0.289062 10.703125 0.230469 10.855469 0.230469 C 11.011719 0.230469 11.144531 0.289062 11.257812 0.402344 L 12.742188 1.882812 C 12.855469 1.996094 12.910156 2.132812 12.910156 2.285156 C 12.910156 2.441406 12.855469 2.574219 12.742188 2.6875 Z M 12.742188 2.6875 "/>
+</g>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" preserveAspectRatio="xMinYMid meet">
+<g id="surface1">
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(60%,60%,60%);fill-opacity:1;" d="M 10.722656 8.964844 L 13.457031 6.3125 L 9.6875 5.757812 L 8 2.347656 L 6.3125 5.757812 L 2.542969 6.3125 L 5.277344 8.964844 L 4.625 12.722656 L 8 10.945312 L 11.367188 12.722656 Z M 15.429688 5.777344 C 15.429688 5.90625 15.351562 6.050781 15.195312 6.207031 L 11.957031 9.367188 L 12.722656 13.832031 C 12.730469 13.871094 12.730469 13.929688 12.730469 14.007812 C 12.730469 14.304688 12.609375 14.457031 12.367188 14.457031 C 12.253906 14.457031 12.132812 14.417969 12.007812 14.347656 L 8 12.242188 L 3.992188 14.347656 C 3.859375 14.417969 3.742188 14.457031 3.632812 14.457031 C 3.507812 14.457031 3.414062 14.414062 3.351562 14.324219 C 3.289062 14.238281 3.257812 14.132812 3.257812 14.007812 C 3.257812 13.972656 3.265625 13.914062 3.277344 13.832031 L 4.042969 9.367188 L 0.792969 6.207031 C 0.644531 6.042969 0.570312 5.902344 0.570312 5.777344 C 0.570312 5.554688 0.738281 5.417969 1.070312 5.367188 L 5.554688 4.714844 L 7.5625 0.652344 C 7.675781 0.40625 7.820312 0.285156 8 0.285156 C 8.179688 0.285156 8.324219 0.40625 8.4375 0.652344 L 10.445312 4.714844 L 14.929688 5.367188 C 15.261719 5.417969 15.429688 5.554688 15.429688 5.777344 Z M 15.429688 5.777344 "/>
+</g>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" preserveAspectRatio="xMinYMid meet">
+<g id="surface1">
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(60%,60%,60%);fill-opacity:1;" d="M 10.285156 8 C 10.285156 8.15625 10.230469 8.289062 10.117188 8.402344 L 6.117188 12.402344 C 6.003906 12.515625 5.867188 12.570312 5.714844 12.570312 C 5.558594 12.570312 5.425781 12.515625 5.3125 12.402344 C 5.199219 12.289062 5.144531 12.15625 5.144531 12 L 5.144531 4 C 5.144531 3.84375 5.199219 3.710938 5.3125 3.597656 C 5.425781 3.484375 5.558594 3.429688 5.714844 3.429688 C 5.867188 3.429688 6.003906 3.484375 6.117188 3.597656 L 10.117188 7.597656 C 10.230469 7.710938 10.285156 7.84375 10.285156 8 Z M 10.285156 8 "/>
+</g>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" preserveAspectRatio="xMinYMid meet">
+<g id="surface1">
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(60%,60%,60%);fill-opacity:1;" d="M 11.402344 11.597656 C 11.449219 11.710938 11.433594 11.816406 11.355469 11.910156 L 8.230469 15.339844 C 8.171875 15.398438 8.105469 15.429688 8.027344 15.429688 C 7.945312 15.429688 7.871094 15.398438 7.8125 15.339844 L 4.644531 11.910156 C 4.566406 11.816406 4.550781 11.710938 4.597656 11.597656 C 4.652344 11.484375 4.738281 11.429688 4.855469 11.429688 L 6.855469 11.429688 L 6.855469 0.285156 C 6.855469 0.203125 6.882812 0.132812 6.9375 0.0820312 C 6.992188 0.0273438 7.058594 0 7.144531 0 L 8.855469 0 C 8.941406 0 9.007812 0.0273438 9.0625 0.0820312 C 9.117188 0.132812 9.144531 0.203125 9.144531 0.285156 L 9.144531 11.429688 L 11.144531 11.429688 C 11.269531 11.429688 11.355469 11.484375 11.402344 11.597656 Z M 11.402344 11.597656 "/>
+</g>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" preserveAspectRatio="xMinYMid meet">
+<g id="surface1">
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(60%,60%,60%);fill-opacity:1;" d="M 11.402344 4.402344 C 11.347656 4.515625 11.261719 4.570312 11.144531 4.570312 L 9.144531 4.570312 L 9.144531 15.714844 C 9.144531 15.796875 9.117188 15.867188 9.0625 15.917969 C 9.007812 15.972656 8.941406 16 8.855469 16 L 7.144531 16 C 7.058594 16 6.992188 15.972656 6.9375 15.917969 C 6.882812 15.867188 6.855469 15.796875 6.855469 15.714844 L 6.855469 4.570312 L 4.855469 4.570312 C 4.730469 4.570312 4.644531 4.515625 4.597656 4.402344 C 4.550781 4.289062 4.566406 4.183594 4.644531 4.089844 L 7.769531 0.660156 C 7.828125 0.601562 7.894531 0.570312 7.972656 0.570312 C 8.054688 0.570312 8.128906 0.601562 8.1875 0.660156 L 11.355469 4.089844 C 11.433594 4.183594 11.449219 4.289062 11.402344 4.402344 Z M 11.402344 4.402344 "/>
+</g>
+</svg>
.que.ddimageortext div.droparea .dropzones {
position: absolute;
top: 0;
+ /*rtl:ignore*/
left: 0;
}
{{#tags}}
<li>
<a href="{{viewurl}}" class="{{#isstandard}}standardtag{{/isstandard}} s{{size}}"
- {{#count}}title="{{#str}}numberofentries, blog, {{count}}{{/str}}{{/count}}">
+ {{#count}}title="{{#str}}numberofentries, blog, {{count}}{{/str}}"{{/count}}>
{{#flag}}
- <span class="flagged-tag">{{name}}</span></a>
+ <span class="flagged-tag">{{name}}</span>
{{/flag}}
{{^flag}}
- {{name}}</a>
+ {{name}}
{{/flag}}
+ </a>
</li>
{{/tags}}
</ul>
$filemanageroptions = array('maxbytes' => $CFG->maxbytes,
'subdirs' => 0,
'maxfiles' => 1,
- 'accepted_types' => 'web_image');
+ 'accepted_types' => 'optimised_image');
file_prepare_draft_area($draftitemid, $filemanagercontext->id, 'user', 'newicon', 0, $filemanageroptions);
$user->imagefile = $draftitemid;
// Create form.
$filemanageroptions = array('maxbytes' => $CFG->maxbytes,
'subdirs' => 0,
'maxfiles' => 1,
- 'accepted_types' => 'web_image');
+ 'accepted_types' => 'optimised_image');
file_prepare_draft_area($draftitemid, $filemanagercontext->id, 'user', 'newicon', 0, $filemanageroptions);
$user->imagefile = $draftitemid;
// Create form.
$filemanageroptions = array('maxbytes' => $CFG->maxbytes,
'subdirs' => 0,
'maxfiles' => 1,
- 'accepted_types' => 'web_image');
+ 'accepted_types' => 'optimised_image');
$transaction = $DB->start_delegated_transaction();
throw new moodle_exception('noprofileedit', 'auth');
}
- $filemanageroptions = array('maxbytes' => $CFG->maxbytes, 'subdirs' => 0, 'maxfiles' => 1, 'accepted_types' => 'web_image');
+ $filemanageroptions = array(
+ 'maxbytes' => $CFG->maxbytes,
+ 'subdirs' => 0,
+ 'maxfiles' => 1,
+ 'accepted_types' => 'optimised_image'
+ );
$user->deletepicture = $params['delete'];
$user->imagefile = $params['draftitemid'];
$success = core_user::update_picture($user, $filemanageroptions);
When I follow "Profile" in the user menu
Then I should see "Gronya,Beecham" in the ".usermenu" "css_element"
And I should see "Gronya,Beecham" in the ".page-context-header" "css_element"
+ And I should see "You are logged in as Gronya,Beecham" in the "page-footer" "region"
And I log out
Scenario: As an admin, 'fullnamedisplay' should be used when using the 'log in as' function
When I navigate to "Users > Accounts > Browse list of users" in site administration
And I follow "Jane, Nina, Niamh, Cholmondely"
And I follow "Log in as"
- Then I should see "You are logged in as Nee,Chumlee"
+ Then I should see "You are logged in as Nee,Chumlee" in the ".usermenu" "css_element"
+ And I should see "You are logged in as Jane, Nina, Niamh, Cholmondely" in the "region-main" "region"
+ And I should see "You are logged in as Nee,Chumlee" in the "page-footer" "region"
And I log out
Scenario: As an admin, 'fullnamedisplay' should be used when viewing another user's site profile
defined('MOODLE_INTERNAL') || die();
-$version = 2020061501.06; // 20200615 = branching date YYYYMMDD - do not modify!
+$version = 2020061501.08; // 20200615 = branching date YYYYMMDD - do not modify!
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
-$release = '3.9.1+ (Build: 20200807)'; // Human-friendly version name
+$release = '3.9.1+ (Build: 20200814)'; // Human-friendly version name
$branch = '39'; // This version's branch.
$maturity = MATURITY_STABLE; // This version's maturity level.