theme/bootstrapbase/style/
theme/clean/style/custom.css
theme/more/style/custom.css
+theme/boost/style/moodle.css
node_modules/
vendor/
admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
}
}
},
+ sass: {
+ dist: {
+ files: {
+ "theme/boost/style/moodle.css": "theme/boost/scss/preset/default.scss"
+ }
+ },
+ options: {
+ includePaths: ["theme/boost/scss/"]
+ }
+ },
watch: {
options: {
nospawn: true // We need not to spawn so config can be changed dynamically.
'# Generated by "grunt ignorefiles"',
'theme/bootstrapbase/style/',
'theme/clean/style/custom.css',
- 'theme/more/style/custom.css'
+ 'theme/more/style/custom.css',
+ 'theme/boost/style/moodle.css'
].concat(thirdPartyPaths);
grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
};
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-contrib-watch');
+ grunt.loadNpmTasks('grunt-sass');
grunt.loadNpmTasks('grunt-eslint');
grunt.loadNpmTasks('grunt-stylelint');
grunt.registerTask('js', ['amd', 'yui']);
// Register CSS taks.
- grunt.registerTask('css', ['stylelint:scss', 'stylelint:less', 'less:bootstrapbase', 'stylelint:css']);
+ grunt.registerTask('css', ['stylelint:scss', 'sass', 'stylelint:less', 'less:bootstrapbase', 'stylelint:css']);
// Register the startup task.
grunt.registerTask('startup', 'Run the correct tasks for the current directory', tasks.startup);
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Form for selective purging of caches.
+ *
+ * @package core
+ * @copyright 2018 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_admin\form;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Form for selecting which caches to purge on admin/purgecaches.php
+ *
+ * @package core
+ * @copyright 2018 The Open University
+ * @author Mark Johnson <mark.johnson@open.ac.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class purge_caches extends \moodleform {
+ /**
+ * Define a "Purge all caches" button, and a fieldset with checkboxes for selectively purging separate caches.
+ */
+ public function definition() {
+ $mform = $this->_form;
+ $mform->addElement('hidden', 'returnurl', $this->_customdata['returnurl']);
+ $mform->setType('returnurl', PARAM_LOCALURL);
+ $mform->addElement('submit', 'all', get_string('purgecaches', 'admin'));
+ $mform->addElement('header', 'purgecacheheader', get_string('purgeselectedcaches', 'admin'));
+ $checkboxes = [
+ $mform->createElement('advcheckbox', 'theme', '', get_string('purgethemecache', 'admin')),
+ $mform->createElement('advcheckbox', 'lang', '', get_string('purgelangcache', 'admin')),
+ $mform->createElement('advcheckbox', 'js', '', get_string('purgejscache', 'admin')),
+ $mform->createElement('advcheckbox', 'filter', '', get_string('purgefiltercache', 'admin')),
+ $mform->createElement('advcheckbox', 'muc', '', get_string('purgemuc', 'admin')),
+ $mform->createElement('advcheckbox', 'other', '', get_string('purgeothercaches', 'admin'))
+ ];
+ $mform->addGroup($checkboxes, 'purgeselectedoptions');
+ $mform->addElement('submit', 'purgeselectedcaches', get_string('purgeselectedcaches', 'admin'));
+ }
+
+ /**
+ * If the "Purge selected caches" button was pressed, ensure at least one cache was selected.
+ *
+ * @param array $data
+ * @param array $files
+ * @return array Error messages
+ */
+ public function validation($data, $files) {
+ $errors = [];
+ if (isset($data['purgeselectedcaches']) && empty(array_filter($data['purgeselectedoptions']))) {
+ $errors['purgeselectedoptions'] = get_string('purgecachesnoneselected', 'admin');
+ }
+ return $errors;
+ }
+}
require(__DIR__.'/../../config.php');
require_once($CFG->libdir.'/clilib.php');
-list($options, $unrecognized) = cli_get_params(array('help' => false), array('h' => 'help'));
+$longoptions = [
+ 'help' => false,
+ 'muc' => false,
+ 'theme' => false,
+ 'lang' => false,
+ 'js' => false,
+ 'filter' => false,
+ 'other' => false
+];
+list($options, $unrecognized) = cli_get_params($longoptions, ['h' => 'help']);
if ($unrecognized) {
$unrecognized = implode("\n ", $unrecognized);
}
if ($options['help']) {
- $help =
-"Invalidates all Moodle internal caches
+ // The indentation of this string is "wrong" but this is to avoid a extra whitespace in console output.
+ $help = <<<EOF
+Invalidates Moodle internal caches
+
+Specific caches can be defined (alone or in combination) using arguments. If none are specified,
+all caches will be purged.
Options:
-h, --help Print out this help
+ --muc Purge all MUC caches (includes lang cache)
+ --theme Purge theme cache
+ --lang Purge language string cache
+ --js Purge JavaScript cache
+ --filter Purge text filter cache
+ --other Purge all file caches and other miscellaneous caches (may include MUC
+ if using cachestore_file).
Example:
-\$sudo -u www-data /usr/bin/php admin/cli/purge_caches.php
-";
+\$ sudo -u www-data /usr/bin/php admin/cli/purge_caches.php
+
+EOF;
echo $help;
exit(0);
}
-purge_all_caches();
+purge_caches(array_filter($options));
exit(0);
require_once($CFG->libdir.'/adminlib.php');
$confirm = optional_param('confirm', 0, PARAM_BOOL);
-$returnurl = optional_param('returnurl', null, PARAM_LOCALURL);
+$returnurl = optional_param('returnurl', '/admin/purgecaches.php', PARAM_LOCALURL);
+$returnurl = new moodle_url($returnurl);
admin_externalpage_setup('purgecaches');
+$form = new core_admin\form\purge_caches(null, ['returnurl' => $returnurl]);
+
// If we have got here as a confirmed aciton, do it.
-if ($confirm && confirm_sesskey()) {
+if ($data = $form->get_data()) {
// Valid request. Purge, and redirect the user back to where they came from.
- purge_all_caches();
+ $selected = $data->purgeselectedoptions;
+ purge_caches($selected);
- if ($returnurl) {
- $returnurl = $CFG->wwwroot . $returnurl;
+ if (isset($data->all)) {
+ $message = get_string('purgecachesfinished', 'admin');
} else {
- $returnurl = new moodle_url('/admin/purgecaches.php');
+ $message = get_string('purgeselectedcachesfinished', 'admin');
}
- redirect($returnurl, get_string('purgecachesfinished', 'admin'));
+
+} else if ($confirm && confirm_sesskey()) {
+ purge_caches();
+ $message = get_string('purgecachesfinished', 'admin');
}
-// Otherwise, show a button to actually purge the caches.
-$actionurl = new moodle_url('/admin/purgecaches.php', array('sesskey'=>sesskey(), 'confirm'=>1));
-if ($returnurl) {
- $actionurl->param('returnurl', $returnurl);
+if (isset($message)) {
+ redirect($returnurl, $message);
}
+// Otherwise, show a form to actually purge the caches.
+
echo $OUTPUT->header();
-echo $OUTPUT->heading(get_string('purgecaches', 'admin'));
+echo $OUTPUT->heading(get_string('purgecachespage', 'admin'));
echo $OUTPUT->box_start('generalbox', 'notice');
echo html_writer::tag('p', get_string('purgecachesconfirm', 'admin'));
-echo $OUTPUT->single_button($actionurl, get_string('purgecaches', 'admin'), 'post');
+echo $form->render();
echo $OUTPUT->box_end();
echo $OUTPUT->footer();
$ADMIN->add('development', new admin_externalpage('mnettestclient', new lang_string('testclient', 'mnet'), "$CFG->wwwroot/$CFG->admin/mnet/testclient.php"));
}
- $ADMIN->add('development', new admin_externalpage('purgecaches', new lang_string('purgecaches','admin'), "$CFG->wwwroot/$CFG->admin/purgecaches.php"));
+ $ADMIN->add('development', new admin_externalpage('purgecaches', new lang_string('purgecachespage', 'admin'),
+ "$CFG->wwwroot/$CFG->admin/purgecaches.php"));
$ADMIN->add('development', new admin_externalpage('thirdpartylibs', new lang_string('thirdpartylibs','admin'), "$CFG->wwwroot/$CFG->admin/thirdpartylibs.php"));
} // end of speedup
--- /dev/null
+@core @core_admin
+Feature: Purge caches
+ In order to see changes to cached data
+ As a Moodle administrator
+ I want manually purge different data and file caches
+
+ Background:
+ Given I log in as "admin"
+ And I navigate to "Development > Purge caches" in site administration
+
+ Scenario: Purge all caches
+ Given I should not see "All caches were purged"
+ When I press "Purge all caches"
+ Then I should see "All caches were purged"
+
+ Scenario: Purge selected caches
+ Given I should not see "Selected caches were purged"
+ When I set the field "Themes" to "1"
+ And I press "Purge selected caches"
+ Then I should see "The selected caches were purged"
+
+ Scenario: Purge selected caches without selecting any caches
+ Given I should not see "Select one or more caches to purge"
+ When I press "Purge selected caches"
+ Then I should not see "The selected caches were purged"
+ And I should see "Select one or more caches to purge"
+
+ Scenario: Redirect back to the original page after following a Purge all caches link
+ Given I am on site homepage
+ And I should see "Available courses"
+ And I should not see "All caches were purged"
+ When I follow "Purge all caches"
+ Then I should see "All caches were purged"
+ And I should see "Available courses"
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * JS module for the data requests filter.
+ *
+ * @module tool_dataprivacy/request_filter
+ * @package tool_dataprivacy
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'], function($, Autocomplete, Str, Notification) {
+
+ /**
+ * Selectors.
+ *
+ * @access private
+ * @type {{REQUEST_FILTERS: string}}
+ */
+ var SELECTORS = {
+ REQUEST_FILTERS: '#request-filters'
+ };
+
+ /**
+ * Init function.
+ *
+ * @method init
+ * @private
+ */
+ var init = function() {
+ var stringkeys = [
+ {
+ key: 'filter',
+ component: 'moodle'
+ },
+ {
+ key: 'nofiltersapplied',
+ component: 'moodle'
+ }
+ ];
+
+ Str.get_strings(stringkeys).then(function(langstrings) {
+ var placeholder = langstrings[0];
+ var noSelectionString = langstrings[1];
+ return Autocomplete.enhance(SELECTORS.REQUEST_FILTERS, false, '', placeholder, false, true, noSelectionString, true);
+ }).fail(Notification.exception);
+
+ var last = $(SELECTORS.REQUEST_FILTERS).val();
+ $(SELECTORS.REQUEST_FILTERS).on('change', function() {
+ var current = $(this).val();
+ // Prevent form from submitting unnecessarily, eg. on blur when no filter is selected.
+ if (last.join(',') !== current.join(',')) {
+ // If we're submitting without filters, set the hidden input 'filters-cleared' to 1.
+ if (current.length === 0) {
+ $('#filters-cleared').val(1);
+ }
+ $(this.form).submit();
+ }
+ });
+ };
+
+ return /** @alias module:core/form-autocomplete */ {
+ /**
+ * Initialise the unified user filter.
+ *
+ * @method init
+ */
+ init: function() {
+ init();
+ }
+ };
+});
* (e.g. Users with the Data Protection Officer roles)
*
* @param int $userid The User ID.
+ * @param int[] $statuses The status filters.
+ * @param int[] $types The request type filters.
+ * @param string $sort The order by clause.
+ * @param int $offset Amount of records to skip.
+ * @param int $limit Amount of records to fetch.
* @return data_request[]
+ * @throws coding_exception
* @throws dml_exception
*/
- public static function get_data_requests($userid = 0) {
+ public static function get_data_requests($userid = 0, $statuses = [], $types = [], $sort = '', $offset = 0, $limit = 0) {
global $DB, $USER;
$results = [];
- $sort = 'status ASC, timemodified ASC';
+ $sqlparams = [];
+ $sqlconditions = [];
+
+ // Set default sort.
+ if (empty($sort)) {
+ $sort = 'status ASC, timemodified ASC';
+ }
+
+ // Set status filters.
+ if (!empty($statuses)) {
+ list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
+ $sqlconditions[] = "status $statusinsql";
+ }
+
+ // Set request type filter.
+ if (!empty($types)) {
+ list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
+ $sqlconditions[] = "type $typeinsql";
+ $sqlparams = array_merge($sqlparams, $typeparams);
+ }
+
if ($userid) {
// Get the data requests for the user or data requests made by the user.
- $select = "(userid = :userid OR requestedby = :requestedby)";
+ $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
$params = [
'userid' => $userid,
'requestedby' => $userid
$alloweduserids = array_merge($alloweduserids, array_keys($children));
}
list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
- $select .= " AND userid $insql";
- $params = array_merge($params, $inparams);
+ $sqlconditions[] .= "userid $insql";
+ $select = implode(' AND ', $sqlconditions);
+ $params = array_merge($params, $inparams, $sqlparams);
- $results = data_request::get_records_select($select, $params, $sort);
+ $results = data_request::get_records_select($select, $params, $sort, '*', $offset, $limit);
} else {
// If the current user is one of the site's Data Protection Officers, then fetch all data requests.
if (self::is_site_dpo($USER->id)) {
- $results = data_request::get_records(null, $sort, '');
+ if (!empty($sqlconditions)) {
+ $select = implode(' AND ', $sqlconditions);
+ $results = data_request::get_records_select($select, $sqlparams, $sort, '*', $offset, $limit);
+ } else {
+ $results = data_request::get_records(null, $sort, '', $offset, $limit);
+ }
}
}
return $results;
}
+ /**
+ * Fetches the count of data request records based on the given parameters.
+ *
+ * @param int $userid The User ID.
+ * @param int[] $statuses The status filters.
+ * @param int[] $types The request type filters.
+ * @return int
+ * @throws coding_exception
+ * @throws dml_exception
+ */
+ public static function get_data_requests_count($userid = 0, $statuses = [], $types = []) {
+ global $DB, $USER;
+ $count = 0;
+ $sqlparams = [];
+ $sqlconditions = [];
+ if (!empty($statuses)) {
+ list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
+ $sqlconditions[] = "status $statusinsql";
+ }
+ if (!empty($types)) {
+ list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
+ $sqlconditions[] = "type $typeinsql";
+ $sqlparams = array_merge($sqlparams, $typeparams);
+ }
+ if ($userid) {
+ // Get the data requests for the user or data requests made by the user.
+ $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
+ $params = [
+ 'userid' => $userid,
+ 'requestedby' => $userid
+ ];
+
+ // Build a list of user IDs that the user is allowed to make data requests for.
+ // Of course, the user should be included in this list.
+ $alloweduserids = [$userid];
+ // Get any users that the user can make data requests for.
+ if ($children = helper::get_children_of_user($userid)) {
+ // Get the list of user IDs of the children and merge to the allowed user IDs.
+ $alloweduserids = array_merge($alloweduserids, array_keys($children));
+ }
+ list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
+ $sqlconditions[] .= "userid $insql";
+ $select = implode(' AND ', $sqlconditions);
+ $params = array_merge($params, $inparams, $sqlparams);
+
+ $count = data_request::count_records_select($select, $params);
+ } else {
+ // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
+ if (self::is_site_dpo($USER->id)) {
+ if (!empty($sqlconditions)) {
+ $select = implode(' AND ', $sqlconditions);
+ $count = data_request::count_records_select($select, $sqlparams);
+ } else {
+ $count = data_request::count_records();
+ }
+ }
+ }
+
+ return $count;
+ }
+
/**
* Checks whether there is already an existing pending/in-progress data request for a user for a given request type.
*
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helper {
+ /** The default number of results to be shown per page. */
+ const DEFAULT_PAGE_SIZE = 20;
+
+ /** Filter constant associated with the request type filter. */
+ const FILTER_TYPE = 1;
+
+ /** Filter constant associated with the request status filter. */
+ const FILTER_STATUS = 2;
+
+ /** The request filters preference key. */
+ const PREF_REQUEST_FILTERS = 'tool_dataprivacy_request-filters';
/**
* Retrieves the human-readable text value of a data request type.
* @throws moodle_exception
*/
public static function get_request_type_string($requesttype) {
- switch ($requesttype) {
- case api::DATAREQUEST_TYPE_EXPORT:
- return get_string('requesttypeexport', 'tool_dataprivacy');
- case api::DATAREQUEST_TYPE_DELETE:
- return get_string('requesttypedelete', 'tool_dataprivacy');
- case api::DATAREQUEST_TYPE_OTHERS:
- return get_string('requesttypeothers', 'tool_dataprivacy');
- default:
- throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
+ $types = self::get_request_types();
+ if (!isset($types[$requesttype])) {
+ throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
}
+ return $types[$requesttype];
}
/**
* @throws moodle_exception
*/
public static function get_shortened_request_type_string($requesttype) {
- switch ($requesttype) {
- case api::DATAREQUEST_TYPE_EXPORT:
- return get_string('requesttypeexportshort', 'tool_dataprivacy');
- case api::DATAREQUEST_TYPE_DELETE:
- return get_string('requesttypedeleteshort', 'tool_dataprivacy');
- case api::DATAREQUEST_TYPE_OTHERS:
- return get_string('requesttypeothersshort', 'tool_dataprivacy');
- default:
- throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
+ $types = self::get_request_types_short();
+ if (!isset($types[$requesttype])) {
+ throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
}
+ return $types[$requesttype];
+ }
+
+ /**
+ * Returns the key value-pairs of request type code and their string value.
+ *
+ * @return array
+ */
+ public static function get_request_types() {
+ return [
+ api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexport', 'tool_dataprivacy'),
+ api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedelete', 'tool_dataprivacy'),
+ api::DATAREQUEST_TYPE_OTHERS => get_string('requesttypeothers', 'tool_dataprivacy'),
+ ];
+ }
+
+ /**
+ * Returns the key value-pairs of request type code and their shortened string value.
+ *
+ * @return array
+ */
+ public static function get_request_types_short() {
+ return [
+ api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexportshort', 'tool_dataprivacy'),
+ api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedeleteshort', 'tool_dataprivacy'),
+ api::DATAREQUEST_TYPE_OTHERS => get_string('requesttypeothersshort', 'tool_dataprivacy'),
+ ];
}
/**
*
* @param int $status The request status.
* @return string
- * @throws coding_exception
* @throws moodle_exception
*/
public static function get_request_status_string($status) {
- switch ($status) {
- case api::DATAREQUEST_STATUS_PENDING:
- return get_string('statuspending', 'tool_dataprivacy');
- case api::DATAREQUEST_STATUS_PREPROCESSING:
- return get_string('statuspreprocessing', 'tool_dataprivacy');
- case api::DATAREQUEST_STATUS_AWAITING_APPROVAL:
- return get_string('statusawaitingapproval', 'tool_dataprivacy');
- case api::DATAREQUEST_STATUS_APPROVED:
- return get_string('statusapproved', 'tool_dataprivacy');
- case api::DATAREQUEST_STATUS_PROCESSING:
- return get_string('statusprocessing', 'tool_dataprivacy');
- case api::DATAREQUEST_STATUS_COMPLETE:
- return get_string('statuscomplete', 'tool_dataprivacy');
- case api::DATAREQUEST_STATUS_CANCELLED:
- return get_string('statuscancelled', 'tool_dataprivacy');
- case api::DATAREQUEST_STATUS_REJECTED:
- return get_string('statusrejected', 'tool_dataprivacy');
- default:
- throw new moodle_exception('errorinvalidrequeststatus', 'tool_dataprivacy');
+ $statuses = self::get_request_statuses();
+ if (!isset($statuses[$status])) {
+ throw new moodle_exception('errorinvalidrequeststatus', 'tool_dataprivacy');
}
+ return $statuses[$status];
+ }
+
+ /**
+ * Returns the key value-pairs of request status code and string value.
+ *
+ * @return array
+ */
+ public static function get_request_statuses() {
+ return [
+ api::DATAREQUEST_STATUS_PENDING => get_string('statuspending', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_PREPROCESSING => get_string('statuspreprocessing', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_AWAITING_APPROVAL => get_string('statusawaitingapproval', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_APPROVED => get_string('statusapproved', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_PROCESSING => get_string('statusprocessing', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_COMPLETE => get_string('statuscomplete', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_CANCELLED => get_string('statuscancelled', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_REJECTED => get_string('statusrejected', 'tool_dataprivacy'),
+ ];
}
/**
}
return $finalresults;
}
+
+ /**
+ * Get options for the data requests filter.
+ *
+ * @return array
+ * @throws coding_exception
+ */
+ public static function get_request_filter_options() {
+ $filters = [
+ self::FILTER_TYPE => (object)[
+ 'name' => get_string('requesttype', 'tool_dataprivacy'),
+ 'options' => self::get_request_types_short()
+ ],
+ self::FILTER_STATUS => (object)[
+ 'name' => get_string('requeststatus', 'tool_dataprivacy'),
+ 'options' => self::get_request_statuses()
+ ],
+ ];
+ $options = [];
+ foreach ($filters as $category => $filtercategory) {
+ foreach ($filtercategory->options as $key => $name) {
+ $option = (object)[
+ 'category' => $filtercategory->name,
+ 'name' => $name
+ ];
+ $options["{$category}:{$key}"] = get_string('filteroption', 'tool_dataprivacy', $option);
+ }
+ }
+ return $options;
+ }
}
defined('MOODLE_INTERNAL') || die();
use coding_exception;
-use dml_exception;
use moodle_exception;
use moodle_url;
use renderable;
use stdClass;
use templatable;
use tool_dataprivacy\data_request;
-use tool_dataprivacy\output\expired_contexts_table;
+use tool_dataprivacy\local\helper;
/**
* Class containing data for a user's data requests.
*/
class data_deletion_page implements renderable, templatable {
- /** The default number of results to be shown per page. */
- const DEFAULT_PAGE_SIZE = 20;
-
/** @var data_request[] $requests List of data requests. */
protected $filter = null;
* Construct this renderable.
*
* @param \tool_dataprivacy\data_request[] $filter
- * @param \tool_dataprivacy\expired_contexts_table $expiredcontextstable
+ * @param expired_contexts_table $expiredcontextstable
*/
public function __construct($filter, expired_contexts_table $expiredcontextstable) {
$this->filter = $filter;
* @param renderer_base $output
* @return stdClass
* @throws coding_exception
- * @throws dml_exception
* @throws moodle_exception
*/
public function export_for_template(renderer_base $output) {
$data->filter = $filterselector->export_for_template($output);
ob_start();
- $this->expiredcontextstable->out(self::DEFAULT_PAGE_SIZE, true);
+ $this->expiredcontextstable->out(helper::DEFAULT_PAGE_SIZE, true);
$expiredcontexts = ob_get_contents();
ob_end_clean();
$data->expiredcontexts = $expiredcontexts;
namespace tool_dataprivacy\output;
defined('MOODLE_INTERNAL') || die();
-use action_menu;
-use action_menu_link_secondary;
use coding_exception;
-use context_system;
use dml_exception;
use moodle_exception;
use moodle_url;
use renderable;
use renderer_base;
+use single_select;
use stdClass;
use templatable;
use tool_dataprivacy\api;
-use tool_dataprivacy\data_request;
-use tool_dataprivacy\external\data_request_exporter;
+use tool_dataprivacy\local\helper;
/**
* Class containing data for a user's data requests.
*/
class data_requests_page implements renderable, templatable {
- /** @var data_request[] $requests List of data requests. */
- protected $requests = [];
+ /** @var data_requests_table $table The data requests table. */
+ protected $table;
+
+ /** @var int[] $filters The applied filters. */
+ protected $filters = [];
/**
* Construct this renderable.
*
- * @param data_request[] $requests
+ * @param data_requests_table $table The data requests table.
+ * @param int[] $filters The applied filters.
*/
- public function __construct($requests) {
- $this->requests = $requests;
+ public function __construct($table, $filters) {
+ $this->table = $table;
+ $this->filters = $filters;
}
/**
$data->httpsite = array('message' => $httpwarningmessage, 'announce' => 1);
}
- $requests = [];
- foreach ($this->requests as $request) {
- $requestid = $request->get('id');
- $status = $request->get('status');
- $requestexporter = new data_request_exporter($request, ['context' => context_system::instance()]);
- $item = $requestexporter->export($output);
-
- // Prepare actions.
- $actions = [];
-
- // View action.
- $actionurl = new moodle_url('#');
- $actiondata = ['data-action' => 'view', 'data-requestid' => $requestid];
- $actiontext = get_string('viewrequest', 'tool_dataprivacy');
- $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+ $url = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
+ $filteroptions = helper::get_request_filter_options();
+ $filter = new request_filter($filteroptions, $this->filters, $url);
+ $data->filter = $filter->export_for_template($output);
- if ($status == api::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
- // Approve.
- $actiondata['data-action'] = 'approve';
- $actiontext = get_string('approverequest', 'tool_dataprivacy');
- $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+ ob_start();
+ $this->table->out(helper::DEFAULT_PAGE_SIZE, true);
+ $requests = ob_get_contents();
+ ob_end_clean();
- // Deny.
- $actiondata['data-action'] = 'deny';
- $actiontext = get_string('denyrequest', 'tool_dataprivacy');
- $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
- }
-
- $actionsmenu = new action_menu($actions);
- $actionsmenu->set_menu_trigger(get_string('actions'));
- $actionsmenu->set_owner_selector('request-actions-' . $requestid);
- $actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
- $item->actions = $actionsmenu->export_for_template($output);
-
- $requests[] = $item;
- }
- $data->requests = $requests;
+ $data->datarequests = $requests;
return $data;
}
}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains the class used for the displaying the data requests table.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Jun Pataleta <jun@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy\output;
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/tablelib.php');
+
+use action_menu;
+use action_menu_link_secondary;
+use coding_exception;
+use dml_exception;
+use html_writer;
+use moodle_url;
+use stdClass;
+use table_sql;
+use tool_dataprivacy\api;
+use tool_dataprivacy\external\data_request_exporter;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * The class for displaying the data requests table.
+ *
+ * @copyright 2018 Jun Pataleta <jun@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class data_requests_table extends table_sql {
+
+ /** @var int The user ID. */
+ protected $userid = 0;
+
+ /** @var int[] The status filters. */
+ protected $statuses = [];
+
+ /** @var int[] The request type filters. */
+ protected $types = [];
+
+ /** @var bool Whether this table is being rendered for managing data requests. */
+ protected $manage = false;
+
+ /** @var stdClass[] Array of data request persistents. */
+ protected $datarequests = [];
+
+ /**
+ * data_requests_table constructor.
+ *
+ * @param int $userid The user ID
+ * @param int[] $statuses
+ * @param int[] $types
+ * @param bool $manage
+ * @throws coding_exception
+ */
+ public function __construct($userid = 0, $statuses = [], $types = [], $manage = false) {
+ parent::__construct('data-requests-table');
+
+ $this->userid = $userid;
+ $this->statuses = $statuses;
+ $this->types = $types;
+ $this->manage = $manage;
+
+ $columnheaders = [
+ 'type' => get_string('requesttype', 'tool_dataprivacy'),
+ 'userid' => get_string('user', 'tool_dataprivacy'),
+ 'timecreated' => get_string('daterequested', 'tool_dataprivacy'),
+ 'requestedby' => get_string('requestby', 'tool_dataprivacy'),
+ 'status' => get_string('requeststatus', 'tool_dataprivacy'),
+ 'comments' => get_string('message', 'tool_dataprivacy'),
+ 'actions' => '',
+ ];
+
+ $this->define_columns(array_keys($columnheaders));
+ $this->define_headers(array_values($columnheaders));
+ $this->no_sorting('actions');
+ }
+
+ /**
+ * The type column.
+ *
+ * @param stdClass $data The row data.
+ * @return string
+ */
+ public function col_type($data) {
+ if ($this->manage) {
+ return $data->typenameshort;
+ }
+ return $data->typename;
+ }
+
+ /**
+ * The user column.
+ *
+ * @param stdClass $data The row data.
+ * @return mixed
+ */
+ public function col_userid($data) {
+ $user = $data->foruser;
+ return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]);
+ }
+
+ /**
+ * The context information column.
+ *
+ * @param stdClass $data The row data.
+ * @return string
+ */
+ public function col_timecreated($data) {
+ return userdate($data->timecreated);
+ }
+
+ /**
+ * The requesting user's column.
+ *
+ * @param stdClass $data The row data.
+ * @return mixed
+ */
+ public function col_requestedby($data) {
+ $user = $data->requestedbyuser;
+ return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]);
+ }
+
+ /**
+ * The status column.
+ *
+ * @param stdClass $data The row data.
+ * @return mixed
+ */
+ public function col_status($data) {
+ return html_writer::span($data->statuslabel, 'label ' . $data->statuslabelclass);
+ }
+
+ /**
+ * The comments column.
+ *
+ * @param stdClass $data The row data.
+ * @return string
+ */
+ public function col_comments($data) {
+ return shorten_text($data->comments, 60);
+ }
+
+ /**
+ * The actions column.
+ *
+ * @param stdClass $data The row data.
+ * @return string
+ */
+ public function col_actions($data) {
+ global $OUTPUT;
+
+ $requestid = $data->id;
+ $status = $data->status;
+
+ // Prepare actions.
+ $actions = [];
+
+ // View action.
+ $actionurl = new moodle_url('#');
+ $actiondata = ['data-action' => 'view', 'data-requestid' => $requestid];
+ $actiontext = get_string('viewrequest', 'tool_dataprivacy');
+ $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+
+ if ($status == api::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
+ // Approve.
+ $actiondata['data-action'] = 'approve';
+ $actiontext = get_string('approverequest', 'tool_dataprivacy');
+ $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+
+ // Deny.
+ $actiondata['data-action'] = 'deny';
+ $actiontext = get_string('denyrequest', 'tool_dataprivacy');
+ $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+ }
+
+ $actionsmenu = new action_menu($actions);
+ $actionsmenu->set_menu_trigger(get_string('actions'));
+ $actionsmenu->set_owner_selector('request-actions-' . $requestid);
+ $actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
+
+ return $OUTPUT->render($actionsmenu);
+ }
+
+ /**
+ * Query the database for results to display in the table.
+ *
+ * @param int $pagesize size of page for paginated displayed table.
+ * @param bool $useinitialsbar do you want to use the initials bar.
+ * @throws dml_exception
+ * @throws coding_exception
+ */
+ public function query_db($pagesize, $useinitialsbar = true) {
+ global $PAGE;
+
+ // Count data requests from the given conditions.
+ $total = api::get_data_requests_count($this->userid, $this->statuses, $this->types);
+ $this->pagesize($pagesize, $total);
+
+ $sort = $this->get_sql_sort();
+
+ // Get data requests from the given conditions.
+ $datarequests = api::get_data_requests($this->userid, $this->statuses, $this->types, $sort,
+ $this->get_page_start(), $this->get_page_size());
+ $this->rawdata = [];
+ $context = \context_system::instance();
+ $renderer = $PAGE->get_renderer('tool_dataprivacy');
+ foreach ($datarequests as $persistent) {
+ $exporter = new data_request_exporter($persistent, ['context' => $context]);
+ $this->rawdata[] = $exporter->export($renderer);
+ }
+
+ // Set initial bars.
+ if ($useinitialsbar) {
+ $this->initialbars($total > $pagesize);
+ }
+ }
+
+ /**
+ * Override default implementation to display a more meaningful information to the user.
+ */
+ public function print_nothing_to_display() {
+ global $OUTPUT;
+ echo $this->render_reset_button();
+ $this->print_initials_bar();
+ if (!empty($this->statuses) || !empty($this->types)) {
+ $message = get_string('nodatarequestsmatchingfilter', 'tool_dataprivacy');
+ } else {
+ $message = get_string('nodatarequests', 'tool_dataprivacy');
+ }
+ echo $OUTPUT->notification($message, 'warning');
+ }
+
+ /**
+ * Override the table's show_hide_link method to prevent the show/hide links from rendering.
+ *
+ * @param string $column the column name, index into various names.
+ * @param int $index numerical index of the column.
+ * @return string HTML fragment.
+ */
+ protected function show_hide_link($column, $index) {
+ return '';
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class containing the filter options data for rendering the autocomplete element for the data requests page.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy\output;
+
+use moodle_url;
+use renderable;
+use renderer_base;
+use stdClass;
+use templatable;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class containing the filter options data for rendering the autocomplete element for the data requests page.
+ *
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class request_filter implements renderable, templatable {
+
+ /** @var array $filteroptions The filter options. */
+ protected $filteroptions;
+
+ /** @var array $selectedoptions The list of selected filter option values. */
+ protected $selectedoptions;
+
+ /** @var moodle_url|string $baseurl The url with params needed to call up this page. */
+ protected $baseurl;
+
+ /**
+ * request_filter constructor.
+ *
+ * @param array $filteroptions The filter options.
+ * @param array $selectedoptions The list of selected filter option values.
+ * @param string|moodle_url $baseurl The url with params needed to call up this page.
+ */
+ public function __construct($filteroptions, $selectedoptions, $baseurl = null) {
+ $this->filteroptions = $filteroptions;
+ $this->selectedoptions = $selectedoptions;
+ if (!empty($baseurl)) {
+ $this->baseurl = new moodle_url($baseurl);
+ }
+ }
+
+ /**
+ * Function to export the renderer data in a format that is suitable for a mustache template.
+ *
+ * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
+ * @return stdClass|array
+ */
+ public function export_for_template(renderer_base $output) {
+ global $PAGE;
+ $data = new stdClass();
+ if (empty($this->baseurl)) {
+ $this->baseurl = $PAGE->url;
+ }
+ $data->action = $this->baseurl->out(false);
+
+ foreach ($this->selectedoptions as $option) {
+ if (!isset($this->filteroptions[$option])) {
+ $this->filteroptions[$option] = $option;
+ }
+ }
+
+ $data->filteroptions = [];
+ foreach ($this->filteroptions as $value => $label) {
+ $selected = in_array($value, $this->selectedoptions);
+ $filteroption = (object)[
+ 'value' => $value,
+ 'label' => $label
+ ];
+ $filteroption->selected = $selected;
+ $data->filteroptions[] = $filteroption;
+ }
+ return $data;
+ }
+}
\core_privacy\local\metadata\provider,
// This tool may provide access to and deletion of user data.
- \core_privacy\local\request\plugin\provider {
+ \core_privacy\local\request\plugin\provider,
+
+ // This plugin has some sitewide user preferences to export.
+ \core_privacy\local\request\user_preference_provider {
/**
* Returns meta data about this system.
*
],
'privacy:metadata:request'
);
+
+ $collection->add_user_preference(tool_helper::PREF_REQUEST_FILTERS,
+ 'privacy:metadata:preference:tool_dataprivacy_request-filters');
+
return $collection;
}
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
}
+
+ /**
+ * Export all user preferences for the plugin.
+ *
+ * @param int $userid The userid of the user whose data is to be exported.
+ */
+ public static function export_user_preferences(int $userid) {
+ $preffilter = get_user_preferences(tool_helper::PREF_REQUEST_FILTERS, null, $userid);
+ if ($preffilter !== null) {
+ $filters = json_decode($preffilter);
+ $descriptions = [];
+ foreach ($filters as $filter) {
+ list($category, $value) = explode(':', $filter);
+ $option = new stdClass();
+ switch($category) {
+ case tool_helper::FILTER_TYPE:
+ $option->category = get_string('requesttype', 'tool_dataprivacy');
+ $option->name = tool_helper::get_shortened_request_type_string($value);
+ break;
+ case tool_helper::FILTER_STATUS:
+ $option->category = get_string('requeststatus', 'tool_dataprivacy');
+ $option->name = tool_helper::get_request_status_string($value);
+ break;
+ }
+ $descriptions[] = get_string('filteroption', 'tool_dataprivacy', $option);
+ }
+ // Export the filter preference as comma-separated values and text descriptions.
+ $values = implode(', ', $filters);
+ $descriptionstext = implode(', ', $descriptions);
+ writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_FILTERS, $values, $descriptionstext);
+ }
+ }
}
echo $OUTPUT->header();
echo $OUTPUT->heading($title);
-$requests = tool_dataprivacy\api::get_data_requests();
-$requestlist = new tool_dataprivacy\output\data_requests_page($requests);
+$filtersapplied = optional_param_array('request-filters', [-1], PARAM_NOTAGS);
+$filterscleared = optional_param('filters-cleared', 0, PARAM_INT);
+if ($filtersapplied === [-1]) {
+ // If there are no filters submitted, check if there is a saved filters from the user preferences.
+ $filterprefs = get_user_preferences(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, null);
+ if ($filterprefs && empty($filterscleared)) {
+ $filtersapplied = json_decode($filterprefs);
+ } else {
+ $filtersapplied = [];
+ }
+}
+// Save the current applied filters to the user preferences.
+set_user_preference(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, json_encode($filtersapplied));
+
+$types = [];
+$statuses = [];
+foreach ($filtersapplied as $filter) {
+ list($category, $value) = explode(':', $filter);
+ switch($category) {
+ case \tool_dataprivacy\local\helper::FILTER_TYPE:
+ $types[] = $value;
+ break;
+ case \tool_dataprivacy\local\helper::FILTER_STATUS:
+ $statuses[] = $value;
+ break;
+ }
+}
+
+$table = new \tool_dataprivacy\output\data_requests_table(0, $statuses, $types, true);
+$table->baseurl = $url;
+
+$requestlist = new tool_dataprivacy\output\data_requests_page($table, $filtersapplied);
$requestlistoutput = $PAGE->get_renderer('tool_dataprivacy');
echo $requestlistoutput->render($requestlist);
$string['confirmapproval'] = 'Do you really want to approve this data request?';
$string['confirmcontextdeletion'] = 'Do you really want to confirm the deletion of the selected contexts? This will also delete all of the user data for their respective sub-contexts.';
$string['confirmdenial'] = 'Do you really want deny this data request?';
-$string['contactdataprotectionofficer'] = 'Contact Data Protection Officer';
-$string['contactdataprotectionofficer_desc'] = 'If enabled, users will be able to contact the Data Protection Officer and make a data request via a link on their profile page.';
+$string['contactdataprotectionofficer'] = 'Contact the privacy officer';
+$string['contactdataprotectionofficer_desc'] = 'If enabled, users will be able to contact the privacy officer and make a data request via a link on their profile page.';
$string['contextlevelname10'] = 'Site';
$string['contextlevelname30'] = 'Users';
$string['contextlevelname40'] = 'Course categories';
$string['contextlevelname70'] = 'Activity modules';
$string['contextlevelname80'] = 'Blocks';
$string['contextpurposecategorysaved'] = 'Purpose and category saved.';
-$string['contactdpoviaprivacypolicy'] = 'Please contact the Data Protection Officer as described in the privacy policy.';
+$string['contactdpoviaprivacypolicy'] = 'Please contact the privacy officer as described in the privacy policy.';
$string['createcategory'] = 'Create data category';
$string['createpurpose'] = 'Create data purpose';
$string['datadeletion'] = 'Data deletion';
$string['deny'] = 'Deny';
$string['denyrequest'] = 'Deny request';
$string['download'] = 'Download';
-$string['dporolemapping'] = 'Data Protection Officer role mapping';
-$string['dporolemapping_desc'] = 'The Data Protection Officer can manage data requests. The capability tool/dataprivacy:managedatarequests must be allowed for a role to be listed as a Data Protection Officer role mapping option.';
+$string['dporolemapping'] = 'Privacy officer role mapping';
+$string['dporolemapping_desc'] = 'The privacy officer can manage data requests. The capability tool/dataprivacy:managedatarequests must be allowed for a role to be listed as a privacy officer role mapping option.';
$string['editcategories'] = 'Edit categories';
$string['editcategory'] = 'Edit category';
$string['editcategories'] = 'Edit categories';
$string['errorrequestnotfound'] = 'Request not found';
$string['errorrequestnotwaitingforapproval'] = 'The request is not awaiting approval. Either it is not yet ready or it has already been processed.';
$string['errorsendingmessagetodpo'] = 'An error was encountered while trying to send a message to {$a}.';
-$string['exceptionnotificationsubject'] = "Exception occured while processing privacy data";
-$string['exceptionnotificationbody'] = "<p>Exception occured while calling <b>{\$a->fullmethodname}</b>.<br>This means that plugin <b>{\$a->component}</b> did not complete processing data. Below you can find exception information that can be passed to the plugin developer.</p><pre>{\$a->message}<br>\n\n{\$a->backtrace}</pre>";
+$string['exceptionnotificationsubject'] = 'Exception occurred while processing privacy data';
+$string['exceptionnotificationbody'] = '<p>Exception occurred while calling <b>{$a->fullmethodname}</b>.<br>This means that plugin <b>{$a->component}</b> did not complete the processing of data. The following exception information may be passed on to the plugin developer:</p><pre>{$a->message}<br>
+
+{$a->backtrace}</pre>';
$string['expiredretentionperiodtask'] = 'Expired retention period';
$string['expiry'] = 'Expiry';
$string['expandplugin'] = 'Expand and collapse plugin.';
$string['explanationtitle'] = 'Icons used on this page and what they mean.';
$string['external'] = 'Additional';
$string['externalexplanation'] = 'An additional plugin installed on this site.';
+$string['filteroption'] = '{$a->category}: {$a->name}';
$string['frontpagecourse'] = 'Front page course';
$string['gdpr_art_6_1_a_description'] = 'The data subject has given consent to the processing of his or her personal data for one or more specific purposes';
$string['gdpr_art_6_1_a_name'] = 'Consent (GDPR Art. 6.1(a))';
$string['nchildren'] = '{$a} children';
$string['newrequest'] = 'New request';
$string['nodatarequests'] = 'There are no data requests';
+$string['nodatarequestsmatchingfilter'] = 'There are no data requests matching the given filter';
$string['noactivitiestoload'] = 'No activities';
$string['noassignedroles'] = 'No assigned roles in this context';
$string['noblockstoload'] = 'No blocks';
$string['pluginregistry'] = 'Plugin privacy registry';
$string['pluginregistrytitle'] = 'Plugin privacy compliance registry';
$string['privacy'] = 'Privacy';
+$string['privacy:metadata:preference:tool_dataprivacy_request-filters'] = 'The filters currently applied to the data requests page.';
$string['privacy:metadata:request'] = 'Information from personal data requests (subject access and deletion requests) made for this site.';
$string['privacy:metadata:request:comments'] = 'Any user comments accompanying the request.';
$string['privacy:metadata:request:userid'] = 'The ID of the user to whom the request belongs';
$string['privacy:metadata:request:requestedby'] = 'The ID of the user making the request, if made on behalf of another user.';
-$string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s Data Protection Officer regarding the request.';
+$string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s privacy officer regarding the request.';
$string['privacy:metadata:request:timecreated'] = 'The timestamp indicating when the request was made by the user.';
$string['protected'] = 'Protected';
$string['protectedlabel'] = 'The retention of this data has a higher legal precedent over a user\'s request to be forgotten. This data will only be deleted after the retention period has expired.';
$string['requestemailintro'] = 'You have received a data request:';
$string['requestfor'] = 'Requesting for';
$string['requeststatus'] = 'Status';
-$string['requestsubmitted'] = 'Your request has been submitted to the site\'s Data Protection Officer';
+$string['requestsubmitted'] = 'Your request has been submitted to the privacy officer';
$string['requesttype'] = 'Type';
$string['requesttypeuser'] = '{$a->typename} ({$a->user})';
-$string['requesttype_help'] = 'Select the reason why you would like to contact the site\'s Data Protection Officer';
+$string['requesttype_help'] = 'Select the reason why you would like to contact the privacy officer';
$string['requesttypedelete'] = 'Delete all of my personal data';
$string['requesttypedeleteshort'] = 'Delete';
$string['requesttypeexport'] = 'Export all of my personal data';
$string['requesttypeexportshort'] = 'Export';
$string['requesttypeothers'] = 'General inquiry';
-$string['requesttypeothersshort'] = 'Others';
+$string['requesttypeothersshort'] = 'Message';
$string['requiresattention'] = 'Requires attention.';
$string['requiresattentionexplanation'] = 'This plugin does not implement the Moodle privacy API. If this plugin stores any personal data it will not be able to be exported or deleted through Moodle\'s privacy system.';
$string['resultdeleted'] = 'You recently requested to have your account and personal data in {$a} to be deleted. This process has been completed and you will no longer be able to log in.';
* none
Context variables required for this template:
- * requests - Array of data requests.
+ * newdatarequesturl string The URL pointing to the data request creation page.
+ * datarequests string The HTML of the data requests table.
Example context (json):
{
- "requests": [
- {
- "id": 1,
- "foruser" : {
- "fullname": "Oscar Olsen",
- "profileurl": "#"
+ "newdatarequesturl": "#",
+ "datarequests": "<table><tr><td>This is the table where the list of data requests will be rendered</td></tr></table>",
+ "filter": {
+ "action": "#",
+ "filteroptions": [
+ {
+ "value": "1",
+ "label": "Option 1"
},
- "typenameshort" : "Export",
- "comments": "I would like to download all of my daughter's personal data",
- "statuslabelclass": "label-default",
- "statuslabel": "Pending",
- "timecreated" : 1517902435,
- "requestedbyuser" : {
- "fullname": "Martha Smith",
- "profileurl": "#"
- }
- },
- {
- "id": 2,
- "foruser" : {
- "fullname": "Alexandre Denys",
- "profileurl": "#"
- },
- "typenameshort" : "Export",
- "comments": "Please give me all of the information you have about me...",
- "statuslabelclass": "label-warning",
- "statuslabel": "Awaiting completion",
- "timecreated" : 1517902435,
- "requestedbyuser" : {
- "fullname": "Martha Smith",
- "profileurl": "#"
- }
- },
- {
- "id": 3,
- "foruser" : {
- "fullname": "Hirondino Moura",
- "profileurl": "#"
- },
- "typenameshort" : "Delete",
- "comments": "Please delete all of my son's personal data.",
- "statuslabelclass": "label-success",
- "statuslabel": "Complete",
- "timecreated" : 1517902435,
- "requestedbyuser" : {
- "fullname": "Martha Smith",
- "profileurl": "#"
- }
- },
- {
- "id": 4,
- "foruser" : {
- "fullname": "Florian Krause",
- "profileurl": "#"
+ {
+ "value": "2",
+ "label": "Option 2",
+ "selected": true
},
- "typenameshort" : "Delete",
- "comments": "I would like to request for my personal data to be deleted from your site. Thanks!",
- "statuslabelclass": "label-danger",
- "statuslabel": "Rejected",
- "timecreated" : 1517902435,
- "requestedbyuser" : {
- "fullname": "Martha Smith",
- "profileurl": "#"
+ {
+ "value": "3",
+ "label": "Option 3",
+ "selected": true
}
- },
- {
- "id": 5,
- "foruser" : {
- "fullname": "Nicklas Sørensen",
- "profileurl": "#"
- },
- "typenameshort" : "Export",
- "comments": "Please let me download my data",
- "statuslabelclass": "label-info",
- "statuslabel": "Processing",
- "timecreated" : 1517902435,
- "requestedbyuser" : {
- "fullname": "Martha Smith",
- "profileurl": "#"
- }
- }
- ]
+ ]
+ }
}
}}
<div data-region="datarequests">
<div class="m-t-1 m-b-1">
- <a href="{{newdatarequesturl}}" class="btn btn-primary" data-action="new-request">
- {{#str}}newrequest, tool_dataprivacy{{/str}}
- </a>
+ <div class="pull-right">
+ <a href="{{newdatarequesturl}}" class="btn btn-primary" data-action="new-request">
+ {{#str}}newrequest, tool_dataprivacy{{/str}}
+ </a>
+ </div>
+ {{#filter}}
+ {{>tool_dataprivacy/request_filter}}
+ {{/filter}}
+ </div>
+
+ <div class="m-t-1 m-b-1" data-region="data-requests-table">
+ {{{datarequests}}}
</div>
- <table class="generaltable fullwidth">
- <thead>
- <tr>
- <th scope="col">{{#str}}requesttype, tool_dataprivacy{{/str}}</th>
- <th scope="col">{{#str}}user, tool_dataprivacy{{/str}}</th>
- <th scope="col">{{#str}}daterequested, tool_dataprivacy{{/str}}</th>
- <th scope="col">{{#str}}requestby, tool_dataprivacy{{/str}}</th>
- <th scope="col">{{#str}}requeststatus, tool_dataprivacy{{/str}}</th>
- <th scope="col" colspan="2">{{#str}}message, tool_dataprivacy{{/str}}</th>
- </tr>
- </thead>
- <tbody>
- {{#requests}}
- <tr {{!
- }} data-region="request-node"{{!
- }} data-id="{{id}}"{{!
- }} data-type="{{type}}"{{!
- }} data-status="{{status}}"{{!
- }}>
- <td>{{typenameshort}}</td>
- <td><a href="{{foruser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{foruser.fullname}}</a></td>
- <td>{{#userdate}} {{timecreated}}, {{#str}} strftimedatetime {{/str}} {{/userdate}}</td>
- <td><a href="{{requestedbyuser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{requestedbyuser.fullname}}</a></td>
- <td>
- <span class="label {{statuslabelclass}}">{{statuslabel}}</span>
- </td>
- <td>{{#shortentext}}60, {{comments}}{{/shortentext}}</td>
- <td>
- {{#actions}}
- {{> core/action_menu}}
- {{/actions}}
- </td>
- </tr>
- {{/requests}}
- {{^requests}}
- <tr>
- <td class="text-muted" colspan="6">
- {{#str}}nodatarequests, tool_dataprivacy{{/str}}
- </td>
- </tr>
- {{/requests}}
- </tbody>
- </table>
</div>
{{#js}}
--- /dev/null
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template tool_dataprivacy/request_filter
+
+ Template for the request filter element.
+
+ Context variables required for this template:
+ * action string - The action URL for the form.
+ * filteroptions - Array of filter options.
+ * value string - The option value.
+ * label string - The option label.
+ * selected boolean - Whether the option is selected
+
+ Example context (json):
+ {
+ "action": "#",
+ "filteroptions": [
+ {
+ "value": "1",
+ "label": "Option 1"
+ },
+ {
+ "value": "2",
+ "label": "Option 2",
+ "selected": true
+ },
+ {
+ "value": "3",
+ "label": "Option 3",
+ "selected": true
+ },
+ {
+ "value": "4",
+ "label": "Option 4"
+ }
+ ]
+ }
+}}
+<form method="post" action="{{action}}" class="m-b-1" role="search" id="request_filter_form">
+ <label for="request-filters" class="sr-only">{{#str}}filters{{/str}}</label>
+ <select name="request-filters[]" id="request-filters" multiple="multiple" class="form-autocomplete-original-select">
+ {{#filteroptions}}
+ <option value="{{value}}" {{#selected}}selected="selected"{{/selected}}>{{{label}}}</option>
+ {{/filteroptions}}
+ </select>
+ <input type="hidden" id="filters-cleared" name="filters-cleared" value="0" />
+</form>
+{{#js}}
+require(['tool_dataprivacy/request_filter'], function(Filter) {
+ Filter.init();
+});
+{{/js}}
use tool_dataprivacy\data_registry;
use tool_dataprivacy\expired_context;
use tool_dataprivacy\data_request;
+use tool_dataprivacy\local\helper;
use tool_dataprivacy\task\initiate_data_request_task;
use tool_dataprivacy\task\process_data_request_task;
}
/**
- * Test for api::get_data_requests()
+ * Data provider for \tool_dataprivacy_api_testcase::test_get_data_requests().
+ *
+ * @return array
*/
- public function test_get_data_requests() {
+ public function get_data_requests_provider() {
$generator = new testing_data_generator();
$user1 = $generator->create_user();
$user2 = $generator->create_user();
- $comment = 'sample comment';
+ $user3 = $generator->create_user();
+ $user4 = $generator->create_user();
+ $user5 = $generator->create_user();
+ $users = [$user1, $user2, $user3, $user4, $user5];
+ $completeonly = [api::DATAREQUEST_STATUS_COMPLETE];
+ $completeandcancelled = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_CANCELLED];
- // Make a data request as user 1.
- $this->setUser($user1);
- $d1 = api::create_data_request($user1->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
- // Make a data request as user 2.
- $this->setUser($user2);
- $d2 = api::create_data_request($user2->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
-
- // Fetching data requests of specific users.
- $requests = api::get_data_requests($user1->id);
- $this->assertCount(1, $requests);
- $datarequest = reset($requests);
- $this->assertEquals($d1->to_record(), $datarequest->to_record());
-
- $requests = api::get_data_requests($user2->id);
- $this->assertCount(1, $requests);
- $datarequest = reset($requests);
- $this->assertEquals($d2->to_record(), $datarequest->to_record());
-
- // Fetching data requests of all users.
- // As guest.
- $this->setGuestUser();
- $requests = api::get_data_requests();
- $this->assertEmpty($requests);
-
- // As DPO (admin in this case, which is default if no site DPOs are set).
- $this->setAdminUser();
- $requests = api::get_data_requests();
- $this->assertCount(2, $requests);
+ return [
+ // Own data requests.
+ [$users, $user1, false, $completeonly],
+ // Non-DPO fetching all requets.
+ [$users, $user2, true, $completeonly],
+ // Admin fetching all completed and cancelled requests.
+ [$users, get_admin(), true, $completeandcancelled],
+ // Admin fetching all completed requests.
+ [$users, get_admin(), true, $completeonly],
+ // Guest fetching all requests.
+ [$users, guest_user(), true, $completeonly],
+ ];
+ }
+
+ /**
+ * Test for api::get_data_requests()
+ *
+ * @dataProvider get_data_requests_provider
+ * @param stdClass[] $users Array of users to create data requests for.
+ * @param stdClass $loggeduser The user logging in.
+ * @param boolean $fetchall Whether to fetch all records.
+ * @param int[] $statuses Status filters.
+ */
+ public function test_get_data_requests($users, $loggeduser, $fetchall, $statuses) {
+ $comment = 'Data %s request comment by user %d';
+ $exportstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_EXPORT);
+ $deletionstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_DELETE);
+ // Make a data requests for the users.
+ foreach ($users as $user) {
+ $this->setUser($user);
+ api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $exportstring, $user->id));
+ api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $deletionstring, $user->id));
+ }
+
+ // Log in as the target user.
+ $this->setUser($loggeduser);
+ // Get records count based on the filters.
+ $userid = $loggeduser->id;
+ if ($fetchall) {
+ $userid = 0;
+ }
+ $count = api::get_data_requests_count($userid);
+ if (api::is_site_dpo($loggeduser->id)) {
+ // DPOs should see all the requests.
+ $this->assertEquals(count($users) * 2, $count);
+ } else {
+ if (empty($userid)) {
+ // There should be no data requests for this user available.
+ $this->assertEquals(0, $count);
+ } else {
+ // There should be only one (request with pending status).
+ $this->assertEquals(2, $count);
+ }
+ }
+ // Get data requests.
+ $requests = api::get_data_requests($userid);
+ // The number of requests should match the count.
+ $this->assertCount($count, $requests);
+
+ // Test filtering by status.
+ if ($count && !empty($statuses)) {
+ $filteredcount = api::get_data_requests_count($userid, $statuses);
+ // There should be none as they are all pending.
+ $this->assertEquals(0, $filteredcount);
+ $filteredrequests = api::get_data_requests($userid, $statuses);
+ $this->assertCount($filteredcount, $filteredrequests);
+
+ $statuscounts = [];
+ foreach ($statuses as $stat) {
+ $statuscounts[$stat] = 0;
+ }
+ $numstatus = count($statuses);
+ // Get all requests with status filter and update statuses, randomly.
+ foreach ($requests as $request) {
+ if (rand(0, 1)) {
+ continue;
+ }
+
+ if ($numstatus > 1) {
+ $index = rand(0, $numstatus - 1);
+ $status = $statuses[$index];
+ } else {
+ $status = reset($statuses);
+ }
+ $statuscounts[$status]++;
+ api::update_request_status($request->get('id'), $status);
+ }
+ $total = array_sum($statuscounts);
+ $filteredcount = api::get_data_requests_count($userid, $statuses);
+ $this->assertEquals($total, $filteredcount);
+ $filteredrequests = api::get_data_requests($userid, $statuses);
+ $this->assertCount($filteredcount, $filteredrequests);
+ // Confirm the filtered requests match the status filter(s).
+ foreach ($filteredrequests as $request) {
+ $this->assertContains($request->get('status'), $statuses);
+ }
+
+ if ($numstatus > 1) {
+ // Fetch by individual status to check the numbers match.
+ foreach ($statuses as $status) {
+ $filteredcount = api::get_data_requests_count($userid, [$status]);
+ $this->assertEquals($statuscounts[$status], $filteredcount);
+ $filteredrequests = api::get_data_requests($userid, [$status]);
+ $this->assertCount($filteredcount, $filteredrequests);
+ }
+ }
+ }
}
/**
'name' => $name,
'description' => $description,
'timecreated' => transform::datetime($record->timecreated),
- 'ip' => $record->ip,
'origin' => static::transform_origin($record->origin),
+ 'ip' => $isauthor ? $record->ip : '',
'other' => $other ? $other : []
];
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
- static::call_subplugins_method_with_args('export_user_data', [$contextlist]);
+ if (get_config('tool_log', 'exportlog')) {
+ static::call_subplugins_method_with_args('export_user_data', [$contextlist]);
+ }
}
/**
$string['actlogshdr'] = 'Available log stores';
$string['configlogplugins'] = 'Please enable all required plugins and arrange them in appropriate order.';
+$string['exportlog'] = 'Include logs when exporting.';
+$string['exportlogdetail'] = 'Include logs that relate to the user when exporting.';
$string['logging'] = 'Logging';
$string['managelogging'] = 'Manage log stores';
$string['pluginname'] = 'Log store manager';
defined('MOODLE_INTERNAL') || die();
if ($hassiteconfig) {
+
+ $privacysettings = $ADMIN->locate('privacysettings');
+
+ if ($ADMIN->fulltree) {
+ $privacysettings->add(new admin_setting_configcheckbox('tool_log/exportlog',
+ new lang_string('exportlog', 'tool_log'),
+ new lang_string('exportlogdetail', 'tool_log'), 1)
+ );
+ }
+
$ADMIN->add('modules', new admin_category('logging', new lang_string('logging', 'tool_log')));
$temp = new admin_settingpage('managelogging', new lang_string('managelogging', 'tool_log'));
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2018051401; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2018050800; // Requires this Moodle version.
$plugin->component = 'tool_log'; // Full name of the plugin (used for diagnostics).
*
*/
protected function prepare_policies() {
- global $USER;
-
- if (isguestuser() || empty($USER->id)) {
- $audience = policy_version::AUDIENCE_GUESTS;
- } else {
- $audience = policy_version::AUDIENCE_LOGGEDIN;
- }
- $this->policies = api::list_current_versions($audience);
+ $this->policies = api::list_current_versions();
}
/**
$string['consentbulk'] = 'Consent';
$string['consentdetails'] = 'Give consent on behalf of user';
$string['consentpagetitle'] = 'Consent';
-$string['contactdpo'] = 'For questions regarding the policies please contact the Data Protection Officer.';
+$string['contactdpo'] = 'For any questions about the policies please contact the privacy officer.';
$string['dataproc'] = 'Personal data processing';
$string['deleting'] = 'Deleting a version';
$string['deleteconfirm'] = '<p>Are you sure you want to delete policy <em>\'{$a->name}\'</em>?</p><p>This operation can not be undone.</p>';
$string['mustagreetocontinue'] = 'Before continuing you must agree to all these policies.';
$string['newpolicy'] = 'New policy';
$string['newversion'] = 'New version';
+$string['noactivepolicies'] = 'There are no policies with an active version.';
$string['nofiltersapplied'] = 'No filters applied';
$string['nopermissiontoagreedocs'] = 'No permission to agree to the policies';
$string['nopermissiontoagreedocs_desc'] = 'Sorry, you do not have the required permissions to agree to the policies.<br />You will not be able to use this site until the following policies are agreed:';
if (!empty($CFG->sitepolicyhandler)
&& $CFG->sitepolicyhandler == 'tool_policy'
&& empty($USER->policyagreed)
- && isguestuser()) {
+ && (isguestuser() || !isloggedin())) {
$output = $PAGE->get_renderer('tool_policy');
$page = new \tool_policy\output\guestconsent();
$output = '';
if (!empty($CFG->sitepolicyhandler)
&& $CFG->sitepolicyhandler == 'tool_policy') {
- $url = (new moodle_url('/admin/tool/policy/viewall.php'))->out();
- $output .= html_writer::link($url, get_string('userpolicysettings', 'tool_policy'));
- $output = html_writer::div($output, 'policiesfooter');
+ $policies = api::get_current_versions_ids();
+ if (!empty($policies)) {
+ $url = (new moodle_url('/admin/tool/policy/viewall.php'))->out();
+ $output .= html_writer::link($url, get_string('userpolicysettings', 'tool_policy'));
+ $output = html_writer::div($output, 'policiesfooter');
+ }
}
return $output;
</ul>
</div>
+{{^policies }}
+ {{# str }} noactivepolicies, tool_policy {{/ str }}
+{{/policies }}
+
{{#policies }}
<hr>
<div class="policy_version m-b-3">
And I am on site homepage
And I follow "Log in"
When I press "Create new account"
- Then I should see "This site policy"
+ Then I should see "This site policy" in the "region-main" "region"
And I should see "short text2"
And I should see "full text2"
And I press "Next"
And I should see "Please agree to the following policies"
- And I should see "This site policy"
+ And I should see "This site policy" in the "region-main" "region"
And I should see "short text2"
And I should not see "full text2"
And I set the field "I agree to the This site policy" to "1"
And I am on site homepage
And I follow "Log in"
When I press "Create new account"
- Then I should see "This site policy"
+ Then I should see "This site policy" in the "region-main" "region"
And I should see "short text2"
And I should see "full text2"
And I press "Next"
- And I should see "This privacy policy"
+ And I should see "This privacy policy" in the "region-main" "region"
And I should see "short text3"
And I should see "full text3"
And I press "Next"
And I should see "Please agree to the following policies"
- And I should see "This site policy"
+ And I should see "This site policy" in the "region-main" "region"
And I should see "short text2"
And I should not see "full text2"
- And I should see "This privacy policy"
+ And I should see "This privacy policy" in the "region-main" "region"
And I should see "short text3"
And I should not see "full text3"
- And I should not see "This guests policy"
+ And I should not see "This guests policy" in the "region-main" "region"
And I should not see "short text4"
And I should not see "full text4"
And I set the field "I agree to the This site policy" to "1"
And I am on site homepage
And I follow "Log in"
When I press "Create new account"
- Then I should see "This site policy"
+ Then I should see "This site policy" in the "region-main" "region"
And I should see "short text2"
And I should see "full text2"
When I press "Next"
- Then I should see "This privacy policy"
+ Then I should see "This privacy policy" in the "region-main" "region"
And I should see "short text3"
And I should see "full text3"
When I press "Next"
Then I should see "Please agree to the following policies"
- And I should see "This site policy"
+ And I should see "This site policy" in the "region-main" "region"
And I should see "short text2"
- And I should see "This privacy policy"
+ And I should see "This privacy policy" in the "region-main" "region"
And I should see "short text3"
- And I should not see "This guests policy"
+ And I should not see "This guests policy" in the "region-main" "region"
And I should not see "short text4"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This privacy policy" to "1"
And I follow "Log in"
When I press "Create new account"
# Confirm that the user can view and accept policies when attempting to create another account.
- Then I should see "This site policy"
+ Then I should see "This site policy" in the "region-main" "region"
And I should see "short text2"
And I should see "full text2"
When I press "Next"
- Then I should see "This privacy policy"
+ Then I should see "This privacy policy" in the "region-main" "region"
And I should see "short text3"
And I should see "full text3"
When I press "Next"
Then I should see "Please agree to the following policies"
- And I should see "This site policy"
+ And I should see "This site policy" in the "region-main" "region"
And I should see "short text2"
And I should not see "full text2"
- And I should see "This privacy policy"
+ And I should see "This privacy policy" in the "region-main" "region"
And I should see "short text3"
And I should not see "full text3"
- And I should not see "This guests policy"
+ And I should not see "This guests policy" in the "region-main" "region"
And I should not see "short text4"
And I should not see "full text4"
And I set the field "I agree to the This site policy" to "1"
return false;
}
- // TODO MDL-59259 allow to set course format options for the current course format.
+ // Add data for course format options.
+ if (isset($coursedata['format']) || $exists) {
+ if (isset($coursedata['format'])) {
+ $courseformat = course_get_format((object)['format' => $coursedata['format']]);
+ } else {
+ $courseformat = course_get_format($existingdata);
+ }
+ $coursedata += $courseformat->validate_course_format_options($this->rawdata);
+ }
// Special case, 'numsections' is not a course format option any more but still should apply from defaults.
if (!$exists || !array_key_exists('numsections', $coursedata)) {
$this->assertTrue($co->prepare());
$this->assertFalse($DB->record_exists('course', array('shortname' => 'newcourse')));
$co->proceed();
- $this->assertTrue($DB->record_exists('course', array('shortname' => 'newcourse')));
+ $course = $DB->get_record('course', array('shortname' => 'newcourse'), '*', MUST_EXIST);
+ $this->assertEquals(0, course_get_format($course)->get_course()->coursedisplay);
// Try to add a new course, that already exists.
$coursecount = $DB->count_records('course', array());
$this->assertTrue($co->prepare());
$co->proceed();
$this->assertTrue($DB->record_exists('course', array('shortname' => 'c2')));
+
+ // Add a new course with non-default course format option.
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $data = array('shortname' => 'c3', 'fullname' => 'C3', 'summary' => 'New c3', 'category' => 1,
+ 'format' => 'weeks', 'coursedisplay' => 1);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $course = $DB->get_record('course', array('shortname' => 'c3'), '*', MUST_EXIST);
+ $this->assertEquals(1, course_get_format($course)->get_course()->coursedisplay);
}
public function test_create_with_sections() {
$this->assertTrue($co->prepare());
$co->proceed();
$this->assertEquals('Use this summary', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+
+ // Update course format option.
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $data = array('shortname' => 'c1', 'coursedisplay' => 1);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $course = $DB->get_record('course', array('shortname' => 'c1'), '*', MUST_EXIST);
+ $this->assertEquals(1, course_get_format($course)->get_course()->coursedisplay);
}
public function test_data_saved() {
set_user_preference('auth_forcepasswordchange', 1, $id);
set_user_preference('create_password', 1, $id);
}
+
+ // Save custom profile fields here.
+ require_once($CFG->dirroot . '/user/profile/lib.php');
+ $user->id = $id;
+ profile_save_data($user);
+
// Make sure user context is present.
context_user::instance($id);
}
$nuvalue = core_text::convert($newvalue, 'utf-8', $this->config->ldapencoding);
empty($nuvalue) ? $nuvalue = array() : $nuvalue;
$ouvalue = core_text::convert($oldvalue, 'utf-8', $this->config->ldapencoding);
-
foreach ($ldapkeys as $ldapkey) {
- $ldapkey = $ldapkey;
+ // Skip update if $ldapkey does not exist in LDAP.
+ if (!isset($user_entry[$ldapkey][0])) {
+ $success = false;
+ error_log($this->errorlogtag.get_string('updateremfailfield', 'auth_ldap',
+ array('ldapkey' => $ldapkey,
+ 'key' => $key,
+ 'ouvalue' => $ouvalue,
+ 'nuvalue' => $nuvalue)));
+ continue;
+ }
+
$ldapvalue = $user_entry[$ldapkey][0];
if (!$ambiguous) {
// Skip update if the values already match
$string['start_tls_key'] = 'Use TLS';
$string['updateremfail'] = 'Error updating LDAP record. Error code: {$a->errno}; Error string: {$a->errstring}<br/>Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
$string['updateremfailamb'] = 'Failed to update LDAP with ambiguous field {$a->key}; old moodle value: \'{$a->ouvalue}\', new value: \'{$a->nuvalue}\'';
+$string['updateremfailfield'] = 'Failed to update LDAP with non-existent field (\'{$a->ldapkey}\'). Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
$string['updatepasserror'] = 'Error in user_update_password(). Error code: {$a->errno}; Error string: {$a->errstring}';
$string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiry time. Error code: {$a->errno}; Error string: {$a->errstring}';
$string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expirationtime and/or gracelogins. Error code: {$a->errno}; Error string: {$a->errstring}';
$string['privacy:metadata:external:mahara'] = 'This plugin can send data externally to a linked Mahara application.';
$string['privacy:metadata:external:moodle'] = 'This plugin can send data externally to a linked Moodle application.';
$string['privacy:metadata:mnet_external:address'] = 'The address of the user.';
-$string['privacy:metadata:mnet_external:aim'] = 'The AIM identifier of the user.';
+$string['privacy:metadata:mnet_external:aim'] = 'The AIM identifier of the user';
$string['privacy:metadata:mnet_external:alternatename'] = 'An alternative name for the user.';
$string['privacy:metadata:mnet_external:autosubscribe'] = 'A preference as to if the user should be auto-subscribed to forums the user posts in.';
$string['privacy:metadata:mnet_external:calendartype'] = 'A user preference for the type of calendar to use.';
$string['privacy:metadata:mnet_external:firstname'] = 'The first name of the user.';
$string['privacy:metadata:mnet_external:firstnamephonetic'] = 'The phonetic details about the user\'s first name.';
$string['privacy:metadata:mnet_external:icq'] = 'The ICQ number of the user.';
-$string['privacy:metadata:mnet_external:id'] = 'The identifier for the user.';
-$string['privacy:metadata:mnet_external:idnumber'] = 'An identification number given by the institution.';
+$string['privacy:metadata:mnet_external:id'] = 'The user ID';
+$string['privacy:metadata:mnet_external:idnumber'] = 'An identification number given by the institution';
$string['privacy:metadata:mnet_external:imagealt'] = 'Alternative text for the user\'s image.';
$string['privacy:metadata:mnet_external:institution'] = 'The institution that this user is a member of.';
$string['privacy:metadata:mnet_external:lang'] = 'A user preference for the language shown.';
$string['privacy:metadata:mnet_external:lastnamephonetic'] = 'The phonetic details about the user\'s surname.';
$string['privacy:metadata:mnet_external:maildigest'] = 'A setting for the mail digest for this user.';
$string['privacy:metadata:mnet_external:maildisplay'] = 'A preference for the user about displaying their email address to other users.';
-$string['privacy:metadata:mnet_external:middlename'] = 'The middle name of the user.';
-$string['privacy:metadata:mnet_external:msn'] = 'The MSN identifier of the user.';
+$string['privacy:metadata:mnet_external:middlename'] = 'The middle name of the user';
+$string['privacy:metadata:mnet_external:msn'] = 'The MSN identifier of the user';
$string['privacy:metadata:mnet_external:phone1'] = 'A phone number for the user.';
$string['privacy:metadata:mnet_external:phone2'] = 'An additional phone number for the user.';
$string['privacy:metadata:mnet_external:picture'] = 'The picture details associated with this user.';
$string['privacy:metadata:mnet_external:policyagreed'] = 'A flag to determine if the user has agreed to the site policy.';
-$string['privacy:metadata:mnet_external:skype'] = 'The skype identifier of the user.';
+$string['privacy:metadata:mnet_external:skype'] = 'The Skype identifier of the user';
$string['privacy:metadata:mnet_external:suspended'] = 'A flag to show if the user has been suspended on this system.';
-$string['privacy:metadata:mnet_external:timezone'] = 'The timezone that the user resides in.';
+$string['privacy:metadata:mnet_external:timezone'] = 'The timezone of the user';
$string['privacy:metadata:mnet_external:trackforums'] = 'A preference for forums and tracking them.';
$string['privacy:metadata:mnet_external:trustbitmask'] = 'The trust bit mask';
$string['privacy:metadata:mnet_external:url'] = 'A URL related to this user.';
$string['privacy:metadata:mnet_external:username'] = 'The username for this user.';
-$string['privacy:metadata:mnet_external:yahoo'] = 'The yahoo identifier of the user.';
+$string['privacy:metadata:mnet_external:yahoo'] = 'The Yahoo identifier of the user';
$string['privacy:metadata:mnet_log'] = 'Details of remote actions carried out by a local user logged in a remote system.';
$string['privacy:metadata:mnet_log:action'] = 'Action carried out by the user.';
$string['privacy:metadata:mnet_log:cmid'] = 'ID of the course module.';
$string['privacy:metadata:mnet_session'] = 'The details of each MNet user session in a remote system is stored temporarily.';
$string['privacy:metadata:mnet_session:expires'] = 'Time when the session expires.';
$string['privacy:metadata:mnet_session:mnethostid'] = 'Remote system MNet ID.';
-$string['privacy:metadata:mnet_session:token'] = 'Unique session identifier.';
+$string['privacy:metadata:mnet_session:token'] = 'Unique session identifier';
$string['privacy:metadata:mnet_session:useragent'] = 'String denoting the user agent being which is accessing the page.';
$string['privacy:metadata:mnet_session:userid'] = 'ID of the user jumping to remote system.';
$string['privacy:metadata:mnet_session:username'] = 'Username of the user jumping to remote system.';
$string['privacy:metadata:auth_oauth2:confirmtoken'] = 'The confirmation token.';
$string['privacy:metadata:auth_oauth2:confirmtokenexpires'] = 'The timestamp when the confirmation token expires.';
$string['privacy:metadata:auth_oauth2:email'] = 'The external email that maps to this account.';
-$string['privacy:metadata:auth_oauth2:issuerid'] = 'The identifier of the OAuth 2 issuer for this OAuth 2 login.';
+$string['privacy:metadata:auth_oauth2:issuerid'] = 'The ID of the OAuth 2 issuer for this OAuth 2 login';
$string['privacy:metadata:auth_oauth2:tableexplanation'] = 'OAuth 2 accounts linked to a user\'s Moodle account.';
$string['privacy:metadata:auth_oauth2:timecreated'] = 'The timestamp when the user account was linked to the OAuth 2 login.';
$string['privacy:metadata:auth_oauth2:timemodified'] = 'The timestamp when this record was modified.';
}
/******************************************************************************/
-function LogoutNotification($SessionID){
+/**
+ * Handles SOAP Back-channel logout notification
+ *
+ * @param string $spsessionid SP-provided Shibboleth Session ID
+ * @return SoapFault or void if everything was fine
+ */
+function LogoutNotification($spsessionid) {
global $CFG, $SESSION, $DB;
- // Delete session of user using $SessionID
+ // Delete session of user using $spsessionid.
if(empty($CFG->dbsessions)) {
// File session
// Read session file data
$data = file($dir.'/'.$file);
if (isset($data[0])){
- $user_session = unserializesession($data[0]);
+ $usersession = unserializesession($data[0]);
// Check if we have found session that shall be deleted
- if (isset($user_session['SESSION']) && isset($user_session['SESSION']->shibboleth_session_id)){
+ if (isset($usersession['SESSION']) && isset($usersession['SESSION']->shibboleth_session_id)) {
// If there is a match, delete file
- if ($user_session['SESSION']->shibboleth_session_id == $SessionID){
+ if ($usersession['SESSION']->shibboleth_session_id == $spsessionid) {
// Delete session file
if (!unlink($dir.'/'.$file)){
return new SoapFault('LogoutError', 'Could not delete Moodle session file.');
}
}
} else {
- // DB Session
- //TODO: this needs to be rewritten to use new session stuff
- if (!empty($CFG->sessiontimeout)) {
- $ADODB_SESS_LIFE = $CFG->sessiontimeout;
- }
-
- if ($user_session_data = $DB->get_records_sql('SELECT sesskey, sessdata FROM {sessions2} WHERE expiry > NOW()')) {
- foreach ($user_session_data as $session_data) {
-
- // Get user session
- $user_session = adodb_unserialize( urldecode($session_data->sessdata) );
-
- if (isset($user_session['SESSION']) && isset($user_session['SESSION']->shibboleth_session_id)){
-
- // If there is a match, delete file
- if ($user_session['SESSION']->shibboleth_session_id == $SessionID){
- // Delete this session entry
- if (ADODB_Session::destroy($session_data->sesskey) !== true){
- return new SoapFault('LogoutError', 'Could not delete Moodle session entry in database.');
- }
+ // DB Sessions.
+ $sessions = $DB->get_records_sql(
+ 'SELECT userid, sessdata FROM {sessions} WHERE timemodified > ?',
+ array(time() - $CFG->sessiontimeout)
+ );
+ foreach ($sessions as $session) {
+ // Get user session from DB.
+ if (session_decode(base64_decode($session->sessdata))) {
+ if (isset($_SESSION['SESSION']) && isset($_SESSION['SESSION']->shibboleth_session_id)) {
+ // If there is a match, kill the session.
+ if ($_SESSION['SESSION']->shibboleth_session_id == trim($spsessionid)) {
+ // Delete this user's sessions.
+ \core\session\manager::kill_user_sessions($session->userid);
}
}
}
}
}
-
- // If now SoapFault was thrown the function will return OK as the SP assumes
-
+ // If no SoapFault was thrown, the function will return OK as the SP assumes.
}
/*****************************************************************************/
'name', 'description', 'format', 'courseid', 'groupid', 'userid',
'repeatid', 'modulename', 'instance', 'type', 'eventtype', 'timestart',
'timeduration', 'timesort', 'visible', 'uuid', 'sequence', 'timemodified',
- 'priority'));
+ 'priority', 'location'));
// Build the tree
$events->add_child($event);
'uuid' => $data->uuid,
'sequence' => $data->sequence,
'timemodified' => $data->timemodified,
- 'priority' => isset($data->priority) ? $data->priority : null);
+ 'priority' => isset($data->priority) ? $data->priority : null,
+ 'location' => isset($data->location) ? $data->location : null);
if ($this->name == 'activity_calendar') {
$params['instance'] = $this->task->get_activityid();
} else {
$string['privacy:metadata'] = 'The recent activity block contains a cache of data stored elsewhere in Moodle.';
$string['privacy:metadata:block_recent_activity'] = 'Temporary log of recent teacher activity. Removed after two days';
$string['privacy:metadata:block_recent_activity:action'] = 'Action: created, updated or deleted';
-$string['privacy:metadata:block_recent_activity:cmid'] = 'Course module id';
-$string['privacy:metadata:block_recent_activity:courseid'] = 'Course id';
+$string['privacy:metadata:block_recent_activity:cmid'] = 'Course activity ID';
+$string['privacy:metadata:block_recent_activity:courseid'] = 'Course ID';
$string['privacy:metadata:block_recent_activity:modname'] = 'Module type name (for delete action)';
$string['privacy:metadata:block_recent_activity:timecreated'] = 'Time when action was performed';
$string['privacy:metadata:block_recent_activity:userid'] = 'User performing the action';
$event->get_id()
);
$data->descriptionformat = $event->get_description()->get_format();
+ $data->location = external_format_text($event->get_location(), FORMAT_PLAIN, $related['context']->id)[0];
$data->groupid = $groupid;
$data->userid = $userid;
$data->categoryid = $categoryid;
'default' => null,
'null' => NULL_ALLOWED
],
+ 'location' => [
+ 'type' => PARAM_RAW_TRIMMED,
+ 'optional' => true,
+ 'default' => null,
+ 'null' => NULL_ALLOWED
+ ],
'categoryid' => [
'type' => PARAM_INT,
'optional' => true,
return $this->event->get_description();
}
+ public function get_location() {
+ return $this->event->get_location();
+ }
+
public function get_category() {
return $this->event->get_category();
}
*/
protected $description;
+ /**
+ * @var string $location Location of this event.
+ */
+ protected $location;
+
/**
* @var proxy_interface $category Category for this event.
*/
* @param times_interface $times The times associated with the event.
* @param bool $visible The event's visibility. True for visible, false for invisible.
* @param proxy_interface $subscription The event's subscription.
+ * @param string $location The event's location.
*/
public function __construct(
$id,
$type,
times_interface $times,
$visible,
- proxy_interface $subscription = null
+ proxy_interface $subscription = null,
+ $location = null
) {
$this->id = $id;
$this->name = $name;
$this->description = $description;
+ $this->location = $location;
$this->category = $category;
$this->course = $course;
$this->group = $group;
return $this->description;
}
+ public function get_location() {
+ return $this->location;
+ }
+
public function get_category() {
return $this->category;
}
*/
public function get_description();
+ /**
+ * Get the event's location.
+ *
+ * @return location_interface
+ */
+ public function get_location();
+
/**
* Get the category object associated with the event.
*
(new \DateTimeImmutable())->setTimestamp($dbrow->timemodified)
),
!empty($dbrow->visible),
- $subscription
+ $subscription,
+ $dbrow->location
);
$isactionevent = !empty($dbrow->type) && $dbrow->type == CALENDAR_EVENT_TYPE_ACTION;
$mform->setType('description', PARAM_RAW);
$mform->setAdvanced('description');
+ $mform->addElement('text', 'location', get_string('location', 'moodle'), 'size="50"');
+ $mform->setType('location', PARAM_RAW_TRIMMED);
+ $mform->setAdvanced('location');
+
// Add the variety of elements allowed for selecting event duration.
$this->add_event_duration_elements($mform);
global $DB, $CFG;
$errors = parent::validation($data, $files);
- $coursekey = isset($data['groupcourseid']) ? 'groupcourseid' : 'courseid';
$eventtypes = calendar_get_all_allowed_types();
$eventtype = isset($data['eventtype']) ? $data['eventtype'] : null;
-
+ $coursekey = ($eventtype == 'group') ? 'groupcourseid' : 'courseid';
if (empty($eventtype) || !isset($eventtypes[$eventtype])) {
$errors['eventtype'] = get_string('invalideventtype', 'calendar');
}
'id' => $coalesce('id'),
'name' => $coalesce('name'),
'description' => $coalesce('description'),
+ 'location' => $coalesce('location'),
'format' => $coalesce('format'),
'categoryid' => $coalesce('categoryid'),
'courseid' => $coalesce('courseid'),
'name' => $event->get_name(),
'description' => $event->get_description()->get_value(),
'format' => $event->get_description()->get_format(),
+ 'location' => $event->get_location(),
'courseid' => $event->get_course() ? $event->get_course()->get('id') : null,
'categoryid' => $event->get_category() ? $event->get_category()->get('id') : null,
'groupid' => $event->get_group() ? $event->get_group()->get('id') : null,
$eventdetails = (object) [
'name' => $event->name,
'description' => $event->description,
+ 'location' => $event->location,
'eventtype' => $event->eventtype,
'timestart' => transform::datetime($event->timestart),
'timeduration' => $event->timeduration
details.id as eventid,
details.name as name,
details.description as description,
+ details.location as location,
details.eventtype as eventtype,
details.timestart as timestart,
details.timeduration as timeduration
$ev->add_property('class', 'PUBLIC'); // PUBLIC / PRIVATE / CONFIDENTIAL
$ev->add_property('last-modified', Bennu::timestamp_to_datetime($event->timemodified));
+
+ if (!empty($event->location)) {
+ $ev->add_property('location', $event->location);
+ }
+
$ev->add_property('dtstamp', Bennu::timestamp_to_datetime()); // now
if ($event->timeduration > 0) {
//dtend is better than duration, because it works in Microsoft Outlook and works better in Korganizer
\core_date::set_default_server_timezone();
}
+ $eventrecord->location = empty($event->properties['LOCATION'][0]->value) ? '' :
+ str_replace('\\', '', $event->properties['LOCATION'][0]->value);
$eventrecord->uuid = $event->properties['UID'][0]->value;
$eventrecord->timemodified = time();
</a>
{{/canedit}}
</div>
- {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}}
- <h3 class="name d-inline-block">{{name}}</h3>
- <span class="date pull-xs-right m-r-1">{{{formattedtime}}}</span>
+ {{#icon}}<div class="d-inline-block mt-1 align-top">{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}</div>{{/icon}}
+ <div class="d-inline-block">
+ <h3 class="name d-inline-block">{{name}}</h3>
+ <span class="date pull-xs-right m-r-1">{{{formattedtime}}}</span>
+ <div class="location">{{#location}}{{{location}}}{{/location}}</div>
+ </div>
</div>
<div class="description card-block calendar_event_{{eventtype}}">
<p>{{{description}}}</p>
{
"timestart": 1490320388,
"description": "An random event description",
+ "location": "13th floor, building 42",
"eventtype": "User",
"source": "Ical imported",
"groupname": "Group 1"
<div class="description-content col-xs-11">{{{.}}}</div>
</div>
{{/description}}
+ {{#location}}
+ <div class="row m-t-1">
+ <div class="col-xs-1">{{#pix}} i/location, core, {{#str}} location {{/str}} {{/pix}}</div>
+ <div class="location-content col-xs-11">{{{.}}}</div>
+ </div>
+ {{/location}}
{{#isactionevent}}
<div class="row m-t-1">
<div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
return new event_description('asdf', 1);
}
+ public function get_location() {
+ return 'Cube office';
+ }
+
public function get_category() {
return new \stdClass();
}
| Type of event | user |
| Event title | Really awesome event! |
| Description | Come join this awesome event, sucka! |
+ | Location | Cube office |
And I am on "Course 1" course homepage
When I follow "This month"
And I click on "Really awesome event!" "link"
+ And ".location-content" "css_element" should exist
+ And I should see "Cube office"
And I click on "Edit" "button"
And I set the following fields to these values:
| Event title | Mediocre event :( |
| Description | Wait, this event isn't that great. |
+ | Location | |
And I press "Save"
+ And I should see "Mediocre event"
+ And I click on "Mediocre event :(" "link"
Then I should see "Mediocre event"
+ And ".location-content" "css_element" should not exist
@javascript
Scenario: Module events editing
When I click on "Go to activity" "link"
And I wait to be redirected
Then I should see "Test choice"
+
+ @javascript
+ Scenario: Attempt to create event without fill required fields should display validation errors
+ Given I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I follow "This month"
+ And I click on "New event" "button"
+ When I click on "Save" "button"
+ Then I should see "Required"
+ And I am on site homepage
+ And I follow "Calendar"
+ And I click on "New event" "button"
+ And I set the field "Type of event" to "Course"
+ When I click on "Save" "button"
+ Then I should see "Required"
+ And I should see "Select a course"
+ And I set the field "Event title" to "Really awesome event!"
+ When I click on "Save" "button"
+ Then I should see "Select a course"
And I should see "Event on 2-15-2017"
And I should see "Event on 2-25-2017"
And I click on "Event on 2-15-2017" "link"
+ And I should see "Some place"
And I click on "Edit" "button"
And I set the following fields to these values:
| Event title | Event on 2-20-2017 |
$this->assertEquals($dbrow->description, $event->get_description()->get_value());
$this->assertEquals($dbrow->format, $event->get_description()->get_format());
$this->assertEquals($dbrow->courseid, $event->get_course()->get('id'));
+ $this->assertEquals($dbrow->location, $event->get_location());
if ($dbrow->groupid == 0) {
$this->assertNull($event->get_group());
$event = new \stdClass();
$event->name = 'An event';
$event->description = 'Event description';
+ $event->location = 'Event location';
$event->format = FORMAT_HTML;
$event->eventtype = \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED;
$event->userid = 1;
$event = new \stdClass();
$event->name = 'An event';
$event->description = 'Event description';
+ $event->location = 'Event location';
$event->format = FORMAT_HTML;
$event->eventtype = 'close';
$event->userid = $user->id;
'timesort' => 1486396800,
'visible' => 1,
'timemodified' => 1485793098,
- 'subscriptionid' => null
+ 'subscriptionid' => null,
+ 'location' => 'Test location',
]
],
'timesort' => 1486396800,
'visible' => 1,
'timemodified' => 1485793098,
- 'subscriptionid' => null
+ 'subscriptionid' => null,
+ 'location' => 'Test location',
]
]
];
'timesort' => 1486396800,
'visible' => 1,
'timemodified' => 1485793098,
- 'subscriptionid' => null
+ 'subscriptionid' => null,
+ 'location' => 'Test location',
];
foreach ((array) $skeleton as $key => $value) {
'timemodified' => 123456789,
'timesort' => 123456789,
'visible' => 1,
- 'subscriptionid' => 1
+ 'subscriptionid' => 1,
+ 'location' => 'Test location',
]
);
}
'timemodified' => 123456789,
'timesort' => 123456789,
'visible' => 1,
- 'subscriptionid' => 1
+ 'subscriptionid' => 1,
+ 'location' => 'Test location',
]
);
}
'timemodified' => 123456789,
'timesort' => 123456789,
'visible' => 1,
- 'subscriptionid' => 1
+ 'subscriptionid' => 1,
+ 'location' => 'Test location',
]
);
}
'timemodified' => 123456789,
'timesort' => 123456789,
'visible' => 1,
- 'subscriptionid' => 1
+ 'subscriptionid' => 1,
+ 'location' => 'Test location',
]
);
'timemodified' => 123456789,
'timesort' => 123456789,
'visible' => 1,
- 'subscriptionid' => 1
+ 'subscriptionid' => 1,
+ 'location' => 'Test location',
]
);
'timemodified' => 123456789,
'timesort' => 123456789,
'visible' => true,
- 'subscriptionid' => 1
+ 'subscriptionid' => 1,
+ 'location' => 'Test location',
],
'actioncallbackapplier' => function(event_interface $event) {
$event->testattribute = 'Hello';
'timemodified' => 123456789,
'timesort' => 123456789,
'visible' => true,
- 'subscriptionid' => 1
+ 'subscriptionid' => 1,
+ 'location' => 'Test location',
],
'actioncallbackapplier' => function(event_interface $event) {
$event->testattribute = 'Hello';
'timemodified' => 123456789,
'timesort' => 123456789,
'visible' => true,
- 'subscriptionid' => 1
+ 'subscriptionid' => 1,
+ 'location' => 'Test location',
],
'actioncallbackapplier' => function(event_interface $event) {
$event->testattribute = 'Hello';
return $this->event->get_description();
}
+ public function get_location() {
+ return $this->event->get_location();
+ }
+
public function get_category() {
return $this->event->get_category();
}
/**
* Constructor.
*
- * @param calendar_event $legacyevent Legacy event to exctract IDs etc from.
+ * @param calendar_event $legacyevent Legacy event to extract IDs etc from.
*/
public function __construct($legacyevent = null) {
if ($legacyevent) {
return new event_description('asdf', 1);
}
+ public function get_location() {
+ return 'Cube office';
+ }
+
public function get_category() {
return $this->categoryproxy;
}
$constructorparams['type'],
$constructorparams['times'],
$constructorparams['visible'],
- $constructorparams['subscription']
+ $constructorparams['subscription'],
+ $constructorparams['location']
);
foreach ($constructorparams as $name => $value) {
(new \DateTimeImmutable())->setTimestamp(time())
),
'visible' => true,
- 'subscription' => new std_proxy(1, $lamecallable)
+ 'subscription' => new std_proxy(1, $lamecallable),
+ 'location' => 'Test',
]
],
];
'text' => '',
'format' => 1,
],
+ 'location' => 'Test',
'duration' => 1,
'timedurationuntil' => [
'day' => $timedurationuntil->format('j'),
'format' => 1,
'itemid' => 0
],
+ 'location' => 'Test',
'duration' => 1,
'timedurationuntil' => [
'day' => $timedurationuntil->format('j'),
'text' => '',
'format' => 1,
],
+ 'location' => 'Test',
'duration' => 1,
'timedurationuntil' => [
'day' => $timedurationuntil->format('j'),
'format' => 1,
'itemid' => 0
],
+ 'location' => 'Test',
'duration' => 1,
'timedurationuntil' => [
'day' => $timedurationuntil->format('j'),
'text' => '',
'format' => 1,
],
+ 'location' => 'Test',
'duration' => 1,
'timedurationuntil' => [
'day' => $timedurationuntil->format('j'),
'format' => 1,
'itemid' => 0,
],
+ 'location' => 'Test',
'duration' => 1,
'timedurationuntil' => [
'day' => $timedurationuntil->format('j'),
'text' => '',
'format' => 1,
],
+ 'location' => 'Test',
'duration' => 1,
'timedurationuntil' => [
'day' => $timedurationuntil->format('j'),
'text' => '',
'format' => 1,
],
+ 'location' => 'Test',
'duration' => 1,
'timedurationuntil' => [
'day' => $timedurationuntil->format('j'),
'format' => 1,
'itemid' => 0
],
+ 'location' => 'Test',
'duration' => 1,
'timedurationuntil' => [
'day' => $timedurationuntil->format('j'),
'format' => 1,
'itemid' => 0
],
+ 'location' => 'Test',
'duration' => 1,
'timedurationuntil' => [
'day' => $timedurationuntil->format('j'),
'format' => 1,
'itemid' => 0
],
+ 'location' => 'Test',
'duration' => 1,
'timedurationuntil' => [
'day' => $timedurationuntil->format('j'),
'text' => '',
'format' => 1,
],
+ 'location' => 'Test',
'duration' => 1,
'timedurationuntil' => [
'day' => $timedurationuntil->format('j'),
DESCRIPTION:Event on 2-15-2017
CLASS:PUBLIC
LAST-MODIFIED:20170226T014326Z
+LOCATION:Some place
DTSTAMP:20170226T014355Z
DTSTART;VALUE=DATE:20170214
DTEND;VALUE=DATE:20170215
(new \DateTimeImmutable())->setTimestamp($record->timemodified)
),
!empty($record->visible),
- $subscription
+ $subscription,
+ $record->location
);
$action = new action(
[
'name' => 'Start of assignment',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => $course->id,
'groupid' => 0,
], [
'name' => 'Start of lesson',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => $course->id,
'groupid' => 0,
[
'name' => 'Assignment 1 due date',
'description' => '',
+ 'location' => 'Test',
'format' => 0,
'courseid' => $course->id,
'groupid' => 0,
], [
'name' => 'Assignment 1 due date - User override',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => 0,
'groupid' => 0,
], [
'name' => 'Assignment 1 due date - Group A override',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => $course->id,
'groupid' => $group1->id,
], [
'name' => 'Assignment 1 due date - Group B override',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => $course->id,
'groupid' => $group2->id,
[
'name' => 'Repeating site event',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => SITEID,
'groupid' => 0,
[
'name' => 'Repeating site event',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => SITEID,
'groupid' => 0,
[
'name' => 'Start of assignment',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => $course->id,
'groupid' => 0,
], [
'name' => 'Start of lesson',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => $course->id,
'groupid' => 0,
[
'name' => 'Assignment 1 due date',
'description' => '',
+ 'location' => 'Test',
'format' => 0,
'courseid' => $course->id,
'groupid' => 0,
], [
'name' => 'Assignment 1 due date - User override',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => 0,
'groupid' => 0,
], [
'name' => 'Assignment 1 due date - Group A override',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => $course->id,
'groupid' => $group1->id,
], [
'name' => 'Assignment 1 due date - Group B override',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => $course->id,
'groupid' => $group2->id,
[
'name' => 'Repeating site event',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => SITEID,
'groupid' => 0,
[
'name' => 'Repeating site event',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => SITEID,
'groupid' => 0,
[
'name' => 'Start of assignment',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => $course->id,
'groupid' => 0,
], [
'name' => 'Start of lesson',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => $course->id,
'groupid' => 0,
[
'name' => 'Assignment 1 due date',
'description' => '',
+ 'location' => 'Test',
'format' => 0,
'courseid' => $course->id,
'groupid' => 0,
], [
'name' => 'Assignment 1 due date - User override',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => 0,
'groupid' => 0,
], [
'name' => 'Assignment 1 due date - Group A override',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => $course->id,
'groupid' => $group1->id,
], [
'name' => 'Assignment 1 due date - Group B override',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => $course->id,
'groupid' => $group2->id,
[
'name' => 'Repeating site event',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => SITEID,
'groupid' => 0,
[
'name' => 'Repeating site event',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'courseid' => SITEID,
'groupid' => 0,
'name' => 'E1',
'eventtype' => 'category',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'categoryid' => $category1->id,
'userid' => 2,
'name' => 'E2',
'eventtype' => 'category',
'description' => '',
+ 'location' => 'Test',
'format' => 1,
'categoryid' => $category2->id,
'userid' => 2,
$this->assertCount(2, $events);
}
}
-
(new \DateTimeImmutable())->setTimestamp($dbrow->timemodified)
),
!empty($dbrow->visible),
- new std_proxy($dbrow->subscriptionid, $identity)
+ new std_proxy($dbrow->subscriptionid, $identity),
+ $dbrow->location
);
}
}
protected function detail_pair($key, $value, $class ='') {
$html = html_writer::start_div('detail-pair row yui3-g '.preg_replace('#[^a-zA-Z0-9_\-]#', '-', $class));
$html .= html_writer::div(html_writer::span($key), 'pair-key span3 col-md-3 yui3-u-1-4');
- $html .= html_writer::div(html_writer::span($value), 'pair-value span9 col-md-9 m-b-1 yui3-u-3-4');
+ $html .= html_writer::div(html_writer::span($value), 'pair-value span9 col-md-9 m-b-1 yui3-u-3-4 form-inline');
$html .= html_writer::end_div();
return $html;
}
* @return \core_search\document
*/
public function get_document($record, $options = array()) {
+ global $CFG;
+ require_once($CFG->dirroot . '/course/lib.php');
+
// Get the context, modinfo, and section.
try {
$context = \context_course::instance($record->course);
break;
case 'visible':
- if (has_capability('moodle/category:manage', $context)
- or has_capability('moodle/category:viewhiddencategories',
- context_system::instance())) {
+ if (has_capability('moodle/category:viewhiddencategories', $context)) {
$value = clean_param($crit['value'], PARAM_INT);
$conditions[$key] = $value;
$wheres[] = $key . " = :" . $key;
if (!isset($excludedcats[$category->id])) {
// Final check to see if the category is visible to the user.
- if ($category->visible
- or has_capability('moodle/category:viewhiddencategories', context_system::instance())
- or has_capability('moodle/category:manage', $context)) {
+ if ($category->visible or has_capability('moodle/category:viewhiddencategories', $context)) {
$categoryinfo = array();
$categoryinfo['id'] = $category->id;
return array();
}
+ /**
+ * Prepares values of course or section format options before storing them in DB
+ *
+ * If an option has invalid value it is not returned
+ *
+ * @param array $rawdata associative array of the proposed course/section format options
+ * @param int|null $sectionid null if it is course format option
+ * @return array array of options that have valid values
+ */
+ protected function validate_format_options(array $rawdata, int $sectionid = null) : array {
+ if (!$sectionid) {
+ $allformatoptions = $this->course_format_options(true);
+ } else {
+ $allformatoptions = $this->section_format_options(true);
+ }
+ $data = array_intersect_key($rawdata, $allformatoptions);
+ foreach ($data as $key => $value) {
+ $option = $allformatoptions[$key] + ['type' => PARAM_RAW, 'element_type' => null, 'element_attributes' => [[]]];
+ $data[$key] = clean_param($value, $option['type']);
+ if ($option['element_type'] === 'select' && !array_key_exists($data[$key], $option['element_attributes'][0])) {
+ // Value invalid for select element, skip.
+ unset($data[$key]);
+ }
+ }
+ return $data;
+ }
+
+ /**
+ * Validates format options for the course
+ *
+ * @param array $data data to insert/update
+ * @return array array of options that have valid values
+ */
+ public function validate_course_format_options(array $data) : array {
+ return $this->validate_format_options($data);
+ }
+
/**
* Updates format options for a course or section
*
*/
protected function update_format_options($data, $sectionid = null) {
global $DB;
+ $data = $this->validate_format_options((array)$data, $sectionid);
if (!$sectionid) {
$allformatoptions = $this->course_format_options();
$sectionid = 0;
'sectionid' => $sectionid
), '', 'name,id,value');
$changed = $needrebuild = false;
- $data = (array)$data;
foreach ($defaultoptions as $key => $value) {
if (isset($records[$key])) {
if (array_key_exists($key, $data) && $records[$key]->value !== $data[$key]) {
Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
+=== 3.6 ===
+* New method validate_format_options() cleans the values of the course/section format options before inserting them
+ in the database. Course format options can now be set in tool_uploadcourse and validation of user-submitted data is important.
+ Note that validate_format_options() is now always called when somebody creates or edits course or section and also
+ during restore and course upload. Default implementation validates against the definition of the form elements for
+ format options.
+
=== 3.5 ===
* Course formats should overwrite get_config_for_external function to return the course format settings viewable by the
current user.
$mform->addElement('header', 'groupheader', get_string('groups'));
$mform->addElement('checkbox', 'reset_groups_remove', get_string('deleteallgroups', 'group'));
- $mform->setAdvanced('reset_groups_remove');
$mform->addElement('checkbox', 'reset_groups_members', get_string('removegroupsmembers', 'group'));
- $mform->setAdvanced('reset_groups_members');
$mform->disabledIf('reset_groups_members', 'reset_groups_remove', 'checked');
$mform->addElement('checkbox', 'reset_groupings_remove', get_string('deleteallgroupings', 'group'));
- $mform->setAdvanced('reset_groupings_remove');
$mform->addElement('checkbox', 'reset_groupings_members', get_string('removegroupingsmembers', 'group'));
- $mform->setAdvanced('reset_groupings_members');
$mform->disabledIf('reset_groupings_members', 'reset_groupings_remove', 'checked');
$unsupported_mods = array();
}
}
}}
-<div class="m-t-2 m-b-1">
+<div class="m-t-2 m-b-1 activity-navigation">
{{< core/columns-1to1to1}}
{{$column1}}
<div class="pull-left">
// Set the required capabilities by the external function.
$context = context_system::instance();
$roleid = $this->assignUserCapability('moodle/category:manage', $context->id);
+ $this->assignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
// Retrieve category1 + sub-categories except not visible ones
$categories = core_course_external::get_categories(array(
$this->assertEquals($DB->count_records('course_categories'), count($categories));
- $this->unassignUserCapability('moodle/category:manage', $context->id, $roleid);
+ $this->unassignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
- // Ensure maxdepthcategory is 2 and retrieve all categories without category:manage capability. It should retrieve all
- // visible categories as well.
+ // Ensure maxdepthcategory is 2 and retrieve all categories without category:viewhiddencategories capability.
+ // It should retrieve all visible categories as well.
set_config('maxcategorydepth', 2);
$categories = core_course_external::get_categories();
*/
$string['dataformat'] = 'Javascript Object Notation (.json)';
-$string['privacy:metadata'] = 'The JavaScript Object Notation data format plugin does not store any personal data.';
-$string['shortname'] = 'JSON';
+$string['privacy:metadata'] = 'The JavaScript Object Notation (JSON) data format plugin does not store any personal data.';
+$string['shortname'] = 'JavaScript Object Notation (JSON)';
$string['dataformat'] = 'OpenDocument (.ods)';
$string['privacy:metadata'] = 'The OpenDocument data format plugin does not store any personal data.';
-$string['shortname'] = 'OpenDoc';
+$string['shortname'] = 'OpenDocument';
</pre>';
$string['privacy:metadata:enrol_flatfile'] = 'The Flat file (CSV) enrolment plugin may store personal data relating to future enrolments in the enrol_flatfile table.';
$string['privacy:metadata:enrol_flatfile:action'] = 'The enrolment action expected at the given date.';
-$string['privacy:metadata:enrol_flatfile:courseid'] = 'The courseid to which the enrolment relates.';
-$string['privacy:metadata:enrol_flatfile:roleid'] = 'The id of the role to be assigned or revoked.';
+$string['privacy:metadata:enrol_flatfile:courseid'] = 'The course ID to which the enrolment relates';
+$string['privacy:metadata:enrol_flatfile:roleid'] = 'The ID of the role to be assigned or unassigned';
$string['privacy:metadata:enrol_flatfile:timestart'] = 'The time at which the enrolment change starts.';
$string['privacy:metadata:enrol_flatfile:timeend'] = 'The time at which the enrolment change ends.';
$string['privacy:metadata:enrol_flatfile:timemodified'] = 'The modification time of this enrolment change.';
-$string['privacy:metadata:enrol_flatfile:userid'] = 'The id of the user to which the role assignment relates.';
+$string['privacy:metadata:enrol_flatfile:userid'] = 'The ID of the user to which the role assignment relates';
$string['privacy:metadata:enrol_paypal:enrol_paypal:receiver_id'] = 'Unique PayPal account ID of the payment recipient (i.e., the merchant).';
$string['privacy:metadata:enrol_paypal:enrol_paypal:tax'] = 'Amount of tax charged on payment.';
$string['privacy:metadata:enrol_paypal:enrol_paypal:timeupdated'] = 'The time of Moodle being notified by PayPal about the payment.';
-$string['privacy:metadata:enrol_paypal:enrol_paypal:txn_id'] = 'The merchant\'s original transaction identification number for the payment from the buyer, against which the case was registered.';
+$string['privacy:metadata:enrol_paypal:enrol_paypal:txn_id'] = 'The merchant\'s original transaction identification number for the payment from the buyer, against which the case was registered';
$string['privacy:metadata:enrol_paypal:enrol_paypal:userid'] = 'The ID of the user who bought the course enrolment.';
$string['privacy:metadata:enrol_paypal:paypal_com'] = 'The PayPal enrolment plugin transmits user data from Moodle to the PayPal website.';
$string['privacy:metadata:enrol_paypal:paypal_com:address'] = 'Address of the user who is buying the course.';
* @param context $context The current context.
*/
public function setup($page, $context) {
- // This only requires execution once per request.
- static $jsinitialised = false;
- if (empty($jsinitialised)) {
+ if ($page->requires->should_create_one_time_item_now('filter_mathjaxloader-scripts')) {
$url = get_config('filter_mathjaxloader', 'httpsurl');
$lang = $this->map_language_code(current_language());
$url = new moodle_url($url, array('delayStartupUntil' => 'configured'));
$params = array('mathjaxconfig' => $config, 'lang' => $lang);
$page->requires->yui_module('moodle-filter_mathjaxloader-loader', 'M.filter_mathjaxloader.configure', array($params));
-
- $jsinitialised = true;
}
}
$string['langdownloaderror'] = 'Dissortadament l\'idioma "{$a}" no es pot baixar. La instal·lació prosseguirà en anglès.';
$string['memorylimithelp'] = '<p>El lÃmit de memòria del PHP del vostre servidor actualment està definit en {$a}.</p>
-<p>Això pot causar que Moodle tingui problemes de memòria més endavant, especialment si teniu molts mòduls habilitats i/o molts usuaris.</p>
+<p>Això pot causar que Moodle tingui problemes de memòria més endavant, especialment, si teniu molts mòduls habilitats i/o molts usuaris.</p>
<p>És recomanable que configureu el PHP amb un lÃmit superior, com ara 40 MB, sempre que sigui possible. Hi ha diverses maneres de fer això:</p>
<ol>
<li>Si podeu, recompileu el PHP amb <i>--enable-memory-limit</i>. Això permetrà que Moodle defineixi el lÃmit de memòria per si mateix.</li>
-<li>Si teniu accés al fitxer php.ini, podeu canviar el parà metre <b>memory_limit</b> a 40 MB. Si no hi teniu accés podeu demanar al vostre administrador que ho faci ell.</li>
-<li>En alguns servidors PHP podeu crear un fitxer .htaccess dins del directori de Moodle amb aquesta lÃnia:
+<li>Si teniu accés al fitxer php.ini, podeu canviar el parà metre <b>memory_limit</b> a 40 MB. Si no hi teniu accés, podeu demanar al vostre administrador que us ho faci.</li>
+<li>En alguns servidors PHP podeu crear un fitxer .htaccess dins el directori de Moodle amb aquesta lÃnia:
<p><blockquote>php_value memory_limit 40M</blockquote></p>
-<p>Tanmateix, en alguns servidors això farà que no funcioni <b>cap</b> pà gina PHP (es visualitzaran errors) en el qual cas haurÃeu de suprimir el fitxer .htaccess.</p></li>
+<p>Tanmateix, en alguns servidors això farà que no funcioni <b>cap</b> pà gina PHP (es visualitzaran errors); en aquest cas, haurÃeu de suprimir el fitxer .htaccess.</p></li>
</ol>';
$string['paths'] = 'Camins';
$string['pathserrcreatedataroot'] = 'L\'instal·lador no pot crear el directori de dades ({$a->dataroot}).';
$string['purgecaches'] = 'Purge all caches';
$string['purgecachesconfirm'] = 'Moodle can cache themes, javascript, language strings, filtered text, rss feeds and many other pieces of calculated data. Purging these caches will delete that data from the server and force browsers to refetch data, so that you can be sure you are seeing the most up-to-date values produced by the current code. There is no danger in purging caches, but your site may appear slower for a while until the server and clients calculate new information and cache it.';
$string['purgecachesfinished'] = 'All caches were purged.';
+$string['purgecachesnoneselected'] = 'Select one or more caches to purge';
+$string['purgecachespage'] = 'Purge caches';
+$string['purgefiltercache'] = 'Text filters';
+$string['purgejscache'] = 'JavaScript';
+$string['purgelangcache'] = 'Language strings';
+$string['purgemuc'] = 'All MUC caches';
+$string['purgeothercaches'] = 'All file and miscellaneous caches';
+$string['purgeselectedcaches'] = 'Purge selected caches';
+$string['purgeselectedcachesfinished'] = 'The selected caches were purged.';
+$string['purgethemecache'] = 'Themes';
$string['requestcategoryselection'] = 'Enable category selection';
$string['restorecourse'] = 'Restore course';
$string['restorernewroleid'] = 'Restorers\' role in courses';
$string['privacy:metadata:analytics:indicatorcalc:endtime'] = 'Calculation end time';
$string['privacy:metadata:analytics:indicatorcalc:contextid'] = 'The context';
$string['privacy:metadata:analytics:indicatorcalc:sampleorigin'] = 'The origin table of the sample';
-$string['privacy:metadata:analytics:indicatorcalc:sampleid'] = 'The sample id';
+$string['privacy:metadata:analytics:indicatorcalc:sampleid'] = 'The sample ID';
$string['privacy:metadata:analytics:indicatorcalc:indicator'] = 'The indicator calculator class';
$string['privacy:metadata:analytics:indicatorcalc:value'] = 'The calculated value';
$string['privacy:metadata:analytics:indicatorcalc:timecreated'] = 'When the prediction was made';
$string['privacy:metadata:analytics:predictions'] = 'Predictions';
-$string['privacy:metadata:analytics:predictions:modelid'] = 'The model id';
+$string['privacy:metadata:analytics:predictions:modelid'] = 'The model ID';
$string['privacy:metadata:analytics:predictions:contextid'] = 'The context';
-$string['privacy:metadata:analytics:predictions:sampleid'] = 'The sample id';
+$string['privacy:metadata:analytics:predictions:sampleid'] = 'The sample ID';
$string['privacy:metadata:analytics:predictions:rangeindex'] = 'The index of the time splitting method';
$string['privacy:metadata:analytics:predictions:prediction'] = 'The prediction';
$string['privacy:metadata:analytics:predictions:predictionscore'] = 'The prediction score';
$string['privacy:metadata:analytics:predictions:timestart'] = 'Calculations time start';
$string['privacy:metadata:analytics:predictions:timeend'] = 'Calculations time end';
$string['privacy:metadata:analytics:predictionactions'] = 'Prediction actions';
-$string['privacy:metadata:analytics:predictionactions:predictionid'] = 'The prediction id';
+$string['privacy:metadata:analytics:predictionactions:predictionid'] = 'The prediction ID';
$string['privacy:metadata:analytics:predictionactions:userid'] = 'The user that made the action';
$string['privacy:metadata:analytics:predictionactions:actionname'] = 'The action name';
$string['privacy:metadata:analytics:predictionactions:timecreated'] = 'When the prediction action was performed';
$string['privacy:metadata:post:subject'] = 'Blog entry title.';
$string['privacy:metadata:post:summary'] = 'Blog entry.';
$string['privacy:metadata:post:content'] = 'The content of an external blog entry.';
-$string['privacy:metadata:post:uniquehash'] = 'A unique identifier for an external entry, typically a URL.';
+$string['privacy:metadata:post:uniquehash'] = 'A unique identifier for an external entry, typically a URL';
$string['privacy:metadata:post:publishstate'] = 'Whether the entry is visible to others or not';
$string['privacy:metadata:post:created'] = 'Date when the entry was created.';
$string['privacy:metadata:post:lastmodified'] = 'Date when the entry was last modified.';
$string['idnumber'] = 'Cohort ID';
$string['memberscount'] = 'Cohort size';
$string['name'] = 'Name';
-$string['namecolumnmissing'] = 'There is something wrong with the format of the CSV file. Please check that it includes column names.';
+$string['namecolumnmissing'] = 'There is something wrong with the format of the CSV file. Please check that it includes the correct column names. Note that Upload cohorts only allows you to add new users to an existing cohort and does not allow removal from an existing cohort.';
$string['namefieldempty'] = 'Field name can not be empty';
$string['newnamefor'] = 'New name for cohort {$a}';
$string['newidnumberfor'] = 'New ID number for cohort {$a}';
$string['privacy:metadata:evidence:desca'] = 'The optional parameters of the translatable evidence description';
$string['privacy:metadata:evidence:desccomponent'] = 'The component of the translatable evidence description';
$string['privacy:metadata:evidence:descidentifier'] = 'An identifier of the translatable evidence description';
-$string['privacy:metadata:evidence:grade'] = 'The grade associted with the evidence';
+$string['privacy:metadata:evidence:grade'] = 'The grade associated with the evidence';
$string['privacy:metadata:evidence:note'] = 'A non-localised note attached to the evidence';
$string['privacy:metadata:evidence:url'] = 'A URL associated with the evidence';
$string['privacy:metadata:plan:description'] = 'The description of the learning plan';
$string['pending'] = 'Pending';
$string['periodpostenrolment'] = 'Period post enrolment';
$string['privacy:metadata:completionstate'] = 'If the activity has been completed';
-$string['privacy:metadata:course'] = 'A course identifier.';
+$string['privacy:metadata:course'] = 'A course identifier';
$string['privacy:metadata:coursecompletedsummary'] = 'Stores information about users who have completed criteria in a course';
$string['privacy:metadata:coursemoduleid'] = 'The activity ID';
$string['privacy:metadata:coursemodulesummary'] = 'Stores activity completion data for a user';
$string['notregisteredonhub'] = 'Your administrator needs to register this site with Moodle.net before you can share a course.';
$string['operation'] = 'Actions';
$string['participantnumberaverage'] = 'Average number of participants ({$a})';
+$string['policyagreed'] = 'Privacy notice and data processing agreement';
+$string['policyagreeddesc'] = 'I agree to the <a href="{$a}" target="_blank">Privacy notice and data processing agreement</a> for Moodle.net';
$string['postaladdress'] = 'Postal address';
$string['postaladdress_help'] = 'Postal address of this site, or of the entity represented by this site.';
$string['postsnumber'] = 'Number of posts ({$a})';
$string['idnumbergrouping'] = 'Grouping ID number';
$string['idnumbergrouping_help'] = 'The ID number of a grouping is only used when matching the grouping against external systems and is not displayed anywhere on the site. If the grouping has an official code name it may be entered, otherwise the field can be left blank.';
$string['idnumbermod'] = 'ID number';
-$string['idnumbermod_help'] = 'Setting an ID number provides a way of identifying the activity for grade calculation purposes. If the activity is not included in any grade calculation then the ID number field may be left blank.
+$string['idnumbermod_help'] = 'Setting an ID number provides a way of identifying the activity or resource for purposes such as grade calculation or custom reporting. Otherwise the field may be left blank.
-The ID number can also be set in the gradebook, though it can only be edited on the activity settings page.';
+For gradable activities, the ID number can also be set in the gradebook, though it can only be edited on the activity settings page.';
$string['idnumbertaken'] = 'This ID number is already taken';
$string['imagealt'] = 'Picture description';
$string['import'] = 'Import';
$string['privacy:metadata:events_queue:eventdata'] = 'The data stored in the event.';
$string['privacy:metadata:events_queue:stackdump'] = 'Any stacktrace associated with this event.';
$string['privacy:metadata:events_queue:timecreated'] = 'The time that this event was created.';
-$string['privacy:metadata:events_queue:userid'] = 'The userid associated with this event.';
+$string['privacy:metadata:events_queue:userid'] = 'The user ID associated with this event';
$string['privacy:metadata:log'] = 'A collection of past events';
$string['privacy:metadata:log:action'] = 'A description of the action';
$string['privacy:metadata:log:cmid'] = 'cmid';
$string['personalnotes'] = 'Personal notes';
$string['privacy:metadata:core_notes'] = 'The Notes component stores user notes within the core subsystem.';
$string['privacy:metadata:core_notes:content'] = 'The content of the note.';
-$string['privacy:metadata:core_notes:courseid'] = 'The Id of the course associated with the note.';
+$string['privacy:metadata:core_notes:courseid'] = 'The ID of the course associated with the note';
$string['privacy:metadata:core_notes:created'] = 'The creation date/time for the note.';
$string['privacy:metadata:core_notes:lastmodified'] = 'The last modified date/time for the note.';
$string['privacy:metadata:core_notes:publishstate'] = 'The publish state of the note.';
-$string['privacy:metadata:core_notes:userid'] = 'The Id of the user associated with the note.';
+$string['privacy:metadata:core_notes:userid'] = 'The ID of the user associated with the note';
$string['publishstate'] = 'Context';
$string['publishstate_help'] = 'A note\'s context determines who can see the note in everyday use. Users should be aware that all notes, including personal ones, may be disclosed under the laws of their jurisdictions.
$string['privacy:metadata:preference:showadvanced'] = 'Handle the toggle advanced mode button.';
$string['privacy:metadata:role_assignments'] = 'Role assignments';
$string['privacy:metadata:role_assignments:component'] = 'Plugin responsible for role assignment, empty when manually assigned.';
-$string['privacy:metadata:role_assignments:itemid'] = 'The Id of enrolment/auth instance responsible for this role assignment.';
-$string['privacy:metadata:role_assignments:modifierid'] = 'The Id of the user who created or modified the role assignment.';
-$string['privacy:metadata:role_assignments:roleid'] = 'The Id of the role.';
+$string['privacy:metadata:role_assignments:itemid'] = 'The ID of enrolment/auth instance responsible for this role assignment';
+$string['privacy:metadata:role_assignments:modifierid'] = 'The ID of the user who created or modified the role assignment';
+$string['privacy:metadata:role_assignments:roleid'] = 'The ID of the role';
$string['privacy:metadata:role_assignments:tableexplanation'] = 'This table stores the assigned roles in each context.';
$string['privacy:metadata:role_assignments:timemodified'] = 'The date when the role assignment was created or modified.';
-$string['privacy:metadata:role_assignments:userid'] = 'The Id of the user.';
+$string['privacy:metadata:role_assignments:userid'] = 'The ID of the user';
$string['privacy:metadata:role_capabilities'] = 'Role capabilities';
$string['privacy:metadata:role_capabilities:capability'] = 'The name of the capability.';
-$string['privacy:metadata:role_capabilities:modifierid'] = 'The Id of the user who created or modified the capability.';
+$string['privacy:metadata:role_capabilities:modifierid'] = 'The ID of the user who created or modified the capability';
$string['privacy:metadata:role_capabilities:permission'] = 'The permission for a capability: inherit, allow, prevent or prohibit.';
-$string['privacy:metadata:role_capabilities:roleid'] = 'The Id of the role.';
-$string['privacy:metadata:role_capabilities:tableexplanation'] = 'This table stores the capabilities and the override capabilities for a particular role in a particular context.';
+$string['privacy:metadata:role_capabilities:roleid'] = 'The ID of the role';
+$string['privacy:metadata:role_capabilities:tableexplanation'] = 'The capabilities and override capabilities for a particular role in a particular context';
$string['privacy:metadata:role_capabilities:timemodified'] = 'The date when the capability was created or modified.';
$string['privacy:metadata:role_cohortroles'] = 'Roles to cohort';
$string['globalsearchdisabled'] = 'Global searching is not enabled.';
$string['gradualreindex'] = 'Gradual reindex {$a}';
$string['gradualreindex_confirm'] = 'Are you sure you want to reindex {$a}? This may take some time, although existing data will remain available during the reindex.';
-$string['gradualreindex_queued'] = 'Reindexing has been requested for {$a->name} ({$a->count} contexts). This indexing will be carried out by the ‘Global search indexing’ scheduled task.';
+$string['gradualreindex_queued'] = 'Reindexing has been requested for {$a->name} ({$a->count} contexts). This indexing will be carried out by the "Global search indexing" scheduled task.';
$string['checkdb'] = 'Check database';
$string['checkdbadvice'] = 'Check your database for any problems.';
$string['checkdir'] = 'Check dir';
$string['privacy:draftfilespath'] = 'Draft files';
$string['privacy:lastaccesspath'] = 'Last access to courses';
$string['privacy:metadata:address'] = 'The address of the user.';
-$string['privacy:metadata:aim'] = 'The AIM identifier of the user.';
+$string['privacy:metadata:aim'] = 'The AIM identifier of the user';
$string['privacy:metadata:alternatename'] = 'An alternative name for the user.';
-$string['privacy:metadata:appid'] = 'The app id, usually something like com.moodle.moodlemobile';
+$string['privacy:metadata:appid'] = 'The app ID, usually something like com.moodle.moodlemobile';
$string['privacy:metadata:auth'] = 'The authentication plugin used for this user record.';
$string['privacy:metadata:autosubscribe'] = 'A preference as to if the user should be auto-subscribed to forums the user posts in.';
$string['privacy:metadata:calendartype'] = 'A user preference for the type of calendar to use.';
-$string['privacy:metadata:category'] = 'The category identifier.';
+$string['privacy:metadata:category'] = 'The category identifier';
$string['privacy:metadata:city'] = 'The city of the user.';
$string['privacy:metadata:confirmed'] = 'If this is an active user or not.';
$string['privacy:metadata:country'] = 'The country that the user is in.';
-$string['privacy:metadata:courseid'] = 'An identifier for a course.';
+$string['privacy:metadata:courseid'] = 'Course ID';
$string['privacy:metadata:currentlogin'] = 'The current login for this user.';
$string['privacy:metadata:data'] = 'Data relating to the custom user field from the user.';
$string['privacy:metadata:deleted'] = 'A flag to show if the user has been deleted or not.';
$string['privacy:metadata:fullname'] = 'The fullname for this course.';
$string['privacy:metadata:hash'] = 'A hash of a previous password.';
$string['privacy:metadata:icq'] = 'The ICQ number of the user.';
-$string['privacy:metadata:id'] = 'The identifier for the user.';
-$string['privacy:metadata:idnumber'] = 'An identification number given by the institution.';
+$string['privacy:metadata:id'] = 'The user ID';
+$string['privacy:metadata:idnumber'] = 'An identification number given by the institution';
$string['privacy:metadata:imagealt'] = 'Alternative text for the user\'s image.';
$string['privacy:metadata:infotablesummary'] = 'Stores custom user information.';
$string['privacy:metadata:institution'] = 'The institution that this user is a member of.';
$string['privacy:metadata:lastnamephonetic'] = 'The phonetic details about the user\'s surname.';
$string['privacy:metadata:maildigest'] = 'A setting for the mail digest for this user.';
$string['privacy:metadata:maildisplay'] = 'A preference for the user about displaying their email address to other users.';
-$string['privacy:metadata:middlename'] = 'The middle name of the user.';
-$string['privacy:metadata:mnethostid'] = 'An identifier for the mnet host if used.';
+$string['privacy:metadata:middlename'] = 'The middle name of the user';
+$string['privacy:metadata:mnethostid'] = 'An identifier for the MNet host if used';
$string['privacy:metadata:model'] = 'The device name, occam or iPhone etc..';
-$string['privacy:metadata:msn'] = 'The MSN identifier of the user.';
+$string['privacy:metadata:msn'] = 'The MSN identifier of the user';
$string['privacy:metadata:my_pages'] = 'User pages - dashboard and profile. This table does not contain personal data and only used to link dashboard blocks to users';
$string['privacy:metadata:my_pages:name'] = 'Page name';
$string['privacy:metadata:my_pages:private'] = 'Whether or not the page is private (dashboard) or public (profile)';
$string['privacy:metadata:picture'] = 'The picture details associated with this user.';
$string['privacy:metadata:platform'] = 'The device platform, Android or iOS etc';
$string['privacy:metadata:policyagreed'] = 'A flag to determine if the user has agreed to the site policy.';
-$string['privacy:metadata:pushid'] = 'The device PUSH token/key/identifier/registration id';
+$string['privacy:metadata:pushid'] = 'The device PUSH token/key/identifier/registration ID';
$string['privacy:metadata:reason'] = 'The reason for requesting this course.';
-$string['privacy:metadata:requester'] = 'An identifier to a user that requested this course.';
+$string['privacy:metadata:requester'] = 'The ID of the user who requested the course';
$string['privacy:metadata:requestsummary'] = 'Stores information about requests for courses that users make.';
$string['privacy:metadata:suspended'] = 'A flag to show if the user has been suspended on this system.';
$string['privacy:metadata:user_preferences'] = 'Preferences associated with the given user';
$string['privacy:metadata:user_preferences:name'] = 'Preference name';
-$string['privacy:metadata:user_preferences:userid'] = 'User id';
+$string['privacy:metadata:user_preferences:userid'] = 'The user ID';
$string['privacy:metadata:user_preferences:value'] = 'Preference value';
$string['privacy:metadata:username'] = 'The username for this user.';
$string['privacy:metadata:secret'] = 'Secret.. not sure.';
$string['privacy:metadata:sessiontablesummary'] = 'Database based session storage';
$string['privacy:metadata:shortname'] = 'A short name for the course.';
$string['privacy:metadata:sid'] = 'The session ID';
-$string['privacy:metadata:skype'] = 'The skype identifier of the user.';
+$string['privacy:metadata:skype'] = 'The Skype identifier of the user';
$string['privacy:metadata:state'] = '0 means a normal session';
$string['privacy:metadata:summary'] = 'A description of the course.';
$string['privacy:metadata:theme'] = 'A user preference for the theme to display.';
$string['privacy:metadata:timemodified'] = 'The time this records was modified.';
$string['privacy:metadata:timererequested'] = 'The time the user re-requested the password reset.';
$string['privacy:metadata:timerequested'] = 'The time that the user first requested this password reset';
-$string['privacy:metadata:timezone'] = 'The timezone that the user resides in.';
+$string['privacy:metadata:timezone'] = 'The timezone of the user';
$string['privacy:metadata:token'] = 'secret set and emailed to user';
$string['privacy:metadata:trackforums'] = 'A preference for forums and tracking them.';
$string['privacy:metadata:trustbitmask'] = 'The trust bit mask';
-$string['privacy:metadata:yahoo'] = 'The yahoo identifier of the user.';
+$string['privacy:metadata:yahoo'] = 'The Yahoo identifier of the user';
$string['privacy:metadata:url'] = 'A URL related to this user.';
$string['privacy:metadata:userid'] = 'The user ID linked to this table.';
$string['privacy:metadata:usertablesummary'] = 'This table stores the main personal data about the user.';
$string['privacy:metadata:user_private_key:userid'] = 'The user associated with the key.';
$string['privacy:metadata:user_private_key:instance'] = 'The instance of the script.';
$string['privacy:metadata:user_private_key:iprestriction'] = 'The IP address range that this key can be used from.';
-$string['privacy:metadata:user_private_key:validuntil'] = 'The date and time that the private key is valid until.';
+$string['privacy:metadata:user_private_key:validuntil'] = 'The date that the private key is valid until';
$string['privacy:metadata:user_private_key:timecreated'] = 'The date and time that the key was created.';
$string['privacy:metadata:user_private_key'] = 'Private keys for the user.';
$string['userkey'] = 'User key';
$string['privacy:metadata:tokens:creatorid'] = 'The ID of the user who created the token';
$string['privacy:metadata:tokens:iprestriction'] = 'IP restricted to use this token';
$string['privacy:metadata:tokens:lastaccess'] = 'The date at which the token was last used';
-$string['privacy:metadata:tokens:privatetoken'] = 'A more private token occasionally used to validate certain operations, such as SSO.';
+$string['privacy:metadata:tokens:privatetoken'] = 'A more private token occasionally used to validate certain operations, such as SSO';
$string['privacy:metadata:tokens:timecreated'] = 'The date at which the token was created';
$string['privacy:metadata:tokens:token'] = 'The user\'s token';
$string['privacy:metadata:tokens:tokentype'] = 'The type of token';
$string['privacy:metadata:tokens:userid'] = 'The ID of the user whose token it is';
-$string['privacy:metadata:tokens:validuntil'] = 'The date at which the token becomes invalid';
+$string['privacy:metadata:tokens:validuntil'] = 'The date that the token is valid until';
$string['privacy:request:notexportedsecurity'] = 'Not exported for security reasons';
$string['protocol'] = 'Protocol';
$string['removefunction'] = 'Remove';
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.0
*/
-define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/yui'],
- function($, ajax, templates, notification, str, Y) {
+define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/modal_factory', 'core/modal_events'],
+ function($, ajax, templates, notification, str, ModalFactory, ModalEvents) {
return /** @alias module:core/tag */ {
/**
return;
}
var tempElement = $("<input type='hidden'/>").attr('name', this.name);
+ var saveButtonText = '';
+ var tagOptions = [];
+ tags.each(function() {
+ var tagid = $(this).val(),
+ tagname = $('.inplaceeditable[data-itemtype=tagname][data-itemid=' + tagid + ']').attr('data-value');
+ tagOptions.push({
+ id: tagid,
+ name: tagname
+ });
+ });
+
str.get_strings([
{key: 'combineselected', component: 'tag'},
- {key: 'selectmaintag', component: 'tag'},
- {key: 'continue'},
- {key: 'cancel'},
- ]).done(function(s) {
- var el = $('<div><form id="combinetags_form" class="form-inline">' +
- '<p class="description"></p><p class="options"></p>' +
- '<p class="mdl-align"><input type="submit" class="btn btn-primary" id="combinetags_submit"/>' +
- '<input type="button" class="btn btn-secondary" id="combinetags_cancel"/></p>' +
- '</form></div>');
- el.find('.description').html(s[1]);
- el.find('#combinetags_submit').attr('value', s[2]);
- el.find('#combinetags_cancel').attr('value', s[3]);
- var fldset = el.find('.options');
- tags.each(function() {
- var tagid = $(this).val(),
- tagname = $('.inplaceeditable[data-itemtype=tagname][data-itemid=' + tagid + ']').attr('data-value');
- fldset.append($('<input type="radio" name="maintag" id="combinetags_maintag_' + tagid + '" value="' +
- tagid + '"/><label for="combinetags_maintag_' + tagid + '">' + tagname + '</label><br>'));
+ {key: 'continue'}
+ ]).then(function(langStrings) {
+ var modalTitle = langStrings[0];
+ saveButtonText = langStrings[1];
+ var templateContext = {
+ tags: tagOptions
+ };
+ return ModalFactory.create({
+ title: modalTitle,
+ body: templates.render('core_tag/combine_tags', templateContext),
+ type: ModalFactory.types.SAVE_CANCEL
});
- // TODO: MDL-57778 Convert to core/modal.
- Y.use('moodle-core-notification-dialogue', function() {
- var panel = new M.core.dialogue({
- draggable: true,
- modal: true,
- closeButton: true,
- headerContent: s[0],
- bodyContent: el.html()
- });
- panel.show();
- $('#combinetags_form input[type=radio]').first().focus().prop('checked', true);
- $('#combinetags_form #combinetags_cancel').on('click', function() {
- panel.destroy();
- });
- $('#combinetags_form').on('submit', function() {
- tempElement.appendTo(form);
- var maintag = $('input[name=maintag]:checked', '#combinetags_form').val();
- $("<input type='hidden'/>").attr('name', 'maintag').attr('value', maintag).appendTo(form);
- form.submit();
- return false;
- });
+ }).then(function(modal) {
+ modal.setSaveButtonText(saveButtonText);
+
+ // Handle save event.
+ modal.getRoot().on(ModalEvents.save, function(e) {
+ e.preventDefault();
+
+ // Append this temp element in the form in the tags list, not the form in the modal. Confusing, right?!?
+ tempElement.appendTo(form);
+ // Get the selected tag from the modal.
+ var maintag = $('input[name=maintag]:checked', '#combinetags_form').val();
+ // Append this in the tags list form.
+ $("<input type='hidden'/>").attr('name', 'maintag').attr('value', maintag).appendTo(form);
+ // Submit the tags list form.
+ form.submit();
});
- });
+
+ // Handle hidden event.
+ modal.getRoot().on(ModalEvents.hidden, function() {
+ // Destroy when hidden.
+ modal.destroy();
+ });
+
+ modal.show();
+ // Tick the first option.
+ $('#combinetags_form input[type=radio]').first().focus().prop('checked', true);
+
+ return;
+
+ }).catch(notification.exception);
});
// When user changes tag name to some name that already exists suggest to combine the tags.
// Form for adding standard tags.
$('body').on('click', 'a[data-action=addstandardtag]', function(e) {
e.preventDefault();
+
+ var saveButtonText = '';
str.get_strings([
{key: 'addotags', component: 'tag'},
- {key: 'inputstandardtags', component: 'tag'},
- {key: 'continue'},
- {key: 'cancel'},
- ]).done(function(s) {
- var el = $('<div><form id="addtags_form" class="form-inline" method="POST">' +
- '<input type="hidden" name="action" value="addstandardtag"/>' +
- '<input type="hidden" name="sesskey" value="' + M.cfg.sesskey + '"/>' +
- '<p><label for="id_tagslist">' + s[1] + '</label>' +
- '<input type="text" id="id_tagslist" name="tagslist"/></p>' +
- '<p class="mdl-align"><input type="submit" class="btn btn-primary" id="addtags_submit"/>' +
- '<input type="button" class="btn btn-secondary" id="addtags_cancel"/></p>' +
- '</form></div>');
- el.find('#addtags_form').attr('action', window.location.href);
- el.find('#addtags_submit').attr('value', s[2]);
- el.find('#addtags_cancel').attr('value', s[3]);
- // TODO: MDL-57778 Convert to core/modal.
- Y.use('moodle-core-notification-dialogue', function() {
- var panel = new M.core.dialogue({
- draggable: true,
- modal: true,
- closeButton: true,
- headerContent: s[0],
- bodyContent: el.html()
- });
- panel.show();
- $('#addtags_form input[type=text]').focus();
- $('#addtags_form #addtags_cancel').on('click', function() {
- panel.destroy();
+ {key: 'continue'}
+ ]).then(function(langStrings) {
+ var modalTitle = langStrings[0];
+ saveButtonText = langStrings[1];
+ var templateContext = {
+ actionurl: window.location.href,
+ sesskey: M.cfg.sesskey
+ };
+ return ModalFactory.create({
+ title: modalTitle,
+ body: templates.render('core_tag/add_tags', templateContext),
+ type: ModalFactory.types.SAVE_CANCEL
+ });
+ }).then(function(modal) {
+ modal.setSaveButtonText(saveButtonText);
+
+ // Handle save event.
+ modal.getRoot().on(ModalEvents.save, function(e) {
+ var tagsInput = $(e.currentTarget).find('#id_tagslist');
+ var name = tagsInput.val().trim();
+
+ // Set the text field's value to the trimmed value.
+ tagsInput.val(name);
+
+ // Add submit event listener to the form.
+ var tagsForm = $('#addtags_form');
+ tagsForm.on('submit', function(e) {
+ // Validate the form.
+ var form = $('#addtags_form');
+ if (form[0].checkValidity() === false) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ form.addClass('was-validated');
+
+ // BS2 compatibility.
+ $('[data-region="tagslistinput"]').addClass('error');
+ var errorMessage = $('#id_tagslist_error_message');
+ errorMessage.removeAttr('hidden');
+ errorMessage.addClass('help-block');
});
+
+ // Try to submit the form.
+ tagsForm.submit();
+
+ return false;
});
- });
+
+ // Handle hidden event.
+ modal.getRoot().on(ModalEvents.hidden, function() {
+ // Destroy when hidden.
+ modal.destroy();
+ });
+
+ modal.show();
+
+ return;
+
+ }).catch(notification.exception);
});
},
$('body').on('click', '.addtagcoll > a', function(e) {
e.preventDefault();
- var href = $(this).attr('data-url') + '&sesskey=' + M.cfg.sesskey;
- str.get_strings([
- {key: 'addtagcoll', component: 'tag'},
- {key: 'name'},
- {key: 'searchable', component: 'tag'},
- {key: 'create'},
- {key: 'cancel'},
- ]).done(function(s) {
- var el = $('<div><form id="addtagcoll_form" class="form-inline">' +
- '<p><label for="addtagcoll_name"></label>: ' +
- '<input id="addtagcoll_name" type="text"/></p>' +
- '<p><label for="addtagcoll_searchable"></label>: ' +
- '<input id="addtagcoll_searchable" type="checkbox" value="1" checked/></p>' +
- '<p class="mdl-align"><input type="submit" class="btn btn-primary" id="addtagcoll_submit"/>' +
- '<input type="button" class="btn btn-secondary" id="addtagcoll_cancel"/></p>' +
- '</form></div>');
- el.find('label[for="addtagcoll_name"]').html(s[1]);
- el.find('label[for="addtagcoll_searchable"]').html(s[2]);
- el.find('#addtagcoll_submit').attr('value', s[3]);
- el.find('#addtagcoll_cancel').attr('value', s[4]);
- // TODO: MDL-57778 Convert to core/modal.
- Y.use('moodle-core-notification-dialogue', function() {
- var panel = new M.core.dialogue({
- draggable: true,
- modal: true,
- closeButton: true,
- headerContent: s[0],
- bodyContent: el.html()
- });
- panel.show();
- $('#addtagcoll_form #addtagcoll_name').focus();
- $('#addtagcoll_form #addtagcoll_cancel').on('click', function() {
- panel.destroy();
- });
- $('#addtagcoll_form').on('submit', function() {
- var name = $('#addtagcoll_form #addtagcoll_name').val();
- var searchable = $('#addtagcoll_form #addtagcoll_searchable').prop('checked') ? 1 : 0;
- if (String(name).length > 0) {
- window.location.href = href + "&name=" + encodeURIComponent(name) + "&searchable=" + searchable;
- }
- return false;
- });
- });
+ var keys = [
+ {
+ key: 'addtagcoll',
+ component: 'tag'
+ },
+ {
+ key: 'create'
}
- );
+ ];
+
+ var href = $(this).attr('data-url');
+ var saveButtonText = '';
+ str.get_strings(keys).then(function(langStrings) {
+ var modalTitle = langStrings[0];
+ saveButtonText = langStrings[1];
+ var templateContext = {
+ actionurl: href,
+ sesskey: M.cfg.sesskey
+ };
+ return ModalFactory.create({
+ title: modalTitle,
+ body: templates.render('core_tag/add_tag_collection', templateContext),
+ type: ModalFactory.types.SAVE_CANCEL
+ });
+ }).then(function(modal) {
+ modal.setSaveButtonText(saveButtonText);
+
+ // Handle save event.
+ modal.getRoot().on(ModalEvents.save, function(e) {
+ var collectionInput = $(e.currentTarget).find('#addtagcoll_name');
+ var name = collectionInput.val().trim();
+ // Set the text field's value to the trimmed value.
+ collectionInput.val(name);
+
+ // Add submit event listener to the form.
+ var form = $('#addtagcoll_form');
+ form.on('submit', function(e) {
+ // Validate the form.
+ if (form[0].checkValidity() === false) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ form.addClass('was-validated');
+
+ // BS2 compatibility.
+ $('[data-region="addtagcoll_nameinput"]').addClass('error');
+ var errorMessage = $('#id_addtagcoll_name_error_message');
+ errorMessage.removeAttr('hidden');
+ errorMessage.addClass('help-block');
+ });
+
+ // Try to submit the form.
+ form.submit();
+
+ return false;
+ });
+
+ // Handle hidden event.
+ modal.getRoot().on(ModalEvents.hidden, function() {
+ // Destroy when hidden.
+ modal.destroy();
+ });
+
+ modal.show();
+
+ return;
+ }).catch(notification.exception);
});
$('body').on('click', '.tag-collections-table .action_delete', function(e) {
/** @var Fields used in a site registration form.
* IMPORTANT: any new fields with non-empty defaults have to be added to CONFIRM_NEW_FIELDS */
const FORM_FIELDS = ['name', 'description', 'contactname', 'contactemail', 'contactphone', 'imageurl', 'privacy', 'street',
- 'regioncode', 'countrycode', 'geolocation', 'contactable', 'emailalert', 'emailalertemail', 'commnews', 'commnewsemail', 'language'];
+ 'regioncode', 'countrycode', 'geolocation', 'contactable', 'emailalert', 'emailalertemail', 'commnews', 'commnewsemail',
+ 'language', 'policyagreed'];
/** @var List of new FORM_FIELDS or siteinfo fields added indexed by the version when they were added.
* If site was already registered, admin will be promted to confirm new registration data manually. Until registration is manually confirmed,
'language' => explode('_', current_language())[0],
'geolocation' => '',
'emailalert' => 1,
- 'commnews' => 1
+ 'commnews' => 1,
+ 'policyagreed' => 0
]);
$mform->addElement('hidden', 'imageurl', ''); // TODO: temporary.
$mform->setType('imageurl', PARAM_URL);
+ $mform->addElement('checkbox', 'policyagreed', get_string('policyagreed', 'hub'),
+ get_string('policyagreeddesc', 'hub', HUB_MOODLEORGHUBURL . '/privacy'));
+ $mform->addRule('policyagreed', $strrequired, 'required', null, 'client');
+
$mform->addElement('header', 'sitestats', get_string('sendfollowinginfo', 'hub'));
$mform->setExpanded('sitestats', !empty($highlightfields));
$mform->addElement('static', 'urlstring', get_string('siteurl', 'hub'), $siteinfo['url']);
if (empty($siteinfo['commnewsnewemail'])) {
$siteinfo['commnewsemail'] = '';
}
- $this->set_data($siteinfo);
+
+ // Set data. Always require to check policyagreed even if it was checked earlier.
+ $this->set_data(['policyagreed' => 0] + $siteinfo);
}
/**
if (debugging('', DEBUG_DEVELOPER)) {
// Display debugging message for developers who added fields to the form and forgot to add them to registration::FORM_FIELDS.
- $keys = array_diff(array_keys((array)$data), ['returnurl', 'mform_isexpanded_id_sitestats', 'submitbutton', 'update']);
+ $keys = array_diff(array_keys((array)$data),
+ ['returnurl', 'mform_isexpanded_id_sitestats', 'submitbutton', 'update']);
if ($extrafields = array_diff($keys, registration::FORM_FIELDS)) {
debugging('Found extra fields in the form results: ' . join(', ', $extrafields), DEBUG_DEVELOPER);
}
'core:i/item' => 'fa-circle',
'core:i/loading' => 'fa-circle-o-notch fa-spin',
'core:i/loading_small' => 'fa-circle-o-notch fa-spin',
+ 'core:i/location' => 'fa-map-marker',
'core:i/lock' => 'fa-lock',
'core:i/log' => 'fa-list-alt',
'core:i/mahara_host' => 'fa-id-badge',
<?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20180403" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20180618" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="subscriptionid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The event_subscription id this event is associated with."/>
<FIELD NAME="priority" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The event's display priority. For multiple events with the same module name, instance and eventtype (e.g. for group overrides), the one with the higher priority will be displayed."/>
+ <FIELD NAME="location" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Event Location"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+ <KEY NAME="fk_user" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
+ <INDEXES>
+ <INDEX NAME="ix_concomitem" UNIQUE="false" FIELDS="contextid, commentarea, itemid" COMMENT="Allows the comments API to load comments for a particular area effectively."/>
+ </INDEXES>
</TABLE>
<TABLE NAME="external_services" COMMENT="built in and custom external services">
<FIELDS>
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
+ if ($oldversion < 2018062800.01) {
+ // Add foreign key fk_user to the comments table.
+ $table = new xmldb_table('comments');
+ $key = new xmldb_key('fk_user', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id'));
+ $dbman->add_key($table, $key);
+
+ upgrade_main_savepoint(true, 2018062800.01);
+ }
+
+ if ($oldversion < 2018062800.02) {
+ // Add composite index ix_concomitem to the table comments.
+ $table = new xmldb_table('comments');
+ $index = new xmldb_index('ix_concomitem', XMLDB_INDEX_NOTUNIQUE, array('contextid', 'commentarea', 'itemid'));
+
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ upgrade_main_savepoint(true, 2018062800.02);
+ }
+
+ if ($oldversion < 2018062800.03) {
+ // Define field location to be added to event.
+ $table = new xmldb_table('event');
+ $field = new xmldb_field('location', XMLDB_TYPE_TEXT, null, null, null, null, null, 'priority');
+
+ // Conditionally launch add field location.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // Main savepoint reached.
+ upgrade_main_savepoint(true, 2018062800.03);
+ }
+
return true;
}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class null_filter_manager {
+ /**
+ * As for the equivalent {@link filter_manager} method.
+ *
+ * @param string $text The text to filter
+ * @param context $context not used.
+ * @param array $options not used
+ * @param array $skipfilters not used
+ * @return string resulting text.
+ */
public function filter_text($text, $context, array $options = array(),
array $skipfilters = null) {
return $text;
}
+ /**
+ * As for the equivalent {@link filter_manager} method.
+ *
+ * @param string $string The text to filter
+ * @param context $context not used.
+ * @return string resulting string
+ */
public function filter_string($string, $context) {
return $string;
}
+ /**
+ * As for the equivalent {@link filter_manager} method.
+ *
+ * @deprecated Since Moodle 3.0 MDL-50491.
+ */
public function text_filtering_hash() {
throw new coding_exception('filter_manager::text_filtering_hash() can not be used any more');
}
/**
* Override this function to actually implement the filtering.
*
- * @param $text some HTML content.
+ * @param string $text some HTML content to process.
* @param array $options options passed to the filters
- * @return the HTML content after the filtering has been applied.
+ * @return string the HTML content after the filtering has been applied.
*/
public abstract function filter($text, array $options = array());
}
* sorted in alphabetical order of name.
*/
function filter_get_all_installed() {
- global $CFG;
-
$filternames = array();
foreach (core_component::get_plugin_list('filter') as $filter => $fulldir) {
if (is_readable("$fulldir/filter.php")) {
* @return array where the keys and values are both the filter name, like 'tex'.
*/
function filter_get_globally_enabled() {
- static $enabledfilters = null;
- if (is_null($enabledfilters)) {
- $filters = filter_get_global_states();
- $enabledfilters = array();
- foreach ($filters as $filter => $filerinfo) {
- if ($filerinfo->active != TEXTFILTER_DISABLED) {
- $enabledfilters[$filter] = $filter;
- }
+ $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'core_filter', 'global_filters');
+ $enabledfilters = $cache->get('enabled');
+ if ($enabledfilters !== false) {
+ return $enabledfilters;
+ }
+
+ $filters = filter_get_global_states();
+ $enabledfilters = array();
+ foreach ($filters as $filter => $filerinfo) {
+ if ($filerinfo->active != TEXTFILTER_DISABLED) {
+ $enabledfilters[$filter] = $filter;
}
}
+
+ $cache->set('enabled', $enabledfilters);
return $enabledfilters;
}
// A list of open/close tags that we should not replace within
// Extended to include <script>, <textarea>, <select> and <a> tags
// Regular expression allows tags with or without attributes
- $filterignoretagsopen = array('<head>' , '<nolink>' , '<span class="nolink">',
+ $filterignoretagsopen = array('<head>' , '<nolink>' , '<span(\s[^>]*?)?class="nolink"(\s[^>]*?)?>',
'<script(\s[^>]*?)?>', '<textarea(\s[^>]*?)?>',
'<select(\s[^>]*?)?>', '<a(\s[^>]*?)?>');
$filterignoretagsclose = array('</head>', '</nolink>', '</span>',
}
/**
- * @todo Document this function
- * @param array $linkarray
- * @return array
+ * Remove duplicate from a list of {@link filterobject}.
+ *
+ * @param filterobject[] $linkarray a list of filterobject.
+ * @return filterobject[] the same list, but with dupicates removed.
*/
function filter_remove_duplicates($linkarray) {
if (count($options->repositories) > 0) {
$straddlink = get_string('choosealink', 'repository');
$str .= <<<EOD
-<button id="filepicker-button-js-{$clientid}" class="visibleifjs btn btn-secondary">
+<button type="button" id="filepicker-button-js-{$clientid}" class="visibleifjs btn btn-secondary">
$straddlink
</button>
EOD;
/**
* Invalidates browser caches and cached data in temp.
*
- * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
- * {@link phpunit_util::reset_dataroot()}
- *
* @return void
*/
function purge_all_caches() {
- global $CFG, $DB;
+ purge_caches();
+}
- reset_text_filters_cache();
- js_reset_all_caches();
- theme_reset_all_caches();
- get_string_manager()->reset_caches();
+/**
+ * Selectively invalidate different types of cache.
+ *
+ * Purges the cache areas specified. By default, this will purge all caches but can selectively purge specific
+ * areas alone or in combination.
+ *
+ * @param bool[] $options Specific parts of the cache to purge. Valid options are:
+ * 'muc' Purge MUC caches?
+ * 'theme' Purge theme cache?
+ * 'lang' Purge language string cache?
+ * 'js' Purge javascript cache?
+ * 'filter' Purge text filter cache?
+ * 'other' Purge all other caches?
+ */
+function purge_caches($options = []) {
+ $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'filter', 'other'], false);
+ if (empty(array_filter($options))) {
+ $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
+ } else {
+ $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
+ }
+ if ($options['muc']) {
+ cache_helper::purge_all();
+ }
+ if ($options['theme']) {
+ theme_reset_all_caches();
+ }
+ if ($options['lang']) {
+ get_string_manager()->reset_caches();
+ }
+ if ($options['js']) {
+ js_reset_all_caches();
+ }
+ if ($options['filter']) {
+ reset_text_filters_cache();
+ }
+ if ($options['other']) {
+ purge_other_caches();
+ }
+}
+
+/**
+ * Purge all non-MUC caches not otherwise purged in purge_caches.
+ *
+ * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
+ * {@link phpunit_util::reset_dataroot()}
+ */
+function purge_other_caches() {
+ global $DB, $CFG;
core_text::reset_caches();
if (class_exists('core_plugin_manager')) {
core_plugin_manager::reset_caches();
}
$DB->reset_caches();
- cache_helper::purge_all();
// Purge all other caches: rss, simplepie, etc.
clearstatcache();
}
if ($navoptions->grades) {
$url = new moodle_url('/grade/report/index.php', array('id'=>$course->id));
- $gradenode = $coursenode->add(get_string('grades'), $url, self::TYPE_SETTING, null, 'grades', new pix_icon('i/grades', ''));
+ $gradenode = $coursenode->add(get_string('grades'), $url, self::TYPE_SETTING, null,
+ 'grades', new pix_icon('i/grades', ''));
+ // If the page type matches the grade part, then make the nav drawer grade node (incl. all sub pages) active.
+ if (strpos($this->page->pagetype, 'grade-') === 0) {
+ $gradenode->make_active();
+ }
}
return true;
*/
function theme_reset_all_caches() {
global $CFG, $PAGE;
+ require_once("{$CFG->libdir}/filelib.php");
$next = theme_get_next_revision();
theme_set_revision($next);
// Purge compiled post processed css.
cache::make('core', 'postprocessedcss')->purge();
+ // Delete all old theme localcaches.
+ $themecachedirs = glob("{$CFG->localcachedir}/theme/*", GLOB_ONLYDIR);
+ foreach ($themecachedirs as $localcachedir) {
+ fulldelete($localcachedir);
+ }
+
if ($PAGE) {
$PAGE->reload_theme();
}
*/
public $remapiconcache = [];
+ /**
+ * The name of the function to call to get precompiled CSS.
+ * @var string
+ */
+ public $precompiledcsscallback = null;
+
/**
* Load the config.php file for a particular theme, and return an instance
* of this class. (That is, this is a factory method.)
'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'uarrow', 'darrow',
'hidefromselector', 'doctype', 'yuicssmodules', 'blockrtlmanipulations',
'lessfile', 'extralesscallback', 'lessvariablescallback', 'blockrendermethod',
- 'scss', 'extrascsscallback', 'prescsscallback', 'csstreepostprocessor', 'addblockposition', 'iconsystem');
+ 'scss', 'extrascsscallback', 'prescsscallback', 'csstreepostprocessor', 'addblockposition',
+ 'iconsystem', 'precompiledcsscallback');
foreach ($config as $key=>$value) {
if (in_array($key, $configurable)) {
} else {
if ($type === 'theme' && $identifier === self::SCSS_KEY) {
// We need the content from SCSS because this is the SCSS file from the theme.
- $csscontent .= $this->get_css_content_from_scss(false);
+ if ($compiled = $this->get_css_content_from_scss(false)) {
+ $csscontent .= $compiled;
+ } else {
+ // The compiler failed so default back to any precompiled css that might
+ // exist.
+ $csscontent .= $this->get_precompiled_css_content();
+ }
} else if ($type === 'theme' && $identifier === $this->lessfile) {
// We need the content from LESS because this is the LESS file from the theme.
$csscontent .= $this->get_css_content_from_less(false);
return $compiled;
}
+ /**
+ * Return the precompiled CSS if the precompiledcsscallback exists.
+ *
+ * @return string Return compiled css.
+ */
+ public function get_precompiled_css_content() {
+ $configs = [$this] + $this->parent_configs;
+ $css = '';
+
+ foreach ($configs as $config) {
+ if (isset($config->precompiledcsscallback)) {
+ $function = $config->precompiledcsscallback;
+ if (function_exists($function)) {
+ $css .= $function($this);
+ }
+ }
+ }
+ return $css;
+ }
+
/**
* Get the icon system to use.
*
global $CFG;
if ($this->mustache === null) {
+ require_once("{$CFG->libdir}/filelib.php");
+
$themename = $this->page->theme->name;
$themerev = theme_get_revision();
+ // Create new localcache directory.
$cachedir = make_localcache_directory("mustache/$themerev/$themename");
+ // Remove old localcache directories.
+ $mustachecachedirs = glob("{$CFG->localcachedir}/mustache/*", GLOB_ONLYDIR);
+ foreach ($mustachecachedirs as $localcachedir) {
+ $cachedrev = [];
+ preg_match("/\/mustache\/([0-9]+)$/", $localcachedir, $cachedrev);
+ $cachedrev = isset($cachedrev[1]) ? intval($cachedrev[1]) : 0;
+ if ($cachedrev > 0 && $cachedrev < $themerev) {
+ fulldelete($localcachedir);
+ }
+ }
+
$loader = new \core\output\mustache_filesystem_loader();
$stringhelper = new \core\output\mustache_string_helper();
$quotehelper = new \core\output\mustache_quote_helper();
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Steps definitions related to filters.
+ *
+ * @package core
+ * @category test
+ * @copyright 2018 the Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// Note: You cannot use MOODLE_INTERNAL test here, or include files which do so.
+// This file is required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../behat/behat_base.php');
+
+/**
+ * Steps definitions related to filters.
+ *
+ * @package core
+ * @category test
+ * @copyright 2018 the Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_filters extends behat_base {
+
+ /**
+ * Set the global filter configuration.
+ *
+ * @Given /^the "(?P<filter_name>(?:[^"]|\\")*)" filter is "(on|off|disabled)"$/
+ *
+ * @param string $filtername the name of a filter, e.g. 'glossary'.
+ * @param string $statename 'on', 'off' or 'disabled'.
+ */
+ public function the_filter_is($filtername, $statename) {
+ require_once(__DIR__ . '/../../filterlib.php');
+
+ switch ($statename) {
+ case 'on':
+ $state = TEXTFILTER_ON;
+ break;
+ case 'off':
+ $state = TEXTFILTER_OFF;
+ break;
+ case 'disabled':
+ $state = TEXTFILTER_DISABLED;
+ break;
+ default:
+ throw new coding_exception('Unknown filter state: ' . $statename);
+ }
+ filter_set_global_state($filtername, $state);
+ }
+}