$optionalsubsystems->add(new admin_setting_configcheckbox('messaging', new lang_string('messaging', 'admin'), new lang_string('configmessaging','admin'), 1));
- $optionalsubsystems->add(new admin_setting_configcheckbox('messaginghidereadnotifications', new lang_string('messaginghidereadnotifications', 'admin'), new lang_string('configmessaginghidereadnotifications','admin'), 0));
+ $optionalsubsystems->add(new admin_setting_configcheckbox('messagingallusers',
+ new lang_string('messagingallusers', 'admin'),
+ new lang_string('configmessagingallusers', 'admin'),
+ 0)
+ );
$options = array(DAYSECS=>new lang_string('secondstotime86400'), WEEKSECS=>new lang_string('secondstotime604800'), 2620800=>new lang_string('nummonths', 'moodle', 1), 15724800=>new lang_string('nummonths', 'moodle', 6),0=>new lang_string('never'));
$optionalsubsystems->add(new admin_setting_configselect('messagingdeletereadnotificationsdelay', new lang_string('messagingdeletereadnotificationsdelay', 'admin'), new lang_string('configmessagingdeletereadnotificationsdelay', 'admin'), 604800, $options));
--- /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/>.
+
+/**
+ * AMD module for data registry defaults actions.
+ *
+ * @module tool_dataprivacy/defaultsactions
+ * @package tool_dataprivacy
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+ 'jquery',
+ 'core/ajax',
+ 'core/notification',
+ 'core/str',
+ 'core/modal_factory',
+ 'core/modal_events',
+ 'core/templates'],
+function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
+
+ /**
+ * List of action selectors.
+ *
+ * @type {{EDIT_LEVEL_DEFAULTS: string}}
+ * @type {{NEW_ACTIVITY_DEFAULTS: string}}
+ * @type {{EDIT_ACTIVITY_DEFAULTS: string}}
+ * @type {{DELETE_ACTIVITY_DEFAULTS: string}}
+ */
+ var ACTIONS = {
+ EDIT_LEVEL_DEFAULTS: '[data-action="edit-level-defaults"]',
+ NEW_ACTIVITY_DEFAULTS: '[data-action="new-activity-defaults"]',
+ EDIT_ACTIVITY_DEFAULTS: '[data-action="edit-activity-defaults"]',
+ DELETE_ACTIVITY_DEFAULTS: '[data-action="delete-activity-defaults"]'
+ };
+
+ /** @type {{INHERIT: Number}} **/
+ var INHERIT = -1;
+
+ /**
+ * DefaultsActions class.
+ */
+ var DefaultsActions = function() {
+ this.registerEvents();
+ };
+
+ /**
+ * Register event listeners.
+ */
+ DefaultsActions.prototype.registerEvents = function() {
+ $(ACTIONS.EDIT_LEVEL_DEFAULTS).click(function(e) {
+ e.preventDefault();
+
+ var button = $(this);
+ var contextLevel = button.data('contextlevel');
+ var category = button.data('category');
+ var purpose = button.data('purpose');
+
+ // Get options.
+ var requests = [
+ {methodname: 'tool_dataprivacy_get_category_options', args: {}},
+ {methodname: 'tool_dataprivacy_get_purpose_options', args: {}}
+ ];
+
+ var promises = Ajax.call(requests);
+ var titlePromise = Str.get_string('editdefaults', 'tool_dataprivacy', $('#defaults-header').text());
+ $.when(promises[0], promises[1], titlePromise).then(function(categoryResponse, purposeResponse, title) {
+ var categories = categoryResponse.options;
+ var purposes = purposeResponse.options;
+ showDefaultsFormModal(title, contextLevel, category, purpose, null, categories, purposes, null);
+
+ return true;
+ }).catch(Notification.exception);
+ });
+
+ $(ACTIONS.NEW_ACTIVITY_DEFAULTS).click(function(e) {
+ e.preventDefault();
+
+ var button = $(this);
+ var contextLevel = button.data('contextlevel');
+
+ // Get options.
+ var requests = [
+ {methodname: 'tool_dataprivacy_get_category_options', args: {}},
+ {methodname: 'tool_dataprivacy_get_purpose_options', args: {}},
+ {methodname: 'tool_dataprivacy_get_activity_options', args: {'nodefaults': true}}
+ ];
+
+ var promises = Ajax.call(requests);
+ var titlePromise = Str.get_string('addnewdefaults', 'tool_dataprivacy');
+
+ $.when(promises[0], promises[1], promises[2], titlePromise).then(
+ function(categoryResponse, purposeResponse, activityResponse, title) {
+ var categories = categoryResponse.options;
+ var purposes = purposeResponse.options;
+ var activities = activityResponse.options;
+
+ showDefaultsFormModal(title, contextLevel, null, null, null, categories, purposes, activities);
+
+ return true;
+
+ }).catch(Notification.exception);
+ }
+ );
+
+ $(ACTIONS.EDIT_ACTIVITY_DEFAULTS).click(function(e) {
+ e.preventDefault();
+
+ var button = $(this);
+ var contextLevel = button.data('contextlevel');
+ var category = button.data('category');
+ var purpose = button.data('purpose');
+ var activity = button.data('activityname');
+
+ // Get options.
+ var requests = [
+ {methodname: 'tool_dataprivacy_get_category_options', args: {}},
+ {methodname: 'tool_dataprivacy_get_purpose_options', args: {}},
+ {methodname: 'tool_dataprivacy_get_activity_options', args: {}}
+ ];
+
+ var promises = Ajax.call(requests);
+ var titlePromise = Str.get_string('editmoduledefaults', 'tool_dataprivacy');
+
+ $.when(promises[0], promises[1], promises[2], titlePromise).then(
+ function(categoryResponse, purposeResponse, activityResponse, title) {
+ var categories = categoryResponse.options;
+ var purposes = purposeResponse.options;
+ var activities = activityResponse.options;
+
+ showDefaultsFormModal(title, contextLevel, category, purpose, activity, categories, purposes, activities);
+
+ return true;
+
+ }).catch(Notification.exception);
+ }
+ );
+
+ $(ACTIONS.DELETE_ACTIVITY_DEFAULTS).click(function(e) {
+ e.preventDefault();
+
+ var button = $(this);
+ var contextLevel = button.data('contextlevel');
+ var activity = button.data('activityname');
+ var activityDisplayName = button.data('activitydisplayname');
+ // Set category and purpose to inherit (-1).
+ var category = INHERIT;
+ var purpose = INHERIT;
+
+ ModalFactory.create({
+ title: Str.get_string('deletedefaults', 'tool_dataprivacy', activityDisplayName),
+ body: Templates.render('tool_dataprivacy/delete_activity_defaults', {"activityname": activityDisplayName}),
+ type: ModalFactory.types.SAVE_CANCEL,
+ large: true
+ }).then(function(modal) {
+ modal.setSaveButtonText(Str.get_string('delete'));
+
+ // Handle save event.
+ modal.getRoot().on(ModalEvents.save, function() {
+ setContextDefaults(contextLevel, category, purpose, activity, false);
+ });
+
+ // Handle hidden event.
+ modal.getRoot().on(ModalEvents.hidden, function() {
+ // Destroy when hidden.
+ modal.destroy();
+ });
+
+ modal.show();
+
+ return true;
+ }).catch(Notification.exception);
+ });
+ };
+
+ /**
+ * Prepares and renders the modal for setting the defaults for the given context level/plugin.
+ *
+ * @param {String} title The modal's title.
+ * @param {Number} contextLevel The context level to set defaults for.
+ * @param {Number} category The current category ID.
+ * @param {Number} purpose The current purpose ID.
+ * @param {String} activity The plugin name of the activity. Optional.
+ * @param {Array} categoryOptions The list of category options.
+ * @param {Array} purposeOptions The list of purpose options.
+ * @param {Array} activityOptions The list of activity options. Optional.
+ */
+ function showDefaultsFormModal(title, contextLevel, category, purpose, activity,
+ categoryOptions, purposeOptions, activityOptions) {
+
+ if (category !== null) {
+ categoryOptions.forEach(function(currentValue) {
+ if (currentValue.id === category) {
+ currentValue.selected = true;
+ }
+ });
+ }
+
+ if (purpose !== null) {
+ purposeOptions.forEach(function(currentValue) {
+ if (currentValue.id === purpose) {
+ currentValue.selected = true;
+ }
+ });
+ }
+
+ var templateContext = {
+ "contextlevel": contextLevel,
+ "categoryoptions": categoryOptions,
+ "purposeoptions": purposeOptions
+ };
+
+ // Check the activityOptions parameter that was passed.
+ if (activityOptions !== null && activityOptions.length) {
+ // Check the activity parameter that was passed.
+ if (activity === null) {
+ // We're setting a new defaults for a module.
+ templateContext.newactivitydefaults = true;
+
+ } else {
+ // Edit mode. Set selection.
+ activityOptions.forEach(function(currentValue) {
+ if (activity === currentValue.name) {
+ currentValue.selected = true;
+ }
+ });
+ }
+
+ templateContext.modemodule = true;
+ templateContext.activityoptions = activityOptions;
+ }
+
+ ModalFactory.create({
+ title: title,
+ body: Templates.render('tool_dataprivacy/category_purpose_form', templateContext),
+ type: ModalFactory.types.SAVE_CANCEL,
+ large: true
+ }).then(function(modal) {
+
+ // Handle save event.
+ modal.getRoot().on(ModalEvents.save, function() {
+ var activity = $('#activity');
+ var activityVal = typeof activity !== 'undefined' ? activity.val() : null;
+ var override = $('#override');
+ var overrideVal = typeof override !== 'undefined' ? override.is(':checked') : false;
+
+ setContextDefaults($('#contextlevel').val(), $('#category').val(), $('#purpose').val(), activityVal, overrideVal);
+ });
+
+ // Handle hidden event.
+ modal.getRoot().on(ModalEvents.hidden, function() {
+ // Destroy when hidden.
+ modal.destroy();
+ });
+
+ modal.show();
+
+ return modal;
+ }).catch(Notification.exception);
+ }
+
+ /**
+ * Calls a the tool_dataprivacy_set_context_defaults WS function.
+ *
+ * @param {Number} contextLevel The context level.
+ * @param {Number} category The category ID.
+ * @param {Number} purpose The purpose ID.
+ * @param {String} activity The plugin name of the activity module.
+ * @param {Boolean} override Whether to override custom instances.
+ */
+ function setContextDefaults(contextLevel, category, purpose, activity, override) {
+ var request = {
+ methodname: 'tool_dataprivacy_set_context_defaults',
+ args: {
+ 'contextlevel': contextLevel,
+ 'category': category,
+ 'purpose': purpose,
+ 'override': override,
+ 'activity': activity
+ }
+ };
+
+ Ajax.call([request])[0].done(function(data) {
+ if (data.result) {
+ window.location.reload();
+ }
+ });
+ }
+
+ return /** @alias module:tool_dataprivacy/defaultsactions */ {
+ // Public variables and functions.
+
+ /**
+ * Initialise the module.
+ *
+ * @method init
+ * @return {DefaultsActions}
+ */
+ 'init': function() {
+ return new DefaultsActions();
+ }
+ };
+});
$output = $PAGE->get_renderer('tool_dataprivacy');
echo $output->header();
+echo $output->heading($title);
$categories = \tool_dataprivacy\api::get_categories();
$renderable = new \tool_dataprivacy\output\categories($categories);
namespace tool_dataprivacy;
use coding_exception;
-use context_course;
+use context_helper;
use context_system;
use core\invalid_persistent_exception;
use core\message\message;
}
/**
- * Check's whether the current user has the capability to manage data requests.
+ * Checks whether the current user has the capability to manage data requests.
*
* @param int $userid The user ID.
* @return bool
- * @throws coding_exception
- * @throws dml_exception
*/
public static function can_manage_data_requests($userid) {
- $context = context_system::instance();
-
- // A user can manage data requests if he/she has the site DPO role and has the capability to manage data requests.
- return self::is_site_dpo($userid) && has_capability('tool/dataprivacy:managedatarequests', $context, $userid);
+ // Privacy officers can manage data requests.
+ return self::is_site_dpo($userid);
}
/**
require_capability('tool/dataprivacy:managedataregistry', $context);
}
+ /**
+ * Fetches the list of configured privacy officer roles.
+ *
+ * Every time this function is called, it checks each role if they have the 'managedatarequests' capability and removes
+ * any role that doesn't have the required capability anymore.
+ *
+ * @return int[]
+ * @throws dml_exception
+ */
+ public static function get_assigned_privacy_officer_roles() {
+ $roleids = [];
+
+ // Get roles from config.
+ $configroleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
+ if (!empty($configroleids)) {
+ // Fetch roles that have the capability to manage data requests.
+ $capableroles = array_keys(get_roles_with_capability('tool/dataprivacy:managedatarequests'));
+
+ // Extract the configured roles that have the capability from the list of capable roles.
+ $roleids = array_intersect($capableroles, $configroleids);
+ }
+
+ return $roleids;
+ }
+
/**
* Fetches the role shortnames of Data Protection Officer roles.
*
public static function get_dpo_role_names() : array {
global $DB;
- $dporoleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
+ $dporoleids = self::get_assigned_privacy_officer_roles();
$dponames = array();
if (!empty($dporoleids)) {
}
/**
- * Fetches the list of users with the Data Protection Officer role.
- *
- * @throws dml_exception
+ * Fetches the list of users with the Privacy Officer role.
*/
public static function get_site_dpos() {
// Get role(s) that can manage data requests.
- $dporoles = explode(',', get_config('tool_dataprivacy', 'dporoles'));
+ $dporoles = self::get_assigned_privacy_officer_roles();
$dpos = [];
$context = context_system::instance();
foreach ($dporoles as $roleid) {
- if (empty($roleid)) {
- continue;
- }
$allnames = get_all_user_name_fields(true, 'u');
$fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
}
/**
- * Checks whether a given user is a site DPO.
+ * Checks whether a given user is a site Privacy Officer.
*
* @param int $userid The user ID.
* @return bool
- * @throws dml_exception
*/
public static function is_site_dpo($userid) {
$dpos = self::get_site_dpos();
- return array_key_exists($userid, $dpos);
+ return array_key_exists($userid, $dpos) || is_siteadmin();
}
/**
'requestedby' => $requestedby->fullname,
'requesttype' => $typetext,
'requestdate' => userdate($requestdata->timecreated),
+ 'requestorigin' => $SITE->fullname,
+ 'requestoriginurl' => new moodle_url('/'),
'requestcomments' => $requestdata->messagehtml,
'datarequestsurl' => $datarequestsurl
];
return $approvedcollection;
}
+
+ /**
+ * Updates the default category and purpose for a given context level (and optionally, a plugin).
+ *
+ * @param int $contextlevel The context level.
+ * @param int $categoryid The ID matching the category.
+ * @param int $purposeid The ID matching the purpose record.
+ * @param int $activity The name of the activity that we're making a defaults configuration for.
+ * @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
+ * @return boolean True if set/unset config succeeds. Otherwise, it throws an exception.
+ */
+ public static function set_context_defaults($contextlevel, $categoryid, $purposeid, $activity = null, $override = false) {
+ global $DB;
+
+ self::check_can_manage_data_registry();
+
+ // Get the class name associated with this context level.
+ $classname = context_helper::get_class_for_level($contextlevel);
+ list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname, $activity);
+
+ // Check the default category to be set.
+ if ($categoryid == context_instance::INHERIT) {
+ unset_config($categoryvar, 'tool_dataprivacy');
+
+ } else {
+ // Make sure the given category ID exists first.
+ $categorypersistent = new category($categoryid);
+ $categorypersistent->read();
+
+ // Then set the new default value.
+ set_config($categoryvar, $categoryid, 'tool_dataprivacy');
+ }
+
+ // Check the default purpose to be set.
+ if ($purposeid == context_instance::INHERIT) {
+ // If the defaults is set to inherit, just unset the config value.
+ unset_config($purposevar, 'tool_dataprivacy');
+
+ } else {
+ // Make sure the given purpose ID exists first.
+ $purposepersistent = new purpose($purposeid);
+ $purposepersistent->read();
+
+ // Then set the new default value.
+ set_config($purposevar, $purposeid, 'tool_dataprivacy');
+ }
+
+ // Unset instances that have been assigned with custom purpose and category, if override was specified.
+ if ($override) {
+ // We'd like to find context IDs that we want to unset.
+ $statements = ["SELECT c.id as contextid FROM {context} c"];
+ // Based on this context level.
+ $params = ['contextlevel' => $contextlevel];
+
+ if ($contextlevel == CONTEXT_MODULE) {
+ // If we're deleting module context instances, we need to make sure the instance ID is in the course modules table.
+ $statements[] = "JOIN {course_modules} cm ON cm.id = c.instanceid";
+ // And that the module is listed on the modules table.
+ $statements[] = "JOIN {modules} m ON m.id = cm.module";
+
+ if ($activity) {
+ // If we're overriding for an activity module, make sure that the context instance matches that activity.
+ $statements[] = "AND m.name = :modname";
+ $params['modname'] = $activity;
+ }
+ }
+ // Make sure this context instance exists in the tool_dataprivacy_ctxinstance table.
+ $statements[] = "JOIN {tool_dataprivacy_ctxinstance} tdc ON tdc.contextid = c.id";
+ // And that the context level of this instance matches the given context level.
+ $statements[] = "WHERE c.contextlevel = :contextlevel";
+
+ // Build our SQL query by gluing the statements.
+ $sql = implode("\n", $statements);
+
+ // Get the context records matching our query.
+ $contextids = $DB->get_fieldset_sql($sql, $params);
+
+ // Delete the matching context instances.
+ foreach ($contextids as $contextid) {
+ if ($instance = context_instance::get_record_by_contextid($contextid, false)) {
+ self::unset_context_instance($instance);
+ }
+ }
+ }
+
+ return true;
+ }
}
namespace tool_dataprivacy;
use coding_exception;
-use tool_dataprivacy\purpose;
-use tool_dataprivacy\category;
-use tool_dataprivacy\contextlevel;
-use tool_dataprivacy\context_instance;
+use core\persistent;
defined('MOODLE_INTERNAL') || die();
/**
* Returns purpose and category var names from a context class name
*
- * @param string $classname
+ * @param string $classname The context level's class.
+ * @param string $pluginname The name of the plugin associated with the context level.
* @return string[]
*/
- public static function var_names_from_context($classname) {
+ public static function var_names_from_context($classname, $pluginname = '') {
+ $pluginname = trim($pluginname);
+ if (!empty($pluginname)) {
+ $categoryvar = $classname . '_' . $pluginname . '_category';
+ $purposevar = $classname . '_' . $pluginname . '_purpose';
+ } else {
+ $categoryvar = $classname . '_category';
+ $purposevar = $classname . '_purpose';
+ }
return [
- $classname . '_purpose',
- $classname . '_category',
+ $purposevar,
+ $categoryvar
];
}
*
* The caller code is responsible of checking that $contextlevel is an integer.
*
- * @param int $contextlevel
- * @return int|false[]
+ * @param int $contextlevel The context level.
+ * @param string $pluginname The name of the plugin associated with the context level.
+ * @return int[]|false[]
*/
- public static function get_defaults($contextlevel) {
+ public static function get_defaults($contextlevel, $pluginname = '') {
$classname = \context_helper::get_class_for_level($contextlevel);
- list($purposevar, $categoryvar) = self::var_names_from_context($classname);
+ list($purposevar, $categoryvar) = self::var_names_from_context($classname, $pluginname);
$purposeid = get_config('tool_dataprivacy', $purposevar);
$categoryid = get_config('tool_dataprivacy', $categoryvar);
+ if (!empty($pluginname)) {
+ list($purposevar, $categoryvar) = self::var_names_from_context($classname);
+ // If the plugin-level doesn't have a default purpose set, try the context level.
+ if ($purposeid === false) {
+ $purposeid = get_config('tool_dataprivacy', $purposevar);
+ }
+
+ // If the plugin-level doesn't have a default category set, try the context level.
+ if ($categoryid === false) {
+ $categoryid = get_config('tool_dataprivacy', $categoryvar);
+ }
+ }
+
if (empty($purposeid)) {
$purposeid = false;
}
// Use the context level value as we don't allow people to set specific instances values.
return self::get_effective_contextlevel_value($context->contextlevel, $element);
} else {
+ // Check if we need to pass the plugin name of an activity.
+ $forplugin = '';
+ if ($context->contextlevel == CONTEXT_MODULE) {
+ list($course, $cm) = get_course_and_cm_from_cmid($context->instanceid);
+ $forplugin = $cm->modname;
+ }
// Use the default context level value.
list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category(
- $context->contextlevel
+ $context->contextlevel, false, false, $forplugin
);
return self::get_element_instance($element, $$fieldname);
}
* Returns the effective default purpose and category for a context level.
*
* @param int $contextlevel
- * @param int $forcedpurposevalue Use this value as if this was this context level purpose.
- * @param int $forcedcategoryvalue Use this value as if this was this context level category.
+ * @param int|bool $forcedpurposevalue Use this value as if this was this context level purpose.
+ * @param int|bool $forcedcategoryvalue Use this value as if this was this context level category.
+ * @param string $activity The plugin name of the activity.
* @return int[]
*/
public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false,
- $forcedcategoryvalue = false) {
+ $forcedcategoryvalue = false, $activity = '') {
- list($purposeid, $categoryid) = self::get_defaults($contextlevel);
+ list($purposeid, $categoryid) = self::get_defaults($contextlevel, $activity);
// Honour forced values.
if ($forcedpurposevalue) {
]);
}
+ /**
+ * Parameters for set_context_defaults().
+ *
+ * @return external_function_parameters
+ */
+ public static function set_context_defaults_parameters() {
+ return new external_function_parameters([
+ 'contextlevel' => new external_value(PARAM_INT, 'The context level', VALUE_REQUIRED),
+ 'category' => new external_value(PARAM_INT, 'The default category for the given context level', VALUE_REQUIRED),
+ 'purpose' => new external_value(PARAM_INT, 'The default purpose for the given context level', VALUE_REQUIRED),
+ 'activity' => new external_value(PARAM_PLUGIN, 'The plugin name of the activity', VALUE_DEFAULT, null),
+ 'override' => new external_value(PARAM_BOOL, 'Whether to override existing instances with the defaults', VALUE_DEFAULT,
+ false),
+ ]);
+ }
+
+ /**
+ * Updates the default category and purpose for a given context level (and optionally, a plugin).
+ *
+ * @param int $contextlevel The context level.
+ * @param int $category The ID matching the category.
+ * @param int $purpose The ID matching the purpose record.
+ * @param int $activity The name of the activity that we're making a defaults configuration for.
+ * @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
+ * @return array
+ */
+ public static function set_context_defaults($contextlevel, $category, $purpose, $activity, $override) {
+ $warnings = [];
+
+ $params = external_api::validate_parameters(self::set_context_defaults_parameters(), [
+ 'contextlevel' => $contextlevel,
+ 'category' => $category,
+ 'purpose' => $purpose,
+ 'activity' => $activity,
+ 'override' => $override,
+ ]);
+ $contextlevel = $params['contextlevel'];
+ $category = $params['category'];
+ $purpose = $params['purpose'];
+ $activity = $params['activity'];
+ $override = $params['override'];
+
+ // Validate context.
+ $context = context_system::instance();
+ self::validate_context($context);
+
+ // Set the context defaults.
+ $result = api::set_context_defaults($contextlevel, $category, $purpose, $activity, $override);
+
+ return [
+ 'result' => $result,
+ 'warnings' => $warnings
+ ];
+ }
+
+ /**
+ * Returns for set_context_defaults().
+ *
+ * @return external_single_structure
+ */
+ public static function set_context_defaults_returns() {
+ return new external_single_structure([
+ 'result' => new external_value(PARAM_BOOL, 'Whether the context defaults were successfully set or not'),
+ 'warnings' => new external_warnings()
+ ]);
+ }
+
+ /**
+ * Parameters for get_category_options().
+ *
+ * @return external_function_parameters
+ */
+ public static function get_category_options_parameters() {
+ return new external_function_parameters([
+ 'includeinherit' => new external_value(PARAM_BOOL, 'Include option "Inherit"', VALUE_DEFAULT, true),
+ 'includenotset' => new external_value(PARAM_BOOL, 'Include option "Not set"', VALUE_DEFAULT, false),
+ ]);
+ }
+
+ /**
+ * Fetches a list of data category options containing category IDs as keys and the category name for the value.
+ *
+ * @param bool $includeinherit Whether to include the "Inherit" option.
+ * @param bool $includenotset Whether to include the "Not set" option.
+ * @return array
+ */
+ public static function get_category_options($includeinherit, $includenotset) {
+ $warnings = [];
+
+ $params = self::validate_parameters(self::get_category_options_parameters(), [
+ 'includeinherit' => $includeinherit,
+ 'includenotset' => $includenotset
+ ]);
+ $includeinherit = $params['includeinherit'];
+ $includenotset = $params['includenotset'];
+
+ $context = context_system::instance();
+ self::validate_context($context);
+
+ $categories = api::get_categories();
+ $options = data_registry_page::category_options($categories, $includenotset, $includeinherit);
+ $categoryoptions = [];
+ foreach ($options as $id => $name) {
+ $categoryoptions[] = [
+ 'id' => $id,
+ 'name' => $name,
+ ];
+ }
+
+ return [
+ 'options' => $categoryoptions,
+ 'warnings' => $warnings
+ ];
+ }
+
+ /**
+ * Returns for get_category_options().
+ *
+ * @return external_single_structure
+ */
+ public static function get_category_options_returns() {
+ $optiondefinition = new external_single_structure(
+ [
+ 'id' => new external_value(PARAM_INT, 'The category ID'),
+ 'name' => new external_value(PARAM_TEXT, 'The category name'),
+ ]
+ );
+
+ return new external_single_structure([
+ 'options' => new external_multiple_structure($optiondefinition),
+ 'warnings' => new external_warnings()
+ ]);
+ }
+
+ /**
+ * Parameters for get_purpose_options().
+ *
+ * @return external_function_parameters
+ */
+ public static function get_purpose_options_parameters() {
+ return new external_function_parameters([
+ 'includeinherit' => new external_value(PARAM_BOOL, 'Include option "Inherit"', VALUE_DEFAULT, true),
+ 'includenotset' => new external_value(PARAM_BOOL, 'Include option "Not set"', VALUE_DEFAULT, false),
+ ]);
+ }
+
+ /**
+ * Fetches a list of data storage purposes containing purpose IDs as keys and the purpose name for the value.
+ *
+ * @param bool $includeinherit Whether to include the "Inherit" option.
+ * @param bool $includenotset Whether to include the "Not set" option.
+ * @return array
+ */
+ public static function get_purpose_options($includeinherit, $includenotset) {
+ $warnings = [];
+
+ $params = self::validate_parameters(self::get_category_options_parameters(), [
+ 'includeinherit' => $includeinherit,
+ 'includenotset' => $includenotset
+ ]);
+ $includeinherit = $params['includeinherit'];
+ $includenotset = $params['includenotset'];
+
+ $context = context_system::instance();
+ self::validate_context($context);
+
+ $purposes = api::get_purposes();
+ $options = data_registry_page::purpose_options($purposes, $includenotset, $includeinherit);
+ $purposeoptions = [];
+ foreach ($options as $id => $name) {
+ $purposeoptions[] = [
+ 'id' => $id,
+ 'name' => $name,
+ ];
+ }
+
+ return [
+ 'options' => $purposeoptions,
+ 'warnings' => $warnings
+ ];
+ }
+
+ /**
+ * Returns for get_purpose_options().
+ *
+ * @return external_single_structure
+ */
+ public static function get_purpose_options_returns() {
+ $optiondefinition = new external_single_structure(
+ [
+ 'id' => new external_value(PARAM_INT, 'The purpose ID'),
+ 'name' => new external_value(PARAM_TEXT, 'The purpose name'),
+ ]
+ );
+
+ return new external_single_structure([
+ 'options' => new external_multiple_structure($optiondefinition),
+ 'warnings' => new external_warnings()
+ ]);
+ }
+
+ /**
+ * Parameters for get_activity_options().
+ *
+ * @return external_function_parameters
+ */
+ public static function get_activity_options_parameters() {
+ return new external_function_parameters([
+ 'nodefaults' => new external_value(PARAM_BOOL, 'Whether to fetch all activities or only those without defaults',
+ VALUE_DEFAULT, false),
+ ]);
+ }
+
+ /**
+ * Fetches a list of activity options for setting data registry defaults.
+ *
+ * @param boolean $nodefaults If false, it will fetch all of the activities. Otherwise, it will only fetch the activities
+ * that don't have defaults yet (e.g. when adding a new activity module defaults).
+ * @return array
+ */
+ public static function get_activity_options($nodefaults) {
+ $warnings = [];
+
+ $params = self::validate_parameters(self::get_activity_options_parameters(), [
+ 'nodefaults' => $nodefaults,
+ ]);
+ $nodefaults = $params['nodefaults'];
+
+ $context = context_system::instance();
+ self::validate_context($context);
+
+ // Get activity module plugin info.
+ $pluginmanager = \core_plugin_manager::instance();
+ $modplugins = $pluginmanager->get_enabled_plugins('mod');
+ $modoptions = [];
+
+ // Get the module-level defaults. data_registry::get_defaults falls back to this when there are no activity defaults.
+ list($levelpurpose, $levelcategory) = data_registry::get_defaults(CONTEXT_MODULE);
+ foreach ($modplugins as $name) {
+ // Check if we have default purpose and category for this module if we want don't want to fetch everything.
+ if ($nodefaults) {
+ list($purpose, $category) = data_registry::get_defaults(CONTEXT_MODULE, $name);
+ // Compare this with the module-level defaults.
+ if ($purpose !== $levelpurpose || $category !== $levelcategory) {
+ // If the defaults for this activity has been already set, there's no need to add this in the list of options.
+ continue;
+ }
+ }
+
+ $displayname = $pluginmanager->plugin_name('mod_' . $name);
+ $modoptions[] = (object)[
+ 'name' => $name,
+ 'displayname' => $displayname
+ ];
+ }
+
+ return [
+ 'options' => $modoptions,
+ 'warnings' => $warnings
+ ];
+ }
+
+ /**
+ * Returns for get_category_options().
+ *
+ * @return external_single_structure
+ */
+ public static function get_activity_options_returns() {
+ $optionsdefinition = new external_single_structure(
+ [
+ 'name' => new external_value(PARAM_TEXT, 'The plugin name of the activity'),
+ 'displayname' => new external_value(PARAM_TEXT, 'The display name of the activity'),
+ ]
+ );
+
+ return new external_single_structure([
+ 'options' => new external_multiple_structure($optionsdefinition),
+ 'warnings' => new external_warnings()
+ ]);
+ }
+
/**
* Gets the structure of a tree node (link + child branches).
*
defined('MOODLE_INTERNAL') || die();
use core\external\persistent_exporter;
+use tool_dataprivacy\category;
+use tool_dataprivacy\context_instance;
/**
* Class for exporting field data.
'context' => 'context',
);
}
+
+ /**
+ * Utility function that fetches a category name from the given ID.
+ *
+ * @param int $categoryid The category ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID.
+ * @return string The purpose name.
+ */
+ public static function get_name($categoryid) {
+ global $PAGE;
+ if ($categoryid === false || $categoryid == context_instance::INHERIT) {
+ return get_string('inherit', 'tool_dataprivacy');
+ } else if ($categoryid == context_instance::NOTSET) {
+ return get_string('notset', 'tool_dataprivacy');
+ } else {
+ $purpose = new category($categoryid);
+ $output = $PAGE->get_renderer('tool_dataprivacy');
+ $exporter = new self($purpose, ['context' => \context_system::instance()]);
+ $data = $exporter->export($output);
+ return $data->name;
+ }
+ }
}
use DateInterval;
use Exception;
use renderer_base;
+use tool_dataprivacy\context_instance;
use tool_dataprivacy\purpose;
/**
return $values;
}
+
+ /**
+ * Utility function that fetches a purpose name from the given ID.
+ *
+ * @param int $purposeid The purpose ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID.
+ * @return string The purpose name.
+ */
+ public static function get_name($purposeid) {
+ global $PAGE;
+ if ($purposeid === false || $purposeid == context_instance::INHERIT) {
+ return get_string('inherit', 'tool_dataprivacy');
+ } else if ($purposeid == context_instance::NOTSET) {
+ return get_string('notset', 'tool_dataprivacy');
+ } else {
+ $purpose = new purpose($purposeid);
+ $output = $PAGE->get_renderer('tool_dataprivacy');
+ $exporter = new self($purpose, ['context' => \context_system::instance()]);
+ $data = $exporter->export($output);
+ return $data->name;
+ }
+ }
}
+++ /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/>.
-
-/**
- * This file contains the defaults form.
- *
- * @package tool_dataprivacy
- * @copyright 2018 David Monllao
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace tool_dataprivacy\form;
-defined('MOODLE_INTERNAL') || die();
-
-use \tool_dataprivacy\output\data_registry_page;
-
-require_once($CFG->libdir . '/formslib.php');
-require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
-
-/**
- * Context levels defaults form.
- *
- * @package tool_dataprivacy
- * @copyright 2018 David Monllao
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class defaults extends \moodleform {
-
- /**
- * Define the form.
- */
- public function definition() {
- global $OUTPUT;
-
- $mform = $this->_form;
- $mform->setDisableShortforms();
-
- $notification = $OUTPUT->notification(get_string('defaultsinfo', 'tool_dataprivacy'),
- \core\output\notification::NOTIFY_INFO);
- $mform->addElement('html', $notification);
-
- foreach ($this->_customdata['levels'] as $level => $classname) {
-
- $mform->addElement('header', $classname . '-header',
- get_string('contextlevelname' . $level, 'tool_dataprivacy'));
-
- list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
-
- $includeinherit = true;
- if ($level == CONTEXT_SYSTEM) {
- $includeinherit = false;
- }
-
- $categoryoptions = data_registry_page::category_options($this->_customdata['categories'], false, $includeinherit);
- $purposeoptions = data_registry_page::category_options($this->_customdata['purposes'], false, $includeinherit);
-
- $mform->addElement('select', $categoryvar, get_string('category', 'tool_dataprivacy'), $categoryoptions);
- $mform->addHelpButton($categoryvar, 'categorydefault', 'tool_dataprivacy');
- $mform->setType($categoryvar, PARAM_INT);
-
- $mform->addElement('select', $purposevar, get_string('purpose', 'tool_dataprivacy'), $purposeoptions);
- $mform->addHelpButton($purposevar, 'purposedefault', 'tool_dataprivacy');
- $mform->setType($purposevar, PARAM_INT);
- }
-
- $this->add_action_buttons(true, get_string('savechanges'));
- }
-}
/**
* From a list of purpose persistents to a list of id => name purposes.
*
- * @param \tool_dataprivacy\purpose $purposes
+ * @param \tool_dataprivacy\purpose[] $purposes
* @param bool $includenotset
* @param bool $includeinherit
* @return string[]
/**
* From a list of category persistents to a list of id => name categories.
*
- * @param \tool_dataprivacy\category $categories
+ * @param \tool_dataprivacy\category[] $categories
* @param bool $includenotset
* @param bool $includeinherit
* @return string[]
--- /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 data for the data registry defaults.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy\output;
+defined('MOODLE_INTERNAL') || die();
+
+use action_menu_link_primary;
+use coding_exception;
+use moodle_exception;
+use moodle_url;
+use renderable;
+use renderer_base;
+use stdClass;
+use templatable;
+use tool_dataprivacy\data_registry;
+use tool_dataprivacy\external\category_exporter;
+use tool_dataprivacy\external\purpose_exporter;
+
+/**
+ * Class containing data for the data registry defaults.
+ *
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class defaults_page implements renderable, templatable {
+
+ /** @var int $mode The display mode. */
+ protected $mode = null;
+
+ /** @var int $category The default category for the given mode. */
+ protected $category = null;
+
+ /** @var int $purpose The default purpose for the given mode. */
+ protected $purpose = null;
+
+ /** @var stdClass[] $otherdefaults Other defaults for the given mode. */
+ protected $otherdefaults = [];
+
+ /** @var bool $canedit Whether editing is allowed. */
+ protected $canedit = false;
+
+ /**
+ * Construct this renderable.
+ *
+ * @param int $mode The display mode.
+ * @param int $category The default category for the given mode.
+ * @param int $purpose The default purpose for the given mode.
+ * @param stdClass[] $otherdefaults Other defaults for the given mode.
+ * @param bool $canedit Whether editing is allowed.
+ */
+ public function __construct($mode, $category, $purpose, $otherdefaults = [], $canedit = false) {
+ $this->mode = $mode;
+ $this->category = $category;
+ $this->purpose = $purpose;
+ $this->otherdefaults = $otherdefaults;
+ $this->canedit = $canedit;
+ }
+
+ /**
+ * Export this data so it can be used as the context for a mustache template.
+ *
+ * @param renderer_base $output
+ * @return stdClass
+ * @throws coding_exception
+ * @throws moodle_exception
+ */
+ public function export_for_template(renderer_base $output) {
+ $data = new stdClass();
+
+ // Set tab URLs.
+ $coursecaturl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_COURSECAT]);
+ $courseurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_COURSE]);
+ $moduleurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_MODULE]);
+ $blockurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_BLOCK]);
+ $data->coursecaturl = $coursecaturl;
+ $data->courseurl = $courseurl;
+ $data->moduleurl = $moduleurl;
+ $data->blockurl = $blockurl;
+
+ // Set display mode.
+ switch ($this->mode) {
+ case CONTEXT_COURSECAT:
+ $data->modecoursecat = true;
+ break;
+ case CONTEXT_COURSE:
+ $data->modecourse = true;
+ break;
+ case CONTEXT_MODULE:
+ $data->modemodule = true;
+ break;
+ case CONTEXT_BLOCK:
+ $data->modeblock = true;
+ break;
+ default:
+ $data->modecoursecat = true;
+ break;
+ }
+
+ // Set config variables.
+ $configname = \context_helper::get_class_for_level($this->mode);
+ list($purposevar, $categoryvar) = data_registry::var_names_from_context($configname);
+ $data->categoryvar = $categoryvar;
+ $data->purposevar = $purposevar;
+
+ // Set default category.
+ $data->categoryid = $this->category;
+ $data->category = category_exporter::get_name($this->category);
+
+ // Set default purpose.
+ $data->purposeid = $this->purpose;
+ $data->purpose = purpose_exporter::get_name($this->purpose);
+
+ // Set other defaults.
+ $otherdefaults = [];
+ $url = new moodle_url('#');
+ foreach ($this->otherdefaults as $pluginname => $values) {
+ $defaults = [
+ 'name' => $values->name,
+ 'category' => category_exporter::get_name($values->category),
+ 'purpose' => purpose_exporter::get_name($values->purpose),
+ ];
+ if ($this->canedit) {
+ $actions = [];
+ // Edit link.
+ $editattrs = [
+ 'data-action' => 'edit-activity-defaults',
+ 'data-contextlevel' => $this->mode,
+ 'data-activityname' => $pluginname,
+ 'data-category' => $values->category,
+ 'data-purpose' => $values->purpose,
+ ];
+ $editlink = new action_menu_link_primary($url, new \pix_icon('t/edit', get_string('edit')),
+ get_string('edit'), $editattrs);
+ $actions[] = $editlink->export_for_template($output);
+
+ // Delete link.
+ $deleteattrs = [
+ 'data-action' => 'delete-activity-defaults',
+ 'data-contextlevel' => $this->mode,
+ 'data-activityname' => $pluginname,
+ 'data-activitydisplayname' => $values->name,
+ ];
+ $deletelink = new action_menu_link_primary($url, new \pix_icon('t/delete', get_string('delete')),
+ get_string('delete'), $deleteattrs);
+ $actions[] = $deletelink->export_for_template($output);
+
+ $defaults['actions'] = $actions;
+ }
+ $otherdefaults[] = (object)$defaults;
+ }
+ $data->otherdefaults = $otherdefaults;
+
+ $data->canedit = $this->canedit;
+ $data->contextlevel = $this->mode;
+
+ return $data;
+ }
+}
*/
public static function setup(moodle_url $url, $title, $attachtoparentnode = '',
$requiredcapability = 'tool/dataprivacy:managedataregistry') {
- global $PAGE;
+ global $PAGE, $SITE;
$context = context_system::instance();
$PAGE->set_context($context);
$PAGE->set_pagelayout('admin');
$PAGE->set_title($title);
- $PAGE->set_heading($title);
+ $PAGE->set_heading($SITE->fullname);
// If necessary, override the settings navigation to add this page into the breadcrumb navigation.
if ($attachtoparentnode) {
redirect($returnurl, $redirectmessage);
}
-$title = get_string('contactdataprotectionofficer', 'tool_dataprivacy');
-$PAGE->set_heading($title);
+$title = get_string('createnewdatarequest', 'tool_dataprivacy');
+$PAGE->set_heading($SITE->fullname);
$PAGE->set_title($title);
echo $OUTPUT->header();
echo $OUTPUT->heading($title);
\tool_dataprivacy\page_helper::setup($url, $title);
echo $OUTPUT->header();
+echo $OUTPUT->heading($title);
if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
$table = new \tool_dataprivacy\output\expired_contexts_table($filter);
$output = $PAGE->get_renderer('tool_dataprivacy');
echo $output->header();
+echo $OUTPUT->heading($title);
if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
$dataregistry = new tool_dataprivacy\output\data_registry_page($contextlevel, $contextid);
'ajax' => true,
'loginrequired' => true,
],
+ 'tool_dataprivacy_set_context_defaults' => [
+ 'classname' => 'tool_dataprivacy\external',
+ 'methodname' => 'set_context_defaults',
+ 'classpath' => '',
+ 'description' => 'Updates the default category and purpose for a given context level (and optionally, a plugin)',
+ 'type' => 'write',
+ 'capabilities' => 'tool/dataprivacy:managedataregistry',
+ 'ajax' => true,
+ 'loginrequired' => true,
+ ],
+ 'tool_dataprivacy_get_category_options' => [
+ 'classname' => 'tool_dataprivacy\external',
+ 'methodname' => 'get_category_options',
+ 'classpath' => '',
+ 'description' => 'Fetches a list of data category options',
+ 'type' => 'read',
+ 'capabilities' => 'tool/dataprivacy:managedataregistry',
+ 'ajax' => true,
+ 'loginrequired' => true,
+ ],
+ 'tool_dataprivacy_get_purpose_options' => [
+ 'classname' => 'tool_dataprivacy\external',
+ 'methodname' => 'get_purpose_options',
+ 'classpath' => '',
+ 'description' => 'Fetches a list of data storage purpose options',
+ 'type' => 'read',
+ 'capabilities' => 'tool/dataprivacy:managedataregistry',
+ 'ajax' => true,
+ 'loginrequired' => true,
+ ],
+ 'tool_dataprivacy_get_activity_options' => [
+ 'classname' => 'tool_dataprivacy\external',
+ 'methodname' => 'get_activity_options',
+ 'classpath' => '',
+ 'description' => 'Fetches a list of activity options',
+ 'type' => 'read',
+ 'capabilities' => 'tool/dataprivacy:managedataregistry',
+ 'ajax' => true,
+ 'loginrequired' => true,
+ ],
];
\tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry');
-$levels = \context_helper::get_all_levels();
-// They are set through the context level site and user.
-unset($levels[CONTEXT_SYSTEM]);
-unset($levels[CONTEXT_USER]);
+$mode = optional_param('mode', CONTEXT_COURSECAT, PARAM_INT);
+$classname = context_helper::get_class_for_level($mode);
+list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
+$purpose = get_config('tool_dataprivacy', $purposevar);
+$category = get_config('tool_dataprivacy', $categoryvar);
-$customdata = [
- 'levels' => $levels,
- 'purposes' => \tool_dataprivacy\api::get_purposes(),
- 'categories' => \tool_dataprivacy\api::get_categories(),
-];
-$form = new \tool_dataprivacy\form\defaults($PAGE->url->out(false), $customdata);
+$otherdefaults = [];
+if ($mode == CONTEXT_MODULE) {
+ // Get activity module plugin info.
+ $pluginmanager = core_plugin_manager::instance();
+ $modplugins = $pluginmanager->get_enabled_plugins('mod');
-$toform = new stdClass();
-foreach ($levels as $level => $classname) {
- list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
- $toform->{$purposevar} = get_config('tool_dataprivacy', $purposevar);
- $toform->{$categoryvar} = get_config('tool_dataprivacy', $categoryvar);
-}
-$form->set_data($toform);
-
-$returnurl = new \moodle_url('/admin/tool/dataprivacy/dataregistry.php');
-if ($form->is_cancelled()) {
- redirect($returnurl);
-} else if ($data = $form->get_data()) {
-
- foreach ($levels as $level => $classname) {
-
- list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
-
- if (isset($data->{$purposevar})) {
- set_config($purposevar, $data->{$purposevar}, 'tool_dataprivacy');
- }
- if (isset($data->{$categoryvar})) {
- set_config($categoryvar, $data->{$categoryvar}, 'tool_dataprivacy');
+ foreach ($modplugins as $name) {
+ list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname, $name);
+ $plugincategory = get_config('tool_dataprivacy', $categoryvar);
+ $pluginpurpose = get_config('tool_dataprivacy', $purposevar);
+ if ($plugincategory === false && $pluginpurpose === false) {
+ // If no purpose and category has been set for this plugin, then there's no need to show this on the list.
+ continue;
}
+
+ $displayname = $pluginmanager->plugin_name('mod_' . $name);
+ $otherdefaults[$name] = (object)[
+ 'name' => $displayname,
+ 'category' => $plugincategory,
+ 'purpose' => $pluginpurpose,
+ ];
}
- redirect($returnurl, get_string('defaultssaved', 'tool_dataprivacy'),
- 0, \core\output\notification::NOTIFY_SUCCESS);
}
+$defaultspage = new \tool_dataprivacy\output\defaults_page($mode, $category, $purpose, $otherdefaults, true);
+
$output = $PAGE->get_renderer('tool_dataprivacy');
echo $output->header();
-$form->display();
+echo $output->heading($title);
+echo $output->render_from_template('tool_dataprivacy/defaults_page', $defaultspage->export_for_template($output));
echo $output->footer();
$string['pluginname_help'] = 'Data privacy plugin';
$string['activitiesandresources'] = 'Activities and resources';
$string['addcategory'] = 'Add category';
+$string['addnewdefaults'] = 'Add a new module default';
$string['addpurpose'] = 'Add purpose';
$string['approve'] = 'Approve';
$string['approverequest'] = 'Approve request';
$string['contextpurposecategorysaved'] = 'Purpose and category saved.';
$string['contactdpoviaprivacypolicy'] = 'Please contact the privacy officer as described in the privacy policy.';
$string['createcategory'] = 'Create data category';
+$string['createnewdatarequest'] = 'Create a new data request';
$string['createpurpose'] = 'Create data purpose';
$string['datadeletion'] = 'Data deletion';
$string['datadeletionpagehelp'] = 'Data for which the retention period has expired are listed here. Please review and confirm data deletion, which will then be executed by the "Delete expired contexts" scheduled task.';
$string['datecomment'] = '[{$a->date}]: ' . PHP_EOL . ' {$a->comment}';
$string['daterequested'] = 'Date requested';
$string['daterequesteddetail'] = 'Date requested:';
-$string['defaultsinfo'] = 'Default categories and purposes are applied to all newly created instances.';
+$string['defaultsinfo'] = 'Default categories and purposes are applied to all new and existing instances where a value is not set.';
+$string['defaultswarninginfo'] = 'Warning: Changing these defaults may affect the retention period of existing instances.';
$string['deletecategory'] = 'Delete category';
$string['deletecategorytext'] = 'Are you sure you want to delete the category \'{$a}\'?';
+$string['deletedefaults'] = 'Delete defaults: {$a}';
+$string['deletedefaultsconfirmation'] = 'Are you sure you want to delete the default category and purpose for {$a} modules?';
$string['deleteexpiredcontextstask'] = 'Delete expired contexts';
$string['deleteexpireddatarequeststask'] = 'Delete files from completed data requests that have expired';
$string['deletepurpose'] = 'Delete purpose';
$string['editcategories'] = 'Edit categories';
$string['editcategory'] = 'Edit category';
$string['editcategories'] = 'Edit categories';
+$string['editdefaults'] = 'Edit defaults: {$a}';
+$string['editmoduledefaults'] = 'Edit module defaults';
$string['editpurpose'] = 'Edit purpose';
$string['editpurposes'] = 'Edit purposes';
$string['effectiveretentionperiodcourse'] = '{$a} (after the course end date)';
$string['nosubjectaccessrequests'] = 'There are no data requests that you need to act on';
$string['nosystemdefaults'] = 'Site purpose and category have not yet been defined.';
$string['notset'] = 'Not set (use the default value)';
+$string['overrideinstances'] = 'Reset instances with custom values';
$string['pluginregistry'] = 'Plugin privacy registry';
$string['pluginregistrytitle'] = 'Plugin privacy compliance registry';
$string['privacy'] = 'Privacy';
$string['requestemailintro'] = 'You have received a data request:';
$string['requestfor'] = 'Requesting for';
$string['requestmarkedcomplete'] = 'The request has been marked as complete';
+$string['requestorigin'] = 'Request origin';
$string['requeststatus'] = 'Status';
$string['requestsubmitted'] = 'Your request has been submitted to the privacy officer';
$string['requesttype'] = 'Type';
$output = $PAGE->get_renderer('tool_dataprivacy');
echo $output->header();
+echo $output->heading($title);
$purposes = \tool_dataprivacy\api::get_purposes();
$renderable = new \tool_dataprivacy\output\purposes($purposes);
[data-region="data-requests-table"] .moodle-actionmenu {
min-width: 150px;
}
+
+.context-level-view {
+ margin: 1em;
+}
\ No newline at end of file
{{/navigation}}
<div data-region="categories" class="m-t-3 m-b-1">
- <h3>{{#str}}categories, tool_dataprivacy{{/str}}</h3>
<div class="m-y-1">
<button class="btn btn-secondary" data-add-element="category" title="{{#str}}addcategory, tool_dataprivacy{{/str}}">
{{#pix}}t/add, moodle, {{#str}}addcategory, tool_dataprivacy{{/str}}{{/pix}}
--- /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/category_purpose_form
+
+ Manage data registry defaults.
+
+ Classes required for JS:
+
+ Data attributes required for JS:
+
+ Context variables required for this template:
+ * actionurl String The action URL.
+ * contextlevel Number - The context level.
+ * modemodule Boolean - Whether to display the activity select element.
+ * activityoptions Array - An array of objects for the activity select element.
+ * categoryoptions Array - An array of objects for the category select element.
+ * purposeoptions Array - An array of objects for the purpose select element.
+
+ Example context (json):
+ {
+ "actionurl": "#",
+ "contextlevel": 70,
+ "newactivitydefaults": true,
+ "modemodule": true,
+ "activityoptions": [
+ { "name": "assign", "displayname": "Assignment" },
+ { "name": "forum", "displayname": "Forum", "selected": true },
+ { "name": "lesson", "displayname": "Lesson" },
+ { "name": "quiz", "displayname": "Quiz" }
+ ],
+ "categoryoptions": [
+ { "id": 1, "name": "Category 1" },
+ { "id": 2, "name": "Category 2", "selected": true },
+ { "id": 3, "name": "Category 3" }
+ ],
+ "purposeoptions": [
+ { "id": 1, "name": "Purpose 1" },
+ { "id": 2, "name": "Purpose 2" },
+ { "id": 3, "name": "Purpose 3", "selected": true }
+ ]
+ }
+}}
+
+<div class="alert alert-warning" role="alert">
+ {{#str}}defaultswarninginfo, tool_dataprivacy{{/str}}
+</div>
+<form method="post" action="{{actionurl}}" id="category_purpose_form">
+ <input type="hidden" value="{{contextlevel}}" id="contextlevel" />
+ {{#modemodule}}
+ <div class="form-group">
+ <label for="activity">{{#str}}activitymodule{{/str}}</label>
+ {{^newactivitydefaults}}
+ <input type="hidden" id="activity" value="{{#activityoptions}}{{#selected}}{{name}}{{/selected}}{{/activityoptions}}" />
+ {{/newactivitydefaults}}
+ <select class="form-control" {{#newactivitydefaults}}id="activity" {{/newactivitydefaults}}{{^newactivitydefaults}}disabled{{/newactivitydefaults}}>
+ {{#activityoptions}}
+ <option value="{{name}}" {{#selected}}selected{{/selected}}>{{displayname}}</option>
+ {{/activityoptions}}
+ </select>
+ </div>
+ {{/modemodule}}
+ <div class="form-group">
+ <label for="category">{{#str}}category, tool_dataprivacy{{/str}}</label>
+ <select class="form-control" id="category">
+ {{#categoryoptions}}
+ <option value="{{id}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+ {{/categoryoptions}}
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="purpose">{{#str}}purpose, tool_dataprivacy{{/str}}</label>
+ <select class="form-control" id="purpose">
+ {{#purposeoptions}}
+ <option value="{{id}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+ {{/purposeoptions}}
+ </select>
+ </div>
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox" value="1" id="override">
+ <label class="form-check-label" for="override">
+ {{#str}}overrideinstances, tool_dataprivacy{{/str}}
+ </label>
+ </div>
+</form>
* string requestedby The one making the request.
* string requesttype The request type.
* string requestdate The date the request was made.
+ * string requestorigin The name of the site the request originates from.
+ * string requestoriginurl The homepage of the site the request originates from.
* string requestcomments Additional details regarding the request.
* bool forself Whether the request has been made on behalf of another user or not.
* string datarequestsurl The URL to the data requests page.
"requestedby": "Angus Zhang",
"requesttype": "Export user data",
"requestdate": "31 January 2018",
+ "requestorigin": "My Amazing Site",
+ "requestoriginurl": "https://www.bestmoodlesiteever.com",
"requestcomments": "Dear admin,<br/> I would like to request a copy of my son's user data. Thanks!",
"forself": true,
"datarequestsurl": "#"
</td>
</tr>
{{/forself}}
+ <tr>
+ <th scope="row">
+ {{#str}}requestorigin, tool_dataprivacy{{/str}}
+ </th>
+ <td>
+ <a href="{{requestoriginurl}}">{{{requestorigin}}}</a>
+ </td>
+ </tr>
<tr>
<th scope="row">
{{#str}}requestcomments, tool_dataprivacy{{/str}}
--- /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/defaults_display
+
+ Renders the current default category and purpose.
+
+ Classes required for JS:
+
+ Data attributes required for JS:
+
+ Context variables required for this template:
+ * contextlevel Number - The context level.
+ * categoryid Number - The category ID.
+ * purposeid Number - The purpose ID.
+ * category String - The category name.
+ * purpose String - The purpose name.
+ * canedit Boolean - Whether this is being rendered for editing purposes.
+
+ Example context (json):
+ {
+ "category": "Awesome default category",
+ "categoryid": 1,
+ "purpose": "Awesome default purpose",
+ "purposeid": 2,
+ "canedit": true,
+ "contextlevel": 70
+ }
+}}
+<div class="row-fluid rtl-compatible m-t-1 m-b-1">
+ <div class="col-md-9 span9">
+ <div class="row-fluid rtl-compatible m-t-1 m-b-1">
+ <div class="col-md-3 span3">
+ <strong>{{#str}}category, tool_dataprivacy{{/str}}</strong>
+ </div>
+ <div class="col-md-9 span9">
+ {{category}}
+ </div>
+ </div>
+ <div class="row-fluid rtl-compatible m-t-1 m-b-1">
+ <div class="col-md-3 span3">
+ <strong>{{#str}}purpose, tool_dataprivacy{{/str}}</strong>
+ </div>
+ <div class="col-md-9 span9">
+ {{purpose}}
+ </div>
+ </div>
+ </div>
+ <div class="col-md-3 span3 align-self-center">
+ {{#canedit}}
+ <button class="btn btn-default" {{!
+ }}data-action="edit-level-defaults" {{!
+ }}data-contextlevel="{{contextlevel}}" {{!
+ }}data-category="{{categoryid}}" {{!
+ }}data-purpose="{{purposeid}}">
+ {{#str}}edit{{/str}}
+ </button>
+ {{/canedit}}
+ </div>
+</div>
--- /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/defaults_page
+
+ Manage data registry defaults.
+
+ Classes required for JS:
+
+ Data attributes required for JS:
+
+ Context variables required for this template:
+ * contextlevel Number - The context level.
+ * modecoursecat Boolean - Whether we're displaying defaults for course categories.
+ * modecourse Boolean - Whether we're displaying defaults for courses.
+ * modemodule Boolean - Whether we're displaying defaults for activity modules.
+ * modeblock Boolean - Whether we're displaying defaults for blocks.
+ * coursecaturl String - The URL for the course category defaults tab.
+ * courseurl String - The URL for the course defaults tab.
+ * moduleurl String - The URL for the activity module defaults tab.
+ * blockurl String - The URL for the block defaults tab.
+ * purposeid Number - The purpose ID for this context level.
+ * canedit Boolean - Whether this is being rendered for editing purposes.
+ * categoryid Number - The ID of the default category for this context level.
+ * purposeid Number - The ID of the default purpose for this context level.
+ * category String - The category name.
+ * purpose String - The purpose name.
+ * otherdefaults Array - An array containing the defaults for the activity modules.
+
+ Example context (json):
+ {
+ "contextlevel": 70,
+ "modecoursecat": false,
+ "modecourse": false,
+ "modemodule": true,
+ "modeblock": false,
+ "coursecaturl": "#",
+ "courseurl": "#",
+ "moduleurl": "#",
+ "blockurl": "#",
+ "category": "Awesome default category",
+ "purpose": "Awesome default purpose",
+ "canedit": true,
+ "otherdefaults": [
+ {
+ "name": "Assignment",
+ "category": "Category for activity modules",
+ "purpose": "Assessments"
+ },
+ {
+ "name": "Forum",
+ "category": "Category for activity modules",
+ "purpose": "Social interactions"
+ }
+ ]
+ }
+}}
+<div class="card">
+ <div class="card-header">
+ <ul class="nav nav-tabs card-header-tabs">
+ <li class="nav-item">
+ <a class="nav-link {{#modecoursecat}}active{{/modecoursecat}}" href="{{coursecaturl}}">{{#str}}categories{{/str}}</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link {{#modecourse}}active{{/modecourse}}" href="{{courseurl}}">{{#str}}courses{{/str}}</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link {{#modemodule}}active{{/modemodule}}" href="{{moduleurl}}">{{#str}}activitymodules{{/str}}</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link {{#modeblock}}active{{/modeblock}}" href="{{blockurl}}">{{#str}}blocks{{/str}}</a>
+ </li>
+ </ul>
+ </div>
+ <div class="card-body context-level-view">
+ <div class="alert alert-primary" role="alert">
+ {{#str}}defaultsinfo, tool_dataprivacy{{/str}}
+ </div>
+ <h4 class="card-title" id="defaults-header">
+ {{#modecoursecat}}{{#str}}categories{{/str}}{{/modecoursecat}}
+ {{#modecourse}}{{#str}}courses{{/str}}{{/modecourse}}
+ {{#modemodule}}{{#str}}activitymodules{{/str}}{{/modemodule}}
+ {{#modeblock}}{{#str}}blocks{{/str}}{{/modeblock}}
+ </h4>
+ <div>
+ {{> tool_dataprivacy/defaults_display}}
+ {{#canedit}}
+ {{#modemodule}}
+ <button class="btn btn-primary" data-action="new-activity-defaults" data-contextlevel="{{contextlevel}}">
+ {{#str}}addnewdefaults, tool_dataprivacy{{/str}}
+ </button>
+ {{/modemodule}}
+ {{/canedit}}
+ {{#modemodule}}
+ <table class="m-t-1 table table-striped">
+ <thead>
+ <tr>
+ <th> </th>
+ <th scope="col">{{#str}}category, tool_dataprivacy{{/str}}</th>
+ <th scope="col">{{#str}}purpose, tool_dataprivacy{{/str}}</th>
+ {{#canedit}}
+ <th scope="col">{{#str}}actions{{/str}}</th>
+ {{/canedit}}
+ </tr>
+ </thead>
+ <tbody>
+ {{#otherdefaults}}
+ <tr>
+ <th scope="row">{{name}}</th>
+ <td>{{category}}</td>
+ <td>{{purpose}}</td>
+ {{#canedit}}
+ <td>
+ {{#actions}}
+ {{> core/action_menu_link}}
+ {{/actions}}
+ </td>
+ {{/canedit}}
+ </tr>
+ {{/otherdefaults}}
+ </tbody>
+ </table>
+ {{/modemodule}}
+ </div>
+ </div>
+</div>
+
+{{#js}}
+// Initialise the JS.
+require(['tool_dataprivacy/defaultsactions'], function(ActionsMod) {
+ ActionsMod.init();
+});
+{{/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/delete_activity_defaults
+
+ Renders the confirmation.
+
+ Classes required for JS:
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * activityname String - The activity name.
+
+ Example context (json):
+ {
+ "activityname": "Assignment"
+ }
+}}
+
+<div class="alert alert-warning" role="alert">
+ {{#str}}defaultswarninginfo, tool_dataprivacy{{/str}}
+</div>
+<div>
+ {{#str}}deletedefaultsconfirmation, tool_dataprivacy, {{activityname}}{{/str}}
+</div>
{{/navigation}}
<div data-region="purposes" class="m-t-3 m-b-1">
- <h3>{{#str}}purposes, tool_dataprivacy{{/str}}</h3>
<div class="m-y-1">
<button class="btn btn-secondary" data-add-element="purpose" title="{{#str}}addpurpose, tool_dataprivacy{{/str}}">
{{#pix}}t/add, moodle, {{#str}}addpurpose, tool_dataprivacy{{/str}}{{/pix}}
$this->assertEquals($u1->id, $dpo->id);
}
+ /**
+ * Test for \tool_dataprivacy\api::get_assigned_privacy_officer_roles().
+ */
+ public function test_get_assigned_privacy_officer_roles() {
+ global $DB;
+
+ // Erroneously set the manager roles as the PO, even if it doesn't have the managedatarequests capability yet.
+ $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
+ set_config('dporoles', $managerroleid, 'tool_dataprivacy');
+ // Get the assigned PO roles when nothing has been set yet.
+ $roleids = api::get_assigned_privacy_officer_roles();
+ // Confirm that the returned list is empty.
+ $this->assertEmpty($roleids);
+
+ $context = context_system::instance();
+
+ // Give the manager role with the capability to manage data requests.
+ assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
+
+ // Give the editing teacher role with the capability to manage data requests.
+ $editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
+ assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $editingteacherroleid, $context->id, true);
+
+ // Get the non-editing teacher role ID.
+ $teacherroleid = $DB->get_field('role', 'id', array('shortname' => 'teacher'));
+
+ // Erroneously map the manager and the non-editing teacher roles to the PO role.
+ $badconfig = $managerroleid . ',' . $teacherroleid;
+ set_config('dporoles', $badconfig, 'tool_dataprivacy');
+
+ // Get the assigned PO roles.
+ $roleids = api::get_assigned_privacy_officer_roles();
+
+ // There should only be one PO role.
+ $this->assertCount(1, $roleids);
+ // Confirm it contains the manager role.
+ $this->assertContains($managerroleid, $roleids);
+ // And it does not contain the editing teacher role.
+ $this->assertNotContains($editingteacherroleid, $roleids);
+ }
+
/**
* Test for api::approve_data_request().
*/
$this->assertEquals($data->contexts->used, $contextids, '', 0.0, 10, true);
}
+ /**
+ * Data provider for \tool_dataprivacy_api_testcase::test_set_context_defaults
+ */
+ public function set_context_defaults_provider() {
+ $contextlevels = [
+ [CONTEXT_COURSECAT],
+ [CONTEXT_COURSE],
+ [CONTEXT_MODULE],
+ [CONTEXT_BLOCK],
+ ];
+ $paramsets = [
+ [true, true, false, false], // Inherit category and purpose, Not for activity, Don't override.
+ [true, false, false, false], // Inherit category but not purpose, Not for activity, Don't override.
+ [false, true, false, false], // Inherit purpose but not category, Not for activity, Don't override.
+ [false, false, false, false], // Don't inherit both category and purpose, Not for activity, Don't override.
+ [false, false, false, true], // Don't inherit both category and purpose, Not for activity, Override instances.
+ ];
+ $data = [];
+ foreach ($contextlevels as $level) {
+ foreach ($paramsets as $set) {
+ $data[] = array_merge($level, $set);
+ }
+ if ($level == CONTEXT_MODULE) {
+ // Add a combination where defaults for activity is being set.
+ $data[] = [CONTEXT_MODULE, false, false, true, false];
+ $data[] = [CONTEXT_MODULE, false, false, true, true];
+ }
+ }
+ return $data;
+ }
+
+ /**
+ * Test for \tool_dataprivacy\api::set_context_defaults()
+ *
+ * @dataProvider set_context_defaults_provider
+ * @param int $contextlevel The context level
+ * @param bool $inheritcategory Whether to set category value as INHERIT.
+ * @param bool $inheritpurpose Whether to set purpose value as INHERIT.
+ * @param bool $foractivity Whether to set defaults for an activity.
+ * @param bool $override Whether to override instances.
+ */
+ public function test_set_context_defaults($contextlevel, $inheritcategory, $inheritpurpose, $foractivity, $override) {
+ $this->setAdminUser();
+
+ $generator = $this->getDataGenerator();
+
+ // Generate course cat, course, block, assignment, forum instances.
+ $coursecat = $generator->create_category();
+ $course = $generator->create_course(['category' => $coursecat->id]);
+ $block = $generator->create_block('online_users');
+ $assign = $generator->create_module('assign', ['course' => $course->id]);
+ $forum = $generator->create_module('forum', ['course' => $course->id]);
+
+ $coursecatcontext = context_coursecat::instance($coursecat->id);
+ $coursecontext = context_course::instance($course->id);
+ $blockcontext = context_block::instance($block->id);
+
+ list($course, $assigncm) = get_course_and_cm_from_instance($assign->id, 'assign');
+ list($course, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum');
+ $assigncontext = context_module::instance($assigncm->id);
+ $forumcontext = context_module::instance($forumcm->id);
+
+ // Generate purposes and categories.
+ $category1 = api::create_category((object)['name' => 'Test category 1']);
+ $category2 = api::create_category((object)['name' => 'Test category 2']);
+ $purpose1 = api::create_purpose((object)[
+ 'name' => 'Test purpose 1', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+ ]);
+ $purpose2 = api::create_purpose((object)[
+ 'name' => 'Test purpose 2', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+ ]);
+
+ // Assign purposes and categories to contexts.
+ $coursecatctxinstance = api::set_context_instance((object) [
+ 'contextid' => $coursecatcontext->id,
+ 'purposeid' => $purpose1->get('id'),
+ 'categoryid' => $category1->get('id'),
+ ]);
+ $coursectxinstance = api::set_context_instance((object) [
+ 'contextid' => $coursecontext->id,
+ 'purposeid' => $purpose1->get('id'),
+ 'categoryid' => $category1->get('id'),
+ ]);
+ $blockctxinstance = api::set_context_instance((object) [
+ 'contextid' => $blockcontext->id,
+ 'purposeid' => $purpose1->get('id'),
+ 'categoryid' => $category1->get('id'),
+ ]);
+ $assignctxinstance = api::set_context_instance((object) [
+ 'contextid' => $assigncontext->id,
+ 'purposeid' => $purpose1->get('id'),
+ 'categoryid' => $category1->get('id'),
+ ]);
+ $forumctxinstance = api::set_context_instance((object) [
+ 'contextid' => $forumcontext->id,
+ 'purposeid' => $purpose1->get('id'),
+ 'categoryid' => $category1->get('id'),
+ ]);
+
+ $categoryid = $inheritcategory ? context_instance::INHERIT : $category2->get('id');
+ $purposeid = $inheritpurpose ? context_instance::INHERIT : $purpose2->get('id');
+ $activity = '';
+ if ($contextlevel == CONTEXT_MODULE && $foractivity) {
+ $activity = 'assign';
+ }
+ $result = api::set_context_defaults($contextlevel, $categoryid, $purposeid, $activity, $override);
+ $this->assertTrue($result);
+
+ $targetctxinstance = false;
+ switch ($contextlevel) {
+ case CONTEXT_COURSECAT:
+ $targetctxinstance = $coursecatctxinstance;
+ break;
+ case CONTEXT_COURSE:
+ $targetctxinstance = $coursectxinstance;
+ break;
+ case CONTEXT_MODULE:
+ $targetctxinstance = $assignctxinstance;
+ break;
+ case CONTEXT_BLOCK:
+ $targetctxinstance = $blockctxinstance;
+ break;
+ }
+ $this->assertNotFalse($targetctxinstance);
+
+ // Check the context instances.
+ $instanceexists = context_instance::record_exists($targetctxinstance->get('id'));
+ if ($override) {
+ // If overridden, context instances on this context level would have been deleted.
+ $this->assertFalse($instanceexists);
+
+ // Check forum context instance.
+ $forumctxexists = context_instance::record_exists($forumctxinstance->get('id'));
+ if ($contextlevel != CONTEXT_MODULE || $foractivity) {
+ // The forum context instance won't be affected in this test if:
+ // - The overridden defaults are not for context modules.
+ // - Only the defaults for assign have been set.
+ $this->assertTrue($forumctxexists);
+ } else {
+ // If we're overriding for the whole course module context level,
+ // then this forum context instance will be deleted as well.
+ $this->assertFalse($forumctxexists);
+ }
+ } else {
+ // Otherwise, the context instance record remains.
+ $this->assertTrue($instanceexists);
+ }
+
+ // Check defaults.
+ list($defaultpurpose, $defaultcategory) = data_registry::get_defaults($contextlevel, $activity);
+ if (!$inheritpurpose) {
+ $this->assertEquals($purposeid, $defaultpurpose);
+ }
+ if (!$inheritcategory) {
+ $this->assertEquals($categoryid, $defaultcategory);
+ }
+ }
+
/**
* Perform setup for the test_add_request_contexts_with_status_xxxxx tests.
*
--- /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/>.
+
+/**
+ * Step definitions to generate database fixtures for the data privacy tool.
+ *
+ * @package tool_dataprivacy
+ * @category test
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+
+use Behat\Gherkin\Node\TableNode as TableNode;
+use Behat\Behat\Tester\Exception\PendingException as PendingException;
+use tool_dataprivacy\api;
+
+/**
+ * Step definitions to generate database fixtures for the data privacy tool.
+ *
+ * @package tool_dataprivacy
+ * @category test
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_tool_dataprivacy extends behat_base {
+
+ /**
+ * Each element specifies:
+ * - The data generator suffix used.
+ * - The required fields.
+ * - The mapping between other elements references and database field names.
+ * @var array
+ */
+ protected static $elements = array(
+ 'categories' => array(
+ 'datagenerator' => 'category',
+ 'required' => array()
+ ),
+ 'purposes' => array(
+ 'datagenerator' => 'purpose',
+ 'required' => array()
+ ),
+ );
+
+ /**
+ * Creates the specified element. More info about available elements in http://docs.moodle.org/dev/Acceptance_testing#Fixtures.
+ *
+ * @Given /^the following data privacy "(?P<element_string>(?:[^"]|\\")*)" exist:$/
+ *
+ * @param string $elementname The name of the entity to add
+ * @param TableNode $data
+ */
+ public function the_following_data_categories_exist($elementname, TableNode $data) {
+
+ // Now that we need them require the data generators.
+ require_once(__DIR__.'/../../../../../lib/phpunit/classes/util.php');
+
+ if (empty(self::$elements[$elementname])) {
+ throw new PendingException($elementname . ' data generator is not implemented');
+ }
+
+ $datagenerator = testing_util::get_data_generator();
+ $dataprivacygenerator = $datagenerator->get_plugin_generator('tool_dataprivacy');
+
+ $elementdatagenerator = self::$elements[$elementname]['datagenerator'];
+ $requiredfields = self::$elements[$elementname]['required'];
+ if (!empty(self::$elements[$elementname]['switchids'])) {
+ $switchids = self::$elements[$elementname]['switchids'];
+ }
+
+ foreach ($data->getHash() as $elementdata) {
+
+ // Check if all the required fields are there.
+ foreach ($requiredfields as $requiredfield) {
+ if (!isset($elementdata[$requiredfield])) {
+ throw new Exception($elementname . ' requires the field ' . $requiredfield . ' to be specified');
+ }
+ }
+
+ // Switch from human-friendly references to ids.
+ if (isset($switchids)) {
+ foreach ($switchids as $element => $field) {
+ $methodname = 'get_' . $element . '_id';
+
+ // Not all the switch fields are required, default vars will be assigned by data generators.
+ if (isset($elementdata[$element])) {
+ // Temp $id var to avoid problems when $element == $field.
+ $id = $this->{$methodname}($elementdata[$element]);
+ unset($elementdata[$element]);
+ $elementdata[$field] = $id;
+ }
+ }
+ }
+
+ // Preprocess the entities that requires a special treatment.
+ if (method_exists($this, 'preprocess_' . $elementdatagenerator)) {
+ $elementdata = $this->{'preprocess_' . $elementdatagenerator}($elementdata);
+ }
+
+ // Creates element.
+ $methodname = 'create_' . $elementdatagenerator;
+ if (method_exists($dataprivacygenerator, $methodname)) {
+ // Using data generators directly.
+ $dataprivacygenerator->{$methodname}($elementdata);
+
+ } else if (method_exists($this, 'process_' . $elementdatagenerator)) {
+ // Using an alternative to the direct data generator call.
+ $this->{'process_' . $elementdatagenerator}($elementdata);
+ } else {
+ throw new PendingException($elementname . ' data generator is not implemented');
+ }
+ }
+ }
+
+ /**
+ * Sets the data category and data storage purpose for the site.
+ *
+ * @Given /^I set the site category and purpose to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+ *
+ * @param string $category The ID of the category to be set for the instance.
+ * @param string $purpose The ID of the purpose to be set for the instance.
+ */
+ public function i_set_the_site_category_and_purpose($category, $purpose) {
+ $category = \tool_dataprivacy\category::get_record(['name' => $category]);
+ $purpose = \tool_dataprivacy\purpose::get_record(['name' => $purpose]);
+ $data = (object)[
+ 'contextlevel' => CONTEXT_SYSTEM,
+ 'categoryid' => $category->get('id'),
+ 'purposeid' => $purpose->get('id'),
+ ];
+ api::set_contextlevel($data);
+ }
+
+ /**
+ * Sets the data category and data storage purpose for a course category instance.
+ *
+ * @Given /^I set the category and purpose for the course category "(?P<categoryname_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+ *
+ * @param string $name The instance name. It should match the name or the idnumber.
+ * @param string $category The ID of the category to be set for the instance.
+ * @param string $purpose The ID of the purpose to be set for the instance.
+ */
+ public function i_set_the_category_and_purpose_for_course_category($name, $category, $purpose) {
+ global $DB;
+
+ $params = [
+ 'name' => $name,
+ 'idnumber' => $name,
+ ];
+ $select = 'name = :name OR idnumber = :idnumber';
+ $coursecatid = $DB->get_field_select('course_categories', 'id', $select, $params, MUST_EXIST);
+ $context = context_coursecat::instance($coursecatid);
+
+ $this->set_category_and_purpose($context->id, $category, $purpose);
+ }
+
+ /**
+ * Sets the data category and data storage purpose for a course instance.
+ *
+ * @Given /^I set the category and purpose for the course "(?P<coursename_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+ *
+ * @param string $name The instance name. It should match the fullname or the shortname, or the idnumber.
+ * @param string $category The ID of the category to be set for the instance.
+ * @param string $purpose The ID of the purpose to be set for the instance.
+ */
+ public function i_set_the_category_and_purpose_for_course($name, $category, $purpose) {
+ global $DB;
+
+ $params = [
+ 'shortname' => $name,
+ 'fullname' => $name,
+ 'idnumber' => $name,
+ ];
+ $select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
+ $courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
+ $context = context_course::instance($courseid);
+
+ $this->set_category_and_purpose($context->id, $category, $purpose);
+ }
+
+ /**
+ * Sets the data category and data storage purpose for a course instance.
+ *
+ * @Given /^I set the category and purpose for the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" in course "(?P<coursename_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+ *
+ * @param string $name The instance name. It should match the name of the activity.
+ * @param string $type The activity type. E.g. assign, quiz, forum, etc.
+ * @param string $coursename The course name. It should match the fullname or the shortname, or the idnumber.
+ * @param string $category The ID of the category to be set for the instance.
+ * @param string $purpose The ID of the purpose to be set for the instance.
+ */
+ public function i_set_the_category_and_purpose_for_activity($name, $type, $coursename, $category, $purpose) {
+ global $DB;
+
+ $params = [
+ 'shortname' => $coursename,
+ 'fullname' => $coursename,
+ 'idnumber' => $coursename,
+ ];
+ $select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
+ $courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
+
+ $cmid = null;
+ $cms = get_coursemodules_in_course($type, $courseid);
+ foreach ($cms as $cm) {
+ if ($cm->name === $name || $cm->idnumber === $name) {
+ $cmid = $cm->id;
+ break;
+ }
+ }
+ if ($cmid === null) {
+ throw new coding_exception("Activity module '{$name}' of type '{$type}' not found!");
+ }
+ $context = context_module::instance($cmid);
+
+ $this->set_category_and_purpose($context->id, $category, $purpose);
+ }
+
+ /**
+ * Sets the data category and data storage purpose for a course instance.
+ *
+ * @Given /^I set the category and purpose for the "(?P<blockname_string>(?:[^"]|\\")*)" block in the "(?P<coursename_string>(?:[^"]|\\")*)" course to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+ *
+ * @param string $name The instance name. It should match the name of the block. (e.g. online_users)
+ * @param string $coursename The course name. It should match the fullname or the shortname, or the idnumber.
+ * @param string $category The ID of the category to be set for the instance.
+ * @param string $purpose The ID of the purpose to be set for the instance.
+ */
+ public function i_set_the_category_and_purpose_for_block($name, $coursename, $category, $purpose) {
+ global $DB;
+
+ $params = [
+ 'shortname' => $coursename,
+ 'fullname' => $coursename,
+ 'idnumber' => $coursename,
+ ];
+ $select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
+ $courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
+
+ // Fetch the course context.
+ $coursecontext = context_course::instance($courseid);
+
+ // Fetch the block record and context.
+ $blockid = $DB->get_field('block_instances', 'id', ['blockname' => $name, 'parentcontextid' => $coursecontext->id]);
+ $context = context_block::instance($blockid);
+
+ // Set the category and purpose.
+ $this->set_category_and_purpose($context->id, $category, $purpose);
+ }
+
+ /**
+ * Sets the category and purpose for a context instance.
+ *
+ * @param int $contextid The context ID.
+ * @param int $categoryname The category name.
+ * @param int $purposename The purpose name.
+ * @throws coding_exception
+ */
+ protected function set_category_and_purpose($contextid, $categoryname, $purposename) {
+ $category = \tool_dataprivacy\category::get_record(['name' => $categoryname]);
+ $purpose = \tool_dataprivacy\purpose::get_record(['name' => $purposename]);
+
+ api::set_context_instance((object) [
+ 'contextid' => $contextid,
+ 'purposeid' => $purpose->get('id'),
+ 'categoryid' => $category->get('id'),
+ ]);
+ }
+}
--- /dev/null
+@tool @tool_dataprivacy @javascript
+Feature: Manage data registry defaults
+ As the privacy officer
+ In order to manage the data registry
+ I need to be able to manage the default data categories and data storage purposes for various context levels.
+
+ Background:
+ Given I log in as "admin"
+ And the following "categories" exist:
+ | name | idnumber | category |
+ | Science and technology | scitech | |
+ | Physics | st-phys | scitech |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Fundamentals of physics 1 | Physics 101 | st-phys |
+ And the following "activities" exist:
+ | activity | name | idnumber | course |
+ | assign | Assignment 1 | assign1 | Physics 101 |
+ | forum | Forum 1 | forum1 | Physics 101 |
+ And the following "blocks" exist:
+ | blockname | contextlevel | reference | pagetypepattern | defaultregion |
+ | online_users | Course | Physics 101 | course-view-* | site-post |
+ And the following data privacy "categories" exist:
+ | name |
+ | Site category |
+ | Category 1 |
+ | Category 2 |
+ And the following data privacy "purposes" exist:
+ | name | retentionperiod |
+ | Site purpose | P10Y |
+ | Purpose 1 | P3Y |
+ | Purpose 2 | P5Y |
+ And I set the site category and purpose to "Site category" and "Site purpose"
+
+ Scenario: Set course category data registry defaults
+ Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Set defaults" "link"
+ And I should see "Inherit"
+ And I should not see "Add a new module default"
+ And I press "Edit"
+ And I set the field "Category" to "Category 1"
+ And I set the field "Purpose" to "Purpose 1"
+ When I press "Save changes"
+ Then I should see "Category 1"
+ And I should see "Purpose 1"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Science and technology" "link"
+ And I wait until the page is ready
+ And the field "categoryid" matches value "Category 2"
+ And the field "purposeid" matches value "Purpose 2"
+ And I should see "5 years"
+
+ Scenario: Set course category data registry defaults with override
+ Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Set defaults" "link"
+ And I should see "Inherit"
+ And I should not see "Add a new module default"
+ And I press "Edit"
+ And I set the field "Category" to "Category 1"
+ And I set the field "Purpose" to "Purpose 1"
+ And I click on "Reset instances with custom values" "checkbox"
+ When I press "Save changes"
+ Then I should see "Category 1"
+ And I should see "Purpose 1"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Science and technology" "link"
+ And I wait until the page is ready
+ And the field "categoryid" matches value "Not set (use the default value)"
+ And the field "purposeid" matches value "Not set (use the default value)"
+ And I should see "3 years"
+
+ Scenario: Set course data registry defaults
+ Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Set defaults" "link"
+ And I click on "Courses" "link"
+ And I should see "Inherit"
+ And I should not see "Add a new module default"
+ And I press "Edit"
+ And I set the field "Category" to "Category 1"
+ And I set the field "Purpose" to "Purpose 1"
+ When I press "Save changes"
+ Then I should see "Category 1"
+ And I should see "Purpose 1"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Science and technology" "link"
+ And I wait until the page is ready
+ And I click on "Courses" "link"
+ And I wait until the page is ready
+ And I click on "Physics 101" "link"
+ And I wait until the page is ready
+ And the field "categoryid" matches value "Category 2"
+ And the field "purposeid" matches value "Purpose 2"
+ And I should see "5 years (after the course end date)"
+
+ Scenario: Set course data registry defaults with override
+ Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Set defaults" "link"
+ And I click on "Courses" "link"
+ And I should see "Inherit"
+ And I should not see "Add a new module default"
+ And I press "Edit"
+ And I set the field "Category" to "Category 1"
+ And I set the field "Purpose" to "Purpose 1"
+ And I click on "Reset instances with custom values" "checkbox"
+ When I press "Save changes"
+ Then I should see "Category 1"
+ And I should see "Purpose 1"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Science and technology" "link"
+ And I wait until the page is ready
+ And I click on "Courses" "link"
+ And I wait until the page is ready
+ And I click on "Physics 101" "link"
+ And I wait until the page is ready
+ And the field "categoryid" matches value "Not set (use the default value)"
+ And the field "purposeid" matches value "Not set (use the default value)"
+ And I should see "3 years (after the course end date)"
+
+ Scenario: Set module level data registry defaults
+ Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Set defaults" "link"
+ And I click on "Activity modules" "link"
+ And I should see "Inherit"
+ And I should see "Add a new module default"
+ And I press "Edit"
+ And I set the field "Category" to "Category 1"
+ And I set the field "Purpose" to "Purpose 1"
+ When I press "Save changes"
+ Then I should see "Category 1"
+ And I should see "Purpose 1"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Science and technology" "link"
+ And I wait until the page is ready
+ And I click on "Courses" "link"
+ And I wait until the page is ready
+ And I click on "Physics 101" "link"
+ And I wait until the page is ready
+ And I click on "Activities and resources" "link"
+ And I wait until the page is ready
+ And I click on "Assignment 1 (Assignment)" "link"
+ And I wait until the page is ready
+ And the field "categoryid" matches value "Category 2"
+ And the field "purposeid" matches value "Purpose 2"
+ And I should see "5 years (after the course end date)"
+
+ Scenario: Set module level data registry defaults with override
+ Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Set defaults" "link"
+ And I click on "Activity modules" "link"
+ And I should see "Inherit"
+ And I should see "Add a new module default"
+ And I press "Edit"
+ And I set the field "Category" to "Category 1"
+ And I set the field "Purpose" to "Purpose 1"
+ And I click on "Reset instances with custom values" "checkbox"
+ When I press "Save changes"
+ Then I should see "Category 1"
+ And I should see "Purpose 1"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Science and technology" "link"
+ And I wait until the page is ready
+ And I click on "Courses" "link"
+ And I wait until the page is ready
+ And I click on "Physics 101" "link"
+ And I wait until the page is ready
+ And I click on "Activities and resources" "link"
+ And I wait until the page is ready
+ And I click on "Assignment 1 (Assignment)" "link"
+ And I wait until the page is ready
+ And the field "categoryid" matches value "Not set (use the default value)"
+ And the field "purposeid" matches value "Not set (use the default value)"
+ And I click on "Forum 1 (Forum)" "link"
+ And I wait until the page is ready
+ And the field "categoryid" matches value "Not set (use the default value)"
+ And the field "purposeid" matches value "Not set (use the default value)"
+ And I should see "3 years (after the course end date)"
+
+ Scenario: Set data registry defaults for an activity module
+ Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Set defaults" "link"
+ And I click on "Activity modules" "link"
+ And I should see "Inherit"
+ And I should see "Add a new module default"
+ And I press "Add a new module default"
+ And I set the field "Activity module" to "Assignment"
+ And I set the field "Category" to "Category 1"
+ And I set the field "Purpose" to "Purpose 1"
+ When I press "Save changes"
+ Then I should see "Category 1" in the "Assignment" "table_row"
+ And I should see "Purpose 1" in the "Assignment" "table_row"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Science and technology" "link"
+ And I wait until the page is ready
+ And I click on "Courses" "link"
+ And I wait until the page is ready
+ And I click on "Physics 101" "link"
+ And I wait until the page is ready
+ And I click on "Activities and resources" "link"
+ And I wait until the page is ready
+ And I click on "Assignment 1 (Assignment)" "link"
+ And I wait until the page is ready
+ And the field "categoryid" matches value "Category 2"
+ And the field "purposeid" matches value "Purpose 2"
+ And I should see "5 years (after the course end date)"
+
+ Scenario: Set data registry defaults for an activity module with override
+ Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Set defaults" "link"
+ And I click on "Activity modules" "link"
+ And I should see "Inherit"
+ And I should see "Add a new module default"
+ And I press "Add a new module default"
+ And I set the field "Activity module" to "Assignment"
+ And I set the field "Category" to "Category 1"
+ And I set the field "Purpose" to "Purpose 1"
+ And I click on "Reset instances with custom values" "checkbox"
+ When I press "Save changes"
+ Then I should see "Category 1" in the "Assignment" "table_row"
+ And I should see "Purpose 1" in the "Assignment" "table_row"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Science and technology" "link"
+ And I wait until the page is ready
+ And I click on "Courses" "link"
+ And I wait until the page is ready
+ And I click on "Physics 101" "link"
+ And I wait until the page is ready
+ And I click on "Activities and resources" "link"
+ And I wait until the page is ready
+ And I click on "Assignment 1 (Assignment)" "link"
+ And I wait until the page is ready
+ And the field "categoryid" matches value "Not set (use the default value)"
+ And the field "purposeid" matches value "Not set (use the default value)"
+ And I should see "3 years (after the course end date)"
+
+ Scenario: Set block category data registry defaults
+ Given I set the category and purpose for the "online_users" block in the "Physics 101" course to "Category 2" and "Purpose 2"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Set defaults" "link"
+ And I click on "Blocks" "link"
+ And I should see "Inherit"
+ And I should not see "Add a new module default"
+ And I press "Edit"
+ And I set the field "Category" to "Category 1"
+ And I set the field "Purpose" to "Purpose 1"
+ When I press "Save changes"
+ Then I should see "Category 1"
+ And I should see "Purpose 1"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Science and technology" "link"
+ And I wait until the page is ready
+ And I click on "Courses" "link"
+ And I wait until the page is ready
+ And I click on "Physics 101" "link"
+ And I wait until the page is ready
+ And I click on "Blocks" "link"
+ And I wait until the page is ready
+ And I click on "Online users" "link"
+ And I wait until the page is ready
+ And the field "categoryid" matches value "Category 2"
+ And the field "purposeid" matches value "Purpose 2"
+ And I should see "5 years (after the course end date)"
+
+ Scenario: Set course category data registry defaults with override
+ Given I set the category and purpose for the "online_users" block in the "Physics 101" course to "Category 2" and "Purpose 2"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Set defaults" "link"
+ And I click on "Blocks" "link"
+ And I should see "Inherit"
+ And I should not see "Add a new module default"
+ And I press "Edit"
+ And I set the field "Category" to "Category 1"
+ And I set the field "Purpose" to "Purpose 1"
+ And I click on "Reset instances with custom values" "checkbox"
+ When I press "Save changes"
+ Then I should see "Category 1"
+ And I should see "Purpose 1"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Science and technology" "link"
+ And I wait until the page is ready
+ And I click on "Courses" "link"
+ And I wait until the page is ready
+ And I click on "Physics 101" "link"
+ And I wait until the page is ready
+ And I click on "Blocks" "link"
+ And I wait until the page is ready
+ And I click on "Online users" "link"
+ And I wait until the page is ready
+ And the field "categoryid" matches value "Not set (use the default value)"
+ And the field "purposeid" matches value "Not set (use the default value)"
+ And I should see "3 years (after the course end date)"
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
use tool_dataprivacy\api;
+use tool_dataprivacy\context_instance;
use tool_dataprivacy\external;
/**
$this->expectException(dml_missing_record_exception::class);
external::get_data_request($this->requestid + 1);
}
+
+ /**
+ * Test for \tool_dataprivacy\external::set_context_defaults()
+ * when called by a user that doesn't have the manage registry capability.
+ */
+ public function test_set_context_defaults_no_capability() {
+ $generator = $this->getDataGenerator();
+ $user = $generator->create_user();
+ $this->setUser($user);
+ $this->expectException(required_capability_exception::class);
+ external::set_context_defaults(CONTEXT_COURSECAT, context_instance::INHERIT, context_instance::INHERIT, '', false);
+ }
+
+ /**
+ * Test for \tool_dataprivacy\external::set_context_defaults().
+ *
+ * We're just checking the module context level here to test the WS function.
+ * More testing is done in \tool_dataprivacy_api_testcase::test_set_context_defaults().
+ *
+ * @dataProvider get_options_provider
+ * @param bool $modulelevel Whether defaults are to be applied on the module context level or for an activity only.
+ * @param bool $override Whether to override instances.
+ */
+ public function test_set_context_defaults($modulelevel, $override) {
+ $this->setAdminUser();
+ $generator = $this->getDataGenerator();
+
+ // Generate course cat, course, block, assignment, forum instances.
+ $coursecat = $generator->create_category();
+ $course = $generator->create_course(['category' => $coursecat->id]);
+ $assign = $generator->create_module('assign', ['course' => $course->id]);
+ list($course, $assigncm) = get_course_and_cm_from_instance($assign->id, 'assign');
+ $assigncontext = context_module::instance($assigncm->id);
+
+ // Generate purpose and category.
+ $category1 = api::create_category((object)['name' => 'Test category 1']);
+ $category2 = api::create_category((object)['name' => 'Test category 2']);
+ $purpose1 = api::create_purpose((object)[
+ 'name' => 'Test purpose 1', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+ ]);
+ $purpose2 = api::create_purpose((object)[
+ 'name' => 'Test purpose 2', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+ ]);
+
+ // Set a custom purpose and ID for this assignment instance.
+ $assignctxinstance = api::set_context_instance((object) [
+ 'contextid' => $assigncontext->id,
+ 'purposeid' => $purpose1->get('id'),
+ 'categoryid' => $category1->get('id'),
+ ]);
+
+ $modulename = $modulelevel ? 'assign' : '';
+ $categoryid = $category2->get('id');
+ $purposeid = $purpose2->get('id');
+ $result = external::set_context_defaults(CONTEXT_MODULE, $categoryid, $purposeid, $modulename, $override);
+
+ // Extract the result.
+ $return = external_api::clean_returnvalue(external::set_context_defaults_returns(), $result);
+ $this->assertTrue($return['result']);
+
+ // Check the assignment context instance.
+ $instanceexists = context_instance::record_exists($assignctxinstance->get('id'));
+ if ($override) {
+ // The custom assign instance should have been deleted.
+ $this->assertFalse($instanceexists);
+ } else {
+ // The custom assign instance should still exist.
+ $this->assertTrue($instanceexists);
+ }
+
+ // Check the saved defaults.
+ list($savedpurpose, $savedcategory) = \tool_dataprivacy\data_registry::get_defaults(CONTEXT_MODULE, $modulename);
+ $this->assertEquals($categoryid, $savedcategory);
+ $this->assertEquals($purposeid, $savedpurpose);
+ }
+
+ /**
+ * Test for \tool_dataprivacy\external::get_category_options()
+ * when called by a user that doesn't have the manage registry capability.
+ */
+ public function test_get_category_options_no_capability() {
+ $generator = $this->getDataGenerator();
+ $user = $generator->create_user();
+ $this->setUser($user);
+ $this->expectException(required_capability_exception::class);
+ external::get_category_options(true, true);
+ }
+
+ /**
+ * Data provider for \tool_dataprivacy_external_testcase::test_XX_options().
+ */
+ public function get_options_provider() {
+ return [
+ [false, false],
+ [false, true],
+ [true, false],
+ [true, true],
+ ];
+ }
+
+ /**
+ * Test for \tool_dataprivacy\external::get_category_options().
+ *
+ * @dataProvider get_options_provider
+ * @param bool $includeinherit Whether "Inherit" would be included to the options.
+ * @param bool $includenotset Whether "Not set" would be included to the options.
+ */
+ public function test_get_category_options($includeinherit, $includenotset) {
+ $this->setAdminUser();
+
+ // Prepare our expected options.
+ $expectedoptions = [];
+ if ($includeinherit) {
+ $expectedoptions[] = [
+ 'id' => context_instance::INHERIT,
+ 'name' => get_string('inherit', 'tool_dataprivacy'),
+ ];
+ }
+
+ if ($includenotset) {
+ $expectedoptions[] = [
+ 'id' => context_instance::NOTSET,
+ 'name' => get_string('notset', 'tool_dataprivacy'),
+ ];
+ }
+
+ for ($i = 1; $i <= 3; $i++) {
+ $category = api::create_category((object)['name' => 'Category ' . $i]);
+ $expectedoptions[] = [
+ 'id' => $category->get('id'),
+ 'name' => $category->get('name'),
+ ];
+ }
+
+ // Call the WS function.
+ $result = external::get_category_options($includeinherit, $includenotset);
+
+ // Extract the options.
+ $return = (object) external_api::clean_returnvalue(external::get_category_options_returns(), $result);
+ $options = $return->options;
+
+ // Make sure everything checks out.
+ $this->assertCount(count($expectedoptions), $options);
+ foreach ($options as $option) {
+ $this->assertContains($option, $expectedoptions);
+ }
+ }
+
+ /**
+ * Test for \tool_dataprivacy\external::get_purpose_options()
+ * when called by a user that doesn't have the manage registry capability.
+ */
+ public function test_get_purpose_options_no_capability() {
+ $generator = $this->getDataGenerator();
+ $user = $generator->create_user();
+ $this->setUser($user);
+ $this->expectException(required_capability_exception::class);
+ external::get_category_options(true, true);
+ }
+
+ /**
+ * Test for \tool_dataprivacy\external::get_purpose_options().
+ *
+ * @dataProvider get_options_provider
+ * @param bool $includeinherit Whether "Inherit" would be included to the options.
+ * @param bool $includenotset Whether "Not set" would be included to the options.
+ */
+ public function test_get_purpose_options($includeinherit, $includenotset) {
+ $this->setAdminUser();
+
+ // Prepare our expected options.
+ $expectedoptions = [];
+ if ($includeinherit) {
+ $expectedoptions[] = [
+ 'id' => context_instance::INHERIT,
+ 'name' => get_string('inherit', 'tool_dataprivacy'),
+ ];
+ }
+
+ if ($includenotset) {
+ $expectedoptions[] = [
+ 'id' => context_instance::NOTSET,
+ 'name' => get_string('notset', 'tool_dataprivacy'),
+ ];
+ }
+
+ for ($i = 1; $i <= 3; $i++) {
+ $purpose = api::create_purpose((object)[
+ 'name' => 'Purpose ' . $i, 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+ ]);
+ $expectedoptions[] = [
+ 'id' => $purpose->get('id'),
+ 'name' => $purpose->get('name'),
+ ];
+ }
+
+ // Call the WS function.
+ $result = external::get_purpose_options($includeinherit, $includenotset);
+
+ // Extract the options.
+ $return = (object) external_api::clean_returnvalue(external::get_purpose_options_returns(), $result);
+ $options = $return->options;
+
+ // Make sure everything checks out.
+ $this->assertCount(count($expectedoptions), $options);
+ foreach ($options as $option) {
+ $this->assertContains($option, $expectedoptions);
+ }
+ }
+
+ /**
+ * Data provider for \tool_dataprivacy_external_testcase::get_activity_options().
+ */
+ public function get_activity_options_provider() {
+ return [
+ [false, false, true],
+ [false, true, true],
+ [true, false, true],
+ [true, true, true],
+ [false, false, false],
+ [false, true, false],
+ [true, false, false],
+ [true, true, false],
+ ];
+ }
+
+ /**
+ * Test for \tool_dataprivacy\external::get_activity_options().
+ *
+ * @dataProvider get_activity_options_provider
+ * @param bool $inheritcategory Whether the category would be set to "Inherit".
+ * @param bool $inheritpurpose Whether the purpose would be set to "Inherit".
+ * @param bool $nodefaults Whether to fetch only activities that don't have defaults.
+ */
+ public function test_get_activity_options($inheritcategory, $inheritpurpose, $nodefaults) {
+ $this->setAdminUser();
+
+ $category = api::create_category((object)['name' => 'Test category']);
+ $purpose = api::create_purpose((object)[
+ 'name' => 'Test purpose ', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+ ]);
+ $categoryid = $category->get('id');
+ $purposeid = $purpose->get('id');
+
+ if ($inheritcategory) {
+ $categoryid = context_instance::INHERIT;
+ }
+ if ($inheritpurpose) {
+ $purposeid = context_instance::INHERIT;
+ }
+
+ // Set the context default for the assignment module.
+ api::set_context_defaults(CONTEXT_MODULE, $categoryid, $purposeid, 'assign');
+
+ // Call the WS function.
+ $result = external::get_activity_options($nodefaults);
+
+ // Extract the options.
+ $return = (object) external_api::clean_returnvalue(external::get_activity_options_returns(), $result);
+ $options = $return->options;
+
+ // Make sure the options list is not empty.
+ $this->assertNotEmpty($options);
+
+ $pluginwithdefaults = [
+ 'name' => 'assign',
+ 'displayname' => get_string('pluginname', 'assign')
+ ];
+
+ // If we don't want plugins with defaults to be listed or if both of the category and purpose are set to inherit,
+ // the assign module should be listed.
+ if (!$nodefaults || ($inheritcategory && $inheritpurpose)) {
+ $this->assertContains($pluginwithdefaults, $options);
+ } else {
+ $this->assertNotContains($pluginwithdefaults, $options);
+ }
+ }
}
--- /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/>.
+
+/**
+ * Data privacy tool data generator.
+ *
+ * @package tool_dataprivacy
+ * @category test
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use tool_dataprivacy\api;
+use tool_dataprivacy\category;
+use tool_dataprivacy\purpose;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Data privacy tool data generator class.
+ *
+ * @package tool_dataprivacy
+ * @category test
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_dataprivacy_generator extends component_generator_base {
+
+ /** @var int Number of created categories. */
+ protected $categorycount = 0;
+
+ /** @var int Number of created purposes. */
+ protected $purposecount = 0;
+
+ /**
+ * Reset process.
+ *
+ * Do not call directly.
+ *
+ * @return void
+ */
+ public function reset() {
+ $this->categorycount = 0;
+ $this->purposecount = 0;
+ }
+
+ /**
+ * Create a new category.
+ *
+ * @param array|stdClass $record
+ * @return category
+ */
+ public function create_category($record = null) {
+ $this->categorycount++;
+ $i = $this->categorycount;
+ $record = (object)$record;
+
+ if (!isset($record->name)) {
+ $record->name = "Test purpose $i";
+ }
+
+ if (!isset($record->description)) {
+ $record->description = "{$record->name} description";
+ }
+
+ $category = api::create_category($record);
+
+ return $category;
+ }
+
+ /**
+ * Create a new purpose.
+ *
+ * @param array|stdClass $record
+ * @return purpose
+ */
+ public function create_purpose($record = null) {
+ $this->purposecount++;
+ $i = $this->purposecount;
+ $record = (object)$record;
+
+ if (!isset($record->name)) {
+ $record->name = "Test purpose $i";
+ }
+
+ if (!isset($record->description)) {
+ $record->description = "{$record->name} $i description";
+ }
+
+ if (!isset($record->retentionperiod)) {
+ $record->retentionperiod = 'PT1M';
+ }
+
+ if (!isset($record->lawfulbases)) {
+ $record->lawfulbases = 'gdpr_art_6_1_a';
+ }
+
+ $purpose = api::create_purpose($record);
+
+ return $purpose;
+ }
+}
defined('MOODLE_INTERNAL') || die;
-$plugin->version = 2018082100;
+$plugin->version = 2018091000;
$plugin->requires = 2018050800; // Moodle 3.5dev (Build 2018031600) and upwards.
$plugin->component = 'tool_dataprivacy';
}
];
+ /**
+ * @var {object} currentTrigger The triggered HTML jQuery object
+ * @private
+ */
+ AcceptOnBehalf.prototype.currentTrigger = null;
+
+ /**
+ * @var {object} triggers The trigger selectors
+ * @private
+ */
+ AcceptOnBehalf.prototype.triggers = {
+ SINGLE: 'a[data-action=acceptmodal]',
+ BULK: 'input[data-action=acceptmodal]'
+ };
+
/**
* Initialise the class.
*
*/
AcceptOnBehalf.prototype.init = function() {
// Initialise for links accepting policies for individual users.
- var triggers = $('a[data-action=acceptmodal]');
- triggers.on('click', function(e) {
+ $(this.triggers.SINGLE).on('click', function(e) {
e.preventDefault();
+ this.currentTrigger = $(e.currentTarget);
var href = $(e.currentTarget).attr('href'),
formData = href.slice(href.indexOf('?') + 1);
this.showFormModal(formData);
}.bind(this));
// Initialise for multiple users acceptance form.
- triggers = $('form[data-action=acceptmodal]');
- triggers.on('submit', function(e) {
+ $(this.triggers.BULK).on('click', function(e) {
e.preventDefault();
- if ($(e.currentTarget).find('input[type=checkbox][name="userids[]"]:checked').length) {
- var formData = $(e.currentTarget).serialize();
- this.showFormModal(formData, triggers);
+ this.currentTrigger = $(e.currentTarget);
+ var form = $(e.currentTarget).closest('form');
+ if (form.find('input[type=checkbox][name="userids[]"]:checked').length) {
+ var formData = form.serialize();
+ this.showFormModal(formData);
} else {
Str.get_strings(this.stringKeys).done(function(strings) {
Notification.alert('', strings[2], strings[3]);
* Show modal with a form
*
* @param {String} formData
- * @param {object} triggerElement The trigger HTML jQuery object
*/
- AcceptOnBehalf.prototype.showFormModal = function(formData, triggerElement) {
+ AcceptOnBehalf.prototype.showFormModal = function(formData) {
var action;
var params = formData.split('&');
for (var i = 0; i < params.length; i++) {
type: ModalFactory.types.SAVE_CANCEL,
title: title,
body: ''
- }, triggerElement).done(function(modal) {
+ }).done(function(modal) {
this.modal = modal;
this.setupFormModal(formData, saveText);
}.bind(this));
M.core_formchangechecker.reset_form_dirty_state();
});
this.modal.destroy();
+ this.currentTrigger.focus();
};
return /** @alias module:tool_policy/acceptmodal */ {
*/
public function wrap_html_start() {
echo \html_writer::start_tag('form',
- ['action' => new \moodle_url('/admin/tool/policy/accept.php'), 'data-action' => 'acceptmodal']);
+ ['action' => new \moodle_url('/admin/tool/policy/accept.php')]);
echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]);
echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'returnurl',
'value' => $this->get_return_url()]);
public function wrap_html_finish() {
global $PAGE;
if ($this->canagreeany) {
- echo \html_writer::empty_tag('input', ['type' => 'submit',
- 'value' => get_string('consentbulk', 'tool_policy'), 'class' => 'btn btn-primary']);
+ echo \html_writer::empty_tag('input', ['type' => 'submit', 'data-action' => 'acceptmodal',
+ 'value' => get_string('consentbulk', 'tool_policy'), 'class' => 'btn btn-primary m-t-1']);
$PAGE->requires->js_call_amd('tool_policy/acceptmodal', 'getInstance', [\context_system::instance()->id]);
}
echo "</form>\n";
And "Admin User" "link" should exist in the "User One" "table_row"
And "Consent received from a parent" "text" should exist in the "User One" "table_row"
And "Consent not given" "icon" should exist in the "User Two" "table_row"
+
+ @javascript
+ Scenario: Bulk agree on behalf of another users as a manager, multiple policies, javascript on
+ Given I log in as "admin"
+ And I navigate to "Users > Privacy and policies > Manage policies" in site administration
+ And I open the action menu in "This privacy policy" "table_row"
+ And I click on "Set status to \"Active\"" "link" in the "This privacy policy" "table_row"
+ And I press "Activate"
+ And I set the following system permissions of "Manager" role:
+ | capability | permission |
+ | tool/policy:acceptbehalf | Allow |
+ And I log out
+ When I log in as "manager"
+ And I press "Next"
+ And I press "Next"
+ 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 press "Next"
+ And I navigate to "Users > Privacy and policies > User agreements" in site administration
+ And I click on "Select" "checkbox" in the "User One" "table_row"
+ And I press "Consent"
+ And I should see "Give consent on behalf of user(s)"
+ And I should see "One"
+ And I press "Cancel"
+ And I should not see "Give consent on behalf of user(s)"
+ And I click on "Select" "checkbox" in the "User Two" "table_row"
+ And I press "Consent"
+ And I should see "Give consent on behalf of user(s)"
+ And I should see "User One, User Two"
+ When I press "Give consent"
+ Then "Consent given on behalf of user" "icon" should exist in the "User One" "table_row"
+ And I should see "2 of 2" in the "User One" "table_row"
+ And "Consent given on behalf of user" "icon" should exist in the "User Two" "table_row"
+ And I should see "2 of 2" in the "User Two" "table_row"
}
// Allow the renderer to format the title and description.
- $data['title'] = $output->format_title($title);
+ $data['title'] = strip_tags($output->format_title($title));
$data['description'] = $this->showdescription ? $output->format_description($this->description) : null;
return $data;
$currentmemberscount = 0;
if ($currentmembers) {
foreach($currentmembers as $group) {
- $currentmembersoptions .= '<option value="'.$group->id.'.">'.format_string($group->name).'</option>';
+ $currentmembersoptions .= '<option value="' . $group->id . '." title="' . format_string($group->name) . '">' .
+ format_string($group->name) . '</option>';
$currentmemberscount ++;
}
$potentialmemberscount = 0;
if ($potentialmembers) {
foreach($potentialmembers as $group) {
- $potentialmembersoptions .= '<option value="'.$group->id.'.">'.format_string($group->name).'</option>';
+ $potentialmembersoptions .= '<option value="' . $group->id . '." title="' . format_string($group->name) . '">' .
+ format_string($group->name) . '</option>';
$potentialmemberscount ++;
}
} else {
$string['configmaxconsecutiveidentchars'] = 'Passwords must not have more than this number of consecutive identical characters. Use 0 to disable this check.';
$string['configmaxeditingtime'] = 'This specifies the amount of time people have to re-edit forum postings, glossary comments etc. Usually 30 minutes is a good value.';
$string['configmaxevents'] = 'Events to Lookahead';
-$string['configmessaging'] = 'Should the messaging system between site users be enabled?';
+$string['configmessaging'] = 'If enabled, users can send messages to other users on the site.';
$string['configmessagingallowemailoverride'] = 'Allow users to have email message notifications sent to an email address other than the email address in their profile';
-$string['configmessaginghidereadnotifications'] = 'Hide read notifications of events like forum posts when viewing messaging history';
$string['configmessagingdeletereadnotificationsdelay'] = 'Read notifications can be deleted to save space. How long after a notification is read can it be deleted?';
+$string['configmessagingallusers'] = 'If enabled, users can choose to allow anyone on the site to send them a message. Otherwise, users can choose to allow only their contacts or others in their courses to send them messages.';
$string['configminpassworddigits'] = 'Passwords must have at least these many digits.';
$string['configminpasswordlength'] = 'Passwords must be at least these many characters long.';
$string['configminpasswordlower'] = 'Passwords must have at least these many lower case letters.';
$string['mediapluginyoutube'] = 'Enable YouTube links filter';
$string['messaging'] = 'Enable messaging system';
$string['messagingallowemailoverride'] = 'Notification email override';
-$string['messaginghidereadnotifications'] = 'Hide read notifications';
+$string['messagingallusers'] = 'Allow messages from anyone on the site';
$string['messagingdeletereadnotificationsdelay'] = 'Delete read notifications';
$string['minpassworddigits'] = 'Digits';
$string['minpasswordlength'] = 'Password length';
}
if ($oldversion < 2018091200.00) {
- if (!file_exists($CFG->dirroot.'/cache/stores/memcache/settings.php')) {
+ if (!file_exists($CFG->dirroot . '/cache/stores/memcache/settings.php')) {
unset_all_config_for_plugin('cachestore_memcache');
}
upgrade_main_savepoint(true, 2018091200.00);
}
+ if ($oldversion < 2018091400.01) {
+ if (!isset($CFG->messagingallusers)) {
+ // For existing instances, $CFG->messagingallusers would be same value $CFG->messaging has.
+ if (isset($CFG->messaging)) {
+ set_config('messagingallusers', $CFG->messaging);
+ } else {
+ // When $CFG->messaging is not set, default value for $CFG->messaging should be true,
+ // so $CFG->messagingallusers value should be true as well.
+ set_config('messagingallusers', 1);
+ }
+ }
+
+ // Main savepoint reached.
+ upgrade_main_savepoint(true, 2018091400.01);
+ }
+
+ if ($oldversion < 2018091700.01) {
+ // Remove unused setting.
+ unset_config('messaginghidereadnotifications');
+
+ // Main savepoint reached.
+ upgrade_main_savepoint(true, 2018091700.01);
+ }
+
return true;
}
return array();
}
- // Call to standard (parent) getRenameFieldSQL() function
- $results = array_merge($results, parent::getRenameFieldSQL($xmldb_table, $xmldb_field, $newname));
+ // We can't call to standard (parent) getRenameFieldSQL() function since it would enclose the field name
+ // with improper quotes in MSSQL: here, we use a stored procedure to rename the field i.e. a column object;
+ // we need to take care about how this stored procedure expects parameters to be "qualified".
+ $rename = str_replace('TABLENAME', $this->getTableName($xmldb_table), $this->rename_column_sql);
+ // Qualifying the column object could require brackets: use them, regardless the column name not being a reserved word.
+ $rename = str_replace('OLDFIELDNAME', '[' . $xmldb_field->getName() . ']', $rename);
+ // The new field name should be passed as the actual name, w/o any quote.
+ $rename = str_replace('NEWFIELDNAME', $newname, $rename);
+
+ $results[] = $rename;
return $results;
}
$fieldsql = $this->getFieldSQL($xmldb_table, $xmldb_field_clone);
$sql = 'ALTER TABLE ' . $this->getTableName($xmldb_table) . ' CHANGE ' .
- $xmldb_field->getName() . ' ' . $fieldsql;
+ $this->getEncQuoted($xmldb_field->getName()) . ' ' . $fieldsql;
return array($sql);
}
$field = new xmldb_field('type');
$field->set_attributes(XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, 'general', 'course');
+ // 1. Rename the 'type' field into a generic new valid name.
+ // This represents the standard use case.
$dbman->rename_field($table, $field, 'newfieldname');
$columns = $DB->get_columns('test_table0');
$this->assertArrayNotHasKey('type', $columns);
$this->assertArrayHasKey('newfieldname', $columns);
+ $field->setName('newfieldname');
+
+ // 2. Rename the 'newfieldname' field into a reserved word, for testing purposes.
+ // This represents a questionable use case: we should support it but discourage the use of it on peer reviewing.
+ $dbman->rename_field($table, $field, 'where');
+
+ $columns = $DB->get_columns('test_table0');
+
+ $this->assertArrayNotHasKey('newfieldname', $columns);
+ $this->assertArrayHasKey('where', $columns);
+
+ // 3. Create a table with a column name named w/ a reserved word and get rid of it.
+ // This represents a "recovering" use case: a field name could be a reserved word in the future, at least for a DB type.
+ $table = new xmldb_table('test_table_res_word');
+ $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+ $table->add_field('where', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+ $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+ $table->setComment("This is a test'n drop table. You can drop it safely");
+ $dbman->create_table($table);
+ $dbman->table_exists('test_table_res_word');
+
+ $columns = $DB->get_columns('test_table_res_word');
+ $this->assertArrayHasKey('where', $columns);
+ $field = $table->getField('where');
+
+ $dbman->rename_field($table, $field, 'newfieldname');
+
+ $columns = $DB->get_columns('test_table_res_word');
+
+ $this->assertArrayNotHasKey('where', $columns);
+ $this->assertArrayHasKey('newfieldname', $columns);
}
public function testIndexExists() {
* This is a test for sql_generator::getEncQuoted().
*
* @dataProvider test_get_enc_quoted_provider
- * @param string $reserved Whether the column name is reserved or not.
+ * @param bool $reserved Whether the column name is reserved or not.
* @param string $columnname The column name to be quoted, according to the value of $reserved.
**/
public function test_get_enc_quoted($reserved, $columnname) {
}
}
+ /**
+ * Data provider for test_sql_generator_get_rename_field_sql().
+ *
+ * @return array The type-old-new tuple fixture.
+ */
+ public function test_sql_generator_get_rename_field_sql_provider() {
+ return array(
+ // Reserved: an example from SQL-92.
+ // Both names should be reserved.
+ [true, 'from', 'where'],
+ // Not reserved.
+ [false, 'my_old_column_name', 'my_awesome_column_name']
+ );
+ }
+
+ /**
+ * This is a unit test for sql_generator::getRenameFieldSQL().
+ *
+ * @dataProvider test_sql_generator_get_rename_field_sql_provider
+ * @param bool $reserved Whether the column name is reserved or not.
+ * @param string $oldcolumnname The column name to be renamed.
+ * @param string $newcolumnname The new column name.
+ **/
+ public function test_sql_generator_get_rename_field_sql($reserved, $oldcolumnname, $newcolumnname) {
+ $DB = $this->tdb;
+ $gen = $DB->get_manager()->generator;
+ $prefix = $DB->get_prefix();
+
+ $tablename = 'test_get_rename_field_sql';
+ $table = new xmldb_table($tablename);
+ $field = new xmldb_field($oldcolumnname, XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null, null, '0', 'previous');
+
+ $dbfamily = $DB->get_dbfamily();
+ if (!$reserved) {
+ // No need to quote the column name.
+ switch ($dbfamily) {
+ case 'mysql':
+ $this->assertSame(
+ [ "ALTER TABLE {$prefix}$tablename CHANGE $oldcolumnname $newcolumnname BIGINT(11) NOT NULL" ],
+ $gen->getRenameFieldSQL($table, $field, $newcolumnname)
+ );
+ break;
+ case 'sqlite':
+ // Skip it, since the DB is not supported yet.
+ // BTW renaming a column name is already covered by the integration test 'testRenameField'.
+ break;
+ case 'mssql': // The Moodle connection runs under 'QUOTED_IDENTIFIER ON'.
+ $this->assertSame(
+ [ "sp_rename '{$prefix}$tablename.[$oldcolumnname]', '$newcolumnname', 'COLUMN'" ],
+ $gen->getRenameFieldSQL($table, $field, $newcolumnname)
+ );
+ break;
+ case 'oracle':
+ case 'postgres':
+ default:
+ $this->assertSame(
+ [ "ALTER TABLE {$prefix}$tablename RENAME COLUMN $oldcolumnname TO $newcolumnname" ],
+ $gen->getRenameFieldSQL($table, $field, $newcolumnname)
+ );
+ break;
+ }
+ } else {
+ // Column name should be quoted.
+ switch ($dbfamily) {
+ case 'mysql':
+ $this->assertSame(
+ [ "ALTER TABLE {$prefix}$tablename CHANGE `$oldcolumnname` `$newcolumnname` BIGINT(11) NOT NULL" ],
+ $gen->getRenameFieldSQL($table, $field, $newcolumnname)
+ );
+ break;
+ case 'sqlite':
+ // Skip it, since the DB is not supported yet.
+ // BTW renaming a column name is already covered by the integration test 'testRenameField'.
+ break;
+ case 'mssql': // The Moodle connection runs under 'QUOTED_IDENTIFIER ON'.
+ $this->assertSame(
+ [ "sp_rename '{$prefix}$tablename.[$oldcolumnname]', '$newcolumnname', 'COLUMN'" ],
+ $gen->getRenameFieldSQL($table, $field, $newcolumnname)
+ );
+ break;
+ case 'oracle':
+ case 'postgres':
+ default:
+ $this->assertSame(
+ [ "ALTER TABLE {$prefix}$tablename RENAME COLUMN \"$oldcolumnname\" TO \"$newcolumnname\"" ],
+ $gen->getRenameFieldSQL($table, $field, $newcolumnname)
+ );
+ break;
+ }
+ }
+ }
+
// Following methods are not supported == Do not test.
/*
public function testRenameIndex() {
*
* @param calendar_event $event
* @param \core_calendar\action_factory $factory
+ * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default).
* @return \core_calendar\local\event\entities\action_interface|null
*/
function mod_book_core_calendar_provide_event_action(calendar_event $event,
- \core_calendar\action_factory $factory) {
- $cm = get_fast_modinfo($event->courseid)->instances['book'][$event->instance];
+ \core_calendar\action_factory $factory,
+ int $userid = 0) {
+ global $USER;
+
+ if (empty($userid)) {
+ $userid = $USER->id;
+ }
+
+ $cm = get_fast_modinfo($event->courseid, $userid)->instances['book'][$event->instance];
+
+ if (!$cm->uservisible) {
+ // The module is not visible to the user for any reason.
+ return null;
+ }
+
$context = context_module::instance($cm->id);
- if (!has_capability('mod/book:read', $context)) {
+ if (!has_capability('mod/book:read', $context, $userid)) {
return null;
}
$completion = new \completion_info($cm->get_course());
- $completiondata = $completion->get_data($cm, false);
+ $completiondata = $completion->get_data($cm, false, $userid);
if ($completiondata->completionstate != COMPLETION_INCOMPLETE) {
return null;
$this->assertTrue($actionevent->is_actionable());
}
+ public function test_book_core_calendar_provide_event_action_in_hidden_section() {
+ // Create the activity.
+ $course = $this->getDataGenerator()->create_course();
+ $book = $this->getDataGenerator()->create_module('book', array('course' => $course->id));
+
+ // Enrol a student in the course.
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+ // Create a calendar event.
+ $event = $this->create_action_event($course->id, $book->id,
+ \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+ // Set sections 0 as hidden.
+ set_section_visible($course->id, 0, 0);
+
+ // Now, log out.
+ $this->setUser();
+
+ // Create an action factory.
+ $factory = new \core_calendar\action_factory();
+
+ // Decorate action event for the student.
+ $actionevent = mod_book_core_calendar_provide_event_action($event, $factory, $student->id);
+
+ // Confirm the event is not shown at all.
+ $this->assertNull($actionevent);
+ }
+
+ public function test_book_core_calendar_provide_event_action_for_user() {
+ // Create the activity.
+ $course = $this->getDataGenerator()->create_course();
+ $book = $this->getDataGenerator()->create_module('book', array('course' => $course->id));
+
+ // Enrol a student in the course.
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+ // Create a calendar event.
+ $event = $this->create_action_event($course->id, $book->id,
+ \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+ // Now, log out.
+ $this->setUser();
+
+ // Create an action factory.
+ $factory = new \core_calendar\action_factory();
+
+ // Decorate action event for the student.
+ $actionevent = mod_book_core_calendar_provide_event_action($event, $factory, $student->id);
+
+ // Confirm the event was decorated.
+ $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
+ $this->assertEquals(get_string('view'), $actionevent->get_name());
+ $this->assertInstanceOf('moodle_url', $actionevent->get_url());
+ $this->assertEquals(1, $actionevent->get_item_count());
+ $this->assertTrue($actionevent->is_actionable());
+ }
+
public function test_book_core_calendar_provide_event_action_as_non_user() {
global $CFG;
$this->assertNull($actionevent);
}
+ public function test_book_core_calendar_provide_event_action_already_completed_for_user() {
+ global $CFG;
+
+ $CFG->enablecompletion = 1;
+
+ // Create the activity.
+ $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
+ $book = $this->getDataGenerator()->create_module('book', array('course' => $course->id),
+ array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS));
+
+ // Enrol a student in the course.
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+ // Get some additional data.
+ $cm = get_coursemodule_from_instance('book', $book->id);
+
+ // Create a calendar event.
+ $event = $this->create_action_event($course->id, $book->id,
+ \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+ // Mark the activity as completed for the student.
+ $completion = new completion_info($course);
+ $completion->set_module_viewed($cm, $student->id);
+
+ // Create an action factory.
+ $factory = new \core_calendar\action_factory();
+
+ // Decorate action event for the student.
+ $actionevent = mod_book_core_calendar_provide_event_action($event, $factory, $student->id);
+
+ // Ensure result was null.
+ $this->assertNull($actionevent);
+ }
+
/**
* Creates an action event.
*
$worksheet1->write_string($rowoffset1, 0, userdate(time()), $xlsformats->head1);
// Get the completeds.
-$completedscount = feedback_get_completeds_group_count($feedback, $mygroupid, $courseid);
-if ($completedscount > 0) {
- // Write the count of completeds.
- $rowoffset1++;
- $worksheet1->write_string($rowoffset1,
- 0,
- $cm->get_module_type_name(true).': '.strval($completedscount),
- $xlsformats->head1);
-}
+$completedscount = $feedbackstructure->count_completed_responses($mygroupid);
+// Write the count of completeds.
+// Keep consistency and write count of completeds even when they are 0.
+$rowoffset1++;
+$worksheet1->write_string($rowoffset1,
+ 0,
+ get_string('completed_feedbacks', 'feedback').': '.strval($completedscount),
+ $xlsformats->head1);
$rowoffset1++;
$worksheet1->write_string($rowoffset1,
*
* @param calendar_event $event
* @param \core_calendar\action_factory $factory
+ * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default).
* @return \core_calendar\local\event\entities\action_interface|null
*/
function mod_folder_core_calendar_provide_event_action(calendar_event $event,
- \core_calendar\action_factory $factory) {
- $cm = get_fast_modinfo($event->courseid)->instances['folder'][$event->instance];
+ \core_calendar\action_factory $factory,
+ int $userid = 0) {
+ global $USER;
+
+ if (!$userid) {
+ $userid = $USER->id;
+ }
+
+ $cm = get_fast_modinfo($event->courseid, $userid)->instances['folder'][$event->instance];
+
+ if (!$cm->uservisible) {
+ // The module is not visible to the user for any reason.
+ return null;
+ }
$completion = new \completion_info($cm->get_course());
- $completiondata = $completion->get_data($cm, false);
+ $completiondata = $completion->get_data($cm, false, $userid);
if ($completiondata->completionstate != COMPLETION_INCOMPLETE) {
return null;
$this->assertTrue($actionevent->is_actionable());
}
+ public function test_folder_core_calendar_provide_event_action_for_non_user() {
+ global $CFG;
+
+ // Create a course.
+ $course = $this->getDataGenerator()->create_course();
+
+ // Create the activity.
+ $folder = $this->getDataGenerator()->create_module('folder', array('course' => $course->id));
+
+ // Create a calendar event.
+ $event = $this->create_action_event($course->id, $folder->id,
+ \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+ // Now, log out.
+ $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
+ $this->setUser();
+
+ // Create an action factory.
+ $factory = new \core_calendar\action_factory();
+
+ // Decorate action event.
+ $actionevent = mod_folder_core_calendar_provide_event_action($event, $factory);
+
+ // Confirm the event is not shown at all.
+ $this->assertNull($actionevent);
+ }
+
+ public function test_folder_core_calendar_provide_event_action_in_hidden_section() {
+ // Create a course.
+ $course = $this->getDataGenerator()->create_course();
+
+ // Create a student.
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+ // Create the activity.
+ $folder = $this->getDataGenerator()->create_module('folder', array('course' => $course->id));
+
+ // Create a calendar event.
+ $event = $this->create_action_event($course->id, $folder->id,
+ \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+ // Set sections 0 as hidden.
+ set_section_visible($course->id, 0, 0);
+
+ // Create an action factory.
+ $factory = new \core_calendar\action_factory();
+
+ // Decorate action event.
+ $actionevent = mod_folder_core_calendar_provide_event_action($event, $factory, $student->id);
+
+ // Confirm the event is not shown at all.
+ $this->assertNull($actionevent);
+ }
+
+ public function test_folder_core_calendar_provide_event_action_for_user() {
+ // Create a course.
+ $course = $this->getDataGenerator()->create_course();
+
+ // Create a student.
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+ // Create the activity.
+ $folder = $this->getDataGenerator()->create_module('folder', array('course' => $course->id));
+
+ // Create a calendar event.
+ $event = $this->create_action_event($course->id, $folder->id,
+ \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+ // Now, log out.
+ $this->setUser();
+
+ // Create an action factory.
+ $factory = new \core_calendar\action_factory();
+
+ // Decorate action event for the student.
+ $actionevent = mod_folder_core_calendar_provide_event_action($event, $factory, $student->id);
+
+ // Confirm the event was decorated.
+ $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
+ $this->assertEquals(get_string('view'), $actionevent->get_name());
+ $this->assertInstanceOf('moodle_url', $actionevent->get_url());
+ $this->assertEquals(1, $actionevent->get_item_count());
+ $this->assertTrue($actionevent->is_actionable());
+ }
+
public function test_folder_core_calendar_provide_event_action_already_completed() {
global $CFG;
$this->assertNull($actionevent);
}
+ public function test_folder_core_calendar_provide_event_action_already_completed_for_user() {
+ global $CFG;
+
+ $CFG->enablecompletion = 1;
+
+ // Create a course.
+ $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
+
+ // Create a student.
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+ // Create the activity.
+ $folder = $this->getDataGenerator()->create_module('folder', array('course' => $course->id),
+ array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS));
+
+ // Get some additional data.
+ $cm = get_coursemodule_from_instance('folder', $folder->id);
+
+ // Create a calendar event.
+ $event = $this->create_action_event($course->id, $folder->id,
+ \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+ // Mark the activity as completed for the student.
+ $completion = new completion_info($course);
+ $completion->set_module_viewed($cm, $student->id);
+
+ // Now, log out.
+ $this->setUser();
+
+ // Create an action factory.
+ $factory = new \core_calendar\action_factory();
+
+ // Decorate action event for the student.
+ $actionevent = mod_folder_core_calendar_provide_event_action($event, $factory, $student->id);
+
+ // Ensure result was null.
+ $this->assertNull($actionevent);
+ }
+
/**
* Creates an action event.
*
*
* @param calendar_event $event
* @param \core_calendar\action_factory $factory
+ * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default).
* @return \core_calendar\local\event\entities\action_interface|null
*/
function mod_forum_core_calendar_provide_event_action(calendar_event $event,
- \core_calendar\action_factory $factory) {
+ \core_calendar\action_factory $factory,
+ int $userid = 0) {
global $DB, $USER;
- $cm = get_fast_modinfo($event->courseid)->instances['forum'][$event->instance];
+ if (!$userid) {
+ $userid = $USER->id;
+ }
+
+ $cm = get_fast_modinfo($event->courseid, $userid)->instances['forum'][$event->instance];
+
+ if (!$cm->uservisible) {
+ // The module is not visible to the user for any reason.
+ return null;
+ }
+
$context = context_module::instance($cm->id);
- if (!has_capability('mod/forum:viewdiscussion', $context)) {
+ if (!has_capability('mod/forum:viewdiscussion', $context, $userid)) {
return null;
}
$completion = new \completion_info($cm->get_course());
- $completiondata = $completion->get_data($cm, false);
+ $completiondata = $completion->get_data($cm, false, $userid);
if ($completiondata->completionstate != COMPLETION_INCOMPLETE) {
return null;
INNER JOIN {forum_discussions} fd ON fp.discussion=fd.id
WHERE
fp.userid=:userid AND fd.forum=:forumid";
- $postcountparams = array('userid' => $USER->id, 'forumid' => $forum->id);
+ $postcountparams = array('userid' => $userid, 'forumid' => $forum->id);
if ($forum->completiondiscussions) {
- $count = $DB->count_records('forum_discussions', array('forum' => $forum->id, 'userid' => $USER->id));
+ $count = $DB->count_records('forum_discussions', array('forum' => $forum->id, 'userid' => $userid));
$itemcount += ($forum->completiondiscussions >= $count) ? ($forum->completiondiscussions - $count) : 0;
}
$this->assertTrue($actionevent->is_actionable());
}
+ public function test_forum_core_calendar_provide_event_action_in_hidden_section() {
+ global $CFG;
+
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ // Create a course.
+ $course = $this->getDataGenerator()->create_course();
+
+ // Create a student.
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+ // Create the activity.
+ $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id,
+ 'completionreplies' => 5, 'completiondiscussions' => 2));
+
+ // Create a calendar event.
+ $event = $this->create_action_event($course->id, $forum->id,
+ \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+ // Set sections 0 as hidden.
+ set_section_visible($course->id, 0, 0);
+
+ // Now, log out.
+ $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
+ $this->setUser();
+
+ // Create an action factory.
+ $factory = new \core_calendar\action_factory();
+
+ // Decorate action event for the student.
+ $actionevent = mod_forum_core_calendar_provide_event_action($event, $factory, $student->id);
+
+ // Confirm the event is not shown at all.
+ $this->assertNull($actionevent);
+ }
+
+ public function test_forum_core_calendar_provide_event_action_for_user() {
+ global $CFG;
+
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ // Create a course.
+ $course = $this->getDataGenerator()->create_course();
+
+ // Create a student.
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+ // Create the activity.
+ $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id,
+ 'completionreplies' => 5, 'completiondiscussions' => 2));
+
+ // Create a calendar event.
+ $event = $this->create_action_event($course->id, $forum->id,
+ \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+ // Now log out.
+ $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
+ $this->setUser();
+
+ // Create an action factory.
+ $factory = new \core_calendar\action_factory();
+
+ // Decorate action event for the student.
+ $actionevent = mod_forum_core_calendar_provide_event_action($event, $factory, $student->id);
+
+ // Confirm the event was decorated.
+ $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
+ $this->assertEquals(get_string('view'), $actionevent->get_name());
+ $this->assertInstanceOf('moodle_url', $actionevent->get_url());
+ $this->assertEquals(7, $actionevent->get_item_count());
+ $this->assertTrue($actionevent->is_actionable());
+ }
+
public function test_forum_core_calendar_provide_event_action_as_non_user() {
global $CFG;
$this->assertNull($actionevent);
}
+ public function test_forum_core_calendar_provide_event_action_already_completed_for_user() {
+ global $CFG;
+
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ $CFG->enablecompletion = 1;
+
+ // Create a course.
+ $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
+
+ // Create a student.
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+ // Create the activity.
+ $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
+ array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS));
+
+ // Get some additional data.
+ $cm = get_coursemodule_from_instance('forum', $forum->id);
+
+ // Create a calendar event.
+ $event = $this->create_action_event($course->id, $forum->id,
+ \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+ // Mark the activity as completed for the student.
+ $completion = new completion_info($course);
+ $completion->set_module_viewed($cm, $student->id);
+
+ // Create an action factory.
+ $factory = new \core_calendar\action_factory();
+
+ // Decorate action event.
+ $actionevent = mod_forum_core_calendar_provide_event_action($event, $factory, $student->id);
+
+ // Ensure result was null.
+ $this->assertNull($actionevent);
+ }
+
public function test_mod_forum_get_tagged_posts() {
global $DB;
// Get the list of questions for the category
list($usql, $params) = $DB->get_in_or_equal($categorylist);
- $questions = $DB->get_records_select('question', "category {$usql} {$npsql}", $params, 'qtype, name');
+ $questions = $DB->get_records_select('question', "category {$usql} {$npsql}", $params, 'category, qtype, name');
// Iterate through questions, getting stuff we need
$qresults = array();
protected function check_qt_var_name_restrictions($expected) {
global $CFG;
- if ($CFG->debugdeveloper) {
+ if ($CFG->debugdeveloper && $expected !== self::USE_RAW_DATA) {
foreach ($expected as $key => $value) {
if (strpos($key, 'bf_') !== false) {
debugging('The bf_ prefix is reserved and cannot be used by question types', DEBUG_DEVELOPER);
public $translator = null;
public $canaccessbackupdata = true;
protected $importcontext = null;
+ /** @var bool $displayprogress Whether to display progress. */
+ public $displayprogress = true;
// functions to indicate import/export functionality
// override to return true if implemented
$this->canaccessbackupdata = $canaccess;
}
+ /**
+ * Change whether to display progress messages.
+ * There is normally no need to use this function as the
+ * default for $displayprogress is true.
+ * Set to false for unit tests.
+ * @param bool $displayprogress
+ */
+ public function set_display_progress($displayprogress) {
+ $this->displayprogress = $displayprogress;
+ }
+
/***********************
* IMPORTING FUNCTIONS
***********************/
raise_memory_limit(MEMORY_EXTRA);
// STAGE 1: Parse the file
- echo $OUTPUT->notification(get_string('parsingquestions', 'question'), 'notifysuccess');
+ if ($this->displayprogress) {
+ echo $OUTPUT->notification(get_string('parsingquestions', 'question'), 'notifysuccess');
+ }
if (! $lines = $this->readdata($this->filename)) {
echo $OUTPUT->notification(get_string('cannotread', 'question'));
}
// STAGE 2: Write data to database
- echo $OUTPUT->notification(get_string('importingquestions', 'question',
- $this->count_questions($questions)), 'notifysuccess');
+ if ($this->displayprogress) {
+ echo $OUTPUT->notification(get_string('importingquestions', 'question',
+ $this->count_questions($questions)), 'notifysuccess');
+ }
// check for errors before we continue
if ($this->stoponerror and ($this->importerrors>0)) {
if ($this->catfromfile) {
// find/create category object
$catpath = $question->category;
- $newcategory = $this->create_category_path($catpath);
+ $newcategory = $this->create_category_path($catpath, $question);
if (!empty($newcategory)) {
$this->category = $newcategory;
}
$count++;
- echo "<hr /><p><b>{$count}</b>. ".$this->format_question_text($question)."</p>";
+ if ($this->displayprogress) {
+ echo "<hr /><p><b>{$count}</b>. " . $this->format_question_text($question) . "</p>";
+ }
$question->category = $this->category->id;
$question->stamp = make_unique_id_code(); // Set the unique code (not to be changed)
* but if $getcontext is set then ignore the context and use selected category context.
*
* @param string catpath delimited category path
- * @param int courseid course to search for categories
+ * @param object $lastcategoryinfo Contains category information
* @return mixed category object or null if fails
*/
- protected function create_category_path($catpath) {
+ protected function create_category_path($catpath, $lastcategoryinfo = null) {
global $DB;
$catnames = $this->split_category_path($catpath);
$parent = 0;
$this->importcontext = $context;
// Now create any categories that need to be created.
- foreach ($catnames as $catname) {
+ foreach ($catnames as $key => $catname) {
if ($parent == 0) {
$category = question_get_top_category($context->id, true);
$parent = $category->id;
} else if ($category = $DB->get_record('question_categories',
array('name' => $catname, 'contextid' => $context->id, 'parent' => $parent))) {
+ // Do nothing unless the child category appears before the parent category
+ // in the imported xml file. Because the parent was created without info being available
+ // at that time, this allows the info to be added from the xml data.
+ if ($key == (count($catnames) - 1) && $lastcategoryinfo && $lastcategoryinfo->info !== null &&
+ $lastcategoryinfo->info !== "" && $category->info == "") {
+ $category->info = $lastcategoryinfo->info;
+ if ($lastcategoryinfo->infoformat !== null && $lastcategoryinfo->infoformat !== "") {
+ $category->infoformat = $lastcategoryinfo->infoformat;
+ }
+ $DB->update_record('question_categories', $category);
+ }
$parent = $category->id;
} else {
if ($catname == 'top') {
// Should not happen, but if it does just move on.
// Occurs when there has been some import/export that has created
// multiple nested 'top' categories (due to old bug solved by MDL-63165).
- // Not throwing an error here helps clean up old errors (silently).
+ // This basically silently cleans up old errors. Not throwing an exception here.
continue;
}
require_capability('moodle/question:managecategory', $context);
- // create the new category
+ // Create the new category. This will create all the categories in the catpath,
+ // though only the final category will have any info added if available.
$category = new stdClass();
$category->contextid = $context->id;
$category->name = $catname;
$category->info = '';
+ // Only add info (category description) for the final category in the catpath.
+ if ($key == (count($catnames) - 1) && $lastcategoryinfo && $lastcategoryinfo->info !== null &&
+ $lastcategoryinfo->info !== "") {
+ $category->info = $lastcategoryinfo->info;
+ if ($lastcategoryinfo->infoformat !== null && $lastcategoryinfo->infoformat !== "") {
+ $category->infoformat = $lastcategoryinfo->infoformat;
+ }
+ }
$category->parent = $parent;
$category->sortorder = 999;
$category->stamp = make_unique_id_code();
// Array of categories written to file.
$writtencategories = [];
- foreach ($parents as $parent) {
- $categoryname = $this->get_category_path($parent, $this->contexttofile);
- // Create 'dummy' question for category export.
- $dummyquestion = $this->create_dummy_question_representing_category($categoryname);
- $expout .= $this->writequestion($dummyquestion) . "\n";
- $writtencategories[] = $parent;
- }
-
foreach ($questions as $question) {
// used by file api
$contextid = $DB->get_field('question_categories', 'contextid',
if ($question->category != $trackcategory) {
$addnewcat = true;
$trackcategory = $question->category;
- $categoryname = $this->get_category_path($trackcategory, $this->contexttofile);
}
$trackcategoryparents = question_categorylist_parents($trackcategory);
// Check if we need to record empty parents categories.
// If parent is empty.
if (!count($DB->get_records('question', array('category' => $trackcategoryparent)))) {
$categoryname = $this->get_category_path($trackcategoryparent, $this->contexttofile);
- // Create 'dummy' question for parent category.
- $dummyquestion = $this->create_dummy_question_representing_category($categoryname);
- $expout .= $this->writequestion($dummyquestion) . "\n";
- $writtencategories[] = $trackcategoryparent;
+ $categoryinfo = $DB->get_record('question_categories', array('id' => $trackcategoryparent),
+ 'name, info, infoformat', MUST_EXIST);
+ if ($categoryinfo->name != 'top') {
+ // Create 'dummy' question for parent category.
+ $dummyquestion = $this->create_dummy_question_representing_category($categoryname, $categoryinfo);
+ $expout .= $this->writequestion($dummyquestion) . "\n";
+ $writtencategories[] = $trackcategoryparent;
+ }
}
}
}
if ($addnewcat && !in_array($trackcategory, $writtencategories)) {
$categoryname = $this->get_category_path($trackcategory, $this->contexttofile);
+ $categoryinfo = $DB->get_record('question_categories', array('id' => $trackcategory),
+ 'info, infoformat', MUST_EXIST);
// Create 'dummy' question for category.
- $dummyquestion = $this->create_dummy_question_representing_category($categoryname);
+ $dummyquestion = $this->create_dummy_question_representing_category($categoryname, $categoryinfo);
$expout .= $this->writequestion($dummyquestion) . "\n";
$writtencategories[] = $trackcategory;
}
/**
* Create 'dummy' question for category export.
* @param string $categoryname the name of the category
+ * @param object $categoryinfo description of the category
* @return stdClass 'dummy' question for category
*/
- protected function create_dummy_question_representing_category(string $categoryname) {
+ protected function create_dummy_question_representing_category(string $categoryname, $categoryinfo) {
$dummyquestion = new stdClass();
$dummyquestion->qtype = 'category';
$dummyquestion->category = $categoryname;
$dummyquestion->id = 0;
$dummyquestion->questiontextformat = '';
$dummyquestion->contextid = 0;
+ $dummyquestion->info = $categoryinfo->info;
+ $dummyquestion->infoformat = $categoryinfo->infoformat;
$dummyquestion->name = 'Switch category to ' . $categoryname;
return $dummyquestion;
}
And I follow "Export"
And I set the field "id_format_gift" to "1"
And I press "Export questions to file"
- And following "click here" should download between "1650" and "1800" bytes
+ And following "click here" should download between "1600" and "1800" bytes
This files describes API changes for question import/export format plugins.
+=== 3.6 ===
+
+* Saving question category descriptions (info) is now supported in Moodle XML import/export format.
+New xml-structure snippet for a question category:
+<question type="category">
+ <category>
+ <text>${$contexttypename}$/{$category_path}</text>
+ </category>
+ <info format="{$format}">
+ <text>{$info_categorydescription}</text>
+ </info>
+</question>
+* The method importprocess() in question/format.php no longer accepts $category as a parameter.
+If required in a plugin then please override this method.
+
=== 2.3 ===
* This plugin type now supports cron in the standard way. If required, Create a
/**
* Translate human readable format name
* into internal Moodle code number
+ * Note the reverse function is called get_format.
* @param string name format name from xml file
* @return int Moodle format code
*/
* import category. The format is:
* <question type="category">
* <category>tom/dick/harry</category>
+ * <info format="moodle_auto_format"><text>Category description</text></info>
* </question>
*/
protected function import_category($question) {
$qo = new stdClass();
$qo->qtype = 'category';
$qo->category = $this->import_text($question['#']['category'][0]['#']['text']);
+ $qo->info = '';
+ $qo->infoformat = FORMAT_MOODLE;
+ if (array_key_exists('info', $question['#'])) {
+ $qo->info = $this->import_text($question['#']['info'][0]['#']['text']);
+ // The import should have the format in human readable form, so translate to machine readable format.
+ $qo->infoformat = $this->trans_format($question['#']['info'][0]['@']['format']);
+ }
return $qo;
}
// Categories are a special case.
if ($question->qtype == 'category') {
$categorypath = $this->writetext($question->category);
+ $categoryinfo = $this->writetext($question->info);
+ $infoformat = $this->format($question->infoformat);
$expout .= " <question type=\"category\">\n";
$expout .= " <category>\n";
- $expout .= " {$categorypath}\n";
+ $expout .= " {$categorypath}";
$expout .= " </category>\n";
+ $expout .= " <info {$infoformat}>\n";
+ $expout .= " {$categoryinfo}";
+ $expout .= " </info>\n";
$expout .= " </question>\n";
return $expout;
}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<quiz>
+<!-- question: 0 -->
+ <question type="category">
+ <category>
+ <text>$course$/Sigma/Tau</text>
+ </category>
+ <info format="html">
+ <text>This is Tau category for test</text>
+ </info>
+ </question>
+
+<!-- question: 106 -->
+ <question type="essay">
+ <name>
+ <text>Tau Question</text>
+ </name>
+ <questiontext format="moodle_auto_format">
+ <text>Testing Tau Question</text>
+ </questiontext>
+ <generalfeedback format="moodle_auto_format">
+ <text></text>
+ </generalfeedback>
+ <defaultgrade>1.0000000</defaultgrade>
+ <penalty>0.0000000</penalty>
+ <hidden>0</hidden>
+ <responseformat>editor</responseformat>
+ <responserequired>1</responserequired>
+ <responsefieldlines>15</responsefieldlines>
+ <attachments>0</attachments>
+ <attachmentsrequired>0</attachmentsrequired>
+ <graderinfo format="html">
+ <text></text>
+ </graderinfo>
+ <responsetemplate format="html">
+ <text></text>
+ </responsetemplate>
+ </question>
+
+<!-- question: 0 -->
+ <question type="category">
+ <category>
+ <text>$course$/Sigma</text>
+ </category>
+ <info format="html">
+ <text>This is Sigma category for test</text>
+ </info>
+ </question>
+
+<!-- question: 105 -->
+ <question type="shortanswer">
+ <name>
+ <text>Sigma Question</text>
+ </name>
+ <questiontext format="moodle_auto_format">
+ <text>Testing Sigma Question</text>
+ </questiontext>
+ <generalfeedback format="moodle_auto_format">
+ <text></text>
+ </generalfeedback>
+ <defaultgrade>1.0000000</defaultgrade>
+ <penalty>0.3333333</penalty>
+ <hidden>0</hidden>
+ <usecase>0</usecase>
+ <answer fraction="100" format="moodle_auto_format">
+ <text>yes</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ <answer fraction="0" format="moodle_auto_format">
+ <text>no</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ <answer fraction="0" format="moodle_auto_format">
+ <text>may be</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ </question>
+</quiz>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<quiz>
+<!-- question: 0 -->
+ <question type="category">
+ <category>
+ <text>$course$/Alpha</text>
+ </category>
+ <info format="moodle_auto_format">
+ <text>This is Alpha category for test</text>
+ </info>
+ </question>
+
+<!-- question: 91 -->
+ <question type="truefalse">
+ <name>
+ <text>Alpha Question</text>
+ </name>
+ <questiontext format="html">
+ <text><![CDATA[<p>Testing Alpha Question</p>]]></text>
+ </questiontext>
+ <generalfeedback format="html">
+ <text></text>
+ </generalfeedback>
+ <defaultgrade>1.0000000</defaultgrade>
+ <penalty>1.0000000</penalty>
+ <hidden>0</hidden>
+ <answer fraction="100" format="moodle_auto_format">
+ <text>true</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ <answer fraction="0" format="moodle_auto_format">
+ <text>false</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ </question>
+</quiz>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<quiz>
+<!-- question: 0 -->
+ <question type="category">
+ <category>
+ <text>$course$/Alpha</text>
+ </category>
+
+ <info format="moodle_auto_format">
+ <text>This is Alpha category for test</text>
+ </info>
+ </question>
+
+<!-- question: 91 -->
+ <question type="truefalse">
+ <name>
+ <text>Alpha Question</text>
+ </name>
+ <questiontext format="html">
+ <text><![CDATA[<p>Testing Alpha Question</p>]]></text>
+ </questiontext>
+ <generalfeedback format="html">
+ <text></text>
+ </generalfeedback>
+ <defaultgrade>1.0000000</defaultgrade>
+ <penalty>1.0000000</penalty>
+ <hidden>0</hidden>
+ <answer fraction="100" format="moodle_auto_format">
+ <text>true</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ <answer fraction="0" format="moodle_auto_format">
+ <text>false</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ </question>
+</quiz>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<quiz>
+<!-- question: 0 -->
+ <question type="category">
+ <category>
+ <text>$course$/Delta</text>
+ </category>
+ <info format="plain_text">
+ <text>This is Delta category for test</text>
+ </info>
+ </question>
+
+<!-- question: 0 -->
+ <question type="category">
+ <category>
+ <text>$course$/Delta/Epsilon</text>
+ </category>
+ <info format="markdown">
+ <text>This is Epsilon category for test</text>
+ </info>
+ </question>
+
+<!-- question: 0 -->
+ <question type="category">
+ <category>
+ <text>$course$/Delta/Epsilon/Zeta</text>
+ </category>
+ <info format="moodle_auto_format">
+ <text>This is Zeta category for test</text>
+ </info>
+ </question>
+
+<!-- question: 93 -->
+ <question type="truefalse">
+ <name>
+ <text>Zeta Question</text>
+ </name>
+ <questiontext format="html">
+ <text><![CDATA[<p>Testing Zeta Question</p>]]></text>
+ </questiontext>
+ <generalfeedback format="html">
+ <text></text>
+ </generalfeedback>
+ <defaultgrade>1.0000000</defaultgrade>
+ <penalty>1.0000000</penalty>
+ <hidden>0</hidden>
+ <answer fraction="100" format="moodle_auto_format">
+ <text>true</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ <answer fraction="0" format="moodle_auto_format">
+ <text>false</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ </question>
+</quiz>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<quiz>
+<!-- question: 0 -->
+ <question type="category">
+ <category>
+ <text>$course$/Iota</text>
+ </category>
+ <info format="plain_text">
+ <text>This is Iota category for test</text>
+ </info>
+ </question>
+
+<!-- question: 96 -->
+ <question type="truefalse">
+ <name>
+ <text>Iota Question</text>
+ </name>
+ <questiontext format="html">
+ <text><![CDATA[<p>Testing Iota Question</p>]]></text>
+ </questiontext>
+ <generalfeedback format="html">
+ <text></text>
+ </generalfeedback>
+ <defaultgrade>1.0000000</defaultgrade>
+ <penalty>1.0000000</penalty>
+ <hidden>0</hidden>
+ <answer fraction="100" format="moodle_auto_format">
+ <text>true</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ <answer fraction="0" format="moodle_auto_format">
+ <text>false</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ </question>
+
+<!-- question: 0 -->
+ <question type="category">
+ <category>
+ <text>$course$/Iota/Kappa</text>
+ </category>
+ <info format="markdown">
+ <text>This is Kappa category for test</text>
+ </info>
+ </question>
+
+ <!-- question: 106 -->
+ <question type="essay">
+ <name>
+ <text>Kappa Essay Question</text>
+ </name>
+ <questiontext format="moodle_auto_format">
+ <text>Testing Kappa Essay Question</text>
+ </questiontext>
+ <generalfeedback format="moodle_auto_format">
+ <text></text>
+ </generalfeedback>
+ <defaultgrade>1.0000000</defaultgrade>
+ <penalty>0.0000000</penalty>
+ <hidden>0</hidden>
+ <responseformat>editor</responseformat>
+ <responserequired>1</responserequired>
+ <responsefieldlines>10</responsefieldlines>
+ <attachments>0</attachments>
+ <attachmentsrequired>0</attachmentsrequired>
+ <graderinfo format="html">
+ <text></text>
+ </graderinfo>
+ <responsetemplate format="html">
+ <text></text>
+ </responsetemplate>
+ </question>
+
+<!-- question: 97 -->
+ <question type="truefalse">
+ <name>
+ <text>Kappa Question</text>
+ </name>
+ <questiontext format="html">
+ <text><![CDATA[<p>Testing Kappa Question</p>]]></text>
+ </questiontext>
+ <generalfeedback format="html">
+ <text></text>
+ </generalfeedback>
+ <defaultgrade>1.0000000</defaultgrade>
+ <penalty>1.0000000</penalty>
+ <hidden>0</hidden>
+ <answer fraction="100" format="moodle_auto_format">
+ <text>true</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ <answer fraction="0" format="moodle_auto_format">
+ <text>false</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ </question>
+
+<!-- question: 0 -->
+ <question type="category">
+ <category>
+ <text>$course$/Iota/Kappa/Lambda</text>
+ </category>
+ <info format="moodle_auto_format">
+ <text>This is Lambda category for test</text>
+ </info>
+ </question>
+
+<!-- question: 98 -->
+ <question type="truefalse">
+ <name>
+ <text>Lambda Question</text>
+ </name>
+ <questiontext format="html">
+ <text><![CDATA[<p>Testing Lambda Question</p>]]></text>
+ </questiontext>
+ <generalfeedback format="html">
+ <text></text>
+ </generalfeedback>
+ <defaultgrade>1.0000000</defaultgrade>
+ <penalty>1.0000000</penalty>
+ <hidden>0</hidden>
+ <answer fraction="100" format="moodle_auto_format">
+ <text>true</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ <answer fraction="0" format="moodle_auto_format">
+ <text>false</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ </question>
+
+ <!-- question: 0 -->
+ <question type="category">
+ <category>
+ <text>$course$/Iota/Mu</text>
+ </category>
+ <info format="moodle_auto_format">
+ <text>This is Mu category for test</text>
+ </info>
+ </question>
+
+ <!-- question: 99 -->
+ <question type="truefalse">
+ <name>
+ <text>Mu Question</text>
+ </name>
+ <questiontext format="html">
+ <text><![CDATA[<p>Testing Mu Question</p>]]></text>
+ </questiontext>
+ <generalfeedback format="html">
+ <text></text>
+ </generalfeedback>
+ <defaultgrade>1.0000000</defaultgrade>
+ <penalty>1.0000000</penalty>
+ <hidden>0</hidden>
+ <answer fraction="100" format="moodle_auto_format">
+ <text>true</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ <answer fraction="0" format="moodle_auto_format">
+ <text>false</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ </question>
+</quiz>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<quiz>
+<!-- question: 0 -->
+ <question type="category">
+ <category>
+ <text>$course$/Pi</text>
+ </category>
+ </question>
+
+<!-- question: 103 -->
+ <question type="truefalse">
+ <name>
+ <text>Pi Question</text>
+ </name>
+ <questiontext format="html">
+ <text><![CDATA[<p>Testing Pi Question</p>]]></text>
+ </questiontext>
+ <generalfeedback format="html">
+ <text></text>
+ </generalfeedback>
+ <defaultgrade>1.0000000</defaultgrade>
+ <penalty>1.0000000</penalty>
+ <hidden>0</hidden>
+ <answer fraction="100" format="moodle_auto_format">
+ <text>true</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ <answer fraction="0" format="moodle_auto_format">
+ <text>false</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ </question>
+
+<!-- question: 0 -->
+ <question type="category">
+ <category>
+ <text>$course$/Pi/Rho</text>
+ </category>
+ </question>
+
+<!-- question: 104 -->
+ <question type="truefalse">
+ <name>
+ <text>Rho Question</text>
+ </name>
+ <questiontext format="html">
+ <text><![CDATA[<p>Testing Rho Question</p>]]></text>
+ </questiontext>
+ <generalfeedback format="html">
+ <text></text>
+ </generalfeedback>
+ <defaultgrade>1.0000000</defaultgrade>
+ <penalty>1.0000000</penalty>
+ <hidden>0</hidden>
+ <answer fraction="100" format="moodle_auto_format">
+ <text>true</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ <answer fraction="0" format="moodle_auto_format">
+ <text>false</text>
+ <feedback format="html">
+ <text></text>
+ </feedback>
+ </answer>
+ </question>
+</quiz>
--- /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/>.
+/**
+ * Unit tests for export/import description (info) for question category in the Moodle XML format.
+ *
+ * @package qformat_xml
+ * @copyright 2014 Nikita Nikitsky, Volgograd State Technical University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+global $CFG;
+require_once($CFG->libdir . '/questionlib.php');
+require_once($CFG->dirroot . '/question/format/xml/format.php');
+require_once($CFG->dirroot . '/question/format.php');
+require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
+require_once($CFG->dirroot . '/question/editlib.php');
+
+/**
+ * Unit tests for the XML question format import and export.
+ *
+ * @copyright 2014 Nikita Nikitsky, Volgograd State Technical University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qformat_xml_import_export_test extends advanced_testcase {
+ /**
+ * Create object qformat_xml for test.
+ * @param string $filename with name for testing file.
+ * @param object $course
+ * @return object qformat_xml.
+ */
+ public function create_qformat($filename, $course) {
+ global $DB;
+ $qformat = new qformat_xml();
+ $contexts = $DB->get_records('context');
+ $importfile = __DIR__ . '/fixtures/' .$filename;
+ $realfilename = $filename;
+ $qformat->setContexts($contexts);
+ $qformat->setCourse($course);
+ $qformat->setFilename($importfile);
+ $qformat->setRealfilename($realfilename);
+ $qformat->setMatchgrades('error');
+ $qformat->setCatfromfile(1);
+ $qformat->setContextfromfile(1);
+ $qformat->setStoponerror(1);
+ $qformat->setCattofile(1);
+ $qformat->setContexttofile(1);
+ $qformat->set_display_progress(false);
+
+ return $qformat;
+ }
+ /**
+ * Check xml for compliance.
+ * @param string $expectedxml with correct string.
+ * @param string $xml you want to check.
+ */
+ public function assert_same_xml($expectedxml, $xml) {
+ $this->assertEquals(preg_replace('/( +)/', "", str_replace("\n", "",
+ str_replace("\r\n", "\n", str_replace("\t", "\n", $expectedxml)))),
+ preg_replace('/( +)/', "", str_replace("\n", "",
+ str_replace( "\r\n", "\n", str_replace( "\t", "\n", $xml)))));
+ }
+
+ /**
+ * Check xml for compliance.
+ * @param string $expectedxml with correct string.
+ * @param string $xml you want to check.
+ */
+ public function assert_same_xml_random_category($expectedxml, $xml) {
+ $str1 = preg_replace('/( +)/', "",
+ str_replace("\n", "", str_replace("\r\n", "\n",
+ str_replace("\t", "\n", $expectedxml))));
+
+ $str2 = preg_replace('/( +)/', "", str_replace("\n", "",
+ str_replace( "\r\n", "\n", str_replace( "\t", "\n", $xml))));
+
+ $str1 = str_replace("unknownhost+" + '/[0-9]+/' + "+", "", $str1);
+ $this->assertEquals($str1, $str2);
+ }
+
+ /**
+ * Check imported category.
+ * @param string $name imported category name.
+ * @param string $info imported category info field (description of category).
+ * @param int $infoformat imported category info field format.
+ */
+ public function assert_category_imported($name, $info, $infoformat) {
+ global $DB;
+ $category = $DB->get_record('question_categories', ['name' => $name], '*', MUST_EXIST);
+ $this->assertEquals($info, $category->info);
+ $this->assertEquals($infoformat, $category->infoformat);
+ }
+
+ /**
+ * Check a question category has a given parent.
+ * @param string $catname Name of the question category
+ * @param string $parentname Name of the parent category
+ * @throws dml_exception
+ */
+ public function assert_category_has_parent($catname, $parentname) {
+ global $DB;
+ $sql = 'SELECT qc1.*
+ FROM {question_categories} qc1
+ JOIN {question_categories} qc2 ON qc1.parent = qc2.id
+ WHERE qc1.name = ?
+ AND qc2.name = ?';
+ $categories = $DB->get_records_sql($sql, [$catname, $parentname]);
+ $this->assertTrue(count($categories) == 1);
+ }
+
+ /**
+ * Check a question exists in a category.
+ * @param string $qname The name of the question
+ * @param string $catname The name of the category
+ * @throws dml_exception
+ */
+ public function assert_question_in_category($qname, $catname) {
+ global $DB;
+ $question = $DB->get_record('question', ['name' => $qname], '*', MUST_EXIST);
+ $category = $DB->get_record('question_categories', ['name' => $catname], '*', MUST_EXIST);
+ $this->assertEquals($category->id, $question->category);
+ }
+
+ /**
+ * Simple check for importing a category with a description.
+ */
+ public function test_import_category() {
+ $this->resetAfterTest(true);
+ $course = $this->getDataGenerator()->create_course();
+ $this->setAdminUser();
+ $qformat = $this->create_qformat('category_with_description.xml', $course);
+ $imported = $qformat->importprocess();
+ $this->assertTrue($imported);
+ $this->assert_category_imported('Alpha', 'This is Alpha category for test', FORMAT_MOODLE);
+ $this->assert_category_has_parent('Alpha', 'top');
+ }
+
+ /**
+ * Check importing nested categories.
+ */
+ public function test_import_nested_categories() {
+ $this->resetAfterTest(true);
+ $course = $this->getDataGenerator()->create_course();
+ $this->setAdminUser();
+ $qformat = $this->create_qformat('nested_categories.xml', $course);
+ $imported = $qformat->importprocess();
+ $this->assertTrue($imported);
+ $this->assert_category_imported('Delta', 'This is Delta category for test', FORMAT_PLAIN);
+ $this->assert_category_imported('Epsilon', 'This is Epsilon category for test', FORMAT_MARKDOWN);
+ $this->assert_category_imported('Zeta', 'This is Zeta category for test', FORMAT_MOODLE);
+ $this->assert_category_has_parent('Delta', 'top');
+ $this->assert_category_has_parent('Epsilon', 'Delta');
+ $this->assert_category_has_parent('Zeta', 'Epsilon');
+ }
+
+ /**
+ * Check importing nested categories contain the right questions.
+ */
+ public function test_import_nested_categories_with_questions() {
+ $this->resetAfterTest(true);
+ $course = $this->getDataGenerator()->create_course();
+ $this->setAdminUser();
+ $qformat = $this->create_qformat('nested_categories_with_questions.xml', $course);
+ $imported = $qformat->importprocess();
+ $this->assertTrue($imported);
+ $this->assert_category_imported('Iota', 'This is Iota category for test', FORMAT_PLAIN);
+ $this->assert_category_imported('Kappa', 'This is Kappa category for test', FORMAT_MARKDOWN);
+ $this->assert_category_imported('Lambda', 'This is Lambda category for test', FORMAT_MOODLE);
+ $this->assert_category_imported('Mu', 'This is Mu category for test', FORMAT_MOODLE);
+ $this->assert_question_in_category('Iota Question', 'Iota');
+ $this->assert_question_in_category('Kappa Question', 'Kappa');
+ $this->assert_question_in_category('Lambda Question', 'Lambda');
+ $this->assert_question_in_category('Mu Question', 'Mu');
+ $this->assert_category_has_parent('Iota', 'top');
+ $this->assert_category_has_parent('Kappa', 'Iota');
+ $this->assert_category_has_parent('Lambda', 'Kappa');
+ $this->assert_category_has_parent('Mu', 'Iota');
+ }
+
+ /**
+ * Check import of an old file (without format), for backward compatability.
+ */
+ public function test_import_old_format() {
+ $this->resetAfterTest(true);
+ $course = $this->getDataGenerator()->create_course();
+ $this->setAdminUser();
+ $qformat = $this->create_qformat('old_format_file.xml', $course);
+ $imported = $qformat->importprocess();
+ $this->assertTrue($imported);
+ $this->assert_category_imported('Pi', '', FORMAT_MOODLE);
+ $this->assert_category_imported('Rho', '', FORMAT_MOODLE);
+ $this->assert_question_in_category('Pi Question', 'Pi');
+ $this->assert_question_in_category('Rho Question', 'Rho');
+ $this->assert_category_has_parent('Pi', 'top');
+ $this->assert_category_has_parent('Rho', 'Pi');
+ }
+
+ /**
+ * Check the import of an xml file where the child category exists before the parent category.
+ */
+ public function test_import_categories_in_reverse_order() {
+ $this->resetAfterTest(true);
+ $course = $this->getDataGenerator()->create_course();
+ $this->setAdminUser();
+ $qformat = $this->create_qformat('categories_reverse_order.xml', $course);
+ $imported = $qformat->importprocess();
+ $this->assertTrue($imported);
+ $this->assert_category_imported('Sigma', 'This is Sigma category for test', FORMAT_HTML);
+ $this->assert_category_imported('Tau', 'This is Tau category for test', FORMAT_HTML);
+ $this->assert_question_in_category('Sigma Question', 'Sigma');
+ $this->assert_question_in_category('Tau Question', 'Tau');
+ $this->assert_category_has_parent('Sigma', 'top');
+ $this->assert_category_has_parent('Tau', 'Sigma');
+ }
+
+ /**
+ * Simple check for exporting a category.
+ */
+ public function test_export_category() {
+ $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
+ $this->resetAfterTest(true);
+ $course = $this->getDataGenerator()->create_course();
+ $this->setAdminUser();
+ // Note while this loads $qformat with all the 'right' data from the xml file,
+ // the call to setCategory, followed by exportprocess will actually only export data
+ // from the database (created by the generator).
+ $qformat = $this->create_qformat('export_category.xml', $course);
+
+ $category = $generator->create_question_category([
+ 'name' => 'Alpha',
+ 'contextid' => '2',
+ 'info' => 'This is Alpha category for test',
+ 'infoformat' => '0',
+ 'stamp' => make_unique_id_code(),
+ 'parent' => '0',
+ 'sortorder' => '999']);
+ $question = $generator->create_question('truefalse', null, [
+ 'category' => $category->id,
+ 'name' => 'AlphaQuestion',
+ 'questiontext' => ['format' => '1', 'text' => '<p>TestingAlphaQuestion</p>'],
+ 'generalfeedback' => ['format' => '1', 'text' => ''],
+ 'correctanswer' => '1',
+ 'feedbacktrue' => ['format' => '1', 'text' => ''],
+ 'feedbackfalse' => ['format' => '1', 'text' => ''],
+ 'penalty' => '1']);
+ $qformat->setCategory($category);
+
+ $xml = preg_replace('/(<!-- question: )([0-9]+)( -->)/', '', $qformat->exportprocess());
+ $file = preg_replace('/(<!-- question: )([0-9]+)( -->)/', '',
+ file_get_contents(__DIR__ . '/fixtures/export_category.xml'));
+ $this->assert_same_xml($file, $xml);
+ }
+
+ /**
+ * Check exporting nested categories.
+ */
+ public function test_export_nested_categories() {
+ $this->resetAfterTest(true);
+ $course = $this->getDataGenerator()->create_course();
+ $this->setAdminUser();
+ $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
+ $qformat = $this->create_qformat('nested_categories.zml', $course);
+
+ $categorydelta = $generator->create_question_category([
+ 'name' => 'Delta',
+ 'contextid' => '2',
+ 'info' => 'This is Delta category for test',
+ 'infoformat' => '2',
+ 'stamp' => make_unique_id_code(),
+ 'parent' => '0',
+ 'sortorder' => '999']);
+ $categoryepsilon = $generator->create_question_category([
+ 'name' => 'Epsilon',
+ 'contextid' => '2',
+ 'info' => 'This is Epsilon category for test',
+ 'infoformat' => '4',
+ 'stamp' => make_unique_id_code(),
+ 'parent' => $categorydelta->id,
+ 'sortorder' => '999']);
+ $categoryzeta = $generator->create_question_category([
+ 'name' => 'Zeta',
+ 'contextid' => '2',
+ 'info' => 'This is Zeta category for test',
+ 'infoformat' => '0',
+ 'stamp' => make_unique_id_code(),
+ 'parent' => $categoryepsilon->id,
+ 'sortorder' => '999']);
+ $question = $generator->create_question('truefalse', null, [
+ 'category' => $categoryzeta->id,
+ 'name' => 'Zeta Question',
+ 'questiontext' => [
+ 'format' => '1',
+ 'text' => '<p>Testing Zeta Question</p>'],
+ 'generalfeedback' => ['format' => '1', 'text' => ''],
+ 'correctanswer' => '1',
+ 'feedbacktrue' => ['format' => '1', 'text' => ''],
+ 'feedbackfalse' => ['format' => '1', 'text' => ''],
+ 'penalty' => '1']);
+ $qformat->setCategory($categorydelta);
+ $qformat->setCategory($categoryepsilon);
+ $qformat->setCategory($categoryzeta);
+
+ $xml = preg_replace('/(<!-- question: )([0-9]+)( -->)/', '', $qformat->exportprocess());
+ $file = preg_replace('/(<!-- question: )([0-9]+)( -->)/', '',
+ file_get_contents(__DIR__ . '/fixtures/nested_categories.xml'));
+ $this->assert_same_xml($file, $xml);
+ }
+
+ /**
+ * Check exporting nested categories contain the right questions.
+ */
+ public function test_export_nested_categories_with_questions() {
+ $this->resetAfterTest(true);
+ $course = $this->getDataGenerator()->create_course();
+ $this->setAdminUser();
+ $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
+ $qformat = $this->create_qformat('nested_categories_with_questions.xml', $course);
+
+ $categoryiota = $generator->create_question_category([
+ 'name' => 'Iota',
+ 'contextid' => '2',
+ 'info' => 'This is Iota category for test',
+ 'infoformat' => '2',
+ 'stamp' => make_unique_id_code(),
+ 'parent' => '0',
+ 'sortorder' => '999']);
+ $iotaquestion = $generator->create_question('truefalse', null, [
+ 'category' => $categoryiota->id,
+ 'name' => 'Iota Question',
+ 'questiontext' => [
+ 'format' => '1',
+ 'text' => '<p>Testing Iota Question</p>'],
+ 'generalfeedback' => ['format' => '1', 'text' => ''],
+ 'correctanswer' => '1',
+ 'feedbacktrue' => ['format' => '1', 'text' => ''],
+ 'feedbackfalse' => ['format' => '1', 'text' => ''],
+ 'penalty' => '1']);
+ $categorykappa = $generator->create_question_category([
+ 'name' => 'Kappa',
+ 'contextid' => '2',
+ 'info' => 'This is Kappa category for test',
+ 'infoformat' => '4',
+ 'stamp' => make_unique_id_code(),
+ 'parent' => $categoryiota->id,
+ 'sortorder' => '999']);
+ $kappaquestion = $generator->create_question('essay', null, [
+ 'category' => $categorykappa->id,
+ 'name' => 'Kappa Essay Question',
+ 'questiontext' => ['text' => 'Testing Kappa Essay Question'],
+ 'generalfeedback' => '',
+ 'responseformat' => 'editor',
+ 'responserequired' => 1,
+ 'responsefieldlines' => 10,
+ 'attachments' => 0,
+ 'attachmentsrequired' => 0,
+ 'graderinfo' => ['format' => '1', 'text' => ''],
+ 'responsetemplate' => ['format' => '1', 'text' => ''],
+ ]);
+ $kappaquestion1 = $generator->create_question('truefalse', null, [
+ 'category' => $categorykappa->id,
+ 'name' => 'Kappa Question',
+ 'questiontext' => [
+ 'format' => '1',
+ 'text' => '<p>Testing Kappa Question</p>'],
+ 'generalfeedback' => ['format' => '1', 'text' => ''],
+ 'correctanswer' => '1',
+ 'feedbacktrue' => ['format' => '1', 'text' => ''],
+ 'feedbackfalse' => ['format' => '1', 'text' => ''],
+ 'penalty' => '1']);
+ $categorylambda = $generator->create_question_category([
+ 'name' => 'Lambda',
+ 'contextid' => '2',
+ 'info' => 'This is Lambda category for test',
+ 'infoformat' => '0',
+ 'stamp' => make_unique_id_code(),
+ 'parent' => $categorykappa->id,
+ 'sortorder' => '999']);
+ $lambdaquestion = $generator->create_question('truefalse', null, [
+ 'category' => $categorylambda->id,
+ 'name' => 'Lambda Question',
+ 'questiontext' => [
+ 'format' => '1',
+ 'text' => '<p>Testing Lambda Question</p>'],
+ 'generalfeedback' => ['format' => '1', 'text' => ''],
+ 'correctanswer' => '1',
+ 'feedbacktrue' => ['format' => '1', 'text' => ''],
+ 'feedbackfalse' => ['format' => '1', 'text' => ''],
+ 'penalty' => '1']);
+ $categorymu = $generator->create_question_category([
+ 'name' => 'Mu',
+ 'contextid' => '2',
+ 'info' => 'This is Mu category for test',
+ 'infoformat' => '0',
+ 'stamp' => make_unique_id_code(),
+ 'parent' => $categoryiota->id,
+ 'sortorder' => '999']);
+ $muquestion = $generator->create_question('truefalse', null, [
+ 'category' => $categorymu->id,
+ 'name' => 'Mu Question',
+ 'questiontext' => [
+ 'format' => '1',
+ 'text' => '<p>Testing Mu Question</p>'],
+ 'generalfeedback' => ['format' => '1', 'text' => ''],
+ 'correctanswer' => '1',
+ 'feedbacktrue' => ['format' => '1', 'text' => ''],
+ 'feedbackfalse' => ['format' => '1', 'text' => ''],
+ 'penalty' => '1']);
+ $qformat->setCategory($categoryiota);
+
+ $xml = preg_replace('/(<!-- question: )([0-9]+)( -->)/', '', $qformat->exportprocess());
+ $file = preg_replace('/(<!-- question: )([0-9]+)( -->)/', '',
+ file_get_contents(__DIR__ . '/fixtures/nested_categories_with_questions.xml'));
+ $this->assert_same_xml($file, $xml);
+ }
+}
$this->assertEquals('/myfolder/', $file->filepath);
$this->assertEquals(6, $file->size);
}
+
+ public function test_create_dummy_question() {
+
+ $testobject = new mock_qformat_xml();
+ $categoryname = 'name1';
+ $categoryinfo = new stdClass();
+ $categoryinfo->info = 'info1';
+ $categoryinfo->infoformat = 'infoformat1';
+ $dummyquestion = $testobject->mock_create_dummy_question_representing_category($categoryname, $categoryinfo);
+
+ $this->assertEquals('category', $dummyquestion->qtype);
+ $this->assertEquals($categoryname, $dummyquestion->category);
+ $this->assertEquals($categoryinfo->info, $dummyquestion->info);
+ $this->assertEquals($categoryinfo->infoformat, $dummyquestion->infoformat);
+ $this->assertEquals('Switch category to ' . $categoryname, $dummyquestion->name);
+ $this->assertEquals(0, $dummyquestion->id);
+ $this->assertEquals('', $dummyquestion->questiontextformat);
+ $this->assertEquals(0, $dummyquestion->contextid);
+ }
+}
+
+/**
+ * Class mock_qformat_xml exists only to enable testing of the create dummy question category.
+ * @package qformat_xml
+ * @copyright 2018 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mock_qformat_xml extends qformat_xml {
+ /**
+ * Make public an otherwise protected function.
+ * @param string $categoryname the name of the category
+ * @param object $categoryinfo description of the category
+ */
+ public function mock_create_dummy_question_representing_category(string $categoryname, $categoryinfo) {
+ return $this->create_dummy_question_representing_category($categoryname, $categoryinfo);
+ }
}
And I should not see "Student 2" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
# Search by idnumber (only).
- Given I click on ".tag[data-value='student1@example.com']" "css_element"
- When I set the field "Filters" to "SID"
+ And I click on "student1@example.com" "text" in the ".form-autocomplete-selection" "css_element"
+ And I set the field "Filters" to "SID"
And I press key "13" in the field "Filters"
- Then I should see "Student 1" in the "participants" "table"
+ And I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should see "Student 4" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
# Search by city (only).
- Given I click on ".tag[data-value='SID']" "css_element"
- When I set the field "Filters" to "SCITY"
+ And I click on "SID" "text" in the ".form-autocomplete-selection" "css_element"
+ And I set the field "Filters" to "SCITY"
And I press key "13" in the field "Filters"
- Then I should see "Student 1" in the "participants" "table"
+ And I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should see "Student 4" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
# Search by country text (only) - should not match.
- Given I click on ".tag[data-value='SCITY']" "css_element"
- When I set the field "Filters" to "GB"
+ And I click on "SCITY" "text" in the ".form-autocomplete-selection" "css_element"
+ And I set the field "Filters" to "GB"
And I press key "13" in the field "Filters"
- Then I should see "Nothing to display"
+ And I should see "Nothing to display"
# Check no match.
- Given I set the field "Filters" to "NOTHING"
- When I press key "13" in the field "Filters"
- Then I should see "Nothing to display"
+ And I click on "GB" "text" in the ".form-autocomplete-selection" "css_element"
+ And I set the field "Filters" to "NOTHING"
+ And I press key "13" in the field "Filters"
+ And I should see "Nothing to display"
@javascript
Scenario: Filter by user identity fields when cannot see the field data
And I should not see "Student 4" in the "participants" "table"
And I should see "Teacher 1" in the "participants" "table"
# Search for other fields - should only see own results.
- Given I click on ".tag[data-value='@example.com']" "css_element"
- When I set the field "Filters" to "SID"
+ And I click on "@example.com" "text" in the ".form-autocomplete-selection" "css_element"
+ And I set the field "Filters" to "SID"
And I press key "13" in the field "Filters"
- Then I should see "Nothing to display"
- Given I click on ".tag[data-value='SID']" "css_element"
- When I set the field "Filters" to "TID"
+ And I should see "Nothing to display"
+ And I click on "SID" "text" in the ".form-autocomplete-selection" "css_element"
+ And I set the field "Filters" to "TID"
And I press key "13" in the field "Filters"
- Then I should see "Teacher 1" in the "participants" "table"
- Given I set the field "Filters" to "CITY"
- When I press key "13" in the field "Filters"
- Then I should see "Teacher 1" in the "participants" "table"
+ And I should see "Teacher 1" in the "participants" "table"
+ And I set the field "Filters" to "CITY"
+ And I press key "13" in the field "Filters"
+ And I should see "Teacher 1" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
# Check no match.
- Given I set the field "Filters" to "NOTHING"
- When I press key "13" in the field "Filters"
- Then I should see "Nothing to display"
+ And I set the field "Filters" to "NOTHING"
+ And I press key "13" in the field "Filters"
+ And I should see "Nothing to display"
defined('MOODLE_INTERNAL') || die();
-$version = 2018091400.00; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2018091700.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.