Compare commits

..

136 Commits

Author SHA1 Message Date
Jun Pataleta ae82333cf2 weekly release 3.3.9+ 2019-02-01 08:57:00 +08:00
Jun Pataleta 0c5e1ca0d2 Merge branch 'install_33_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_33_STABLE 2019-02-01 08:56:58 +08:00
Eloy Lafuente (stronk7) 1ef6a4c080 Merge branch 'MDL-64737-33-attoprivacysql' of git://github.com/mudrd8mz/moodle into MOODLE_33_STABLE 2019-01-31 22:48:43 +01:00
David Mudrák c2b73b54eb MDL-64737 editor_atto: Fix the privacy export_user_data() implementation
Multiple issues fixed:

* Fixed order of the statements to avoid access to undefined variables.
* Fixed population of the SQL query parameter to make sure they match
  the placeholder in the query.
* Fixed missing table aliases in the second query to make sure the query
  actually works as expected.
2019-01-31 19:24:26 +01:00
David Mudrák 1f47c7fc7b MDL-64737 privacy: Fix privacy provider testsuite file names
Only files ending with "_test.php" suffix are implicit part of a
testsuite. So these two were not actually executed.
2019-01-31 19:24:26 +01:00
AMOS bot 4da970984c Automatically generated installer lang files 2019-01-30 00:10:40 +00:00
AMOS bot d3b93323c4 Automatically generated installer lang files 2019-01-22 00:10:22 +00:00
AMOS bot b68e858798 Automatically generated installer lang files 2019-01-10 00:09:05 +00:00
AMOS bot d02dc6a763 Automatically generated installer lang files 2018-12-19 00:09:35 +00:00
AMOS bot 12cd92baa6 Automatically generated installer lang files 2018-12-07 00:09:55 +00:00
AMOS bot d99ea30311 Automatically generated installer lang files 2018-11-30 00:08:02 +00:00
AMOS bot 472746d97e Automatically generated installer lang files 2018-11-29 00:08:48 +00:00
David Monllao d23d50025e weekly release 3.3.9+ 2018-11-28 11:23:24 +01:00
David Monllao 43f7bb1815 Merge branch 'install_33_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_33_STABLE 2018-11-28 11:23:22 +01:00
David Mudrák c041f919f0 MDL-64205 privacy: Delete orphaned data request records on upgrade
As a result of the bug, some requests can be orphaned - the linked user
record may not exist any more. This is a cleanup of such records.
2018-11-27 09:52:11 +01:00
David Mudrák 32b1faf8b7 MDL-64205 users: Do not delete the actual records of unconfirmed users
There is an automatic data privacy request created to get rid of all the
personal data upon the user deletion. With the actual user record
absent, the data privacy requests page throws an error. Let's do here
same as we do in `delete_incomplete_users_task` and let us not delete
the actual user record.
2018-11-27 09:49:42 +01:00
AMOS bot d2a2855501 Automatically generated installer lang files 2018-11-23 00:07:49 +00:00
David Monllao f66db7a52b weekly release 3.3.9+ 2018-11-20 15:12:07 +01:00
David Monllao a6e6765bf1 Merge branch 'install_33_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_33_STABLE 2018-11-20 15:12:06 +01:00
David Mudrák a00ea08b58 MDL-63994 auth_cas: Include the login token in the simulated login form
The CAS login process relies on the standard authenticate_user_login()
call to set up the user. So we need to inject the login token to pass
the validation.
2018-11-15 21:20:13 +01:00
David Mudrák 1fcfd1b90e MDL-63994 login: Improve the logintoken param input
The logintoken is supposed to arrive as a part of the login form ($frm)
together with the username and password. So it should be handled the
same way - including the opportunity for the auth plugins to provide the
form data via the loginpage_hook().

This also implies that only logintoken coming as a part of the POST
request are taken into account, which is a good thing and another thin
layer in this security mechanism.
2018-11-15 21:20:12 +01:00
David Mudrák 8324204f6d MDL-63994 login: Clarify inline comments on loginpage_hook() usage
While working on the issue, I found these inline comments outdated and
confusing. The auth plugin types can implement a loginpage_hook()
method, the purpose of which is to inject the $user and/or $frm into
this login/index.php script. The new comments should make it more clear.

Note the second comment mentioned a prelogin_hook() which was an old
name of what is now called loginpage_hook(). It had nothing to do with
the existing pre_loginpage_hook() and was only confusing.
2018-11-15 21:20:12 +01:00
AMOS bot 00282f1c56 Automatically generated installer lang files 2018-11-13 00:07:55 +00:00
Eloy Lafuente (stronk7) 5c5ee48bf6 Moodle release 3.3.9 2018-11-10 20:36:16 +01:00
Eloy Lafuente (stronk7) 040430abff weekly release 3.3.8+ 2018-11-10 12:53:14 +01:00
Eloy Lafuente (stronk7) 2582943f92 Merge branch 'MDL-63960-33' of git://github.com/andrewnicols/moodle into MOODLE_33_STABLE 2018-11-10 10:28:39 +01:00
Andrew Nicols 97e8c0abce MDL-63960 dataprivacy: Do not perform strict type checks for id
When inherit is specified in the data registry it is stored as a
string and we cannot perform a strict comparison with it.

We should still compare strict comparison checks against null, or false,
but not for the NOTSET (0) or INHERIT values (-1).
2018-11-10 15:34:11 +08:00
Eloy Lafuente (stronk7) 51894a816f MDL-63919 tool_dataprivacy: admin & notify fixes
This includes the following:

1) Replace $ADMIN->id by get_admin()->id. The former doesn't exist.
2) Only change the notify parameter when it has not been specified
   at creation time (null). If specified, observe it.
3) Set the current user in tests to admin, able to create those
   requests.
2018-11-10 11:01:14 +08:00
Andrew Nicols c0ac0bd256 MDL-62564 tool_dataprivacy: Do not delete deleted user
This also ensures that we do not call delete_user on an already deleted
user.
2018-11-09 17:37:27 +08:00
Jun Pataleta a5c5651a0f Merge branch 'MDL-63942-33' of git://github.com/andrewnicols/moodle into MOODLE_33_STABLE 2018-11-09 11:42:11 +08:00
Andrew Nicols 01e64f89c7 Merge branch 'MDL-63919-33' of git://github.com/mihailges/moodle into MOODLE_33_STABLE 2018-11-09 10:53:48 +08:00
Andrew Nicols a505a4abd7 MDL-63942 dataprivacy: Disable deleted user task by default 2018-11-09 09:39:35 +08:00
Eloy Lafuente (stronk7) c587194dee Merge branch 'MDL-62564-integration-33-1' of git://github.com/mihailges/moodle into MOODLE_33_STABLE 2018-11-08 16:50:45 +01:00
Mihail Geshoski 76c9202996 MDL-63919 privacy: Store the creation method of the data request 2018-11-08 14:50:36 +08:00
Mihail Geshoski 752b63d574 MDL-63919 privacy: Disable notifications for auto created data requests 2018-11-08 14:50:36 +08:00
Mihail Geshoski 4cd4b14df2 MDL-62564 privacy: Create request for deleted users when setting enabled 2018-11-08 10:27:26 +08:00
Mihail Geshoski a49f5519e2 MDL-62564 privacy: Improve bulk deletion 2018-11-08 10:26:40 +08:00
Mihail Geshoski d494848985 MDL-62564 privacy: Add unit tests 2018-11-08 10:15:59 +08:00
Jun Pataleta 950da9db66 MDL-63183 auth_shibboleth: Don't render page when already logged in 2018-11-06 19:10:58 +01:00
Jun Pataleta d3c9d9306c MDL-63183 auth_shibboleth: Add hidden login token in guest login form 2018-11-06 19:10:58 +01:00
Damyon Wiese ff7941f00e MDL-63183 auth: Login protection
CSRF protection for the login form. The authenticate_user_login function was
extended to validate the token (in \core\session\manager) but by default it
does not perform the extra validation. Existing uses of this function from
auth plugins and features like "change password" will continue to work without
changes. New config value $CFG->disablelogintoken can bypass this check.
2018-11-06 19:10:58 +01:00
Eloy Lafuente (stronk7) a02c72d8bb weekly release 3.3.8+ 2018-11-06 19:06:35 +01:00
Eloy Lafuente (stronk7) 3d1ee2568e Merge branch 'install_33_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_33_STABLE 2018-11-06 19:06:33 +01:00
Andrew Nicols bb560c7012 Merge branch 'MDL-62563-33-3' of git://github.com/mihailges/moodle into MOODLE_33_STABLE 2018-11-06 09:57:19 +08:00
AMOS bot 0e8ebb0ead Automatically generated installer lang files 2018-11-06 00:07:43 +00:00
Mihail Geshoski 0375eb8488 MDL-62563 privacy: Create delete data request for existing deleted users 2018-11-05 16:29:47 +08:00
Mihail Geshoski 1f258e679a MDL-62563 privacy: Add unit tests 2018-11-05 16:22:17 +08:00
Eloy Lafuente (stronk7) 13d2621e23 weekly release 3.3.8+ 2018-11-02 21:08:30 +01:00
David Monllao 2709ae4dcc Merge branch 'MDL-62601-33' of git://github.com/andrewnicols/moodle into MOODLE_33_STABLE 2018-10-31 06:47:59 +01:00
Andrew Nicols 9aa05ccfdd MDL-62601 core_privacy: Ensure providers can handle deleted users 2018-10-31 13:44:51 +08:00
Andrew Nicols 6855ebb1eb MDL-62601 editor_atto: Do not use context_user 2018-10-31 13:44:51 +08:00
Eloy Lafuente (stronk7) 3d1949af87 weekly release 3.3.8+ 2018-10-30 18:23:58 +01:00
Andrew Nicols 3cda2cab0e Merge branch 'MDL-63626-33' of git://github.com/rezaies/moodle into MOODLE_33_STABLE 2018-10-30 11:00:43 +08:00
Andrew Nicols e899bccd8e MDL-63267 privacy: Remove use of int hints 2018-10-28 18:16:12 +08:00
Andrew Nicols a94a0c565b MDL-63267 core_grading: More Fix 5.6 type hints 2018-10-27 19:55:13 +08:00
Andrew Nicols 1171be0c0a MDL-63267 core_grading: Fix 5.6 type hints 2018-10-27 19:52:37 +08:00
Andrew Nicols 12963c1460 Merge branch 'MDL-63267-33' of git://github.com/lameze/moodle into MOODLE_33_STABLE 2018-10-27 19:30:23 +08:00
Tim Lock d5fea5ba37 MDL-63267 tool_privacy: Handle modules without privacy support. 2018-10-26 18:51:38 -04:00
Adrian Greeve 76a361a821 MDL-63267 grading methods: Unit test update for fix. 2018-10-26 18:51:38 -04:00
Adrian Greeve fb1740d897 MDL-63267 grading methods: Fix for multiple intances.
We were only exporting the one instance per item id.
Instead we should have been exporting all instances for each
item id.
2018-10-26 18:51:38 -04:00
Adrian Greeve 95433e3763 MDL-63267 tool_dataprivacy: Registry shows deprecated interfaces. 2018-10-26 18:51:38 -04:00
Adrian Greeve af7362c323 MDL-63267 privacy: Create deprecated provider interface.
Any interfaces that have become deprecated should extend
this interface.
2018-10-26 18:51:38 -04:00
Adrian Greeve 07d283e56e MDL-63267 core_grading: Unit test for new methods. 2018-10-26 18:51:38 -04:00
Adrian Greeve f1608e1184 MDL-63267 mod_assign: Update to providers to include adv grading. 2018-10-26 18:51:38 -04:00
Adrian Greeve 958dfb534d MDL-63267 core_grading: Update to providers and interface.
The previous interface was inadequate for retrieving user
information stored in the sub-plugins.

A new interface and methods have been added to successfully
deal with user data.
2018-10-26 18:51:31 -04:00
Eloy Lafuente (stronk7) aa561d76e8 weekly release 3.3.8+ 2018-10-26 18:05:00 +02:00
Eloy Lafuente (stronk7) fd497585a8 Merge branch 'install_33_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_33_STABLE 2018-10-26 18:04:58 +02:00
Eloy Lafuente (stronk7) 62c1be5b19 Merge branch 'MDL-62904-33' of git://github.com/junpataleta/moodle into MOODLE_33_STABLE 2018-10-26 00:04:09 +02:00
Andrew Nicols 258057604b MDL-63619 tool_dataprivacy: Cache purposes in form 2018-10-24 08:56:04 +08:00
Andrew Nicols e21e6d3d5b MDL-63619 tool_dataprivacy: Performance improvement 2018-10-24 08:56:04 +08:00
Andrew Nicols dbf4da68de MDL-63619 tool_dataprivacy: Fix inheritance from parent contexts
Inheritance should behave such that all contexts inherit from their
parent context.

Prior to this fix, if the value was not set on a context, then it was
getting a default of 'Inherit', but instead of inheritting from the
parent context, it was inheritting from its parent context _level_ which
is just wrong.
2018-10-24 08:56:04 +08:00
AMOS bot 20f17d1ee3 Automatically generated installer lang files 2018-10-22 00:07:36 +00:00
Eloy Lafuente (stronk7) a0d09bb67e weekly release 3.3.8+ 2018-10-19 17:10:47 +02:00
Eloy Lafuente (stronk7) fc77529235 Merge branch 'install_33_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_33_STABLE 2018-10-19 17:10:45 +02:00
AMOS bot de2c1990eb Automatically generated installer lang files 2018-10-19 00:08:27 +00:00
Eloy Lafuente (stronk7) f8191ba10f Merge branch 'MDL-63632-33' of git://github.com/andrewnicols/moodle into MOODLE_33_STABLE 2018-10-18 02:39:43 +02:00
AMOS bot 3ea8d9d89a Automatically generated installer lang files 2018-10-18 00:07:44 +00:00
Andrew Nicols ceb8d8dc60 MDL-63632 forum: Delete where not discussion author 2018-10-18 07:20:29 +08:00
Jun Pataleta 4f232817c2 MDL-62904 tool_dataprivacy: Add data request links in profile page 2018-10-17 16:44:09 +08:00
Eloy Lafuente (stronk7) dabce49529 weekly release 3.3.8+ 2018-10-16 16:05:28 +02:00
Eloy Lafuente (stronk7) 67cc6f70c1 Merge branch 'install_33_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_33_STABLE 2018-10-16 16:05:26 +02:00
Andrew Nicols 33adcf9926 Merge branch 'MDL-63421_33' of git://github.com/stronk7/moodle into MOODLE_33_STABLE 2018-10-16 08:56:20 +08:00
AMOS bot 7d3536fb48 Automatically generated installer lang files 2018-10-15 00:07:35 +00:00
Shamim Rezaie 0d7f464d4b MDL-63626 mod_quiz: Fixed a bug when there was no attempt on the quiz 2018-10-12 01:02:16 +11:00
Eloy Lafuente (stronk7) 629a97d5c1 weekly release 3.3.8+ 2018-10-11 13:22:43 +02:00
Eloy Lafuente (stronk7) 16f8828e9f Merge branch 'install_33_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_33_STABLE 2018-10-11 13:22:40 +02:00
AMOS bot fbfa3a0bd2 Automatically generated installer lang files 2018-10-11 00:07:33 +00:00
AMOS bot bb9bb21546 Automatically generated installer lang files 2018-10-10 00:07:30 +00:00
Eloy Lafuente (stronk7) b1da31a63e Merge branch 'MDL-62395-33' of git://github.com/junpataleta/moodle into MOODLE_33_STABLE 2018-10-09 23:07:07 +02:00
Eloy Lafuente (stronk7) c165ee4890 Merge branch 'MDL-63401-33' of git://github.com/andrewnicols/moodle into MOODLE_33_STABLE 2018-10-09 11:36:58 +02:00
Adrian Greeve 0446552496 MDL-62491 core_privacy: php 5.6 fix for using current() 2018-10-09 16:22:25 +08:00
Jun Pataleta 7e954344d2 Merge branch 'MDL-62491-33' of git://github.com/abgreeve/moodle into MOODLE_33_STABLE 2018-10-09 14:39:25 +08:00
Adrian Greeve efd6748031 MDL-62491 navigation: Update to tree.js to allow callbacks. 2018-10-09 14:33:16 +08:00
Adrian Greeve 493f79cc6a MDL-62491 mod_assign: Fill in the rewrite plugin urls properly. 2018-10-09 14:33:16 +08:00
Adrian Greeve 0231ce53b1 MDL-62491 core_privacy: Add html files to the user data export.
This adds html to the data export that allows for easier navigation
and reading of data.
2018-10-09 14:33:16 +08:00
Andrew Nicols 222c900018 MDL-63401 tool_dataprivacy: Change default for require end date
We should preserve the existing behaviour.
2018-10-09 08:32:18 +08:00
Andrew Nicols e4d181c4de MDL-63401 tool_dataprivacy: Simplify where processing 2018-10-09 08:22:26 +08:00
Andrew Nicols 56b96699a6 MDL-63401 tool_dataprivacy: Move final cap check to endpoint 2018-10-09 08:22:26 +08:00
Andrew Nicols 5bc61c9686 MDL-63401 tool_dataprivacy: Allow expiriration of users without end date 2018-10-08 20:54:22 +08:00
Andrew Nicols 62165df6dd MDL-63401 tool_dataprivacy: Rewrite expired deletion handling
This change rewrites the way in which expiry is calculated and addresses
a number of closely related issues:

Users can customise, and add blocks with data to, their dashboard.  When
a user had done so, the user could be flagged for deletion, but the
blocks in their Dashboard were subject to the default block retention
policy. In addition there is no way to override the retention policy for
user blocks.

This change modifies the structure of the expiry mechanism:
- to consider any subcontext of the user context to be a part of the user
  context during calculation. User child contexts are not the property
  of the system, and should not be treated separately.
- the way in which the context expiry mechanism worked was to select
  use a multiple different managers based solely on the context level.
  Because of the use of user blocks, this proved to be unreliable as
  blocks has been attributed purely to courses.
  This has been changed to a single manager which is aware of hierarchy
  and deletions as a whole.
- to prepare for upcoming work relating to more detailed expiry
  mechanisms, a new expiry_info class is introduced and used to
  merge the expiry of child contexts into a working in-memory view.

This changeset includes extensive unit tests.
2018-10-08 20:54:20 +08:00
Andrew Nicols 0cd43cccbf MDL-63401 core: Do not delete the user context
As long as we have a user record, we should not delete the context which
relates to it.
2018-10-08 20:54:00 +08:00
Andrew Nicols 0b9cf6ba5e MDL-63401 tool_dataprivacy: Move cap checks to endpoints from API 2018-10-08 20:54:00 +08:00
Eloy Lafuente (stronk7) 6ef7759473 MDL-63421 env: Moodle 3.4.x and 3.5.x do not support PHP 7.3 2018-10-05 13:38:40 +02:00
Eloy Lafuente (stronk7) 6816976c3b weekly release 3.3.8+ 2018-10-05 12:27:53 +02:00
Jun Pataleta 6e7a73c16d MDL-62395 tool_dataprivacy: Purpose name and description in one column
* Show the purpose's name and description under one column.
2018-10-05 09:29:12 +08:00
Jun Pataleta 6fc275038c MDL-62395 tool_dataprivacy: Fix tables display
* Use standard Bootstrap4 table classes for the categories and purposes
  tables.
* Set 25% width for the name and description columns of the purposes
  table.
* Set 50% width for the description column of the categories table.
2018-10-05 09:29:12 +08:00
AMOS bot 41548fb26e Automatically generated installer lang files 2018-10-04 00:07:37 +00:00
Eloy Lafuente (stronk7) 082ef8ef3c weekly release 3.3.8+ 2018-09-27 15:17:50 +02:00
Andrew Nicols 371168e848 Merge branch 'MDL-63346_33' of https://github.com/stronk7/moodle into MOODLE_33_STABLE 2018-09-25 09:13:44 +08:00
Eloy Lafuente (stronk7) 982839422f weekly release 3.3.8+ 2018-09-20 12:11:59 +02:00
Eloy Lafuente (stronk7) cc22614602 MDL-63346 nodejs: Bump to created from scratch shrinkwrap file
Steps:

- remove node_modules & npm-shrinkwrap.json
- npm cache clear --force
- npm install
- npm shrinkwrap
2018-09-20 10:56:29 +02:00
Eloy Lafuente (stronk7) 094fa8ce8a MDL-63346 travis: GRUNT job immune to npm-shrinkwrap.json changes
When upgrading nodejs/npm versions, there are modifications to
different packages or to the format of the npm-shrinkwrap.json
file. Usually that leads to failed jobs untill the bundled file
is updated.

To avoid that, we just ignore the npm-shrinkwrap.json file, so
just the REAL modifications in css/js/ignore stuff are considered.
2018-09-20 10:55:20 +02:00
Eloy Lafuente (stronk7) eca5403915 Merge branch 'MDL-63116-33-1' of git://github.com/mihailges/moodle into MOODLE_33_STABLE 2018-09-18 23:54:41 +02:00
Mihail Geshoski 5924e71021 MDL-61908 privacy: Fix heading in data privacy pages 2018-09-18 18:51:43 +02:00
Eloy Lafuente (stronk7) b9706f8cf3 Merge branch 'MDL-63109-33' of git://github.com/mihailges/moodle into MOODLE_33_STABLE 2018-09-18 18:32:26 +02:00
Mihail Geshoski b17a7d65a7 MDL-63109 privacy: Add behat tests 2018-09-18 15:08:54 +08:00
Mihail Geshoski 558126aee6 MDL-63109 privacy: Fix the initialisation of the policy acceptance modal 2018-09-18 15:05:15 +08:00
Eloy Lafuente (stronk7) 1cec2b9041 Merge branch 'MDL-63009-33' of git://github.com/mickhawkins/moodle into MOODLE_33_STABLE 2018-09-17 23:31:38 +02:00
Eloy Lafuente (stronk7) 5279b38c8b Merge branch 'MDL-63184-33-2' of git://github.com/junpataleta/moodle into MOODLE_33_STABLE 2018-09-17 23:11:54 +02:00
Jun Pataleta 286e9ef666 MDL-63184 tool_dataprivacy: Ensure PO roles have correct capability 2018-09-17 22:36:16 +08:00
David Monllao 026b1ffe51 Merge branch 'MDL-62554-33' of git://github.com/junpataleta/moodle into MOODLE_33_STABLE 2018-09-17 13:46:17 +02:00
Mihail Geshoski 6eddcd1310 MDL-63116 privacy: Add bulk actions to the privacy request table 2018-09-17 08:28:28 +08:00
Mihail Geshoski 4bb3e3fccf MDL-63116 privacy: Add behat tests 2018-09-17 08:28:28 +08:00
Mihail Geshoski be23442e17 MDL-63116 privacy: Add unit tests 2018-09-17 08:28:28 +08:00
Eloy Lafuente (stronk7) e75470d531 weekly release 3.3.8+ 2018-09-14 11:06:45 +02:00
Eloy Lafuente (stronk7) c9a28c645b MDL-61169 iplookup: Change to another block still remaining to SF 2018-09-13 19:07:07 +02:00
Jun Pataleta df84f822bf MDL-62554 tool_dataprivacy: Integration review fixes 2018-09-13 14:58:27 +08:00
Michael Hawkins 4ca488c639 MDL-63009 tool_dataprivacy: Added site name/link to request emails 2018-09-12 17:50:38 +08:00
Jun Pataleta 745f6e7f61 MDL-62554 tool_dataprivacy: Bump version for new WS functions 2018-09-10 12:21:40 +08:00
Jun Pataleta f5b95df245 MDL-62554 tool_dataprivacy: Fix display of activity options 2018-09-10 12:21:40 +08:00
Jun Pataleta 12131e4806 MDL-62554 tool_dataprivacy: Fix template for Bootstrapbase compatibility 2018-09-10 12:21:40 +08:00
Jun Pataleta 2dfd3b9839 MDL-62554 tool_dataprivacy: Remove defaults form
With the changes in this issue, this form is no longer being used.
2018-09-10 12:21:40 +08:00
Jun Pataleta f68fe6bc00 MDL-62554 tool_dataprivacy: Behat tests for managing registry defaults
* Also new step definitions and generator for:
  - Creating categories and purposes
  - Assigning category and purpose to context instances.
2018-09-10 12:21:40 +08:00
Jun Pataleta ea8b23542a MDL-62554 tool_dataprivacy: Unit tests for new WS/API functions 2018-09-10 12:21:40 +08:00
Jun Pataleta 1a228ad894 MDL-62554 tool_dataprivacy: Fix fetching of default contexts
* We also need to handle default contexts for activities. If defaults
  for an activity is set, fetch that. If not, fetch the defaults for
  the context level.
2018-09-10 12:21:40 +08:00
Jun Pataleta 324e9a2800 MDL-62554 tool_dataprivacy: Allow setting of activity defaults
* Allow the setting of data registry defaults for activity modules.
* Rewrite the defaults page so that it uses templates.
* Use a tabbed layout for the defaults page that shows the default
  category and purpose per context level.
* New API and web service functions that enables the setting of the
  defaults.
2018-09-10 12:21:40 +08:00
156 changed files with 11931 additions and 2486 deletions
+2
View File
@@ -264,7 +264,9 @@ script:
grunt ;
# Add all files to the git index and then run diff --cached to see all changes.
# This ensures that we get the status of all files, including new files.
# We ignore npm-shrinkwrap.json to make the tasks immune to npm changes.
git add . ;
git reset -- npm-shrinkwrap.json ;
git diff --cached --exit-code ;
fi
+2
View File
@@ -2097,6 +2097,7 @@
<VENDOR name="oracle" version="10.2" />
</DATABASE>
<PHP version="7.0.0" level="required">
<RESTRICT function="restrict_php_version_73" message="unsupportedphpversion73" />
</PHP>
<PCREUNICODE level="optional">
<FEEDBACK>
@@ -2273,6 +2274,7 @@
<VENDOR name="oracle" version="10.2" />
</DATABASE>
<PHP version="7.0.0" level="required">
<RESTRICT function="restrict_php_version_73" message="unsupportedphpversion73" />
</PHP>
<PCREUNICODE level="optional">
<FEEDBACK>
@@ -0,0 +1 @@
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates"],function(a,b,c,d,e,f,g){function h(b,d,h,j,k,l,m,n){null!==h&&l.forEach(function(a){a.id===h&&(a.selected=!0)}),null!==j&&m.forEach(function(a){a.id===j&&(a.selected=!0)});var o={contextlevel:d,categoryoptions:l,purposeoptions:m};null!==n&&n.length&&(null===k?o.newactivitydefaults=!0:n.forEach(function(a){k===a.name&&(a.selected=!0)}),o.modemodule=!0,o.activityoptions=n),e.create({title:b,body:g.render("tool_dataprivacy/category_purpose_form",o),type:e.types.SAVE_CANCEL,large:!0}).then(function(b){return b.getRoot().on(f.save,function(){var b=a("#activity"),c="undefined"!=typeof b?b.val():null,d=a("#override"),e="undefined"!=typeof d&&d.is(":checked");i(a("#contextlevel").val(),a("#category").val(),a("#purpose").val(),c,e)}),b.getRoot().on(f.hidden,function(){b.destroy()}),b.show(),b})["catch"](c.exception)}function i(a,c,d,e,f){var g={methodname:"tool_dataprivacy_set_context_defaults",args:{contextlevel:a,category:c,purpose:d,override:f,activity:e}};b.call([g])[0].done(function(a){a.result&&window.location.reload()})}var j={EDIT_LEVEL_DEFAULTS:'[data-action="edit-level-defaults"]',NEW_ACTIVITY_DEFAULTS:'[data-action="new-activity-defaults"]',EDIT_ACTIVITY_DEFAULTS:'[data-action="edit-activity-defaults"]',DELETE_ACTIVITY_DEFAULTS:'[data-action="delete-activity-defaults"]'},k=-1,l=function(){this.registerEvents()};return l.prototype.registerEvents=function(){a(j.EDIT_LEVEL_DEFAULTS).click(function(e){e.preventDefault();var f=a(this),g=f.data("contextlevel"),i=f.data("category"),j=f.data("purpose"),k=[{methodname:"tool_dataprivacy_get_category_options",args:{}},{methodname:"tool_dataprivacy_get_purpose_options",args:{}}],l=b.call(k),m=d.get_string("editdefaults","tool_dataprivacy",a("#defaults-header").text());a.when(l[0],l[1],m).then(function(a,b,c){var d=a.options,e=b.options;return h(c,g,i,j,null,d,e,null),!0})["catch"](c.exception)}),a(j.NEW_ACTIVITY_DEFAULTS).click(function(e){e.preventDefault();var f=a(this),g=f.data("contextlevel"),i=[{methodname:"tool_dataprivacy_get_category_options",args:{}},{methodname:"tool_dataprivacy_get_purpose_options",args:{}},{methodname:"tool_dataprivacy_get_activity_options",args:{nodefaults:!0}}],j=b.call(i),k=d.get_string("addnewdefaults","tool_dataprivacy");a.when(j[0],j[1],j[2],k).then(function(a,b,c,d){var e=a.options,f=b.options,i=c.options;return h(d,g,null,null,null,e,f,i),!0})["catch"](c.exception)}),a(j.EDIT_ACTIVITY_DEFAULTS).click(function(e){e.preventDefault();var f=a(this),g=f.data("contextlevel"),i=f.data("category"),j=f.data("purpose"),k=f.data("activityname"),l=[{methodname:"tool_dataprivacy_get_category_options",args:{}},{methodname:"tool_dataprivacy_get_purpose_options",args:{}},{methodname:"tool_dataprivacy_get_activity_options",args:{}}],m=b.call(l),n=d.get_string("editmoduledefaults","tool_dataprivacy");a.when(m[0],m[1],m[2],n).then(function(a,b,c,d){var e=a.options,f=b.options,l=c.options;return h(d,g,i,j,k,e,f,l),!0})["catch"](c.exception)}),a(j.DELETE_ACTIVITY_DEFAULTS).click(function(b){b.preventDefault();var h=a(this),j=h.data("contextlevel"),l=h.data("activityname"),m=h.data("activitydisplayname"),n=k,o=k;e.create({title:d.get_string("deletedefaults","tool_dataprivacy",m),body:g.render("tool_dataprivacy/delete_activity_defaults",{activityname:m}),type:e.types.SAVE_CANCEL,large:!0}).then(function(a){return a.setSaveButtonText(d.get_string("delete")),a.getRoot().on(f.save,function(){i(j,n,o,l,!1)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a.show(),!0})["catch"](c.exception)})},{init:function(){return new l}}});
+1 -1
View File
@@ -1 +1 @@
define([],function(){return{approve:"tool_dataprivacy-data_request:approve",deny:"tool_dataprivacy-data_request:deny",complete:"tool_dataprivacy-data_request:complete"}});
define([],function(){return{approve:"tool_dataprivacy-data_request:approve",bulkApprove:"tool_dataprivacy-data_request:bulk_approve",deny:"tool_dataprivacy-data_request:deny",bulkDeny:"tool_dataprivacy-data_request:bulk_deny",complete:"tool_dataprivacy-data_request:complete"}});
+1 -1
View File
@@ -1 +1 @@
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates","tool_dataprivacy/data_request_modal","tool_dataprivacy/events"],function(a,b,c,d,e,f,g,h,i){function j(a,b){var g=[],h="",j={requestid:b};switch(a){case i.approve:g=[{key:"approverequest",component:"tool_dataprivacy"},{key:"confirmapproval",component:"tool_dataprivacy"}],h="tool_dataprivacy_approve_data_request";break;case i.deny:g=[{key:"denyrequest",component:"tool_dataprivacy"},{key:"confirmdenial",component:"tool_dataprivacy"}],h="tool_dataprivacy_deny_data_request";break;case i.complete:g=[{key:"markcomplete",component:"tool_dataprivacy"},{key:"confirmcompletion",component:"tool_dataprivacy"}],h="tool_dataprivacy_mark_complete"}var l="";d.get_strings(g).then(function(a){l=a[0];var b=a[1];return e.create({title:l,body:b,type:e.types.SAVE_CANCEL})}).then(function(a){a.setSaveButtonText(l),a.getRoot().on(f.save,function(){k(h,j)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a.show()})["catch"](c.exception)}function k(a,d){var e={methodname:a,args:d};b.call([e])[0].done(function(a){a.result?window.location.reload():c.addNotification({message:a.warnings[0].message,type:"error"})}).fail(c.exception)}var l={APPROVE_REQUEST:'[data-action="approve"]',DENY_REQUEST:'[data-action="deny"]',VIEW_REQUEST:'[data-action="view"]',MARK_COMPLETE:'[data-action="complete"]'},m=function(){this.registerEvents()};return m.prototype.registerEvents=function(){a(l.VIEW_REQUEST).click(function(d){d.preventDefault();var l=a(this).data("requestid"),m={requestid:l},n={methodname:"tool_dataprivacy_get_data_request",args:m},o=b.call([n]);a.when(o[0]).then(function(a){return a.result?a.result:(c.addNotification({message:a.warnings[0].message,type:"error"}),!1)}).then(function(a){var b=e.types.DEFAULT;a.approvedeny?b=h.TYPE:a.canmarkcomplete&&(b=h.TYPE_ENQUIRY);var c=g.render("tool_dataprivacy/request_details",a);return e.create({title:a.typename,body:c,type:b,large:!0})}).then(function(a){a.getRoot().on(i.approve,function(){j(i.approve,l)}),a.getRoot().on(i.deny,function(){j(i.deny,l)}),a.getRoot().on(i.complete,function(){var a={requestid:l};k("tool_dataprivacy_mark_complete",a)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a.show()})["catch"](c.exception)}),a(l.APPROVE_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");j(i.approve,c)}),a(l.DENY_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");j(i.deny,c)}),a(l.MARK_COMPLETE).click(function(b){b.preventDefault(),j(i.complete,a(this).data("requestid"))})},m});
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates","tool_dataprivacy/data_request_modal","tool_dataprivacy/events"],function(a,b,c,d,e,f,g,h,i){function j(a){return{wsfunction:"tool_dataprivacy_approve_data_request",wsparams:{requestid:a}}}function k(a){return{wsfunction:"tool_dataprivacy_bulk_approve_data_requests",wsparams:{requestids:a}}}function l(a){return{wsfunction:"tool_dataprivacy_deny_data_request",wsparams:{requestid:a}}}function m(a){return{wsfunction:"tool_dataprivacy_bulk_deny_data_requests",wsparams:{requestids:a}}}function n(a){return{wsfunction:"tool_dataprivacy_mark_complete",wsparams:{requestid:a}}}function o(a,b){var g=[];switch(a){case i.approve:g=[{key:"approverequest",component:"tool_dataprivacy"},{key:"confirmapproval",component:"tool_dataprivacy"}];break;case i.bulkApprove:g=[{key:"bulkapproverequests",component:"tool_dataprivacy"},{key:"confirmbulkapproval",component:"tool_dataprivacy"}];break;case i.deny:g=[{key:"denyrequest",component:"tool_dataprivacy"},{key:"confirmdenial",component:"tool_dataprivacy"}];break;case i.bulkDeny:g=[{key:"bulkdenyrequests",component:"tool_dataprivacy"},{key:"confirmbulkdenial",component:"tool_dataprivacy"}];break;case i.complete:g=[{key:"markcomplete",component:"tool_dataprivacy"},{key:"confirmcompletion",component:"tool_dataprivacy"}]}var h="";d.get_strings(g).then(function(a){h=a[0];var b=a[1];return e.create({title:h,body:b,type:e.types.SAVE_CANCEL})}).then(function(a){a.setSaveButtonText(h),a.getRoot().on(f.save,function(){p(b.wsfunction,b.wsparams)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a.show()})["catch"](c.exception)}function p(a,d){var e={methodname:a,args:d};b.call([e])[0].done(function(a){a.result?window.location.reload():c.addNotification({message:a.warnings[0].message,type:"error"})}).fail(c.exception)}var q={APPROVE_REQUEST:'[data-action="approve"]',DENY_REQUEST:'[data-action="deny"]',VIEW_REQUEST:'[data-action="view"]',MARK_COMPLETE:'[data-action="complete"]',CHANGE_BULK_ACTION:'[id="bulk-action"]',CONFIRM_BULK_ACTION:'[id="confirm-bulk-action"]',SELECT_ALL:'[data-action="selectall"]'},r={APPROVE:1,DENY:2},s={SELECT_REQUEST:".selectrequests"},t=function(){this.registerEvents()};return t.prototype.registerEvents=function(){a(q.VIEW_REQUEST).click(function(d){d.preventDefault();var k=a(this).data("requestid"),m={requestid:k},n={methodname:"tool_dataprivacy_get_data_request",args:m},q=b.call([n]);a.when(q[0]).then(function(a){return a.result?a.result:(c.addNotification({message:a.warnings[0].message,type:"error"}),!1)}).then(function(a){var b=e.types.DEFAULT;a.approvedeny?b=h.TYPE:a.canmarkcomplete&&(b=h.TYPE_ENQUIRY);var c=g.render("tool_dataprivacy/request_details",a);return e.create({title:a.typename,body:c,type:b,large:!0})}).then(function(a){a.getRoot().on(i.approve,function(){o(i.approve,j(k))}),a.getRoot().on(i.deny,function(){o(i.deny,l(k))}),a.getRoot().on(i.complete,function(){var a={requestid:k};p("tool_dataprivacy_mark_complete",a)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a.show()})["catch"](c.exception)}),a(q.APPROVE_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");o(i.approve,j(c))}),a(q.DENY_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");o(i.deny,l(c))}),a(q.MARK_COMPLETE).click(function(b){b.preventDefault();var c=a(this).data("requestid");o(i.complete,n(c))}),a(q.CONFIRM_BULK_ACTION).click(function(){var b=[],e="",f={},g=[{key:"selectbulkaction",component:"tool_dataprivacy"},{key:"selectdatarequests",component:"tool_dataprivacy"},{key:"ok"}],h=parseInt(a("#bulk-action").val());if(h!=r.APPROVE&&h!=r.DENY)return void d.get_strings(g).done(function(a){c.alert("",a[0],a[2])}).fail(c.exception);if(a(".selectrequests:checked").each(function(){b.push(a(this).val())}),b.length<1)return void d.get_strings(g).done(function(a){c.alert("",a[1],a[2])}).fail(c.exception);switch(h){case r.APPROVE:e=i.bulkApprove,f=k(b);break;case r.DENY:e=i.bulkDeny,f=m(b)}o(e,f)}),a(q.SELECT_ALL).change(function(b){b.preventDefault();var c=a(this).is(":checked");a(s.SELECT_REQUEST).prop("checked",c)})},t});
@@ -0,0 +1,315 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* AMD module for data registry defaults actions.
*
* @module tool_dataprivacy/defaultsactions
* @package tool_dataprivacy
* @copyright 2018 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([
'jquery',
'core/ajax',
'core/notification',
'core/str',
'core/modal_factory',
'core/modal_events',
'core/templates'],
function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
/**
* List of action selectors.
*
* @type {{EDIT_LEVEL_DEFAULTS: string}}
* @type {{NEW_ACTIVITY_DEFAULTS: string}}
* @type {{EDIT_ACTIVITY_DEFAULTS: string}}
* @type {{DELETE_ACTIVITY_DEFAULTS: string}}
*/
var ACTIONS = {
EDIT_LEVEL_DEFAULTS: '[data-action="edit-level-defaults"]',
NEW_ACTIVITY_DEFAULTS: '[data-action="new-activity-defaults"]',
EDIT_ACTIVITY_DEFAULTS: '[data-action="edit-activity-defaults"]',
DELETE_ACTIVITY_DEFAULTS: '[data-action="delete-activity-defaults"]'
};
/** @type {{INHERIT: Number}} **/
var INHERIT = -1;
/**
* DefaultsActions class.
*/
var DefaultsActions = function() {
this.registerEvents();
};
/**
* Register event listeners.
*/
DefaultsActions.prototype.registerEvents = function() {
$(ACTIONS.EDIT_LEVEL_DEFAULTS).click(function(e) {
e.preventDefault();
var button = $(this);
var contextLevel = button.data('contextlevel');
var category = button.data('category');
var purpose = button.data('purpose');
// Get options.
var requests = [
{methodname: 'tool_dataprivacy_get_category_options', args: {}},
{methodname: 'tool_dataprivacy_get_purpose_options', args: {}}
];
var promises = Ajax.call(requests);
var titlePromise = Str.get_string('editdefaults', 'tool_dataprivacy', $('#defaults-header').text());
$.when(promises[0], promises[1], titlePromise).then(function(categoryResponse, purposeResponse, title) {
var categories = categoryResponse.options;
var purposes = purposeResponse.options;
showDefaultsFormModal(title, contextLevel, category, purpose, null, categories, purposes, null);
return true;
}).catch(Notification.exception);
});
$(ACTIONS.NEW_ACTIVITY_DEFAULTS).click(function(e) {
e.preventDefault();
var button = $(this);
var contextLevel = button.data('contextlevel');
// Get options.
var requests = [
{methodname: 'tool_dataprivacy_get_category_options', args: {}},
{methodname: 'tool_dataprivacy_get_purpose_options', args: {}},
{methodname: 'tool_dataprivacy_get_activity_options', args: {'nodefaults': true}}
];
var promises = Ajax.call(requests);
var titlePromise = Str.get_string('addnewdefaults', 'tool_dataprivacy');
$.when(promises[0], promises[1], promises[2], titlePromise).then(
function(categoryResponse, purposeResponse, activityResponse, title) {
var categories = categoryResponse.options;
var purposes = purposeResponse.options;
var activities = activityResponse.options;
showDefaultsFormModal(title, contextLevel, null, null, null, categories, purposes, activities);
return true;
}).catch(Notification.exception);
}
);
$(ACTIONS.EDIT_ACTIVITY_DEFAULTS).click(function(e) {
e.preventDefault();
var button = $(this);
var contextLevel = button.data('contextlevel');
var category = button.data('category');
var purpose = button.data('purpose');
var activity = button.data('activityname');
// Get options.
var requests = [
{methodname: 'tool_dataprivacy_get_category_options', args: {}},
{methodname: 'tool_dataprivacy_get_purpose_options', args: {}},
{methodname: 'tool_dataprivacy_get_activity_options', args: {}}
];
var promises = Ajax.call(requests);
var titlePromise = Str.get_string('editmoduledefaults', 'tool_dataprivacy');
$.when(promises[0], promises[1], promises[2], titlePromise).then(
function(categoryResponse, purposeResponse, activityResponse, title) {
var categories = categoryResponse.options;
var purposes = purposeResponse.options;
var activities = activityResponse.options;
showDefaultsFormModal(title, contextLevel, category, purpose, activity, categories, purposes, activities);
return true;
}).catch(Notification.exception);
}
);
$(ACTIONS.DELETE_ACTIVITY_DEFAULTS).click(function(e) {
e.preventDefault();
var button = $(this);
var contextLevel = button.data('contextlevel');
var activity = button.data('activityname');
var activityDisplayName = button.data('activitydisplayname');
// Set category and purpose to inherit (-1).
var category = INHERIT;
var purpose = INHERIT;
ModalFactory.create({
title: Str.get_string('deletedefaults', 'tool_dataprivacy', activityDisplayName),
body: Templates.render('tool_dataprivacy/delete_activity_defaults', {"activityname": activityDisplayName}),
type: ModalFactory.types.SAVE_CANCEL,
large: true
}).then(function(modal) {
modal.setSaveButtonText(Str.get_string('delete'));
// Handle save event.
modal.getRoot().on(ModalEvents.save, function() {
setContextDefaults(contextLevel, category, purpose, activity, false);
});
// Handle hidden event.
modal.getRoot().on(ModalEvents.hidden, function() {
// Destroy when hidden.
modal.destroy();
});
modal.show();
return true;
}).catch(Notification.exception);
});
};
/**
* Prepares and renders the modal for setting the defaults for the given context level/plugin.
*
* @param {String} title The modal's title.
* @param {Number} contextLevel The context level to set defaults for.
* @param {Number} category The current category ID.
* @param {Number} purpose The current purpose ID.
* @param {String} activity The plugin name of the activity. Optional.
* @param {Array} categoryOptions The list of category options.
* @param {Array} purposeOptions The list of purpose options.
* @param {Array} activityOptions The list of activity options. Optional.
*/
function showDefaultsFormModal(title, contextLevel, category, purpose, activity,
categoryOptions, purposeOptions, activityOptions) {
if (category !== null) {
categoryOptions.forEach(function(currentValue) {
if (currentValue.id === category) {
currentValue.selected = true;
}
});
}
if (purpose !== null) {
purposeOptions.forEach(function(currentValue) {
if (currentValue.id === purpose) {
currentValue.selected = true;
}
});
}
var templateContext = {
"contextlevel": contextLevel,
"categoryoptions": categoryOptions,
"purposeoptions": purposeOptions
};
// Check the activityOptions parameter that was passed.
if (activityOptions !== null && activityOptions.length) {
// Check the activity parameter that was passed.
if (activity === null) {
// We're setting a new defaults for a module.
templateContext.newactivitydefaults = true;
} else {
// Edit mode. Set selection.
activityOptions.forEach(function(currentValue) {
if (activity === currentValue.name) {
currentValue.selected = true;
}
});
}
templateContext.modemodule = true;
templateContext.activityoptions = activityOptions;
}
ModalFactory.create({
title: title,
body: Templates.render('tool_dataprivacy/category_purpose_form', templateContext),
type: ModalFactory.types.SAVE_CANCEL,
large: true
}).then(function(modal) {
// Handle save event.
modal.getRoot().on(ModalEvents.save, function() {
var activity = $('#activity');
var activityVal = typeof activity !== 'undefined' ? activity.val() : null;
var override = $('#override');
var overrideVal = typeof override !== 'undefined' ? override.is(':checked') : false;
setContextDefaults($('#contextlevel').val(), $('#category').val(), $('#purpose').val(), activityVal, overrideVal);
});
// Handle hidden event.
modal.getRoot().on(ModalEvents.hidden, function() {
// Destroy when hidden.
modal.destroy();
});
modal.show();
return modal;
}).catch(Notification.exception);
}
/**
* Calls a the tool_dataprivacy_set_context_defaults WS function.
*
* @param {Number} contextLevel The context level.
* @param {Number} category The category ID.
* @param {Number} purpose The purpose ID.
* @param {String} activity The plugin name of the activity module.
* @param {Boolean} override Whether to override custom instances.
*/
function setContextDefaults(contextLevel, category, purpose, activity, override) {
var request = {
methodname: 'tool_dataprivacy_set_context_defaults',
args: {
'contextlevel': contextLevel,
'category': category,
'purpose': purpose,
'override': override,
'activity': activity
}
};
Ajax.call([request])[0].done(function(data) {
if (data.result) {
window.location.reload();
}
});
}
return /** @alias module:tool_dataprivacy/defaultsactions */ {
// Public variables and functions.
/**
* Initialise the module.
*
* @method init
* @return {DefaultsActions}
*/
'init': function() {
return new DefaultsActions();
}
};
});
+2
View File
@@ -25,7 +25,9 @@
define([], function() {
return {
approve: 'tool_dataprivacy-data_request:approve',
bulkApprove: 'tool_dataprivacy-data_request:bulk_approve',
deny: 'tool_dataprivacy-data_request:deny',
bulkDeny: 'tool_dataprivacy-data_request:bulk_deny',
complete: 'tool_dataprivacy-data_request:complete'
};
});
+187 -16
View File
@@ -40,12 +40,38 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
* @type {{DENY_REQUEST: string}}
* @type {{VIEW_REQUEST: string}}
* @type {{MARK_COMPLETE: string}}
* @type {{CHANGE_BULK_ACTION: string}}
* @type {{CONFIRM_BULK_ACTION: string}}
* @type {{SELECT_ALL: string}}
*/
var ACTIONS = {
APPROVE_REQUEST: '[data-action="approve"]',
DENY_REQUEST: '[data-action="deny"]',
VIEW_REQUEST: '[data-action="view"]',
MARK_COMPLETE: '[data-action="complete"]'
MARK_COMPLETE: '[data-action="complete"]',
CHANGE_BULK_ACTION: '[id="bulk-action"]',
CONFIRM_BULK_ACTION: '[id="confirm-bulk-action"]',
SELECT_ALL: '[data-action="selectall"]'
};
/**
* List of available bulk actions.
*
* @type {{APPROVE: number}}
* @type {{DENY: number}}
*/
var BULK_ACTIONS = {
APPROVE: 1,
DENY: 2
};
/**
* List of selectors.
*
* @type {{SELECT_REQUEST: string}}
*/
var SELECTORS = {
SELECT_REQUEST: '.selectrequests'
};
/**
@@ -105,12 +131,12 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
}).then(function(modal) {
// Handle approve event.
modal.getRoot().on(DataPrivacyEvents.approve, function() {
showConfirmation(DataPrivacyEvents.approve, requestId);
showConfirmation(DataPrivacyEvents.approve, approveEventWsData(requestId));
});
// Handle deny event.
modal.getRoot().on(DataPrivacyEvents.deny, function() {
showConfirmation(DataPrivacyEvents.deny, requestId);
showConfirmation(DataPrivacyEvents.deny, denyEventWsData(requestId));
});
// Handle send event.
@@ -139,34 +165,158 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
e.preventDefault();
var requestId = $(this).data('requestid');
showConfirmation(DataPrivacyEvents.approve, requestId);
showConfirmation(DataPrivacyEvents.approve, approveEventWsData(requestId));
});
$(ACTIONS.DENY_REQUEST).click(function(e) {
e.preventDefault();
var requestId = $(this).data('requestid');
showConfirmation(DataPrivacyEvents.deny, requestId);
showConfirmation(DataPrivacyEvents.deny, denyEventWsData(requestId));
});
$(ACTIONS.MARK_COMPLETE).click(function(e) {
e.preventDefault();
showConfirmation(DataPrivacyEvents.complete, $(this).data('requestid'));
var requestId = $(this).data('requestid');
showConfirmation(DataPrivacyEvents.complete, completeEventWsData(requestId));
});
$(ACTIONS.CONFIRM_BULK_ACTION).click(function() {
var requestIds = [];
var actionEvent = '';
var wsdata = {};
var bulkActionKeys = [
{
key: 'selectbulkaction',
component: 'tool_dataprivacy'
},
{
key: 'selectdatarequests',
component: 'tool_dataprivacy'
},
{
key: 'ok'
}
];
var bulkaction = parseInt($('#bulk-action').val());
if (bulkaction != BULK_ACTIONS.APPROVE && bulkaction != BULK_ACTIONS.DENY) {
Str.get_strings(bulkActionKeys).done(function(langStrings) {
Notification.alert('', langStrings[0], langStrings[2]);
}).fail(Notification.exception);
return;
}
$(".selectrequests:checked").each(function() {
requestIds.push($(this).val());
});
if (requestIds.length < 1) {
Str.get_strings(bulkActionKeys).done(function(langStrings) {
Notification.alert('', langStrings[1], langStrings[2]);
}).fail(Notification.exception);
return;
}
switch (bulkaction) {
case BULK_ACTIONS.APPROVE:
actionEvent = DataPrivacyEvents.bulkApprove;
wsdata = bulkApproveEventWsData(requestIds);
break;
case BULK_ACTIONS.DENY:
actionEvent = DataPrivacyEvents.bulkDeny;
wsdata = bulkDenyEventWsData(requestIds);
}
showConfirmation(actionEvent, wsdata);
});
$(ACTIONS.SELECT_ALL).change(function(e) {
e.preventDefault();
var selectAll = $(this).is(':checked');
$(SELECTORS.SELECT_REQUEST).prop('checked', selectAll);
});
};
/**
* Return the webservice data for the approve request action.
*
* @param {Number} requestId The ID of the request.
* @return {Object}
*/
function approveEventWsData(requestId) {
return {
'wsfunction': 'tool_dataprivacy_approve_data_request',
'wsparams': {'requestid': requestId}
};
}
/**
* Return the webservice data for the bulk approve request action.
*
* @param {Array} requestIds The array of request ID's.
* @return {Object}
*/
function bulkApproveEventWsData(requestIds) {
return {
'wsfunction': 'tool_dataprivacy_bulk_approve_data_requests',
'wsparams': {'requestids': requestIds}
};
}
/**
* Return the webservice data for the deny request action.
*
* @param {Number} requestId The ID of the request.
* @return {Object}
*/
function denyEventWsData(requestId) {
return {
'wsfunction': 'tool_dataprivacy_deny_data_request',
'wsparams': {'requestid': requestId}
};
}
/**
* Return the webservice data for the bulk deny request action.
*
* @param {Array} requestIds The array of request ID's.
* @return {Object}
*/
function bulkDenyEventWsData(requestIds) {
return {
'wsfunction': 'tool_dataprivacy_bulk_deny_data_requests',
'wsparams': {'requestids': requestIds}
};
}
/**
* Return the webservice data for the complete request action.
*
* @param {Number} requestId The ID of the request.
* @return {Object}
*/
function completeEventWsData(requestId) {
return {
'wsfunction': 'tool_dataprivacy_mark_complete',
'wsparams': {'requestid': requestId}
};
}
/**
* Show the confirmation dialogue.
*
* @param {String} action The action name.
* @param {Number} requestId The request ID.
* @param {Object} wsdata Object containing ws data.
*/
function showConfirmation(action, requestId) {
function showConfirmation(action, wsdata) {
var keys = [];
var wsfunction = '';
var params = {
'requestid': requestId
};
switch (action) {
case DataPrivacyEvents.approve:
keys = [
@@ -179,7 +329,18 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
component: 'tool_dataprivacy'
}
];
wsfunction = 'tool_dataprivacy_approve_data_request';
break;
case DataPrivacyEvents.bulkApprove:
keys = [
{
key: 'bulkapproverequests',
component: 'tool_dataprivacy'
},
{
key: 'confirmbulkapproval',
component: 'tool_dataprivacy'
}
];
break;
case DataPrivacyEvents.deny:
keys = [
@@ -192,7 +353,18 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
component: 'tool_dataprivacy'
}
];
wsfunction = 'tool_dataprivacy_deny_data_request';
break;
case DataPrivacyEvents.bulkDeny:
keys = [
{
key: 'bulkdenyrequests',
component: 'tool_dataprivacy'
},
{
key: 'confirmbulkdenial',
component: 'tool_dataprivacy'
}
];
break;
case DataPrivacyEvents.complete:
keys = [
@@ -205,7 +377,6 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
component: 'tool_dataprivacy'
}
];
wsfunction = 'tool_dataprivacy_mark_complete';
break;
}
@@ -223,7 +394,7 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
// Handle save event.
modal.getRoot().on(ModalEvents.save, function() {
handleSave(wsfunction, params);
handleSave(wsdata.wsfunction, wsdata.wsparams);
});
// Handle hidden event.
+1
View File
@@ -33,6 +33,7 @@ $title = get_string('editcategories', 'tool_dataprivacy');
$output = $PAGE->get_renderer('tool_dataprivacy');
echo $output->header();
echo $output->heading($title);
$categories = \tool_dataprivacy\api::get_categories();
$renderable = new \tool_dataprivacy\output\categories($categories);
+179 -80
View File
@@ -24,7 +24,7 @@
namespace tool_dataprivacy;
use coding_exception;
use context_course;
use context_helper;
use context_system;
use core\invalid_persistent_exception;
use core\message\message;
@@ -41,6 +41,7 @@ use tool_dataprivacy\external\data_request_exporter;
use tool_dataprivacy\local\helper;
use tool_dataprivacy\task\initiate_data_request_task;
use tool_dataprivacy\task\process_data_request_task;
use tool_dataprivacy\data_request;
defined('MOODLE_INTERNAL') || die();
@@ -94,6 +95,12 @@ class api {
/** Data delete request completed, account is removed. */
const DATAREQUEST_STATUS_DELETED = 10;
/** Approve data request. */
const DATAREQUEST_ACTION_APPROVE = 1;
/** Reject data request. */
const DATAREQUEST_ACTION_REJECT = 2;
/**
* Determines whether the user can contact the site's Data Protection Officer via Moodle.
*
@@ -105,18 +112,14 @@ class api {
}
/**
* Check's whether the current user has the capability to manage data requests.
* Checks whether the current user has the capability to manage data requests.
*
* @param int $userid The user ID.
* @return bool
* @throws coding_exception
* @throws dml_exception
*/
public static function can_manage_data_requests($userid) {
$context = context_system::instance();
// A user can manage data requests if he/she has the site DPO role and has the capability to manage data requests.
return self::is_site_dpo($userid) && has_capability('tool/dataprivacy:managedatarequests', $context, $userid);
// Privacy officers can manage data requests.
return self::is_site_dpo($userid);
}
/**
@@ -136,6 +139,31 @@ class api {
require_capability('tool/dataprivacy:managedataregistry', $context);
}
/**
* Fetches the list of configured privacy officer roles.
*
* Every time this function is called, it checks each role if they have the 'managedatarequests' capability and removes
* any role that doesn't have the required capability anymore.
*
* @return int[]
* @throws dml_exception
*/
public static function get_assigned_privacy_officer_roles() {
$roleids = [];
// Get roles from config.
$configroleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
if (!empty($configroleids)) {
// Fetch roles that have the capability to manage data requests.
$capableroles = array_keys(get_roles_with_capability('tool/dataprivacy:managedatarequests'));
// Extract the configured roles that have the capability from the list of capable roles.
$roleids = array_intersect($capableroles, $configroleids);
}
return $roleids;
}
/**
* Fetches the role shortnames of Data Protection Officer roles.
*
@@ -144,7 +172,7 @@ class api {
public static function get_dpo_role_names() {
global $DB;
$dporoleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
$dporoleids = self::get_assigned_privacy_officer_roles();
$dponames = array();
if (!empty($dporoleids)) {
@@ -156,20 +184,15 @@ class api {
}
/**
* Fetches the list of users with the Data Protection Officer role.
*
* @throws dml_exception
* Fetches the list of users with the Privacy Officer role.
*/
public static function get_site_dpos() {
// Get role(s) that can manage data requests.
$dporoles = explode(',', get_config('tool_dataprivacy', 'dporoles'));
$dporoles = self::get_assigned_privacy_officer_roles();
$dpos = [];
$context = context_system::instance();
foreach ($dporoles as $roleid) {
if (empty($roleid)) {
continue;
}
$allnames = get_all_user_name_fields(true, 'u');
$fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
@@ -189,15 +212,14 @@ class api {
}
/**
* Checks whether a given user is a site DPO.
* Checks whether a given user is a site Privacy Officer.
*
* @param int $userid The user ID.
* @return bool
* @throws dml_exception
*/
public static function is_site_dpo($userid) {
$dpos = self::get_site_dpos();
return array_key_exists($userid, $dpos);
return array_key_exists($userid, $dpos) || is_siteadmin();
}
/**
@@ -206,33 +228,24 @@ class api {
* @param int $foruser The user whom the request is being made for.
* @param int $type The request type.
* @param string $comments Request comments.
* @param int $creationmethod The creation method of the data request.
* @return data_request
* @throws invalid_persistent_exception
* @throws coding_exception
*/
public static function create_data_request($foruser, $type, $comments = '') {
public static function create_data_request($foruser, $type, $comments = '',
$creationmethod = data_request::DATAREQUEST_CREATION_MANUAL) {
global $USER;
$datarequest = new data_request();
// The user the request is being made for.
$datarequest->set('userid', $foruser);
$requestinguser = $USER->id;
// Check when the user is making a request on behalf of another.
if ($requestinguser != $foruser) {
if (self::is_site_dpo($requestinguser)) {
// The user making the request is a DPO. Should be fine.
$datarequest->set('dpo', $requestinguser);
} else {
// If not a DPO, only users with the capability to make data requests for the user should be allowed.
// (e.g. users with the Parent role, etc).
if (!self::can_create_data_request_for_user($foruser)) {
$forusercontext = \context_user::instance($foruser);
throw new required_capability_exception($forusercontext,
'tool/dataprivacy:makedatarequestsforchildren', 'nopermissions', '');
}
}
}
// The cron is considered to be a guest user when it creates a data request.
// NOTE: This should probably be changed. We should leave the default value for $requestinguser if
// the request is not explicitly created by a specific user.
$requestinguser = (isguestuser() && $creationmethod == data_request::DATAREQUEST_CREATION_AUTO) ?
get_admin()->id : $USER->id;
// The user making the request.
$datarequest->set('requestedby', $requestinguser);
// Set status.
@@ -241,6 +254,8 @@ class api {
$datarequest->set('type', $type);
// Set request comments.
$datarequest->set('comments', $comments);
// Set the creation method.
$datarequest->set('creationmethod', $creationmethod);
// Store subject access request.
$datarequest->create();
@@ -263,6 +278,7 @@ class api {
* @param int $userid The User ID.
* @param int[] $statuses The status filters.
* @param int[] $types The request type filters.
* @param int[] $creationmethods The request creation method filters.
* @param string $sort The order by clause.
* @param int $offset Amount of records to skip.
* @param int $limit Amount of records to fetch.
@@ -270,7 +286,8 @@ class api {
* @throws coding_exception
* @throws dml_exception
*/
public static function get_data_requests($userid = 0, $statuses = [], $types = [], $sort = '', $offset = 0, $limit = 0) {
public static function get_data_requests($userid = 0, $statuses = [], $types = [], $creationmethods = [],
$sort = '', $offset = 0, $limit = 0) {
global $DB, $USER;
$results = [];
$sqlparams = [];
@@ -294,6 +311,13 @@ class api {
$sqlparams = array_merge($sqlparams, $typeparams);
}
// Set request creation method filter.
if (!empty($creationmethods)) {
list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED);
$sqlconditions[] = "creationmethod $typeinsql";
$sqlparams = array_merge($sqlparams, $typeparams);
}
if ($userid) {
// Get the data requests for the user or data requests made by the user.
$sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
@@ -336,7 +360,7 @@ class api {
if (!empty($expiredrequests)) {
data_request::expire($expiredrequests);
$results = self::get_data_requests($userid, $statuses, $types, $sort, $offset, $limit);
$results = self::get_data_requests($userid, $statuses, $types, $creationmethods, $sort, $offset, $limit);
}
}
@@ -349,11 +373,12 @@ class api {
* @param int $userid The User ID.
* @param int[] $statuses The status filters.
* @param int[] $types The request type filters.
* @param int[] $creationmethods The request creation method filters.
* @return int
* @throws coding_exception
* @throws dml_exception
*/
public static function get_data_requests_count($userid = 0, $statuses = [], $types = []) {
public static function get_data_requests_count($userid = 0, $statuses = [], $types = [], $creationmethods = []) {
global $DB, $USER;
$count = 0;
$sqlparams = [];
@@ -367,6 +392,11 @@ class api {
$sqlconditions[] = "type $typeinsql";
$sqlparams = array_merge($sqlparams, $typeparams);
}
if (!empty($creationmethods)) {
list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED);
$sqlconditions[] = "creationmethod $typeinsql";
$sqlparams = array_merge($sqlparams, $typeparams);
}
if ($userid) {
// Get the data requests for the user or data requests made by the user.
$sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
@@ -616,6 +646,8 @@ class api {
'requestedby' => $requestedby->fullname,
'requesttype' => $typetext,
'requestdate' => userdate($requestdata->timecreated),
'requestorigin' => $SITE->fullname,
'requestoriginurl' => new moodle_url('/'),
'requestcomments' => $requestdata->messagehtml,
'datarequestsurl' => $datarequestsurl
];
@@ -641,16 +673,31 @@ class api {
/**
* Checks whether a non-DPO user can make a data request for another user.
*
* @param int $user The user ID of the target user.
* @param int $requester The user ID of the user making the request.
* @return bool
* @throws coding_exception
* @param int $user The user ID of the target user.
* @param int $requester The user ID of the user making the request.
* @return bool
*/
public static function can_create_data_request_for_user($user, $requester = null) {
$usercontext = \context_user::instance($user);
return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
}
/**
* Require that the current user can make a data request for the specified other user.
*
* @param int $user The user ID of the target user.
* @param int $requester The user ID of the user making the request.
* @return bool
*/
public static function require_can_create_data_request_for_user($user, $requester = null) {
$usercontext = \context_user::instance($user);
require_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
return true;
}
/**
* Checks whether a user can download a data request.
*
@@ -706,8 +753,6 @@ class api {
* @return \tool_dataprivacy\purpose.
*/
public static function create_purpose(stdClass $record) {
self::check_can_manage_data_registry();
$purpose = new purpose(0, $record);
$purpose->create();
@@ -721,8 +766,6 @@ class api {
* @return \tool_dataprivacy\purpose.
*/
public static function update_purpose(stdClass $record) {
self::check_can_manage_data_registry();
if (!isset($record->sensitivedatareasons)) {
$record->sensitivedatareasons = '';
}
@@ -742,8 +785,6 @@ class api {
* @return bool
*/
public static function delete_purpose($id) {
self::check_can_manage_data_registry();
$purpose = new purpose($id);
if ($purpose->is_used()) {
throw new \moodle_exception('Purpose with id ' . $id . ' can not be deleted because it is used.');
@@ -757,8 +798,6 @@ class api {
* @return \tool_dataprivacy\purpose[]
*/
public static function get_purposes() {
self::check_can_manage_data_registry();
return purpose::get_records([], 'name', 'ASC');
}
@@ -769,8 +808,6 @@ class api {
* @return \tool_dataprivacy\category.
*/
public static function create_category(stdClass $record) {
self::check_can_manage_data_registry();
$category = new category(0, $record);
$category->create();
@@ -784,8 +821,6 @@ class api {
* @return \tool_dataprivacy\category.
*/
public static function update_category(stdClass $record) {
self::check_can_manage_data_registry();
$category = new category($record->id);
$category->from_record($record);
@@ -801,8 +836,6 @@ class api {
* @return bool
*/
public static function delete_category($id) {
self::check_can_manage_data_registry();
$category = new category($id);
if ($category->is_used()) {
throw new \moodle_exception('Category with id ' . $id . ' can not be deleted because it is used.');
@@ -816,8 +849,6 @@ class api {
* @return \tool_dataprivacy\category[]
*/
public static function get_categories() {
self::check_can_manage_data_registry();
return category::get_records([], 'name', 'ASC');
}
@@ -828,8 +859,6 @@ class api {
* @return \tool_dataprivacy\context_instance
*/
public static function set_context_instance($record) {
self::check_can_manage_data_registry($record->contextid);
if ($instance = context_instance::get_record_by_contextid($record->contextid, false)) {
// Update.
$instance->from_record($record);
@@ -856,7 +885,6 @@ class api {
* @return null
*/
public static function unset_context_instance(context_instance $instance) {
self::check_can_manage_data_registry($instance->get('contextid'));
$instance->delete();
}
@@ -870,9 +898,6 @@ class api {
public static function set_contextlevel($record) {
global $DB;
// Only manager at system level can set this.
self::check_can_manage_data_registry();
if ($record->contextlevel != CONTEXT_SYSTEM && $record->contextlevel != CONTEXT_USER) {
throw new \coding_exception('Only context system and context user can set a contextlevel ' .
'purpose and retention');
@@ -903,8 +928,7 @@ class api {
* @param int $forcedvalue Use this categoryid value as if this was this context instance category.
* @return category|false
*/
public static function get_effective_context_category(\context $context, $forcedvalue=false) {
self::check_can_manage_data_registry($context->id);
public static function get_effective_context_category(\context $context, $forcedvalue = false) {
if (!data_registry::defaults_set()) {
return false;
}
@@ -919,8 +943,7 @@ class api {
* @param int $forcedvalue Use this purposeid value as if this was this context instance purpose.
* @return purpose|false
*/
public static function get_effective_context_purpose(\context $context, $forcedvalue=false) {
self::check_can_manage_data_registry($context->id);
public static function get_effective_context_purpose(\context $context, $forcedvalue = false) {
if (!data_registry::defaults_set()) {
return false;
}
@@ -932,16 +955,14 @@ class api {
* Returns the effective category given a context level.
*
* @param int $contextlevel
* @param int $forcedvalue Use this categoryid value as if this was this context level category.
* @return category|false
*/
public static function get_effective_contextlevel_category($contextlevel, $forcedvalue=false) {
self::check_can_manage_data_registry(\context_system::instance()->id);
public static function get_effective_contextlevel_category($contextlevel) {
if (!data_registry::defaults_set()) {
return false;
}
return data_registry::get_effective_contextlevel_value($contextlevel, 'category', $forcedvalue);
return data_registry::get_effective_contextlevel_value($contextlevel, 'category');
}
/**
@@ -952,7 +973,6 @@ class api {
* @return purpose|false
*/
public static function get_effective_contextlevel_purpose($contextlevel, $forcedvalue=false) {
self::check_can_manage_data_registry(\context_system::instance()->id);
if (!data_registry::defaults_set()) {
return false;
}
@@ -967,8 +987,6 @@ class api {
* @return \tool_dataprivacy\expired_context
*/
public static function create_expired_context($contextid) {
self::check_can_manage_data_registry();
$record = (object)[
'contextid' => $contextid,
'status' => expired_context::STATUS_EXPIRED,
@@ -986,8 +1004,6 @@ class api {
* @return bool True on success.
*/
public static function delete_expired_context($id) {
self::check_can_manage_data_registry();
$expiredcontext = new expired_context($id);
return $expiredcontext->delete();
}
@@ -1000,8 +1016,6 @@ class api {
* @return null
*/
public static function set_expired_context_status(expired_context $expiredctx, $status) {
self::check_can_manage_data_registry();
$expiredctx->set('status', $status);
$expiredctx->save();
}
@@ -1138,4 +1152,89 @@ class api {
return $approvedcollection;
}
/**
* Updates the default category and purpose for a given context level (and optionally, a plugin).
*
* @param int $contextlevel The context level.
* @param int $categoryid The ID matching the category.
* @param int $purposeid The ID matching the purpose record.
* @param int $activity The name of the activity that we're making a defaults configuration for.
* @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
* @return boolean True if set/unset config succeeds. Otherwise, it throws an exception.
*/
public static function set_context_defaults($contextlevel, $categoryid, $purposeid, $activity = null, $override = false) {
global $DB;
// Get the class name associated with this context level.
$classname = context_helper::get_class_for_level($contextlevel);
list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname, $activity);
// Check the default category to be set.
if ($categoryid == context_instance::INHERIT) {
unset_config($categoryvar, 'tool_dataprivacy');
} else {
// Make sure the given category ID exists first.
$categorypersistent = new category($categoryid);
$categorypersistent->read();
// Then set the new default value.
set_config($categoryvar, $categoryid, 'tool_dataprivacy');
}
// Check the default purpose to be set.
if ($purposeid == context_instance::INHERIT) {
// If the defaults is set to inherit, just unset the config value.
unset_config($purposevar, 'tool_dataprivacy');
} else {
// Make sure the given purpose ID exists first.
$purposepersistent = new purpose($purposeid);
$purposepersistent->read();
// Then set the new default value.
set_config($purposevar, $purposeid, 'tool_dataprivacy');
}
// Unset instances that have been assigned with custom purpose and category, if override was specified.
if ($override) {
// We'd like to find context IDs that we want to unset.
$statements = ["SELECT c.id as contextid FROM {context} c"];
// Based on this context level.
$params = ['contextlevel' => $contextlevel];
if ($contextlevel == CONTEXT_MODULE) {
// If we're deleting module context instances, we need to make sure the instance ID is in the course modules table.
$statements[] = "JOIN {course_modules} cm ON cm.id = c.instanceid";
// And that the module is listed on the modules table.
$statements[] = "JOIN {modules} m ON m.id = cm.module";
if ($activity) {
// If we're overriding for an activity module, make sure that the context instance matches that activity.
$statements[] = "AND m.name = :modname";
$params['modname'] = $activity;
}
}
// Make sure this context instance exists in the tool_dataprivacy_ctxinstance table.
$statements[] = "JOIN {tool_dataprivacy_ctxinstance} tdc ON tdc.contextid = c.id";
// And that the context level of this instance matches the given context level.
$statements[] = "WHERE c.contextlevel = :contextlevel";
// Build our SQL query by gluing the statements.
$sql = implode("\n", $statements);
// Get the context records matching our query.
$contextids = $DB->get_fieldset_sql($sql, $params);
// Delete the matching context instances.
foreach ($contextids as $contextid) {
if ($instance = context_instance::get_record_by_contextid($contextid, false)) {
self::unset_context_instance($instance);
}
}
}
return true;
}
}
+126 -125
View File
@@ -28,10 +28,7 @@
namespace tool_dataprivacy;
use coding_exception;
use tool_dataprivacy\purpose;
use tool_dataprivacy\category;
use tool_dataprivacy\contextlevel;
use tool_dataprivacy\context_instance;
use core\persistent;
defined('MOODLE_INTERNAL') || die();
@@ -44,28 +41,25 @@ require_once($CFG->libdir . '/coursecatlib.php');
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class data_registry {
/**
* @var array Inheritance between context levels.
*/
private static $contextlevelinheritance = [
CONTEXT_USER => [CONTEXT_SYSTEM],
CONTEXT_COURSECAT => [CONTEXT_SYSTEM],
CONTEXT_COURSE => [CONTEXT_COURSECAT, CONTEXT_SYSTEM],
CONTEXT_MODULE => [CONTEXT_COURSE, CONTEXT_COURSECAT, CONTEXT_SYSTEM],
CONTEXT_BLOCK => [CONTEXT_COURSE, CONTEXT_COURSECAT, CONTEXT_SYSTEM],
];
/**
* Returns purpose and category var names from a context class name
*
* @param string $classname
* @param string $classname The context level's class.
* @param string $pluginname The name of the plugin associated with the context level.
* @return string[]
*/
public static function var_names_from_context($classname) {
public static function var_names_from_context($classname, $pluginname = '') {
$pluginname = trim($pluginname);
if (!empty($pluginname)) {
$categoryvar = $classname . '_' . $pluginname . '_category';
$purposevar = $classname . '_' . $pluginname . '_purpose';
} else {
$categoryvar = $classname . '_category';
$purposevar = $classname . '_purpose';
}
return [
$classname . '_purpose',
$classname . '_category',
$purposevar,
$categoryvar
];
}
@@ -74,22 +68,35 @@ class data_registry {
*
* The caller code is responsible of checking that $contextlevel is an integer.
*
* @param int $contextlevel
* @return int|false[]
* @param int $contextlevel The context level.
* @param string $pluginname The name of the plugin associated with the context level.
* @return int[]|false[]
*/
public static function get_defaults($contextlevel) {
public static function get_defaults($contextlevel, $pluginname = '') {
$classname = \context_helper::get_class_for_level($contextlevel);
list($purposevar, $categoryvar) = self::var_names_from_context($classname);
list($purposevar, $categoryvar) = self::var_names_from_context($classname, $pluginname);
$purposeid = get_config('tool_dataprivacy', $purposevar);
$categoryid = get_config('tool_dataprivacy', $categoryvar);
if (!empty($pluginname)) {
list($purposevar, $categoryvar) = self::var_names_from_context($classname);
// If the plugin-level doesn't have a default purpose set, try the context level.
if ($purposeid == false) {
$purposeid = get_config('tool_dataprivacy', $purposevar);
}
// If the plugin-level doesn't have a default category set, try the context level.
if ($categoryid == false) {
$categoryid = get_config('tool_dataprivacy', $categoryvar);
}
}
if (empty($purposeid)) {
$purposeid = false;
$purposeid = context_instance::NOTSET;
}
if (empty($categoryid)) {
$categoryid = false;
$categoryid = context_instance::NOTSET;
}
return [$purposeid, $categoryid];
@@ -171,55 +178,93 @@ class data_registry {
* @param int|false $forcedvalue Use this value as if this was this context instance value.
* @return persistent|false It return a 'purpose' instance or a 'category' instance, depending on $element
*/
public static function get_effective_context_value(\context $context, $element, $forcedvalue=false) {
public static function get_effective_context_value(\context $context, $element, $forcedvalue = false) {
global $DB;
if ($element !== 'purpose' && $element !== 'category') {
throw new coding_exception('Only \'purpose\' and \'category\' are supported.');
}
$fieldname = $element . 'id';
if ($forcedvalue === false) {
$instance = context_instance::get_record_by_contextid($context->id, false);
if (!$instance) {
// If the instance does not have a value defaults to not set, so we grab the context level default as its value.
$instancevalue = context_instance::NOTSET;
} else {
$instancevalue = $instance->get($fieldname);
}
if (!empty($forcedvalue) && ($forcedvalue == context_instance::INHERIT)) {
// Do not include the current context when calculating the value.
// This has the effect that an inheritted value is calculated.
$parentcontextids = $context->get_parent_context_ids(false);
} else if (!empty($forcedvalue) && ($forcedvalue != context_instance::NOTSET)) {
return self::get_element_instance($element, $forcedvalue);
} else {
$instancevalue = $forcedvalue;
// Fetch all parent contexts, including self.
$parentcontextids = $context->get_parent_context_ids(true);
}
list($insql, $inparams) = $DB->get_in_or_equal($parentcontextids, SQL_PARAMS_NAMED);
$inparams['contextmodule'] = CONTEXT_MODULE;
// Not set.
if ($instancevalue == context_instance::NOTSET) {
if ('purpose' === $element) {
$elementjoin = 'LEFT JOIN {tool_dataprivacy_purpose} ele ON ctxins.purposeid = ele.id';
$elementfields = purpose::get_sql_fields('ele', 'ele');
} else {
$elementjoin = 'LEFT JOIN {tool_dataprivacy_category} ele ON ctxins.categoryid = ele.id';
$elementfields = category::get_sql_fields('ele', 'ele');
}
$contextfields = \context_helper::get_preload_record_columns_sql('ctx');
$fields = implode(', ', ['ctx.id', 'm.name AS modname', $contextfields, $elementfields]);
// The effective value varies depending on the context level.
if ($context->contextlevel == CONTEXT_USER) {
// Use the context level value as we don't allow people to set specific instances values.
return self::get_effective_contextlevel_value($context->contextlevel, $element);
} else {
// Use the default context level value.
list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category(
$context->contextlevel
);
return self::get_element_instance($element, $$fieldname);
$sql = "SELECT $fields
FROM {context} ctx
LEFT JOIN {tool_dataprivacy_ctxinstance} ctxins ON ctx.id = ctxins.contextid
LEFT JOIN {course_modules} cm ON ctx.contextlevel = :contextmodule AND ctx.instanceid = cm.id
LEFT JOIN {modules} m ON m.id = cm.module
{$elementjoin}
WHERE ctx.id {$insql}
ORDER BY ctx.path DESC";
$contextinstances = $DB->get_records_sql($sql, $inparams);
// Check whether this context is a user context, or a child of a user context.
// All children of a User context share the same context and cannot be set individually.
foreach ($contextinstances as $record) {
\context_helper::preload_from_record($record);
$parent = \context::instance_by_id($record->id, false);
if ($parent->contextlevel == CONTEXT_USER) {
// Use the context level value for the user.
return self::get_effective_contextlevel_value(CONTEXT_USER, $element);
}
}
// Specific value for this context instance.
if ($instancevalue != context_instance::INHERIT) {
return self::get_element_instance($element, $instancevalue);
foreach ($contextinstances as $record) {
$parent = \context::instance_by_id($record->id, false);
$checkcontextlevel = false;
if (empty($record->eleid)) {
$checkcontextlevel = true;
}
if (!empty($forcedvalue) && context_instance::NOTSET == $forcedvalue) {
$checkcontextlevel = true;
}
if ($checkcontextlevel) {
// Check for a value at the contextlevel
$forplugin = empty($record->modname) ? '' : $record->modname;
list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category(
$parent->contextlevel, false, false, $forplugin);
$instancevalue = $$fieldname;
if (context_instance::NOTSET != $instancevalue && context_instance::INHERIT != $instancevalue) {
// There is an actual value. Return it.
return self::get_element_instance($element, $instancevalue);
}
} else {
$elementclass = "\\tool_dataprivacy\\{$element}";
$instance = new $elementclass(null, $elementclass::extract_record($record, 'ele'));
$instance->validate();
return $instance;
}
}
// This context is using inherited so let's return the parent effective value.
$parentcontext = $context->get_parent_context();
if (!$parentcontext) {
return false;
}
// The forced value should not be transmitted to parent contexts.
return self::get_effective_context_value($parentcontext, $element);
throw new coding_exception('Something went wrong, system defaults should be set and we should already have a value.');
}
/**
@@ -231,11 +276,9 @@ class data_registry {
*
* @param int $contextlevel
* @param string $element 'category' or 'purpose'
* @param int $forcedvalue Use this value as if this was this context level purpose.
* @return \tool_dataprivacy\purpose|false
*/
public static function get_effective_contextlevel_value($contextlevel, $element, $forcedvalue = false) {
public static function get_effective_contextlevel_value($contextlevel, $element) {
if ($element !== 'purpose' && $element !== 'category') {
throw new coding_exception('Only \'purpose\' and \'category\' are supported.');
}
@@ -246,53 +289,30 @@ class data_registry {
'have a purpose or a category.');
}
if ($forcedvalue === false) {
$instance = contextlevel::get_record_by_contextlevel($contextlevel, false);
if (!$instance) {
// If the context level does not have a value defaults to not set, so we grab the context level default as
// its value.
$instancevalue = context_instance::NOTSET;
} else {
$instancevalue = $instance->get($fieldname);
}
} else {
$instancevalue = $forcedvalue;
}
list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category($contextlevel);
// Not set -> Use the default context level value.
if ($instancevalue == context_instance::NOTSET) {
list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category($contextlevel);
// Note: The $$fieldname points to either $purposeid, or $categoryid.
if (context_instance::NOTSET != $$fieldname && context_instance::INHERIT != $$fieldname) {
// There is a specific value set.
return self::get_element_instance($element, $$fieldname);
}
// Specific value for this context instance.
if ($instancevalue != context_instance::INHERIT) {
return self::get_element_instance($element, $instancevalue);
}
if ($contextlevel == CONTEXT_SYSTEM) {
throw new coding_exception('Something went wrong, system defaults should be set and we should already have a value.');
}
// If we reach this point is that we are inheriting so get the parent context level and repeat.
$parentcontextlevel = reset(self::$contextlevelinheritance[$contextlevel]);
// Forced value are intentionally not passed as the force value should only affect the immediate context level.
return self::get_effective_contextlevel_value($parentcontextlevel, $element);
throw new coding_exception('Something went wrong, system defaults should be set and we should already have a value.');
}
/**
* Returns the effective default purpose and category for a context level.
*
* @param int $contextlevel
* @param int $forcedpurposevalue Use this value as if this was this context level purpose.
* @param int $forcedcategoryvalue Use this value as if this was this context level category.
* @param int|bool $forcedpurposevalue Use this value as if this was this context level purpose.
* @param int|bool $forcedcategoryvalue Use this value as if this was this context level category.
* @param string $component The name of the component to check.
* @return int[]
*/
public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false,
$forcedcategoryvalue = false) {
list($purposeid, $categoryid) = self::get_defaults($contextlevel);
$forcedcategoryvalue = false, $component = '') {
// Get the defaults for this context level.
list($purposeid, $categoryid) = self::get_defaults($contextlevel, $component);
// Honour forced values.
if ($forcedpurposevalue) {
@@ -302,37 +322,19 @@ class data_registry {
$categoryid = $forcedcategoryvalue;
}
// Not set == INHERIT for defaults.
if ($purposeid == context_instance::INHERIT || $purposeid == context_instance::NOTSET) {
$purposeid = false;
}
if ($categoryid == context_instance::INHERIT || $categoryid == context_instance::NOTSET) {
$categoryid = false;
}
if ($contextlevel == CONTEXT_USER) {
// Only user context levels inherit from a parent context level.
list($parentpurposeid, $parentcategoryid) = self::get_defaults(CONTEXT_SYSTEM);
if ($contextlevel != CONTEXT_SYSTEM && ($purposeid === false || $categoryid === false)) {
foreach (self::$contextlevelinheritance[$contextlevel] as $parent) {
if (context_instance::INHERIT == $purposeid || context_instance::NOTSET == $purposeid) {
$purposeid = (int)$parentpurposeid;
}
list($parentpurposeid, $parentcategoryid) = self::get_defaults($parent);
// Not set == INHERIT for defaults.
if ($parentpurposeid == context_instance::INHERIT || $parentpurposeid == context_instance::NOTSET) {
$parentpurposeid = false;
}
if ($parentcategoryid == context_instance::INHERIT || $parentcategoryid == context_instance::NOTSET) {
$parentcategoryid = false;
}
if ($purposeid === false && $parentpurposeid) {
$purposeid = $parentpurposeid;
}
if ($categoryid === false && $parentcategoryid) {
$categoryid = $parentcategoryid;
}
if (context_instance::INHERIT == $categoryid || context_instance::NOTSET == $categoryid) {
$categoryid = $parentcategoryid;
}
}
// They may still be false, but we return anyway.
return [$purposeid, $categoryid];
}
@@ -345,7 +347,6 @@ class data_registry {
* @return \core\persistent
*/
private static function get_element_instance($element, $id) {
if ($element !== 'purpose' && $element !== 'category') {
throw new coding_exception('No other elements than purpose and category are allowed');
}
@@ -21,7 +21,9 @@
* @copyright 2018 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy;
defined('MOODLE_INTERNAL') || die();
use core\persistent;
@@ -37,6 +39,12 @@ class data_request extends persistent {
/** The table name this persistent object maps to. */
const TABLE = 'tool_dataprivacy_request';
/** Data request created manually. */
const DATAREQUEST_CREATION_MANUAL = 0;
/** Data request created automatically. */
const DATAREQUEST_CREATION_AUTO = 1;
/**
* Return the definition of the properties of this model.
*
@@ -111,6 +119,14 @@ class data_request extends persistent {
'type' => PARAM_INT,
'default' => FORMAT_PLAIN
],
'creationmethod' => [
'default' => self::DATAREQUEST_CREATION_MANUAL,
'choices' => [
self::DATAREQUEST_CREATION_MANUAL,
self::DATAREQUEST_CREATION_AUTO
],
'type' => PARAM_INT
],
];
}
@@ -0,0 +1,63 @@
<?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/>.
/**
* Event observers supported by this module.
*
* @package tool_dataprivacy
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy\event;
use \tool_dataprivacy\api;
use \tool_dataprivacy\data_request;
defined('MOODLE_INTERNAL') || die();
/**
* Event observers supported by this module.
*
* @package tool_dataprivacy
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_deleted_observer {
/**
* Create user data deletion request when the user is deleted.
*
* @param \core\event\user_deleted $event
*/
public static function create_delete_data_request(\core\event\user_deleted $event) {
// Automatic creation of deletion requests must be enabled.
if (get_config('tool_dataprivacy', 'automaticdeletionrequests')) {
$requesttypes = [api::DATAREQUEST_TYPE_DELETE];
$requeststatuses = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_DELETED];
$hasongoingdeleterequests = api::has_ongoing_request($event->objectid, $requesttypes[0]);
$hascompleteddeleterequest = (api::get_data_requests_count($event->objectid, $requeststatuses,
$requesttypes) > 0) ? true : false;
if (!$hasongoingdeleterequests && !$hascompleteddeleterequest) {
api::create_data_request($event->objectid, $requesttypes[0],
get_string('datarequestcreatedupondelete', 'tool_dataprivacy'),
data_request::DATAREQUEST_CREATION_AUTO);
}
}
}
}
@@ -159,4 +159,51 @@ class expired_context extends \core\persistent {
return $DB->count_records_sql($sql, $params);
}
/**
* Create a new expired_context based on the context, and expiry_info object.
*
* @param \context $context
* @param expiry_info $info
* @return expired_context
*/
public static function create_from_expiry_info(\context $context, expiry_info $info) {
$record = (object) [
'contextid' => $context->id,
'status' => self::STATUS_EXPIRED,
];
$expiredcontext = new static(0, $record);
$expiredcontext->save();
return $expiredcontext;
}
/**
* Update the expired_context from an expiry_info object which relates to this context.
*
* @param expiry_info $info
* @return $this
*/
public function update_from_expiry_info(expiry_info $info) {
return $this;
}
/**
* Check whether this expired_context record is in a state ready for deletion to actually take place.
*
* @return bool
*/
public function can_process_deletion() {
return ($this->get('status') == self::STATUS_APPROVED);
}
/**
* Check whether this expired_context record has already been cleaned.
*
* @return bool
*/
public function is_complete() {
return ($this->get('status') == self::STATUS_CLEANED);
}
}
@@ -34,121 +34,680 @@ defined('MOODLE_INTERNAL') || die();
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class expired_contexts_manager {
class expired_contexts_manager {
/**
* Number of deleted contexts for each scheduled task run.
*/
const DELETE_LIMIT = 200;
/**
* Returns the list of expired context instances.
*
* @return \stdClass[]
*/
abstract protected function get_expired_contexts();
/** @var progress_trace The log progress tracer */
protected $progresstracer = null;
/**
* Specify with context levels this expired contexts manager is deleting.
*
* @return int[]
*/
abstract protected function get_context_levels();
/** @var manager The privacy manager */
protected $manager = null;
/**
* Flag expired contexts as expired.
*
* @return int The number of contexts flagged as expired.
* @return int[] The number of contexts flagged as expired for courses, and users.
*/
public function flag_expired() {
public function flag_expired_contexts() {
if (!$this->check_requirements()) {
return 0;
return [0, 0];
}
$contexts = $this->get_expired_contexts();
foreach ($contexts as $context) {
api::create_expired_context($context->id);
// Clear old and stale records first.
static::clear_old_records();
$data = static::get_nested_expiry_info_for_courses();
$coursecount = 0;
foreach ($data as $expiryrecord) {
if ($this->update_from_expiry_info($expiryrecord)) {
$coursecount++;
}
}
return count($contexts);
$data = static::get_nested_expiry_info_for_user();
$usercount = 0;
foreach ($data as $expiryrecord) {
if ($this->update_from_expiry_info($expiryrecord)) {
$usercount++;
}
}
return [$coursecount, $usercount];
}
/**
* Clear old and stale records.
*/
protected static function clear_old_records() {
global $DB;
$sql = "SELECT dpctx.*
FROM {tool_dataprivacy_ctxexpired} dpctx
LEFT JOIN {context} ctx ON ctx.id = dpctx.contextid
WHERE ctx.id IS NULL";
$orphaned = $DB->get_recordset_sql($sql);
foreach ($orphaned as $orphan) {
$expiredcontext = new expired_context(0, $orphan);
$expiredcontext->delete();
}
// Delete any child of a user context.
$parentpath = $DB->sql_concat('ctxuser.path', "'/%'");
$params = [
'contextuser' => CONTEXT_USER,
];
$sql = "SELECT dpctx.*
FROM {tool_dataprivacy_ctxexpired} dpctx
WHERE dpctx.contextid IN (
SELECT ctx.id
FROM {context} ctxuser
JOIN {context} ctx ON ctx.path LIKE {$parentpath}
WHERE ctxuser.contextlevel = :contextuser
)";
$userchildren = $DB->get_recordset_sql($sql, $params);
foreach ($userchildren as $child) {
$expiredcontext = new expired_context(0, $child);
$expiredcontext->delete();
}
}
/**
* Get the full nested set of expiry data relating to all contexts.
*
* @param string $contextpath A contexpath to restrict results to
* @return \stdClass[]
*/
protected static function get_nested_expiry_info($contextpath = '') {
$coursepaths = self::get_nested_expiry_info_for_courses($contextpath);
$userpaths = self::get_nested_expiry_info_for_user($contextpath);
return array_merge($coursepaths, $userpaths);
}
/**
* Get the full nested set of expiry data relating to course-related contexts.
*
* @param string $contextpath A contexpath to restrict results to
* @return \stdClass[]
*/
protected static function get_nested_expiry_info_for_courses($contextpath = '') {
global $DB;
$contextfields = \context_helper::get_preload_record_columns_sql('ctx');
$expiredfields = expired_context::get_sql_fields('expiredctx', 'expiredctx');
$purposefields = 'dpctx.purposeid';
$coursefields = 'ctxcourse.expirydate AS expirydate';
$fields = implode(', ', ['ctx.id', $contextfields, $expiredfields, $coursefields, $purposefields]);
// We want all contexts at course-dependant levels.
$parentpath = $DB->sql_concat('ctxcourse.path', "'/%'");
// This SQL query returns all course-dependant contexts (including the course context)
// which course end date already passed.
// This is ordered by the context path in reverse order, which will give the child nodes before any parent node.
$params = [
'contextlevel' => CONTEXT_COURSE,
];
$where = '';
if (!empty($contextpath)) {
$where = "WHERE (ctx.path = :pathmatchexact OR ctx.path LIKE :pathmatchchildren)";
$params['pathmatchexact'] = $contextpath;
$params['pathmatchchildren'] = "{$contextpath}/%";
}
$sql = "SELECT $fields
FROM {context} ctx
JOIN (
SELECT c.enddate AS expirydate, subctx.path
FROM {context} subctx
JOIN {course} c
ON subctx.contextlevel = :contextlevel
AND subctx.instanceid = c.id
AND c.format != 'site'
) ctxcourse
ON ctx.path LIKE {$parentpath} OR ctx.path = ctxcourse.path
LEFT JOIN {tool_dataprivacy_ctxinstance} dpctx
ON dpctx.contextid = ctx.id
LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx
ON ctx.id = expiredctx.contextid
{$where}
ORDER BY ctx.path DESC";
return self::get_nested_expiry_info_from_sql($sql, $params);
}
/**
* Get the full nested set of expiry data.
*
* @param string $contextpath A contexpath to restrict results to
* @return \stdClass[]
*/
protected static function get_nested_expiry_info_for_user($contextpath = '') {
global $DB;
$contextfields = \context_helper::get_preload_record_columns_sql('ctx');
$expiredfields = expired_context::get_sql_fields('expiredctx', 'expiredctx');
$purposefields = 'dpctx.purposeid';
$userfields = 'u.lastaccess AS expirydate';
$fields = implode(', ', ['ctx.id', $contextfields, $expiredfields, $userfields, $purposefields]);
// We want all contexts at user-dependant levels.
$parentpath = $DB->sql_concat('ctxuser.path', "'/%'");
// This SQL query returns all user-dependant contexts (including the user context)
// This is ordered by the context path in reverse order, which will give the child nodes before any parent node.
$params = [
'contextlevel' => CONTEXT_USER,
];
$where = '';
if (!empty($contextpath)) {
$where = "AND ctx.path = :pathmatchexact";
$params['pathmatchexact'] = $contextpath;
}
$sql = "SELECT $fields, u.deleted AS userdeleted
FROM {context} ctx
JOIN {user} u ON ctx.instanceid = u.id
LEFT JOIN {tool_dataprivacy_ctxinstance} dpctx
ON dpctx.contextid = ctx.id
LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx
ON ctx.id = expiredctx.contextid
WHERE ctx.contextlevel = :contextlevel {$where}
ORDER BY ctx.path DESC";
return self::get_nested_expiry_info_from_sql($sql, $params);
}
/**
* Get the full nested set of expiry data given appropriate SQL.
* Only contexts which have expired will be included.
*
* @param string $sql The SQL used to select the nested information.
* @param array $params The params required by the SQL.
* @return \stdClass[]
*/
protected static function get_nested_expiry_info_from_sql($sql, array $params) {
global $DB;
$fulllist = $DB->get_recordset_sql($sql, $params);
$datalist = [];
$expiredcontents = [];
$pathstoskip = [];
foreach ($fulllist as $record) {
\context_helper::preload_from_record($record);
$context = \context::instance_by_id($record->id, false);
if (!self::is_eligible_for_deletion($pathstoskip, $context)) {
// We should skip this context, and therefore all of it's children.
$datalist = array_filter($datalist, function($data, $path) use ($context) {
// Remove any child of this context.
// Technically this should never be fulfilled because the query is ordered in path DESC, but is kept
// in to be certain.
return (false === strpos($path, "{$context->path}/"));
}, ARRAY_FILTER_USE_BOTH);
if ($record->expiredctxid) {
// There was previously an expired context record.
// Delete it to be on the safe side.
$expiredcontext = new expired_context(null, expired_context::extract_record($record, 'expiredctx'));
$expiredcontext->delete();
}
continue;
}
$purposevalue = $record->purposeid !== null ? $record->purposeid : context_instance::NOTSET;
$purpose = api::get_effective_context_purpose($context, $purposevalue);
if ($context instanceof \context_user && !empty($record->userdeleted)) {
$expiryinfo = static::get_expiry_info($purpose, $record->userdeleted);
} else {
$expiryinfo = static::get_expiry_info($purpose, $record->expirydate);
}
foreach ($datalist as $path => $data) {
// Merge with already-processed children.
if (strpos($path, $context->path) !== 0) {
continue;
}
$expiryinfo->merge_with_child($data->info);
}
$datalist[$context->path] = (object) [
'context' => $context,
'record' => $record,
'purpose' => $purpose,
'info' => $expiryinfo,
];
}
$fulllist->close();
return $datalist;
}
/**
* Check whether the supplied context would be elible for deletion.
*
* @param array $pathstoskip A set of paths which should be skipped
* @param \context $context
* @return bool
*/
protected static function is_eligible_for_deletion(array &$pathstoskip, \context $context) {
$shouldskip = false;
// Check whether any of the child contexts are ineligble.
$shouldskip = !empty(array_filter($pathstoskip, function($path) use ($context) {
// If any child context has already been skipped then it will appear in this list.
// Since paths include parents, test if the context under test appears as the haystack in the skipped
// context's needle.
return false !== (strpos($context->path, $path));
}));
if (!$shouldskip && $context instanceof \context_user) {
// The context instanceid is the user's ID.
if (isguestuser($context->instanceid) || is_siteadmin($context->instanceid)) {
// This is an admin, or the guest and cannot be deleted.
$shouldskip = true;
}
if (!$shouldskip) {
$courses = enrol_get_users_courses($context->instanceid, false, ['enddate']);
$requireenddate = self::require_all_end_dates_for_user_deletion();
foreach ($courses as $course) {
if (empty($course->enddate)) {
// This course has no end date.
if ($requireenddate) {
// Course end dates are required, and this course has no end date.
$shouldskip = true;
break;
}
// Course end dates are not required. The subsequent checks are pointless at this time so just
// skip them.
continue;
}
if ($course->enddate >= time()) {
// This course is still in the future.
$shouldskip = true;
break;
}
// This course has an end date which is in the past.
if (!self::is_course_expired($course)) {
// This course has not expired yet.
$shouldskip = true;
break;
}
}
}
}
if ($shouldskip) {
// Add this to the list of contexts to skip for parentage checks.
$pathstoskip[] = $context->path;
}
return !$shouldskip;
}
/**
* Deletes the expired contexts.
*
* @return int The number of deleted contexts.
* @return int[] The number of deleted contexts.
*/
public function delete() {
$numprocessed = 0;
public function process_approved_deletions() {
if (!$this->check_requirements()) {
return $numprocessed;
return [0, 0];
}
$privacymanager = new manager();
$privacymanager->set_observer(new \tool_dataprivacy\manager_observer());
$expiredcontexts = expired_context::get_records(['status' => expired_context::STATUS_APPROVED]);
$totalprocessed = 0;
$usercount = 0;
$coursecount = 0;
foreach ($expiredcontexts as $expiredctx) {
$context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
if (empty($context)) {
// Unable to process this request further.
// We have no context to delete.
$expiredctx->delete();
continue;
}
foreach ($this->get_context_levels() as $level) {
$expiredcontexts = expired_context::get_records_by_contextlevel($level, expired_context::STATUS_APPROVED);
foreach ($expiredcontexts as $expiredctx) {
if (!$this->delete_expired_context($privacymanager, $expiredctx)) {
continue;
if ($this->delete_expired_context($expiredctx)) {
if ($context instanceof \context_user) {
$usercount++;
} else {
$coursecount++;
}
$numprocessed += 1;
if ($numprocessed == self::DELETE_LIMIT) {
// Close the recordset.
$expiredcontexts->close();
break 2;
$totalprocessed++;
if ($totalprocessed >= $this->get_delete_limit()) {
break;
}
}
}
return $numprocessed;
return [$coursecount, $usercount];
}
/**
* Deletes user data from the provided context.
*
* @param manager $privacymanager
* @param expired_context $expiredctx
* @return \context|false
*/
protected function delete_expired_context(manager $privacymanager, expired_context $expiredctx) {
protected function delete_expired_context(expired_context $expiredctx) {
$context = \context::instance_by_id($expiredctx->get('contextid'));
$context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
if (!$context) {
api::delete_expired_context($expiredctx->get('contextid'));
$this->get_progress()->output("Deleting context {$context->id} - " . $context->get_context_name(true, true));
// Update the expired_context and verify that it is still ready for deletion.
$expiredctx = $this->update_expired_context($expiredctx);
if (empty($expiredctx)) {
$this->get_progress()->output("Context has changed since approval and is no longer pending approval. Skipping", 1);
return false;
}
if (!PHPUNIT_TEST) {
mtrace('Deleting context ' . $context->id . ' - ' .
shorten_text($context->get_context_name(true, true)));
if (!$expiredctx->can_process_deletion()) {
// This only happens if the record was updated after being first fetched.
$this->get_progress()->output("Context has changed since approval and must be re-approved. Skipping", 1);
$expiredctx->set('status', expired_context::STATUS_EXPIRED);
$expiredctx->save();
return false;
}
$privacymanager->delete_data_for_all_users_in_context($context);
api::set_expired_context_status($expiredctx, expired_context::STATUS_CLEANED);
$privacymanager = $this->get_privacy_manager();
if ($context instanceof \context_user) {
$this->delete_expired_user_context($expiredctx);
} else {
// This context is fully expired - that is that the default retention period has been reached.
$privacymanager->delete_data_for_all_users_in_context($context);
}
// Mark the record as cleaned.
$expiredctx->set('status', expired_context::STATUS_CLEANED);
$expiredctx->save();
return $context;
}
/**
* Deletes user data from the provided user context.
*
* @param expired_context $expiredctx
*/
protected function delete_expired_user_context(expired_context $expiredctx) {
global $DB;
$contextid = $expiredctx->get('contextid');
$context = \context::instance_by_id($contextid);
$user = \core_user::get_user($context->instanceid, '*', MUST_EXIST);
$privacymanager = $this->get_privacy_manager();
// Delete all child contexts of the user context.
$parentpath = $DB->sql_concat('ctxuser.path', "'/%'");
$params = [
'contextlevel' => CONTEXT_USER,
'contextid' => $expiredctx->get('contextid'),
];
$fields = \context_helper::get_preload_record_columns_sql('ctx');
$sql = "SELECT ctx.id, $fields
FROM {context} ctxuser
JOIN {context} ctx ON ctx.path LIKE {$parentpath}
WHERE ctxuser.contextlevel = :contextlevel AND ctxuser.id = :contextid
ORDER BY ctx.path DESC";
$children = $DB->get_recordset_sql($sql, $params);
foreach ($children as $child) {
\context_helper::preload_from_record($child);
$context = \context::instance_by_id($child->id);
$privacymanager->delete_data_for_all_users_in_context($context);
}
$children->close();
// Delete all unprotected data that the user holds.
$approvedlistcollection = new \core_privacy\local\request\contextlist_collection($user->id);
$contextlistcollection = $privacymanager->get_contexts_for_userid($user->id);
foreach ($contextlistcollection as $contextlist) {
$contextids = [];
$approvedlistcollection->add_contextlist(new \core_privacy\local\request\approved_contextlist(
$user,
$contextlist->get_component(),
$contextlist->get_contextids()
));
}
$privacymanager->delete_data_for_user($approvedlistcollection, $this->get_progress());
// Delete the user context.
$context = \context::instance_by_id($expiredctx->get('contextid'));
$privacymanager->delete_data_for_all_users_in_context($context);
// This user is now fully expired - finish by deleting the user.
delete_user($user);
}
/**
* Whether end dates are required on all courses in order for a user to be expired from them.
*
* @return bool
*/
protected static function require_all_end_dates_for_user_deletion() {
$requireenddate = get_config('tool_dataprivacy', 'requireallenddatesforuserdeletion');
return !empty($requireenddate);
}
/**
* Check that the requirements to start deleting contexts are satisified.
*
* @return bool
*/
protected function check_requirements() {
api::check_can_manage_data_registry(\context_system::instance()->id);
if (!data_registry::defaults_set()) {
return false;
}
return true;
}
/**
* Check whether a date is beyond the specified period.
*
* @param string $period The Expiry Period
* @param int $comparisondate The date for comparison
* @return bool
*/
protected static function has_expired($period, $comparisondate) {
$dt = new \DateTime();
$dt->setTimestamp($comparisondate);
$dt->add(new \DateInterval($period));
return (time() >= $dt->getTimestamp());
}
/**
* Get the expiry info object for the specified purpose and comparison date.
*
* @param purpose $purpose The purpose of this context
* @param int $comparisondate The date for comparison
* @return expiry_info
*/
protected static function get_expiry_info(purpose $purpose, $comparisondate = 0) {
if (empty($comparisondate)) {
// The date is empty, therefore this context cannot be considered for automatic expiry.
$defaultexpired = false;
} else {
$defaultexpired = static::has_expired($purpose->get('retentionperiod'), $comparisondate);
}
return new expiry_info($defaultexpired);
}
/**
* Update or delete the expired_context from the expiry_info object.
* This function depends upon the data structure returned from get_nested_expiry_info.
*
* If the context is expired in any way, then an expired_context will be returned, otherwise null will be returned.
*
* @param \stdClass $expiryrecord
* @return expired_context|null
*/
protected function update_from_expiry_info(\stdClass $expiryrecord) {
if ($expiryrecord->info->is_any_expired()) {
// The context is expired in some fashion.
// Create or update as required.
if ($expiryrecord->record->expiredctxid) {
$expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx'));
$expiredcontext->update_from_expiry_info($expiryrecord->info);
if ($expiredcontext->is_complete()) {
return null;
}
} else {
$expiredcontext = expired_context::create_from_expiry_info($expiryrecord->context, $expiryrecord->info);
}
return $expiredcontext;
} else {
// The context is not expired.
if ($expiryrecord->record->expiredctxid) {
// There was previously an expired context record, but it is no longer relevant.
// Delete it to be on the safe side.
$expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx'));
$expiredcontext->delete();
}
return null;
}
}
/**
* Update the expired context record.
*
* Note: You should use the return value as the provided value will be used to fetch data only.
*
* @param expired_context $expiredctx The record to update
* @return expired_context|null
*/
protected function update_expired_context(expired_context $expiredctx) {
// Fetch the context from the expired_context record.
$context = \context::instance_by_id($expiredctx->get('contextid'));
// Fetch the current nested expiry data.
$expiryrecords = self::get_nested_expiry_info($context->path);
// Find the current record.
if (empty($expiryrecords[$context->path])) {
$expiredctx->delete();
return null;
}
// Refresh the record.
// Note: Use the returned expiredctx.
$expiredctx = $this->update_from_expiry_info($expiryrecords[$context->path]);
if (empty($expiredctx)) {
return null;
}
if (!$context instanceof \context_user) {
// Where the target context is not a user, we check all children of the context.
// The expiryrecords array only contains children, fetched from the get_nested_expiry_info call above.
// No need to check that these _are_ children.
foreach ($expiryrecords as $expiryrecord) {
if ($expiryrecord->context->id === $context->id) {
// This is record for the context being tested that we checked earlier.
continue;
}
if (empty($expiryrecord->record->expiredctxid)) {
// There is no expired context record for this context.
// If there is no record, then this context cannot have been approved for removal.
return null;
}
// Fetch the expired_context object for this record.
// This needs to be updated from the expiry_info data too as there may be child changes to consider.
$expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx'));
$expiredcontext->update_from_expiry_info($expiryrecord->info);
if (!$expiredcontext->is_complete()) {
return null;
}
}
}
return $expiredctx;
}
/**
* Check whether the course has expired.
*
* @param \stdClass $course
* @return bool
*/
protected static function is_course_expired(\stdClass $course) {
$context = \context_course::instance($course->id);
$expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
return !empty($expiryrecords[$context->path]) && $expiryrecords[$context->path]->info->is_fully_expired();
}
/**
* Create a new instance of the privacy manager.
*
* @return manager
*/
protected function get_privacy_manager() {
if (null === $this->manager) {
$this->manager = new manager();
$this->manager->set_observer(new \tool_dataprivacy\manager_observer());
}
return $this->manager;
}
/**
* Fetch the limit for the maximum number of contexts to delete in one session.
*
* @return int
*/
protected function get_delete_limit() {
return self::DELETE_LIMIT;
}
/**
* Get the progress tracer.
*
* @return \progress_trace
*/
protected function get_progress() {
if (null === $this->progresstracer) {
$this->set_progress(new \text_progress_trace());
}
return $this->progresstracer;
}
/**
* Set a specific tracer for the task.
*
* @param \progress_trace $trace
* @return $this
*/
public function set_progress(\progress_trace $trace) {
$this->progresstracer = $trace;
return $this;
}
}
@@ -1,135 +0,0 @@
<?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 contexts manager for CONTEXT_COURSE, CONTEXT_MODULE and CONTEXT_BLOCK.
*
* @package tool_dataprivacy
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy;
use tool_dataprivacy\purpose;
defined('MOODLE_INTERNAL') || die();
/**
* Expired contexts manager for CONTEXT_COURSE, CONTEXT_MODULE and CONTEXT_BLOCK.
*
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class expired_course_related_contexts extends \tool_dataprivacy\expired_contexts_manager {
/**
* Course-related context levels.
*
* @return int[]
*/
protected function get_context_levels() {
return [CONTEXT_MODULE, CONTEXT_BLOCK, CONTEXT_COURSE];
}
/**
* Returns a recordset with user context instances that are possibly expired (to be confirmed by get_recordset_callback).
*
* @return \stdClass[]
*/
protected function get_expired_contexts() {
global $DB;
// Including context info + course end date + purposeid (this last one only if defined).
$fields = 'ctx.id AS id, ctxcourse.enddate AS courseenddate, dpctx.purposeid AS purposeid, ' .
\context_helper::get_preload_record_columns_sql('ctx');
// We want all contexts at course-dependant levels.
$parentpath = $DB->sql_concat('ctxcourse.path', "'/%'");
// This SQL query returns all course-dependant contexts (including the course context)
// which course end date already passed.
$sql = "SELECT $fields
FROM {context} ctx
JOIN (
SELECT c.enddate, subctx.path
FROM {context} subctx
JOIN {course} c
ON subctx.contextlevel = ? AND subctx.instanceid = c.id
WHERE c.enddate < ? AND c.enddate > 0
) ctxcourse
ON ctx.path LIKE {$parentpath} OR ctx.path = ctxcourse.path
LEFT JOIN {tool_dataprivacy_ctxinstance} dpctx
ON dpctx.contextid = ctx.id
LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx
ON ctx.id = expiredctx.contextid
WHERE expiredctx.id IS NULL
ORDER BY ctx.contextlevel DESC, ctx.path";
$possiblyexpired = $DB->get_recordset_sql($sql, [CONTEXT_COURSE, time()]);
$expiredcontexts = [];
$excludedcontextids = [];
foreach ($possiblyexpired as $record) {
\context_helper::preload_from_record($record);
// No strict checking as the context may already be deleted (e.g. we just deleted a course,
// module contexts below it will not exist).
$context = \context::instance_by_id($record->id, false);
if (!$context) {
continue;
}
// We pass the value we just got from SQL so get_effective_context_purpose don't need to query
// the db again to retrieve it. If there is no tool_dataprovider_ctxinstance record
// $record->purposeid will be null which is ok as it would force get_effective_context_purpose
// to return the default purpose for the context context level (no db queries involved).
$purposevalue = $record->purposeid !== null ? $record->purposeid : context_instance::NOTSET;
// It should be cheap as system purposes and context level purposes will be retrieved from a cache most of the time.
$purpose = api::get_effective_context_purpose($context, $purposevalue);
$dt = new \DateTime();
$dt->setTimestamp($record->courseenddate);
$di = new \DateInterval($purpose->get('retentionperiod'));
$dt->add($di);
if (time() < $dt->getTimestamp()) {
// Exclude this context ID as it has not reached the retention period yet.
$excludedcontextids[] = $context->id;
continue;
}
// Check if this context has children that have not yet expired.
$hasunexpiredchildren = false;
$children = $context->get_child_contexts();
foreach ($children as $child) {
if (in_array($child->id, $excludedcontextids)) {
$hasunexpiredchildren = true;
break;
}
}
if ($hasunexpiredchildren) {
// Exclude this context ID as it has children that have not yet expired.
$excludedcontextids[] = $context->id;
continue;
}
$expiredcontexts[$context->id] = $context;
}
return $expiredcontexts;
}
}
@@ -1,150 +0,0 @@
<?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 contexts manager for CONTEXT_USER.
*
* @package tool_dataprivacy
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy;
use core_privacy\manager;
defined('MOODLE_INTERNAL') || die();
/**
* Expired contexts manager for CONTEXT_USER.
*
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class expired_user_contexts extends \tool_dataprivacy\expired_contexts_manager {
/**
* Only user level.
*
* @return int[]
*/
protected function get_context_levels() {
return [CONTEXT_USER];
}
/**
* Returns the user context instances that are expired.
*
* @return \stdClass[]
*/
protected function get_expired_contexts() {
global $DB;
// Including context info + last login timestamp.
$fields = 'ctx.id AS id, ' . \context_helper::get_preload_record_columns_sql('ctx');
$purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER);
// Calculate what is considered expired according to the context level effective purpose (= now + retention period).
$expiredtime = new \DateTime();
$retention = new \DateInterval($purpose->get('retentionperiod'));
$expiredtime->sub($retention);
$sql = "SELECT $fields FROM {context} ctx
JOIN {user} u ON ctx.contextlevel = ? AND ctx.instanceid = u.id
LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx ON ctx.id = expiredctx.contextid
WHERE u.lastaccess <= ? AND u.lastaccess > 0 AND expiredctx.id IS NULL
ORDER BY ctx.path, ctx.contextlevel ASC";
$possiblyexpired = $DB->get_recordset_sql($sql, [CONTEXT_USER, $expiredtime->getTimestamp()]);
$expiredcontexts = [];
foreach ($possiblyexpired as $record) {
\context_helper::preload_from_record($record);
// No strict checking as the context may already be deleted (e.g. we just deleted a course,
// module contexts below it will not exist).
$context = \context::instance_by_id($record->id, false);
if (!$context) {
continue;
}
if (is_siteadmin($context->instanceid)) {
continue;
}
$courses = enrol_get_users_courses($context->instanceid, false, ['enddate']);
foreach ($courses as $course) {
if (!$course->enddate) {
// We can not know it what is going on here, so we prefer to be conservative.
continue 2;
}
if ($course->enddate >= time()) {
// Future or ongoing course.
continue 2;
}
}
$expiredcontexts[$context->id] = $context;
}
return $expiredcontexts;
}
/**
* Deletes user data from the provided context.
*
* Overwritten to delete the user.
*
* @param manager $privacymanager
* @param expired_context $expiredctx
* @return \context|false
*/
protected function delete_expired_context(manager $privacymanager, expired_context $expiredctx) {
$context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
if (!$context) {
api::delete_expired_context($expiredctx->get('contextid'));
return false;
}
if (!PHPUNIT_TEST) {
mtrace('Deleting context ' . $context->id . ' - ' .
shorten_text($context->get_context_name(true, true)));
}
// To ensure that all user data is deleted, instead of deleting by context, we run through and collect any stray
// contexts for the user that may still exist and call delete_data_for_user().
$user = \core_user::get_user($context->instanceid, '*', MUST_EXIST);
$approvedlistcollection = new \core_privacy\local\request\contextlist_collection($user->id);
$contextlistcollection = $privacymanager->get_contexts_for_userid($user->id);
foreach ($contextlistcollection as $contextlist) {
$approvedlistcollection->add_contextlist(new \core_privacy\local\request\approved_contextlist(
$user,
$contextlist->get_component(),
$contextlist->get_contextids()
));
}
$privacymanager->delete_data_for_user($approvedlistcollection);
api::set_expired_context_status($expiredctx, expired_context::STATUS_CLEANED);
// Delete the user.
delete_user($user);
return $context;
}
}
@@ -0,0 +1,93 @@
<?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/>.
/**
* Expiry Data.
*
* @package tool_dataprivacy
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy;
use core_privacy\manager;
defined('MOODLE_INTERNAL') || die();
/**
* Expiry Data.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class expiry_info {
/** @var bool Whether this context is fully expired */
protected $isexpired = false;
/**
* Constructor for the expiry_info class.
*
* @param bool $isexpired Whether the retention period for this context has expired yet.
*/
public function __construct($isexpired) {
$this->isexpired = $isexpired;
}
/**
* Whether this context has 'fully' expired.
* That is to say that the default retention period has been reached, and that there are no unexpired roles.
*
* @return bool
*/
public function is_fully_expired() {
return $this->isexpired;
}
/**
* Whether any part of this context has expired.
*
* @return bool
*/
public function is_any_expired() {
if ($this->is_fully_expired()) {
return true;
}
return false;
}
/**
* Merge this expiry_info object with another belonging to a child context in order to set the 'safest' heritage.
*
* It is not possible to delete any part of a context that is not deleted by a parent.
* So if a course's retention policy has been reached, then only parts where the children have also expired can be
* deleted.
*
* @param expiry_info $child The child record to merge with.
* @return $this
*/
public function merge_with_child(expiry_info $child) {
if ($child->is_fully_expired()) {
return $this;
}
// If the child is not fully expired, then none of the parents can be either.
$this->isexpired = false;
return $this;
}
}
+499 -27
View File
@@ -90,17 +90,30 @@ class external extends external_api {
]);
$requestid = $params['requestid'];
// Validate context.
// Validate context and access to manage the registry.
$context = context_user::instance($USER->id);
self::validate_context($context);
// Ensure the request exists.
$select = 'id = :id AND (userid = :userid OR requestedby = :requestedby)';
$params = ['id' => $requestid, 'userid' => $USER->id, 'requestedby' => $USER->id];
$requestexists = data_request::record_exists_select($select, $params);
$requests = data_request::get_records_select($select, $params);
$requestexists = count($requests) === 1;
$result = false;
if ($requestexists) {
$request = reset($requests);
$datasubject = $request->get('userid');
if ($datasubject !== $USER->id) {
// The user is not the subject. Check that they can cancel this request.
if (!api::can_create_data_request_for_user($datasubject)) {
$forusercontext = \context_user::instance($datasubject);
throw new required_capability_exception($forusercontext,
'tool/dataprivacy:makedatarequestsforchildren', 'nopermissions', '');
}
}
// TODO: Do we want a request to be non-cancellable past a certain point? E.g. When it's already approved/processing.
$result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_CANCELLED);
} else {
@@ -249,9 +262,10 @@ class external extends external_api {
]);
$requestid = $params['requestid'];
// Validate context.
// Validate context and access to manage the registry.
$context = context_system::instance();
self::validate_context($context);
api::check_can_manage_data_registry();
$message = get_string('markedcomplete', 'tool_dataprivacy');
// Update the data request record.
@@ -406,6 +420,85 @@ class external extends external_api {
]);
}
/**
* Parameter description for bulk_approve_data_requests().
*
* @since Moodle 3.5
* @return external_function_parameters
*/
public static function bulk_approve_data_requests_parameters() {
return new external_function_parameters([
'requestids' => new external_multiple_structure(
new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
)
]);
}
/**
* Bulk approve bulk data request.
*
* @since Moodle 3.5
* @param array $requestids Array consisting the request ID's.
* @return array
* @throws coding_exception
* @throws dml_exception
* @throws invalid_parameter_exception
* @throws restricted_context_exception
* @throws moodle_exception
*/
public static function bulk_approve_data_requests($requestids) {
$warnings = [];
$result = false;
$params = external_api::validate_parameters(self::bulk_approve_data_requests_parameters(), [
'requestids' => $requestids
]);
$requestids = $params['requestids'];
// Validate context.
$context = context_system::instance();
self::validate_context($context);
require_capability('tool/dataprivacy:managedatarequests', $context);
foreach ($requestids as $requestid) {
// Ensure the request exists.
$requestexists = data_request::record_exists($requestid);
if ($requestexists) {
api::approve_data_request($requestid);
} else {
$warnings[] = [
'item' => $requestid,
'warningcode' => 'errorrequestnotfound',
'message' => get_string('errorrequestnotfound', 'tool_dataprivacy')
];
}
}
if (empty($warnings)) {
$result = true;
// Add notification in the session to be shown when the page is reloaded on the JS side.
notification::success(get_string('requestsapproved', 'tool_dataprivacy'));
}
return [
'result' => $result,
'warnings' => $warnings
];
}
/**
* Parameter description for bulk_approve_data_requests().
*
* @since Moodle 3.5
* @return external_description
*/
public static function bulk_approve_data_requests_returns() {
return new external_single_structure([
'result' => new external_value(PARAM_BOOL, 'The processing result'),
'warnings' => new external_warnings()
]);
}
/**
* Parameter description for deny_data_request().
*
@@ -475,6 +568,85 @@ class external extends external_api {
]);
}
/**
* Parameter description for bulk_deny_data_requests().
*
* @since Moodle 3.5
* @return external_function_parameters
*/
public static function bulk_deny_data_requests_parameters() {
return new external_function_parameters([
'requestids' => new external_multiple_structure(
new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
)
]);
}
/**
* Bulk deny data requests.
*
* @since Moodle 3.5
* @param array $requestids Array consisting of request ID's.
* @return array
* @throws coding_exception
* @throws dml_exception
* @throws invalid_parameter_exception
* @throws restricted_context_exception
* @throws moodle_exception
*/
public static function bulk_deny_data_requests($requestids) {
$warnings = [];
$result = false;
$params = external_api::validate_parameters(self::bulk_deny_data_requests_parameters(), [
'requestids' => $requestids
]);
$requestids = $params['requestids'];
// Validate context.
$context = context_system::instance();
self::validate_context($context);
require_capability('tool/dataprivacy:managedatarequests', $context);
foreach ($requestids as $requestid) {
// Ensure the request exists.
$requestexists = data_request::record_exists($requestid);
if ($requestexists) {
api::deny_data_request($requestid);
} else {
$warnings[] = [
'item' => $requestid,
'warningcode' => 'errorrequestnotfound',
'message' => get_string('errorrequestnotfound', 'tool_dataprivacy')
];
}
}
if (empty($warnings)) {
$result = true;
// Add notification in the session to be shown when the page is reloaded on the JS side.
notification::success(get_string('requestsdenied', 'tool_dataprivacy'));
}
return [
'result' => $result,
'warnings' => $warnings
];
}
/**
* Parameter description for bulk_deny_data_requests().
*
* @since Moodle 3.5
* @return external_description
*/
public static function bulk_deny_data_requests_returns() {
return new external_single_structure([
'result' => new external_value(PARAM_BOOL, 'The processing result'),
'warnings' => new external_warnings()
]);
}
/**
* Parameter description for get_data_request().
*
@@ -567,7 +739,9 @@ class external extends external_api {
'jsonformdata' => $jsonformdata
]);
// Validate context and access to manage the registry.
self::validate_context(\context_system::instance());
api::check_can_manage_data_registry();
$serialiseddata = json_decode($params['jsonformdata']);
$data = array();
@@ -632,6 +806,10 @@ class external extends external_api {
'id' => $id
]);
// Validate context and access to manage the registry.
self::validate_context(\context_system::instance());
api::check_can_manage_data_registry();
$result = api::delete_purpose($params['id']);
return [
@@ -678,7 +856,9 @@ class external extends external_api {
'jsonformdata' => $jsonformdata
]);
// Validate context and access to manage the registry.
self::validate_context(\context_system::instance());
api::check_can_manage_data_registry();
$serialiseddata = json_decode($params['jsonformdata']);
$data = array();
@@ -743,6 +923,10 @@ class external extends external_api {
'id' => $id
]);
// Validate context and access to manage the registry.
self::validate_context(\context_system::instance());
api::check_can_manage_data_registry();
$result = api::delete_category($params['id']);
return [
@@ -789,8 +973,9 @@ class external extends external_api {
'jsonformdata' => $jsonformdata
]);
// Extra permission checkings are delegated to api::set_contextlevel.
// Validate context and access to manage the registry.
self::validate_context(\context_system::instance());
api::check_can_manage_data_registry();
$serialiseddata = json_decode($params['jsonformdata']);
$data = array();
@@ -804,7 +989,6 @@ class external extends external_api {
$contextlevel = api::set_contextlevel($validateddata);
} else if ($errors = $mform->is_validated()) {
$warnings[] = json_encode($errors);
throw new moodle_exception('generalerror');
}
if ($contextlevel) {
@@ -856,8 +1040,9 @@ class external extends external_api {
'jsonformdata' => $jsonformdata
]);
// Extra permission checkings are delegated to api::set_context_instance.
// Validate context and access to manage the registry.
self::validate_context(\context_system::instance());
api::check_can_manage_data_registry();
$serialiseddata = json_decode($params['jsonformdata']);
$data = array();
@@ -867,6 +1052,7 @@ class external extends external_api {
$customdata = \tool_dataprivacy\form\context_instance::get_context_instance_customdata($context);
$mform = new \tool_dataprivacy\form\context_instance(null, $customdata, 'post', '', null, true, $data);
if ($validateddata = $mform->get_data()) {
api::check_can_manage_data_registry($validateddata->contextid);
$context = api::set_context_instance($validateddata);
} else if ($errors = $mform->is_validated()) {
$warnings[] = json_encode($errors);
@@ -990,9 +1176,9 @@ class external extends external_api {
]);
$ids = $params['ids'];
// Validate context.
$context = context_system::instance();
self::validate_context($context);
// Validate context and access to manage the registry.
self::validate_context(\context_system::instance());
api::check_can_manage_data_registry();
$result = true;
if (!empty($ids)) {
@@ -1002,24 +1188,27 @@ class external extends external_api {
$expiredcontext = new expired_context($id);
$targetcontext = context_helper::instance_by_id($expiredcontext->get('contextid'));
// Fetch this context's child contexts. Make sure that all of the child contexts are flagged for deletion.
$childcontexts = $targetcontext->get_child_contexts();
foreach ($childcontexts as $child) {
if ($expiredchildcontext = expired_context::get_record(['contextid' => $child->id])) {
// Add this child context to the list for approval.
$expiredcontextstoapprove[] = $expiredchildcontext;
} else {
// This context has not yet been flagged for deletion.
$result = false;
$message = get_string('errorcontexthasunexpiredchildren', 'tool_dataprivacy',
$targetcontext->get_context_name(false));
$warnings[] = [
'item' => 'tool_dataprivacy_ctxexpired',
'warningcode' => 'errorcontexthasunexpiredchildren',
'message' => $message
];
// Exit the process.
break 2;
if (!$targetcontext instanceof \context_user) {
// Fetch this context's child contexts. Make sure that all of the child contexts are flagged for deletion.
// User context children do not need to be considered.
$childcontexts = $targetcontext->get_child_contexts();
foreach ($childcontexts as $child) {
if ($expiredchildcontext = expired_context::get_record(['contextid' => $child->id])) {
// Add this child context to the list for approval.
$expiredcontextstoapprove[] = $expiredchildcontext;
} else {
// This context has not yet been flagged for deletion.
$result = false;
$message = get_string('errorcontexthasunexpiredchildren', 'tool_dataprivacy',
$targetcontext->get_context_name(false));
$warnings[] = [
'item' => 'tool_dataprivacy_ctxexpired',
'warningcode' => 'errorcontexthasunexpiredchildren',
'message' => $message
];
// Exit the process.
break 2;
}
}
}
@@ -1065,6 +1254,289 @@ class external extends external_api {
]);
}
/**
* Parameters for set_context_defaults().
*
* @return external_function_parameters
*/
public static function set_context_defaults_parameters() {
return new external_function_parameters([
'contextlevel' => new external_value(PARAM_INT, 'The context level', VALUE_REQUIRED),
'category' => new external_value(PARAM_INT, 'The default category for the given context level', VALUE_REQUIRED),
'purpose' => new external_value(PARAM_INT, 'The default purpose for the given context level', VALUE_REQUIRED),
'activity' => new external_value(PARAM_PLUGIN, 'The plugin name of the activity', VALUE_DEFAULT, null),
'override' => new external_value(PARAM_BOOL, 'Whether to override existing instances with the defaults', VALUE_DEFAULT,
false),
]);
}
/**
* Updates the default category and purpose for a given context level (and optionally, a plugin).
*
* @param int $contextlevel The context level.
* @param int $category The ID matching the category.
* @param int $purpose The ID matching the purpose record.
* @param int $activity The name of the activity that we're making a defaults configuration for.
* @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
* @return array
*/
public static function set_context_defaults($contextlevel, $category, $purpose, $activity, $override) {
$warnings = [];
$params = external_api::validate_parameters(self::set_context_defaults_parameters(), [
'contextlevel' => $contextlevel,
'category' => $category,
'purpose' => $purpose,
'activity' => $activity,
'override' => $override,
]);
$contextlevel = $params['contextlevel'];
$category = $params['category'];
$purpose = $params['purpose'];
$activity = $params['activity'];
$override = $params['override'];
// Validate context.
$context = context_system::instance();
self::validate_context($context);
api::check_can_manage_data_registry();
// Set the context defaults.
$result = api::set_context_defaults($contextlevel, $category, $purpose, $activity, $override);
return [
'result' => $result,
'warnings' => $warnings
];
}
/**
* Returns for set_context_defaults().
*
* @return external_single_structure
*/
public static function set_context_defaults_returns() {
return new external_single_structure([
'result' => new external_value(PARAM_BOOL, 'Whether the context defaults were successfully set or not'),
'warnings' => new external_warnings()
]);
}
/**
* Parameters for get_category_options().
*
* @return external_function_parameters
*/
public static function get_category_options_parameters() {
return new external_function_parameters([
'includeinherit' => new external_value(PARAM_BOOL, 'Include option "Inherit"', VALUE_DEFAULT, true),
'includenotset' => new external_value(PARAM_BOOL, 'Include option "Not set"', VALUE_DEFAULT, false),
]);
}
/**
* Fetches a list of data category options containing category IDs as keys and the category name for the value.
*
* @param bool $includeinherit Whether to include the "Inherit" option.
* @param bool $includenotset Whether to include the "Not set" option.
* @return array
*/
public static function get_category_options($includeinherit, $includenotset) {
$warnings = [];
$params = self::validate_parameters(self::get_category_options_parameters(), [
'includeinherit' => $includeinherit,
'includenotset' => $includenotset
]);
$includeinherit = $params['includeinherit'];
$includenotset = $params['includenotset'];
$context = context_system::instance();
self::validate_context($context);
api::check_can_manage_data_registry();
$categories = api::get_categories();
$options = data_registry_page::category_options($categories, $includenotset, $includeinherit);
$categoryoptions = [];
foreach ($options as $id => $name) {
$categoryoptions[] = [
'id' => $id,
'name' => $name,
];
}
return [
'options' => $categoryoptions,
'warnings' => $warnings
];
}
/**
* Returns for get_category_options().
*
* @return external_single_structure
*/
public static function get_category_options_returns() {
$optiondefinition = new external_single_structure(
[
'id' => new external_value(PARAM_INT, 'The category ID'),
'name' => new external_value(PARAM_TEXT, 'The category name'),
]
);
return new external_single_structure([
'options' => new external_multiple_structure($optiondefinition),
'warnings' => new external_warnings()
]);
}
/**
* Parameters for get_purpose_options().
*
* @return external_function_parameters
*/
public static function get_purpose_options_parameters() {
return new external_function_parameters([
'includeinherit' => new external_value(PARAM_BOOL, 'Include option "Inherit"', VALUE_DEFAULT, true),
'includenotset' => new external_value(PARAM_BOOL, 'Include option "Not set"', VALUE_DEFAULT, false),
]);
}
/**
* Fetches a list of data storage purposes containing purpose IDs as keys and the purpose name for the value.
*
* @param bool $includeinherit Whether to include the "Inherit" option.
* @param bool $includenotset Whether to include the "Not set" option.
* @return array
*/
public static function get_purpose_options($includeinherit, $includenotset) {
$warnings = [];
$params = self::validate_parameters(self::get_category_options_parameters(), [
'includeinherit' => $includeinherit,
'includenotset' => $includenotset
]);
$includeinherit = $params['includeinherit'];
$includenotset = $params['includenotset'];
$context = context_system::instance();
self::validate_context($context);
$purposes = api::get_purposes();
$options = data_registry_page::purpose_options($purposes, $includenotset, $includeinherit);
$purposeoptions = [];
foreach ($options as $id => $name) {
$purposeoptions[] = [
'id' => $id,
'name' => $name,
];
}
return [
'options' => $purposeoptions,
'warnings' => $warnings
];
}
/**
* Returns for get_purpose_options().
*
* @return external_single_structure
*/
public static function get_purpose_options_returns() {
$optiondefinition = new external_single_structure(
[
'id' => new external_value(PARAM_INT, 'The purpose ID'),
'name' => new external_value(PARAM_TEXT, 'The purpose name'),
]
);
return new external_single_structure([
'options' => new external_multiple_structure($optiondefinition),
'warnings' => new external_warnings()
]);
}
/**
* Parameters for get_activity_options().
*
* @return external_function_parameters
*/
public static function get_activity_options_parameters() {
return new external_function_parameters([
'nodefaults' => new external_value(PARAM_BOOL, 'Whether to fetch all activities or only those without defaults',
VALUE_DEFAULT, false),
]);
}
/**
* Fetches a list of activity options for setting data registry defaults.
*
* @param boolean $nodefaults If false, it will fetch all of the activities. Otherwise, it will only fetch the activities
* that don't have defaults yet (e.g. when adding a new activity module defaults).
* @return array
*/
public static function get_activity_options($nodefaults) {
$warnings = [];
$params = self::validate_parameters(self::get_activity_options_parameters(), [
'nodefaults' => $nodefaults,
]);
$nodefaults = $params['nodefaults'];
$context = context_system::instance();
self::validate_context($context);
// Get activity module plugin info.
$pluginmanager = \core_plugin_manager::instance();
$modplugins = $pluginmanager->get_enabled_plugins('mod');
$modoptions = [];
// Get the module-level defaults. data_registry::get_defaults falls back to this when there are no activity defaults.
list($levelpurpose, $levelcategory) = data_registry::get_defaults(CONTEXT_MODULE);
foreach ($modplugins as $name) {
// Check if we have default purpose and category for this module if we want don't want to fetch everything.
if ($nodefaults) {
list($purpose, $category) = data_registry::get_defaults(CONTEXT_MODULE, $name);
// Compare this with the module-level defaults.
if ($purpose !== $levelpurpose || $category !== $levelcategory) {
// If the defaults for this activity has been already set, there's no need to add this in the list of options.
continue;
}
}
$displayname = $pluginmanager->plugin_name('mod_' . $name);
$modoptions[] = (object)[
'name' => $name,
'displayname' => $displayname
];
}
return [
'options' => $modoptions,
'warnings' => $warnings
];
}
/**
* Returns for get_category_options().
*
* @return external_single_structure
*/
public static function get_activity_options_returns() {
$optionsdefinition = new external_single_structure(
[
'name' => new external_value(PARAM_TEXT, 'The plugin name of the activity'),
'displayname' => new external_value(PARAM_TEXT, 'The display name of the activity'),
]
);
return new external_single_structure([
'options' => new external_multiple_structure($optionsdefinition),
'warnings' => new external_warnings()
]);
}
/**
* Gets the structure of a tree node (link + child branches).
*
@@ -25,6 +25,8 @@ namespace tool_dataprivacy\external;
defined('MOODLE_INTERNAL') || die();
use core\external\persistent_exporter;
use tool_dataprivacy\category;
use tool_dataprivacy\context_instance;
/**
* Class for exporting field data.
@@ -53,4 +55,25 @@ class category_exporter extends persistent_exporter {
'context' => 'context',
);
}
/**
* Utility function that fetches a category name from the given ID.
*
* @param int $categoryid The category ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID.
* @return string The purpose name.
*/
public static function get_name($categoryid) {
global $PAGE;
if ($categoryid === false || $categoryid == context_instance::INHERIT) {
return get_string('inherit', 'tool_dataprivacy');
} else if ($categoryid == context_instance::NOTSET) {
return get_string('notset', 'tool_dataprivacy');
} else {
$purpose = new category($categoryid);
$output = $PAGE->get_renderer('tool_dataprivacy');
$exporter = new self($purpose, ['context' => \context_system::instance()]);
$data = $exporter->export($output);
return $data->name;
}
}
}
@@ -29,6 +29,7 @@ use core\external\persistent_exporter;
use DateInterval;
use Exception;
use renderer_base;
use tool_dataprivacy\context_instance;
use tool_dataprivacy\purpose;
/**
@@ -143,4 +144,25 @@ class purpose_exporter extends persistent_exporter {
return $values;
}
/**
* Utility function that fetches a purpose name from the given ID.
*
* @param int $purposeid The purpose ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID.
* @return string The purpose name.
*/
public static function get_name($purposeid) {
global $PAGE;
if ($purposeid === false || $purposeid == context_instance::INHERIT) {
return get_string('inherit', 'tool_dataprivacy');
} else if ($purposeid == context_instance::NOTSET) {
return get_string('notset', 'tool_dataprivacy');
} else {
$purpose = new purpose($purposeid);
$output = $PAGE->get_renderer('tool_dataprivacy');
$exporter = new self($purpose, ['context' => \context_system::instance()]);
$data = $exporter->export($output);
return $data->name;
}
}
}
@@ -145,12 +145,12 @@ class context_instance extends \core\form\persistent {
$persistent->set('contextid', $context->id);
}
$purposeoptions = \tool_dataprivacy\output\data_registry_page::purpose_options(
api::get_purposes()
);
$categoryoptions = \tool_dataprivacy\output\data_registry_page::category_options(
api::get_categories()
);
$purposes = [];
foreach (api::get_purposes() as $purpose) {
$purposes[$purpose->get('id')] = $purpose;
}
$purposeoptions = \tool_dataprivacy\output\data_registry_page::purpose_options($purposes);
$categoryoptions = \tool_dataprivacy\output\data_registry_page::category_options(api::get_categories());
$customdata = [
'context' => $context,
@@ -168,9 +168,14 @@ class context_instance extends \core\form\persistent {
$context);
$customdata['purposeretentionperiods'] = [];
foreach ($purposeoptions as $optionvalue => $unused) {
// Get the effective purpose if $optionvalue would be the selected value.
$purpose = api::get_effective_context_purpose($context, $optionvalue);
foreach (array_keys($purposeoptions) as $optionvalue) {
if (isset($purposes[$optionvalue])) {
$purpose = $purposes[$optionvalue];
} else {
// Get the effective purpose if $optionvalue would be the selected value.
$purpose = api::get_effective_context_purpose($context, $optionvalue);
}
$retentionperiod = self::get_retention_display_text(
$purpose,
@@ -1,81 +0,0 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file contains the defaults form.
*
* @package tool_dataprivacy
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy\form;
defined('MOODLE_INTERNAL') || die();
use \tool_dataprivacy\output\data_registry_page;
require_once($CFG->libdir . '/formslib.php');
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
/**
* Context levels defaults form.
*
* @package tool_dataprivacy
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class defaults extends \moodleform {
/**
* Define the form.
*/
public function definition() {
global $OUTPUT;
$mform = $this->_form;
$mform->setDisableShortforms();
$notification = $OUTPUT->notification(get_string('defaultsinfo', 'tool_dataprivacy'),
\core\output\notification::NOTIFY_INFO);
$mform->addElement('html', $notification);
foreach ($this->_customdata['levels'] as $level => $classname) {
$mform->addElement('header', $classname . '-header',
get_string('contextlevelname' . $level, 'tool_dataprivacy'));
list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
$includeinherit = true;
if ($level == CONTEXT_SYSTEM) {
$includeinherit = false;
}
$categoryoptions = data_registry_page::category_options($this->_customdata['categories'], false, $includeinherit);
$purposeoptions = data_registry_page::category_options($this->_customdata['purposes'], false, $includeinherit);
$mform->addElement('select', $categoryvar, get_string('category', 'tool_dataprivacy'), $categoryoptions);
$mform->addHelpButton($categoryvar, 'categorydefault', 'tool_dataprivacy');
$mform->setType($categoryvar, PARAM_INT);
$mform->addElement('select', $purposevar, get_string('purpose', 'tool_dataprivacy'), $purposeoptions);
$mform->addHelpButton($purposevar, 'purposedefault', 'tool_dataprivacy');
$mform->setType($purposevar, PARAM_INT);
}
$this->add_action_buttons(true, get_string('savechanges'));
}
}
@@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die();
use coding_exception;
use moodle_exception;
use tool_dataprivacy\api;
use tool_dataprivacy\data_request;
/**
* Class containing helper functions for the data privacy tool.
@@ -44,9 +45,15 @@ class helper {
/** Filter constant associated with the request status filter. */
const FILTER_STATUS = 2;
/** Filter constant associated with the request creation filter. */
const FILTER_CREATION = 3;
/** The request filters preference key. */
const PREF_REQUEST_FILTERS = 'tool_dataprivacy_request-filters';
/** The number of data request records per page preference key. */
const PREF_REQUEST_PERPAGE = 'tool_dataprivacy_request-perpage';
/**
* Retrieves the human-readable text value of a data request type.
*
@@ -142,6 +149,34 @@ class helper {
];
}
/**
* Retrieves the human-readable value of a data request creation method.
*
* @param int $creation The request creation method.
* @return string
* @throws moodle_exception
*/
public static function get_request_creation_method_string($creation) {
$creationmethods = self::get_request_creation_methods();
if (!isset($creationmethods[$creation])) {
throw new moodle_exception('errorinvalidrequestcreationmethod', 'tool_dataprivacy');
}
return $creationmethods[$creation];
}
/**
* Returns the key value-pairs of request creation method code and string value.
*
* @return array
*/
public static function get_request_creation_methods() {
return [
data_request::DATAREQUEST_CREATION_MANUAL => get_string('creationmanual', 'tool_dataprivacy'),
data_request::DATAREQUEST_CREATION_AUTO => get_string('creationauto', 'tool_dataprivacy'),
];
}
/**
* Get the users that a user can make data request for.
*
@@ -196,6 +231,10 @@ class helper {
'name' => get_string('requeststatus', 'tool_dataprivacy'),
'options' => self::get_request_statuses()
],
self::FILTER_CREATION => (object)[
'name' => get_string('requestcreation', 'tool_dataprivacy'),
'options' => self::get_request_creation_methods()
],
];
$options = [];
foreach ($filters as $category => $filtercategory) {
@@ -76,6 +76,18 @@ class metadata_registry {
if (isset($contributedplugins[$plugintype][$shortname])) {
$internaldata['external'] = true;
}
// Check if the interface is deprecated.
if (!$manager->is_empty_subsystem($component)) {
$classname = $manager->get_provider_classname_for_component($component);
if (class_exists($classname)) {
$componentclass = new $classname();
if ($componentclass instanceof \core_privacy\local\deprecated) {
$internaldata['deprecated'] = true;
}
}
}
return $internaldata;
}, $leaves['plugins']);
$fullyrichtree[$branch]['plugin_type_raw'] = $plugintype;
@@ -426,7 +426,7 @@ class data_registry_page implements renderable, templatable {
/**
* From a list of purpose persistents to a list of id => name purposes.
*
* @param \tool_dataprivacy\purpose $purposes
* @param \tool_dataprivacy\purpose[] $purposes
* @param bool $includenotset
* @param bool $includeinherit
* @return string[]
@@ -443,7 +443,7 @@ class data_registry_page implements renderable, templatable {
/**
* From a list of category persistents to a list of id => name categories.
*
* @param \tool_dataprivacy\category $categories
* @param \tool_dataprivacy\category[] $categories
* @param bool $includenotset
* @param bool $includeinherit
* @return string[]
@@ -86,7 +86,7 @@ class data_requests_page implements renderable, templatable {
$data->filter = $filter->export_for_template($output);
ob_start();
$this->table->out(helper::DEFAULT_PAGE_SIZE, true);
$this->table->out($this->table->get_requests_per_page(), true);
$requests = ob_get_contents();
ob_end_clean();
@@ -62,24 +62,38 @@ class data_requests_table extends table_sql {
/** @var \tool_dataprivacy\data_request[] Array of data request persistents. */
protected $datarequests = [];
/** @var int The number of data request to be displayed per page. */
protected $perpage;
/** @var int[] The available options for the number of data request to be displayed per page. */
protected $perpageoptions = [25, 50, 100, 250];
/**
* data_requests_table constructor.
*
* @param int $userid The user ID
* @param int[] $statuses
* @param int[] $types
* @param int[] $creationmethods
* @param bool $manage
* @throws coding_exception
*/
public function __construct($userid = 0, $statuses = [], $types = [], $manage = false) {
public function __construct($userid = 0, $statuses = [], $types = [], $creationmethods = [], $manage = false) {
parent::__construct('data-requests-table');
$this->userid = $userid;
$this->statuses = $statuses;
$this->types = $types;
$this->creationmethods = $creationmethods;
$this->manage = $manage;
$checkboxattrs = [
'title' => get_string('selectall'),
'data-action' => 'selectall'
];
$columnheaders = [
'select' => html_writer::checkbox('selectall', 1, false, null, $checkboxattrs),
'type' => get_string('requesttype', 'tool_dataprivacy'),
'userid' => get_string('user', 'tool_dataprivacy'),
'timecreated' => get_string('daterequested', 'tool_dataprivacy'),
@@ -91,7 +105,26 @@ class data_requests_table extends table_sql {
$this->define_columns(array_keys($columnheaders));
$this->define_headers(array_values($columnheaders));
$this->no_sorting('actions');
$this->no_sorting('select', 'actions');
}
/**
* The select column.
*
* @param stdClass $data The row data.
* @return string
*/
public function col_select($data) {
if ($data->status == \tool_dataprivacy\api::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
$stringdata = [
'username' => $data->foruser->fullname,
'requesttype' => \core_text::strtolower($data->typenameshort)
];
return \html_writer::checkbox('requestids[]', $data->id, false, '',
['class' => 'selectrequests', 'title' => get_string('selectuserdatarequest',
'tool_dataprivacy', $stringdata)]);
}
}
/**
@@ -242,11 +275,12 @@ class data_requests_table extends table_sql {
$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());
$datarequests = api::get_data_requests($this->userid, $this->statuses, $this->types,
$this->creationmethods, $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);
$total = api::get_data_requests_count($this->userid, $this->statuses, $this->types,
$this->creationmethods);
$this->pagesize($pagesize, $total);
$this->rawdata = [];
@@ -290,4 +324,72 @@ class data_requests_table extends table_sql {
protected function show_hide_link($column, $index) {
return '';
}
/**
* Override the table's wrap_html_finish method in order to render the bulk actions and
* records per page options.
*/
public function wrap_html_finish() {
global $OUTPUT;
$data = new stdClass();
$data->options = [
[
'value' => 0,
'name' => ''
],
[
'value' => \tool_dataprivacy\api::DATAREQUEST_ACTION_APPROVE,
'name' => get_string('approve', 'tool_dataprivacy')
],
[
'value' => \tool_dataprivacy\api::DATAREQUEST_ACTION_REJECT,
'name' => get_string('deny', 'tool_dataprivacy')
]
];
$perpageoptions = array_combine($this->perpageoptions, $this->perpageoptions);
$perpageselect = new \single_select(new moodle_url(''), 'perpage',
$perpageoptions, get_user_preferences('tool_dataprivacy_request-perpage'), null, 'selectgroup');
$perpageselect->label = get_string('perpage', 'moodle');
$data->perpage = $OUTPUT->render($perpageselect);
echo $OUTPUT->render_from_template('tool_dataprivacy/data_requests_bulk_actions', $data);
}
/**
* Set the number of data request records to be displayed per page.
*
* @param int $perpage The number of data request records.
*/
public function set_requests_per_page($perpage) {
$this->perpage = $perpage;
}
/**
* Get the number of data request records to be displayed per page.
*
* @return int The number of data request records.
*/
public function get_requests_per_page() {
return $this->perpage;
}
/**
* Set the available options for the number of data request to be displayed per page.
*
* @param array $perpageoptions The available options for the number of data request to be displayed per page.
*/
public function set_requests_per_page_options($perpageoptions) {
$this->$perpageoptions = $perpageoptions;
}
/**
* Get the available options for the number of data request to be displayed per page.
*
* @return array The available options for the number of data request to be displayed per page.
*/
public function get_requests_per_page_options() {
return $this->perpageoptions;
}
}
@@ -0,0 +1,178 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Class containing data for the data registry defaults.
*
* @package tool_dataprivacy
* @copyright 2018 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy\output;
defined('MOODLE_INTERNAL') || die();
use action_menu_link_primary;
use coding_exception;
use moodle_exception;
use moodle_url;
use renderable;
use renderer_base;
use stdClass;
use templatable;
use tool_dataprivacy\data_registry;
use tool_dataprivacy\external\category_exporter;
use tool_dataprivacy\external\purpose_exporter;
/**
* Class containing data for the data registry defaults.
*
* @copyright 2018 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class defaults_page implements renderable, templatable {
/** @var int $mode The display mode. */
protected $mode = null;
/** @var int $category The default category for the given mode. */
protected $category = null;
/** @var int $purpose The default purpose for the given mode. */
protected $purpose = null;
/** @var stdClass[] $otherdefaults Other defaults for the given mode. */
protected $otherdefaults = [];
/** @var bool $canedit Whether editing is allowed. */
protected $canedit = false;
/**
* Construct this renderable.
*
* @param int $mode The display mode.
* @param int $category The default category for the given mode.
* @param int $purpose The default purpose for the given mode.
* @param stdClass[] $otherdefaults Other defaults for the given mode.
* @param bool $canedit Whether editing is allowed.
*/
public function __construct($mode, $category, $purpose, $otherdefaults = [], $canedit = false) {
$this->mode = $mode;
$this->category = $category;
$this->purpose = $purpose;
$this->otherdefaults = $otherdefaults;
$this->canedit = $canedit;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output
* @return stdClass
* @throws coding_exception
* @throws moodle_exception
*/
public function export_for_template(renderer_base $output) {
$data = new stdClass();
// Set tab URLs.
$coursecaturl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_COURSECAT]);
$courseurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_COURSE]);
$moduleurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_MODULE]);
$blockurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_BLOCK]);
$data->coursecaturl = $coursecaturl;
$data->courseurl = $courseurl;
$data->moduleurl = $moduleurl;
$data->blockurl = $blockurl;
// Set display mode.
switch ($this->mode) {
case CONTEXT_COURSECAT:
$data->modecoursecat = true;
break;
case CONTEXT_COURSE:
$data->modecourse = true;
break;
case CONTEXT_MODULE:
$data->modemodule = true;
break;
case CONTEXT_BLOCK:
$data->modeblock = true;
break;
default:
$data->modecoursecat = true;
break;
}
// Set config variables.
$configname = \context_helper::get_class_for_level($this->mode);
list($purposevar, $categoryvar) = data_registry::var_names_from_context($configname);
$data->categoryvar = $categoryvar;
$data->purposevar = $purposevar;
// Set default category.
$data->categoryid = $this->category;
$data->category = category_exporter::get_name($this->category);
// Set default purpose.
$data->purposeid = $this->purpose;
$data->purpose = purpose_exporter::get_name($this->purpose);
// Set other defaults.
$otherdefaults = [];
$url = new moodle_url('#');
foreach ($this->otherdefaults as $pluginname => $values) {
$defaults = [
'name' => $values->name,
'category' => category_exporter::get_name($values->category),
'purpose' => purpose_exporter::get_name($values->purpose),
];
if ($this->canedit) {
$actions = [];
// Edit link.
$editattrs = [
'data-action' => 'edit-activity-defaults',
'data-contextlevel' => $this->mode,
'data-activityname' => $pluginname,
'data-category' => $values->category,
'data-purpose' => $values->purpose,
];
$editlink = new action_menu_link_primary($url, new \pix_icon('t/edit', get_string('edit')),
get_string('edit'), $editattrs);
$actions[] = $editlink->export_for_template($output);
// Delete link.
$deleteattrs = [
'data-action' => 'delete-activity-defaults',
'data-contextlevel' => $this->mode,
'data-activityname' => $pluginname,
'data-activitydisplayname' => $values->name,
];
$deletelink = new action_menu_link_primary($url, new \pix_icon('t/delete', get_string('delete')),
get_string('delete'), $deleteattrs);
$actions[] = $deletelink->export_for_template($output);
$defaults['actions'] = $actions;
}
$otherdefaults[] = (object)$defaults;
}
$data->otherdefaults = $otherdefaults;
$data->canedit = $this->canedit;
$data->contextlevel = $this->mode;
return $data;
}
}
@@ -47,7 +47,7 @@ class page_helper {
*/
public static function setup(moodle_url $url, $title, $attachtoparentnode = '',
$requiredcapability = 'tool/dataprivacy:managedataregistry') {
global $PAGE;
global $PAGE, $SITE;
$context = context_system::instance();
@@ -65,7 +65,7 @@ class page_helper {
$PAGE->set_context($context);
$PAGE->set_pagelayout('admin');
$PAGE->set_title($title);
$PAGE->set_heading($title);
$PAGE->set_heading($SITE->fullname);
// If necessary, override the settings navigation to add this page into the breadcrumb navigation.
if ($attachtoparentnode) {
@@ -76,6 +76,8 @@ class provider implements
$collection->add_user_preference(tool_helper::PREF_REQUEST_FILTERS,
'privacy:metadata:preference:tool_dataprivacy_request-filters');
$collection->add_user_preference(tool_helper::PREF_REQUEST_PERPAGE,
'privacy:metadata:preference:tool_dataprivacy_request-perpage');
return $collection;
}
@@ -134,6 +136,8 @@ class provider implements
$data->type = tool_helper::get_shortened_request_type_string($record->type);
// Status.
$data->status = tool_helper::get_request_status_string($record->status);
// Creation method.
$data->creationmethod = tool_helper::get_request_creation_method_string($record->creationmethod);
// Comments.
$data->comments = $record->comments;
// The DPO's comment about this request.
@@ -192,6 +196,10 @@ class provider implements
$option->category = get_string('requeststatus', 'tool_dataprivacy');
$option->name = tool_helper::get_request_status_string($value);
break;
case tool_helper::FILTER_CREATION:
$option->category = get_string('requestcreation', 'tool_dataprivacy');
$option->name = tool_helper::get_request_creation_method_string($value);
break;
}
$descriptions[] = get_string('filteroption', 'tool_dataprivacy', $option);
}
@@ -200,5 +208,11 @@ class provider implements
$descriptionstext = implode(', ', $descriptions);
writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_FILTERS, $values, $descriptionstext);
}
$prefperpage = get_user_preferences(tool_helper::PREF_REQUEST_PERPAGE, null, $userid);
if ($prefperpage !== null) {
writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_PERPAGE, $prefperpage,
get_string('privacy:metadata:preference:tool_dataprivacy_request-perpage', 'tool_dataprivacy'));
}
}
}
@@ -0,0 +1,91 @@
<?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 create delete data request for pre-existing deleted users.
*
* @package tool_dataprivacy
* @copyright 2018 Mihail Geshoski
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy\task;
use core\task\scheduled_task;
use tool_dataprivacy\api;
use tool_dataprivacy\data_request;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
/**
* Scheduled task to create delete data request for pre-existing deleted users.
*
* @package tool_dataprivacy
* @copyright 2018 Mihail Geshoski
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class delete_existing_deleted_users extends scheduled_task {
/**
* Returns the task name.
*
* @return string
*/
public function get_name() {
return get_string('deleteexistingdeleteduserstask', 'tool_dataprivacy');
}
/**
* Run the task to delete expired data request files and update request statuses.
*
*/
public function execute() {
global $DB;
// Automatic creation of deletion requests must be enabled.
if (get_config('tool_dataprivacy', 'automaticdeletionrequests')) {
// Select all deleted users that do not have any delete data requests created for them.
$sql = "SELECT DISTINCT(u.id)
FROM {user} u
LEFT JOIN {tool_dataprivacy_request} r
ON u.id = r.userid
WHERE u.deleted = ?
AND (r.id IS NULL
OR r.type != ?)";
$params = [
1,
api::DATAREQUEST_TYPE_DELETE
];
$deletedusers = $DB->get_records_sql($sql, $params);
$createdrequests = 0;
foreach ($deletedusers as $user) {
api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE,
get_string('datarequestcreatedfromscheduledtask', 'tool_dataprivacy'),
data_request::DATAREQUEST_CREATION_AUTO);
$createdrequests++;
}
if ($createdrequests > 0) {
mtrace($createdrequests . ' delete data request(s) created for existing deleted users');
}
}
}
}
@@ -52,18 +52,10 @@ class delete_expired_contexts extends scheduled_task {
/**
* Run the task to delete context instances based on their retention periods.
*
*/
public function execute() {
$manager = new \tool_dataprivacy\expired_course_related_contexts();
$deleted = $manager->delete();
if ($deleted > 0) {
mtrace($deleted . ' course-related contexts have been deleted');
}
$manager = new \tool_dataprivacy\expired_user_contexts();
$deleted = $manager->delete();
if ($deleted > 0) {
mtrace($deleted . ' user contexts have been deleted');
}
$manager = new \tool_dataprivacy\expired_contexts_manager();
list($courses, $users) = $manager->process_approved_deletions();
mtrace("Processed deletions for {$courses} course contexts, and {$users} user contexts as expired");
}
}
@@ -54,15 +54,8 @@ class expired_retention_period extends scheduled_task {
* Run the task to flag context instances as expired.
*/
public function execute() {
$manager = new \tool_dataprivacy\expired_course_related_contexts();
$flagged = $manager->flag_expired();
if ($flagged > 0) {
mtrace($flagged . ' course-related contexts have been flagged as expired');
}
$manager = new \tool_dataprivacy\expired_user_contexts();
$flagged = $manager->flag_expired();
if ($flagged > 0) {
mtrace($flagged . ' user contexts have been flagged as expired');
}
$manager = new \tool_dataprivacy\expired_contexts_manager();
list($courses, $users) = $manager->flag_expired_contexts();
mtrace("Flagged {$courses} course contexts, and {$users} user contexts as expired");
}
}
@@ -70,27 +70,6 @@ class initiate_data_request_task extends adhoc_task {
return;
}
$requestedby = $datarequest->get('requestedby');
$valid = true;
$comment = '';
$foruser = $datarequest->get('userid');
if ($foruser != $requestedby) {
if (!$valid = api::can_create_data_request_for_user($foruser, $requestedby)) {
$params = (object)[
'requestedby' => $requestedby,
'userid' => $foruser
];
$comment = get_string('errornocapabilitytorequestforothers', 'tool_dataprivacy', $params);
mtrace($comment);
}
}
// Reject the request outright if it's invalid.
if (!$valid) {
$dpo = $datarequest->get('dpo');
api::update_request_status($requestid, api::DATAREQUEST_STATUS_REJECTED, $dpo, $comment);
return;
}
// Update the status of this request as pre-processing.
mtrace('Generating the contexts containing personal data for the user...');
api::update_request_status($requestid, api::DATAREQUEST_STATUS_PREPROCESSING);
@@ -109,13 +88,19 @@ class initiate_data_request_task extends adhoc_task {
// Get the list of the site Data Protection Officers.
$dpos = api::get_site_dpos();
// Email the data request to the Data Protection Officer(s)/Admin(s).
foreach ($dpos as $dpo) {
$dponame = fullname($dpo);
if (api::notify_dpo($dpo, $datarequest)) {
mtrace('Message sent to DPO: ' . $dponame);
} else {
mtrace('A problem was encountered while sending the message to the DPO: ' . $dponame);
// We should prevent DPO(s)/Admin(s) being flooded with notifications for each request when bulk delete
// data requests are being created.
// NOTE: This should be improved, we should not totally disable the notifications for automatically
// created requests. Possibly, we should create one notification for these such cases.
if ($datarequest->get('creationmethod') != data_request::DATAREQUEST_CREATION_AUTO) {
// Email the data request to the Data Protection Officer(s)/Admin(s).
foreach ($dpos as $dpo) {
$dponame = fullname($dpo);
if (api::notify_dpo($dpo, $datarequest)) {
mtrace('Message sent to DPO: ' . $dponame);
} else {
mtrace('A problem was encountered while sending the message to the DPO: ' . $dponame);
}
}
}
}
@@ -76,17 +76,25 @@ class process_data_request_task extends adhoc_task {
// Get the user details now. We might not be able to retrieve it later if it's a deletion processing.
$foruser = core_user::get_user($request->userid);
$usercontext = \context_user::instance($foruser->id);
// 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;
$deleteuser = false;
if ($request->type == api::DATAREQUEST_TYPE_EXPORT) {
// Run as the user performing the export.
cron_setup_user($foruser);
// Get the user context.
$usercontext = \context_user::instance($foruser->id, IGNORE_MISSING);
if (!$usercontext) {
mtrace("Request {$requestid} cannot be processed due to a missing user context instance for the user
with ID {$foruser->id}. Skipping...");
return;
}
// Get the collection of approved_contextlist objects needed for core_privacy data export.
$approvedclcollection = api::get_approved_contextlist_collection_for_request($requestpersistent);
@@ -120,6 +128,7 @@ class process_data_request_task extends adhoc_task {
$manager->delete_data_for_user($approvedclcollection);
$completestatus = api::DATAREQUEST_STATUS_DELETED;
$deleteuser = !$foruser->deleted;
}
// When the preparation of the metadata finishes, update the request status to awaiting approval.
@@ -195,12 +204,19 @@ class process_data_request_task extends adhoc_task {
// Send message to the user involved.
if ($notifyuser) {
$messagesent = false;
if ($emailonly) {
email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
// Do not sent an email if the user has been deleted. The user email has been previously deleted.
if (!$foruser->deleted) {
$messagesent = email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
}
} else {
message_send($message);
$messagesent = message_send($message);
}
if ($messagesent) {
mtrace('Message sent to user: ' . $messagetextdata['username']);
}
mtrace('Message sent to user: ' . $messagetextdata['username']);
}
// Send to requester as well in some circumstances.
@@ -248,7 +264,7 @@ class process_data_request_task extends adhoc_task {
cron_setup_user();
if ($request->type == api::DATAREQUEST_TYPE_DELETE) {
if ($deleteuser) {
// Delete the user.
delete_user($foruser);
}
+13 -4
View File
@@ -24,12 +24,12 @@
require_once('../../../config.php');
require_once('lib.php');
require_once('classes/api.php');
require_once('createdatarequest_form.php');
$manage = optional_param('manage', 0, PARAM_INT);
$requesttype = optional_param('type', \tool_dataprivacy\api::DATAREQUEST_TYPE_EXPORT, PARAM_INT);
$url = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php', ['manage' => $manage]);
$url = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php', ['manage' => $manage, 'type' => $requesttype]);
$PAGE->set_url($url);
@@ -59,6 +59,7 @@ if (!$manage && !\tool_dataprivacy\api::can_contact_dpo()) {
}
$mform = new tool_dataprivacy_data_request_form($url->out(false), ['manage' => !empty($manage)]);
$mform->set_data(['type' => $requesttype]);
// Data request cancelled.
if ($mform->is_cancelled()) {
@@ -67,6 +68,14 @@ if ($mform->is_cancelled()) {
// Data request submitted.
if ($data = $mform->get_data()) {
if ($data->userid != $USER->id) {
if (!\tool_dataprivacy\api::can_manage_data_requests($USER->id)) {
// If not a DPO, only users with the capability to make data requests for the user should be allowed.
// (e.g. users with the Parent role, etc).
\tool_dataprivacy\api::require_can_create_data_request_for_user($data->userid);
}
}
\tool_dataprivacy\api::create_data_request($data->userid, $data->type, $data->comments);
if ($manage) {
@@ -78,8 +87,8 @@ if ($data = $mform->get_data()) {
redirect($returnurl, $redirectmessage);
}
$title = get_string('contactdataprotectionofficer', 'tool_dataprivacy');
$PAGE->set_heading($title);
$title = get_string('createnewdatarequest', 'tool_dataprivacy');
$PAGE->set_heading($SITE->fullname);
$PAGE->set_title($title);
echo $OUTPUT->header();
echo $OUTPUT->heading($title);
+1
View File
@@ -36,6 +36,7 @@ $title = get_string('datadeletion', 'tool_dataprivacy');
\tool_dataprivacy\page_helper::setup($url, $title);
echo $OUTPUT->header();
echo $OUTPUT->heading($title);
if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
$table = new \tool_dataprivacy\output\expired_contexts_table($filter);
+1
View File
@@ -37,6 +37,7 @@ $title = get_string('dataregistry', 'tool_dataprivacy');
$output = $PAGE->get_renderer('tool_dataprivacy');
echo $output->header();
echo $OUTPUT->heading($title);
if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
$dataregistry = new tool_dataprivacy\output\data_registry_page($contextlevel, $contextid);
+14 -1
View File
@@ -27,6 +27,8 @@ require_once('lib.php');
require_login(null, false);
$perpage = optional_param('perpage', 0, PARAM_INT);
$url = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
$title = get_string('datarequests', 'tool_dataprivacy');
@@ -53,6 +55,7 @@ if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
$types = [];
$statuses = [];
$creationmethods = [];
foreach ($filtersapplied as $filter) {
list($category, $value) = explode(':', $filter);
switch($category) {
@@ -62,10 +65,20 @@ if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
case \tool_dataprivacy\local\helper::FILTER_STATUS:
$statuses[] = $value;
break;
case \tool_dataprivacy\local\helper::FILTER_CREATION:
$creationmethods[] = $value;
break;
}
}
$table = new \tool_dataprivacy\output\data_requests_table(0, $statuses, $types, true);
$table = new \tool_dataprivacy\output\data_requests_table(0, $statuses, $types, $creationmethods, true);
if (!empty($perpage)) {
set_user_preference(\tool_dataprivacy\local\helper::PREF_REQUEST_PERPAGE, $perpage);
} else {
$prefperpage = get_user_preferences(\tool_dataprivacy\local\helper::PREF_REQUEST_PERPAGE);
$perpage = ($prefperpage) ? $prefperpage : $table->get_requests_per_page_options()[0];
}
$table->set_requests_per_page($perpage);
$table->baseurl = $url;
$requestlist = new tool_dataprivacy\output\data_requests_page($table, $filtersapplied);
+32
View File
@@ -0,0 +1,32 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file defines observers needed by the plugin.
*
* @package tool_dataprivacy
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$observers = [
[
'eventname' => '\core\event\user_deleted',
'callback' => '\tool_dataprivacy\event\user_deleted_observer::create_delete_data_request',
],
];
+1
View File
@@ -19,6 +19,7 @@
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user who created/modified this request object"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time this data request was created"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The last time this data request was updated"/>
<FIELD NAME="creationmethod" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The type of the creation method of the data request"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+60
View File
@@ -73,6 +73,16 @@ $functions = [
'ajax' => true,
'loginrequired' => true,
],
'tool_dataprivacy_bulk_approve_data_requests' => [
'classname' => 'tool_dataprivacy\external',
'methodname' => 'bulk_approve_data_requests',
'classpath' => '',
'description' => 'Bulk approve data requests',
'type' => 'write',
'capabilities' => 'tool/dataprivacy:managedatarequests',
'ajax' => true,
'loginrequired' => true,
],
'tool_dataprivacy_deny_data_request' => [
'classname' => 'tool_dataprivacy\external',
'methodname' => 'deny_data_request',
@@ -83,6 +93,16 @@ $functions = [
'ajax' => true,
'loginrequired' => true,
],
'tool_dataprivacy_bulk_deny_data_requests' => [
'classname' => 'tool_dataprivacy\external',
'methodname' => 'bulk_deny_data_requests',
'classpath' => '',
'description' => 'Bulk deny data requests',
'type' => 'write',
'capabilities' => 'tool/dataprivacy:managedatarequests',
'ajax' => true,
'loginrequired' => true,
],
'tool_dataprivacy_get_users' => [
'classname' => 'tool_dataprivacy\external',
'methodname' => 'get_users',
@@ -173,4 +193,44 @@ $functions = [
'ajax' => true,
'loginrequired' => true,
],
'tool_dataprivacy_set_context_defaults' => [
'classname' => 'tool_dataprivacy\external',
'methodname' => 'set_context_defaults',
'classpath' => '',
'description' => 'Updates the default category and purpose for a given context level (and optionally, a plugin)',
'type' => 'write',
'capabilities' => 'tool/dataprivacy:managedataregistry',
'ajax' => true,
'loginrequired' => true,
],
'tool_dataprivacy_get_category_options' => [
'classname' => 'tool_dataprivacy\external',
'methodname' => 'get_category_options',
'classpath' => '',
'description' => 'Fetches a list of data category options',
'type' => 'read',
'capabilities' => 'tool/dataprivacy:managedataregistry',
'ajax' => true,
'loginrequired' => true,
],
'tool_dataprivacy_get_purpose_options' => [
'classname' => 'tool_dataprivacy\external',
'methodname' => 'get_purpose_options',
'classpath' => '',
'description' => 'Fetches a list of data storage purpose options',
'type' => 'read',
'capabilities' => 'tool/dataprivacy:managedataregistry',
'ajax' => true,
'loginrequired' => true,
],
'tool_dataprivacy_get_activity_options' => [
'classname' => 'tool_dataprivacy\external',
'methodname' => 'get_activity_options',
'classpath' => '',
'description' => 'Fetches a list of activity options',
'type' => 'read',
'capabilities' => 'tool/dataprivacy:managedataregistry',
'ajax' => true,
'loginrequired' => true,
],
];
+9
View File
@@ -50,5 +50,14 @@ $tasks = array(
'day' => '*',
'dayofweek' => '*',
'month' => '*'
), array(
'classname' => 'tool_dataprivacy\task\delete_existing_deleted_users',
'blocking' => 0,
'minute' => 'R',
'hour' => 'R',
'day' => '*',
'dayofweek' => '*',
'month' => '*',
'disabled' => true,
),
);
+28
View File
@@ -201,5 +201,33 @@ function xmldb_tool_dataprivacy_upgrade($oldversion) {
upgrade_plugin_savepoint(true, 2017051517, 'tool', 'dataprivacy');
}
if ($oldversion < 2017051554) {
// Define field sensitivedatareasons to be added to tool_dataprivacy_purpose.
$table = new xmldb_table('tool_dataprivacy_request');
$field = new xmldb_field('creationmethod', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, 0, 'timemodified');
// Conditionally launch add field sensitivedatareasons.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Dataprivacy savepoint reached.
upgrade_plugin_savepoint(true, 2017051554, 'tool', 'dataprivacy');
}
if ($oldversion < 2017051556) {
// Delete orphaned data privacy requests.
$sql = "SELECT r.id
FROM {tool_dataprivacy_request} r LEFT JOIN {user} u ON r.userid = u.id
WHERE u.id IS NULL";
$orphaned = $DB->get_fieldset_sql($sql);
if ($orphaned) {
$DB->delete_records_list('tool_dataprivacy_request', 'id', $orphaned);
}
upgrade_plugin_savepoint(true, 2017051556, 'tool', 'dataprivacy');
}
return true;
}
+28 -35
View File
@@ -32,47 +32,40 @@ $title = get_string('setdefaults', 'tool_dataprivacy');
\tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry');
$levels = \context_helper::get_all_levels();
// They are set through the context level site and user.
unset($levels[CONTEXT_SYSTEM]);
unset($levels[CONTEXT_USER]);
$mode = optional_param('mode', CONTEXT_COURSECAT, PARAM_INT);
$classname = context_helper::get_class_for_level($mode);
list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
$purpose = get_config('tool_dataprivacy', $purposevar);
$category = get_config('tool_dataprivacy', $categoryvar);
$customdata = [
'levels' => $levels,
'purposes' => \tool_dataprivacy\api::get_purposes(),
'categories' => \tool_dataprivacy\api::get_categories(),
];
$form = new \tool_dataprivacy\form\defaults($PAGE->url->out(false), $customdata);
$otherdefaults = [];
if ($mode == CONTEXT_MODULE) {
// Get activity module plugin info.
$pluginmanager = core_plugin_manager::instance();
$modplugins = $pluginmanager->get_enabled_plugins('mod');
$toform = new stdClass();
foreach ($levels as $level => $classname) {
list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
$toform->{$purposevar} = get_config('tool_dataprivacy', $purposevar);
$toform->{$categoryvar} = get_config('tool_dataprivacy', $categoryvar);
}
$form->set_data($toform);
$returnurl = new \moodle_url('/admin/tool/dataprivacy/dataregistry.php');
if ($form->is_cancelled()) {
redirect($returnurl);
} else if ($data = $form->get_data()) {
foreach ($levels as $level => $classname) {
list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
if (isset($data->{$purposevar})) {
set_config($purposevar, $data->{$purposevar}, 'tool_dataprivacy');
}
if (isset($data->{$categoryvar})) {
set_config($categoryvar, $data->{$categoryvar}, 'tool_dataprivacy');
foreach ($modplugins as $name) {
list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname, $name);
$plugincategory = get_config('tool_dataprivacy', $categoryvar);
$pluginpurpose = get_config('tool_dataprivacy', $purposevar);
if ($plugincategory === false && $pluginpurpose === false) {
// If no purpose and category has been set for this plugin, then there's no need to show this on the list.
continue;
}
$displayname = $pluginmanager->plugin_name('mod_' . $name);
$otherdefaults[$name] = (object)[
'name' => $displayname,
'category' => $plugincategory,
'purpose' => $pluginpurpose,
];
}
redirect($returnurl, get_string('defaultssaved', 'tool_dataprivacy'),
0, \core\output\notification::NOTIFY_SUCCESS);
}
$defaultspage = new \tool_dataprivacy\output\defaults_page($mode, $category, $purpose, $otherdefaults, true);
$output = $PAGE->get_renderer('tool_dataprivacy');
echo $output->header();
$form->display();
echo $output->heading($title);
echo $output->render_from_template('tool_dataprivacy/defaults_page', $defaultspage->export_for_template($output));
echo $output->footer();
@@ -28,9 +28,14 @@ $string['pluginname'] = 'Data privacy';
$string['pluginname_help'] = 'Data privacy plugin';
$string['activitiesandresources'] = 'Activities and resources';
$string['addcategory'] = 'Add category';
$string['addnewdefaults'] = 'Add a new module default';
$string['addpurpose'] = 'Add purpose';
$string['approve'] = 'Approve';
$string['approverequest'] = 'Approve request';
$string['automaticdeletionrequests'] = 'Create automatic data deletion requests';
$string['automaticdeletionrequests_desc'] = 'If enabled, automatic delete data request will be created upon user deletion or for each existing deleted user which data was not fully deleted.';
$string['bulkapproverequests'] = 'Approve requests';
$string['bulkdenyrequests'] = 'Deny requests';
$string['cachedef_purpose'] = 'Data purposes';
$string['cachedef_contextlevel'] = 'Context levels purpose and category';
$string['cancelrequest'] = 'Cancel request';
@@ -46,9 +51,11 @@ $string['categoryupdated'] = 'Category updated';
$string['close'] = 'Close';
$string['compliant'] = 'Compliant';
$string['confirmapproval'] = 'Do you really want to approve this data request?';
$string['confirmbulkapproval'] = 'Do you really want to bulk approve the selected data requests?';
$string['confirmcompletion'] = 'Do you really want to mark this user enquiry as complete?';
$string['confirmcontextdeletion'] = 'Do you really want to confirm the deletion of the selected contexts? This will also delete all of the user data for their respective sub-contexts.';
$string['confirmdenial'] = 'Do you really want deny this data request?';
$string['confirmbulkdenial'] = 'Do you really want to bulk deny the selected data requests?';
$string['contactdataprotectionofficer'] = 'Contact the privacy officer';
$string['contactdataprotectionofficer_desc'] = 'If enabled, users will be able to contact the privacy officer and make a data request via a link on their profile page.';
$string['contextlevelname10'] = 'Site';
@@ -60,7 +67,10 @@ $string['contextlevelname80'] = 'Blocks';
$string['contextpurposecategorysaved'] = 'Purpose and category saved.';
$string['contactdpoviaprivacypolicy'] = 'Please contact the privacy officer as described in the privacy policy.';
$string['createcategory'] = 'Create data category';
$string['createnewdatarequest'] = 'Create a new data request';
$string['createpurpose'] = 'Create data purpose';
$string['creationauto'] = 'Automatically';
$string['creationmanual'] = 'Manually';
$string['datadeletion'] = 'Data deletion';
$string['datadeletionpagehelp'] = 'Data for which the retention period has expired are listed here. Please review and confirm data deletion, which will then be executed by the "Delete expired contexts" scheduled task.';
$string['dataprivacy:makedatarequestsforchildren'] = 'Make data requests for minors';
@@ -71,21 +81,30 @@ $string['dataprivacy:downloadallrequests'] = 'Download exported data for everyon
$string['dataregistry'] = 'Data registry';
$string['dataregistryinfo'] = 'The data registry enables categories (types of data) and purposes (the reasons for processing data) to be set for all content on the site - from users and courses down to activities and blocks. For each purpose, a retention period may be set. When a retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
$string['datarequestcreatedforuser'] = 'Data request created for {$a}';
$string['datarequestcreatedfromscheduledtask'] = 'Automatically created from a scheduled task (pre-existing deleted user).';
$string['datarequestemailsubject'] = 'Data request: {$a}';
$string['datarequestcreatedupondelete'] = 'Automatically created upon user deletion.';
$string['datarequests'] = 'Data requests';
$string['datecomment'] = '[{$a->date}]: ' . PHP_EOL . ' {$a->comment}';
$string['daterequested'] = 'Date requested';
$string['daterequesteddetail'] = 'Date requested:';
$string['defaultsinfo'] = 'Default categories and purposes are applied to all newly created instances.';
$string['defaultsinfo'] = 'Default categories and purposes are applied to all new and existing instances where a value is not set.';
$string['defaultswarninginfo'] = 'Warning: Changing these defaults may affect the retention period of existing instances.';
$string['deletecategory'] = 'Delete category';
$string['deletecategorytext'] = 'Are you sure you want to delete the category \'{$a}\'?';
$string['deletedefaults'] = 'Delete defaults: {$a}';
$string['deletedefaultsconfirmation'] = 'Are you sure you want to delete the default category and purpose for {$a} modules?';
$string['deleteexpiredcontextstask'] = 'Delete expired contexts';
$string['deleteexistingdeleteduserstask'] = 'Create delete data request for pre-existing deleted users';
$string['deleteexpireddatarequeststask'] = 'Delete files from completed data requests that have expired';
$string['deletemyaccount'] = 'Delete my account';
$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['deprecated'] = 'Deprecated';
$string['deprecatedexplanation'] = 'This plugin is using an old version of one of the privacy interfaces and should be updated.';
$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';
@@ -93,11 +112,14 @@ $string['dporolemapping_desc'] = 'The privacy officer can manage data requests.
$string['editcategories'] = 'Edit categories';
$string['editcategory'] = 'Edit category';
$string['editcategories'] = 'Edit categories';
$string['editdefaults'] = 'Edit defaults: {$a}';
$string['editmoduledefaults'] = 'Edit module defaults';
$string['editpurpose'] = 'Edit purpose';
$string['editpurposes'] = 'Edit purposes';
$string['effectiveretentionperiodcourse'] = '{$a} (after the course end date)';
$string['effectiveretentionperioduser'] = '{$a} (since the last time the user accessed the site)';
$string['emailsalutation'] = 'Dear {$a},';
$string['errorinvalidrequestcreationmethod'] = 'Invalid request creation method!';
$string['errorinvalidrequeststatus'] = 'Invalid request status!';
$string['errorinvalidrequesttype'] = 'Invalid request type!';
$string['errornocapabilitytorequestforothers'] = 'User {$a->requestedby} doesn\'t have the capability to make a data request on behalf of user {$a->userid}';
@@ -185,11 +207,13 @@ $string['nopurposes'] = 'There are no purposes yet';
$string['nosubjectaccessrequests'] = 'There are no data requests that you need to act on';
$string['nosystemdefaults'] = 'Site purpose and category have not yet been defined.';
$string['notset'] = 'Not set (use the default value)';
$string['overrideinstances'] = 'Reset instances with custom values';
$string['pluginregistry'] = 'Plugin privacy registry';
$string['pluginregistrytitle'] = 'Plugin privacy compliance registry';
$string['privacy'] = 'Privacy';
$string['privacyofficeronly'] = 'Only users who are assigned a privacy officer role ({$a}) have access to this content';
$string['privacy:metadata:preference:tool_dataprivacy_request-filters'] = 'The filters currently applied to the data requests page.';
$string['privacy:metadata:preference:tool_dataprivacy_request-perpage'] = 'The number of data requests the user prefers to see on one page';
$string['privacy:metadata:request'] = 'Information from personal data requests (subject access and deletion requests) made for this site.';
$string['privacy:metadata:request:comments'] = 'Any user comments accompanying the request.';
$string['privacy:metadata:request:userid'] = 'The ID of the user to whom the request belongs';
@@ -215,10 +239,14 @@ $string['requestby'] = 'Requested by';
$string['requestbydetail'] = 'Requested by:';
$string['requestcomments'] = 'Comments';
$string['requestcomments_help'] = 'This box enables you to enter any further details about your data request.';
$string['requestcreation'] = 'Creation';
$string['requestdenied'] = 'The request has been denied';
$string['requestemailintro'] = 'You have received a data request:';
$string['requestfor'] = 'Requesting for';
$string['requestmarkedcomplete'] = 'The request has been marked as complete';
$string['requestorigin'] = 'Request origin';
$string['requestsapproved'] = 'The requests have been approved';
$string['requestsdenied'] = 'The requests have been denied';
$string['requeststatus'] = 'Status';
$string['requestsubmitted'] = 'Your request has been submitted to the privacy officer';
$string['requesttype'] = 'Type';
@@ -230,6 +258,15 @@ $string['requesttypeexport'] = 'Export all of my personal data';
$string['requesttypeexportshort'] = 'Export';
$string['requesttypeothers'] = 'General inquiry';
$string['requesttypeothersshort'] = 'Message';
$string['requireallenddatesforuserdeletion'] = 'Consider courses without end date as active';
$string['requireallenddatesforuserdeletion_desc'] = 'When calculating user expiry, several factors are considered:
* the user\'s last login time is compared against the retention period for users; and
* whether the user is actively enrolled in any courses.
When checking the active enrolment of a corse, if the course has no end date then this setting is used to determine whether that course is considered active or not.
If the course has no end date, and this setting is enabled, then the user cannot be deleted.';
$string['requiresattention'] = 'Requires attention.';
$string['requiresattentionexplanation'] = 'This plugin does not implement the Moodle privacy API. If this plugin stores any personal data it will not be able to be exported or deleted through Moodle\'s privacy system.';
$string['resultdeleted'] = 'You recently requested to have your account and personal data in {$a} to be deleted. This process has been completed and you will no longer be able to log in.';
@@ -239,6 +276,9 @@ $string['retentionperiod'] = 'Retention period';
$string['retentionperiod_help'] = 'The retention period specifies the length of time that data should be kept for. When the retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
$string['retentionperiodnotdefined'] = 'No retention period was defined';
$string['retentionperiodzero'] = 'No retention period';
$string['selectbulkaction'] = 'Please select a bulk action.';
$string['selectdatarequests'] = 'Please select data requests.';
$string['selectuserdatarequest'] = 'Select {$a->username}\'s {$a->requesttype} data request.';
$string['send'] = 'Send';
$string['sensitivedatareasons'] = 'Sensitive personal data processing reasons';
$string['sensitivedatareasons_help'] = 'Select one or more applicable reasons that exempts the prohibition of processing sensitive personal data tied to this purpose. For more information, please see <a href="https://gdpr-info.eu/art-9-gdpr/" target="_blank">GDPR Art. 9.2</a>';
+22
View File
@@ -63,6 +63,28 @@ function tool_dataprivacy_myprofile_navigation(tree $tree, $user, $iscurrentuser
$node = new core_user\output\myprofile\node('privacyandpolicies', 'datarequests',
get_string('datarequests', 'tool_dataprivacy'), null, $url);
$category->add_node($node);
// Check if the user has an ongoing data export request.
$hasexportrequest = \tool_dataprivacy\api::has_ongoing_request($user->id, \tool_dataprivacy\api::DATAREQUEST_TYPE_EXPORT);
// Show data export link only if the user doesn't have an ongoing data export request.
if (!$hasexportrequest) {
$exportparams = ['type' => \tool_dataprivacy\api::DATAREQUEST_TYPE_EXPORT];
$exporturl = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php', $exportparams);
$exportnode = new core_user\output\myprofile\node('privacyandpolicies', 'requestdataexport',
get_string('requesttypeexport', 'tool_dataprivacy'), null, $exporturl);
$category->add_node($exportnode);
}
// Check if the user has an ongoing data deletion request.
$hasdeleterequest = \tool_dataprivacy\api::has_ongoing_request($user->id, \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE);
// Show data deletion link only if the user doesn't have an ongoing data deletion request.
if (!$hasdeleterequest) {
$deleteparams = ['type' => \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE];
$deleteurl = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php', $deleteparams);
$deletenode = new core_user\output\myprofile\node('privacyandpolicies', 'requestdatadeletion',
get_string('deletemyaccount', 'tool_dataprivacy'), null, $deleteurl);
$category->add_node($deletenode);
}
}
// Add the Privacy category to the tree if it's not empty and it doesn't exist.
+1 -1
View File
@@ -55,7 +55,7 @@ $PAGE->set_title($title);
echo $OUTPUT->header();
echo $OUTPUT->heading($title);
$requests = tool_dataprivacy\api::get_data_requests($USER->id, [], [], 'timecreated DESC');
$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);
+1
View File
@@ -33,6 +33,7 @@ $title = get_string('editpurposes', 'tool_dataprivacy');
$output = $PAGE->get_renderer('tool_dataprivacy');
echo $output->header();
echo $output->heading($title);
$purposes = \tool_dataprivacy\api::get_purposes();
$renderable = new \tool_dataprivacy\output\purposes($purposes);
+14
View File
@@ -34,6 +34,14 @@ if ($hassiteconfig) {
new lang_string('contactdataprotectionofficer_desc', 'tool_dataprivacy'), 0)
);
// Automatically create delete data request for users upon user deletion.
// Automatically create delete data request for pre-existing deleted users.
// Disabled by default.
$privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/automaticdeletionrequests',
new lang_string('automaticdeletionrequests', 'tool_dataprivacy'),
new lang_string('automaticdeletionrequests_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'),
@@ -60,6 +68,12 @@ if ($hassiteconfig) {
new lang_string('dporolemapping_desc', 'tool_dataprivacy'), null, $roles)
);
}
// When calculating user expiry, should courses which have no end date be considered.
$privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/requireallenddatesforuserdeletion',
new lang_string('requireallenddatesforuserdeletion', 'tool_dataprivacy'),
new lang_string('requireallenddatesforuserdeletion_desc', 'tool_dataprivacy'),
1));
}
}
+12
View File
@@ -32,3 +32,15 @@ dd a.contactdpo {
#page-admin-tool-dataprivacy-datarequests [data-region="data-requests-table"] .moodle-actionmenu {
min-width: 150px;
}
.context-level-view {
margin: 1em;
}
[data-region="purposes"] .col-name-description {
width: 25%;
}
[data-region="categories"] .col-description {
width: 50%;
}
@@ -51,18 +51,17 @@
{{/navigation}}
<div data-region="categories" class="m-t-3 m-b-1">
<h3>{{#str}}categories, tool_dataprivacy{{/str}}</h3>
<div class="m-y-1">
<button class="btn btn-secondary" data-add-element="category" title="{{#str}}addcategory, tool_dataprivacy{{/str}}">
{{#pix}}t/add, moodle, {{#str}}addcategory, tool_dataprivacy{{/str}}{{/pix}}
</button>
</div>
<table class="generaltable fullwidth">
<table class="table table-striped table-hover">
<caption class="accesshide">{{#str}}categorieslist, tool_dataprivacy{{/str}}</caption>
<thead>
<tr>
<th scope="col">{{#str}}name{{/str}}</th>
<th scope="col">{{#str}}description{{/str}}</th>
<th scope="col" class="col-description">{{#str}}description{{/str}}</th>
<th scope="col">{{#str}}actions{{/str}}</th>
</tr>
</thead>
@@ -0,0 +1,99 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template tool_dataprivacy/category_purpose_form
Manage data registry defaults.
Classes required for JS:
Data attributes required for JS:
Context variables required for this template:
* actionurl String The action URL.
* contextlevel Number - The context level.
* modemodule Boolean - Whether to display the activity select element.
* activityoptions Array - An array of objects for the activity select element.
* categoryoptions Array - An array of objects for the category select element.
* purposeoptions Array - An array of objects for the purpose select element.
Example context (json):
{
"actionurl": "#",
"contextlevel": 70,
"newactivitydefaults": true,
"modemodule": true,
"activityoptions": [
{ "name": "assign", "displayname": "Assignment" },
{ "name": "forum", "displayname": "Forum", "selected": true },
{ "name": "lesson", "displayname": "Lesson" },
{ "name": "quiz", "displayname": "Quiz" }
],
"categoryoptions": [
{ "id": 1, "name": "Category 1" },
{ "id": 2, "name": "Category 2", "selected": true },
{ "id": 3, "name": "Category 3" }
],
"purposeoptions": [
{ "id": 1, "name": "Purpose 1" },
{ "id": 2, "name": "Purpose 2" },
{ "id": 3, "name": "Purpose 3", "selected": true }
]
}
}}
<div class="alert alert-warning" role="alert">
{{#str}}defaultswarninginfo, tool_dataprivacy{{/str}}
</div>
<form method="post" action="{{actionurl}}" id="category_purpose_form">
<input type="hidden" value="{{contextlevel}}" id="contextlevel" />
{{#modemodule}}
<div class="form-group">
<label for="activity">{{#str}}activitymodule{{/str}}</label>
{{^newactivitydefaults}}
<input type="hidden" id="activity" value="{{#activityoptions}}{{#selected}}{{name}}{{/selected}}{{/activityoptions}}" />
{{/newactivitydefaults}}
<select class="form-control" {{#newactivitydefaults}}id="activity" {{/newactivitydefaults}}{{^newactivitydefaults}}disabled{{/newactivitydefaults}}>
{{#activityoptions}}
<option value="{{name}}" {{#selected}}selected{{/selected}}>{{displayname}}</option>
{{/activityoptions}}
</select>
</div>
{{/modemodule}}
<div class="form-group">
<label for="category">{{#str}}category, tool_dataprivacy{{/str}}</label>
<select class="form-control" id="category">
{{#categoryoptions}}
<option value="{{id}}" {{#selected}}selected{{/selected}}>{{name}}</option>
{{/categoryoptions}}
</select>
</div>
<div class="form-group">
<label for="purpose">{{#str}}purpose, tool_dataprivacy{{/str}}</label>
<select class="form-control" id="purpose">
{{#purposeoptions}}
<option value="{{id}}" {{#selected}}selected{{/selected}}>{{name}}</option>
{{/purposeoptions}}
</select>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="1" id="override">
<label class="form-check-label" for="override">
{{#str}}overrideinstances, tool_dataprivacy{{/str}}
</label>
</div>
</form>
@@ -61,6 +61,9 @@
{{#external}}
<span class="badge badge-pill badge-notice">{{#str}}external, tool_dataprivacy{{/str}}</span>
{{/external}}
{{#deprecated}}
<span class="badge badge-pill badge-warning">{{#str}}deprecated, tool_dataprivacy{{/str}}</span>
{{/deprecated}}
</div>
{{#compliant}}
@@ -45,6 +45,8 @@
<dd>{{#str}}requiresattentionexplanation, tool_dataprivacy{{/str}}</dd>
<dt><span class="badge badge-pill badge-notice">{{#str}}external, tool_dataprivacy{{/str}}</span></dt>
<dd>{{#str}}externalexplanation, tool_dataprivacy{{/str}}</dd>
<dt><span class="badge badge-pill badge-warning">{{#str}}deprecated, tool_dataprivacy{{/str}}</span></dt>
<dd>{{#str}}deprecatedexplanation, tool_dataprivacy{{/str}}</dd>
</dl>
<hr />
<div class="clearfix"><a class="tool_dataprivacy-expand-all pull-right" href="#" data-visibility-state='visible'>{{#str}}visible, tool_dataprivacy{{/str}}</a></div>
@@ -31,6 +31,8 @@
* string requestedby The one making the request.
* string requesttype The request type.
* string requestdate The date the request was made.
* string requestorigin The name of the site the request originates from.
* string requestoriginurl The homepage of the site the request originates from.
* string requestcomments Additional details regarding the request.
* bool forself Whether the request has been made on behalf of another user or not.
* string datarequestsurl The URL to the data requests page.
@@ -42,6 +44,8 @@
"requestedby": "Angus Zhang",
"requesttype": "Export user data",
"requestdate": "31 January 2018",
"requestorigin": "My Amazing Site",
"requestoriginurl": "https://www.bestmoodlesiteever.com",
"requestcomments": "Dear admin,<br/> I would like to request a copy of my son's user data. Thanks!",
"forself": true,
"datarequestsurl": "#"
@@ -90,6 +94,14 @@
</td>
</tr>
{{/forself}}
<tr>
<th scope="row">
{{#str}}requestorigin, tool_dataprivacy{{/str}}
</th>
<td>
<a href="{{requestoriginurl}}">{{{requestorigin}}}</a>
</td>
</tr>
<tr>
<th scope="row">
{{#str}}requestcomments, tool_dataprivacy{{/str}}
@@ -0,0 +1,59 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template tool_dataprivacy/data_requests_bulk_actions
Moodle template for the bulk action select element in the data requests page.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* options - Array of options for the select with value and name.
* perpage - HTML content of the records per page select element.
Example context (json):
{
"options": [
{
"value": 1,
"name": "Approve"
},
{
"value": 2,
"name": "Deny"
}
],
"perpage" : "<div class='singleselect'></div>"
}
}}
<div class="m-t-1 d-inline-block w-100">
<div class="pull-left">
<select id="bulk-action" class="select custom-select">
{{#options}}
<option value="{{ value }}">{{ name }}</option>
{{/options}}
</select>
<button class="btn btn-primary" id="confirm-bulk-action">{{# str}} confirm {{/ str}}</button>
</div>
<div class="pull-right">
{{{ perpage }}}
</div>
</div>
@@ -0,0 +1,74 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template tool_dataprivacy/defaults_display
Renders the current default category and purpose.
Classes required for JS:
Data attributes required for JS:
Context variables required for this template:
* contextlevel Number - The context level.
* categoryid Number - The category ID.
* purposeid Number - The purpose ID.
* category String - The category name.
* purpose String - The purpose name.
* canedit Boolean - Whether this is being rendered for editing purposes.
Example context (json):
{
"category": "Awesome default category",
"categoryid": 1,
"purpose": "Awesome default purpose",
"purposeid": 2,
"canedit": true,
"contextlevel": 70
}
}}
<div class="row-fluid rtl-compatible m-t-1 m-b-1">
<div class="col-md-9 span9">
<div class="row-fluid rtl-compatible m-t-1 m-b-1">
<div class="col-md-3 span3">
<strong>{{#str}}category, tool_dataprivacy{{/str}}</strong>
</div>
<div class="col-md-9 span9">
{{category}}
</div>
</div>
<div class="row-fluid rtl-compatible m-t-1 m-b-1">
<div class="col-md-3 span3">
<strong>{{#str}}purpose, tool_dataprivacy{{/str}}</strong>
</div>
<div class="col-md-9 span9">
{{purpose}}
</div>
</div>
</div>
<div class="col-md-3 span3 align-self-center">
{{#canedit}}
<button class="btn btn-default" {{!
}}data-action="edit-level-defaults" {{!
}}data-contextlevel="{{contextlevel}}" {{!
}}data-category="{{categoryid}}" {{!
}}data-purpose="{{purposeid}}">
{{#str}}edit{{/str}}
</button>
{{/canedit}}
</div>
</div>
@@ -0,0 +1,147 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template tool_dataprivacy/defaults_page
Manage data registry defaults.
Classes required for JS:
Data attributes required for JS:
Context variables required for this template:
* contextlevel Number - The context level.
* modecoursecat Boolean - Whether we're displaying defaults for course categories.
* modecourse Boolean - Whether we're displaying defaults for courses.
* modemodule Boolean - Whether we're displaying defaults for activity modules.
* modeblock Boolean - Whether we're displaying defaults for blocks.
* coursecaturl String - The URL for the course category defaults tab.
* courseurl String - The URL for the course defaults tab.
* moduleurl String - The URL for the activity module defaults tab.
* blockurl String - The URL for the block defaults tab.
* purposeid Number - The purpose ID for this context level.
* canedit Boolean - Whether this is being rendered for editing purposes.
* categoryid Number - The ID of the default category for this context level.
* purposeid Number - The ID of the default purpose for this context level.
* category String - The category name.
* purpose String - The purpose name.
* otherdefaults Array - An array containing the defaults for the activity modules.
Example context (json):
{
"contextlevel": 70,
"modecoursecat": false,
"modecourse": false,
"modemodule": true,
"modeblock": false,
"coursecaturl": "#",
"courseurl": "#",
"moduleurl": "#",
"blockurl": "#",
"category": "Awesome default category",
"purpose": "Awesome default purpose",
"canedit": true,
"otherdefaults": [
{
"name": "Assignment",
"category": "Category for activity modules",
"purpose": "Assessments"
},
{
"name": "Forum",
"category": "Category for activity modules",
"purpose": "Social interactions"
}
]
}
}}
<div class="card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link {{#modecoursecat}}active{{/modecoursecat}}" href="{{coursecaturl}}">{{#str}}categories{{/str}}</a>
</li>
<li class="nav-item">
<a class="nav-link {{#modecourse}}active{{/modecourse}}" href="{{courseurl}}">{{#str}}courses{{/str}}</a>
</li>
<li class="nav-item">
<a class="nav-link {{#modemodule}}active{{/modemodule}}" href="{{moduleurl}}">{{#str}}activitymodules{{/str}}</a>
</li>
<li class="nav-item">
<a class="nav-link {{#modeblock}}active{{/modeblock}}" href="{{blockurl}}">{{#str}}blocks{{/str}}</a>
</li>
</ul>
</div>
<div class="card-body context-level-view">
<div class="alert alert-primary" role="alert">
{{#str}}defaultsinfo, tool_dataprivacy{{/str}}
</div>
<h4 class="card-title" id="defaults-header">
{{#modecoursecat}}{{#str}}categories{{/str}}{{/modecoursecat}}
{{#modecourse}}{{#str}}courses{{/str}}{{/modecourse}}
{{#modemodule}}{{#str}}activitymodules{{/str}}{{/modemodule}}
{{#modeblock}}{{#str}}blocks{{/str}}{{/modeblock}}
</h4>
<div>
{{> tool_dataprivacy/defaults_display}}
{{#canedit}}
{{#modemodule}}
<button class="btn btn-primary" data-action="new-activity-defaults" data-contextlevel="{{contextlevel}}">
{{#str}}addnewdefaults, tool_dataprivacy{{/str}}
</button>
{{/modemodule}}
{{/canedit}}
{{#modemodule}}
<table class="m-t-1 table table-striped">
<thead>
<tr>
<th>&nbsp;</th>
<th scope="col">{{#str}}category, tool_dataprivacy{{/str}}</th>
<th scope="col">{{#str}}purpose, tool_dataprivacy{{/str}}</th>
{{#canedit}}
<th scope="col">{{#str}}actions{{/str}}</th>
{{/canedit}}
</tr>
</thead>
<tbody>
{{#otherdefaults}}
<tr>
<th scope="row">{{name}}</th>
<td>{{category}}</td>
<td>{{purpose}}</td>
{{#canedit}}
<td>
{{#actions}}
{{> core/action_menu_link}}
{{/actions}}
</td>
{{/canedit}}
</tr>
{{/otherdefaults}}
</tbody>
</table>
{{/modemodule}}
</div>
</div>
</div>
{{#js}}
// Initialise the JS.
require(['tool_dataprivacy/defaultsactions'], function(ActionsMod) {
ActionsMod.init();
});
{{/js}}
@@ -0,0 +1,41 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template tool_dataprivacy/delete_activity_defaults
Renders the confirmation.
Classes required for JS:
Data attributes required for JS:
* none
Context variables required for this template:
* activityname String - The activity name.
Example context (json):
{
"activityname": "Assignment"
}
}}
<div class="alert alert-warning" role="alert">
{{#str}}defaultswarninginfo, tool_dataprivacy{{/str}}
</div>
<div>
{{#str}}deletedefaultsconfirmation, tool_dataprivacy, {{activityname}}{{/str}}
</div>
@@ -58,32 +58,38 @@
{{/navigation}}
<div data-region="purposes" class="m-t-3 m-b-1">
<h3>{{#str}}purposes, tool_dataprivacy{{/str}}</h3>
<div class="m-y-1">
<button class="btn btn-secondary" data-add-element="purpose" title="{{#str}}addpurpose, tool_dataprivacy{{/str}}">
{{#pix}}t/add, moodle, {{#str}}addpurpose, tool_dataprivacy{{/str}}{{/pix}}
</button>
</div>
<table class="generaltable fullwidth">
<table class="table table-striped table-hover">
<caption class="accesshide">{{#str}}purposeslist, tool_dataprivacy{{/str}}</caption>
<thead>
<tr style="display: flex;">
<th scope="col" class="col-md-2">{{#str}}name{{/str}}</th>
<th scope="col" class="col-md-3">{{#str}}description{{/str}}</th>
<th scope="col" class="col-md-2">{{#str}}lawfulbases, tool_dataprivacy{{/str}}</th>
<th scope="col" class="col-md-2">{{#str}}sensitivedatareasons, tool_dataprivacy{{/str}}</th>
<th scope="col" class="col-md-1">{{#str}}retentionperiod, tool_dataprivacy{{/str}}</th>
<th scope="col" class="col-md-1">{{#str}}protected, tool_dataprivacy{{/str}}</th>
<th scope="col" class="col-md-1">{{#str}}actions{{/str}}</th>
<tr>
<th scope="col" class="col-name-description">{{#str}}name{{/str}}</th>
<th scope="col">{{#str}}lawfulbases, tool_dataprivacy{{/str}}</th>
<th scope="col">{{#str}}sensitivedatareasons, tool_dataprivacy{{/str}}</th>
<th scope="col">{{#str}}retentionperiod, tool_dataprivacy{{/str}}</th>
<th scope="col">{{#str}}protected, tool_dataprivacy{{/str}}</th>
<th scope="col">{{#str}}actions{{/str}}</th>
</tr>
</thead>
<tbody>
{{#purposes}}
<tr data-purposeid="{{id}}">
<td class="col-md-2">{{{name}}}</td>
<td class="col-md-3">{{{description}}}</td>
<td class="col-md-2">
<ul>
<td>
<dl>
<dt>
{{{name}}}
</dt>
<dd>
{{{description}}}
</dd>
</dl>
</td>
<td>
<ul class="list-unstyled">
{{#formattedlawfulbases}}
<li>
<span>{{name}}{{# pix }} i/info, core, {{description}} {{/ pix }}</span>
@@ -91,8 +97,8 @@
{{/formattedlawfulbases}}
</ul>
</td>
<td class="col-md-2">
<ul>
<td>
<ul class="list-unstyled">
{{#formattedsensitivedatareasons}}
<li>
<span>{{name}}{{# pix }} i/info, core, {{description}} {{/ pix }}</span>
@@ -100,8 +106,8 @@
{{/formattedsensitivedatareasons}}
</ul>
</td>
<td class="col-md-1">{{formattedretentionperiod}}</td>
<td class="col-md-1">
<td>{{formattedretentionperiod}}</td>
<td>
{{#protected}}
{{#pix}}i/checked, core, {{#str}}yes{{/str}}{{/pix}}
{{/protected}}
@@ -109,7 +115,7 @@
{{#str}}no{{/str}}
{{/protected}}
</td>
<td class="col-md-1">
<td>
{{#actions}}
{{> core/action_menu}}
{{/actions}}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,284 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Step definitions to generate database fixtures for the data privacy tool.
*
* @package tool_dataprivacy
* @category test
* @copyright 2018 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
use Behat\Gherkin\Node\TableNode as TableNode;
use Behat\Behat\Tester\Exception\PendingException as PendingException;
use tool_dataprivacy\api;
/**
* Step definitions to generate database fixtures for the data privacy tool.
*
* @package tool_dataprivacy
* @category test
* @copyright 2018 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_tool_dataprivacy extends behat_base {
/**
* Each element specifies:
* - The data generator suffix used.
* - The required fields.
* - The mapping between other elements references and database field names.
* @var array
*/
protected static $elements = array(
'categories' => array(
'datagenerator' => 'category',
'required' => array()
),
'purposes' => array(
'datagenerator' => 'purpose',
'required' => array()
),
);
/**
* Creates the specified element. More info about available elements in http://docs.moodle.org/dev/Acceptance_testing#Fixtures.
*
* @Given /^the following data privacy "(?P<element_string>(?:[^"]|\\")*)" exist:$/
*
* @param string $elementname The name of the entity to add
* @param TableNode $data
*/
public function the_following_data_categories_exist($elementname, TableNode $data) {
// Now that we need them require the data generators.
require_once(__DIR__.'/../../../../../lib/phpunit/classes/util.php');
if (empty(self::$elements[$elementname])) {
throw new PendingException($elementname . ' data generator is not implemented');
}
$datagenerator = testing_util::get_data_generator();
$dataprivacygenerator = $datagenerator->get_plugin_generator('tool_dataprivacy');
$elementdatagenerator = self::$elements[$elementname]['datagenerator'];
$requiredfields = self::$elements[$elementname]['required'];
if (!empty(self::$elements[$elementname]['switchids'])) {
$switchids = self::$elements[$elementname]['switchids'];
}
foreach ($data->getHash() as $elementdata) {
// Check if all the required fields are there.
foreach ($requiredfields as $requiredfield) {
if (!isset($elementdata[$requiredfield])) {
throw new Exception($elementname . ' requires the field ' . $requiredfield . ' to be specified');
}
}
// Switch from human-friendly references to ids.
if (isset($switchids)) {
foreach ($switchids as $element => $field) {
$methodname = 'get_' . $element . '_id';
// Not all the switch fields are required, default vars will be assigned by data generators.
if (isset($elementdata[$element])) {
// Temp $id var to avoid problems when $element == $field.
$id = $this->{$methodname}($elementdata[$element]);
unset($elementdata[$element]);
$elementdata[$field] = $id;
}
}
}
// Preprocess the entities that requires a special treatment.
if (method_exists($this, 'preprocess_' . $elementdatagenerator)) {
$elementdata = $this->{'preprocess_' . $elementdatagenerator}($elementdata);
}
// Creates element.
$methodname = 'create_' . $elementdatagenerator;
if (method_exists($dataprivacygenerator, $methodname)) {
// Using data generators directly.
$dataprivacygenerator->{$methodname}($elementdata);
} else if (method_exists($this, 'process_' . $elementdatagenerator)) {
// Using an alternative to the direct data generator call.
$this->{'process_' . $elementdatagenerator}($elementdata);
} else {
throw new PendingException($elementname . ' data generator is not implemented');
}
}
}
/**
* Sets the data category and data storage purpose for the site.
*
* @Given /^I set the site category and purpose to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
*
* @param string $category The ID of the category to be set for the instance.
* @param string $purpose The ID of the purpose to be set for the instance.
*/
public function i_set_the_site_category_and_purpose($category, $purpose) {
$category = \tool_dataprivacy\category::get_record(['name' => $category]);
$purpose = \tool_dataprivacy\purpose::get_record(['name' => $purpose]);
$data = (object)[
'contextlevel' => CONTEXT_SYSTEM,
'categoryid' => $category->get('id'),
'purposeid' => $purpose->get('id'),
];
api::set_contextlevel($data);
}
/**
* Sets the data category and data storage purpose for a course category instance.
*
* @Given /^I set the category and purpose for the course category "(?P<categoryname_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
*
* @param string $name The instance name. It should match the name or the idnumber.
* @param string $category The ID of the category to be set for the instance.
* @param string $purpose The ID of the purpose to be set for the instance.
*/
public function i_set_the_category_and_purpose_for_course_category($name, $category, $purpose) {
global $DB;
$params = [
'name' => $name,
'idnumber' => $name,
];
$select = 'name = :name OR idnumber = :idnumber';
$coursecatid = $DB->get_field_select('course_categories', 'id', $select, $params, MUST_EXIST);
$context = context_coursecat::instance($coursecatid);
$this->set_category_and_purpose($context->id, $category, $purpose);
}
/**
* Sets the data category and data storage purpose for a course instance.
*
* @Given /^I set the category and purpose for the course "(?P<coursename_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
*
* @param string $name The instance name. It should match the fullname or the shortname, or the idnumber.
* @param string $category The ID of the category to be set for the instance.
* @param string $purpose The ID of the purpose to be set for the instance.
*/
public function i_set_the_category_and_purpose_for_course($name, $category, $purpose) {
global $DB;
$params = [
'shortname' => $name,
'fullname' => $name,
'idnumber' => $name,
];
$select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
$courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
$context = context_course::instance($courseid);
$this->set_category_and_purpose($context->id, $category, $purpose);
}
/**
* Sets the data category and data storage purpose for a course instance.
*
* @Given /^I set the category and purpose for the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" in course "(?P<coursename_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
*
* @param string $name The instance name. It should match the name of the activity.
* @param string $type The activity type. E.g. assign, quiz, forum, etc.
* @param string $coursename The course name. It should match the fullname or the shortname, or the idnumber.
* @param string $category The ID of the category to be set for the instance.
* @param string $purpose The ID of the purpose to be set for the instance.
*/
public function i_set_the_category_and_purpose_for_activity($name, $type, $coursename, $category, $purpose) {
global $DB;
$params = [
'shortname' => $coursename,
'fullname' => $coursename,
'idnumber' => $coursename,
];
$select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
$courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
$cmid = null;
$cms = get_coursemodules_in_course($type, $courseid);
foreach ($cms as $cm) {
if ($cm->name === $name || $cm->idnumber === $name) {
$cmid = $cm->id;
break;
}
}
if ($cmid === null) {
throw new coding_exception("Activity module '{$name}' of type '{$type}' not found!");
}
$context = context_module::instance($cmid);
$this->set_category_and_purpose($context->id, $category, $purpose);
}
/**
* Sets the data category and data storage purpose for a course instance.
*
* @Given /^I set the category and purpose for the "(?P<blockname_string>(?:[^"]|\\")*)" block in the "(?P<coursename_string>(?:[^"]|\\")*)" course to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
*
* @param string $name The instance name. It should match the name of the block. (e.g. online_users)
* @param string $coursename The course name. It should match the fullname or the shortname, or the idnumber.
* @param string $category The ID of the category to be set for the instance.
* @param string $purpose The ID of the purpose to be set for the instance.
*/
public function i_set_the_category_and_purpose_for_block($name, $coursename, $category, $purpose) {
global $DB;
$params = [
'shortname' => $coursename,
'fullname' => $coursename,
'idnumber' => $coursename,
];
$select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
$courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
// Fetch the course context.
$coursecontext = context_course::instance($courseid);
// Fetch the block record and context.
$blockid = $DB->get_field('block_instances', 'id', ['blockname' => $name, 'parentcontextid' => $coursecontext->id]);
$context = context_block::instance($blockid);
// Set the category and purpose.
$this->set_category_and_purpose($context->id, $category, $purpose);
}
/**
* Sets the category and purpose for a context instance.
*
* @param int $contextid The context ID.
* @param int $categoryname The category name.
* @param int $purposename The purpose name.
* @throws coding_exception
*/
protected function set_category_and_purpose($contextid, $categoryname, $purposename) {
$category = \tool_dataprivacy\category::get_record(['name' => $categoryname]);
$purpose = \tool_dataprivacy\purpose::get_record(['name' => $purposename]);
api::set_context_instance((object) [
'contextid' => $contextid,
'purposeid' => $purpose->get('id'),
'categoryid' => $category->get('id'),
]);
}
}
@@ -56,3 +56,93 @@ Feature: Manage data requests
And I click on "Actions" "link" in the "Jane Doe" "table_row"
And I should see "View the request"
But I should not see "Mark as complete"
@javascript
Scenario: Bulk accepting requests
Given I log in as "student1"
And I follow "Profile" in the user menu
And I should see "Data requests"
And I click on "Data requests" "link"
And I should see "New request"
And I click on "New request" "link"
And I should see "Type"
And I should see "Comments"
And I set the field "Type" to "Export all of my personal data"
And I set the field "Comments" to "Comment1"
And I press "Save changes"
And I should see "Your request has been submitted to the privacy officer"
And I log out
And I log in as "student2"
And I follow "Profile" in the user menu
And I should see "Data requests"
And I click on "Data requests" "link"
And I should see "New request"
And I click on "New request" "link"
And I should see "Type"
And I should see "Comments"
And I set the field "Type" to "Export all of my personal data"
And I set the field "Comments" to "Comment2"
And I press "Save changes"
And I should see "Your request has been submitted to the privacy officer"
And I log out
And I trigger cron
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I should see "Comment1" in the "John Doe" "table_row"
And I should see "Awaiting approval" in the "John Doe" "table_row"
And I should see "Comment2" in the "Jane Doe" "table_row"
And I should see "Awaiting approval" in the "Jane Doe" "table_row"
And I click on ".selectrequests" "css_element" in the "John Doe" "table_row"
And I click on ".selectrequests" "css_element" in the "Jane Doe" "table_row"
And I set the field with xpath "//select[@id='bulk-action']" to "Approve"
And I press "Confirm"
And I should see "Approve requests"
And I should see "Do you really want to bulk approve the selected data requests?"
When I press "Approve requests"
Then I should see "Approved" in the "John Doe" "table_row"
And I should see "Approved" in the "Jane Doe" "table_row"
@javascript
Scenario: Bulk denying requests
Given I log in as "student1"
And I follow "Profile" in the user menu
And I should see "Data requests"
And I click on "Data requests" "link"
And I should see "New request"
And I click on "New request" "link"
And I should see "Type"
And I should see "Comments"
And I set the field "Type" to "Export all of my personal data"
And I set the field "Comments" to "Comment1"
And I press "Save changes"
And I should see "Your request has been submitted to the privacy officer"
And I log out
And I log in as "student2"
And I follow "Profile" in the user menu
And I should see "Data requests"
And I click on "Data requests" "link"
And I should see "New request"
And I click on "New request" "link"
And I should see "Type"
And I should see "Comments"
And I set the field "Type" to "Export all of my personal data"
And I set the field "Comments" to "Comment2"
And I press "Save changes"
And I should see "Your request has been submitted to the privacy officer"
And I log out
And I trigger cron
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I should see "Comment1" in the "John Doe" "table_row"
And I should see "Awaiting approval" in the "John Doe" "table_row"
And I should see "Comment2" in the "Jane Doe" "table_row"
And I should see "Awaiting approval" in the "Jane Doe" "table_row"
And I click on ".selectrequests" "css_element" in the "John Doe" "table_row"
And I click on ".selectrequests" "css_element" in the "Jane Doe" "table_row"
And I set the field with xpath "//select[@id='bulk-action']" to "Deny"
And I press "Confirm"
And I should see "Deny requests"
And I should see "Do you really want to bulk deny the selected data requests?"
When I press "Deny requests"
Then I should see "Rejected" in the "John Doe" "table_row"
And I should see "Rejected" in the "Jane Doe" "table_row"
@@ -0,0 +1,373 @@
@tool @tool_dataprivacy @javascript
Feature: Manage data registry defaults
As the privacy officer
In order to manage the data registry
I need to be able to manage the default data categories and data storage purposes for various context levels.
Background:
Given I log in as "admin"
And the following "categories" exist:
| name | idnumber | category |
| Science and technology | scitech | |
| Physics | st-phys | scitech |
And the following "courses" exist:
| fullname | shortname | category |
| Fundamentals of physics 1 | Physics 101 | st-phys |
And the following "activities" exist:
| activity | name | idnumber | course |
| assign | Assignment 1 | assign1 | Physics 101 |
| forum | Forum 1 | forum1 | Physics 101 |
And the following "blocks" exist:
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
| online_users | Course | Physics 101 | course-view-* | site-post |
And the following data privacy "categories" exist:
| name |
| Site category |
| Category 1 |
| Category 2 |
And the following data privacy "purposes" exist:
| name | retentionperiod |
| Site purpose | P10Y |
| Purpose 1 | P3Y |
| Purpose 2 | P5Y |
And I set the site category and purpose to "Site category" and "Site purpose"
# Setting a default for course categories should apply to everything beneath that category.
Scenario: Set course category data registry defaults
Given I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I should see "Inherit"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
When I press "Save changes"
Then I should see "Category 1"
And I should see "Purpose 1"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Not set (use the default value)"
And the field "purposeid" matches value "Not set (use the default value)"
And I should see "3 years"
And I click on "Courses" "link"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And I should see "3 years"
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I should see "3 years"
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And I should see "3 years"
# When Setting a default for course categories, and overriding a specific category, only that category and its
# children will be overridden.
# If any child is a course category, it will get the default.
Scenario: Set course category data registry defaults with override
Given I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
And I press "Save changes"
And I should see "Category 1"
And I should see "Purpose 1"
And I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
When I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
Then the field "categoryid" matches value "Category 2"
And the field "purposeid" matches value "Purpose 2"
And I should see "5 years"
And I click on "Courses" "link"
And I wait until the page is ready
# Physics 101 is also a category, so it will get the category default.
And I click on "Physics 101" "link"
And I wait until the page is ready
And I should see "3 years"
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I should see "3 years"
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And I should see "3 years"
# When overriding a specific category, only that category and its children will be overridden.
Scenario: Set course category data registry defaults with override
Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
When I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
Then the field "categoryid" matches value "Category 2"
And the field "purposeid" matches value "Purpose 2"
And I should see "5 years"
And I click on "Courses" "link"
And I wait until the page is ready
# Physics 101 is also a category, so it will get the category default.
And I click on "Physics 101" "link"
And I wait until the page is ready
And I should see "5 years"
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I should see "5 years"
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And I should see "5 years"
# Resetting instances removes custom values.
Scenario: Set course category data registry defaults with override
Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
When I click on "Reset instances with custom values" "checkbox"
And I press "Save changes"
And I should see "Category 1"
And I should see "Purpose 1"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
Then the field "categoryid" matches value "Not set (use the default value)"
And the field "purposeid" matches value "Not set (use the default value)"
And I should see "3 years"
Scenario: Set course data registry defaults
Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I click on "Courses" "link"
And I should see "Inherit"
And I should not see "Add a new module default"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
When I press "Save changes"
Then I should see "Category 1"
And I should see "Purpose 1"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And I click on "Courses" "link"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Category 2"
And the field "purposeid" matches value "Purpose 2"
And I should see "5 years (after the course end date)"
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I should see "5 years"
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And I should see "5 years"
Scenario: Set course data registry defaults with override
Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I click on "Courses" "link"
And I should see "Inherit"
And I should not see "Add a new module default"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
And I click on "Reset instances with custom values" "checkbox"
When I press "Save changes"
Then I should see "Category 1"
And I should see "Purpose 1"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And I click on "Courses" "link"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Not set (use the default value)"
And the field "purposeid" matches value "Not set (use the default value)"
And I should see "3 years (after the course end date)"
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I should see "3 years"
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And I should see "3 years"
Scenario: Set module level data registry defaults
Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I click on "Activity modules" "link"
And I should see "Inherit"
And I should see "Add a new module default"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
When I press "Save changes"
Then I should see "Category 1"
And I should see "Purpose 1"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And I click on "Courses" "link"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Category 2"
And the field "purposeid" matches value "Purpose 2"
And I should see "5 years (after the course end date)"
Scenario: Set module level data registry defaults with override
Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I click on "Activity modules" "link"
And I should see "Inherit"
And I should see "Add a new module default"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
And I click on "Reset instances with custom values" "checkbox"
When I press "Save changes"
Then I should see "Category 1"
And I should see "Purpose 1"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And I click on "Courses" "link"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Not set (use the default value)"
And the field "purposeid" matches value "Not set (use the default value)"
And I click on "Forum 1 (Forum)" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Not set (use the default value)"
And the field "purposeid" matches value "Not set (use the default value)"
And I should see "3 years (after the course end date)"
Scenario: Set data registry defaults for an activity module
Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I click on "Activity modules" "link"
And I should see "Inherit"
And I should see "Add a new module default"
And I press "Add a new module default"
And I set the field "Activity module" to "Assignment"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
When I press "Save changes"
Then I should see "Category 1" in the "Assignment" "table_row"
And I should see "Purpose 1" in the "Assignment" "table_row"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And I click on "Courses" "link"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Category 2"
And the field "purposeid" matches value "Purpose 2"
And I should see "5 years (after the course end date)"
Scenario: Set data registry defaults for an activity module with override
Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I click on "Activity modules" "link"
And I should see "Inherit"
And I should see "Add a new module default"
And I press "Add a new module default"
And I set the field "Activity module" to "Assignment"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
And I click on "Reset instances with custom values" "checkbox"
When I press "Save changes"
Then I should see "Category 1" in the "Assignment" "table_row"
And I should see "Purpose 1" in the "Assignment" "table_row"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And I click on "Courses" "link"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Not set (use the default value)"
And the field "purposeid" matches value "Not set (use the default value)"
And I should see "3 years (after the course end date)"
Scenario: Set block category data registry defaults
Given I set the category and purpose for the "online_users" block in the "Physics 101" course to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I click on "Blocks" "link"
And I should see "Inherit"
And I should not see "Add a new module default"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
When I press "Save changes"
Then I should see "Category 1"
And I should see "Purpose 1"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And I click on "Courses" "link"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And I click on "Blocks" "link"
And I wait until the page is ready
And I click on "Online users" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Category 2"
And the field "purposeid" matches value "Purpose 2"
And I should see "5 years (after the course end date)"
Scenario: Set course category data registry defaults with override
Given I set the category and purpose for the "online_users" block in the "Physics 101" course to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I click on "Blocks" "link"
And I should see "Inherit"
And I should not see "Add a new module default"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
And I click on "Reset instances with custom values" "checkbox"
When I press "Save changes"
Then I should see "Category 1"
And I should see "Purpose 1"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And I click on "Courses" "link"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And I click on "Blocks" "link"
And I wait until the page is ready
And I click on "Online users" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Not set (use the default value)"
And the field "purposeid" matches value "Not set (use the default value)"
And I should see "3 years (after the course end date)"
@@ -0,0 +1,57 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Unit tests for the data_registry class.
*
* @package tool_dataprivacy
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use \tool_dataprivacy\data_registry;
/**
* Unit tests for the data_registry class.
*
* @package tool_dataprivacy
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_dataprivacy_dataregistry_testcase extends advanced_testcase {
/**
* Ensure that the get_effective_context_value only errors if provided an inappropriate element.
*
* This test is not great because we only test a limited set of values. This is a fault of the underlying API.
*/
public function test_get_effective_context_value_invalid_element() {
$this->expectException(coding_exception::class);
data_registry::get_effective_context_value(\context_system::instance(), 'invalid');
}
/**
* Ensure that the get_effective_contextlevel_value only errors if provided an inappropriate element.
*
* This test is not great because we only test a limited set of values. This is a fault of the underlying API.
*/
public function test_get_effective_contextlevel_value_invalid_element() {
$this->expectException(coding_exception::class);
data_registry::get_effective_contextlevel_value(\context_system::instance(), 'invalid');
}
}
File diff suppressed because it is too large Load Diff
+752 -66
View File
@@ -28,6 +28,7 @@ global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
use tool_dataprivacy\api;
use tool_dataprivacy\context_instance;
use tool_dataprivacy\external;
/**
@@ -39,71 +40,85 @@ use tool_dataprivacy\external;
*/
class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
/** @var stdClass The user making the request. */
protected $requester;
/** @var int The data request ID. */
protected $requestid;
/**
* Setup function- we will create a course and add an assign instance to it.
*/
protected function setUp() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
// Login as user.
$this->setUser($requester->id);
// Test data request creation.
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
$this->requestid = $datarequest->get('id');
$this->requester = $requester;
// Log out the user and set force login to true.
$this->setUser();
}
/**
* Test for external::approve_data_request() with the user not logged in.
*/
public function test_approve_data_request_not_logged_in() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
// Test data request creation.
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Log out the user and set force login to true.
$this->setUser();
$this->expectException(require_login_exception::class);
external::approve_data_request($this->requestid);
external::approve_data_request($datarequest->get('id'));
}
/**
* Test for external::approve_data_request() with the user not having a DPO role.
*/
public function test_approve_data_request_not_dpo() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
// Test data request creation.
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Login as the requester.
$this->setUser($this->requester->id);
$this->setUser($requester);
$this->expectException(required_capability_exception::class);
external::approve_data_request($this->requestid);
external::approve_data_request($datarequest->get('id'));
}
/**
* Test for external::approve_data_request() for request that's not ready for approval
*/
public function test_approve_data_request_not_waiting_for_approval() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
// Test data request creation.
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
$this->setAdminUser();
$this->expectException(moodle_exception::class);
external::approve_data_request($this->requestid);
external::approve_data_request($datarequest->get('id'));
}
/**
* Test for external::approve_data_request()
*/
public function test_approve_data_request() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
// Test data request creation.
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
$this->setAdminUser();
api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::approve_data_request($this->requestid);
api::update_request_status($datarequest->get('id'), api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::approve_data_request($datarequest->get('id'));
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
$this->assertTrue($return->result);
$this->assertEmpty($return->warnings);
@@ -113,10 +128,13 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test for external::approve_data_request() for a non-existent request ID.
*/
public function test_approve_data_request_non_existent() {
$this->resetAfterTest();
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
$this->setAdminUser();
api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::approve_data_request($this->requestid + 1);
$result = external::approve_data_request(1);
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
$this->assertFalse($return->result);
$this->assertCount(1, $return->warnings);
@@ -128,13 +146,21 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test for external::cancel_data_request() of another user.
*/
public function test_cancel_data_request_other_user() {
$generator = $this->getDataGenerator();
$otheruser = $generator->create_user();
$this->resetAfterTest();
// Login as another user.
$generator = new testing_data_generator();
$requester = $generator->create_user();
$otheruser = $generator->create_user();
$comment = 'sample comment';
// Test data request creation.
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Login as other user.
$this->setUser($otheruser);
$result = external::cancel_data_request($this->requestid);
$result = external::cancel_data_request($datarequest->get('id'));
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
$this->assertFalse($return->result);
$this->assertCount(1, $return->warnings);
@@ -142,14 +168,107 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
$this->assertEquals('errorrequestnotfound', $warning['warningcode']);
}
/**
* Test cancellation of a request where you are the requester of another user's data.
*/
public function test_cancel_data_request_other_user_as_requester() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$otheruser = $generator->create_user();
$comment = 'sample comment';
// Assign requester as otheruser'sparent.
$systemcontext = \context_system::instance();
$parentrole = $generator->create_role();
assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
role_assign($parentrole, $requester->id, \context_user::instance($otheruser->id));
// Test data request creation.
$this->setUser($requester);
$datarequest = api::create_data_request($otheruser->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
$result = external::cancel_data_request($datarequest->get('id'));
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
$this->assertTrue($return->result);
$this->assertEmpty($return->warnings);
}
/**
* Test cancellation of a request where you are the requester of another user's data.
*/
public function test_cancel_data_request_requester_lost_permissions() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$otheruser = $generator->create_user();
$comment = 'sample comment';
// Assign requester as otheruser'sparent.
$systemcontext = \context_system::instance();
$parentrole = $generator->create_role();
assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
role_assign($parentrole, $requester->id, \context_user::instance($otheruser->id));
$this->setUser($requester);
$datarequest = api::create_data_request($otheruser->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Unassign the role.
role_unassign($parentrole, $requester->id, \context_user::instance($otheruser->id)->id);
// This user can no longer make the request.
$this->expectException(required_capability_exception::class);
$result = external::cancel_data_request($datarequest->get('id'));
}
/**
* Test cancellation of a request where you are the requester of another user's data.
*/
public function test_cancel_data_request_other_user_as_child() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$otheruser = $generator->create_user();
$comment = 'sample comment';
// Assign requester as otheruser'sparent.
$systemcontext = \context_system::instance();
$parentrole = $generator->create_role();
assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
role_assign($parentrole, $requester->id, \context_user::instance($otheruser->id));
// Test data request creation.
$this->setUser($otheruser);
$datarequest = api::create_data_request($otheruser->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
$result = external::cancel_data_request($datarequest->get('id'));
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
$this->assertTrue($return->result);
$this->assertEmpty($return->warnings);
}
/**
* Test for external::cancel_data_request()
*/
public function test_cancel_data_request() {
// Login as the requester.
$this->setUser($this->requester);
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
// Test data request creation.
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Test cancellation.
$this->setUser($requester);
$result = external::cancel_data_request($datarequest->get('id'));
$result = external::cancel_data_request($this->requestid);
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
$this->assertTrue($return->result);
$this->assertEmpty($return->warnings);
@@ -159,10 +278,12 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test contact DPO.
*/
public function test_contact_dpo() {
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$this->setUser($user);
$this->resetAfterTest();
$generator = new testing_data_generator();
$user = $generator->create_user();
$this->setUser($user);
$message = 'Hello world!';
$result = external::contact_dpo($message);
$return = (object) external_api::clean_returnvalue(external::contact_dpo_returns(), $result);
@@ -174,10 +295,12 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test contact DPO with message containing invalid input.
*/
public function test_contact_dpo_with_nasty_input() {
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$this->setUser($user);
$this->resetAfterTest();
$generator = new testing_data_generator();
$user = $generator->create_user();
$this->setUser($user);
$this->expectException('invalid_parameter_exception');
external::contact_dpo('de<>\\..scription');
}
@@ -186,38 +309,77 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test for external::deny_data_request() with the user not logged in.
*/
public function test_deny_data_request_not_logged_in() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
// Test data request creation.
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Log out.
$this->setUser();
$this->expectException(require_login_exception::class);
external::deny_data_request($this->requestid);
external::deny_data_request($datarequest->get('id'));
}
/**
* Test for external::deny_data_request() with the user not having a DPO role.
*/
public function test_deny_data_request_not_dpo() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Login as the requester.
$this->setUser($this->requester->id);
$this->setUser($requester);
$this->expectException(required_capability_exception::class);
external::deny_data_request($this->requestid);
external::deny_data_request($datarequest->get('id'));
}
/**
* Test for external::deny_data_request() for request that's not ready for approval
*/
public function test_deny_data_request_not_waiting_for_approval() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
$this->setAdminUser();
$this->expectException(moodle_exception::class);
external::deny_data_request($this->requestid);
external::deny_data_request($datarequest->get('id'));
}
/**
* Test for external::deny_data_request()
*/
public function test_deny_data_request() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
$this->setAdminUser();
api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::approve_data_request($this->requestid);
api::update_request_status($datarequest->get('id'), api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::approve_data_request($datarequest->get('id'));
$return = (object) external_api::clean_returnvalue(external::deny_data_request_returns(), $result);
$this->assertTrue($return->result);
$this->assertEmpty($return->warnings);
@@ -227,10 +389,12 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test for external::deny_data_request() for a non-existent request ID.
*/
public function test_deny_data_request_non_existent() {
$this->resetAfterTest();
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
$this->setAdminUser();
api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::deny_data_request($this->requestid + 1);
$result = external::deny_data_request(1);
$return = (object) external_api::clean_returnvalue(external::deny_data_request_returns(), $result);
$this->assertFalse($return->result);
$this->assertCount(1, $return->warnings);
@@ -242,34 +406,62 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test for external::get_data_request() with the user not logged in.
*/
public function test_get_data_request_not_logged_in() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
$this->setUser();
$this->expectException(require_login_exception::class);
external::get_data_request($this->requestid);
external::get_data_request($datarequest->get('id'));
}
/**
* Test for external::get_data_request() with the user not having a DPO role.
*/
public function test_get_data_request_not_dpo() {
$generator = $this->getDataGenerator();
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$otheruser = $generator->create_user();
// Login as the requester.
$comment = 'sample comment';
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Login as the otheruser.
$this->setUser($otheruser);
$this->expectException(required_capability_exception::class);
external::get_data_request($this->requestid);
external::get_data_request($datarequest->get('id'));
}
/**
* Test for external::get_data_request()
*/
public function test_get_data_request() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
$this->setAdminUser();
$result = external::get_data_request($this->requestid);
$result = external::get_data_request($datarequest->get('id'));
$return = (object) external_api::clean_returnvalue(external::get_data_request_returns(), $result);
$this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $return->result['type']);
$this->assertEquals('sample comment', $return->result['comments']);
$this->assertEquals($this->requester->id, $return->result['userid']);
$this->assertEquals($this->requester->id, $return->result['requestedby']);
$this->assertEquals($requester->id, $return->result['userid']);
$this->assertEquals($requester->id, $return->result['requestedby']);
$this->assertEmpty($return->warnings);
}
@@ -277,9 +469,503 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test for external::get_data_request() for a non-existent request ID.
*/
public function test_get_data_request_non_existent() {
$this->resetAfterTest();
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
$this->setAdminUser();
$this->expectException(dml_missing_record_exception::class);
external::get_data_request($this->requestid + 1);
external::get_data_request(1);
}
/**
* Test for \tool_dataprivacy\external::set_context_defaults()
* when called by a user that doesn't have the manage registry capability.
*/
public function test_set_context_defaults_no_capability() {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$this->setUser($user);
$this->expectException(required_capability_exception::class);
external::set_context_defaults(CONTEXT_COURSECAT, context_instance::INHERIT, context_instance::INHERIT, '', false);
}
/**
* Test for \tool_dataprivacy\external::set_context_defaults().
*
* We're just checking the module context level here to test the WS function.
* More testing is done in \tool_dataprivacy_api_testcase::test_set_context_defaults().
*
* @dataProvider get_options_provider
* @param bool $modulelevel Whether defaults are to be applied on the module context level or for an activity only.
* @param bool $override Whether to override instances.
*/
public function test_set_context_defaults($modulelevel, $override) {
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
// Generate course cat, course, block, assignment, forum instances.
$coursecat = $generator->create_category();
$course = $generator->create_course(['category' => $coursecat->id]);
$assign = $generator->create_module('assign', ['course' => $course->id]);
list($course, $assigncm) = get_course_and_cm_from_instance($assign->id, 'assign');
$assigncontext = context_module::instance($assigncm->id);
// Generate purpose and category.
$category1 = api::create_category((object)['name' => 'Test category 1']);
$category2 = api::create_category((object)['name' => 'Test category 2']);
$purpose1 = api::create_purpose((object)[
'name' => 'Test purpose 1', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
]);
$purpose2 = api::create_purpose((object)[
'name' => 'Test purpose 2', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
]);
// Set a custom purpose and ID for this assignment instance.
$assignctxinstance = api::set_context_instance((object) [
'contextid' => $assigncontext->id,
'purposeid' => $purpose1->get('id'),
'categoryid' => $category1->get('id'),
]);
$modulename = $modulelevel ? 'assign' : '';
$categoryid = $category2->get('id');
$purposeid = $purpose2->get('id');
$result = external::set_context_defaults(CONTEXT_MODULE, $categoryid, $purposeid, $modulename, $override);
// Extract the result.
$return = external_api::clean_returnvalue(external::set_context_defaults_returns(), $result);
$this->assertTrue($return['result']);
// Check the assignment context instance.
$instanceexists = context_instance::record_exists($assignctxinstance->get('id'));
if ($override) {
// The custom assign instance should have been deleted.
$this->assertFalse($instanceexists);
} else {
// The custom assign instance should still exist.
$this->assertTrue($instanceexists);
}
// Check the saved defaults.
list($savedpurpose, $savedcategory) = \tool_dataprivacy\data_registry::get_defaults(CONTEXT_MODULE, $modulename);
$this->assertEquals($categoryid, $savedcategory);
$this->assertEquals($purposeid, $savedpurpose);
}
/**
* Test for \tool_dataprivacy\external::get_category_options()
* when called by a user that doesn't have the manage registry capability.
*/
public function test_get_category_options_no_capability() {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$this->expectException(required_capability_exception::class);
external::get_category_options(true, true);
}
/**
* Data provider for \tool_dataprivacy_external_testcase::test_XX_options().
*/
public function get_options_provider() {
return [
[false, false],
[false, true],
[true, false],
[true, true],
];
}
/**
* Test for \tool_dataprivacy\external::get_category_options().
*
* @dataProvider get_options_provider
* @param bool $includeinherit Whether "Inherit" would be included to the options.
* @param bool $includenotset Whether "Not set" would be included to the options.
*/
public function test_get_category_options($includeinherit, $includenotset) {
$this->resetAfterTest();
$this->setAdminUser();
// Prepare our expected options.
$expectedoptions = [];
if ($includeinherit) {
$expectedoptions[] = [
'id' => context_instance::INHERIT,
'name' => get_string('inherit', 'tool_dataprivacy'),
];
}
if ($includenotset) {
$expectedoptions[] = [
'id' => context_instance::NOTSET,
'name' => get_string('notset', 'tool_dataprivacy'),
];
}
for ($i = 1; $i <= 3; $i++) {
$category = api::create_category((object)['name' => 'Category ' . $i]);
$expectedoptions[] = [
'id' => $category->get('id'),
'name' => $category->get('name'),
];
}
// Call the WS function.
$result = external::get_category_options($includeinherit, $includenotset);
// Extract the options.
$return = (object) external_api::clean_returnvalue(external::get_category_options_returns(), $result);
$options = $return->options;
// Make sure everything checks out.
$this->assertCount(count($expectedoptions), $options);
foreach ($options as $option) {
$this->assertContains($option, $expectedoptions);
}
}
/**
* Test for \tool_dataprivacy\external::get_purpose_options()
* when called by a user that doesn't have the manage registry capability.
*/
public function test_get_purpose_options_no_capability() {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$this->setUser($user);
$this->expectException(required_capability_exception::class);
external::get_category_options(true, true);
}
/**
* Test for \tool_dataprivacy\external::get_purpose_options().
*
* @dataProvider get_options_provider
* @param bool $includeinherit Whether "Inherit" would be included to the options.
* @param bool $includenotset Whether "Not set" would be included to the options.
*/
public function test_get_purpose_options($includeinherit, $includenotset) {
$this->resetAfterTest();
$this->setAdminUser();
// Prepare our expected options.
$expectedoptions = [];
if ($includeinherit) {
$expectedoptions[] = [
'id' => context_instance::INHERIT,
'name' => get_string('inherit', 'tool_dataprivacy'),
];
}
if ($includenotset) {
$expectedoptions[] = [
'id' => context_instance::NOTSET,
'name' => get_string('notset', 'tool_dataprivacy'),
];
}
for ($i = 1; $i <= 3; $i++) {
$purpose = api::create_purpose((object)[
'name' => 'Purpose ' . $i, 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
]);
$expectedoptions[] = [
'id' => $purpose->get('id'),
'name' => $purpose->get('name'),
];
}
// Call the WS function.
$result = external::get_purpose_options($includeinherit, $includenotset);
// Extract the options.
$return = (object) external_api::clean_returnvalue(external::get_purpose_options_returns(), $result);
$options = $return->options;
// Make sure everything checks out.
$this->assertCount(count($expectedoptions), $options);
foreach ($options as $option) {
$this->assertContains($option, $expectedoptions);
}
}
/**
* Data provider for \tool_dataprivacy_external_testcase::get_activity_options().
*/
public function get_activity_options_provider() {
return [
[false, false, true],
[false, true, true],
[true, false, true],
[true, true, true],
[false, false, false],
[false, true, false],
[true, false, false],
[true, true, false],
];
}
/**
* Test for \tool_dataprivacy\external::get_activity_options().
*
* @dataProvider get_activity_options_provider
* @param bool $inheritcategory Whether the category would be set to "Inherit".
* @param bool $inheritpurpose Whether the purpose would be set to "Inherit".
* @param bool $nodefaults Whether to fetch only activities that don't have defaults.
*/
public function test_get_activity_options($inheritcategory, $inheritpurpose, $nodefaults) {
$this->resetAfterTest();
$this->setAdminUser();
$category = api::create_category((object)['name' => 'Test category']);
$purpose = api::create_purpose((object)[
'name' => 'Test purpose ', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
]);
$categoryid = $category->get('id');
$purposeid = $purpose->get('id');
if ($inheritcategory) {
$categoryid = context_instance::INHERIT;
}
if ($inheritpurpose) {
$purposeid = context_instance::INHERIT;
}
// Set the context default for the assignment module.
api::set_context_defaults(CONTEXT_MODULE, $categoryid, $purposeid, 'assign');
// Call the WS function.
$result = external::get_activity_options($nodefaults);
// Extract the options.
$return = (object) external_api::clean_returnvalue(external::get_activity_options_returns(), $result);
$options = $return->options;
// Make sure the options list is not empty.
$this->assertNotEmpty($options);
$pluginwithdefaults = [
'name' => 'assign',
'displayname' => get_string('pluginname', 'assign')
];
// If we don't want plugins with defaults to be listed or if both of the category and purpose are set to inherit,
// the assign module should be listed.
if (!$nodefaults || ($inheritcategory && $inheritpurpose)) {
$this->assertContains($pluginwithdefaults, $options);
} else {
$this->assertNotContains($pluginwithdefaults, $options);
}
}
/**
* Test for external::bulk_approve_data_requests().
*/
public function test_bulk_approve_data_requests() {
$this->resetAfterTest();
// Create delete data requests.
$requester1 = $this->getDataGenerator()->create_user();
$this->setUser($requester1->id);
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid1 = $datarequest1->get('id');
$requester2 = $this->getDataGenerator()->create_user();
$this->setUser($requester2->id);
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid2 = $datarequest2->get('id');
// Approve the requests.
$this->setAdminUser();
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::bulk_approve_data_requests([$requestid1, $requestid2]);
$return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
$this->assertTrue($return->result);
$this->assertEmpty($return->warnings);
}
/**
* Test for external::bulk_approve_data_requests() for a non-existent request ID.
*/
public function test_bulk_approve_data_requests_non_existent() {
$this->resetAfterTest();
$this->setAdminUser();
$result = external::bulk_approve_data_requests([42]);
$return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
$this->assertFalse($return->result);
$this->assertCount(1, $return->warnings);
$warning = reset($return->warnings);
$this->assertEquals('errorrequestnotfound', $warning['warningcode']);
$this->assertEquals(42, $warning['item']);
}
/**
* Test for external::bulk_deny_data_requests() for a user without permission to deny requests.
*/
public function test_bulk_approve_data_requests_no_permission() {
$this->resetAfterTest();
// Create delete data requests.
$requester1 = $this->getDataGenerator()->create_user();
$this->setUser($requester1->id);
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid1 = $datarequest1->get('id');
$requester2 = $this->getDataGenerator()->create_user();
$this->setUser($requester2->id);
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid2 = $datarequest2->get('id');
$this->setAdminUser();
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
// Approve the requests.
$uut = $this->getDataGenerator()->create_user();
$this->setUser($uut);
$this->expectException(required_capability_exception::class);
$result = external::bulk_approve_data_requests([$requestid1, $requestid2]);
}
/**
* Test for external::bulk_deny_data_requests() for a user without permission to deny requests.
*/
public function test_bulk_approve_data_requests_own_request() {
$this->resetAfterTest();
// Create delete data requests.
$requester1 = $this->getDataGenerator()->create_user();
$this->setUser($requester1->id);
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid1 = $datarequest1->get('id');
$requester2 = $this->getDataGenerator()->create_user();
$this->setUser($requester2->id);
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid2 = $datarequest2->get('id');
$this->setAdminUser();
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
// Deny the requests.
$this->setUser($requester1);
$this->expectException(required_capability_exception::class);
$result = external::bulk_approve_data_requests([$requestid1]);
}
/**
* Test for external::bulk_deny_data_requests().
*/
public function test_bulk_deny_data_requests() {
$this->resetAfterTest();
// Create delete data requests.
$requester1 = $this->getDataGenerator()->create_user();
$this->setUser($requester1->id);
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid1 = $datarequest1->get('id');
$requester2 = $this->getDataGenerator()->create_user();
$this->setUser($requester2->id);
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid2 = $datarequest2->get('id');
// Deny the requests.
$this->setAdminUser();
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::bulk_deny_data_requests([$requestid1, $requestid2]);
$return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
$this->assertTrue($return->result);
$this->assertEmpty($return->warnings);
}
/**
* Test for external::bulk_deny_data_requests() for a non-existent request ID.
*/
public function test_bulk_deny_data_requests_non_existent() {
$this->resetAfterTest();
$this->setAdminUser();
$result = external::bulk_deny_data_requests([42]);
$return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
$this->assertFalse($return->result);
$this->assertCount(1, $return->warnings);
$warning = reset($return->warnings);
$this->assertEquals('errorrequestnotfound', $warning['warningcode']);
$this->assertEquals(42, $warning['item']);
}
/**
* Test for external::bulk_deny_data_requests() for a user without permission to deny requests.
*/
public function test_bulk_deny_data_requests_no_permission() {
$this->resetAfterTest();
// Create delete data requests.
$requester1 = $this->getDataGenerator()->create_user();
$this->setUser($requester1->id);
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid1 = $datarequest1->get('id');
$requester2 = $this->getDataGenerator()->create_user();
$this->setUser($requester2->id);
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid2 = $datarequest2->get('id');
$this->setAdminUser();
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
// Deny the requests.
$uut = $this->getDataGenerator()->create_user();
$this->setUser($uut);
$this->expectException(required_capability_exception::class);
$result = external::bulk_deny_data_requests([$requestid1, $requestid2]);
}
/**
* Test for external::bulk_deny_data_requests() for a user cannot approve their own request.
*/
public function test_bulk_deny_data_requests_own_request() {
$this->resetAfterTest();
// Create delete data requests.
$requester1 = $this->getDataGenerator()->create_user();
$this->setUser($requester1->id);
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid1 = $datarequest1->get('id');
$requester2 = $this->getDataGenerator()->create_user();
$this->setUser($requester2->id);
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid2 = $datarequest2->get('id');
$this->setAdminUser();
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
// Deny the requests.
$this->setUser($requester1);
$this->expectException(required_capability_exception::class);
$result = external::bulk_deny_data_requests([$requestid1]);
}
}
@@ -0,0 +1,115 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Data privacy tool data generator.
*
* @package tool_dataprivacy
* @category test
* @copyright 2018 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use tool_dataprivacy\api;
use tool_dataprivacy\category;
use tool_dataprivacy\purpose;
defined('MOODLE_INTERNAL') || die();
/**
* Data privacy tool data generator class.
*
* @package tool_dataprivacy
* @category test
* @copyright 2018 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_dataprivacy_generator extends component_generator_base {
/** @var int Number of created categories. */
protected $categorycount = 0;
/** @var int Number of created purposes. */
protected $purposecount = 0;
/**
* Reset process.
*
* Do not call directly.
*
* @return void
*/
public function reset() {
$this->categorycount = 0;
$this->purposecount = 0;
}
/**
* Create a new category.
*
* @param array|stdClass $record
* @return category
*/
public function create_category($record = null) {
$this->categorycount++;
$i = $this->categorycount;
$record = (object)$record;
if (!isset($record->name)) {
$record->name = "Test purpose $i";
}
if (!isset($record->description)) {
$record->description = "{$record->name} description";
}
$category = api::create_category($record);
return $category;
}
/**
* Create a new purpose.
*
* @param array|stdClass $record
* @return purpose
*/
public function create_purpose($record = null) {
$this->purposecount++;
$i = $this->purposecount;
$record = (object)$record;
if (!isset($record->name)) {
$record->name = "Test purpose $i";
}
if (!isset($record->description)) {
$record->description = "{$record->name} $i description";
}
if (!isset($record->retentionperiod)) {
$record->retentionperiod = 'PT1M';
}
if (!isset($record->lawfulbases)) {
$record->lawfulbases = 'gdpr_art_6_1_a';
}
$purpose = api::create_purpose($record);
return $purpose;
}
}
+240
View File
@@ -0,0 +1,240 @@
<?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/>.
/**
* Tests for scheduled tasks.
*
* @package tool_dataprivacy
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once('data_privacy_testcase.php');
use tool_dataprivacy\api;
/**
* Tests for scheduled tasks.
*
* @package tool_dataprivacy
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_dataprivacy_task_testcase extends data_privacy_testcase {
/**
* Test tearDown.
*/
public function tearDown() {
\core_privacy\local\request\writer::reset();
}
/**
* Ensure that a delete data request for pre-existing deleted users
* is created when there are not any existing data requests
* for that particular user.
*/
public function test_delete_existing_deleted_users_task_no_previous_requests() {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create a user.
$user = $this->getDataGenerator()->create_user();
// Mark the user as deleted.
$user->deleted = 1;
$DB->update_record('user', $user);
// The user should not have a delete data request.
$this->assertCount(0, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
$this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
// After running the scheduled task, the deleted user should have a delete data request.
$this->assertCount(1, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
}
/**
* Ensure that a delete data request for pre-existing deleted users
* is not being created when automatic creation of delete data requests is disabled.
*/
public function test_delete_existing_deleted_users_task_automatic_creation_disabled() {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Disable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 0, 'tool_dataprivacy');
// Create a user.
$user = $this->getDataGenerator()->create_user();
// Mark the user as deleted.
$user->deleted = 1;
$DB->update_record('user', $user);
// The user should not have a delete data request.
$this->assertCount(0, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
$this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
// After running the scheduled task, the deleted user should still not have a delete data request.
$this->assertCount(0, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
}
/**
* Ensure that a delete data request for pre-existing deleted users
* is created when there are existing non-delete data requests
* for that particular user.
*/
public function test_delete_existing_deleted_users_task_existing_export_data_requests() {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create a user.
$user = $this->getDataGenerator()->create_user();
// Create export data request for the user.
api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT);
// Mark the user as deleted.
$user->deleted = 1;
$DB->update_record('user', $user);
// The user should have a export data request.
$this->assertCount(1, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_EXPORT]));
// The user should not have a delete data request.
$this->assertCount(0, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
$this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
// After running the scheduled task, the deleted user should have a delete data request.
$this->assertCount(1, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
}
/**
* Ensure that a delete data request for pre-existing deleted users
* is not created when there are existing ongoing delete data requests
* for that particular user.
*/
public function test_delete_existing_deleted_users_task_existing_ongoing_delete_data_requests() {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create a user.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Create delete data request for the user.
$datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
$requestid = $datarequest->get('id');
api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
// The user should have an ongoing delete data request.
$this->assertCount(1, api::get_data_requests($user->id,
[api::DATAREQUEST_STATUS_AWAITING_APPROVAL], [api::DATAREQUEST_TYPE_DELETE]));
// Mark the user as deleted.
$user->deleted = 1;
$DB->update_record('user', $user);
// The user should still have the existing ongoing delete data request.
$this->assertCount(1, \tool_dataprivacy\api::get_data_requests($user->id,
[api::DATAREQUEST_STATUS_AWAITING_APPROVAL], [api::DATAREQUEST_TYPE_DELETE]));
$this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
// After running the scheduled task, the user should have only one delete data request.
$this->assertCount(1, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
// The user should not have a newly created delete data request.
$this->assertCount(0, api::get_data_requests($user->id,
[api::DATAREQUEST_STATUS_PENDING], [api::DATAREQUEST_TYPE_DELETE]));
}
/**
* Ensure that a delete data request for pre-existing deleted users
* is not created when there are existing finished delete data requests
* for that particular user.
*/
public function test_delete_existing_deleted_users_task_existing_finished_delete_data_requests() {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create a user.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Create delete data request for the user.
$datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
$requestid = $datarequest->get('id');
api::update_request_status($requestid, api::DATAREQUEST_STATUS_CANCELLED);
// The user should have a delete data request.
$this->assertCount(1, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
// The user should not have an ongoing data requests.
$this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
// Mark the user as deleted.
$user->deleted = 1;
$DB->update_record('user', $user);
// The user should still have the existing finished delete data request.
$this->assertCount(1, \tool_dataprivacy\api::get_data_requests($user->id,
[api::DATAREQUEST_STATUS_CANCELLED], [api::DATAREQUEST_TYPE_DELETE]));
$this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
// After running the scheduled task, the user should still have one delete data requests.
$this->assertCount(1, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
// The user should still have the existing finished delete data request.
$this->assertCount(1, \tool_dataprivacy\api::get_data_requests($user->id,
[api::DATAREQUEST_STATUS_CANCELLED], [api::DATAREQUEST_TYPE_DELETE]));
}
/**
* Helper to execute a particular task.
*
* @param string $task The task.
*/
private function execute_task($task) {
// Run the scheduled task.
ob_start();
$task = \core\task\manager::get_scheduled_task($task);
$task->execute();
ob_end_clean();
}
}
@@ -0,0 +1,207 @@
<?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/>.
/**
* Tests for the event observer.
*
* @package tool_dataprivacy
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use \tool_dataprivacy\event\user_deleted_observer;
use \tool_dataprivacy\api;
/**
* Event observer test.
*
* @package tool_dataprivacy
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_dataprivacy_user_deleted_observer_testcase extends advanced_testcase {
/**
* Ensure that a delete data request is created upon user deletion.
*/
public function test_create_delete_data_request() {
$this->resetAfterTest();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create another user who is not a DPO.
$user = $this->getDataGenerator()->create_user();
$event = $this->trigger_delete_user_event($user);
user_deleted_observer::create_delete_data_request($event);
// Validate that delete data request has been created.
$this->assertTrue(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
}
/**
* Ensure that a delete data request is not created upon user deletion if automatic creation of
* delete data requests is disabled.
*/
public function test_create_delete_data_request_automatic_creation_disabled() {
$this->resetAfterTest();
// Disable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 0, 'tool_dataprivacy');
// Create another user who is not a DPO.
$user = $this->getDataGenerator()->create_user();
$event = $this->trigger_delete_user_event($user);
user_deleted_observer::create_delete_data_request($event);
// Validate that delete data request has been created.
$this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
}
/**
* Ensure that a delete data request is being created upon user deletion
* if an ongoing export data request (or any other except delete data request) for that user already exists.
*/
public function test_create_delete_data_request_export_data_request_preexists() {
$this->resetAfterTest();
$this->setAdminUser();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create another user who is not a DPO.
$user = $this->getDataGenerator()->create_user();
// Create a delete data request for $user.
api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT);
// Validate that delete data request has been created.
$this->assertTrue(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_EXPORT));
$this->assertEquals(0, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
$event = $this->trigger_delete_user_event($user);
user_deleted_observer::create_delete_data_request($event);
// Validate that delete data request has been created.
$this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
}
/**
* Ensure that a delete data request is not being created upon user deletion
* if an ongoing delete data request for that user already exists.
*/
public function test_create_delete_data_request_ongoing_delete_data_request_preexists() {
$this->resetAfterTest();
$this->setAdminUser();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create another user who is not a DPO.
$user = $this->getDataGenerator()->create_user();
// Create a delete data request for $user.
api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
// Validate that delete data request has been created.
$this->assertTrue(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
$event = $this->trigger_delete_user_event($user);
user_deleted_observer::create_delete_data_request($event);
// Validate that additional delete data request has not been created.
$this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
}
/**
* Ensure that a delete data request is being created upon user deletion
* if a finished delete data request (excluding complete) for that user already exists.
*/
public function test_create_delete_data_request_canceled_delete_data_request_preexists() {
$this->resetAfterTest();
$this->setAdminUser();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create another user who is not a DPO.
$user = $this->getDataGenerator()->create_user();
// Create a delete data request for $user.
$datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
$requestid = $datarequest->get('id');
api::update_request_status($requestid, api::DATAREQUEST_STATUS_CANCELLED);
// Validate that delete data request has been created and the status has been updated to 'Canceled'.
$this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
$this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
$event = $this->trigger_delete_user_event($user);
user_deleted_observer::create_delete_data_request($event);
// Validate that additional delete data request has been created.
$this->assertEquals(2, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
$this->assertTrue(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
}
/**
* Ensure that a delete data request is being created upon user deletion
* if a completed delete data request for that user already exists.
*/
public function test_create_delete_data_request_completed_delete_data_request_preexists() {
$this->resetAfterTest();
$this->setAdminUser();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create another user who is not a DPO.
$user = $this->getDataGenerator()->create_user();
// Create a delete data request for $user.
$datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
$requestid = $datarequest->get('id');
api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE);
// Validate that delete data request has been created and the status has been updated to 'Completed'.
$this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
$this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
$event = $this->trigger_delete_user_event($user);
user_deleted_observer::create_delete_data_request($event);
// Validate that additional delete data request has not been created.
$this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
$this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
}
/**
* Helper to trigger and capture the delete user event.
*
* @param object $user The user object.
* @return \core\event\user_deleted $event The returned event.
*/
private function trigger_delete_user_event($user) {
$sink = $this->redirectEvents();
delete_user($user);
$events = $sink->get_events();
$sink->close();
$event = reset($events);
// Validate event data.
$this->assertInstanceOf('\core\event\user_deleted', $event);
return $event;
}
}
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die;
$plugin->version = 2017051550;
$plugin->version = 2017051556;
$plugin->requires = 2017051507.00; // Moodle 3.3.7 (Build: 20180517) and upwards.
$plugin->component = 'tool_dataprivacy';
+1 -1
View File
@@ -1 +1 @@
define(["jquery","core/str","core/modal_factory","core/modal_events","core/notification","core/fragment","core/ajax","core/yui"],function(a,b,c,d,e,f,g,h){"use strict";var i=function(a){this.contextid=a,this.init()};return i.prototype.modal=null,i.prototype.contextid=-1,i.prototype.stringKeys=[{key:"consentdetails",component:"tool_policy"},{key:"iagreetothepolicy",component:"tool_policy"},{key:"selectusersforconsent",component:"tool_policy"},{key:"ok"},{key:"revokedetails",component:"tool_policy"},{key:"irevokethepolicy",component:"tool_policy"}],i.prototype.init=function(){var c=a("a[data-action=acceptmodal]");c.on("click",function(b){b.preventDefault();var c=a(b.currentTarget).attr("href"),d=c.slice(c.indexOf("?")+1);this.showFormModal(d)}.bind(this)),c=a("form[data-action=acceptmodal]"),c.on("submit",function(d){if(d.preventDefault(),a(d.currentTarget).find('input[type=checkbox][name="userids[]"]:checked').length){var f=a(d.currentTarget).serialize();this.showFormModal(f,c)}else b.get_strings(this.stringKeys).done(function(a){e.alert("",a[2],a[3])})}.bind(this))},i.prototype.showFormModal=function(a,d){for(var f,g=a.split("&"),h=0;h<g.length;h++){var i=g[h].split("=");"action"==i[0]&&(f=i[1])}b.get_strings(this.stringKeys).done(function(b){var e,g;"revoke"==f?(e=b[4],g=b[5]):(e=b[0],g=b[1]),c.create({type:c.types.SAVE_CANCEL,title:e,body:""},d).done(function(b){this.modal=b,this.setupFormModal(a,g)}.bind(this))}.bind(this)).fail(e.exception)},i.prototype.setupFormModal=function(a,b){var c=this.modal;c.setLarge(),c.setSaveButtonText(b),c.getRoot().on(d.hidden,this.destroy.bind(this)),c.setBody(this.getBody(a)),c.getRoot().on(d.save,this.submitForm.bind(this)),c.getRoot().on("submit","form",this.submitFormAjax.bind(this)),c.show()},i.prototype.getBody=function(a){"undefined"==typeof a&&(a={});var b={jsonformdata:JSON.stringify(a)};return f.loadFragment("tool_policy","accept_on_behalf",this.contextid,b)},i.prototype.submitFormAjax=function(a){a.preventDefault();var b=this.modal.getRoot().find("form").serialize(),c=g.call([{methodname:"tool_policy_submit_accept_on_behalf",args:{jsonformdata:JSON.stringify(b)}}]);c[0].done(function(a){a.validationerrors?this.modal.setBody(this.getBody(b)):this.close()}.bind(this)).fail(e.exception)},i.prototype.submitForm=function(a){a.preventDefault(),this.modal.getRoot().find("form").submit()},i.prototype.close=function(){this.destroy(),document.location.reload()},i.prototype.destroy=function(){h.use("moodle-core-formchangechecker",function(){M.core_formchangechecker.reset_form_dirty_state()}),this.modal.destroy()},{getInstance:function(a){return new i(a)}}});
define(["jquery","core/str","core/modal_factory","core/modal_events","core/notification","core/fragment","core/ajax","core/yui"],function(a,b,c,d,e,f,g,h){"use strict";var i=function(a){this.contextid=a,this.init()};return i.prototype.modal=null,i.prototype.contextid=-1,i.prototype.stringKeys=[{key:"consentdetails",component:"tool_policy"},{key:"iagreetothepolicy",component:"tool_policy"},{key:"selectusersforconsent",component:"tool_policy"},{key:"ok"},{key:"revokedetails",component:"tool_policy"},{key:"irevokethepolicy",component:"tool_policy"}],i.prototype.currentTrigger=null,i.prototype.triggers={SINGLE:"a[data-action=acceptmodal]",BULK:"input[data-action=acceptmodal]"},i.prototype.init=function(){a(this.triggers.SINGLE).on("click",function(b){b.preventDefault(),this.currentTrigger=a(b.currentTarget);var c=a(b.currentTarget).attr("href"),d=c.slice(c.indexOf("?")+1);this.showFormModal(d)}.bind(this)),a(this.triggers.BULK).on("click",function(c){c.preventDefault(),this.currentTrigger=a(c.currentTarget);var d=a(c.currentTarget).closest("form");if(d.find('input[type=checkbox][name="userids[]"]:checked').length){var f=d.serialize();this.showFormModal(f)}else b.get_strings(this.stringKeys).done(function(a){e.alert("",a[2],a[3])})}.bind(this))},i.prototype.showFormModal=function(a){for(var d,f=a.split("&"),g=0;g<f.length;g++){var h=f[g].split("=");"action"==h[0]&&(d=h[1])}b.get_strings(this.stringKeys).done(function(b){var e,f;"revoke"==d?(e=b[4],f=b[5]):(e=b[0],f=b[1]),c.create({type:c.types.SAVE_CANCEL,title:e,body:""}).done(function(b){this.modal=b,this.setupFormModal(a,f)}.bind(this))}.bind(this)).fail(e.exception)},i.prototype.setupFormModal=function(a,b){var c=this.modal;c.setLarge(),c.setSaveButtonText(b),c.getRoot().on(d.hidden,this.destroy.bind(this)),c.setBody(this.getBody(a)),c.getRoot().on(d.save,this.submitForm.bind(this)),c.getRoot().on("submit","form",this.submitFormAjax.bind(this)),c.show()},i.prototype.getBody=function(a){"undefined"==typeof a&&(a={});var b={jsonformdata:JSON.stringify(a)};return f.loadFragment("tool_policy","accept_on_behalf",this.contextid,b)},i.prototype.submitFormAjax=function(a){a.preventDefault();var b=this.modal.getRoot().find("form").serialize(),c=g.call([{methodname:"tool_policy_submit_accept_on_behalf",args:{jsonformdata:JSON.stringify(b)}}]);c[0].done(function(a){a.validationerrors?this.modal.setBody(this.getBody(b)):this.close()}.bind(this)).fail(e.exception)},i.prototype.submitForm=function(a){a.preventDefault(),this.modal.getRoot().find("form").submit()},i.prototype.close=function(){this.destroy(),document.location.reload()},i.prototype.destroy=function(){h.use("moodle-core-formchangechecker",function(){M.core_formchangechecker.reset_form_dirty_state()}),this.modal.destroy(),this.currentTrigger.focus()},{getInstance:function(a){return new i(a)}}});
+26 -10
View File
@@ -82,6 +82,21 @@ define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/n
}
];
/**
* @var {object} currentTrigger The triggered HTML jQuery object
* @private
*/
AcceptOnBehalf.prototype.currentTrigger = null;
/**
* @var {object} triggers The trigger selectors
* @private
*/
AcceptOnBehalf.prototype.triggers = {
SINGLE: 'a[data-action=acceptmodal]',
BULK: 'input[data-action=acceptmodal]'
};
/**
* Initialise the class.
*
@@ -89,21 +104,22 @@ define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/n
*/
AcceptOnBehalf.prototype.init = function() {
// Initialise for links accepting policies for individual users.
var triggers = $('a[data-action=acceptmodal]');
triggers.on('click', function(e) {
$(this.triggers.SINGLE).on('click', function(e) {
e.preventDefault();
this.currentTrigger = $(e.currentTarget);
var href = $(e.currentTarget).attr('href'),
formData = href.slice(href.indexOf('?') + 1);
this.showFormModal(formData);
}.bind(this));
// Initialise for multiple users acceptance form.
triggers = $('form[data-action=acceptmodal]');
triggers.on('submit', function(e) {
$(this.triggers.BULK).on('click', function(e) {
e.preventDefault();
if ($(e.currentTarget).find('input[type=checkbox][name="userids[]"]:checked').length) {
var formData = $(e.currentTarget).serialize();
this.showFormModal(formData, triggers);
this.currentTrigger = $(e.currentTarget);
var form = $(e.currentTarget).closest('form');
if (form.find('input[type=checkbox][name="userids[]"]:checked').length) {
var formData = form.serialize();
this.showFormModal(formData);
} else {
Str.get_strings(this.stringKeys).done(function(strings) {
Notification.alert('', strings[2], strings[3]);
@@ -116,9 +132,8 @@ define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/n
* Show modal with a form
*
* @param {String} formData
* @param {object} triggerElement The trigger HTML jQuery object
*/
AcceptOnBehalf.prototype.showFormModal = function(formData, triggerElement) {
AcceptOnBehalf.prototype.showFormModal = function(formData) {
var action;
var params = formData.split('&');
for (var i = 0; i < params.length; i++) {
@@ -143,7 +158,7 @@ define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/n
type: ModalFactory.types.SAVE_CANCEL,
title: title,
body: ''
}, triggerElement).done(function(modal) {
}).done(function(modal) {
this.modal = modal;
this.setupFormModal(formData, saveText);
}.bind(this));
@@ -250,6 +265,7 @@ define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/n
M.core_formchangechecker.reset_form_dirty_state();
});
this.modal.destroy();
this.currentTrigger.focus();
};
return /** @alias module:tool_policy/acceptmodal */ {
@@ -415,7 +415,7 @@ class acceptances_table extends \table_sql {
*/
public function wrap_html_start() {
echo \html_writer::start_tag('form',
['action' => new \moodle_url('/admin/tool/policy/accept.php'), 'data-action' => 'acceptmodal']);
['action' => new \moodle_url('/admin/tool/policy/accept.php')]);
echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]);
echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'returnurl',
'value' => $this->get_return_url()]);
@@ -433,8 +433,8 @@ class acceptances_table extends \table_sql {
public function wrap_html_finish() {
global $PAGE;
if ($this->canagreeany) {
echo \html_writer::empty_tag('input', ['type' => 'submit',
'value' => get_string('consentbulk', 'tool_policy'), 'class' => 'btn btn-primary']);
echo \html_writer::empty_tag('input', ['type' => 'submit', 'data-action' => 'acceptmodal',
'value' => get_string('consentbulk', 'tool_policy'), 'class' => 'btn btn-primary m-t-1']);
$PAGE->requires->js_call_amd('tool_policy/acceptmodal', 'getInstance', [\context_system::instance()->id]);
}
echo "</form>\n";
@@ -260,3 +260,37 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
And "Admin User" "link" should exist in the "User One" "table_row"
And "Consent received from a parent" "text" should exist in the "User One" "table_row"
And "Consent not given" "icon" should exist in the "User Two" "table_row"
@javascript
Scenario: Bulk agree on behalf of another users as a manager, multiple policies, javascript on
Given I log in as "admin"
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
And I open the action menu in "This privacy policy" "table_row"
And I click on "Set status to \"Active\"" "link" in the "This privacy policy" "table_row"
And I press "Activate"
And I set the following system permissions of "Manager" role:
| capability | permission |
| tool/policy:acceptbehalf | Allow |
And I log out
When I log in as "manager"
And I press "Next"
And I press "Next"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This privacy policy" to "1"
And I press "Next"
And I navigate to "Users > Privacy and policies > User agreements" in site administration
And I click on "Select" "checkbox" in the "User One" "table_row"
And I press "Consent"
And I should see "Give consent on behalf of user(s)"
And I should see "One"
And I press "Cancel"
And I should not see "Give consent on behalf of user(s)"
And I click on "Select" "checkbox" in the "User Two" "table_row"
And I press "Consent"
And I should see "Give consent on behalf of user(s)"
And I should see "User One, User Two"
When I press "Give consent"
Then "Consent given on behalf of user" "icon" should exist in the "User One" "table_row"
And I should see "2 of 2" in the "User One" "table_row"
And "Consent given on behalf of user" "icon" should exist in the "User Two" "table_row"
And I should see "2 of 2" in the "User Two" "table_row"
+2
View File
@@ -154,6 +154,7 @@ class auth_plugin_cas extends auth_plugin_ldap {
$frm = new stdClass();
$frm->username = phpCAS::getUser();
$frm->password = 'passwdCas';
$frm->logintoken = \core\session\manager::get_login_token();
// Redirect to a course if multi-auth is activated, authCAS is set to CAS and the courseid is specified.
if ($this->config->multiauth && !empty($courseid)) {
@@ -167,6 +168,7 @@ class auth_plugin_cas extends auth_plugin_ldap {
$frm = new stdClass();
$frm->username = 'guest';
$frm->password = 'guest';
$frm->logintoken = \core\session\manager::get_login_token();
return;
}
+4
View File
@@ -70,6 +70,8 @@ class login implements renderable, templatable {
public $signupurl;
/** @var string The user name to pre-fill the form with. */
public $username;
/** @var string The csrf token to limit login to requests that come from the login form. */
public $logintoken;
/**
* Constructor.
@@ -104,6 +106,7 @@ class login implements renderable, templatable {
// Identity providers.
$this->identityproviders = \auth_plugin_base::get_identity_providers($authsequence);
$this->logintoken = \core\session\manager::get_login_token();
}
/**
@@ -136,6 +139,7 @@ class login implements renderable, templatable {
$data->rememberusername = $this->rememberusername;
$data->signupurl = $this->signupurl->out(false);
$data->username = $this->username;
$data->logintoken = $this->logintoken;
return $data;
}
+2 -1
View File
@@ -1803,7 +1803,8 @@ class auth_plugin_ldap extends auth_plugin_base {
// Here we want to trigger the whole authentication machinery
// to make sure no step is bypassed...
$user = authenticate_user_login($username, $key);
$reason = null;
$user = authenticate_user_login($username, $key, false, $reason, false);
if ($user) {
complete_user_login($user);
+2 -1
View File
@@ -49,9 +49,10 @@
$frm->password = generate_password(8);
/// Check if the user has actually submitted login data to us
$reason = null;
if ($shibbolethauth->user_login($frm->username, $frm->password)
&& $user = authenticate_user_login($frm->username, $frm->password)) {
&& $user = authenticate_user_login($frm->username, $frm->password, false, $reason, false)) {
complete_user_login($user);
if (user_not_fully_set_up($USER, true)) {
+1
View File
@@ -54,6 +54,7 @@ if ($show_instructions) {
</div>
<form action="../../login/index.php" method="post" id="guestlogin">
<div class="guestform">
<input type="hidden" name="logintoken" value="<?php echo s(\core\session\manager::get_login_token()); ?>" />
<input type="hidden" name="username" value="guest" />
<input type="hidden" name="password" value="guest" />
<input type="submit" value="<?php print_string("loginguest") ?>" />
+13 -1
View File
@@ -69,7 +69,19 @@ $PAGE->https_required();
$PAGE->set_heading($site->fullname);
echo $OUTPUT->header();
include("index_form.html");
if (isloggedin() and !isguestuser()) {
// Prevent logging when already logged in, we do not want them to relogin by accident because sesskey would be changed.
echo $OUTPUT->box_start();
$params = array('sesskey' => sesskey(), 'loginpage' => 1);
$logout = new single_button(new moodle_url('/login/logout.php', $params), get_string('logout'), 'post');
$continue = new single_button(new moodle_url('/'), get_string('cancel'), 'get');
echo $OUTPUT->confirm(get_string('alreadyloggedin', 'error', fullname($USER)), $logout, $continue);
echo $OUTPUT->box_end();
} else {
include("index_form.html");
}
echo $OUTPUT->footer();
+4
View File
@@ -1,6 +1,10 @@
This files describes API changes in /auth/* - plugins,
information provided here is intended especially for developers.
=== 3.3.9 ===
* Login forms generated from Moodle must include a login token to protect automated logins. See \core\session\manager::get_login_token().
=== 3.3.5 ===
* Self registration plugins should use core_privacy\local\sitepolicy\manager instead of directly checking
+1
View File
@@ -92,6 +92,7 @@ class block_login extends block_base {
$this->content->text .= '<div class="form-group">';
$this->content->text .= '<input type="submit" class="btn btn-primary btn-block" value="'.get_string('login').'" />';
$this->content->text .= '</div>';
$this->content->text .= '<input type="hidden" name="logintoken" value="'.s(\core\session\manager::get_login_token()).'" />';
$this->content->text .= "</form>\n";
+5
View File
@@ -553,6 +553,11 @@ $CFG->admin = 'admin';
//
// $CFG->upgradekey = 'put_some_password-like_value_here';
//
// Disable login token validation for login pages. Login token validation is enabled
// by default unless $CFG->alternateloginurl is set.
//
// $CFG->disablelogintoken = true;
//
//=========================================================================
// 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
//=========================================================================
@@ -26,8 +26,6 @@ namespace core_grading\privacy;
defined('MOODLE_INTERNAL') || die();
use core_privacy\local\request\approved_contextlist;
/**
* The trait used to provide backwards compatability for third-party plugins.
*
@@ -36,9 +34,32 @@ use core_privacy\local\request\approved_contextlist;
*/
trait gradingform_legacy_polyfill {
/**
* Export user data relating to an instance ID.
*
* @param \context $context Context to use with the export writer.
* @param int $instanceid The instance ID to export data for.
* @param array $subcontext The directory to export this data to.
*/
public static function export_gradingform_instance_data(\context $context, $instanceid, array $subcontext) {
static::_export_gradingform_instance_data($context, $instanceid, $subcontext);
}
/**
* Deletes all user data related to the provided instance IDs.
*
* @param array $instanceids The instance IDs to delete information from.
*/
public static function delete_gradingform_for_instances(array $instanceids) {
static::_delete_gradingform_for_instances($instanceids);
}
/**
* This method is used to export any user data this sub-plugin has using the object to get the context and userid.
*
* @deprecated Since Moodle 3.6 MDL-62535 Please use the methods in the gradingform_provider_v2 interface.
* @todo MDL-63137 remove this method.
*
* @param context $context Context owner of the data.
* @param stdClass $definition Grading definition entry to export.
* @param int $userid The user whose information is to be exported.
@@ -46,25 +67,34 @@ trait gradingform_legacy_polyfill {
* @return stdClass The data to export.
*/
public static function get_gradingform_export_data(\context $context, $definition, $userid) {
debugging('This method is deprecated. Please use the gradingform_provider_v2 interface', DEBUG_DEVELOPER);
return static::_get_gradingform_export_data($context, $definition, $userid);
}
/**
* Any call to this method should delete all user data for the context defined.
*
* @deprecated Since Moodle 3.6 MDL-62535 Please use the methods in the gradingform_provider_v2 interface.
* @todo MDL-63137 remove this method.
*
* @param context $context Context owner of the data.
*/
public static function delete_gradingform_for_context(\context $context) {
debugging('This method is deprecated. Please use the gradingform_provider_v2 interface', DEBUG_DEVELOPER);
static::_delete_gradingform_for_context($context);
}
/**
* A call to this method should delete user data (where practicle) from the userid and context.
*
* @deprecated Since Moodle 3.6 MDL-62535 Please use the methods in the gradingform_provider_v2 interface.
* @todo MDL-63137 remove this method.
*
* @param int $userid The user whose information is to be deleted.
* @param context $context Context owner of the data.
*/
public static function delete_gradingform_for_userid($userid, \context $context) {
debugging('This method is deprecated. Please use the gradingform_provider_v2 interface', DEBUG_DEVELOPER);
static::_delete_gradingform_for_userid($userid, $context);
}
}
@@ -19,6 +19,9 @@
*
* Grading method plugins should implement this if they store personal information.
*
* @deprecated since Moodle 3.6 MDL-62535 Please use the gradingform_provider_v2 interface
* @todo MDL-63167 Remove this file.
*
* @package core_grading
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -31,11 +34,15 @@ defined('MOODLE_INTERNAL') || die();
use core_privacy\local\request\approved_contextlist;
interface gradingform_provider extends
\core_privacy\local\request\plugin\subsystem_provider {
\core_privacy\local\request\plugin\subsystem_provider,
\core_privacy\local\deprecated {
/**
* This method is used to export any user data this sub-plugin has using the object to get the context and userid.
*
* @deprecated since Moodle 3.6 MDL-62535 Please use the methods in the gradingform_provider_v2 interface
* @todo MDL-63167 Remove this file.
*
* @param \context $context Context owner of the data.
* @param \stdClass $definition Grading definition entry to export.
* @param int $userid The user whose information is to be exported.
@@ -47,6 +54,9 @@ interface gradingform_provider extends
/**
* Any call to this method should delete all user data for the context defined.
*
* @deprecated since Moodle 3.6 MDL-62535 Please use the methods in the gradingform_provider_v2 interface
* @todo MDL-63167 Remove this file.
*
* @param \context $context Context owner of the data.
*/
public static function delete_gradingform_for_context(\context $context);
@@ -54,6 +64,9 @@ interface gradingform_provider extends
/**
* A call to this method should delete user data (where practicle) from the userid and context.
*
* @deprecated since Moodle 3.6 MDL-62535 Please use the methods in the gradingform_provider_v2 interface
* @todo MDL-63167 Remove this file.
*
* @param int $userid The user to delete.
* @param \context $context the context to refine the deletion.
*/
@@ -0,0 +1,46 @@
<?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/>.
/**
* Privacy class for requesting user data.
*
* @package core_grading
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_grading\privacy;
defined('MOODLE_INTERNAL') || die();
interface gradingform_provider_v2 extends
\core_privacy\local\request\plugin\subsystem_provider {
/**
* Export user data relating to an instance ID.
*
* @param \context $context Context to use with the export writer.
* @param int $instanceid The instance ID to export data for.
* @param array $subcontext The directory to export this data to.
*/
public static function export_gradingform_instance_data(\context $context, $instanceid, array $subcontext);
/**
* Deletes all user data related to the provided instance IDs.
*
* @param array $instanceids The instance IDs to delete information from.
*/
public static function delete_gradingform_for_instances(array $instanceids);
}
+79 -8
View File
@@ -133,6 +133,73 @@ class provider implements
}
}
/**
* Export all user data related to a context and itemid.
*
* @param \context $context Context to export on.
* @param int $itemid Item ID to export on.
* @param array $subcontext Directory location to export to.
*/
public static function export_item_data(\context $context, $itemid, array $subcontext) {
global $DB;
$sql = "SELECT gi.id AS instanceid, gd.id AS definitionid, gd.method
FROM {grading_areas} ga
JOIN {grading_definitions} gd ON gd.areaid = ga.id
JOIN {grading_instances} gi ON gi.definitionid = gd.id AND gi.itemid = :itemid
WHERE ga.contextid = :contextid";
$params = [
'itemid' => $itemid,
'contextid' => $context->id,
];
$records = $DB->get_recordset_sql($sql, $params);
foreach ($records as $record) {
$instancedata = manager::component_class_callback(
"gradingform_{$record->method}",
gradingform_provider_v2::class,
'export_gradingform_instance_data',
[$context, $record->instanceid, $subcontext]
);
}
$records->close();
}
/**
* Deletes all user data related to a context and possibly an itemid.
*
* @param \context $context The context to delete on.
* @param int|null $itemid An optional item ID to refine the deletion.
*/
public static function delete_instance_data(\context $context, $itemid = null) {
global $DB;
$itemsql = '';
$params = ['contextid' => $context->id];
if (isset($itemid)) {
$params['itemid'] = $itemid;
$itemsql = 'AND gi.itemid = :itemid';
}
$sql = "SELECT gi.id AS instanceid, gd.id, gd.method
FROM {grading_definitions} gd
JOIN {grading_instances} gi ON gi.definitionid = gd.id
JOIN {grading_areas} ga ON ga.id = gd.areaid
WHERE ga.contextid = :contextid $itemsql";
$records = $DB->get_records_sql($sql, $params);
if ($records) {
$firstrecord = current($records);
$method = $firstrecord->method;
$instanceids = array_map(function($record) {
return $record->instanceid;
}, $records);
manager::component_class_callback(
"gradingform_{$method}",
gradingform_provider_v2::class,
'delete_gradingform_for_instances',
[$instanceids]);
// Delete grading_instances rows.
$DB->delete_records_list('grading_instances', 'id', $instanceids);
}
}
/**
* Exports the data related to grading definitions within the specified context/subcontext.
*
@@ -191,6 +258,8 @@ class provider implements
if (!empty($definition->timecopied)) {
$tmpdata['timecopied'] = transform::datetime($definition->timecopied);
}
// MDL-63167 - This section is to be removed with the final deprecation of the gradingform_provider interface.
// Export gradingform information (if needed).
$instancedata = manager::component_class_callback(
"gradingform_{$definition->method}",
@@ -201,6 +270,7 @@ class provider implements
if (null !== $instancedata) {
$tmpdata = array_merge($tmpdata, $instancedata);
}
// End of section to be removed with deprecation.
$defdata[] = (object) $tmpdata;
@@ -258,34 +328,35 @@ class provider implements
}
/**
* Delete all use data which matches the specified $context.
* No deletion of the advanced grading is done.
*
* We never delete grading content.
*
* @param context $context A user context.
* @param \context $context the context to delete in.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
// MDL-63167 - This section is to be removed with the final deprecation of the gradingform_provider interface.
manager::plugintype_class_callback(
'gradingform',
gradingform_provider::class,
'delete_gradingform_for_context',
[$context]
);
// End of section to be removed for final deprecation.
}
/**
* Delete all user data for the specified user, in the specified contexts.
* Deletion of data in this provider is only related to grades and so can not be
* deleted for the creator of the advanced grade criteria.
*
* We never delete grading content.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
* @param approved_contextlist $contextlist a list of contexts approved for deletion.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
// MDL-63167 - This section is to be removed with the final deprecation of the gradingform_provider interface.
manager::plugintype_class_callback(
'gradingform',
gradingform_provider::class,
'delete_gradingform_for_userid',
[$contextlist]
);
// End of section to be removed for final deprecation.
}
}
@@ -38,6 +38,7 @@ use \core_privacy\local\request\writer;
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_grading\privacy\gradingform_provider_v2,
\core_privacy\local\request\user_preference_provider {
/**
@@ -47,6 +48,12 @@ class provider implements
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection) {
$collection->add_database_table('gradingform_guide_fillings', [
'instanceid' => 'privacy:metadata:instanceid',
'criterionid' => 'privacy:metadata:criterionid',
'remark' => 'privacy:metadata:remark',
'score' => 'privacy:metadata:score'
], 'privacy:metadata:fillingssummary');
$collection->add_user_preference(
'gradingform_guide-showmarkerdesc',
'privacy:metadata:preference:showmarkerdesc'
@@ -59,6 +66,38 @@ class provider implements
return $collection;
}
/**
* Export user data relating to an instance ID.
*
* @param \context $context Context to use with the export writer.
* @param int $instanceid The instance ID to export data for.
* @param array $subcontext The directory to export this data to.
*/
public static function export_gradingform_instance_data(\context $context, $instanceid, array $subcontext) {
global $DB;
// Get records from the provided params.
$params = ['instanceid' => $instanceid];
$sql = "SELECT gc.shortname, gc.description, gc.maxscore, gf.score, gf.remark
FROM {gradingform_guide_fillings} gf
JOIN {gradingform_guide_criteria} gc ON gc.id = gf.criterionid
WHERE gf.instanceid = :instanceid";
$records = $DB->get_records_sql($sql, $params);
if ($records) {
$subcontext = array_merge($subcontext, [get_string('guide', 'gradingform_guide'), $instanceid]);
writer::with_context($context)->export_data($subcontext, (object) $records);
}
}
/**
* Deletes all user data related to the provided instance IDs.
*
* @param array $instanceids The instance IDs to delete information from.
*/
public static function delete_gradingform_for_instances(array $instanceids) {
global $DB;
$DB->delete_records_list('gradingform_guide_fillings', 'instanceid', $instanceids);
}
/**
* Store all user preferences for the plugin.
*
@@ -75,8 +75,13 @@ $string['name'] = 'Name';
$string['needregrademessage'] = 'The marking guide definition was changed after this student had been graded. The student can not see this marking guide until you check the marking guide and update the grade.';
$string['pluginname'] = 'Marking guide';
$string['previewmarkingguide'] = 'Preview marking guide';
$string['privacy:metadata:criterionid'] = 'An identifier to a criterion for advanced marking.';
$string['privacy:metadata:fillingssummary'] = 'Stores information about a user\'s grade and feedback for the marking guide.';
$string['privacy:metadata:instanceid'] = 'An identifier to a grade used by an activity.';
$string['privacy:metadata:preference:showmarkerdesc'] = 'Whether to show marker criterion descriptions';
$string['privacy:metadata:preference:showstudentdesc'] = 'Whether to show student criterion descriptions';
$string['privacy:metadata:remark'] = 'Remarks related to this grade criterion.';
$string['privacy:metadata:score'] = 'A score for this grade criterion.';
$string['regrademessage1'] = 'You are about to save changes to a marking guide that has already been used for grading. Please indicate if existing grades need to be reviewed. If you set this then the marking guide will be hidden from students until their item is regraded.';
$string['regrademessage5'] = 'You are about to save significant changes to a marking guide that has already been used for grading. The gradebook value will be unchanged, but the marking guide will be hidden from students until their item is regraded.';
$string['regradeoption0'] = 'Do not mark for regrade';
@@ -25,6 +25,9 @@
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/grade/grading/tests/fixtures/marking_guide.php');
use \core_privacy\tests\provider_testcase;
use \core_privacy\local\request\writer;
use \gradingform_guide\privacy\provider;
@@ -74,4 +77,150 @@ class gradingform_guide_privacy_testcase extends provider_testcase {
$this->assertEquals(get_string('no'), $prefs->{'gradingform_guide-showmarkerdesc'}->value);
$this->assertEquals(get_string('yes'), $prefs->{'gradingform_guide-showstudentdesc'}->value);
}
/**
* Test the export of rubric data.
*/
public function test_get_gradingform_export_data() {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$module = $this->getDataGenerator()->create_module('assign', ['course' => $course]);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$modulecontext = context_module::instance($module->cmid);
$guide = new test_guide($modulecontext, 'testrubrib', 'Description text');
$guide->add_criteria(
'Spelling mistakes',
'Full marks will be given for no spelling mistakes.',
'Deduct 5 points per spelling mistake made.',
25
);
$guide->add_criteria(
'Pictures',
'Full marks will be given for including 3 pictures.',
'Give 5 points for each picture present',
15
);
$guide->create_guide();
$controller = $guide->manager->get_controller('guide');
// In the situation of mod_assign this would be the id from assign_grades.
$itemid = 1;
$instance = $controller->create_instance($user->id, $itemid);
// I need the ids for the criteria and there doesn't seem to be a nice method to get it.
$criteria = $DB->get_records('gradingform_guide_criteria');
$data = ['criteria' => []];
foreach ($criteria as $key => $value) {
if ($value->shortname == 'Spelling mistakes') {
$data['criteria'][$key]['remark'] = 'This user made several mistakes.';
$data['criteria'][$key]['remarkformat'] = 0;
$data['criteria'][$key]['score'] = 5;
} else {
$data['criteria'][$key]['remark'] = 'This user has two pictures.';
$data['criteria'][$key]['remarkformat'] = 0;
$data['criteria'][$key]['score'] = 10;
}
}
$data['itemid'] = $itemid;
// Update this instance with data.
$instance->update($data);
$instanceid = $instance->get_data('id');
// Let's try the method we are testing.
provider::export_gradingform_instance_data($modulecontext, $instance->get_id(), ['Test']);
$data = (array) writer::with_context($modulecontext)->get_data(['Test', 'Marking guide', $instanceid]);
$this->assertCount(2, $data);
$this->assertEquals('Spelling mistakes', $data['Spelling mistakes']->shortname);
$this->assertEquals('This user made several mistakes.', $data['Spelling mistakes']->remark);
$this->assertEquals('Pictures', $data['Pictures']->shortname);
$this->assertEquals('This user has two pictures.', $data['Pictures']->remark);
}
/**
* Test the deletion of rubric user information via the instance ID.
*/
public function test_delete_gradingform_for_instances() {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$module = $this->getDataGenerator()->create_module('assign', ['course' => $course]);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$modulecontext = context_module::instance($module->cmid);
$guide = new test_guide($modulecontext, 'testrubrib', 'Description text');
$guide->add_criteria(
'Spelling mistakes',
'Full marks will be given for no spelling mistakes.',
'Deduct 5 points per spelling mistake made.',
25
);
$guide->add_criteria(
'Pictures',
'Full marks will be given for including 3 pictures.',
'Give 5 points for each picture present',
15
);
$guide->create_guide();
$controller = $guide->manager->get_controller('guide');
// In the situation of mod_assign this would be the id from assign_grades.
$itemid = 1;
$instance = $controller->create_instance($user->id, $itemid);
// I need the ids for the criteria and there doesn't seem to be a nice method to get it.
$criteria = $DB->get_records('gradingform_guide_criteria');
$data = ['criteria' => []];
foreach ($criteria as $key => $value) {
if ($value->shortname == 'Spelling mistakes') {
$data['criteria'][$key]['remark'] = 'This user made several mistakes.';
$data['criteria'][$key]['remarkformat'] = 0;
$data['criteria'][$key]['score'] = 5;
} else {
$data['criteria'][$key]['remark'] = 'This user has two pictures.';
$data['criteria'][$key]['remarkformat'] = 0;
$data['criteria'][$key]['score'] = 10;
}
}
$data['itemid'] = $itemid;
// Update this instance with data.
$instance->update($data);
$itemid = 2;
$instance = $controller->create_instance($user->id, $itemid);
// I need the ids for the criteria and there doesn't seem to be a nice method to get it.
$criteria = $DB->get_records('gradingform_guide_criteria');
$data = ['criteria' => []];
foreach ($criteria as $key => $value) {
if ($value->shortname == 'Spelling mistakes') {
$data['criteria'][$key]['remark'] = 'This user made no mistakes.';
$data['criteria'][$key]['remarkformat'] = 0;
$data['criteria'][$key]['score'] = 25;
} else {
$data['criteria'][$key]['remark'] = 'This user has one pictures.';
$data['criteria'][$key]['remarkformat'] = 0;
$data['criteria'][$key]['score'] = 5;
}
}
$data['itemid'] = $itemid;
// Update this instance with data.
$instance->update($data);
// Check how many records we have in the fillings table.
$records = $DB->get_records('gradingform_guide_fillings');
$this->assertCount(4, $records);
// Let's delete one of the instances (the last one would be the easiest).
provider::delete_gradingform_for_instances([$instance->get_id()]);
$records = $DB->get_records('gradingform_guide_fillings');
$this->assertCount(2, $records);
foreach ($records as $record) {
$this->assertNotEquals($instance->get_id(), $record->instanceid);
}
}
}
@@ -26,6 +26,8 @@ namespace gradingform_rubric\privacy;
defined('MOODLE_INTERNAL') || die();
use \core_privacy\local\metadata\collection;
/**
* Privacy class for requesting user data.
*
@@ -33,15 +35,55 @@ defined('MOODLE_INTERNAL') || die();
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\null_provider {
\core_privacy\local\metadata\provider,
\core_grading\privacy\gradingform_provider_v2 {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
* Returns meta data about this system.
*
* @return string
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_reason() {
return 'privacy:metadata';
public static function get_metadata(collection $collection) {
$collection->add_database_table('gradingform_rubric_fillings', [
'instanceid' => 'privacy:metadata:instanceid',
'criterionid' => 'privacy:metadata:criterionid',
'levelid' => 'privacy:metadata:levelid',
'remark' => 'privacy:metadata:remark'
], 'privacy:metadata:fillingssummary');
return $collection;
}
/**
* Export user data relating to an instance ID.
*
* @param \context $context Context to use with the export writer.
* @param int $instanceid The instance ID to export data for.
* @param array $subcontext The directory to export this data to.
*/
public static function export_gradingform_instance_data(\context $context, $instanceid, array $subcontext) {
global $DB;
// Get records from the provided params.
$params = ['instanceid' => $instanceid];
$sql = "SELECT rc.description, rl.definition, rl.score, rf.remark
FROM {gradingform_rubric_fillings} rf
JOIN {gradingform_rubric_criteria} rc ON rc.id = rf.criterionid
JOIN {gradingform_rubric_levels} rl ON rf.levelid = rl.id
WHERE rf.instanceid = :instanceid";
$records = $DB->get_records_sql($sql, $params);
if ($records) {
$subcontext = array_merge($subcontext, [get_string('rubric', 'gradingform_rubric'), $instanceid]);
\core_privacy\local\request\writer::with_context($context)->export_data($subcontext, (object) $records);
}
}
/**
* Deletes all user data related to the provided instance IDs.
*
* @param array $instanceids The instance IDs to delete information from.
*/
public static function delete_gradingform_for_instances(array $instanceids) {
global $DB;
$DB->delete_records_list('gradingform_rubric_fillings', 'instanceid', $instanceids);
}
}
@@ -59,7 +59,11 @@ $string['name'] = 'Name';
$string['needregrademessage'] = 'The rubric definition was changed after this student had been graded. The student can not see this rubric until you check the rubric and update the grade.';
$string['pluginname'] = 'Rubric';
$string['previewrubric'] = 'Preview rubric';
$string['privacy:metadata'] = 'The rubric grading form plugin does not store any personal data.';
$string['privacy:metadata:criterionid'] = 'An identifier for a specific criterion being graded.';
$string['privacy:metadata:fillingssummary'] = 'Stores information about the user\'s grade created by the rubric.';
$string['privacy:metadata:instanceid'] = 'An identifier relating to a grade in an activity.';
$string['privacy:metadata:levelid'] = 'The level obtained in the rubric.';
$string['privacy:metadata:remark'] = 'Remarks related to the rubric criterion being assessed.';
$string['regrademessage1'] = 'You are about to save changes to a rubric that has already been used for grading. Please indicate if existing grades need to be reviewed. If you set this then the rubric will be hidden from students until their item is regraded.';
$string['regrademessage5'] = 'You are about to save significant changes to a rubric that has already been used for grading. The gradebook value will be unchanged, but the rubric will be hidden from students until their item is regraded.';
$string['regradeoption0'] = 'Do not mark for regrade';
@@ -0,0 +1,313 @@
<?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/>.
/**
* Privacy tests for gradingform_rubric
*
* @package gradingform_rubric
* @category test
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use \core_privacy\tests\provider_testcase;
use \core_privacy\local\request\writer;
use \gradingform_rubric\privacy\provider;
/**
* Privacy tests for gradingform_rubric
*
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class gradingform_rubric_privacy_testcase extends provider_testcase {
/**
* Test the export of rubric data.
*/
public function test_get_gradingform_export_data() {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$module = $this->getDataGenerator()->create_module('assign', ['course' => $course]);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$modulecontext = context_module::instance($module->cmid);
$rubric = new test_rubric($modulecontext, 'testrubrib', 'Description text');
$criterion = new test_criterion('Spelling is important');
$criterion->add_level('Nothing but mistakes', 0);
$criterion->add_level('Several mistakes', 1);
$criterion->add_level('No mistakes', 2);
$rubric->add_criteria($criterion);
$criterion = new test_criterion('Pictures');
$criterion->add_level('No pictures', 0);
$criterion->add_level('One picture', 1);
$criterion->add_level('More than one picture', 2);
$rubric->add_criteria($criterion);
$rubric->create_rubric();
$controller = $rubric->manager->get_controller('rubric');
// In the situation of mod_assign this would be the id from assign_grades.
$itemid = 1;
$instance = $controller->create_instance($user->id, $itemid);
// I need the ids for the criteria and there doesn't seem to be a nice method to get it.
$criteria = $DB->get_records('gradingform_rubric_criteria');
$data = ['criteria' => []];
foreach ($criteria as $key => $value) {
if ($value->description == 'Spelling is important') {
$level = $DB->get_record('gradingform_rubric_levels', ['criterionid' => $key, 'score' => 1]);
$data['criteria'][$key]['levelid'] = $level->id;
$data['criteria'][$key]['remark'] = 'This user made several mistakes.';
} else {
$level = $DB->get_record('gradingform_rubric_levels', ['criterionid' => $key, 'score' => 0]);
$data['criteria'][$key]['levelid'] = $level->id;
$data['criteria'][$key]['remark'] = 'Please add more pictures.';
}
}
$data['itemid'] = $itemid;
// Update this instance with data.
$instance->update($data);
$instanceid = $instance->get_data('id');
// Let's try the method we are testing.
provider::export_gradingform_instance_data($modulecontext, $instance->get_id(), ['Test']);
$data = (array) writer::with_context($modulecontext)->get_data(['Test', 'Rubric', $instanceid]);
$this->assertCount(2, $data);
$this->assertEquals('Spelling is important', $data['Spelling is important']->description);
$this->assertEquals('This user made several mistakes.', $data['Spelling is important']->remark);
$this->assertEquals('Pictures', $data['Pictures']->description);
$this->assertEquals('Please add more pictures.', $data['Pictures']->remark);
}
/**
* Test the deletion of rubric user information via the instance ID.
*/
public function test_delete_gradingform_for_instances() {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$module = $this->getDataGenerator()->create_module('assign', ['course' => $course]);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$modulecontext = context_module::instance($module->cmid);
$rubric = new test_rubric($modulecontext, 'testrubrib', 'Description text');
$criterion = new test_criterion('Spelling is important');
$criterion->add_level('Nothing but mistakes', 0);
$criterion->add_level('Several mistakes', 1);
$criterion->add_level('No mistakes', 2);
$rubric->add_criteria($criterion);
$criterion = new test_criterion('Pictures');
$criterion->add_level('No pictures', 0);
$criterion->add_level('One picture', 1);
$criterion->add_level('More than one picture', 2);
$rubric->add_criteria($criterion);
$rubric->create_rubric();
$controller = $rubric->manager->get_controller('rubric');
// In the situation of mod_assign this would be the id from assign_grades.
$itemid = 1;
$instance = $controller->create_instance($user->id, $itemid);
// I need the ids for the criteria and there doesn't seem to be a nice method to get it.
$criteria = $DB->get_records('gradingform_rubric_criteria');
$data = ['criteria' => []];
foreach ($criteria as $key => $value) {
if ($value->description == 'Spelling is important') {
$level = $DB->get_record('gradingform_rubric_levels', ['criterionid' => $key, 'score' => 1]);
$data['criteria'][$key]['levelid'] = $level->id;
$data['criteria'][$key]['remark'] = 'This user made several mistakes.';
} else {
$level = $DB->get_record('gradingform_rubric_levels', ['criterionid' => $key, 'score' => 0]);
$data['criteria'][$key]['levelid'] = $level->id;
$data['criteria'][$key]['remark'] = 'Please add more pictures.';
}
}
$data['itemid'] = $itemid;
// Update this instance with data.
$instance->update($data);
// Second instance.
$itemid = 2;
$instance = $controller->create_instance($user->id, $itemid);
// I need the ids for the criteria and there doesn't seem to be a nice method to get it.
$criteria = $DB->get_records('gradingform_rubric_criteria');
$data = ['criteria' => []];
foreach ($criteria as $key => $value) {
if ($value->description == 'Spelling is important') {
$level = $DB->get_record('gradingform_rubric_levels', ['criterionid' => $key, 'score' => 0]);
$data['criteria'][$key]['levelid'] = $level->id;
$data['criteria'][$key]['remark'] = 'Too many mistakes. Please try again.';
} else {
$level = $DB->get_record('gradingform_rubric_levels', ['criterionid' => $key, 'score' => 2]);
$data['criteria'][$key]['levelid'] = $level->id;
$data['criteria'][$key]['remark'] = 'Great number of pictures. Well done.';
}
}
$data['itemid'] = $itemid;
// Update this instance with data.
$instance->update($data);
// Check how many records we have in the fillings table.
$records = $DB->get_records('gradingform_rubric_fillings');
$this->assertCount(4, $records);
// Let's delete one of the instances (the last one would be the easiest).
provider::delete_gradingform_for_instances([$instance->get_id()]);
$records = $DB->get_records('gradingform_rubric_fillings');
$this->assertCount(2, $records);
foreach ($records as $record) {
$this->assertNotEquals($instance->get_id(), $record->instanceid);
}
}
}
/**
* Convenience class to create rubrics.
*
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_rubric {
/** @var array $criteria The criteria for this rubric. */
protected $criteria = [];
/** @var context The context that this rubric is in. */
protected $context;
/** @var string The name of this rubric. */
protected $name;
/** @var string A description for this rubric. */
protected $text;
/** @var integer The current criterion ID. This is incremented when a new criterion is added. */
protected $criterionid = 0;
/** @var grading_manager An object for managing the rubric. */
public $manager;
/**
* Constuctor for this rubric.
*
* @param context $context The context that this rubric is being used in.
* @param string $name Name of the rubric.
* @param string $text Description of the rubric.
*/
public function __construct($context, $name, $text) {
$this->context = $context;
$this->name = $name;
$this->text = $text;
$this->manager = get_grading_manager();
$this->manager->set_context($context);
$this->manager->set_component('mod_assign');
$this->manager->set_area('submission');
}
/**
* Creates the rubric using the appropriate APIs.
*/
public function create_rubric() {
$data = (object) [
'areaid' => $this->context->id,
'returnurl' => '',
'name' => $this->name,
'description_editor' => [
'text' => $this->text,
'format' => 1,
'itemid' => 1
],
'rubric' => [
'criteria' => $this->criteria,
'options' => [
'sortlevelsasc' => 1,
'lockzeropoints' => 1,
'showdescriptionteacher' => 1,
'showdescriptionstudent' => 1,
'showscoreteacher' => 1,
'showscorestudent' => 1,
'enableremarks' => 1,
'showremarksstudent' => 1
]
],
'saverubric' => 'Save rubric and make it ready',
'status' => 20
];
$controller = $this->manager->get_controller('rubric');
$controller->update_definition($data);
}
/**
* Adds a criterion to the rubric.
*
* @param test_criterion $criterion The criterion object (class below).
*/
public function add_criteria($criterion) {
$this->criterionid++;
$this->criteria['NEWID' . $this->criterionid] = [
'description' => $criterion->description,
'sortorder' => $this->criterionid,
'levels' => $criterion->levels
];
}
}
/**
* Convenience class to create rubric criterion.
*
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_criterion {
/** @var string $description A description of the criterion. */
public $description;
/** @var integer $sortorder sort order of the criterion. */
public $sortorder = 0;
/** @var integer $levelid The current level id for this level*/
public $levelid = 0;
/** @var array $levels The levels for this criterion. */
public $levels = [];
/**
* Constructor for this test_criterion object
*
* @param string $description A description of this criterion.
*/
public function __construct($description) {
$this->description = $description;
}
/**
* Adds levels to the criterion.
*
* @param string $definition The definition for this level.
* @param int $score The score received if this level is selected.
*/
public function add_level($definition, $score) {
$this->levelid++;
$this->levels['NEWID' . $this->levelid] = [
'definition' => $definition,
'score' => $score
];
}
}
+5
View File
@@ -1,6 +1,11 @@
This files describes API changes in /grade/grading/form/* - Advanced grading methods
information provided here is intended especially for developers.
=== 3.6 ===
* The privacy interface gradingform_provider has been deprecated. Please use
gradingform_provider_v2 instead.
=== 2.5.2 ===
* Grading methods now can return grade with decimals. See API functions
+118
View File
@@ -0,0 +1,118 @@
<?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/>.
/**
* A test guide class fixture.
*
* @package core_grading
* @category test
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Convenience class to create marking guides.
*
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_guide {
/** @var array $criteria Criteria for this marking guide. */
protected $criteria = [];
/** @var context $context The context that this marking guide is used in. */
protected $context;
/** @var string $name The name of this marking guide. */
protected $name;
/** @var string $text A description of this marking guide. */
protected $text;
/** @var integer $criterionid The current id for the criterion. */
protected $criterionid = 0;
/** @var integer $sortorder The current id for the sort order. */
protected $sortorder = 0;
/** @var grading_manager $manager The grading manager to handle creating the real marking guide. */
public $manager;
/**
* The constuctor for this test_guide object.
*
* @param context $context The context that this marking guide is used in.
* @param string $name The name of the marking guide.
* @param string $text The description of the marking guide.
*/
public function __construct($context, $name, $text) {
$this->context = $context;
$this->name = $name;
$this->text = $text;
$this->manager = get_grading_manager();
$this->manager->set_context($context);
$this->manager->set_component('mod_assign');
$this->manager->set_area('submission');
}
/**
* Uses the appropriate data and APIs to create a marking guide.
*/
public function create_guide() {
$data = (object) [
'areaid' => $this->context->id,
'returnurl' => '',
'name' => $this->name,
'description_editor' => [
'text' => $this->text,
'format' => 1,
'itemid' => 1
],
'guide' => [
'criteria' => $this->criteria,
'options' => [
'alwaysshowdefinition' => 1,
'showmarkspercriterionstudents' => 1
],
'comments' => []
],
'saveguide' => 'Continue',
'status' => 20
];
$controller = $this->manager->get_controller('guide');
$controller->update_definition($data);
}
/**
* Adds criteria to the marking guide.
*
* @param string $shortname The shortname for the criterion.
* @param string $description The description for the criterion.
* @param string $descriptionmarkers The description for the marker for this criterion.
* @param int $maxscore The maximum score possible for this criterion.
*/
public function add_criteria($shortname, $description, $descriptionmarkers, $maxscore) {
$this->criterionid++;
$this->sortorder++;
$this->criteria['NEWID' . $this->criterionid] = [
'sortorder' => $this->sortorder,
'shortname' => $shortname,
'description' => $description,
'descriptionmarkers' => $descriptionmarkers,
'maxscore' => $maxscore
];
}
}
@@ -33,7 +33,45 @@ defined('MOODLE_INTERNAL') || die();
*/
class gradeform_privacy_legacy_polyfill_test extends advanced_testcase {
/**
* Test that the core_grading\privacy\legacy_polyfill works and that the static _get_grading_export_data can be called.
* Test that the core_grading\privacy\legacy_polyfill works and that the static _export_gradingform_instance_data can be called.
*/
public function test_export_gradingform_instance_data() {
$context = context_system::instance();
$mock = $this->createMock(test_gradingform_legacy_polyfill_mock_wrapper::class);
$mock->expects($this->once())
->method('get_return_value')
->with('_export_gradingform_instance_data', [$context, 3, ['subcontext']]);
test_legacy_polyfill_gradingform_provider::$mock = $mock;
test_legacy_polyfill_gradingform_provider::export_gradingform_instance_data($context, 3, ['subcontext']);
}
/**
* Test for _get_metadata shim.
*/
public function test_get_metadata() {
$collection = new \core_privacy\local\metadata\collection('core_gradingform');
$this->assertSame($collection, test_legacy_polyfill_gradingform_provider::get_metadata($collection));
}
/**
* Test the _delete_gradingform_for_instances shim.
*/
public function test_delete_gradingform_for_instances() {
$context = context_system::instance();
$mock = $this->createMock(test_gradingform_legacy_polyfill_mock_wrapper::class);
$mock->expects($this->once())
->method('get_return_value')
->with('_delete_gradingform_for_instances', [[3, 17]]);
test_legacy_polyfill_gradingform_provider::$mock = $mock;
test_legacy_polyfill_gradingform_provider::delete_gradingform_for_instances([3, 17]);
}
/**
* Test the __get_gradingform_export_data shim.
*/
public function test_get_gradingform_export_data() {
$userid = 476;
@@ -46,14 +84,7 @@ class gradeform_privacy_legacy_polyfill_test extends advanced_testcase {
test_legacy_polyfill_gradingform_provider::$mock = $mock;
test_legacy_polyfill_gradingform_provider::get_gradingform_export_data($context, (object)[], $userid);
}
/**
* Test for _get_metadata shim.
*/
public function test_get_metadata() {
$collection = new \core_privacy\local\metadata\collection('core_gradingform');
$this->assertSame($collection, test_legacy_polyfill_gradingform_provider::get_metadata($collection));
$this->assertDebuggingCalled();
}
/**
@@ -69,10 +100,11 @@ class gradeform_privacy_legacy_polyfill_test extends advanced_testcase {
test_legacy_polyfill_gradingform_provider::$mock = $mock;
test_legacy_polyfill_gradingform_provider::delete_gradingform_for_context($context);
$this->assertDebuggingCalled();
}
/**
* Test the _delete_gradingform_for_context shim.
* Test the _delete_gradingform_for_userid shim.
*/
public function test_delete_gradingform_for_user() {
$userid = 696;
@@ -85,6 +117,7 @@ class gradeform_privacy_legacy_polyfill_test extends advanced_testcase {
test_legacy_polyfill_gradingform_provider::$mock = $mock;
test_legacy_polyfill_gradingform_provider::delete_gradingform_for_userid($userid, $context);
$this->assertDebuggingCalled();
}
}
@@ -96,7 +129,8 @@ class gradeform_privacy_legacy_polyfill_test extends advanced_testcase {
*/
class test_legacy_polyfill_gradingform_provider implements
\core_privacy\local\metadata\provider,
\core_grading\privacy\gradingform_provider {
\core_grading\privacy\gradingform_provider,
\core_grading\privacy\gradingform_provider_v2 {
use \core_grading\privacy\gradingform_legacy_polyfill;
use \core_privacy\local\legacy_polyfill;
@@ -107,32 +141,22 @@ class test_legacy_polyfill_gradingform_provider implements
public static $mock = null;
/**
* Export all user data for the gradingform plugin.
* Export user data relating to an instance ID.
*
* @param context $context
* @param stdClass $definition
* @param int $userid
* @param \context $context Context to use with the export writer.
* @param int $instanceid The instance ID to export data for.
* @param array $subcontext The directory to export this data to.
*/
protected static function _get_gradingform_export_data(\context $context, $definition, $userid) {
protected static function _export_gradingform_instance_data(\context $context, $instanceid, $subcontext) {
static::$mock->get_return_value(__FUNCTION__, func_get_args());
}
/**
* Deletes all user data for the given context.
* Deletes all user data related to the provided instance IDs.
*
* @param context $context
* @param array $instanceids The instance IDs to delete information from.
*/
protected static function _delete_gradingform_for_context(\context $context) {
static::$mock->get_return_value(__FUNCTION__, func_get_args());
}
/**
* Delete personal data for the given user and context.
*
* @param int $userid
* @param context $context
*/
protected static function _delete_gradingform_for_userid($userid, \context $context) {
protected static function _delete_gradingform_for_instances($instanceids) {
static::$mock->get_return_value(__FUNCTION__, func_get_args());
}
@@ -145,6 +169,47 @@ class test_legacy_polyfill_gradingform_provider implements
protected static function _get_metadata(\core_privacy\local\metadata\collection $collection) {
return $collection;
}
/**
* This method is used to export any user data this sub-plugin has using the object to get the context and userid.
*
* @deprecated Since Moodle 3.6 MDL-62535 Please use the methods in the gradingform_provider_v2 interface.
* @todo MDL-63137 remove this method.
*
* @param context $context Context owner of the data.
* @param stdClass $definition Grading definition entry to export.
* @param int $userid The user whose information is to be exported.
*
* @return stdClass The data to export.
*/
protected static function _get_gradingform_export_data(\context $context, $definition, $userid) {
static::$mock->get_return_value(__FUNCTION__, func_get_args());
}
/**
* Any call to this method should delete all user data for the context defined.
*
* @deprecated Since Moodle 3.6 MDL-62535 Please use the methods in the gradingform_provider_v2 interface.
* @todo MDL-63137 remove this method.
*
* @param context $context Context owner of the data.
*/
protected static function _delete_gradingform_for_context(\context $context) {
static::$mock->get_return_value(__FUNCTION__, func_get_args());
}
/**
* A call to this method should delete user data (where practicle) from the userid and context.
*
* @deprecated Since Moodle 3.6 MDL-62535 Please use the methods in the gradingform_provider_v2 interface.
* @todo MDL-63137 remove this method.
*
* @param int $userid The user whose information is to be deleted.
* @param context $context Context owner of the data.
*/
protected static function _delete_gradingform_for_userid($userid, \context $context) {
static::$mock->get_return_value(__FUNCTION__, func_get_args());
}
}
/**
+152
View File
@@ -25,6 +25,9 @@
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/grade/grading/tests/fixtures/marking_guide.php');
use \core_privacy\tests\provider_testcase;
use \core_privacy\local\request\approved_contextlist;
use \core_privacy\local\request\transform;
@@ -243,6 +246,155 @@ class core_grading_privacy_testcase extends provider_testcase {
$this->assertCount(2, $DB->get_records('grading_definitions'));
}
/**
* Test exporting user data relating to an item ID.
*/
public function test_export_item_data() {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$module = $this->getDataGenerator()->create_module('assign', ['course' => $course]);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$modulecontext = context_module::instance($module->cmid);
$guide = new test_guide($modulecontext, 'testrubrib', 'Description text');
$guide->add_criteria(
'Spelling mistakes',
'Full marks will be given for no spelling mistakes.',
'Deduct 5 points per spelling mistake made.',
25
);
$guide->add_criteria(
'Pictures',
'Full marks will be given for including 3 pictures.',
'Give 5 points for each picture present',
15
);
$guide->create_guide();
$controller = $guide->manager->get_controller('guide');
// In the situation of mod_assign this would be the id from assign_grades.
$itemid = 1;
$instance = $controller->create_instance($user->id, $itemid);
// I need the ids for the criteria and there doesn't seem to be a nice method to get it.
$criteria = $DB->get_records('gradingform_guide_criteria');
$data = ['criteria' => []];
foreach ($criteria as $key => $value) {
if ($value->shortname == 'Spelling mistakes') {
$data['criteria'][$key]['remark'] = 'This user made several mistakes.';
$data['criteria'][$key]['remarkformat'] = 0;
$data['criteria'][$key]['score'] = 5;
} else {
$data['criteria'][$key]['remark'] = 'This user has two pictures.';
$data['criteria'][$key]['remarkformat'] = 0;
$data['criteria'][$key]['score'] = 10;
}
}
$data['itemid'] = $itemid;
// Update this instance with data.
$instance->update($data);
$instanceid = $instance->get_data('id');
provider::export_item_data($modulecontext, $itemid, ['Test']);
$data = (array) writer::with_context($modulecontext)->get_data(['Test', 'Marking guide', $instanceid]);
$this->assertCount(2, $data);
$this->assertEquals('This user made several mistakes.', $data['Spelling mistakes']->remark);
$this->assertEquals(5, $data['Spelling mistakes']->score);
$this->assertEquals('This user has two pictures.', $data['Pictures']->remark);
$this->assertEquals(10, $data['Pictures']->score);
}
/**
* Test deleting user data related to a context and item ID.
*/
public function test_delete_instance_data() {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$module = $this->getDataGenerator()->create_module('assign', ['course' => $course]);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$modulecontext = context_module::instance($module->cmid);
$guide = new test_guide($modulecontext, 'testrubrib', 'Description text');
$guide->add_criteria(
'Spelling mistakes',
'Full marks will be given for no spelling mistakes.',
'Deduct 5 points per spelling mistake made.',
25
);
$guide->add_criteria(
'Pictures',
'Full marks will be given for including 3 pictures.',
'Give 5 points for each picture present',
15
);
$guide->create_guide();
$controller = $guide->manager->get_controller('guide');
// In the situation of mod_assign this would be the id from assign_grades.
$itemid = 1;
$instance = $controller->create_instance($user->id, $itemid);
// I need the ids for the criteria and there doesn't seem to be a nice method to get it.
$criteria = $DB->get_records('gradingform_guide_criteria');
$data = ['criteria' => []];
foreach ($criteria as $key => $value) {
if ($value->shortname == 'Spelling mistakes') {
$data['criteria'][$key]['remark'] = 'This user made several mistakes.';
$data['criteria'][$key]['remarkformat'] = 0;
$data['criteria'][$key]['score'] = 5;
} else {
$data['criteria'][$key]['remark'] = 'This user has two pictures.';
$data['criteria'][$key]['remarkformat'] = 0;
$data['criteria'][$key]['score'] = 10;
}
}
$data['itemid'] = $itemid;
// Update this instance with data.
$instance->update($data);
$itemid = 2;
$instance = $controller->create_instance($user->id, $itemid);
// I need the ids for the criteria and there doesn't seem to be a nice method to get it.
$criteria = $DB->get_records('gradingform_guide_criteria');
$data = ['criteria' => []];
foreach ($criteria as $key => $value) {
if ($value->shortname == 'Spelling mistakes') {
$data['criteria'][$key]['remark'] = 'This user made no mistakes.';
$data['criteria'][$key]['remarkformat'] = 0;
$data['criteria'][$key]['score'] = 25;
} else {
$data['criteria'][$key]['remark'] = 'This user has one pictures.';
$data['criteria'][$key]['remarkformat'] = 0;
$data['criteria'][$key]['score'] = 5;
}
}
$data['itemid'] = $itemid;
// Update this instance with data.
$instance->update($data);
// Check how many records we have in the fillings table.
$records = $DB->get_records('gradingform_guide_fillings');
$this->assertCount(4, $records);
// Let's delete one of the instances (the last one would be the easiest).
provider::delete_instance_data($modulecontext, $itemid);
$records = $DB->get_records('gradingform_guide_fillings');
$this->assertCount(2, $records);
foreach ($records as $record) {
$this->assertNotEquals($instance->get_id(), $record->instanceid);
}
// This will delete all the rest of the instances for this context.
provider::delete_instance_data($modulecontext);
$records = $DB->get_records('gradingform_guide_fillings');
$this->assertEmpty($records);
}
/**
* Helper function to setup the environment.
*

Some files were not shown because too many files have changed in this diff Show More