Scenario: Enable registration of multiple accounts with the same email address
Given the following config values are set as admin:
| allowaccountssameemail | 1 |
- When I navigate to "Add a new user" node in "Site administration>Users>Accounts"
+ When I navigate to "Users > Accounts > Add a new user" in site administration
And I set the following fields to these values:
| Username | testmultiemailuser1 |
| Choose an authentication method | Manual accounts |
Scenario: Disable registration of multiple accounts with the same email address
Given the following config values are set as admin:
| allowaccountssameemail | 0 |
- When I navigate to "Add a new user" node in "Site administration>Users>Accounts"
+ When I navigate to "Users > Accounts > Add a new user" in site administration
And I set the following fields to these values:
| Username | testmultiemailuser1 |
| Choose an authentication method | Manual accounts |
And the following config values are set as admin:
| enableavailability | 1 |
And I am on homepage
- And I navigate to "Manage restrictions" node in "Site administration > Plugins > Availability restrictions"
+ And I navigate to "Plugins > Availability restrictions > Manage restrictions" in site administration
# Having clicked on it, I should also see the list of plugins.
And I should see "Restriction by date"
| Course 1 | C1 | topics |
And I log in as "admin"
And I am on site homepage
- When I navigate to "Manage restrictions" node in "Site administration > Plugins > Availability restrictions"
+ When I navigate to "Plugins > Availability restrictions > Manage restrictions" in site administration
# Check the icon is there (it should be a Hide icon, meaning is currently visible).
Then "Hide" "icon" should exist in the "Restriction by date" "table_row"
| student1 | CHSB |
| student1 | CHC |
When I log in as "admin"
- And I navigate to "Cohorts" node in "Site administration > Users > Accounts"
+ And I navigate to "Users > Accounts > Cohorts" in site administration
Then the following should exist in the "cohorts" table:
| Name | Cohort size |
| System cohort A | 1 |
| wiki | C1 | wiki1 | Test this one | Test this one | Test this one | collaborative | 0 |
And I log in as "admin"
And I am on "Course 1" course homepage
- And I navigate to "Reset" node in "Course administration"
+ And I navigate to "Reset" in current page administration
# Select (multi-select) - Checking "the select box should contain".
And I expand all fieldsets
And the "Unenrol users" select box should contain "No roles"
And the field "two" matches value ""
# Check if field xpath set/match works.
And I am on "Course 1" course homepage
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I set the field with xpath "//input[@id='id_idnumber']" to "Course id number"
And the field with xpath "//input[@name='idnumber']" matches value "Course id number"
And the field with xpath "//input[@name='idnumber']" does not match value ""
And I press "Save and display"
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And the field "Course ID number" matches value "Course id number"
Scenario: with JS disabled all form fields getters and setters works as expected
Background:
Given I am on homepage
And I log in as "admin"
- And I navigate to "Acceptance testing" node in "Site administration > Development"
+ And I navigate to "Development > Acceptance testing" in site administration
@javascript
Scenario: Accessing the list
var stringkeys = [
{
key: 'deletecategory',
- component: 'tool_dataprivacy',
- param: categoryname
+ component: 'tool_dataprivacy'
},
{
key: 'deletecategorytext',
component: 'tool_dataprivacy',
param: categoryname
+ },
+ {
+ key: 'delete'
}
];
Str.get_strings(stringkeys).then(function(langStrings) {
var title = langStrings[0];
var confirmMessage = langStrings[1];
+ var buttonText = langStrings[2];
return ModalFactory.create({
title: title,
body: confirmMessage,
type: ModalFactory.types.SAVE_CANCEL
}).then(function(modal) {
- modal.setSaveButtonText(title);
+ modal.setSaveButtonText(buttonText);
// Handle save event.
modal.getRoot().on(ModalEvents.save, function() {
var stringkeys = [
{
key: 'deletepurpose',
- component: 'tool_dataprivacy',
- param: purposename
+ component: 'tool_dataprivacy'
},
{
key: 'deletepurposetext',
component: 'tool_dataprivacy',
param: purposename
+ },
+ {
+ key: 'delete'
}
];
Str.get_strings(stringkeys).then(function(langStrings) {
var title = langStrings[0];
var confirmMessage = langStrings[1];
+ var buttonText = langStrings[2];
return ModalFactory.create({
title: title,
body: confirmMessage,
type: ModalFactory.types.SAVE_CANCEL
}).then(function(modal) {
- modal.setSaveButtonText(title);
+ modal.setSaveButtonText(buttonText);
// Handle save event.
modal.getRoot().on(ModalEvents.save, function() {
/** The request is now being processed. */
const DATAREQUEST_STATUS_PROCESSING = 4;
- /** Data request completed. */
+ /** Information/other request completed. */
const DATAREQUEST_STATUS_COMPLETE = 5;
/** Data request cancelled by the user. */
/** Data request rejected by the DPO. */
const DATAREQUEST_STATUS_REJECTED = 7;
+ /** Data request download ready. */
+ const DATAREQUEST_STATUS_DOWNLOAD_READY = 8;
+
+ /** Data request expired. */
+ const DATAREQUEST_STATUS_EXPIRED = 9;
+
+ /** Data delete request completed, account is removed. */
+ const DATAREQUEST_STATUS_DELETED = 10;
+
/**
* Determines whether the user can contact the site's Data Protection Officer via Moodle.
*
}
}
+ // If any are due to expire, expire them and re-fetch updated data.
+ if (empty($statuses)
+ || in_array(self::DATAREQUEST_STATUS_DOWNLOAD_READY, $statuses)
+ || in_array(self::DATAREQUEST_STATUS_EXPIRED, $statuses)) {
+ $expiredrequests = data_request::get_expired_requests($userid);
+
+ if (!empty($expiredrequests)) {
+ data_request::expire($expiredrequests);
+ $results = self::get_data_requests($userid, $statuses, $types, $sort, $offset, $limit);
+ }
+ }
+
return $results;
}
self::DATAREQUEST_STATUS_COMPLETE,
self::DATAREQUEST_STATUS_CANCELLED,
self::DATAREQUEST_STATUS_REJECTED,
+ self::DATAREQUEST_STATUS_DOWNLOAD_READY,
+ self::DATAREQUEST_STATUS_EXPIRED,
+ self::DATAREQUEST_STATUS_DELETED,
];
list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED);
$select = 'type = :type AND userid = :userid AND status NOT ' . $insql;
self::DATAREQUEST_STATUS_COMPLETE,
self::DATAREQUEST_STATUS_CANCELLED,
self::DATAREQUEST_STATUS_REJECTED,
+ self::DATAREQUEST_STATUS_DOWNLOAD_READY,
+ self::DATAREQUEST_STATUS_EXPIRED,
+ self::DATAREQUEST_STATUS_DELETED,
];
return !in_array($status, $finalstatuses);
api::DATAREQUEST_STATUS_COMPLETE,
api::DATAREQUEST_STATUS_CANCELLED,
api::DATAREQUEST_STATUS_REJECTED,
+ api::DATAREQUEST_STATUS_DOWNLOAD_READY,
+ api::DATAREQUEST_STATUS_EXPIRED,
+ api::DATAREQUEST_STATUS_DELETED,
],
'type' => PARAM_INT
],
],
];
}
+
+ /**
+ * Determines whether a completed data export request has expired.
+ * The response will be valid regardless of the expiry scheduled task having run.
+ *
+ * @param data_request $request the data request object whose expiry will be checked.
+ * @return bool true if the request has expired.
+ */
+ public static function is_expired(data_request $request) {
+ $result = false;
+
+ // Only export requests expire.
+ if ($request->get('type') == api::DATAREQUEST_TYPE_EXPORT) {
+ switch ($request->get('status')) {
+ // Expired requests are obviously expired.
+ case api::DATAREQUEST_STATUS_EXPIRED:
+ $result = true;
+ break;
+ // Complete requests are expired if the expiry time has elapsed.
+ case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
+ $expiryseconds = get_config('tool_dataprivacy', 'privacyrequestexpiry');
+ if ($expiryseconds > 0 && time() >= ($request->get('timemodified') + $expiryseconds)) {
+ $result = true;
+ }
+ break;
+ }
+ }
+
+ return $result;
+ }
+
+
+
+ /**
+ * Fetch completed data requests which are due to expire.
+ *
+ * @param int $userid Optional user ID to filter by.
+ *
+ * @return array Details of completed requests which are due to expire.
+ */
+ public static function get_expired_requests($userid = 0) {
+ global $DB;
+
+ $expiryseconds = get_config('tool_dataprivacy', 'privacyrequestexpiry');
+ $expirytime = strtotime("-{$expiryseconds} second");
+ $table = self::TABLE;
+ $sqlwhere = 'type = :export_type AND status = :completestatus AND timemodified <= :expirytime';
+ $params = array(
+ 'export_type' => api::DATAREQUEST_TYPE_EXPORT,
+ 'completestatus' => api::DATAREQUEST_STATUS_DOWNLOAD_READY,
+ 'expirytime' => $expirytime,
+ );
+ $sort = 'id';
+ $fields = 'id, userid';
+
+ // Filter by user ID if specified.
+ if ($userid > 0) {
+ $sqlwhere .= ' AND (userid = :userid OR requestedby = :requestedby)';
+ $params['userid'] = $userid;
+ $params['requestedby'] = $userid;
+ }
+
+ return $DB->get_records_select_menu($table, $sqlwhere, $params, $sort, $fields, 0, 2000);
+ }
+
+ /**
+ * Expire a given set of data requests.
+ * Update request status and delete the files.
+ *
+ * @param array $expiredrequests [requestid => userid]
+ *
+ * @return void
+ */
+ public static function expire($expiredrequests) {
+ global $DB;
+
+ $ids = array_keys($expiredrequests);
+
+ if (count($ids) > 0) {
+ list($insql, $inparams) = $DB->get_in_or_equal($ids);
+ $initialparams = array(api::DATAREQUEST_STATUS_EXPIRED, time());
+ $params = array_merge($initialparams, $inparams);
+
+ $update = "UPDATE {" . self::TABLE . "}
+ SET status = ?, timemodified = ?
+ WHERE id $insql";
+
+ if ($DB->execute($update, $params)) {
+ $fs = get_file_storage();
+
+ foreach ($expiredrequests as $id => $userid) {
+ $usercontext = \context_user::instance($userid);
+ $fs->delete_area_files($usercontext->id, 'tool_dataprivacy', 'export', $id);
+ }
+ }
+ }
+ }
}
switch ($this->persistent->get('status')) {
case api::DATAREQUEST_STATUS_PENDING:
- $values['statuslabelclass'] = 'label-default';
+ $values['statuslabelclass'] = 'label-info';
// Request can be manually completed for general enquiry requests.
$values['canmarkcomplete'] = $requesttype == api::DATAREQUEST_TYPE_OTHERS;
break;
$values['statuslabelclass'] = 'label-info';
break;
case api::DATAREQUEST_STATUS_COMPLETE:
+ case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
+ case api::DATAREQUEST_STATUS_DELETED:
$values['statuslabelclass'] = 'label-success';
break;
case api::DATAREQUEST_STATUS_CANCELLED:
case api::DATAREQUEST_STATUS_REJECTED:
$values['statuslabelclass'] = 'label-important';
break;
+ case api::DATAREQUEST_STATUS_EXPIRED:
+ $values['statuslabelclass'] = 'label-default';
+ break;
}
return $values;
if (!isset($statuses[$status])) {
throw new moodle_exception('errorinvalidrequeststatus', 'tool_dataprivacy');
}
+
return $statuses[$status];
}
api::DATAREQUEST_STATUS_APPROVED => get_string('statusapproved', 'tool_dataprivacy'),
api::DATAREQUEST_STATUS_PROCESSING => get_string('statusprocessing', 'tool_dataprivacy'),
api::DATAREQUEST_STATUS_COMPLETE => get_string('statuscomplete', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_DOWNLOAD_READY => get_string('statusready', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_EXPIRED => get_string('statusexpired', 'tool_dataprivacy'),
api::DATAREQUEST_STATUS_CANCELLED => get_string('statuscancelled', 'tool_dataprivacy'),
api::DATAREQUEST_STATUS_REJECTED => get_string('statusrejected', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_DELETED => get_string('statusdeleted', 'tool_dataprivacy'),
];
}
/** @var bool Whether this table is being rendered for managing data requests. */
protected $manage = false;
- /** @var stdClass[] Array of data request persistents. */
+ /** @var \tool_dataprivacy\data_request[] Array of data request persistents. */
protected $datarequests = [];
/**
$actiontext = get_string('denyrequest', 'tool_dataprivacy');
$actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
break;
- }
-
- if ($status == api::DATAREQUEST_STATUS_COMPLETE) {
- $userid = $data->foruser->id;
- $usercontext = \context_user::instance($userid, IGNORE_MISSING);
- if ($usercontext && api::can_download_data_request_for_user($userid, $data->requestedbyuser->id)) {
- $actions[] = api::get_download_link($usercontext, $requestid);
- }
+ case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
+ $userid = $data->foruser->id;
+ $usercontext = \context_user::instance($userid, IGNORE_MISSING);
+ // If user has permission to view download link, show relevant action item.
+ if ($usercontext && api::can_download_data_request_for_user($userid, $data->requestedbyuser->id)) {
+ $actions[] = api::get_download_link($usercontext, $requestid);
+ }
+ break;
}
$actionsmenu = new action_menu($actions);
public function query_db($pagesize, $useinitialsbar = true) {
global $PAGE;
- // Count data requests from the given conditions.
- $total = api::get_data_requests_count($this->userid, $this->statuses, $this->types);
- $this->pagesize($pagesize, $total);
+ // Set dummy page total until we fetch full result set.
+ $this->pagesize($pagesize, $pagesize + 1);
$sort = $this->get_sql_sort();
// Get data requests from the given conditions.
$datarequests = api::get_data_requests($this->userid, $this->statuses, $this->types, $sort,
$this->get_page_start(), $this->get_page_size());
+
+ // Count data requests from the given conditions.
+ $total = api::get_data_requests_count($this->userid, $this->statuses, $this->types);
+ $this->pagesize($pagesize, $total);
+
$this->rawdata = [];
$context = \context_system::instance();
$renderer = $PAGE->get_renderer('tool_dataprivacy');
+
foreach ($datarequests as $persistent) {
+ $this->datarequests[$persistent->get('id')] = $persistent;
$exporter = new data_request_exporter($persistent, ['context' => $context]);
$this->rawdata[] = $exporter->export($renderer);
}
$item->statuslabelclass = 'label-success';
$item->statuslabel = get_string('statuscomplete', 'tool_dataprivacy');
$cancancel = false;
- // Show download links only for export-type data requests.
- $candownload = $type == api::DATAREQUEST_TYPE_EXPORT;
+ break;
+ case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
+ $item->statuslabelclass = 'label-success';
+ $item->statuslabel = get_string('statusready', 'tool_dataprivacy');
+ $cancancel = false;
+ $candownload = true;
+
if ($usercontext) {
$candownload = api::can_download_data_request_for_user(
$request->get('userid'), $request->get('requestedby'));
}
break;
+ case api::DATAREQUEST_STATUS_DELETED:
+ $item->statuslabelclass = 'label-success';
+ $item->statuslabel = get_string('statusdeleted', 'tool_dataprivacy');
+ $cancancel = false;
+ break;
+ case api::DATAREQUEST_STATUS_EXPIRED:
+ $item->statuslabelclass = 'label-default';
+ $item->statuslabel = get_string('statusexpired', 'tool_dataprivacy');
+ $item->statuslabeltitle = get_string('downloadexpireduser', 'tool_dataprivacy');
+ $cancancel = false;
+ break;
case api::DATAREQUEST_STATUS_CANCELLED:
case api::DATAREQUEST_STATUS_REJECTED:
$cancancel = false;
--- /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/>.
+
+/**
+ * Scheduled task to delete files and update statuses of expired data requests.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Michael Hawkins
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_dataprivacy\task;
+
+use coding_exception;
+use core\task\scheduled_task;
+use tool_dataprivacy\api;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
+
+/**
+ * Scheduled task to delete files and update request statuses once they expire.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Michael Hawkins
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class delete_expired_requests extends scheduled_task {
+
+ /**
+ * Returns the task name.
+ *
+ * @return string
+ */
+ public function get_name() {
+ return get_string('deleteexpireddatarequeststask', 'tool_dataprivacy');
+ }
+
+ /**
+ * Run the task to delete expired data request files and update request statuses.
+ *
+ */
+ public function execute() {
+ $expiredrequests = \tool_dataprivacy\data_request::get_expired_requests();
+ $deletecount = count($expiredrequests);
+
+ if ($deletecount > 0) {
+ \tool_dataprivacy\data_request::expire($expiredrequests);
+
+ mtrace($deletecount . ' expired completed data requests have been deleted');
+ }
+ }
+}
// Update the status of this request as pre-processing.
mtrace('Processing request...');
api::update_request_status($requestid, api::DATAREQUEST_STATUS_PROCESSING);
+ $completestatus = api::DATAREQUEST_STATUS_COMPLETE;
if ($request->type == api::DATAREQUEST_TYPE_EXPORT) {
// Get the collection of approved_contextlist objects needed for core_privacy data export.
$filerecord->author = fullname($foruser);
// Save somewhere.
$thing = $fs->create_file_from_pathname($filerecord, $exportedcontent);
-
+ $completestatus = api::DATAREQUEST_STATUS_DOWNLOAD_READY;
} else if ($request->type == api::DATAREQUEST_TYPE_DELETE) {
// Get the collection of approved_contextlist objects needed for core_privacy data deletion.
$approvedclcollection = api::get_approved_contextlist_collection_for_request($requestpersistent);
$manager->set_observer(new \tool_dataprivacy\manager_observer());
$manager->delete_data_for_user($approvedclcollection);
+ $completestatus = api::DATAREQUEST_STATUS_DELETED;
}
// When the preparation of the metadata finishes, update the request status to awaiting approval.
- api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE);
+ api::update_request_status($requestid, $completestatus);
mtrace('The processing of the user data request has been completed...');
// Create message to notify the user regarding the processing results.
<?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20180313" COMMENT="XMLDB file for Moodle tool/dataprivacy"
+<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20180821" COMMENT="XMLDB file for Moodle tool/dataprivacy"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
>
<FIELD NAME="commentsformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID the request is being made for"/>
<FIELD NAME="requestedby" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID of the one making the request"/>
- <FIELD NAME="status" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The current status of the data request"/>
+ <FIELD NAME="status" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The current status of the data request"/>
<FIELD NAME="dpo" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID of the Data Protection Officer who is reviewing th request"/>
<FIELD NAME="dpocomment" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="DPO's comments (e.g. reason for rejecting the request, etc.)"/>
<FIELD NAME="dpocommentformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
- <FIELD NAME="status" TYPE="int" LENGTH="2" DEFAULT="0" NOTNULL="true" SEQUENCE="false"/>
+ <FIELD NAME="status" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</KEYS>
</TABLE>
</TABLES>
-</XMLDB>
+</XMLDB>
\ No newline at end of file
'day' => '*',
'dayofweek' => '*',
'month' => '*'
+ ), array(
+ 'classname' => 'tool_dataprivacy\task\delete_expired_requests',
+ 'blocking' => 0,
+ 'minute' => 'R',
+ 'hour' => 'R',
+ 'day' => '*',
+ 'dayofweek' => '*',
+ 'month' => '*'
),
);
upgrade_plugin_savepoint(true, 2018051405, 'tool', 'dataprivacy');
}
+ if ($oldversion < 2018051406) {
+ // Update completed delete requests to new delete status.
+ $query = "UPDATE {tool_dataprivacy_request}
+ SET status = :setstatus
+ WHERE type = :type
+ AND status = :wherestatus";
+ $params = array(
+ 'setstatus' => 10, // Request deleted.
+ 'type' => 2, // Delete type.
+ 'wherestatus' => 5, // Request completed.
+ );
+
+ $DB->execute($query, $params);
+
+ // Update completed data export requests to new download ready status.
+ $params = array(
+ 'setstatus' => 8, // Request download ready.
+ 'type' => 1, // export type.
+ 'wherestatus' => 5, // Request completed.
+ );
+
+ $DB->execute($query, $params);
+
+ upgrade_plugin_savepoint(true, 2018051406, 'tool', 'dataprivacy');
+ }
+
+ if ($oldversion < 2018082100) {
+
+ // Changing precision of field status on table tool_dataprivacy_request to (2).
+ $table = new xmldb_table('tool_dataprivacy_request');
+ $field = new xmldb_field('status', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'requestedby');
+
+ // Launch change of precision for field status.
+ $dbman->change_field_precision($table, $field);
+
+ // Dataprivacy savepoint reached.
+ upgrade_plugin_savepoint(true, 2018082100, 'tool', 'dataprivacy');
+ }
+
return true;
}
$string['daterequested'] = 'Date requested';
$string['daterequesteddetail'] = 'Date requested:';
$string['defaultsinfo'] = 'Default categories and purposes are applied to all newly created instances.';
-$string['deletecategory'] = 'Delete "{$a}" category';
-$string['deletecategorytext'] = 'Are you sure you want to delete "{$a}" category?';
+$string['deletecategory'] = 'Delete category';
+$string['deletecategorytext'] = 'Are you sure you want to delete the category \'{$a}\'?';
$string['deleteexpiredcontextstask'] = 'Delete expired contexts';
-$string['deletepurpose'] = 'Delete "{$a}" purpose';
-$string['deletepurposetext'] = 'Are you sure you want to delete "{$a}" purpose?';
+$string['deleteexpireddatarequeststask'] = 'Delete files from completed data requests that have expired';
+$string['deletepurpose'] = 'Delete purpose';
+$string['deletepurposetext'] = 'Are you sure you want to delete the purpose \'{$a}\'?';
$string['defaultssaved'] = 'Defaults saved';
$string['deny'] = 'Deny';
$string['denyrequest'] = 'Deny request';
$string['download'] = 'Download';
+$string['downloadexpireduser'] = 'Download has expired. Submit a new request if you wish to export your personal data.';
$string['dporolemapping'] = 'Privacy officer role mapping';
$string['dporolemapping_desc'] = 'The privacy officer can manage data requests. The capability tool/dataprivacy:managedatarequests must be allowed for a role to be listed as a privacy officer role mapping option.';
$string['editcategories'] = 'Edit categories';
$string['privacy:metadata:request:requestedby'] = 'The ID of the user making the request, if made on behalf of another user.';
$string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s privacy officer regarding the request.';
$string['privacy:metadata:request:timecreated'] = 'The timestamp indicating when the request was made by the user.';
+$string['privacyrequestexpiry'] = 'Data request expiry';
+$string['privacyrequestexpiry_desc'] = 'The amount of time that approved data requests will be available for download before expiring. 0 means no time limit.';
$string['protected'] = 'Protected';
$string['protectedlabel'] = 'The retention of this data has a higher legal precedent over a user\'s request to be forgotten. This data will only be deleted after the retention period has expired.';
$string['purpose'] = 'Purpose';
$string['statusawaitingapproval'] = 'Awaiting approval';
$string['statuscancelled'] = 'Cancelled';
$string['statuscomplete'] = 'Complete';
+$string['statusready'] = 'Download ready';
+$string['statusdeleted'] = 'Deleted';
$string['statusdetail'] = 'Status:';
+$string['statusexpired'] = 'Expired';
$string['statuspreprocessing'] = 'Pre-processing';
$string['statusprocessing'] = 'Processing';
$string['statuspending'] = 'Pending';
return false;
}
+ // Make the file unavailable if it has expired.
+ if (\tool_dataprivacy\data_request::is_expired($datarequest)) {
+ send_file_not_found();
+ }
+
// All good. Serve the exported data.
$fs = get_file_storage();
$relativepath = implode('/', $args);
echo $OUTPUT->header();
echo $OUTPUT->heading($title);
-$requests = tool_dataprivacy\api::get_data_requests($USER->id);
+$requests = tool_dataprivacy\api::get_data_requests($USER->id, [], [], 'timecreated DESC');
$requestlist = new tool_dataprivacy\output\my_data_requests_page($requests);
$requestlistoutput = $PAGE->get_renderer('tool_dataprivacy');
echo $requestlistoutput->render($requestlist);
new lang_string('contactdataprotectionofficer_desc', 'tool_dataprivacy'), 0)
);
+ // Set days approved data requests will be accessible. 1 week default.
+ $privacysettings->add(new admin_setting_configduration('tool_dataprivacy/privacyrequestexpiry',
+ new lang_string('privacyrequestexpiry', 'tool_dataprivacy'),
+ new lang_string('privacyrequestexpiry_desc', 'tool_dataprivacy'),
+ WEEKSECS, 1));
+
// Fetch roles that are assignable.
$assignableroles = get_assignable_roles(context_system::instance());
<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">
+ <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}}
</button>
</div>
"typename" : "Data deletion",
"comments": "Please delete all of my son's personal data.",
"statuslabelclass": "label-success",
- "statuslabel": "Complete",
+ "statuslabel": "Deleted",
"timecreated" : 1517902087,
"requestedbyuser" : {
"fullname": "Martha Smith",
"fullname": "Martha Smith",
"profileurl": "#"
}
+ },
+ {
+ "id": 6,
+ "typename" : "Data export",
+ "comments": "Please let me download my data",
+ "statuslabelclass": "label",
+ "statuslabel": "Expired",
+ "statuslabeltitle": "Download has expired. Submit a new request if you wish to export your personal data.",
+ "timecreated" : 1517902087,
+ "requestedbyuser" : {
+ "fullname": "Martha Smith",
+ "profileurl": "#"
+ }
}
]
}
<td>{{#userdate}} {{timecreated}}, {{#str}} strftimedatetime {{/str}} {{/userdate}}</td>
<td><a href="{{requestedbyuser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{requestedbyuser.fullname}}</a></td>
<td>
- <span class="label {{statuslabelclass}}">{{statuslabel}}</span>
+ <span class="label {{statuslabelclass}}" title="{{statuslabeltitle}}">{{statuslabel}}</span>
</td>
<td>{{comments}}</td>
<td>
<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">
+ <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}}
</button>
</div>
$requestid = $datarequest->get('id');
// Update with a valid status.
- $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE);
+ $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_DOWNLOAD_READY);
$this->assertTrue($result);
// Fetch the request record again.
$datarequest = new data_request($requestid);
- $this->assertEquals(api::DATAREQUEST_STATUS_COMPLETE, $datarequest->get('status'));
+ $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $datarequest->get('status'));
// Update with an invalid status.
$this->expectException(invalid_persistent_exception::class);
* @return array
*/
public function get_data_requests_provider() {
- $completeonly = [api::DATAREQUEST_STATUS_COMPLETE];
- $completeandcancelled = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_CANCELLED];
+ $completeonly = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_DOWNLOAD_READY, api::DATAREQUEST_STATUS_DELETED];
+ $completeandcancelled = array_merge($completeonly, [api::DATAREQUEST_STATUS_CANCELLED]);
return [
// Own data requests.
[api::DATAREQUEST_STATUS_COMPLETE, false],
[api::DATAREQUEST_STATUS_CANCELLED, false],
[api::DATAREQUEST_STATUS_REJECTED, false],
+ [api::DATAREQUEST_STATUS_DOWNLOAD_READY, false],
+ [api::DATAREQUEST_STATUS_EXPIRED, false],
+ [api::DATAREQUEST_STATUS_DELETED, false],
];
}
--- /dev/null
+@tool @tool_dataprivacy
+Feature: Data delete from the privacy API
+ In order to delete data for users and meet legal requirements
+ As an admin, user, or parent
+ I need to be able to request a user and their data data be deleted
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname |
+ | victim | Victim User | 1 |
+ | parent | Long-suffering | Parent |
+ And the following "roles" exist:
+ | shortname | name | archetype |
+ | tired | Tired | |
+ And the following "permission overrides" exist:
+ | capability | permission | role | contextlevel | reference |
+ | tool/dataprivacy:makedatarequestsforchildren | Allow | tired | System | |
+ And the following "role assigns" exist:
+ | user | role | contextlevel | reference |
+ | parent | tired | User | victim |
+ And the following config values are set as admin:
+ | contactdataprotectionofficer | 1 | tool_dataprivacy |
+
+ @javascript
+ Scenario: As admin, delete a user and their data
+ Given I log in as "victim"
+ And I should see "Victim User 1"
+ And I log out
+
+ And I log in as "admin"
+ And I navigate to "Users > Privacy and policies > Data requests" in site administration
+ And I follow "New request"
+ And I set the field "Requesting for" to "Victim User 1"
+ And I set the field "Type" to "Delete all of my personal data"
+ And I press "Save changes"
+ Then I should see "Victim User 1"
+ And I should see "Pending" in the "Victim User 1" "table_row"
+ And I run all adhoc tasks
+ And I reload the page
+ And I should see "Awaiting approval" in the "Victim User 1" "table_row"
+ And I follow "Actions"
+ And I follow "Approve request"
+ And I press "Approve request"
+ And I should see "Approved" in the "Victim User 1" "table_row"
+ And I run all adhoc tasks
+ And I reload the page
+ And I should see "Deleted" in the "Victim User 1" "table_row"
+
+ And I log out
+ And I log in as "victim"
+ And I should see "Invalid login"
+
+ @javascript
+ Scenario: As a student, request deletion of account and data
+ Given I log in as "victim"
+ And I follow "Profile" in the user menu
+ And I follow "Data requests"
+ And I follow "New request"
+ And I set the field "Type" to "Delete all of my personal data"
+ And I press "Save changes"
+ Then I should see "Delete all of my personal data"
+ And I should see "Pending" in the "Delete all of my personal data" "table_row"
+ And I run all adhoc tasks
+ And I reload the page
+ And I should see "Awaiting approval" in the "Delete all of my personal data" "table_row"
+
+ And I log out
+ And I log in as "admin"
+ And I navigate to "Users > Privacy and policies > Data requests" in site administration
+ And I follow "Actions"
+ And I follow "Approve request"
+ And I press "Approve request"
+
+ And I log out
+ And I log in as "victim"
+ And I follow "Profile" in the user menu
+ And I follow "Data requests"
+ And I should see "Approved" in the "Delete all of my personal data" "table_row"
+ And I run all adhoc tasks
+ And I reload the page
+ And I should see "Your session has timed out"
+ And I log in as "victim"
+ And I should see "Invalid login"
+
+ And I log in as "admin"
+ And I am on site homepage
+ And I navigate to "Users > Privacy and policies > Data requests" in site administration
+ And I should see "Deleted"
+
+ @javascript
+ Scenario: As a parent, request account and data deletion for my child
+ Given I log in as "parent"
+ And I follow "Profile" in the user menu
+ And I follow "Data requests"
+ And I follow "New request"
+ And I set the field "Requesting for" to "Victim User 1"
+ And I set the field "Type" to "Delete all of my personal data"
+ And I press "Save changes"
+ Then I should see "Victim User 1"
+ And I should see "Pending" in the "Victim User 1" "table_row"
+ And I run all adhoc tasks
+ And I reload the page
+ And I should see "Awaiting approval" in the "Victim User 1" "table_row"
+
+ And I log out
+ And I log in as "admin"
+ And I navigate to "Users > Privacy and policies > Data requests" in site administration
+ And I follow "Actions"
+ And I follow "Approve request"
+ And I press "Approve request"
+
+ And I log out
+ And I log in as "parent"
+ And I follow "Profile" in the user menu
+ And I follow "Data requests"
+ And I should see "Approved" in the "Victim User 1" "table_row"
+ And I run all adhoc tasks
+ And I reload the page
+ And I should see "You don't have any personal data requests"
| user | role | contextlevel | reference |
| parent | tired | User | victim |
And the following config values are set as admin:
- | contactdataprotectionofficer | 1 | tool_dataprivacy |
+ | contactdataprotectionofficer | 1 | tool_dataprivacy |
+ | privacyrequestexpiry | 55 | tool_dataprivacy |
@javascript
- Scenario: As admin, export data for a user and download it
+ Scenario: As admin, export data for a user and download it, unless it has expired
Given I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "New request"
And I should see "Approved" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
- And I should see "Complete" in the "Victim User 1" "table_row"
+ And I should see "Download ready" in the "Victim User 1" "table_row"
And I follow "Actions"
And following "Download" should download between "1" and "100000" bytes
+ And the following config values are set as admin:
+ | privacyrequestexpiry | 1 | tool_dataprivacy |
+ And I wait "1" seconds
+ And I navigate to "Users > Privacy and policies > Data requests" in site administration
+ And I should see "Expired" in the "Victim User 1" "table_row"
+ And I follow "Actions"
+ And I should not see "Download"
@javascript
- Scenario: As a student, request data export and then download it when approved
+ Scenario: As a student, request data export and then download it when approved, unless it has expired
Given I log in as "victim"
And I follow "Profile" in the user menu
And I follow "Data requests"
And I should see "Approved" in the "Export all of my personal data" "table_row"
And I run all adhoc tasks
And I reload the page
- And I should see "Complete" in the "Export all of my personal data" "table_row"
+ And I should see "Download ready" in the "Export all of my personal data" "table_row"
And I follow "Actions"
And following "Download" should download between "1" and "100000" bytes
+ And the following config values are set as admin:
+ | privacyrequestexpiry | 1 | tool_dataprivacy |
+ And I wait "1" seconds
+ And I reload the page
+
+ And I should see "Expired" in the "Export all of my personal data" "table_row"
+ And I should not see "Actions"
+
@javascript
Scenario: As a parent, request data export for my child because I don't trust the little blighter
Given I log in as "parent"
And I should see "Approved" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
- And I should see "Complete" in the "Victim User 1" "table_row"
+ And I should see "Download ready" in the "Victim User 1" "table_row"
And I follow "Actions"
And following "Download" should download between "1" and "100000" bytes
+
+ And the following config values are set as admin:
+ | privacyrequestexpiry | 1 | tool_dataprivacy |
+ And I wait "1" seconds
+ And I reload the page
+
+ And I should see "Expired" in the "Victim User 1" "table_row"
+ And I should not see "Actions"
--- /dev/null
+@tool @tool_dataprivacy @javascript
+Feature: Manage data categories
+ As the privacy officer
+ In order to manage the data registry
+ I need to be able to manage the data categories for the data registry
+
+ Background:
+ Given I log in as "admin"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Edit" "link"
+ And I choose "Categories" in the open action menu
+ And I press "Add category"
+ And I set the field "Name" to "Category 1"
+ And I set the field "Description" to "Category 1 description"
+ When I press "Save"
+ Then I should see "Category 1" in the "List of data categories" "table"
+ And I should see "Category 1 description" in the "Category 1" "table_row"
+
+ Scenario: Update a data category
+ Given I click on "Actions" "link" in the "Category 1" "table_row"
+ And I choose "Edit" in the open action menu
+ And I set the field "Name" to "Category 1 edited"
+ And I set the field "Description" to "Category 1 description edited"
+ When I press "Save changes"
+ Then I should see "Category 1 edited" in the "List of data categories" "table"
+ And I should see "Category 1 description edited" in the "List of data categories" "table"
+
+ Scenario: Delete a data category
+ Given I click on "Actions" "link" in the "Category 1" "table_row"
+ And I choose "Delete" in the open action menu
+ And I should see "Delete category"
+ And I should see "Are you sure you want to delete the category 'Category 1'?"
+ When I press "Delete"
+ Then I should not see "Category 1" in the "List of data categories" "table"
--- /dev/null
+@tool @tool_dataprivacy @javascript
+Feature: Manage data storage purposes
+ As the privacy officer
+ In order to manage the data registry
+ I need to be able to manage the data storage purposes for the data registry
+
+ Background:
+ Given I log in as "admin"
+ And I navigate to "Users > Privacy and policies > Data registry" in site administration
+ And I click on "Edit" "link"
+ And I choose "Purposes" in the open action menu
+ And I press "Add purpose"
+ And I set the field "Name" to "Purpose 1"
+ And I set the field "Description" to "Purpose 1 description"
+ And I click on ".form-autocomplete-downarrow" "css_element" in the "Lawful bases" "form_row"
+ And I click on "Contract (GDPR Art. 6.1(b))" "list_item"
+ And I click on "Legal obligation (GDPR Art 6.1(c))" "list_item"
+ And I press key "27" in the field "Lawful bases"
+ And I click on ".form-autocomplete-downarrow" "css_element" in the "Sensitive personal data processing reasons" "form_row"
+ And I click on "Explicit consent (GDPR Art. 9.2(a))" "list_item"
+ And I press key "27" in the field "Sensitive personal data processing reasons"
+ And I set the field "retentionperiodnumber" to "2"
+ When I press "Save"
+ Then I should see "Purpose 1" in the "List of data purposes" "table"
+ And I should see "Contract (GDPR Art. 6.1(b))" in the "Purpose 1" "table_row"
+ And I should see "Legal obligation (GDPR Art 6.1(c))" in the "Purpose 1" "table_row"
+ And I should see "Explicit consent (GDPR Art. 9.2(a))" in the "Purpose 1" "table_row"
+ And I should see "2 years" in the "Purpose 1" "table_row"
+ And I should see "No" in the "Purpose 1" "table_row"
+
+ Scenario: Update a data storage purpose
+ Given I click on "Actions" "link" in the "Purpose 1" "table_row"
+ And I choose "Edit" in the open action menu
+ And I set the field "Name" to "Purpose 1 edited"
+ And I set the field "Description" to "Purpose 1 description edited"
+ And I click on "Legal obligation (GDPR Art 6.1(c))" "text" in the ".form-autocomplete-selection" "css_element"
+ And I click on ".form-autocomplete-downarrow" "css_element" in the "Lawful bases" "form_row"
+ And I click on "Vital interests (GDPR Art. 6.1(d))" "list_item"
+ And I press key "27" in the field "Lawful bases"
+ And I set the field "retentionperiodnumber" to "3"
+ And I click on "protected" "checkbox"
+ When I press "Save changes"
+ Then I should see "Purpose 1 edited" in the "List of data purposes" "table"
+ And I should see "Purpose 1 description edited" in the "Purpose 1 edited" "table_row"
+ And I should see "Vital interests (GDPR Art. 6.1(d))" in the "Purpose 1 edited" "table_row"
+ And I should see "3 years" in the "Purpose 1 edited" "table_row"
+ But I should not see "Legal obligation (GDPR Art 6.1(c))" in the "Purpose 1 edited" "table_row"
+ And I should not see "No" in the "Purpose 1 edited" "table_row"
+
+ Scenario: Delete a data storage purpose
+ Given I click on "Actions" "link" in the "Purpose 1" "table_row"
+ And I choose "Delete" in the open action menu
+ And I should see "Delete purpose"
+ And I should see "Are you sure you want to delete the purpose 'Purpose 1'?"
+ When I press "Delete"
+ Then I should not see "Purpose 1" in the "List of data purposes" "table"
--- /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/>.
+
+/**
+ * Parent class for tests which need data privacy functionality.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Michael Hawkins
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Parent class for tests which need data privacy functionality.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Michael Hawkins
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class data_privacy_testcase extends advanced_testcase {
+
+ /**
+ * Assign one or more user IDs as site DPO
+ *
+ * @param stdClass|array $users User ID or array of user IDs to be assigned as site DPO
+ * @return void
+ */
+ protected function assign_site_dpo($users) {
+ global $DB;
+ $this->resetAfterTest();
+
+ if (!is_array($users)) {
+ $users = array($users);
+ }
+
+ $context = context_system::instance();
+
+ // Give the manager role with the capability to manage data requests.
+ $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
+ assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
+
+ // Assign user(s) as manager.
+ foreach ($users as $user) {
+ role_assign($managerroleid, $user->id, $context->id);
+ }
+
+ // Only map the manager role to the DPO role.
+ set_config('dporoles', $managerroleid, 'tool_dataprivacy');
+ }
+}
--- /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/>.
+
+/**
+ * Expired data requests tests.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Michael Hawkins
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use tool_dataprivacy\api;
+use tool_dataprivacy\data_request;
+
+defined('MOODLE_INTERNAL') || die();
+global $CFG;
+
+require_once('data_privacy_testcase.php');
+
+/**
+ * Expired data requests tests.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Michael Hawkins
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_dataprivacy_expired_data_requests_testcase extends data_privacy_testcase {
+
+ /**
+ * Test tearDown.
+ */
+ public function tearDown() {
+ \core_privacy\local\request\writer::reset();
+ }
+
+ /**
+ * Test finding and deleting expired data requests
+ */
+ public function test_data_request_expiry() {
+ global $DB;
+ $this->resetAfterTest();
+ \core_privacy\local\request\writer::setup_real_writer_instance();
+
+ // Set up test users.
+ $this->setAdminUser();
+ $studentuser = $this->getDataGenerator()->create_user();
+ $studentusercontext = context_user::instance($studentuser->id);
+
+ $dpouser = $this->getDataGenerator()->create_user();
+ $this->assign_site_dpo($dpouser);
+
+ // Set request expiry to 5 minutes.
+ set_config('privacyrequestexpiry', 300, 'tool_dataprivacy');
+
+ // Create and approve data request.
+ $this->setUser($studentuser->id);
+ $datarequest = api::create_data_request($studentuser->id, api::DATAREQUEST_TYPE_EXPORT);
+ $this->setAdminUser();
+ ob_start();
+ $this->runAdhocTasks('\tool_dataprivacy\task\initiate_data_request_task');
+ $requestid = $datarequest->get('id');
+ $this->setUser($dpouser->id);
+ api::approve_data_request($requestid);
+ $this->setAdminUser();
+ $this->runAdhocTasks('\tool_dataprivacy\task\process_data_request_task');
+ ob_end_clean();
+
+ // Confirm approved and exported.
+ $request = new data_request($requestid);
+ $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
+ $fileconditions = array(
+ 'userid' => $studentuser->id,
+ 'component' => 'tool_dataprivacy',
+ 'filearea' => 'export',
+ 'itemid' => $requestid,
+ 'contextid' => $studentusercontext->id,
+ );
+ $this->assertEquals(2, $DB->count_records('files', $fileconditions));
+
+ // Run expiry deletion - should not affect test export.
+ $expiredrequests = data_request::get_expired_requests();
+ $this->assertEquals(0, count($expiredrequests));
+ data_request::expire($expiredrequests);
+
+ // Confirm test export was not deleted.
+ $request = new data_request($requestid);
+ $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
+ $this->assertEquals(2, $DB->count_records('files', $fileconditions));
+
+ // Change request expiry to 1 second and allow it to elapse.
+ set_config('privacyrequestexpiry', 1, 'tool_dataprivacy');
+ $this->waitForSecond();
+
+ // Re-run expiry deletion, confirm the request expires and export is deleted.
+ $expiredrequests = data_request::get_expired_requests();
+ $this->assertEquals(1, count($expiredrequests));
+ data_request::expire($expiredrequests);
+
+ $request = new data_request($requestid);
+ $this->assertEquals(api::DATAREQUEST_STATUS_EXPIRED, $request->get('status'));
+ $this->assertEquals(0, $DB->count_records('files', $fileconditions));
+ }
+
+
+ /**
+ * Test for \tool_dataprivacy\data_request::is_expired()
+ * Tests for the expected request status to protect from false positive/negative,
+ * then tests is_expired() is returning the expected response.
+ */
+ public function test_is_expired() {
+ $this->resetAfterTest();
+ \core_privacy\local\request\writer::setup_real_writer_instance();
+
+ // Set request expiry beyond this test.
+ set_config('privacyrequestexpiry', 20, 'tool_dataprivacy');
+
+ $admin = get_admin();
+ $this->setAdminUser();
+
+ // Create export request.
+ $datarequest = api::create_data_request($admin->id, api::DATAREQUEST_TYPE_EXPORT);
+ $requestid = $datarequest->get('id');
+
+ // Approve the request.
+ ob_start();
+ $this->runAdhocTasks('\tool_dataprivacy\task\initiate_data_request_task');
+ $this->setAdminUser();
+ api::approve_data_request($requestid);
+ $this->runAdhocTasks('\tool_dataprivacy\task\process_data_request_task');
+ ob_end_clean();
+
+ // Test Download ready (not expired) response.
+ $request = new data_request($requestid);
+ $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
+ $result = data_request::is_expired($request);
+ $this->assertFalse($result);
+
+ // Let request expiry time lapse.
+ set_config('privacyrequestexpiry', 1, 'tool_dataprivacy');
+ $this->waitForSecond();
+
+ // Test Download ready (time expired) response.
+ $request = new data_request($requestid);
+ $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
+ $result = data_request::is_expired($request);
+ $this->assertTrue($result);
+
+ // Run the expiry task to properly expire the request.
+ ob_start();
+ $task = \core\task\manager::get_scheduled_task('\tool_dataprivacy\task\delete_expired_requests');
+ $task->execute();
+ ob_end_clean();
+
+ // Test Expired response status response.
+ $request = new data_request($requestid);
+ $this->assertEquals(api::DATAREQUEST_STATUS_EXPIRED, $request->get('status'));
+ $result = data_request::is_expired($request);
+ $this->assertTrue($result);
+ }
+}
*/
defined('MOODLE_INTERNAL') || die();
+require_once('data_privacy_testcase.php');
/**
* API tests.
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class tool_dataprivacy_manager_observer_testcase extends advanced_testcase {
-
- /**
- * Helper to set andn return two users who are DPOs.
- */
- protected function setup_site_dpos() {
- global $DB;
- $this->resetAfterTest();
-
- $generator = new testing_data_generator();
- $u1 = $this->getDataGenerator()->create_user();
- $u2 = $this->getDataGenerator()->create_user();
-
- $context = context_system::instance();
-
- // Give the manager role with the capability to manage data requests.
- $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
- assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
-
- // Assign both users as manager.
- role_assign($managerroleid, $u1->id, $context->id);
- role_assign($managerroleid, $u2->id, $context->id);
-
- // Only map the manager role to the DPO role.
- set_config('dporoles', $managerroleid, 'tool_dataprivacy');
-
- return \tool_dataprivacy\api::get_site_dpos();
- }
-
+class tool_dataprivacy_manager_observer_testcase extends data_privacy_testcase {
/**
* Ensure that when users are configured as DPO, they are sent an message upon failure.
*/
// Create another user who is not a DPO.
$this->getDataGenerator()->create_user();
- // Create the DPOs.
- $dpos = $this->setup_site_dpos();
+ // Create two DPOs.
+ $dpo1 = $this->getDataGenerator()->create_user();
+ $dpo2 = $this->getDataGenerator()->create_user();
+ $this->assign_site_dpo(array($dpo1, $dpo2));
+ $dpos = \tool_dataprivacy\api::get_site_dpos();
$observer = new \tool_dataprivacy\manager_observer();
defined('MOODLE_INTERNAL') || die;
-$plugin->version = 2018051405;
+$plugin->version = 2018082100;
$plugin->requires = 2018050800; // Moodle 3.5dev (Build 2018031600) and upwards.
$plugin->component = 'tool_dataprivacy';
Scenario: Add a new file type
Given I log in as "admin"
- And I navigate to "File types" node in "Site administration > Server"
+ And I navigate to "Server > File types" in site administration
And I press "Add"
# Try setting all the form fields, not just the optional ones.
And I set the following fields to these values:
Scenario: Update an existing file type
Given I log in as "admin"
- And I navigate to "File types" node in "Site administration > Server"
+ And I navigate to "Server > File types" in site administration
When I click on "Edit 7z" "link"
And I set the following fields to these values:
| Extension | doc |
Scenario: Change the text option (was buggy)
Given I log in as "admin"
- And I navigate to "File types" node in "Site administration > Server"
+ And I navigate to "Server > File types" in site administration
When I click on "Edit 7z" "link"
And I set the following fields to these values:
| Description type | Custom description specified in this form |
Scenario: Try to select a text option without entering a value.
Given I log in as "admin"
- And I navigate to "File types" node in "Site administration > Server"
+ And I navigate to "Server > File types" in site administration
When I click on "Edit dmg" "link"
And I set the field "Description type" to "Custom description"
And I press "Save changes"
Scenario: Delete an existing file type
Given I log in as "admin"
- And I navigate to "File types" node in "Site administration > Server"
+ And I navigate to "Server > File types" in site administration
When I click on "Delete 7z" "link"
Then I should see "Are you absolutely sure you want to remove .7z?"
And I press "Yes"
Scenario: Delete a custom file type
Given I log in as "admin"
- And I navigate to "File types" node in "Site administration > Server"
+ And I navigate to "Server > File types" in site administration
And I press "Add"
And I set the following fields to these values:
| Extension | frog |
Scenario: Revert changes to deleted file type
Given I log in as "admin"
- And I navigate to "File types" node in "Site administration > Server"
+ And I navigate to "Server > File types" in site administration
When I click on "Delete 7z" "link"
And I press "Yes"
And I follow "Restore 7z to Moodle defaults"
Scenario: Revert changes to updated file type
Given I log in as "admin"
- And I navigate to "File types" node in "Site administration > Server"
+ And I navigate to "Server > File types" in site administration
And I click on "Edit 7z" "link"
And I set the following fields to these values:
| Type groups | document |
| fullname | shortname |
| Course 1 | C1 |
And I log in as "admin"
- And I navigate to "File types" node in "Site administration > Server"
+ And I navigate to "Server > File types" in site administration
And I press "Add"
And I set the following fields to these values:
| Extension | frog |
@javascript
Scenario: Go to the HTTPS replace report screen. Make sure broken domains are reported.
- When I navigate to "HTTP security" node in "Site administration > Security"
+ When I navigate to "Security > HTTP security" in site administration
And I follow "HTTPS conversion tool"
And I press "Continue"
Then I should see "intentionally.unavailable"
@javascript
Scenario: Use the find and replace tool.
- When I navigate to "HTTP security" node in "Site administration > Security"
+ When I navigate to "Security > HTTP security" in site administration
And I follow "HTTPS conversion tool"
And I press "Continue"
And I set the field "I understand the risks of this operation" to "1"
Scenario: Install language pack
Given I log in as "admin"
- And I navigate to "Language packs" node in "Site administration > Language"
+ And I navigate to "Language > Language packs" in site administration
When I set the field "Available language packs" to "en_ar"
And I press "Install selected language pack(s)"
Then I should see "Language pack 'en_ar' was successfully installed"
And the "Installed language packs" select box should contain "en_ar"
- And I navigate to "Live logs" node in "Site administration > Reports"
+ And I navigate to "Reports > Live logs" in site administration
And I should see "The language pack 'en_ar' was installed."
And I log out
Scenario: Update language pack
Given outdated langpack 'en_ar' is installed
And I log in as "admin"
- And I navigate to "Language packs" node in "Site administration > Language"
+ And I navigate to "Language > Language packs" in site administration
When I press "Update all installed language packs"
Then I should see "Language pack 'en_ar' was successfully updated"
And I should see "Language pack update completed"
- And I navigate to "Live logs" node in "Site administration > Reports"
+ And I navigate to "Reports > Live logs" in site administration
And I should see "The language pack 'en_ar' was updated."
And I log out
Scenario: Try to uninstall language pack
Given I log in as "admin"
- And I navigate to "Language packs" node in "Site administration > Language"
+ And I navigate to "Language > Language packs" in site administration
And I set the field "Available language packs" to "en_ar"
And I press "Install selected language pack(s)"
When I set the field "Installed language packs" to "en_ar"
Then I should see "Language pack 'en_ar' was uninstalled"
And the "Installed language packs" select box should not contain "en_ar"
And the "Available language packs" select box should contain "en_ar"
- And I navigate to "Live logs" node in "Site administration > Reports"
+ And I navigate to "Reports > Live logs" in site administration
And I should see "The language pack 'en_ar' was removed."
And I should see "Language pack uninstalled"
And I log out
Scenario: Try to uninstall English language pack
Given I log in as "admin"
- And I navigate to "Language packs" node in "Site administration > Language"
+ And I navigate to "Language > Language packs" in site administration
When I set the field "Installed language packs" to "en"
And I press "Uninstall selected language pack(s)"
Then I should see "The English language pack cannot be uninstalled."
- And I navigate to "Live logs" node in "Site administration > Reports"
+ And I navigate to "Reports > Live logs" in site administration
And I should not see "Language pack uninstalled"
And I log out
Scenario: Tool is disabled by default.
Given I log in as "admin"
- When I navigate to "Event monitoring rules" node in "Site administration > Reports"
+ When I navigate to "Reports > Event monitoring rules" in site administration
Then I should see "Event monitoring is currently disabled"
And I should see "Enable"
And I should not see "Add a new rule"
| user | course | role |
| teacher1 | C1 | editingteacher |
And I log in as "admin"
- And I navigate to "Event monitoring rules" node in "Site administration > Reports"
+ And I navigate to "Reports > Event monitoring rules" in site administration
And I click on "Enable" "link"
And I am on "Course 1" course homepage
- And I navigate to "Event monitoring rules" node in "Course administration > Reports"
+ And I navigate to "Reports > Event monitoring rules" in current page administration
And I press "Add a new rule"
And I set the following fields to these values:
| name | New rule course level |
| minutes | 1 |
| Notification message | The forum post was created. {modulelink} |
And I press "Save changes"
- And I navigate to "Event monitoring rules" node in "Site administration > Reports"
+ And I navigate to "Reports > Event monitoring rules" in site administration
And I press "Add a new rule"
And I set the following fields to these values:
| name | New rule site level |
Scenario: Add a rule on course level
Given I log in as "teacher1"
And I am on "Course 1" course homepage
- And I navigate to "Event monitoring rules" node in "Course administration > Reports"
+ And I navigate to "Reports > Event monitoring rules" in current page administration
When I press "Add a new rule"
And I set the following fields to these values:
| name | New rule |
Scenario: Delete a rule on course level
Given I log in as "teacher1"
And I am on "Course 1" course homepage
- And I navigate to "Event monitoring rules" node in "Course administration > Reports"
+ And I navigate to "Reports > Event monitoring rules" in current page administration
When I click on "Delete rule" "link"
Then I should see "Are you sure you want to delete the rule \"New rule course level\"?"
And I press "Continue"
Scenario: Edit a rule on course level
Given I log in as "teacher1"
And I am on "Course 1" course homepage
- And I navigate to "Event monitoring rules" node in "Course administration > Reports"
+ And I navigate to "Reports > Event monitoring rules" in current page administration
When I click on "Edit rule" "link"
And I set the following fields to these values:
| name | New rule quiz |
Scenario: Duplicate a rule on course level
Given I log in as "teacher1"
And I am on "Course 1" course homepage
- And I navigate to "Event monitoring rules" node in "Course administration > Reports"
+ And I navigate to "Reports > Event monitoring rules" in current page administration
When I click on "Duplicate rule" "link" in the "New rule course level" "table_row"
Then I should see "Rule successfully duplicated"
And "#toolmonitorrules_r1" "css_element" should appear before "#toolmonitorrules_r2" "css_element"
Scenario: Add a rule on site level
Given I log in as "admin"
- And I navigate to "Event monitoring rules" node in "Site administration > Reports"
+ And I navigate to "Reports > Event monitoring rules" in site administration
When I press "Add a new rule"
And I set the following fields to these values:
| name | New rule |
Scenario: Delete a rule on site level
Given I log in as "admin"
- And I navigate to "Event monitoring rules" node in "Site administration > Reports"
+ And I navigate to "Reports > Event monitoring rules" in site administration
When I click on "Delete rule" "link"
Then I should see "Are you sure you want to delete the rule \"New rule site level\"?"
And I press "Continue"
Scenario: Edit a rule on site level
Given I log in as "admin"
- And I navigate to "Event monitoring rules" node in "Site administration > Reports"
+ And I navigate to "Reports > Event monitoring rules" in site administration
When I click on "Edit rule" "link"
And I set the following fields to these values:
| name | New Rule Quiz |
Scenario: Duplicate a rule on site level
Given I log in as "teacher1"
And I am on "Course 1" course homepage
- And I navigate to "Event monitoring rules" node in "Course administration > Reports"
+ And I navigate to "Reports > Event monitoring rules" in current page administration
When I click on "Duplicate rule" "link" in the "New rule site level" "table_row"
Then I should see "Rule successfully duplicated"
And "#toolmonitorrules_r2" "css_element" should appear after "#toolmonitorrules_r1" "css_element"
| teacher2 | C1 | teacher |
| teacher2 | C2 | editingteacher |
And I log in as "admin"
- And I navigate to "Event monitoring rules" node in "Site administration > Reports"
+ And I navigate to "Reports > Event monitoring rules" in site administration
And I click on "Enable" "link"
And I am on "Course 1" course homepage
- And I navigate to "Event monitoring rules" node in "Course administration > Reports"
+ And I navigate to "Reports > Event monitoring rules" in current page administration
And I press "Add a new rule"
And I set the following fields to these values:
| name | New rule course level |
| minutes | 1 |
| Notification message | The course was viewed. {modulelink} |
And I press "Save changes"
- And I navigate to "Event monitoring rules" node in "Site administration > Reports"
+ And I navigate to "Reports > Event monitoring rules" in site administration
And I press "Add a new rule"
And I set the following fields to these values:
| name | New rule site level |
| minutes | 1 |
| Notification message | The course was viewed. {modulelink} |
And I press "Save changes"
- And I navigate to "Define roles" node in "Site administration > Users > Permissions"
+ And I navigate to "Users > Permissions > Define roles" in site administration
And I follow "Non-editing teacher"
And I press "Edit"
And I click on "tool/monitor:managerules" "checkbox"
And I log out
# Create new policy document.
And I log in as "admin"
- And I navigate to "Manage policies" node in "Site administration > Users > Privacy and policies"
+ And I navigate to "Users > Privacy and policies > Manage policies" in site administration
And I should see "Policies and agreements"
And I should see "New policy"
And I follow "New policy"
And I log out
# Create new version of the policy document.
And I log in as "admin"
- And I navigate to "Manage policies" node in "Site administration > Users > Privacy and policies"
+ And I navigate to "Users > Privacy and policies > Manage policies" in site administration
When I follow "Actions"
Then I should see "View"
And I should see "Edit"
And I am on "Course 1" course homepage with editing mode on
And I delete "Quiz 1" activity
And I run all adhoc tasks
- And I navigate to "Recycle bin" node in "Course administration"
+ And I navigate to "Recycle bin" in current page administration
And I should see "Quiz 1"
And I click on "Restore" "link" in the "region-main" "region"
And I log out
| Assignment name | Test assign |
| Description | Test |
And I delete "Test assign" activity
- When I navigate to "Recycle bin" node in "Course administration"
+ When I navigate to "Recycle bin" in current page administration
Then I should see "Test assign"
And I should see "Contents will be permanently deleted after 7 days"
And I click on "Restore" "link" in the "region-main" "region"
And I go to the courses management page
And I should see "Course 2" in the "#course-listing" "css_element"
And I am on "Course 2" course homepage
- And I navigate to "Groups" node in "Course administration > Users"
+ And I navigate to "Users > Groups" in current page administration
And I follow "Overview"
And "Student 1" "text" should exist in the "Group A" "table_row"
And "Student 2" "text" should exist in the "Group A" "table_row"
| Description | Test |
And I delete "Test assign" activity
And I run all adhoc tasks
- And I navigate to "Recycle bin" node in "Course administration"
+ And I navigate to "Recycle bin" in current page administration
When I click on "Delete" "link"
Then I should see "Are you sure you want to delete the selected item from the recycle bin?"
And I press "Cancel"
And I delete "Test assign 1" activity
And I delete "Test assign 2" activity
And I run all adhoc tasks
- And I navigate to "Recycle bin" node in "Course administration"
+ And I navigate to "Recycle bin" in current page administration
And I should see "Test assign 1"
And I should see "Test assign 2"
When I click on "Delete all" "link"
Background:
Given the scheduled task "\core\task\send_new_user_passwords_task" has a fail delay of "60" seconds
And I log in as "admin"
- And I navigate to "Scheduled tasks" node in "Site administration > Server"
+ And I navigate to "Server > Scheduled tasks" in site administration
Scenario: Clear fail delay
When I click on "Clear" "text" in the "Send new user passwords" "table_row"
Background:
Given I log in as "admin"
- And I navigate to "Scheduled tasks" node in "Site administration > Server"
+ And I navigate to "Server > Scheduled tasks" in site administration
Scenario: Disable scheduled task
When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row"
Scenario: Run a task
Given I log in as "admin"
- When I navigate to "Scheduled tasks" node in "Site administration > Server"
+ When I navigate to "Server > Scheduled tasks" in site administration
Then I should see "Never" in the "Log table cleanup" "table_row"
And I click on "Run now" "text" in the "Log table cleanup" "table_row"
Scenario: Cancel running a task
Given I log in as "admin"
- When I navigate to "Scheduled tasks" node in "Site administration > Server"
+ When I navigate to "Server > Scheduled tasks" in site administration
And I click on "Run now" "text" in the "Log table cleanup" "table_row"
And I press "Cancel"
# Confirm we're back on the scheduled tasks page by looking for the table.
Given the following config values are set as admin:
| enablerunnow | 0 | tool_task |
When I log in as "admin"
- And I navigate to "Scheduled tasks" node in "Site administration > Server"
+ And I navigate to "Server > Scheduled tasks" in site administration
Then I should not see "Run now"
| fullname | shortname | category |
| First course | C1 | 0 |
And I log in as "admin"
- And I navigate to "Upload courses" node in "Site administration > Courses"
+ And I navigate to "Courses > Upload courses" in site administration
@javascript
Scenario: Creation of unexisting courses
| fullname | shortname | category |
| Some random name | C1 | 0 |
And I log in as "admin"
- And I navigate to "Upload courses" node in "Site administration > Courses"
+ And I navigate to "Courses > Upload courses" in site administration
@javascript
Scenario: Updating a course fullname
| Section 1 | math102 | S1 |
| Section 3 | math102 | S3 |
And I log in as "admin"
- And I navigate to "Upload users" node in "Site administration > Users > Accounts"
+ And I navigate to "Users > Accounts >Upload users" in site administration
When I upload "lib/tests/fixtures/upload_users.csv" file to "File" filemanager
And I press "Upload users"
Then I should see "Upload users preview"
| Section 1 | math102 | S1 |
| Section 3 | math102 | S3 |
And I log in as "admin"
- And I navigate to "Upload users" node in "Site administration > Users > Accounts"
+ And I navigate to "Users > Accounts > Upload users" in site administration
When I upload "lib/tests/fixtures/upload_users.csv" file to "File" filemanager
And I press "Upload users"
And I set the following fields to these values:
Scenario: Upload users with custom profile fields
# Create user profile field.
Given I log in as "admin"
- And I navigate to "User profile fields" node in "Site administration > Users > Accounts"
+ And I navigate to "Users > Accounts > User profile fields" in site administration
And I set the field "datatype" to "Text area"
And I set the following fields to these values:
| Short name | superfield |
| Name | Super field |
And I click on "Save changes" "button"
# Upload users.
- When I navigate to "Upload users" node in "Site administration > Users > Accounts"
+ When I navigate to "Users > Accounts > Upload users" in site administration
And I upload "lib/tests/fixtures/upload_users_profile.csv" file to "File" filemanager
And I press "Upload users"
And I press "Upload users"
# Check that users were created and the superfield is filled.
- And I navigate to "Browse list of users" node in "Site administration > Users > Accounts"
+ And I navigate to "Users > Accounts > Browse list of users" in site administration
And I follow "Tom Jones"
And I should see "Super field"
And I should see "The big guy"
* @Given /^I open the User tour settings page$/
*/
public function i_open_the_user_tour_settings_page() {
- $this->execute('behat_navigation::i_navigate_to_node_in', [
- get_string('usertours', 'tool_usertours'),
- implode(' > ', [
- get_string('administrationsite', 'moodle'),
- get_string('appearance', 'admin'),
- ])
- ]);
+ $this->execute('behat_navigation::i_navigate_to_in_site_administration',
+ get_string('appearance', 'admin') . ' > ' .
+ get_string('usertours', 'tool_usertours')
+ );
}
}
Background:
Given I log in as "admin"
- And I navigate to "Privacy settings" node in "Site administration > Users > Privacy and policies"
+ And I navigate to "Users > Privacy and policies > Privacy settings" in site administration
Scenario: Admin provides valid value for 'Age of digital consent'.
Given I set the field "s__agedigitalconsentmap" to multiline:
# Set up course.
Given I log in as "teacher1"
And I am on "Course 1" course homepage
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I expand all fieldsets
And I set the field "Enable completion tracking" to "Yes"
And I press "Save and display"
Scenario: Test with custom user profile field
# Add custom field.
Given I log in as "admin"
- And I navigate to "User profile fields" node in "Site administration > Users > Accounts"
+ And I navigate to "Users > Accounts > User profile fields" in site administration
And I set the field "datatype" to "Text input"
And I set the following fields to these values:
| Short name | superfield |
And I click on "Save changes" "button"
# Set field value for user.
- And I navigate to "Browse list of users" node in "Site administration > Users > Accounts"
+ And I navigate to "Users > Accounts > Browse list of users" in site administration
And I click on ".icon[title=Edit]" "css_element" in the "s@example.com" "table_row"
And I expand all fieldsets
And I set the field "Super field" to "Bananaman"
Scenario: Include groups and groupings when importing a course to another course
Given I import "Course 1" course into "Course 2" course using this options:
| Initial | Include groups and groupings | 1 |
- When I navigate to "Groups" node in "Course administration > Users"
+ When I navigate to "Users > Groups" in current page administration
Then I should see "Group 1"
And I should see "Group 2"
And I follow "Groupings"
Scenario: Do not include groups and groupings when importing a course to another course
Given I import "Course 1" course into "Course 2" course using this options:
| Initial | Include groups and groupings | 0 |
- When I navigate to "Groups" node in "Course administration > Users"
+ When I navigate to "Users > Groups" in current page administration
Then I should not see "Group 1"
And I should not see "Group 2"
And I follow "Groupings"
And I should see "Test forum name"
And I should see "Topic 15"
And I should not see "Topic 16"
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I expand all fieldsets
And the field "id_format" matches value "Topics format"
And I press "Cancel"
And I add a "Forum" to section "1" and I fill the form with:
| Forum name | Test forum post backup name |
| Description | Test forum post backup description |
- And I navigate to "Restore" node in "Course administration"
+ And I navigate to "Restore" in current page administration
And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
| Schema | Section 3 | 0 |
Then I should see "Course 1"
When I restore "test_backup.mbz" backup into a new course using this options:
Then I should see "Topic 1"
And I should see "Test forum name"
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I expand all fieldsets
And the field "id_format" matches value "Topics format"
And I set the following fields to these values:
And I press "Save and display"
And I should see "1 January - 7 January"
And I should see "Test forum name"
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I expand all fieldsets
And the field "id_format" matches value "Weekly format"
And I set the following fields to these values:
| id_format | Social format |
And I press "Save and display"
And I should see "An open forum for chatting about anything you want to"
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I expand all fieldsets
And the field "id_format" matches value "Social format"
And I press "Cancel"
| Confirmation | Filename | test_backup.mbz |
And I restore "test_backup.mbz" backup into "Course 2" course using this options:
| Schema | Overwrite course configuration | Yes |
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I expand all fieldsets
Then the field "id_format" matches value "Topics format"
And the field "Course layout" matches value "Show one section per page"
| Confirmation | Filename | test_backup.mbz |
And I restore "test_backup.mbz" backup into "Course 2" course using this options:
| Schema | Overwrite course configuration | No |
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I expand all fieldsets
Then the field "id_format" matches value "Topics format"
And the field "Course short name" matches value "C2"
| Initial | Include enrolled users | 0 |
| Confirmation | Filename | test_backup.mbz |
And I am on "Course 2" course homepage
- And I navigate to "Restore" node in "Course administration"
+ And I navigate to "Restore" in current page administration
And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
| Schema | Overwrite course configuration | Yes |
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I expand all fieldsets
Then the field "id_format" matches value "Topics format"
And the field "Course layout" matches value "Show one section per page"
| Initial | Include enrolled users | 0 |
| Confirmation | Filename | test_backup.mbz |
And I am on "Course 2" course homepage
- And I navigate to "Restore" node in "Course administration"
+ And I navigate to "Restore" in current page administration
And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
| Schema | Overwrite course configuration | No |
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I expand all fieldsets
Then the field "id_format" matches value "Topics format"
And the field "Course short name" matches value "C2"
| Initial | Include enrolled users | 0 |
| Confirmation | Filename | test_backup.mbz |
And I am on "Course 4" course homepage
- And I navigate to "Restore" node in "Course administration"
+ And I navigate to "Restore" in current page administration
And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
| Schema | Overwrite course configuration | No |
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I expand all fieldsets
Then the field "id_format" matches value "Topics format"
And the field "Course short name" matches value "C4"
@javascript
Scenario: Restore a backup with user data with site config for including users set to 0
- Given I navigate to "General restore defaults" node in "Site administration > Courses > Backups"
+ Given I navigate to "Courses > Backups > General restore defaults" in site administration
And I set the field "s_restore_restore_general_users" to ""
And I press "Save changes"
And I am on "Course 1" course homepage
- And I navigate to "Restore" node in "Course administration"
+ And I navigate to "Restore" in current page administration
# "User data" marks the user data field for the section
# "-" marks the user data field for the data activity
And I restore "test_backup.mbz" backup into a new course using this options:
@javascript
Scenario: Restore a backup with user data with local and site config config for including users set to 0
- Given I navigate to "General restore defaults" node in "Site administration > Courses > Backups"
+ Given I navigate to "Courses > Backups > General restore defaults" in site administration
And I set the field "s_restore_restore_general_users" to ""
And I press "Save changes"
And I am on "Course 1" course homepage
- And I navigate to "Restore" node in "Course administration"
+ And I navigate to "Restore" in current page administration
When I restore "test_backup.mbz" backup into a new course using this options:
| Settings | Include enrolled users | 0 |
Then I should see "Test database name"
@javascript
Scenario: Setting badges settings
- Given I navigate to "Badges settings" node in "Site administration > Badges"
+ Given I navigate to "Badges > Badges settings" in site administration
And I set the field "Default badge issuer name" to "Test Badge Site"
And I set the field "Default badge issuer contact details" to "testuser@example.com"
And I press "Save changes"
And I press "Customise this page"
# TODO MDL-57120 site "Badges" link not accessible without navigation block.
And I add the "Navigation" block if not present
- Given I navigate to "Site badges" node in "Site pages"
+ And I click on "Site pages" "list_item" in the "Navigation" "block"
+ Given I click on "Site badges" "link" in the "Navigation" "block"
Then I should see "There are no badges available."
@javascript @_file_upload
Scenario: Add a badge
- Given I navigate to "Add a new badge" node in "Site administration > Badges"
+ Given I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Test badge with 'apostrophe' and other friends (<>&@#) |
| Description | Test badge description |
And I log in as "teacher1"
And I am on "Course 1" course homepage
# Create course badge 1.
- And I navigate to "Add a new badge" node in "Course administration > Badges"
+ And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge 1 |
And I press "Enable access"
And I press "Continue"
# Badge #2
- And I navigate to "Add a new badge" node in "Course administration > Badges"
+ And I am on "Course 1" course homepage
+ And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge 2 |
@javascript
Scenario: Award profile badge
Given I log in as "admin"
- And I navigate to "Add a new badge" node in "Site administration > Badges"
+ And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Profile Badge |
| Description | Test badge description |
| teacher | teacher | 1 | teacher1@example.com |
| student | student | 1 | student1@example.com |
And I log in as "admin"
- And I navigate to "Add a new badge" node in "Site administration > Badges"
+ And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
| student2 | C1 | student |
And I log in as "teacher1"
And I am on "Course 1" course homepage
- And I navigate to "Add a new badge" node in "Course administration > Badges"
+ And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| student1 | C1 | student |
And I log in as "teacher1"
And I am on "Course 1" course homepage
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| Enable completion tracking | Yes |
And I press "Save and display"
| Description | Submit your online text |
| id_completion | 1 |
And I am on "Course 1" course homepage
- And I navigate to "Add a new badge" node in "Course administration > Badges"
+ And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| student1 | C1 | student |
And I log in as "teacher1"
And I am on "Course 1" course homepage
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| Enable completion tracking | Yes |
And I press "Save and display"
| Description | Submit your online text |
| assignsubmission_onlinetext_enabled | 1 |
| id_completion | 1 |
- And I navigate to "Course completion" node in "Course administration"
+ And I navigate to "Course completion" in current page administration
And I set the field "id_overall_aggregation" to "2"
And I click on "Condition: Activity completion" "link"
And I set the field "Assignment - Test assignment name" to "1"
And I press "Save changes"
And I am on "Course 1" course homepage
- And I navigate to "Add a new badge" node in "Course administration > Badges"
+ And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
And I log in as "teacher1"
And I am on "Course 1" course homepage
# Create course badge 1.
- And I navigate to "Add a new badge" node in "Course administration > Badges"
+ And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge 1 |
And I follow "Recipients (1)"
Then I should see "Recipients (1)"
# Add course badge 2.
- And I navigate to "Add a new badge" node in "Course administration > Badges"
+ And I am on "Course 1" course homepage
+ And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge 2 |
| student2 | C1 | student |
And I log in as "teacher1"
And I am on "Course 1" course homepage
- And I navigate to "Add a new badge" node in "Course administration > Badges"
+ And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| user1 | CH1 |
| user2 | CH2 |
And I log in as "admin"
- And I navigate to "Add a new badge" node in "Site administration > Badges"
+ And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
| user2 | CH1 |
| user2 | CH3 |
And I log in as "admin"
- And I navigate to "Add a new badge" node in "Site administration > Badges"
+ And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
| user3 | CH2 |
| user3 | CH3 |
And I log in as "admin"
- And I navigate to "Add a new badge" node in "Site administration > Badges"
+ And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
| user1 | CH1 |
| user2 | CH2 |
And I log in as "admin"
- And I navigate to "Add a new badge" node in "Site administration > Badges"
+ And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
| user2 | CH2 |
| user3 | CH2 |
And I log in as "admin"
- And I navigate to "Add a new badge" node in "Site administration > Badges"
+ And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
| user2 | CH2 |
| user2 | CH2 |
And I log in as "admin"
- And I navigate to "Add a new badge" node in "Site administration > Badges"
+ And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
| user2 | CH1 |
| user3 | CH2 |
And I log in as "admin"
- And I navigate to "Add a new badge" node in "Site administration > Badges"
+ And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
| user1 | CH2 |
| user2 | CH2 |
And I log in as "admin"
- And I navigate to "Add a new badge" node in "Site administration > Badges"
+ And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge 1 |
| Description | Site badge description |
And I press "Enable access"
When I press "Continue"
And I should see "Recipients (1)"
- And I navigate to "Add a new badge" node in "Site administration > Badges"
+ And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge 2 |
| Description | Site badge description |
| user2 | Second | User | second@example.com |
| user3 | Third | User | third@example.com |
And I log in as "admin"
- And I navigate to "Add a new badge" node in "Site administration > Badges"
+ And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge 1 |
| Description | Site badge description |
And I press "Enable access"
When I press "Continue"
And I should see "Recipients (0)"
- And I navigate to "Add a new badge" node in "Site administration > Badges"
+ And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge 2 |
| Description | Site badge description |
And I press "Save"
And I press "Enable access"
And I press "Continue"
- Then I navigate to "Cohorts" node in "Site administration > Users > Accounts"
+ Then I navigate to "Users > Accounts >Cohorts" in site administration
And I add "First User (first@example.com)" user to "CH1" cohort members
And I add "First User (first@example.com)" user to "CH2" cohort members
And I add "Second User (second@example.com)" user to "CH2" cohort members
| username | firstname | lastname | email |
| user1 | First | User | first@example.com |
And I log in as "admin"
- And I navigate to "Add a new badge" node in "Site administration > Badges"
+ And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
Scenario: Check the default roles are visible
Given I log in as "manager1"
And I am on "Course 1" course homepage
- And I navigate to "Add a new badge" node in "Course administration > Badges"
+ And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
Scenario: Check hidden roles are not visible
Given I log in as "teacher1"
And I am on "Course 1" course homepage
- And I navigate to "Add a new badge" node in "Course administration > Badges"
+ And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
Background:
Given I log in as "admin"
- And I navigate to "Scheduled tasks" node in "Site administration > Server"
+ And I navigate to "Server > Scheduled tasks" in site administration
And I click on "Bookmark this page" "link" in the "Admin bookmarks" "block"
And I log out
# Test bookmark functionality using the "User profile fields" page as our bookmark.
Scenario: Admin page can be bookmarked
Given I log in as "admin"
- And I navigate to "User profile fields" node in "Site administration > Users > Accounts"
+ And I navigate to "Users > Accounts > User profile fields" in site administration
When I click on "Bookmark this page" "link" in the "Admin bookmarks" "block"
Then I should see "User profile fields" in the "Admin bookmarks" "block"
# See the existing bookmark is there too.
Scenario: Admin page can be accessed through bookmarks block
Given I log in as "admin"
- And I navigate to "Notifications" node in "Site administration"
+ And I navigate to "Notifications" in site administration
And I click on "Scheduled tasks" "link" in the "Admin bookmarks" "block"
# Verify that we are on the right page.
Then I should see "Scheduled tasks" in the "h1" "css_element"
Scenario: Admin page can be removed from bookmarks
Given I log in as "admin"
- And I navigate to "Notifications" node in "Site administration"
+ And I navigate to "Notifications" in site administration
And I click on "Scheduled tasks" "link" in the "Admin bookmarks" "block"
When I click on "Unbookmark this page" "link" in the "Admin bookmarks" "block"
Then I should see "Bookmark deleted"
And I log in as "teacher1"
And I am on "Course 1" course homepage
# Issue badge 1 of 2
- And I navigate to "Add a new badge" node in "Course administration > Badges"
+ And I navigate to "Badges > Add a new badge" in current page administration
And I set the following fields to these values:
| id_name | Badge 1 |
| id_description | Badge 1 |
And I set the field "potentialrecipients[]" to "Teacher 1 (teacher1@example.com)"
And I press "Award badge"
# Issue Badge 2 of 2
- And I navigate to "Add a new badge" node in "Course administration > Badges"
+ And I am on "Course 1" course homepage
+ And I navigate to "Badges > Add a new badge" in current page administration
And I set the following fields to these values:
| id_name | Badge 2 |
| id_description | Badge 2 |
And I log in as "teacher1"
And I am on "Course 1" course homepage
# Issue badge 1 of 2
- And I navigate to "Add a new badge" node in "Course administration > Badges"
+ And I navigate to "Badges > Add a new badge" in current page administration
And I set the following fields to these values:
| id_name | Badge 1 |
| id_description | Badge 1 |
| teacher1 | C1 | editingteacher |
And I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Latest badges" block
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
# Issue badge 1 of 2
- And I navigate to "Add a new badge" node in "Course administration > Badges"
+ And I navigate to "Badges > Add a new badge" in current page administration
And I set the following fields to these values:
| id_name | Badge 1 |
| id_description | Badge 1 |
| student1 | Student | 1 | student1@example.com | S1 |
And I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Blog menu" block
And I log out
| student1 | Student | 1 | student1@example.com | S1 |
And I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Recent blog entries" block
# TODO MDL-57120 site "Blogs" link not accessible without navigation block.
And I add the "Navigation" block if not present
Scenario: Students use the recent blog entries block to view blogs
Given I log in as "student1"
And I am on site homepage
- And I navigate to "Site blogs" node in "Site pages"
+ And I click on "Site blogs" "link" in the "Navigation" "block"
And I follow "Add a new entry"
When I set the following fields to these values:
| Entry title | S1 First Blog |
Scenario: Students only see a few entries in the recent blog entries block
Given I log in as "student1"
And I am on site homepage
- And I navigate to "Site blogs" node in "Site pages"
+ And I click on "Site blogs" "link" in the "Navigation" "block"
And I follow "Add a new entry"
# Blog 1 of 5
And I set the following fields to these values:
Then I log out
And I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I configure the "Recent blog entries" block
And I set the following fields to these values:
| id_config_numberofrecentblogentries | 2 |
| student2 | G2 |
When I log in as "teacher1"
And I am on "Course 1" course homepage
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| id_groupmode | Separate groups |
| id_groupmodeforce | Yes |
| student2 | G2 |
When I log in as "teacher1"
And I am on "Course 1" course homepage
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| id_groupmode | Separate groups |
| id_groupmodeforce | Yes |
| id_eventtype | Site |
| id_name | My Site Event |
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Upcoming events" block
And I log out
When I log in as "teacher1"
| teacher1 | Teacher | 1 | teacher1@example.com | T1 |
And I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Comments" block
And I follow "Show comments"
And I add "I'm a comment from admin" comment to comments block
Scenario: Add the block to a the course where completion is disabled
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| Enable completion tracking | No |
And I press "Save and display"
| Require view | 1 |
And I press "Save and return to course"
When I add the "Course completion status" block
- And I navigate to "Course completion" node in "Course administration"
+ And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the following fields to these values:
| Test page name | 1 |
| Require view | 1 |
And I press "Save and return to course"
And I add the "Course completion status" block
- And I navigate to "Course completion" node in "Course administration"
+ And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the following fields to these values:
| Test page name | 1 |
| Require view | 1 |
And I press "Save and return to course"
And I add the "Course completion status" block
- And I navigate to "Course completion" node in "Course administration"
+ And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the following fields to these values:
| Test page name | 1 |
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add the "Course completion status" block
- And I navigate to "Course completion" node in "Course administration"
+ And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the following fields to these values:
| Teacher | 1 |
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
- And I navigate to "Course completion" node in "Course administration > Reports"
+ And I navigate to "Reports > Course completion" in current page administration
And I follow "Click to mark user complete"
# Running completion task just after clicking sometimes fail, as record
# should be created before the task runs.
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add the "Course completion status" block
- And I navigate to "Course completion" node in "Course administration"
+ And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the following fields to these values:
| Teacher | 1 |
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
- And I navigate to "Course completion" node in "Course administration > Reports"
+ And I navigate to "Reports > Course completion" in current page administration
And I follow "Click to mark user complete"
And I log out
And I log in as "student1"
And I log out
And I log in as "teacher2"
And I am on "Course 1" course homepage
- And I navigate to "Course completion" node in "Course administration > Reports"
+ And I navigate to "Reports > Course completion" in current page administration
And I follow "Click to mark user complete"
# Running completion task just after clicking sometimes fail, as record
# should be created before the task runs.
And I am on "Course 1" course homepage with editing mode on
And I add the "Course completion status" block
And I add the "Self completion" block
- And I navigate to "Course completion" node in "Course administration"
+ And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the following fields to these values:
| id_criteria_self | 1 |
Scenario: Add the course list block on the frontpage and navigate to the course listing
Given I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Courses" block
And I log out
When I log in as "teacher1"
Scenario: Add the course list block on the frontpage page and navigate to another course
Given I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Courses" block
And I log out
When I log in as "teacher1"
Scenario: Add the course list block on the frontpage page and view as an admin
Given I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
When I add the "Courses" block
Then I should see "Miscellaneous" in the "Course categories" "block"
And I should see "Category 1" in the "Course categories" "block"
Scenario: Add the course list block on the frontpage page and view as a guest
Given I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Courses" block
And I log out
When I log in as "guest"
And I am on site homepage
And I turn editing mode on
And I add the "Course/site summary" block
- And I navigate to "Edit settings" node in "Front page settings"
+ And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| summary | Proved the summary block works! |
And I press "Save changes"
| testuser | testpass | Test | User | student1@example.com |
And I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Login" block
Scenario: Login block visible to non-logged in users
| teacher1 | Teacher | One | teacher1@example.com | T1 |
And I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Logged in user" block
And I log out
And I press "Save changes"
And I turn editing mode off
And I am on "Course 2" course homepage
- And I navigate to "Enrolment methods" node in "Course administration > Users"
+ And I navigate to "Users > Enrolment methods" in current page administration
And I click on "Edit" "link" in the "Guest access" "table_row"
And I set the following fields to these values:
| Allow guest access | Yes |
Scenario: View the online users block on the front page and see myself
Given I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
When I add the "Online users" block
Then I should see "Admin User" in the "Online users" "block"
And I should see "1 online user" in the "Online users" "block"
Scenario: View the online users block on the front page as a logged in user
Given I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Online users" block
And I log out
And I log in as "student2"
Scenario: View the online users block on the front page as a guest
Given I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Online users" block
And I log out
And I log in as "student2"
Scenario: Hide/show user's online status from/to other users in the online users block on front page
Given I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Online users" block
And I log out
When I log in as "student1"
| student1 | Sam | Student | student1@example.com |
And I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "People" block
And I log out
| teacher1 | Teacher | 1 | teacher1@example.com |
And I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Private files" block
And I log out
* Remove old entries from table block_recent_activity
*/
public function execute() {
- global $DB;
+ global $CFG, $DB;
+ require_once("{$CFG->dirroot}/course/lib.php");
+
// Those entries will never be displayed as RECENT anyway.
- $DB->delete_records_select('block_recent_activity', 'timecreated < ?',
- array(time() - COURSE_MAX_RECENT_PERIOD));
+ $DB->delete_records_select('block_recent_activity', 'timecreated < ?', [
+ time() - COURSE_MAX_RECENT_PERIOD,
+ ]);
}
}
*/
class block_rss_client extends block_base {
- /** The maximum time in seconds that cron will wait between attempts to retry failing RSS feeds. */
- const CLIENT_MAX_SKIPTIME = 43200; // 60 * 60 * 12 seconds.
-
/** @var bool track whether any of the output feeds have recorded failures */
private $hasfailedfeeds = false;
return core_text::substr($title, 0, $max - 3) . '...';
}
}
-
- /**
- * Calculates a new skip time for a record based on the current skip time.
- *
- * @param int $currentskip The curreent skip time of a record.
- * @return int A new skip time that should be set.
- */
- public function calculate_skiptime($currentskip) {
- // The default time to skiptime.
- $newskiptime = $this->cron * 1.1;
- if ($currentskip > 0) {
- // Double the last time.
- $newskiptime = $currentskip * 2;
- }
- if ($newskiptime > self::CLIENT_MAX_SKIPTIME) {
- // Do not allow the skip time to increase indefinatly.
- $newskiptime = self::CLIENT_MAX_SKIPTIME;
- }
- return $newskiptime;
- }
}
*/
class refreshfeeds extends \core\task\scheduled_task {
+ /** The maximum time in seconds that cron will wait between attempts to retry failing RSS feeds. */
+ const CLIENT_MAX_SKIPTIME = HOURSECS * 12;
+
/**
* Name for this task.
*
*/
public function execute() {
global $CFG, $DB;
- require_once($CFG->libdir.'/simplepie/moodle_simplepie.php');
+ require_once("{$CFG->libdir}/simplepie/moodle_simplepie.php");
// We are going to measure execution times.
$starttime = microtime();
continue;
}
- // Fetch the rss feed, using standard simplepie caching
- // so feeds will be renewed only if cache has expired.
- \core_php_time_limit::raise(60);
-
- $feed = new \moodle_simplepie();
- // Set timeout for longer than normal to be agressive at
- // fetching feeds if possible..
- $feed->set_timeout(40);
- $feed->set_cache_duration(0);
- $feed->set_feed_url($rec->url);
- $feed->init();
+ $feed = $this->fetch_feed($rec->url);
if ($feed->error()) {
// Skip this feed (for an ever-increasing time if it keeps failing).
- $block = new \block_rss_client();
- $rec->skiptime = $block->calculate_skiptime($rec->skiptime);
+ $rec->skiptime = $this->calculate_skiptime($rec->skiptime);
$rec->skipuntil = time() + $rec->skiptime;
$DB->update_record('block_rss_client', $rec);
mtrace("Error: could not load/find the RSS feed - skipping for {$rec->skiptime} seconds.");
// Show times.
mtrace($counter . ' feeds refreshed (took ' . microtime_diff($starttime, microtime()) . ' seconds)');
+ }
+
+ /**
+ * Fetch a feed for the specified URL.
+ *
+ * @param string $url The URL to fetch
+ * @return \moodle_simplepie
+ */
+ protected function fetch_feed(string $url) : \moodle_simplepie {
+ // Fetch the rss feed, using standard simplepie caching so feeds will be renewed only if cache has expired.
+ \core_php_time_limit::raise(60);
+
+ $feed = new \moodle_simplepie();
+
+ // Set timeout for longer than normal to be agressive at fetching feeds if possible..
+ $feed->set_timeout(40);
+ $feed->set_cache_duration(0);
+ $feed->set_feed_url($url);
+ $feed->init();
+
+ return $feed;
+ }
+
+ /**
+ * Calculates a new skip time for a record based on the current skip time.
+ *
+ * @param int $currentskip The current skip time of a record.
+ * @return int The newly calculated skip time.
+ */
+ protected function calculate_skiptime(int $currentskip) : int {
+ // If the feed has never failed, then the initial skiptime will be 0. We use a default of 5 minutes in this case.
+ // If the feed has previously failed then we double that time.
+ $newskiptime = max(MINSECS * 5, ($currentskip * 2));
+ // Max out at the CLIENT_MAX_SKIPTIME.
+ return min($newskiptime, self::CLIENT_MAX_SKIPTIME);
}
}
$this->assertContains('0 feeds refreshed (took ', $cronoutput);
}
+ /**
+ * Data provider for skip time tests.
+ *
+ * @return array
+ */
+ public function skip_time_increase_provider() : array {
+ return [
+ 'Never failed' => [
+ 'skiptime' => 0,
+ 'skipuntil' => 0,
+ 'newvalue' => MINSECS * 5,
+ ],
+ 'Failed before' => [
+ // This should just double the time.
+ 'skiptime' => 330,
+ 'skipuntil' => time(),
+ 'newvalue' => 660,
+ ],
+ 'Near max' => [
+ 'skiptime' => \block_rss_client\task\refreshfeeds::CLIENT_MAX_SKIPTIME - 5,
+ 'skipuntil' => time(),
+ 'newvalue' => \block_rss_client\task\refreshfeeds::CLIENT_MAX_SKIPTIME,
+ ],
+ ];
+ }
+
/**
* Test that when a feed has an error the skip time is increased correctly.
+ *
+ * @dataProvider skip_time_increase_provider
*/
- public function test_error() {
+ public function test_error($skiptime, $skipuntil, $newvalue) {
global $DB, $CFG;
$this->resetAfterTest();
+
+ require_once("{$CFG->libdir}/simplepie/moodle_simplepie.php");
+
$time = time();
// A record that has failed before.
- $record = (object) array(
+ $record = (object) [
'userid' => 1,
'title' => 'Skip test feed',
'preferredtitle' => '',
'description' => 'A feed to test the skip time.',
'shared' => 0,
'url' => 'http://example.com/rss',
- 'skiptime' => 330,
- 'skipuntil' => $time - 300,
- );
+ 'skiptime' => $skiptime,
+ 'skipuntil' => $skipuntil,
+ ];
$record->id = $DB->insert_record('block_rss_client', $record);
- // A record that has not failed before.
- $record2 = (object) array(
- 'userid' => 1,
- 'title' => 'Skip test feed',
- 'preferredtitle' => '',
- 'description' => 'A feed to test the skip time.',
- 'shared' => 0,
- 'url' => 'http://example.com/rss2',
- 'skiptime' => 0,
- 'skipuntil' => 0,
- );
- $record2->id = $DB->insert_record('block_rss_client', $record2);
+ // Run the scheduled task and have it fail.
+ $task = $this->getMockBuilder(\block_rss_client\task\refreshfeeds::class)
+ ->setMethods(['fetch_feed'])
+ ->getMock();
- // A record that is near the maximum wait time.
- $record3 = (object) array(
- 'userid' => 1,
- 'title' => 'Skip test feed',
- 'preferredtitle' => '',
- 'description' => 'A feed to test the skip time.',
- 'shared' => 0,
- 'url' => 'http://example.com/rss3',
- 'skiptime' => block_rss_client::CLIENT_MAX_SKIPTIME - 5,
- 'skipuntil' => $time - 1,
- );
- $record3->id = $DB->insert_record('block_rss_client', $record3);
+ $piemock = $this->getMockBuilder(\moodle_simplepie::class)
+ ->setMethods(['error'])
+ ->getMock();
- // Run the scheduled task.
- $task = new \block_rss_client\task\refreshfeeds();
- ob_start();
+ $piemock->method('error')
+ ->willReturn(true);
- // Silence SimplePie php notices.
- $errorlevel = error_reporting($CFG->debug & ~E_USER_NOTICE);
- $task->execute();
- error_reporting($errorlevel);
+ $task->method('fetch_feed')
+ ->willReturn($piemock);
- $cronoutput = ob_get_clean();
- $skiptime1 = $record->skiptime * 2;
- $message1 = 'http://example.com/rss Error: could not load/find the RSS feed - skipping for ' . $skiptime1 . ' seconds.';
- $this->assertContains($message1, $cronoutput);
- $skiptime2 = 0;
- $message2 = 'http://example.com/rss2 Error: could not load/find the RSS feed - skipping for ' . $skiptime2 . ' seconds.';
- $this->assertContains($message2, $cronoutput);
- $skiptime3 = block_rss_client::CLIENT_MAX_SKIPTIME;
- $message3 = 'http://example.com/rss3 Error: could not load/find the RSS feed - skipping for ' . $skiptime3 . ' seconds.';
- $this->assertContains($message3, $cronoutput);
- $this->assertContains('0 feeds refreshed (took ', $cronoutput);
-
- // Test that the records have been correctly updated.
- $newrecord = $DB->get_record('block_rss_client', array('id' => $record->id));
- $this->assertAttributeEquals($skiptime1, 'skiptime', $newrecord);
- $this->assertAttributeGreaterThanOrEqual($time + $skiptime1, 'skipuntil', $newrecord);
- $newrecord2 = $DB->get_record('block_rss_client', array('id' => $record2->id));
- $this->assertAttributeEquals($skiptime2, 'skiptime', $newrecord2);
- $this->assertAttributeGreaterThanOrEqual($time + $skiptime2, 'skipuntil', $newrecord2);
- $newrecord3 = $DB->get_record('block_rss_client', array('id' => $record3->id));
- $this->assertAttributeEquals($skiptime3, 'skiptime', $newrecord3);
- $this->assertAttributeGreaterThanOrEqual($time + $skiptime3, 'skipuntil', $newrecord3);
+ // Run the cron and capture its output.
+ $this->expectOutputRegex("/.*Error: could not load\/find the RSS feed - skipping for {$newvalue} seconds.*/");
+ $task->execute();
}
}
| student1 | C1 | student |
And I log in as "teacher1"
And I am on "Course 1" course homepage
- And I navigate to "Edit settings" node in "Course administration"
+ And I navigate to "Edit settings" in current page administration
And I set the field "id_newsitems" to "1"
And I press "Save and display"
And I turn editing mode on
| student1 | Student | 1 | student1@example.com | S1 |
And I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Search forums" block
And I log out
Scenario: Add a URL in menu block and ensure it appears
Given I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Main menu" block
When I add a "URL" to section "0" and I fill the form with:
| Name | google |
Scenario: Edit name of acitivity in-place in site main menu block
Given I log in as "admin"
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Main menu" block
When I add a "Forum" to section "0" and I fill the form with:
| Forum name | My forum name |
And I set the following administration settings values:
| allowstealth | 1 |
And I am on site homepage
- And I navigate to "Turn editing on" node in "Front page settings"
+ And I navigate to "Turn editing on" in current page administration
And I add the "Main menu" block
When I add a "Forum" to section "0" and I fill the form with:
| Forum name | Visible forum |
And the field "Availability" matches value "Make available but not shown on course page"
And I press "Save and return to course"
And "My forum name" activity in site main menu block should be available but hidden from course page
- And I navigate to "Turn editing off" node in "Front page settings"
+ And I navigate to "Turn editing off" in current page administration
And "My forum name" activity in site main menu block should be available but hidden from course page
And I log out
And I should not see "My forum name" in the "Main menu" "block"
And I press "Customise this page"
# TODO MDL-57120 site "Tags" link not accessible without navigation block.
And I add the "Navigation" block if not present
- And I navigate to "Tags" node in "Site pages"
+ And I click on "Site pages" "list_item" in the "Navigation" "block"
+ And I click on "Tags" "link" in the "Navigation" "block"
And I add the "Flickr" block
And I configure the "Flickr" block
Then I should see "Flickr block title"
| manager1 | Acceptance test site | manager |
# Allow at least one role assignment in the block context:
And I log in as "admin"
- And I navigate to "Define roles" node in "Site administration > Users > Permissions"
+ And I navigate to "Users > Permissions > Define roles" in site administration
And I follow "Edit Non-editing teacher role"
And I set the following fields to these values:
| Block | 1 |
| testuser2 | C1 | student |
And I log in as "admin"
And I am on site homepage
- And I navigate to "Blog" node in "Site administration > Appearance"
+ And I navigate to "Appearance > Blog" in site administration
And I set the following fields to these values:
| Blog visibility | Users can only see their own blog |
And I press "Save changes"
And I press "Save changes"
And I log out
And I log in as "testuser"
- And I navigate to "Site blogs" node in "Site pages"
+ And I click on "Site pages" "list_item" in the "Navigation" "block"
+ And I click on "Site blogs" "link" in the "Navigation" "block"
And I follow "Add a new entry"
And I set the following fields to these values:
| Entry title | Blog post from user 1 |
Scenario: Commenting on my own blog entry
Given I am on site homepage
And I log in as "testuser"
- And I navigate to "Site blogs" node in "Site pages"
+ And I click on "Site pages" "list_item" in the "Navigation" "block"
+ And I click on "Site blogs" "link" in the "Navigation" "block"
And I follow "Blog post from user 1"
And I should see "User 1 blog post content"
And I follow "Comments (0)"
Scenario: Deleting my own comment
Given I am on site homepage
And I log in as "testuser"
- And I navigate to "Site blogs" node in "Site pages"
+ And I click on "Site pages" "list_item" in the "Navigation" "block"
+ And I click on "Site blogs" "link" in the "Navigation" "block"
And I follow "Blog post from user 1"
And I should see "User 1 blog post content"
And I follow "Comments (0)"
Given I am on site homepage
And I log in as "testuser2"
And I am on site homepage
- And I navigate to "Site blogs" node in "Site pages"
+ And I click on "Site pages" "list_item" in the "Navigation" "block"
+ And I click on "Site blogs" "link" in the "Navigation" "block"
And I follow "Blog post from user 1"
When I follow "Comments (0)"
And I set the field "content" to "$My own >nasty< \"string\"!"
And I press "Save changes"
And I log out
And I log in as "testuser"
- And I navigate to "Site blogs" node in "Site pages"
+ And I click on "Site blogs" "link" in the "Navigation" "block"
And I follow "Add a new entry"
And I set the following fields to these values:
| Entry title | Blog post one |
| Blog entry body | User 1 blog post content |
And I press "Save changes"
And I am on site homepage
- And I navigate to "Site blogs" node in "Site pages"
+ And I click on "Site blogs" "link" in the "Navigation" "block"
Scenario: Delete blog post results in post deleted
Given I follow "Blog post one"
$invalidationeventset = false;
$factory = cache_factory::instance();
$inuse = $factory->get_caches_in_use();
+ $purgetoken = null;
foreach ($instance->get_definitions() as $name => $definitionarr) {
$definition = cache_definition::load($name, $definitionarr);
if ($definition->invalidates_on_event($event)) {
$data = array();
}
// Add our keys to them with the current cache timestamp.
+ if (null === $purgetoken) {
+ $purgetoken = cache::get_purge_token(true);
+ }
foreach ($keys as $key) {
- $data[$key] = cache::now();
+ $data[$key] = $purgetoken;
}
// Set that data back to the cache.
$cache->set($event, $data);
$invalidationeventset = false;
$factory = cache_factory::instance();
$inuse = $factory->get_caches_in_use();
+ $purgetoken = null;
foreach ($instance->get_definitions() as $name => $definitionarr) {
$definition = cache_definition::load($name, $definitionarr);
if ($definition->invalidates_on_event($event)) {
// Get the event invalidation cache.
$cache = cache::make('core', 'eventinvalidation');
// Create a key to invalidate all.
+ if (null === $purgetoken) {
+ $purgetoken = cache::get_purge_token(true);
+ }
$data = array(
- 'purged' => cache::now()
+ 'purged' => $purgetoken,
);
// Set that data back to the cache.
$cache->set($event, $data);
*/
protected static $now;
+ /**
+ * A purge token used to distinguish between multiple cache purges in the same second.
+ * This is in the format <microtime>-<random string>.
+ *
+ * @var string
+ */
+ protected static $purgetoken;
+
/**
* The definition used when loading this cache if there was one.
* @var cache_definition
return;
}
+ // Each cache stores the current 'lastinvalidation' value within the cache itself.
$lastinvalidation = $this->get('lastinvalidation');
if ($lastinvalidation === false) {
- // This is a new cache or purged globally, there won't be anything to invalidate.
- // Set the time of the last invalidation and move on.
- $this->set('lastinvalidation', self::now());
+ // There is currently no value for the lastinvalidation token, therefore the token is not set, and there
+ // can be nothing to invalidate.
+ // Set the lastinvalidation value to the current purge token and return early.
+ $this->set('lastinvalidation', self::get_purge_token());
return;
- } else if ($lastinvalidation == self::now()) {
- // We've already invalidated during this request.
+ } else if ($lastinvalidation == self::get_purge_token()) {
+ // The current purge request has already been fully handled by this cache.
return;
}
- // Get the event invalidation cache.
+ /*
+ * Now that the whole cache check is complete, we check the meaning of any specific cache invalidation events.
+ * These are stored in the core/eventinvalidation cache as an multi-dimensinoal array in the form:
+ * [
+ * eventname => [
+ * keyname => purgetoken,
+ * ]
+ * ]
+ *
+ * The 'keyname' value is used to delete a specific key in the cache.
+ * If the keyname is set to the special value 'purged', then the whole cache is purged instead.
+ *
+ * The 'purgetoken' is the token that this key was last purged.
+ * a) If the purgetoken matches the last invalidation, then the key/cache is not purged.
+ * b) If the purgetoken is newer than the last invalidation, then the key/cache is not purged.
+ * c) If the purge token is older than the last invalidation, or it has a different token component, then the
+ * cache is purged.
+ *
+ * Option b should not happen under normal operation, but may happen in race condition whereby a long-running
+ * request's cache is cleared in another process during that request, and prior to that long-running request
+ * creating the cache. In such a condition, it would be incorrect to clear that cache.
+ */
$cache = self::make('core', 'eventinvalidation');
$events = $cache->get_many($this->definition->get_invalidation_events());
$todelete = array();
$purgeall = false;
+
// Iterate the returned data for the events.
foreach ($events as $event => $keys) {
if ($keys === false) {
// No data to be invalidated yet.
continue;
}
+
// Look at each key and check the timestamp.
- foreach ($keys as $key => $timestamp) {
+ foreach ($keys as $key => $purgetoken) {
// If the timestamp of the event is more than or equal to the last invalidation (happened between the last
- // invalidation and now)then we need to invaliate the key.
- if ($timestamp >= $lastinvalidation) {
+ // invalidation and now), then we need to invaliate the key.
+ if (self::compare_purge_tokens($purgetoken, $lastinvalidation) > 0) {
if ($key === 'purged') {
$purgeall = true;
break;
}
// Set the time of the last invalidation.
if ($purgeall || !empty($todelete)) {
- $this->set('lastinvalidation', self::now());
+ $this->set('lastinvalidation', self::get_purge_token(true));
}
}
* This stamp needs to be used for all ttl and time based operations to ensure that we don't end up with
* timing issues.
*
- * @return int
+ * @param bool $float Whether to use floating precision accuracy.
+ * @return int|float
*/
- public static function now() {
+ public static function now($float = false) {
if (self::$now === null) {
- self::$now = time();
+ self::$now = microtime(true);
+ }
+
+ if ($float) {
+ return self::$now;
+ } else {
+ return (int) self::$now;
+ }
+ }
+
+ /**
+ * Get a 'purge' token used for cache invalidation handling.
+ *
+ * Note: This function is intended for use from within the Cache API only and not by plugins, or cache stores.
+ *
+ * @param bool $reset Whether to reset the token and generate a new one.
+ * @return string
+ */
+ public static function get_purge_token($reset = false) {
+ if (self::$purgetoken === null || $reset) {
+ self::$now = null;
+ self::$purgetoken = self::now(true) . '-' . uniqid('', true);
+ }
+
+ return self::$purgetoken;
+ }
+
+ /**
+ * Compare a pair of purge tokens.
+ *
+ * If the two tokens are identical, then the return value is 0.
+ * If the time component of token A is newer than token B, then a positive value is returned.
+ * If the time component of token B is newer than token A, then a negative value is returned.
+ *
+ * Note: This function is intended for use from within the Cache API only and not by plugins, or cache stores.
+ *
+ * @param string $tokena
+ * @param string $tokenb
+ * @return int
+ */
+ public static function compare_purge_tokens($tokena, $tokenb) {
+ if ($tokena === $tokenb) {
+ // There is an exact match.
+ return 0;
+ }
+
+ // The token for when the cache was last invalidated.
+ list($atime) = explode('-', "{$tokena}-", 2);
+
+ // The token for this cache.
+ list($btime) = explode('-', "{$tokenb}-", 2);
+
+ if ($atime >= $btime) {
+ // Token A is newer.
+ return 1;
+ } else {
+ // Token A is older.
+ return -1;
}
- return self::$now;
}
}
$timefile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las-cache/lastinvalidation-$hash.cache";
// Make sure the file is correct.
$this->assertTrue(file_exists($timefile));
- $timecont = serialize(cache::now() - 60); // Back 60sec in the past to force it to re-invalidate.
+ $timecont = serialize(cache::now(true) - 60); // Back 60sec in the past to force it to re-invalidate.
make_writable_directory(dirname($timefile));
file_put_contents($timefile, $timecont);
$this->assertTrue(file_exists($timefile));
// Test 2: Rebuild and test the invalidation of the event via the invalidation cache.
cach