Compare commits

..

495 Commits

Author SHA1 Message Date
Eloy Lafuente (stronk7) d1e834866e Moodle release 3.4.6 2018-11-10 20:36:22 +01:00
Eloy Lafuente (stronk7) fc8544f5a8 weekly release 3.4.5+ 2018-11-10 12:53:19 +01:00
Eloy Lafuente (stronk7) cdb82d9b38 Merge branch 'MDL-63960-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-11-10 10:28:26 +01:00
Andrew Nicols fb14aa9291 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) b8d51a20e4 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:08 +08:00
Eloy Lafuente (stronk7) 6e3c97c71c Merge branch 'MDL-63596-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-11-10 03:51:53 +01:00
Andrew Nicols b5b4734ba0 MDL-63596 backup: Add missing include 2018-11-10 10:49:47 +08:00
Eloy Lafuente (stronk7) 14b0c3f56c Merge branch 'MDL-63596-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-11-09 19:33:05 +01:00
Damyon Wiese 6a8a4cb217 MDL-63596 backup: Deal with controller types
For privacy requests, deal with all 3 controller types.
2018-11-09 17:56:22 +08:00
Mihail Geshoski ec9b96c5b3 MDL-63596 core_backup: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-11-09 17:56:22 +08:00
Andrew Nicols a273b993e9 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:36:11 +08:00
Jun Pataleta 693929f632 Merge branch 'MDL-63867-34' of git://github.com/mickhawkins/moodle into MOODLE_34_STABLE 2018-11-09 15:29:54 +08:00
Michael Hawkins 529f4cff0e MDL-63867 tool_dataprivacy: Add site purpose checks to tasks
If a site purpose is not defined, requests cannot be processed.
2018-11-09 15:21:39 +08:00
Michael Hawkins 052d678465 MDL-63867 tool_dataprivacy: Fix context expired/unprotected user check
It now checks the system context has been defined, since that is
required for data privacy to be set up correctly, and the check
to be valid. This also fixes an error being thrown when checking
pending delete requests in cron.
2018-11-09 15:21:39 +08:00
Jun Pataleta f00facd901 Merge branch 'MDL-63946-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-11-09 13:41:21 +08:00
Andrew Nicols 2c8c29f2d5 MDL-63946 plagirism: Filename typo fix 2018-11-09 13:25:33 +08:00
Jun Pataleta ce6d64be15 Merge branch 'MDL-63924-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-11-09 11:56:10 +08:00
Jun Pataleta f08e3a0cb9 Merge branch 'MDL-63942-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-11-09 11:42:29 +08:00
Peter 6adc784d16 MDL-62415 repo_filepicker: Display search results in tree view
Disable overriding call to list all files in tree view and show search results instead
2018-11-09 10:55:10 +08:00
Andrew Nicols 054e9ecbe4 Merge branch 'MDL-63919-34' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-11-09 10:53:18 +08:00
Jun Pataleta 5190239c44 Merge branch 'MDL-62589-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-11-09 10:36:37 +08:00
Andrew Nicols e5e418338e MDL-63942 dataprivacy: Disable deleted user task by default 2018-11-09 09:39:35 +08:00
Damyon Wiese 1d56c59038 Merge branch 'MDL-63671-34-2' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-11-09 09:14:30 +08:00
Eloy Lafuente (stronk7) 8e8029a4d0 Merge branch 'MDL-62564-integration-34-1' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-11-08 16:50:29 +01:00
Andrew Nicols 22aabbbc48 MDL-63924 privacy: Add shared user providers to subsytsems 2018-11-08 21:27:47 +08:00
Andrew Nicols 18f218dc3a MDL-63924 core_privacy: Add shared_userlist provider 2018-11-08 21:27:47 +08:00
Mihail Geshoski 0c3857ca51 MDL-63671 tool_cohortroles: Limit data to the system context in provider 2018-11-08 16:07:57 +08:00
Mihail Geshoski 6ac295cd26 MDL-63919 privacy: Store the creation method of the data request 2018-11-08 14:50:22 +08:00
Mihail Geshoski 41a0a8f97f MDL-63919 privacy: Disable notifications for auto created data requests 2018-11-08 14:50:22 +08:00
Jun Pataleta d59a7b781f Merge branch 'MDL-63902-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-11-08 13:38:44 +08:00
Andrew Nicols 6be14b1ff0 MDL-62589 dataprivacy: Add ability to resubmit a request 2018-11-08 11:45:35 +08:00
Jun Pataleta ce6f4feac9 Merge branch 'MDL-63840-34' of https://github.com/lucaboesch/moodle into MOODLE_34_STABLE 2018-11-08 11:21:36 +08:00
Mihail Geshoski 42487b3544 MDL-62564 privacy: Create request for deleted users when setting enabled 2018-11-08 10:15:23 +08:00
Mihail Geshoski cef56ddf5e MDL-62564 privacy: Improve bulk deletion 2018-11-08 10:14:39 +08:00
Mihail Geshoski 28bb3a62bb MDL-62564 privacy: Add unit tests 2018-11-08 10:14:39 +08:00
Andrew Nicols 0878ecb537 MDL-63902 dataprivacy: Check course children not the course
When checking the expiry and protected state of a context, we need to do
so knowing what kind of use that context has.

If it is used in the user context, then only the user context matters.
If it is used within a course, then that child context must be checked
in relation to the course.
2018-11-08 09:13:15 +08:00
Andrew Nicols 6997ab6b25 MDL-62589 dataprivacy: Correct not in sql 2018-11-08 09:03:31 +08:00
Luca Bösch ab192796be MDL-63840 theme: block menu cog size. 2018-11-07 18:08:51 +01:00
Eloy Lafuente (stronk7) 3623efa604 Merge branch 'MDL-62294-34' of git://github.com/junpataleta/moodle into MOODLE_34_STABLE 2018-11-07 17:25:54 +01:00
Dan Marsden 2ac7cdf32a MDL-63800 mod_assign: don't show grade if not set. 2018-11-07 15:28:32 +08:00
Damyon Wiese 7fb9023bec Merge branch 'MDL-63839_34' of git://github.com/markn86/moodle into MOODLE_34_STABLE 2018-11-07 14:28:33 +08:00
Damyon Wiese ea5f2ed91d Merge branch 'MDL-63451_34' of https://github.com/peterspicer-catalyst/moodle into MOODLE_34_STABLE 2018-11-07 13:04:54 +08:00
Jun Pataleta a8e3df906d MDL-63183 auth_shibboleth: Don't render page when already logged in 2018-11-07 00:17:48 +01:00
Jun Pataleta daf0893181 MDL-63183 auth_shibboleth: Add hidden login token in guest login form 2018-11-07 00:17:48 +01:00
Damyon Wiese 8f3b93f686 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-07 00:17:48 +01:00
Eloy Lafuente (stronk7) ed2d5791ed Merge branch 'm34_MDL-63789_Search_SOLR_Fixes_Proxy_Auth' of https://github.com/scara/moodle into MOODLE_34_STABLE 2018-11-07 00:08:32 +01:00
Eloy Lafuente (stronk7) 75bcad753d weekly release 3.4.5+ 2018-11-06 19:06:41 +01:00
Eloy Lafuente (stronk7) 8f57f1940f Merge branch 'install_34_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_34_STABLE 2018-11-06 19:06:39 +01:00
Andrew Nicols bacac1e7e0 Merge branch 'MDL-63711-34-fix' of git://github.com/junpataleta/moodle into MOODLE_34_STABLE 2018-11-06 14:21:37 +08:00
Jun Pataleta 496281a0b9 MDL-63711 core_grade: Use canonicalize for checking userlist IDs 2018-11-06 14:17:09 +08:00
Andrew Nicols 4d82bfa076 Merge branch 'MDL-62563-34-3' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-11-06 10:02:15 +08:00
AMOS bot aa3befa1e1 Automatically generated installer lang files 2018-11-06 00:07:33 +00:00
David Monllao 7b7665f35f Merge branch 'MDL-63711-34' of git://github.com/abgreeve/moodle into MOODLE_34_STABLE 2018-11-05 16:35:58 +01:00
Eloy Lafuente (stronk7) 273486c49e Merge branch 'MDL-63748-34' of git://github.com/jleyva/moodle into MOODLE_34_STABLE 2018-11-05 13:11:59 +01:00
David Monllao d85a4a9900 Merge branch 'MDL-63764-34' of git://github.com/mickhawkins/moodle into MOODLE_34_STABLE 2018-11-05 12:46:42 +01:00
David Monllao 0b62f4608f Merge branch 'MDL-63700-34' of git://github.com/mickhawkins/moodle into MOODLE_34_STABLE 2018-11-05 11:14:04 +01:00
Mihail Geshoski 2115bda424 MDL-62563 privacy: Create delete data request for existing deleted users 2018-11-05 16:25:47 +08:00
Mihail Geshoski 7db764172f MDL-62563 privacy: Add unit tests 2018-11-05 16:22:15 +08:00
Andrew Nicols 9faabb3400 Merge branch 'MDL-63689-34-workshopprivacy' of git://github.com/mudrd8mz/moodle into MOODLE_34_STABLE 2018-11-05 15:01:15 +08:00
Damyon Wiese a168c3ed17 Merge branch 'MDL-63717-34' of git://github.com/jleyva/moodle into MOODLE_34_STABLE 2018-11-05 13:45:58 +08:00
Andrew Nicols 41a03384f7 Merge branch 'MDL-63564-34' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-11-05 13:23:41 +08:00
Mark Nelson ba27222184 MDL-63839 core_message: use component name for Behat tag 2018-11-05 10:36:51 +08:00
Andrew Nicols c80fbc1762 Merge branch 'MDL-63712-34' of git://github.com/mickhawkins/moodle into MOODLE_34_STABLE 2018-11-05 09:34:55 +08:00
Eloy Lafuente (stronk7) 1955206566 Merge branch 'MDL-63818-34-2' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-11-05 00:03:25 +01:00
Matteo Scaramuccia 3899e71bf2 MDL-63789 search_solr: use proxy creds, if any
Also use proxy port, IF any.
2018-11-02 23:35:37 +01:00
Eloy Lafuente (stronk7) ea40b6fb60 weekly release 3.4.5+ 2018-11-02 21:08:34 +01:00
Adrian Greeve e606b429f3 MDL-63711 core_grades: Delete data for specified users in a context. 2018-11-03 03:04:18 +08:00
Juan Leyva 3d4bef51fa MDL-63748 core_auth: Fix redirect parameter being ignored 2018-11-02 15:25:33 +01:00
Juan Leyva 95733c47af MDL-63717 tool_mobile: Ensure URLs are valid
We need to clean the URLs returned by the get_public_config WS because some old plugins didn't validate the stored data.
2018-11-02 14:35:41 +01:00
Mihail Geshoski 030cc62697 MDL-63564 core_question: Avoid using UNION in get_users_in_context() 2018-11-02 14:40:53 +08:00
Shamim Rezaie 465256bf63 MDL-63564 mod_quiz: Added delete_quizaccess_data_for_users to polyfill
This issue is a part of the MDL-62560 Epic.
2018-11-02 14:40:52 +08:00
Shamim Rezaie 80ea64da26 MDL-63564 mod_quiz: support removal of multiple users in a context
This issue is a part of the MDL-62560 Epic.
2018-11-02 14:40:52 +08:00
Shamim Rezaie 92efc5c352 MDL-63564 mod_quiz: Added quizaccess_user_provider interface
This issue is a part of the MDL-62560 Epic.
2018-11-02 14:40:52 +08:00
Shamim Rezaie 53ccb0ac47 MDL-63564 question: Add helper to fetch users in context
This issue is a part of the MDL-62560 Epic.
2018-11-02 14:40:52 +08:00
Shamim Rezaie a04d18dbaa MDL-63564 core_question: support removal of multiple users in a context
This issue is a part of the MDL-62560 Epic.
2018-11-02 14:40:51 +08:00
Andrew Nicols 7453948947 Merge branch 'MDL-63701-34-1' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-11-02 12:03:51 +08:00
Andrew Nicols f7f4e3022c Merge branch 'MDL-63666-34' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-11-02 10:53:13 +08:00
Mihail Geshoski 3725fa78d4 MDL-63666 logging: Rename delete_data_for_all_users() function 2018-11-02 09:42:32 +08:00
Adrian Greeve d86001b150 MDL-63666 logging: Add support for removal of context users.
This creates a new interface for the logging subplugins to
implement for the users to be deleted separately.
2018-11-02 09:42:32 +08:00
Andrew Nicols 99f56b6847 Merge branch 'MDL-63703-34-1' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-11-02 09:01:00 +08:00
Andrew Nicols d0851ea944 MDL-63702 core_block: Correct phpdoc for get_users 2018-11-02 08:35:07 +08:00
Andrew Nicols a6589f69f5 MDL-63422 blog: Correct phpdoc for get_users 2018-11-02 08:35:07 +08:00
Andrew Nicols 18a1d5100f Merge branch 'MDL-63816-34' of git://github.com/mickhawkins/moodle into MOODLE_34_STABLE 2018-11-02 08:01:06 +08:00
Eloy Lafuente (stronk7) b5d14e08a2 Merge branch 'MDL-63746-34' of git://github.com/damyon/moodle into MOODLE_34_STABLE 2018-11-01 15:38:48 +01:00
Andrew Nicols 036efee8b5 MDL-63818 core: Add all relevant module context caps
This change ensures that all capabilities at module context level are
shown within a module, only filtering out those which belong to another
module or the subplugins of another module.

This has the effect that module-level capabilities are included
providing they relate to
- the module being queried
- any subplugin of that module
- any capability at module level which does not belong to another module
  or its subplugin
- any other explicitly included capability

This allows permissions to be defined on a capability at a context
level.
2018-11-01 20:52:20 +08:00
Michael Hawkins 9898c108a3 MDL-63712 core_message: Unit test improvements for fetching context/user
This issue is part of the MDL-62560 Epic.
2018-11-01 16:55:16 +08:00
Michael Hawkins a05d4df146 MDL-63712 core_message: Minor provider improvements
This issue is part of the MDL-62560 Epic.
2018-11-01 16:13:49 +08:00
Shamim Rezaie 23d4767f8c MDL-63712 core_message: support removal of multiple users in a context
This issue is a part of the MDL-62560 Epic.
2018-11-01 14:52:28 +08:00
Shamim Rezaie 734d9f403b MDL-63712 core_message: Data should be in user context, not system
This issue is a part of the MDL-62560 Epic.
2018-11-01 14:52:18 +08:00
Mihail Geshoski 708dd97405 MDL-63701 editor_atto: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-11-01 14:21:26 +08:00
Mihail Geshoski dfc0106255 MDL-63703 core_role: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-11-01 12:14:07 +08:00
Damyon Wiese 39e707450a MDL-63746 boost: Dropdown menu styles
Show focus outlines when a menu button has focus.

Remove btn styling from menu buttons as the additional padding makes things not line up.
2018-11-01 11:07:40 +08:00
Andrew Nicols 850050ed87 Merge branch 'MDL-63566-34' of git://github.com/mickhawkins/moodle into MOODLE_34_STABLE 2018-11-01 10:16:32 +08:00
Adrian Greeve 63ca7583d6 MDL-63566 tool_dataprivacy: New interface check for userlist.
This adds another check to see if the plugins are implementing the
new core_userlist_provider.
2018-11-01 10:09:17 +08:00
Jun Pataleta bf81ba3456 Merge branch 'MDL-63826_34' of git://github.com/stronk7/moodle into MOODLE_34_STABLE 2018-11-01 09:28:13 +08:00
Eloy Lafuente (stronk7) 6a4e92da5c MDL-63826 tests: Don't rely on random array keys for testing 2018-11-01 00:34:55 +01:00
Andrew Nicols 7e163a6473 Merge branch 'm34_MDL-63727_Wrong_SCO_Launched_Link' of https://github.com/scara/moodle into MOODLE_34_STABLE 2018-11-01 07:02:36 +08:00
Matteo Scaramuccia 925c732053 MDL-63727 mod_scorm: Fixed typo in test_launch_sco() 2018-10-31 22:40:56 +01:00
Davo Smith 035c8b349c MDL-63727 mod_scorm: fix typo in sco_launched URL 2018-10-31 22:40:56 +01:00
David Monllao 39ce011f9d Merge branch 'MDL-63726-34' of https://github.com/lucaboesch/moodle into MOODLE_34_STABLE 2018-10-31 10:22:21 +01:00
Luca Bösch 4f32e3af8c MDL-63726 tool_dataprivacy: En-/disable 'Data retention summary' link 2018-10-31 09:10:40 +01:00
Andrew Nicols ae7c8cd2e0 Merge branch 'MDL-63684-34-1' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-10-31 15:18:09 +08:00
Mihail Geshoski d57c7e7c94 MDL-63684 core_calendar: Fix test_get_contexts_for_userid() test
This issue is part of the MDL-62560 Epic.
2018-10-31 14:12:20 +08:00
Mihail Geshoski 9b669f6787 MDL-63684 core_calendar: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-31 14:12:20 +08:00
Andrew Nicols 0cdf08265a Merge branch 'MDL-63702-34' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-10-31 14:07:48 +08:00
Michael Hawkins 684740b24e MDL-63700 core_analytics: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-31 13:48:33 +08:00
David Monllao a2890d9301 Merge branch 'MDL-62601-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-10-31 06:47:48 +01:00
Andrew Nicols e2f33953c8 MDL-62601 core_privacy: Ensure providers can handle deleted users 2018-10-31 13:45:00 +08:00
Andrew Nicols 534842a3a1 MDL-62601 editor_atto: Do not use context_user 2018-10-31 13:45:00 +08:00
Jun Pataleta e5a82f8e18 Merge branch 'MDL-63690-34-integration' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-10-31 12:52:09 +08:00
Michael Hawkins 646de1f8a8 MDL-63816 privacy: Corrected provider get_users_in_context descriptions 2018-10-31 12:14:41 +08:00
Michael Hawkins 51c8b3543f MDL-63816 privacy: Corrected interface get_users_in_context description 2018-10-31 12:14:41 +08:00
Mihail Geshoski 7aed850593 MDL-63690 core_blog: Handle when user blog posts are not being returned 2018-10-31 11:51:47 +08:00
Jun Pataleta ba6125b60f Merge branch 'MDL-60897-34' of git://github.com/junpataleta/moodle into MOODLE_34_STABLE 2018-10-31 11:33:54 +08:00
Andrew Nicols 3f557f8beb Merge branch 'MDL-63624-34' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-10-31 11:22:29 +08:00
Mihail Geshoski 589bfad95e MDL-63624 tool_cohortroles: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-31 11:09:56 +08:00
Adrian Greeve 69e66bf86f MDL-63702 core_block: Update to use new userlist interface.
This allows user data to be deleted on a role basis.
2018-10-31 10:05:34 +08:00
Jun Pataleta 7a132e9418 MDL-60897 qtype_multianswer: Code style fixes
And Behat steps improvement, too!
2018-10-31 09:57:18 +08:00
Jean-Michel Vedrine 0a76c61d4e MDL-60897 qtype_multianswer: Invalid cloze-questions saved to db 2018-10-31 09:52:41 +08:00
Andrew Nicols 45def3e813 Merge branch 'MDL-63530-34-2' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-10-31 09:22:46 +08:00
Andrew Nicols 8c75fc2dd7 Merge branch 'MDL-63606-34-2' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-10-31 08:56:52 +08:00
Andrew Nicols f47eb4322a Merge branch 'MDL-63690-34' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-10-31 08:28:08 +08:00
Eloy Lafuente (stronk7) 4b681328f3 weekly release 3.4.5+ 2018-10-30 18:24:03 +01:00
David Mudrák 2ca0f3f174 MDL-63689 workshop: Add support for removal of multiple context users
The mod_workshop\privacy\provider now implements the new
core_userlist_provider interface. Deleting (erasing) data follows the
same principles as in other existing methods - we do not actually delete
data that might affect other users. Instead, we only erase the sensitive
owned personal information such as provided content.
2018-10-30 14:35:45 +01:00
Mihail Geshoski c2a596ba08 MDL-63606 core_badges: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-30 16:42:36 +08:00
Mihail Geshoski 66ec209ac6 MDL-63530 core_notes: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-30 16:19:40 +08:00
Andrew Nicols d2f8852662 Merge branch 'MDL-63634-34' of git://github.com/rezaies/moodle into MOODLE_34_STABLE 2018-10-30 12:05:58 +08:00
Andrew Nicols 1873af51cc Merge branch 'MDL-63626-34' of git://github.com/rezaies/moodle into MOODLE_34_STABLE 2018-10-30 10:59:54 +08:00
Jun Pataleta f54f0adc7f Merge branch 'MDL-63531-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-10-30 10:50:39 +08:00
Adrian Greeve 3a0288dfef MDL-63531 privacy: Update to comments provider to simplfy sql.
This updates the get_users_in_context_from_sql query to include
the context id to simplify the calls made from other components.
2018-10-30 09:47:14 +08:00
Adrian Greeve 45bcb6111e MDL-63531 assignfeedback: Update to use the new privacy interface.
This now uses the new interface to allow the deletion of users based
on a context.
2018-10-30 09:47:14 +08:00
Adrian Greeve cc678d7593 MDL-63531 assignsubmission: Update to use the new interface.
This updates the plugins to use the new inteface for deleting
data for users in a context.
2018-10-30 09:47:14 +08:00
Andrew Nicols 26babcb57b Merge branch 'MDL-63656-34' of git://github.com/mickhawkins/moodle into MOODLE_34_STABLE 2018-10-30 09:36:14 +08:00
Adrian Greeve da8d77b521 MDL-63531 mod_assign: Update mod assign to use new interface.
This introduces a new interface for assign sub-plugins and
updates the mod_assign provider to implement the new general
interface for deleting data for users in a context.
2018-10-30 08:48:07 +08:00
Eloy Lafuente (stronk7) 572bd2fb5e Merge branch 'MDL-63730-34-enfix' of git://github.com/mudrd8mz/moodle into MOODLE_34_STABLE 2018-10-29 13:19:41 +01:00
David Monllao 4ab60c91f4 Merge branch 'MDL-63528-34' of git://github.com/junpataleta/moodle into MOODLE_34_STABLE 2018-10-29 11:18:32 +01:00
Michael Hawkins ed40358c90 MDL-63656 tool_messageinbound: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-29 14:16:08 +08:00
Michael Hawkins 99eefdd8b2 MDL-63764 core_competency: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-29 13:03:13 +08:00
Michael Hawkins b463e023b2 MDL-63764 core_competency: Support bulk user delete course competencies
This issue is a part of the MDL-62560 Epic.
Also made user IDs optional, so user course competencies can be deleted by course ID only.
2018-10-29 13:03:13 +08:00
Jun Pataleta 747e8433e5 Merge branch 'MDL-63622-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-10-29 11:43:45 +08:00
Adrian Greeve 793f109279 MDL-63622 core_grading: Add support for removal of context users.
Core grading now implements the new core_userlist_provider
interface to allow for deletion of specific users in a context.
2018-10-29 09:20:37 +08:00
Andrew Nicols 7ce597a687 Merge branch 'MDL-63665-34' of git://github.com/rezaies/moodle into MOODLE_34_STABLE 2018-10-29 09:15:23 +08:00
Andrew Nicols 30c5170ec2 Merge branch 'MDL-63680-34' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-10-29 08:40:57 +08:00
Andrew Nicols 65c19b684c Merge branch 'MDL-63267-34' of git://github.com/lameze/moodle into MOODLE_34_STABLE 2018-10-27 19:22:18 +08:00
Tim Lock e250be6d66 MDL-63267 tool_privacy: Handle modules without privacy support. 2018-10-26 18:33:01 -04:00
Adrian Greeve d7ac44446e MDL-63267 grading methods: Unit test update for fix. 2018-10-26 18:33:01 -04:00
Adrian Greeve 47e0ed7b3e 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:33:01 -04:00
Adrian Greeve f6020ec3aa MDL-63267 tool_dataprivacy: Registry shows deprecated interfaces. 2018-10-26 18:33:01 -04:00
Adrian Greeve a97a654851 MDL-63267 privacy: Create deprecated provider interface.
Any interfaces that have become deprecated should extend
this interface.
2018-10-26 18:33:01 -04:00
Adrian Greeve 8f31f87f2f MDL-63267 core_grading: Unit test for new methods. 2018-10-26 18:33:01 -04:00
Adrian Greeve 327707245c MDL-63267 mod_assign: Update to providers to include adv grading. 2018-10-26 18:33:01 -04:00
Adrian Greeve 152a5227b9 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:33:01 -04:00
Eloy Lafuente (stronk7) a8c87c20e9 weekly release 3.4.5+ 2018-10-26 18:05:04 +02:00
David Mudrák 54944a93f7 MDL-63730 lang: Update Behat scenarios to use the new strings 2018-10-26 14:02:01 +02:00
Helen Foster 82047d18a6 MDL-63730 lang: Merge English strings from the en_fix language pack
Significant string changes:

* withselectedusers_help, core - removing mention of 'Add a common note'
  as the option no longer exists
* considereddigitalminor and digitalminor_desc, core - more
  child-friendly wording
* auth_dbtype, auth_db - fixed broken link
2018-10-26 14:00:43 +02:00
Shamim Rezaie 0eb0612886 MDL-63634 enrol_paypal: Improve query performance
This issue is a part of the MDL-62560 Epic.
2018-10-26 19:36:56 +11:00
Shamim Rezaie 9109e547ae MDL-63634 enrol_paypal: support removal of multiple users in a context
This issue is a part of the MDL-62560 Epic.
2018-10-26 19:36:56 +11:00
Shamim Rezaie ded33b32cc MDL-63634 enrol_paypal: Fix a typo in unit tests
This issue is a part of the MDL-62560 Epic.
2018-10-26 19:36:56 +11:00
Eloy Lafuente (stronk7) 48bc997f74 Merge branch 'MDL-63765-34' of git://github.com/damyon/moodle into MOODLE_34_STABLE 2018-10-26 10:22:49 +02:00
Damyon Wiese 7fa74a2020 MDL-63765 behat: Fix behat tests opening a menu
If javascript is disabled, we don't need to open the menus at all. If it is enabled
we should use the custom step where possible.

In some tests (user profile) we have to use link_or_button because
the element that opens the menu will be a link or a button in different themes.
2018-10-26 16:15:02 +08:00
Shamim Rezaie 821a5d9efb MDL-63665 mod_choice: no exception when course_modules doesn't exist
This issue is a part of the MDL-62560 Epic.
2018-10-26 18:49:51 +11:00
Shamim Rezaie 89c6cddff5 MDL-63665 mod_choice: support removal of multiple users in a context
This issue is a part of the MDL-62560 Epic.
2018-10-26 18:49:51 +11:00
Mihail Geshoski 05aca6a266 MDL-63680 report_stats: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-26 13:09:43 +08:00
Andrew Nicols 323c407874 Merge branch 'MDL-63673-34' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-10-26 11:42:29 +08:00
Andrew Nicols 0ada140505 Merge branch 'MDL-63682-34' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-10-26 11:03:46 +08:00
Mihail Geshoski e936dd2040 MDL-63682 mnetservice_enrol: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-26 10:24:50 +08:00
Andrew Nicols d505813ce6 Merge branch 'MDL-63688-34-1' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-10-26 10:17:53 +08:00
Mihail Geshoski f41f2d53cf MDL-63690 core_privacy: Handle when add_users() receives an empty array 2018-10-26 09:28:03 +08:00
Adrian Greeve f32c037c7a MDL-63690 core_blog: Update providers for userlist deletion. 2018-10-26 09:27:44 +08:00
Eloy Lafuente (stronk7) f2d07ac911 Merge branch 'MDL-62904-34' of git://github.com/junpataleta/moodle into MOODLE_34_STABLE 2018-10-26 00:04:01 +02:00
Andrew Nicols 01189bf5a4 Merge branch 'MDL-63552-34' of git://github.com/mickhawkins/moodle into MOODLE_34_STABLE 2018-10-25 15:45:31 +08:00
Andrew Nicols 632aac365f Merge branch 'MDL-63679-34' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-10-25 15:36:00 +08:00
Andrew Nicols fe3a63d8a4 Merge branch 'MDL-63655_34' of git://github.com/stronk7/moodle into MOODLE_34_STABLE 2018-10-25 14:57:37 +08:00
Michael Hawkins d22b87614d MDL-63552 search_solr: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-25 11:56:38 +08:00
Eloy Lafuente (stronk7) 6522b3d418 Merge branch 'MDL-62145-34' of git://github.com/damyon/moodle into MOODLE_34_STABLE 2018-10-25 01:35:30 +02:00
Eloy Lafuente (stronk7) 526b2cfc90 Merge branch 'MDL-63714-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-10-25 00:11:09 +02:00
Mihail Geshoski 81fa52d9ca MDL-63688 core_tag: Add method that returns users in context
This issue is part of the MDL-62560 Epic.
2018-10-24 09:28:11 +08:00
Andrew Nicols bcbc20b6d9 MDL-63619 tool_dataprivacy: Cache purposes in form 2018-10-24 08:49:43 +08:00
Andrew Nicols 7646385cdc MDL-63619 tool_dataprivacy: Performance improvement 2018-10-24 08:49:43 +08:00
Andrew Nicols 737a2f46ce 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:49:43 +08:00
Andrew Nicols 98e2aa1669 MDL-63714 javascript: Add new core/pending module 2018-10-24 08:44:06 +08:00
Andrew Nicols 5be7156473 MDL-63714 core: Wrap doRender in pendingjs 2018-10-24 08:44:06 +08:00
Andrew Nicols 4e30dbbef2 MDL-63714 javascript: Improve docs for pendingjs 2018-10-24 08:44:06 +08:00
Mihail Geshoski b36794f0f8 MDL-63673 fileconverter_googledrive: Support removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-24 07:04:50 +08:00
David Monllao 7fff8f46e8 weekly release 3.4.5+ 2018-10-23 16:39:23 +02:00
David Monllao 22f9a36ead Merge branch 'install_34_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_34_STABLE 2018-10-23 16:39:22 +02:00
David Monllao 50d6a67262 Merge branch 'MDL-62139-34-fix3' of git://github.com/damyon/moodle into MOODLE_34_STABLE 2018-10-23 11:17:24 +02:00
Damyon Wiese 1d97b8af8a MDL-62139 behat: Invalid test
If javascript is disabled, we can't open the menu.
2018-10-23 16:35:03 +08:00
Damyon Wiese e93868b26e MDL-62139 output: Revert template changes
Revert accessibility changes for menus in standard themes.
2018-10-23 10:15:36 +02:00
Damyon Wiese c47dbbbd5d MDL-62139 output: Revert template changes
Revert accessibility changes for menus in standard themes.
2018-10-23 15:50:23 +08:00
Mihail Geshoski 73069b9b59 MDL-63679 ltiservice_memberships: Support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-23 13:37:05 +08:00
Andrew Nicols 65b16113c0 MDL-63657 completion: Coding style fix 2018-10-23 11:33:23 +08:00
Andrew Nicols a2572fb75c Merge branch 'MDL-63668-34' of git://github.com/mickhawkins/moodle into MOODLE_34_STABLE 2018-10-23 11:33:08 +08:00
Andrew Nicols 6d3187bec4 MDL-63657 core_files: Coding style fix 2018-10-23 11:27:56 +08:00
Andrew Nicols b25a9d2ce0 Merge branch 'MDL-63640-34' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-10-23 11:27:48 +08:00
Andrew Nicols 6e27e3201f MDL-63657 tool_mobile: Coding style fix 2018-10-23 11:22:43 +08:00
Andrew Nicols 7448bba870 Merge branch 'MDL-63657-34' of git://github.com/mickhawkins/moodle into MOODLE_34_STABLE 2018-10-23 11:22:25 +08:00
Andrew Nicols a83e5b8f03 MDL-63535 core_rss: Coding style fix 2018-10-23 11:17:53 +08:00
Andrew Nicols b3e8985ce9 Merge branch 'MDL-63535-34' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-10-23 11:17:11 +08:00
Michael Hawkins 61d6025b0e MDL-63657 tool_mobile: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-23 11:02:24 +08:00
Andrew Nicols d5ec4f8378 MDL-63664 tool_policy: Whitespace fix 2018-10-23 10:53:57 +08:00
Andrew Nicols c90f269158 Merge branch 'MDL-63664-34' of git://github.com/mickhawkins/moodle into MOODLE_34_STABLE 2018-10-23 10:52:04 +08:00
Michael Hawkins f2423ec4df MDL-63668 core_course: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-23 10:45:29 +08:00
Michael Hawkins ff3a01cdac MDL-63668 core_completion: Add get and delete by approved userlist.
This issue is a part of the MDL-62560 Epic.
2018-10-23 10:45:29 +08:00
Michael Hawkins 13bcb76d5a MDL-63664 tool_policy: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-23 10:40:13 +08:00
Mihail Geshoski 1ad86e27d0 MDL-63535 core_userkey: Add method that returns users in context
This issue is part of the MDL-62560 Epic.
2018-10-23 10:29:50 +08:00
Mihail Geshoski 8fbb8bc0fd MDL-63535 core_rss: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-23 10:29:50 +08:00
Andrew Nicols 83edd966a1 Merge branch 'MDL-63649-34' of git://github.com/rezaies/moodle into MOODLE_34_STABLE 2018-10-23 10:19:40 +08:00
Mihail Geshoski 5ee7ec1b9c MDL-63640 core_files: Add method that returns users in context
This issue is part of the MDL-62560 Epic.
2018-10-23 10:12:12 +08:00
Andrew Nicols b3ad855ded Merge branch 'MDL-63713-34' of git://github.com/rezaies/moodle into MOODLE_34_STABLE 2018-10-23 10:09:32 +08:00
Shamim Rezaie e5a542a73e MDL-63649 enrol_lti: support removal of multiple users in a context
This issue is a part of the MDL-62560 Epic.
2018-10-23 13:06:46 +11:00
Shamim Rezaie 991aa49b7b MDL-63713 core: Support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-23 13:05:49 +11:00
Andrew Nicols a9564af46a MDL-62560 tool_dataprivacy: Behat fix for protected check 2018-10-23 09:21:56 +08:00
David Monllao 1378ac0662 Merge branch 'MDL-62144-34' of git://github.com/damyon/moodle into MOODLE_34_STABLE 2018-10-22 16:35:33 +02:00
David Monllao 10dfab520c Merge branch 'MDL-61052-34' of git://github.com/damyon/moodle into MOODLE_34_STABLE 2018-10-22 14:37:40 +02:00
David Monllao 6118d537c3 Merge branch 'MDL-62560-34' into MOODLE_34_STABLE 2018-10-22 13:01:36 +02:00
Andrew Nicols 674b262d9e MDL-62560 tool_dataprivacy: Add a purpose override cache 2018-10-22 13:01:18 +02:00
Andrew Nicols b8fa9168ba MDL-62560 tool_dataprivacy: Fetch user purpose once 2018-10-22 13:01:17 +02:00
Andrew Nicols aefebb9f84 MDL-62560 tool_dataprivacy: Add status logging 2018-10-22 13:01:17 +02:00
Andrew Nicols dc59b9475d MDL-62560 tool_dataprivacy: Add unit test for filtered userlist 2018-10-22 13:01:17 +02:00
Mihail Geshoski a4cca8f5e3 MDL-63636 block_recent_activity: Support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:01:16 +02:00
Mihail Geshoski 023ff07502 MDL-63621 core_cohort: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:01:16 +02:00
Mihail Geshoski 8f71325a00 MDL-63633 block_comments: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:01:15 +02:00
Mihail Geshoski 9a1fd38274 MDL-63635 block_community: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:01:15 +02:00
Mihail Geshoski 8bae3f76d8 MDL-63593 auth_mnet: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:01:14 +02:00
Mihail Geshoski 6882588c8f MDL-63592 auth_oauth2: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:01:14 +02:00
Mihail Geshoski 6372d0c328 MDL-63637 block_rss_client: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:01:13 +02:00
Mihail Geshoski f7c431b2a5 MDL-63638 block_tag_flickr: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:01:13 +02:00
Mihail Geshoski f359a73542 MDL-63639 core_fileconverter: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:01:12 +02:00
Shamim Rezaie fd239233c7 MDL-63647 enrol_flatfile: Improving existing unit tests
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:01:12 +02:00
Shamim Rezaie 88e2805888 MDL-63647 enrol_flatfile: support removal of multiple users in context
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:01:11 +02:00
Michael Hawkins c1d856ed28 MDL-63659 tool_monitor: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:01:11 +02:00
Mihail Geshoski fe3c821528 MDL-63615 message_airnotifier: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:01:10 +02:00
Mihail Geshoski 23d749161a MDL-63616 message_email: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:01:10 +02:00
Mihail Geshoski f335392b2f MDL-63617 message_jabber: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:01:10 +02:00
Adrian Greeve 5f136c284a MDL-63618 core_plagiarism: Added support to remove users in a context.
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:34 +02:00
Mihail Geshoski fdef31e3c5 MDL-63572 profilefield_checkbox: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:34 +02:00
Mihail Geshoski a33f0b6b18 MDL-63575 profilefield_datetime: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:33 +02:00
Mihail Geshoski 5b71e0236f MDL-63586 profilefield_menu: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:33 +02:00
Mihail Geshoski 376c39da69 MDL-63587 profilefield_text: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:32 +02:00
Mihail Geshoski 68eabea2f9 MDL-63588 profilefield_textarea: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:32 +02:00
Michael Hawkins fcdb29da6c MDL-63538 repository_onedrive: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:31 +02:00
Michael Hawkins 6840d1b424 MDL-63538 repository_youtube: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:31 +02:00
Michael Hawkins 20e16adcde MDL-63538 repository_wikimedia: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:30 +02:00
Michael Hawkins 9d8e74cc4a MDL-63538 repository_picasa: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:30 +02:00
Michael Hawkins edd54f38c3 MDL-63538 repository_merlot: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:30 +02:00
Michael Hawkins 6f0f9e9816 MDL-63538 repository_googledocs: Support removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:29 +02:00
Michael Hawkins 0967aa3fde MDL-63538 repository_flickr_public: Support removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:29 +02:00
Michael Hawkins fbfb768f07 MDL-63538 repository_flickr: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:28 +02:00
Michael Hawkins f4b06c4fb3 MDL-63538 repository_dropbox: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:28 +02:00
Michael Hawkins 19f8412015 MDL-63538 repository_boxnet: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:28 +02:00
Mihail Geshoski 7e6bcd9424 MDL-63536 core_repository: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:27 +02:00
Mihail Geshoski 4db5681054 MDL-63534 core_user: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:27 +02:00
Mihail Geshoski 4a973080dd MDL-63532 core_portfolio: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:26 +02:00
Mihail Geshoski 85439fc637 MDL-63529 core_enrol: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:26 +02:00
Michael Hawkins 1b8dd3096e MDL-63514 mod_wiki: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:26 +02:00
Michael Hawkins 461b388358 MDL-63510 mod_survey: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:25 +02:00
Michael Hawkins 7de1d37f80 MDL-63501 mod_scorm: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:25 +02:00
Michael Hawkins 200916ba98 MDL-63497 mod_lesson: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:24 +02:00
Michael Hawkins c79943ca14 MDL-63497 mod_feedback: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:24 +02:00
Michael Hawkins ebbf35d9a2 MDL-63497 mod_glossary: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
Also added missing ratings include and test to mod_glossary unit tests.
2018-10-22 13:00:24 +02:00
Michael Hawkins c445946955 MDL-63497 mod_data: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:23 +02:00
Michael Hawkins 2642039f65 MDL-63497 block_html: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:23 +02:00
Michael Hawkins 9e96c5292a MDL-63497 mod_chat: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:22 +02:00
Shamim Rezaie 518ebb3323 MDL-63554 cachestore_memcache: Support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:22 +02:00
Shamim Rezaie 1335e0bc3e MDL-63554 cachestore_session: Support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:22 +02:00
Shamim Rezaie 6ea63bc54c MDL-63554 cachestore_redis: Support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:21 +02:00
Shamim Rezaie e3b6fe667d MDL-63554 cachestore_mongodb: Support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:21 +02:00
Shamim Rezaie 699847af7b MDL-63554 cachestore_memcached: Support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:20 +02:00
Shamim Rezaie 5a49fe3f51 MDL-63500 enrol_cohort: refactoring get_contexts_for_userid
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:20 +02:00
Shamim Rezaie 3fcb71bd53 MDL-63500 enrol_meta: refactoring get_contexts_for_userid
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:19 +02:00
Shamim Rezaie 3afb545307 MDL-63500 core_group: get_contexts_for_userid should respect component
The get_contexts_for_userid method should not return contexts for all group memberships.
It should only return the contexts in where there is manual group
membership.

This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:19 +02:00
Shamim Rezaie cfe3ba39e8 MDL-63500 enrol_cohort: Support for removal of multiple context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:19 +02:00
Shamim Rezaie 1b0fc6098e MDL-63500 enrol_meta: Support for removal of multiple context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:18 +02:00
Shamim Rezaie 8f30bd5945 MDL-63500 core_group: support removal of multiple users in a context
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:18 +02:00
Mihail Geshoski 181da22a4d MDL-63533 core_webservice: Unit tests for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:17 +02:00
Mihail Geshoski 549232ade8 MDL-63533 core_webservice: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:17 +02:00
Adrian Greeve 7a5ad3c9f1 MDL-63513 mod_assignment: Add removal of context users.
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:17 +02:00
Michael Hawkins 1b78edeb44 MDL-63498 mod_lti: Add support for removal of context users
This issue is part of the MDL-62560 Epic.
2018-10-22 13:00:16 +02:00
Michael Hawkins 37dffc7b4d MDL-63495 comment: Added provider::get_users_in_context_from_sql
This issue is a part of the MDL-62560 Epic.
2018-10-22 13:00:07 +02:00
Damyon Wiese 1ec78d3549 MDL-61052 assign: Do not queue conversions
Identical submissions do not require individual conversions from the document converter. Text passing through
format_text cannot be trusted and is likely to be unique each time, even for the same text.
2018-10-22 12:03:40 +08:00
Zig Tan 3b34e951b9 MDL-61052 assign: Improve strip_images() to avoid HTML5 tags errors 2018-10-22 12:02:51 +08:00
Damyon Wiese 8be7d9a52f MDL-62145 theme_boost: Custom menu fixes
Make sure the site navigation element is labelled, actionmenu items
have correct role and actionmenu trigger controls the menu.
2018-10-22 11:05:28 +08:00
Damyon Wiese 4fa885d003 MDL-62144 user menu: Accessibility
Ensure user menu passes the accessibility tests.
2018-10-22 10:50:55 +08:00
Damyon Wiese 5cccea615a MDL-62139 output: Accessible action menus
Add label on the "gear" menu and default keyboard and focus controls.

S
2018-10-22 10:39:11 +08:00
Eloy Lafuente (stronk7) bb1e0fdbb9 weekly release 3.4.5+ 2018-10-22 10:39:11 +08:00
AMOS bot b859bed2eb Automatically generated installer lang files 2018-10-22 10:39:11 +08:00
AMOS bot 5f69056fe5 Automatically generated installer lang files 2018-10-22 10:39:11 +08:00
AMOS bot e670975845 Automatically generated installer lang files 2018-10-22 00:07:28 +00:00
Eloy Lafuente (stronk7) c757afbac1 weekly release 3.4.5+ 2018-10-19 17:10:52 +02:00
Eloy Lafuente (stronk7) a28eccf172 Merge branch 'install_34_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_34_STABLE 2018-10-19 17:10:50 +02:00
AMOS bot 532bd1c768 Automatically generated installer lang files 2018-10-19 00:08:19 +00:00
Eloy Lafuente (stronk7) da1507c1dd Merge branch 'MDL-62920_34' of https://github.com/aanabit/moodle into MOODLE_34_STABLE 2018-10-18 23:52:00 +02:00
Eloy Lafuente (stronk7) 6270847e8f Merge branch 'MDL-60145-34' of https://github.com/snake/moodle into MOODLE_34_STABLE 2018-10-18 18:57:58 +02:00
Tim Hunt f777a0b0dc MDL-63403 filter_glossary: changes made during integration review 2018-10-18 18:45:38 +02:00
Tim Hunt 86543cb1db MDL-63403 filter_glossary: only create replacement HTML if needed 2018-10-18 18:44:37 +02:00
Tim Hunt 59045c0a75 MDL-63403 filter_glossary: only prepare the replacement if used 2018-10-18 18:44:32 +02:00
Tim Hunt 957e3dbbd6 MDL-63403 filter_glossary: clean up filterobject class
This is another preliminary commit, rationalising the ->work... fields
and also moving the code that sets them up into a separate function.
This is also to prepare for the next commit.
2018-10-18 18:44:26 +02:00
Tim Hunt 6f9e7f7989 MDL-63403 filter_glossary: only prepare_phrases_for_filtering once 2018-10-18 18:44:23 +02:00
Tim Hunt d6be60ed51 MDL-63403 filter_glossary: use preg features for fullmatch
This eliminates a lot of necessary string manipulation for something
that preg can just do with the \b assertion.

The change also extracts all the work to prepare ->work_phrase (renamed
->regexp) into a separate function. This is to pave the way for future
efficiency gains, but for now I have not done them, so it is easier to
verify by inspection that this commit does not change behaviour.

Also, some tidy-ups to the filterobject structure, to eliminate some
redundant fields.
2018-10-18 18:44:19 +02:00
Tim Hunt 1a710d2dd8 MDL-63403 filter_glossary: cache concept list in a MUC static cache
This is a slight improvement on a suggestion by David Monllaó
2018-10-18 18:44:15 +02:00
Tim Hunt 2e14a686ea MDL-63403 filter_glossary: Fix lang string concatenation 2018-10-18 18:44:11 +02:00
Tim Hunt 8b73e0f6fe MDL-63403 filter_glossary: $GLOSSARY_EXCLUDEENTRY shouldn't stop caching
Also, greatly improved unit tests, to test more cases of how the filter
should work.
2018-10-18 18:44:07 +02:00
Tim Hunt 6cb74724a3 MDL-63403 filterlib: Fix lots of coding style 2018-10-18 18:43:57 +02:00
Jun Pataleta 6871e90c9e Merge branch 'MDL-63667-34-fix2' of git://github.com/damyon/moodle into MOODLE_34_STABLE 2018-10-18 14:39:31 +08:00
Andrew Nicols 0ede37a30c MDL-63667 amd: Check for parent template recursion 2018-10-18 12:12:43 +08:00
Damyon Wiese 1a3a8a9d6d MDL-63667 output: Fix for recursive templates
A template can include itself - e.g. by looping over the context children and rendering a tree.

Ensure this promise still resolves.

Example: admin/tool/lp/templates/competencies_tree.mustache.
2018-10-18 10:54:01 +08:00
Eloy Lafuente (stronk7) be3a6f83c2 Merge branch 'MDL-63632-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-10-18 02:39:33 +02:00
AMOS bot d15cbdd72f Automatically generated installer lang files 2018-10-18 00:07:35 +00:00
Andrew Nicols 7fa07866b7 MDL-63632 forum: Delete where not discussion author 2018-10-18 07:20:29 +08:00
Eloy Lafuente (stronk7) ab4d4619c2 Merge branch 'MDL-63068_34' of https://github.com/aanabit/moodle into MOODLE_34_STABLE 2018-10-18 00:40:16 +02:00
Jun Pataleta 8464779f2a MDL-62904 tool_dataprivacy: Add data request links in profile page 2018-10-17 16:42:56 +08:00
Jun Pataleta 5775e12509 Merge branch 'MDL-63667-34' of git://github.com/damyon/moodle into MOODLE_34_STABLE 2018-10-17 15:03:50 +08:00
Ryan Wyllie 7ffafab4f7 MDL-63667 javascript: fix failed to pre-fetch the template error 2018-10-17 13:53:28 +08:00
Michael Hawkins b6a8e4db77 MDL-63495 comment: Added provider::delete_comments_for_users
This issue is a part of the MDL-62560 Epic.
2018-10-17 13:13:55 +08:00
Andrew Nicols 121f12aa3c MDL-63495 mod_forum: Add intial support for removal of multiple context users
This issue is a part of the MDL-62560 Epic.
2018-10-17 13:13:55 +08:00
Andrew Nicols 924881fba9 MDL-63495 core_rating: Add helper to fetch users in context
This issue is a part of the MDL-62560 Epic.
2018-10-17 13:13:55 +08:00
Andrew Nicols b11130cf15 MDL-63496 tool_dataprivacy: Add configuration for per-role retention
This issue is a part of the MDL-62560 Epic.
2018-10-17 13:13:55 +08:00
Andrew Nicols f3e39f4bce MDL-63496 tool_dataprivacy: Respect expiry with protected flag
This issue is a part of the MDL-62560 Epic.
2018-10-17 13:13:54 +08:00
Andrew Nicols 037f741214 MDL-63496 tool_dataprivacy: Support for per-role retention
This issue is a part of the MDL-62560 Epic.
2018-10-17 13:13:54 +08:00
Michael Hawkins c71a358c69 MDL-63497 tool_dataprivacy: Add support for removal of context users
This issue is a part of the MDL-62560 Epic.
2018-10-17 13:13:54 +08:00
Andrew Nicols f2c6d5cc11 MDL-63495 privacy: Add support for removal of multiple users in a context
This issue is a part of the MDL-62560 Epic.
2018-10-17 13:13:54 +08:00
Eloy Lafuente (stronk7) c600fd5712 weekly release 3.4.5+ 2018-10-16 16:05:34 +02:00
Eloy Lafuente (stronk7) 67634c4102 Merge branch 'install_34_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_34_STABLE 2018-10-16 16:05:32 +02:00
Damyon Wiese 668934466b Merge branch 'MDL-62960-34' of git://github.com/rezaies/moodle into MOODLE_34_STABLE 2018-10-16 11:44:18 +08:00
Shamim Rezaie a183d18041 MDL-62960 calendar: Visuall indicator for valid course event drop zones 2018-10-16 13:14:36 +11:00
Shamim Rezaie c0e7ba026d MDL-62960 calendar: Rename get_module_timestamp_min/max_limit methods
The methods get_module_timestamp_min_limit() and get_module_timestamp_max_limit()
are doing generic stuff and can be used for other calendar event types as well.
I have renamed them to get_timestamp_min_limit() and get_timestamp_max_limit() respectively.
2018-10-16 13:14:26 +11:00
Shamim Rezaie 9211b03e86 MDL-62960 calendar: Implementing course events validity check callback 2018-10-16 13:12:57 +11:00
Shamim Rezaie b43f9b39fa MDL-62960 calendar: Added range check callback for course events
Added core_calendar_get_valid_event_timestart_range callback for course events
2018-10-16 13:12:57 +11:00
Andrew Nicols e098de35bc Merge branch 'MDL-63421_34' of git://github.com/stronk7/moodle into MOODLE_34_STABLE 2018-10-16 08:55:39 +08:00
Eloy Lafuente (stronk7) b6ff1269e8 Merge branch 'MDL-63583-34' of git://github.com/jleyva/moodle into MOODLE_34_STABLE 2018-10-16 00:29:28 +02:00
Eloy Lafuente (stronk7) 4f88dff731 Merge branch 'MDL-63456-34' of git://github.com/merrill-oakland/moodle into MOODLE_34_STABLE 2018-10-16 00:17:26 +02:00
Eloy Lafuente (stronk7) cdc68e910a Merge branch 'MDL-62319-34' of git://github.com/damyon/moodle into MOODLE_34_STABLE 2018-10-16 00:07:23 +02:00
Andrew Nicols 1694162476 Merge branch 'MDL-63542-34' of git://github.com/jleyva/moodle into MOODLE_34_STABLE 2018-10-15 11:40:10 +08:00
AMOS bot c9e89351aa Automatically generated installer lang files 2018-10-15 00:07:27 +00:00
Eloy Lafuente (stronk7) a08235c914 MDL-63655 core: Fix miss-placed parenthesis 2018-10-15 01:03:04 +02:00
Amaia Anabitarte bcd99539b7 MDL-63068 user: Allow underscore for profile custom fields shortname 2018-10-11 16:40:34 +02:00
Amaia Anabitarte acca768961 MDL-62920 user: Uploading deleted user and adding to a cohort 2018-10-11 16:27:48 +02:00
Shamim Rezaie c5d2f4d0b3 MDL-63626 mod_quiz: Fixed a bug when there was no attempt on the quiz 2018-10-12 01:04:17 +11:00
Eloy Lafuente (stronk7) 0dfa4a3c18 weekly release 3.4.5+ 2018-10-11 13:22:49 +02:00
Eloy Lafuente (stronk7) c8ed9615de Merge branch 'install_34_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_34_STABLE 2018-10-11 13:22:47 +02:00
Damyon Wiese ffee9dcc59 MDL-62319 assignfeedback_editpdf: Disable touch scrolling
Assignment editpdf canvas provides it's own scroll tool and the native browser one breaks
the rest of the tools for the canvas. Turn it off.
2018-10-11 13:33:14 +08:00
Nathan Nguyen edbc4b5816 MDL-62319 Assign: Grading interface is broken when using iOS 11.3 devices 2018-10-11 13:33:11 +08:00
AMOS bot aa29f93afe Automatically generated installer lang files 2018-10-11 00:07:26 +00:00
Eloy Lafuente (stronk7) 7918ae0457 Merge branch 'MDL-62891-34-callable_name' of git://github.com/mudrd8mz/moodle into MOODLE_34_STABLE 2018-10-10 23:33:20 +02:00
Eloy Lafuente (stronk7) 33b264e80a Merge branch 'MDL-63225-34' of git://github.com/jleyva/moodle into MOODLE_34_STABLE 2018-10-10 23:26:00 +02:00
Peter Spicer 7e4872e91e MDL-63451 core_message: Properly escape URLs for notification popup 2018-10-10 13:03:42 +01:00
Jake Dallimore 57429ec39b MDL-60145 course: fix bug with group restictions button during mod edit
If a mod doesn't support groups, then we remove the grouping element,
and now also the 'add group/group restriction' helper button too.
2018-10-10 08:43:46 +08:00
AMOS bot 837ac1b79d Automatically generated installer lang files 2018-10-10 00:07:22 +00:00
Eloy Lafuente (stronk7) 447b4bdf5a Merge branch 'MDL-63439-34' of git://github.com/damyon/moodle into MOODLE_34_STABLE 2018-10-10 00:48:29 +02:00
Eloy Lafuente (stronk7) 3d92367a80 Merge branch 'MDL-62395-34' of git://github.com/junpataleta/moodle into MOODLE_34_STABLE 2018-10-09 23:06:47 +02:00
Eloy Lafuente (stronk7) 957c839884 Merge branch 'MDL-63401-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-10-09 11:36:44 +02:00
David Monllao cde9c8ccc0 Merge branch 'MDL-62738_34' of https://github.com/mfabriczy/moodle into MOODLE_34_STABLE 2018-10-09 09:24:39 +02:00
David Monllao 8d6c617e59 Merge branch 'MDL-51969-MOODLE_34_STABLE' of https://github.com/dmitriim/moodle into MOODLE_34_STABLE 2018-10-09 09:21:14 +02:00
Jun Pataleta a474308f74 Merge branch 'MDL-62491-34' of git://github.com/abgreeve/moodle into MOODLE_34_STABLE 2018-10-09 14:39:33 +08:00
Jun Pataleta 8dc8bfdb1d Merge branch 'MDL-63241-34-take2' of https://github.com/lucaboesch/moodle into MOODLE_34_STABLE 2018-10-09 12:48:27 +08:00
Adrian Greeve 42129c29b4 MDL-62491 navigation: Update to tree.js to allow callbacks. 2018-10-09 09:42:14 +08:00
Adrian Greeve bcad932ef8 MDL-62491 mod_assign: Fill in the rewrite plugin urls properly. 2018-10-09 09:42:04 +08:00
Adrian Greeve 40d08b4fb1 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 09:41:55 +08:00
Damyon Wiese f68ad5439b MDL-63439 assign: Unicode charsets for pdf
Prefer to use freesans font for generated pdfs as it has the biggest charset support.
2018-10-09 09:31:34 +08:00
Jun Pataleta 2d065b56c7 Merge branch 'MDL-63194_34' of https://github.com/timhunt/moodle into MOODLE_34_STABLE 2018-10-09 08:55:00 +08:00
Andrew Nicols e5088f07a8 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 bc09a4398c MDL-63401 tool_dataprivacy: Simplify where processing 2018-10-09 08:22:41 +08:00
Andrew Nicols 5e77c5f24f MDL-63401 tool_dataprivacy: Move final cap check to endpoint 2018-10-09 08:22:41 +08:00
Eric Merrill 372476328c MDL-63456 qtype_multichoice: Return a default options object if missing 2018-10-08 16:31:35 -04:00
Eric Merrill 98dec1b08c MDL-63456 question: Improve Aiken error handling and multichoice errors 2018-10-08 16:31:35 -04:00
Tim Hunt 474ee16ec7 MDL-63194 quiz editing: subtle drag-drop bug if moving between sections 2018-10-08 17:46:36 +01:00
Juan Leyva 4105b2d351 MDL-63583 course: Include course/lib.php always
core_course_get_courses_by_field WS must include always course/lib.php to not depend on functions including it.
2018-10-08 16:57:14 +02:00
Andrew Nicols ec259ef812 MDL-63401 tool_dataprivacy: Allow expiriration of users without end date 2018-10-08 20:52:45 +08:00
Andrew Nicols 3e90c7df33 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:52:27 +08:00
Andrew Nicols 6b0512cc52 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:52:27 +08:00
Andrew Nicols a9c0a87ffd MDL-63401 tool_dataprivacy: Move cap checks to endpoints from API 2018-10-08 20:52:27 +08:00
David Monllao 4de8dd37c7 Merge branch 'MDL-63323-34-wrong-classes-attribute-into-activity_navigation' of https://github.com/davidherney/moodle into MOODLE_34_STABLE 2018-10-08 11:30:27 +02:00
Luca Bösch 32052e77f8 MDL-63241 calendar: Add missing table alias in SQL query. 2018-10-07 21:45:53 +02:00
Marcus Fabriczy 35f6db9232 MDL-62738: editor_tinymce: Fix call stack size bug on Safari
The call stack size was being exceeded via a recursive loop brought
by MDL-61189.

To fix, this item removes MDL-36803 and MDL-41328 as the iOS keyboard works
nicely now on Safari with TinyMCE.
2018-10-06 17:30:20 +09:30
Juan Leyva c5ab666ece MDL-63542 core_course: Support stealth activities in WS 2018-10-05 14:51:31 +02:00
Eloy Lafuente (stronk7) 06579f587c MDL-63421 env: Moodle 3.4.x and 3.5.x do not support PHP 7.3 2018-10-05 13:38:00 +02:00
David Mudrák 152b534b6d MDL-62891 core: Stop using var_export() to describe callables
Make use of the newly added function get_callable_name() when reporting
that an exception happened during shutdown.
2018-10-05 08:53:20 +02:00
David Mudrák 4081f82d5c MDL-62891 core: Introduce new get_callable_name() function 2018-10-05 08:53:20 +02:00
Dmitrii Metelkin ef0688bf35 MDL-51969 mod_lti: Fixed missing tool proxy GUID 2018-10-05 15:15:33 +10:00
Jun Pataleta 57241c56c0 weekly release 3.4.5+ 2018-10-05 10:29:04 +08:00
Jun Pataleta 994d82f64d Merge branch 'install_34_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_34_STABLE 2018-10-05 10:29:03 +08:00
Jun Pataleta 4db4682464 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:28:48 +08:00
Jun Pataleta 591a54cd72 Merge branch 'MDL-35788-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-10-04 12:46:07 +08:00
Jun Pataleta 0f6973ac13 MDL-63528 auth: Behat scenario for "Remember username" display 2018-10-04 12:25:15 +08:00
Jun Pataleta 8c9a724672 MDL-63528 auth: Show "Remember username" on login form only when needed 2018-10-04 12:25:15 +08:00
Andrew Nicols 1f700517cd MDL-35788 mod_forum: Move post creation fetch 2018-10-04 11:42:54 +08:00
AMOS bot 7a2da2f137 Automatically generated installer lang files 2018-10-04 00:07:29 +00:00
Eloy Lafuente (stronk7) 7277817938 Merge branch 'MDL-63141-34' of git://github.com/rezaies/moodle into MOODLE_34_STABLE 2018-10-04 00:24:30 +02:00
Eloy Lafuente (stronk7) 6187351760 Merge branch 'MDL-63142-34' of git://github.com/rezaies/moodle into MOODLE_34_STABLE 2018-10-04 00:18:53 +02:00
Eloy Lafuente (stronk7) f65a5b5f34 Merge branch 'MDL-63143-34' of git://github.com/rezaies/moodle into MOODLE_34_STABLE 2018-10-04 00:02:56 +02:00
David Monllao ef1e76b490 Merge branch 'MDL-62558-34' of git://github.com/abgreeve/moodle into MOODLE_34_STABLE 2018-10-03 17:16:17 +02:00
Adrian Greeve df3cc8c9c5 MDL-62558 tool_dataprivacy: Create a summary page for users. 2018-10-03 10:40:34 +08:00
Jun Pataleta 9e026e05df Merge branch 'MDL-35788-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-10-03 10:12:43 +08:00
Eloy Lafuente (stronk7) ed19765c8d Merge branch 'MDL-60685-34' of git://github.com/damyon/moodle into MOODLE_34_STABLE 2018-10-03 00:24:19 +02:00
Eloy Lafuente (stronk7) 31252432a1 Merge branch 'MDL-63435-34' of https://github.com/lucaboesch/moodle into MOODLE_34_STABLE 2018-10-02 23:48:27 +02:00
Juan Leyva d14393fb3d MDL-63225 webservice: Return proper debuginfo on WS exceptions
In order to make developers easy, we should give some clues about the type of the data generating exceptions.
2018-10-02 13:15:37 +02:00
Andrew Nicols 8fe630d49b MDL-35788 forum: Show post creation time not modified 2018-10-01 08:39:56 +08:00
Jun Pataleta bd7573e471 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-09-28 14:33:55 +08:00
David Herney 571ee73a77 MDL-63323 course: Fixed wrong classes key in activity_navigation class
Replaced "classes" by "class" attribute, when the prev and next links are builded into the activity navigation.
2018-09-27 18:44:00 -05:00
Luca Bösch ce3f7aeb16 MDL-63435 gradebook: let activity icons re-appear. 2018-09-27 18:10:40 +02:00
Eloy Lafuente (stronk7) 75d95d140b weekly release 3.4.5+ 2018-09-27 15:17:55 +02:00
Eloy Lafuente (stronk7) 54b432fb4f Merge branch 'MDL-61981-34_nestedmath' of git://github.com/Mankro/moodle into MOODLE_34_STABLE 2018-09-25 23:41:03 +02:00
Eloy Lafuente (stronk7) 0c5279e4c5 Merge branch 'MDL-62279-34' of git://github.com/damyon/moodle into MOODLE_34_STABLE 2018-09-25 23:26:51 +02:00
Eloy Lafuente (stronk7) 514dce5460 Merge branch 'MDL-62714-34-boost_course_visibility_in_navbar' of https://github.com/fskandalis/moodle into MOODLE_34_STABLE 2018-09-25 23:20:13 +02:00
Eloy Lafuente (stronk7) 8e20e22be9 Merge branch 'MDL-60028-34' of https://github.com/paulholden/moodle into MOODLE_34_STABLE 2018-09-25 16:58:44 +02:00
David Monllao 61859ec680 Merge branch 'MDL-63119-34' of git://github.com/lameze/moodle into MOODLE_34_STABLE 2018-09-25 12:36:36 +02:00
David Monllao 816f9a560b Merge branch 'MDL-62717-34' of https://github.com/lucaboesch/moodle into MOODLE_34_STABLE 2018-09-25 12:19:00 +02:00
Andrew Nicols abb789b0fb Merge branch 'MDL-63346_34' of https://github.com/stronk7/moodle into MOODLE_34_STABLE 2018-09-25 08:51:41 +08:00
Eloy Lafuente (stronk7) 0e7711084a MDL-63020 tests: ensure that searching for is null values also works ok 2018-09-24 17:43:19 +02:00
Tim Hunt 70b3253d46 MDL-63020 dml: improve the unit tests 2018-09-24 17:43:19 +02:00
Eloy Lafuente (stronk7) fd5cc5ee03 Merge branch 'MDL-63020_34' of git://github.com/timhunt/moodle into MOODLE_34_STABLE 2018-09-24 17:42:52 +02:00
Paul Holden 4f1725493d MDL-60028 enrol: fix counting of enrolled users in a given context.
Previously users enrolled in a course via multiple enrolment methods
would be counted multiple times.
2018-09-21 10:19:38 +01:00
Tim Hunt eb43bdbbb0 MDL-63020 ddl: fix nullable unique indexes in OCI and MS SQL
This works-around the default non-standard behaviour of these DB engines.
2018-09-20 17:50:30 +01:00
Tim Hunt a27808cd1b MDL-63020 xmldb: Improve PHPdoc comments for better IDE autocomplete 2018-09-20 17:50:28 +01:00
Luca Bösch 1157c4c29a MDL-62717 forum: Create announcements forum with HTML format intro. 2018-09-20 17:47:01 +02:00
Markku Riekkinen 65dc7eba8d MDL-61981 filter_mathjaxloader: add unit tests 2018-09-20 15:00:52 +03:00
Eloy Lafuente (stronk7) f05c493776 weekly release 3.4.5+ 2018-09-20 12:12:05 +02:00
Eloy Lafuente (stronk7) 761179ce00 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:54:51 +02:00
Eloy Lafuente (stronk7) 3763566864 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:53:47 +02:00
Markku Riekkinen 5acd022bf6 MDL-61981 filter_mathjaxloader: nested math environments
The MathJax filter used to break mathematics if inline math was
used inside display math. Nolink spans were added around
the inline math even though it was nested inside another math
environment. This fix is aware of the nesting so that the spans
may be inserted only at the outer math environment. No regular
expressions are used because they do not support detecting
arbitrary, unlimited nesting of parentheses in the text, which
is now needed.
2018-09-19 15:36:37 +03:00
David Monllao 5bf7359a98 Merge branch 'MDL-63140-34' of git://github.com/rezaies/moodle into MOODLE_34_STABLE 2018-09-19 13:31:23 +02:00
David Monllao a0b0988443 Merge branch 'MDL-63135-34' of git://github.com/rezaies/moodle into MOODLE_34_STABLE 2018-09-19 13:13:25 +02:00
Eloy Lafuente (stronk7) cd475230e3 Merge branch 'MDL-63116-34-1' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-09-18 23:51:01 +02:00
Mihail Geshoski 7ef99fc213 MDL-61908 privacy: Fix heading in data privacy pages 2018-09-18 18:51:26 +02:00
Eloy Lafuente (stronk7) 39d9d254f8 Merge branch 'MDL-63109-34' of git://github.com/mihailges/moodle into MOODLE_34_STABLE 2018-09-18 18:32:17 +02:00
David Monllao cb5fb29cd7 Merge branch 'MDL-63138-34' of git://github.com/rezaies/moodle into MOODLE_34_STABLE 2018-09-18 16:41:23 +02:00
David Monllao 693959a524 Merge branch 'MDL-63139-34' of git://github.com/rezaies/moodle into MOODLE_34_STABLE 2018-09-18 16:37:54 +02:00
David Monllao 4a4f563f26 Merge branch 'MDL-63117-34' of git://github.com/rezaies/moodle into MOODLE_34_STABLE 2018-09-18 16:34:37 +02:00
Mihail Geshoski 2eccec9542 MDL-63109 privacy: Add behat tests 2018-09-18 15:08:21 +08:00
Mihail Geshoski 4c9c186fdc MDL-63109 privacy: Fix the initialisation of the policy acceptance modal 2018-09-18 15:05:01 +08:00
Tim Schroeder 0cd1bc0b67 MDL-63119 calendar: now showing event end in modal
When clicking on an event in the month view, a modal opens with
    information about it. That modal did previously only show the start time
    of the event. It now also shows the end (if the event has one).
2018-09-18 13:41:58 +08:00
Jun Pataleta 90a6d883b1 Merge branch 'm34_MDL-63319_MySQL_MSSQL_Rename_Field_Reserved_Word' of https://github.com/scara/moodle into MOODLE_34_STABLE 2018-09-18 13:41:39 +08:00
Eloy Lafuente (stronk7) c883e9bf96 Merge branch 'MDL-58781_34' of https://github.com/aanabit/moodle into MOODLE_34_STABLE 2018-09-17 23:43:35 +02:00
Eloy Lafuente (stronk7) 0b07e65fba Merge branch 'MDL-63009-34' of git://github.com/mickhawkins/moodle into MOODLE_34_STABLE 2018-09-17 23:31:13 +02:00
Eloy Lafuente (stronk7) 390dcea39c Merge branch 'MDL-63184-34-2' of git://github.com/junpataleta/moodle into MOODLE_34_STABLE 2018-09-17 23:11:45 +02:00
Eloy Lafuente (stronk7) ae7dc5dcfb Merge branch 'MDL-63354-34' of https://github.com/HuongNV13/moodle into MOODLE_34_STABLE 2018-09-17 23:06:57 +02:00
Jun Pataleta b333c94a01 MDL-63184 tool_dataprivacy: Ensure PO roles have correct capability 2018-09-17 22:35:49 +08:00
David Monllao 6385c8f0b7 Merge branch 'MDL-62554-34' of git://github.com/junpataleta/moodle into MOODLE_34_STABLE 2018-09-17 13:45:58 +02:00
Eloy Lafuente (stronk7) 8b16f4d7f2 Merge branch 'MDL-63215-34' of https://github.com/lucaboesch/moodle into MOODLE_34_STABLE 2018-09-17 10:43:28 +02:00
Mihail Geshoski 5f01d507cb MDL-63116 privacy: Add bulk actions to the privacy request table 2018-09-17 08:24:13 +08:00
Mihail Geshoski 449b4461ee MDL-63116 privacy: Add behat tests 2018-09-17 08:24:13 +08:00
Mihail Geshoski 980e81275b MDL-63116 privacy: Add unit tests 2018-09-17 08:24:13 +08:00
Michelle Melton aa21de4c84 MDL-63215 block_rss_client: Escaping tags in RSS entries 2018-09-16 23:46:05 +02:00
Matteo Scaramuccia da2d40de4c MDL-63319 ddl: Added getRenameFieldSQL() coverage 2018-09-14 22:45:01 +02:00
Matteo Scaramuccia 41e73ee448 MDL-63319 ddl: Improved rename_field() coverage
DB API should allow to rename fields, including renaming
those columns named using a reserved word.
2018-09-14 22:45:01 +02:00
Matteo Scaramuccia 5dca702169 MDL-63319 ddl: MSSQL should rename reserved words 2018-09-14 22:36:48 +02:00
Matteo Scaramuccia cd579c4b1a MDL-63319 ddl: MySQL should rename reserved words 2018-09-14 22:36:48 +02:00
Amaia Anabitarte ac97d778c7 MDL-58781 mod_feedback: Inconsistency in analysis exported to excel 2018-09-14 12:38:48 +02:00
Shamim Rezaie 308fda5fb1 MDL-63143 mod_lesson: Check if the module is visible to the user 2018-09-14 20:35:58 +10:00
Shamim Rezaie 6ebe20ee9b MDL-63143 mod_lesson: Add userid param to mod_lesson calendar callbacks 2018-09-14 20:35:58 +10:00
Eloy Lafuente (stronk7) 09b00ee19c weekly release 3.4.5+ 2018-09-14 11:06:50 +02:00
Shamim Rezaie 73fb05948f MDL-63142 mod_label: Check if the module is visible to the user 2018-09-14 17:56:11 +10:00
Shamim Rezaie 10da17c8b8 MDL-63142 mod_label: Add userid param to mod_label calendar callbacks 2018-09-14 17:56:11 +10:00
Shamim Rezaie 2b58ab0274 MDL-63141 mod_imscp: Check if the module is visible to the user 2018-09-14 17:50:37 +10:00
Shamim Rezaie e0f7f1ef95 MDL-63141 mod_imscp: Add userid param to mod_imscp calendar callbacks 2018-09-14 17:50:37 +10:00
Shamim Rezaie 9fc96f244c MDL-63140 mod_glossary: Check if the module is visible to the user 2018-09-14 17:44:56 +10:00
Shamim Rezaie 236636ea00 MDL-63140 mod_glossary: Add userid param to glossary calendar callbacks 2018-09-14 17:44:55 +10:00
Huong Nguyen a49d2c334a MDL-63354 Group: Rollover text on long group names is not consistent 2018-09-14 09:45:43 +07:00
Eloy Lafuente (stronk7) 8c4e26977c MDL-61169 iplookup: Change to another block still remaining to SF 2018-09-13 19:06:54 +02:00
Jun Pataleta 35e28afab4 MDL-62554 tool_dataprivacy: Integration review fixes 2018-09-13 14:52:34 +08:00
Michael Hawkins c718e9f671 MDL-63009 tool_dataprivacy: Added site name/link to request emails 2018-09-12 17:45:50 +08:00
Jun Pataleta 7eddff8e55 MDL-62294 lib: Take note of cherry-picked upstream commit
* Make a note of the applied upstream patch in
  lib/lessphp/moodle_readme.txt which fixes the PHP 7.2 compatibility
  issue when counting ruleset rules.
2018-09-12 16:46:37 +08:00
Michaël Marinetti da81219682 MDL-62294 lessphp: Fix for PHP 7.2 compatibility
From upstream commit:
https://github.com/oyejorge/less.php/commit/669acc51817a8da162b5f1b7137e79f0e4acc636
2018-09-12 16:46:37 +08:00
Jun Pataleta 43bd47b67a Merge branch 'mdl63321-moodle34' of https://github.com/tlock/moodle into MOODLE_34_STABLE 2018-09-12 15:33:21 +08:00
Jun Pataleta f251d75803 Merge branch 'MDL-63231-set-default-value-for-int-param-MOODLE_34' of https://github.com/guillalva06/moodle into MOODLE_34_STABLE 2018-09-12 09:58:39 +08:00
Jun Pataleta fc9a4136a7 Merge branch 'MDL-50314_34' of git://github.com/do-you-even-curl/moodle into MOODLE_34_STABLE 2018-09-12 09:43:26 +08:00
Tim Lock b0412aa454 MDL-63321 block: Fix warning in Calendar month block after MDL-60587 2018-09-12 08:31:02 +09:30
Eloy Lafuente (stronk7) 8fde61edd4 Merge branch 'MDL-63136-34' of git://github.com/rezaies/moodle into MOODLE_34_STABLE 2018-09-11 23:53:06 +02:00
Eloy Lafuente (stronk7) b736aab921 Merge branch 'MDL-62528-34' of git://github.com/bmbrands/moodle into MOODLE_34_STABLE 2018-09-11 23:05:44 +02:00
Guillermo Alvarez 95a0dce3d4 MDL-63231 filepicker: Skype validation for null values
In file_get_all_files_in_draftarea function an integer could get
a null value so it is necessary to execute this function only
when is called with a valid integer value.
2018-09-11 09:05:48 -05:00
Fotis Skandalis 21f3395f7a MDL-62714 theme_boost: Course visibility in navigation bar
Added check in navbar.mustache to add dimmed_text class if needed.
Added is_hidden() in navigationlib.php to return if the class needs
to be added.
2018-09-10 16:57:56 +03:00
Jun Pataleta d21dd7fbfc MDL-62554 tool_dataprivacy: Bump version for new WS functions 2018-09-10 12:18:55 +08:00
Jun Pataleta 2035a7a55e MDL-62554 tool_dataprivacy: Fix display of activity options 2018-09-10 12:18:55 +08:00
Jun Pataleta 3becb16f53 MDL-62554 tool_dataprivacy: Fix template for Bootstrapbase compatibility 2018-09-10 12:18:55 +08:00
Jun Pataleta cd6a9957fa MDL-62554 tool_dataprivacy: Remove defaults form
With the changes in this issue, this form is no longer being used.
2018-09-10 12:18:55 +08:00
Jun Pataleta 7b0cd65883 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:18:55 +08:00
Jun Pataleta 402eec348e MDL-62554 tool_dataprivacy: Unit tests for new WS/API functions 2018-09-10 12:18:55 +08:00
Jun Pataleta 42e3825a92 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:18:55 +08:00
Jun Pataleta d6c0be9438 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:18:50 +08:00
Tim Schroeder 0d17150a0c MDL-50314 mod_quiz: removed hardcoded export columns
* The columns 'institution', 'department' and 'email' are no longer
  automatically included in all quiz result exports.
* These columns can still be used, but have to be configured under
  'showuseridentity'.
2018-09-07 14:16:41 +02:00
Shamim Rezaie b8d91beab4 MDL-63136 mod_data: Check if the module is visible to the user 2018-09-07 17:42:32 +10:00
Shamim Rezaie 8f8093ac0f MDL-63136 mod_data: Add userid param to mod_data calendar callbacks 2018-09-07 17:42:31 +10:00
Shamim Rezaie 5eae317838 MDL-63139 mod_forum: Check if the module is visible to the user 2018-09-07 16:56:44 +10:00
Shamim Rezaie 445be5614a MDL-63139 mod_forum: Add userid param to mod_forum calendar callbacks 2018-09-07 16:56:44 +10:00
Shamim Rezaie ae9af4e486 MDL-63138 mod_folder: Check if the module is visible to the user 2018-09-07 15:06:52 +10:00
Shamim Rezaie 08392add89 MDL-63138 mod_folder: Add userid param to mod_folder calendar callback 2018-09-07 15:06:52 +10:00
Shamim Rezaie 75bad7de80 MDL-63135 mod_choice: Check if the module is visible to the user 2018-09-06 20:15:16 +10:00
Shamim Rezaie 056dd08cd2 MDL-63135 mod_choice: Add userid param to mod_choice calendar callback 2018-09-06 20:15:16 +10:00
Shamim Rezaie 1f9034982e MDL-63135 mod_choice: Added choice_get_user_response function
A new function to return choice answers records of a given user in a given choice activity.
2018-09-06 20:15:15 +10:00
Shamim Rezaie ace999418c MDL-63117 mod_book: Check if the module is visible to the user 2018-09-06 17:46:32 +10:00
Shamim Rezaie 5892083e98 MDL-63117 mod_book: Add userid param to mod_book calendar callbacks 2018-09-06 17:46:29 +10:00
Bas Brands 4179c7d022 MDL-62528 forum: Fixed vertical position when loading permalinks 2018-08-15 11:43:05 +02:00
Damyon Wiese 3cb6f1de33 MDL-62279 assign: Properly set the defaults
Before an assignment has been created, we need to use the site defaults,
not the per instance defaults for the settings.
2018-08-03 15:13:25 +08:00
Damyon Wiese ef12348101 MDL-60685 assign: Do not falsely use admin user
When an assign_grades record is automatically populated, do not use the admin user as the default grader.

This would generate false information on the assignment summary screen and send false notifications from
the admin user.
2018-08-02 14:20:02 +08:00
543 changed files with 39570 additions and 4243 deletions
+2
View File
@@ -263,7 +263,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>
+85 -5
View File
@@ -13,6 +13,7 @@
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for core_role.
*
@@ -20,7 +21,9 @@
* @copyright 2018 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_role\privacy;
defined('MOODLE_INTERNAL') || die();
use \core_privacy\local\metadata\collection;
@@ -28,6 +31,8 @@ use \core_privacy\local\request\contextlist;
use \core_privacy\local\request\approved_contextlist;
use \core_privacy\local\request\transform;
use \core_privacy\local\request\writer;
use \core_privacy\local\request\userlist;
use \core_privacy\local\request\approved_userlist;
/**
* Privacy provider for core_role.
@@ -39,7 +44,8 @@ class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\subsystem\provider,
\core_privacy\local\request\subsystem\plugin_provider,
\core_privacy\local\request\user_preference_provider {
\core_privacy\local\request\user_preference_provider,
\core_privacy\local\request\core_userlist_provider {
/**
* Get information about the user data stored by this plugin.
@@ -151,6 +157,50 @@ class provider implements
return $contextlist;
}
/**
* Get the list of users within a specific context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
if (empty($userlist)) {
return;
}
$context = $userlist->get_context();
// Include users who created or modified role capabilities.
$sql = "SELECT modifierid as userid
FROM {role_capabilities}
WHERE contextid = :contextid";
$params = [
'contextid' => $context->id
];
$userlist->add_from_sql('userid', $sql, $params);
// Include users that have a role assigned to them.
$sql = "SELECT userid
FROM {role_assignments}
WHERE contextid = :contextid";
$userlist->add_from_sql('userid', $sql, $params);
// Include users who created or modified the role assignment.
// Differentiate and exclude special cases where tool_cohortroles adds records through the
// "Assign user roles to cohort" feature into the role_assignments table.
// These records should be separately processed in tool_cohortroles.
$sql = "SELECT modifierid as userid
FROM {role_assignments}
WHERE contextid = :contextid
AND component != 'tool_cohortroles'";
$userlist->add_from_sql('userid', $sql, $params);
}
/**
* Export all user data for the specified user, in the specified contexts.
*
@@ -315,6 +365,32 @@ class provider implements
// Remove data from role_assignments.
$DB->delete_records('role_assignments', ['contextid' => $context->id]);
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
global $DB;
// Don't remove data from role_capabilities.
// Because this data affects the whole Moodle, there are override capabilities.
// Don't belong to the modifier user.
$context = $userlist->get_context();
$userids = $userlist->get_userids();
if (empty($userids)) {
return;
}
list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
$params = ['contextid' => $context->id] + $userparams;
// Remove data from role_assignments.
$DB->delete_records_select('role_assignments',
"contextid = :contextid AND userid {$usersql}", $params);
}
/**
* Delete all user data for this user only.
*
@@ -332,10 +408,14 @@ class provider implements
return;
}
$userid = $contextlist->get_user()->id;
foreach ($contextlist->get_contexts() as $context) {
// Only delete the roles assignments where the user is assigned in all contexts.
$DB->delete_records('role_assignments', ['userid' => $userid, 'contextid' => $context->id]);
}
$contextids = $contextlist->get_contextids();
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
$params = ['userid' => $userid] + $contextparams;
// Only delete the roles assignments where the user is assigned in all contexts.
$DB->delete_records_select('role_assignments',
"userid = :userid AND contextid {$contextsql}", $params);
}
/**
* Delete user entries in role_assignments related to the feature
+270
View File
@@ -13,6 +13,7 @@
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy test for core_role
*
@@ -21,13 +22,16 @@
* @copyright 2018 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use \core_role\privacy\provider;
use \core_privacy\local\request\approved_contextlist;
use \core_privacy\local\request\writer;
use \core_privacy\tests\provider_testcase;
use \core_privacy\local\request\transform;
use \tool_cohortroles\api;
use \core_privacy\local\request\approved_userlist;
/**
* Privacy test for core_role
@@ -54,6 +58,7 @@ class core_role_privacy_testcase extends provider_testcase {
$this->assertEquals(get_string('privacy:metadata:preference:showadvanced', 'core_role'),
$prefs->definerole_showadvanced->description);
}
/**
* Check all contexts are returned if there is any user data for this user.
*/
@@ -231,6 +236,7 @@ class core_role_privacy_testcase extends provider_testcase {
}
}
}
/**
* Test for provider::delete_data_for_all_users_in_context().
*/
@@ -316,6 +322,7 @@ class core_role_privacy_testcase extends provider_testcase {
$count = $DB->count_records('role_assignments', ['contextid' => $usercontext2->id]);
$this->assertEquals(0, $count);
}
/**
* Test for provider::delete_data_for_user().
*/
@@ -373,6 +380,7 @@ class core_role_privacy_testcase extends provider_testcase {
$count = $DB->count_records('role_assignments', ['modifierid' => $user->id]);
$this->assertEquals(2, $count);
}
/**
* Export for a user with a key against a script where no instance is specified.
*/
@@ -426,6 +434,7 @@ class core_role_privacy_testcase extends provider_testcase {
$exported = $writer->get_related_data($subcontextteacher, 'cohortroles');
$this->assertEquals($user2->id, reset($exported)->userid);
}
/**
* Test for provider::delete_user_role_to_cohort().
*/
@@ -460,6 +469,267 @@ class core_role_privacy_testcase extends provider_testcase {
$count = $DB->count_records('role_assignments', ['userid' => $user->id, 'component' => 'tool_cohortroles']);
$this->assertEquals(0, $count);
}
/**
* Test that only users within a course context are fetched.
*/
public function test_get_users_in_context() {
global $DB;
$this->resetAfterTest();
$component = 'core_role';
$this->setAdminUser();
$admin = \core_user::get_user_by_username('admin');
// Create user1.
$user1 = $this->getDataGenerator()->create_user();
$usercontext1 = \context_user::instance($user1->id);
// Create user2.
$user2 = $this->getDataGenerator()->create_user();
$usercontext2 = \context_user::instance($user2->id);
// Create course1.
$course1 = $this->getDataGenerator()->create_course();
$coursecontext1 = \context_course::instance($course1->id);
// Create course category.
$coursecat = $this->getDataGenerator()->create_category();
$coursecatcontext = \context_coursecat::instance($coursecat->id);
// Create chat module.
$cm = $this->getDataGenerator()->create_module('chat', ['course' => $course1->id]);
$cmcontext = \context_module::instance($cm->cmid);
$systemcontext = \context_system::instance();
// Create a block.
$block = $this->getDataGenerator()->create_block('online_users');
$blockcontext = \context_block::instance($block->id);
$studentrole = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
$managerrole = $DB->get_record('role', array('shortname' => 'manager'), '*', MUST_EXIST);
// Role assignments CONTEXT_COURSE.
role_assign($studentrole->id, $user1->id, $coursecontext1->id);
role_assign($studentrole->id, $user2->id, $coursecontext1->id);
// Role assignments CONTEXT_COURSECAT.
role_assign($studentrole->id, $user2->id, $coursecatcontext->id);
// Role assignments CONTEXT_SYSTEM.
role_assign($studentrole->id, $user1->id, $systemcontext->id);
// Role assignments CONTEXT_MODULE.
role_assign($studentrole->id, $user2->id, $cmcontext->id);
// Role assigments CONTEXT_BLOCK.
role_assign($studentrole->id, $user1->id, $blockcontext->id);
// Role assigments CONTEXT_USER.
role_assign($managerrole->id, $user1->id, $usercontext2->id);
// Role capabilities.
$this->setUser($user1);
assign_capability('moodle/backup:backupcourse', CAP_ALLOW, $studentrole->id, $cmcontext->id);
// The user list for usercontext1 should not return any users.
$userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
provider::get_users_in_context($userlist1);
$this->assertCount(0, $userlist1);
// The user list for usercontext2 should user1 and admin (role creator).
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
provider::get_users_in_context($userlist2);
$this->assertCount(2, $userlist2);
$expected = [
$user1->id,
$admin->id
];
$this->assertEquals($expected, $userlist2->get_userids(), '', 0.0, 10, true);
// The user list for coursecontext1 should user1, user2 and admin (role creator).
$userlist3 = new \core_privacy\local\request\userlist($coursecontext1, $component);
provider::get_users_in_context($userlist3);
$this->assertCount(3, $userlist3);
$expected = [
$user1->id,
$user2->id,
$admin->id
];
$this->assertEquals($expected, $userlist3->get_userids(), '', 0.0, 10, true);
// The user list for coursecatcontext should user2 and admin (role creator).
$userlist4 = new \core_privacy\local\request\userlist($coursecatcontext, $component);
provider::get_users_in_context($userlist4);
$this->assertCount(2, $userlist4);
$expected = [
$user2->id,
$admin->id
];
$this->assertEquals($expected, $userlist4->get_userids(), '', 0.0, 10, true);
// The user list for systemcontext should user1 and admin (role creator).
$userlist6 = new \core_privacy\local\request\userlist($systemcontext, $component);
provider::get_users_in_context($userlist6);
$this->assertCount(2, $userlist6);
$expected = [
$user1->id,
$admin->id
];
$this->assertEquals($expected, $userlist6->get_userids(), '', 0.0, 10, true);
// The user list for cmcontext should user1, user2 and admin (role creator).
$userlist7 = new \core_privacy\local\request\userlist($cmcontext, $component);
provider::get_users_in_context($userlist7);
$this->assertCount(3, $userlist7);
$expected = [
$user1->id,
$user2->id,
$admin->id
];
$this->assertEquals($expected, $userlist7->get_userids(), '', 0.0, 10, true);
// The user list for blockcontext should user1 and admin (role creator).
$userlist8 = new \core_privacy\local\request\userlist($blockcontext, $component);
provider::get_users_in_context($userlist8);
$this->assertCount(2, $userlist8);
$expected = [
$user1->id,
$admin->id
];
$this->assertEquals($expected, $userlist8->get_userids(), '', 0.0, 10, true);
}
/**
* Test that data for users in approved userlist is deleted.
*/
public function test_delete_data_for_users() {
global $DB;
$this->resetAfterTest();
$component = 'core_role';
$this->setAdminUser();
$admin = \core_user::get_user_by_username('admin');
// Create user1.
$user1 = $this->getDataGenerator()->create_user();
// Create user2.
$user2 = $this->getDataGenerator()->create_user();
$usercontext2 = \context_user::instance($user2->id);
// Create course1.
$course1 = $this->getDataGenerator()->create_course();
$coursecontext1 = \context_course::instance($course1->id);
// Create course category.
$coursecat = $this->getDataGenerator()->create_category();
$coursecatcontext = \context_coursecat::instance($coursecat->id);
// Create chat module.
$cm = $this->getDataGenerator()->create_module('chat', ['course' => $course1->id]);
$cmcontext = \context_module::instance($cm->cmid);
$systemcontext = \context_system::instance();
// Create a block.
$block = $this->getDataGenerator()->create_block('online_users');
$blockcontext = \context_block::instance($block->id);
$studentrole = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
$managerrole = $DB->get_record('role', array('shortname' => 'manager'), '*', MUST_EXIST);
// Role assignments CONTEXT_COURSE.
role_assign($studentrole->id, $user1->id, $coursecontext1->id);
role_assign($studentrole->id, $user2->id, $coursecontext1->id);
// Role assignments CONTEXT_COURSECAT.
role_assign($studentrole->id, $user2->id, $coursecatcontext->id);
// Role assignments CONTEXT_SYSTEM.
role_assign($studentrole->id, $user1->id, $systemcontext->id);
// Role assignments CONTEXT_MODULE.
role_assign($studentrole->id, $user2->id, $cmcontext->id);
// Role assigments CONTEXT_BLOCK.
role_assign($studentrole->id, $user1->id, $blockcontext->id);
// Role assigments CONTEXT_USER.
role_assign($managerrole->id, $user1->id, $usercontext2->id);
// Role capabilities.
$this->setUser($user1);
assign_capability('moodle/backup:backupcourse', CAP_ALLOW, $studentrole->id, $cmcontext->id);
// The user list for usercontext2 should user1 and admin (role creator).
$userlist1 = new \core_privacy\local\request\userlist($usercontext2, $component);
provider::get_users_in_context($userlist1);
$this->assertCount(2, $userlist1);
// The user list for coursecontext1 should user1, user2 and admin (role creator).
$userlist2 = new \core_privacy\local\request\userlist($coursecontext1, $component);
provider::get_users_in_context($userlist2);
$this->assertCount(3, $userlist2);
// The user list for coursecatcontext should user2 and admin (role creator).
$userlist3 = new \core_privacy\local\request\userlist($coursecatcontext, $component);
provider::get_users_in_context($userlist3);
$this->assertCount(2, $userlist3);
// The user list for systemcontext should user1 and admin (role creator).
$userlist4 = new \core_privacy\local\request\userlist($systemcontext, $component);
provider::get_users_in_context($userlist4);
$this->assertCount(2, $userlist4);
// The user list for cmcontext should user1, user2 and admin (role creator).
$userlist5 = new \core_privacy\local\request\userlist($cmcontext, $component);
provider::get_users_in_context($userlist5);
$this->assertCount(3, $userlist5);
// The user list for blockcontext should user1 and admin (role creator).
$userlist6 = new \core_privacy\local\request\userlist($blockcontext, $component);
provider::get_users_in_context($userlist6);
$this->assertCount(2, $userlist6);
// Convert $userlist1 into an approved_contextlist.
$approvedlist1 = new approved_userlist($usercontext2, $component, $userlist1->get_userids());
// Delete using delete_data_for_user.
provider::delete_data_for_users($approvedlist1);
// Re-fetch users in usercontext2.
$userlist1 = new \core_privacy\local\request\userlist($usercontext2, $component);
provider::get_users_in_context($userlist1);
$this->assertCount(0, $userlist1);
// Convert $userlist2 into an approved_contextlist.
$approvedlist2 = new approved_userlist($coursecontext1, $component, $userlist2->get_userids());
// Delete using delete_data_for_user.
provider::delete_data_for_users($approvedlist2);
// Re-fetch users in coursecontext1.
$userlist2 = new \core_privacy\local\request\userlist($coursecontext1, $component);
provider::get_users_in_context($userlist2);
$this->assertCount(0, $userlist2);
// Convert $userlist3 into an approved_contextlist.
$approvedlist3 = new approved_userlist($coursecatcontext, $component, $userlist3->get_userids());
// Delete using delete_data_for_user.
provider::delete_data_for_users($approvedlist3);
// Re-fetch users in coursecatcontext.
$userlist3 = new \core_privacy\local\request\userlist($coursecatcontext, $component);
provider::get_users_in_context($userlist3);
$this->assertCount(0, $userlist3);
// Convert $userlist4 into an approved_contextlist.
$approvedlist4 = new approved_userlist($systemcontext, $component, $userlist4->get_userids());
// Delete using delete_data_for_user.
provider::delete_data_for_users($approvedlist4);
// Re-fetch users in systemcontext.
$userlist4 = new \core_privacy\local\request\userlist($systemcontext, $component);
provider::get_users_in_context($userlist4);
// The data from role_capabilities should still be present. The user list should return the admin user.
$this->assertCount(1, $userlist4);
$expected = [$admin->id];
$this->assertEquals($expected, $userlist4->get_userids());
// Convert $userlist5 into an approved_contextlist.
$approvedlist5 = new approved_userlist($cmcontext, $component, $userlist5->get_userids());
// Delete using delete_data_for_user.
provider::delete_data_for_users($approvedlist5);
// Re-fetch users in cmcontext.
$userlist5 = new \core_privacy\local\request\userlist($cmcontext, $component);
provider::get_users_in_context($userlist5);
// The data from role_capabilities should still be present. The user list should return user1.
$this->assertCount(1, $userlist5);
$expected = [$user1->id];
$this->assertEquals($expected, $userlist5->get_userids());
// Convert $userlist6 into an approved_contextlist.
$approvedlist6 = new approved_userlist($blockcontext, $component, $userlist6->get_userids());
// Delete using delete_data_for_user.
provider::delete_data_for_users($approvedlist6);
// Re-fetch users in blockcontext.
$userlist6 = new \core_privacy\local\request\userlist($blockcontext, $component);
provider::get_users_in_context($userlist6);
$this->assertCount(0, $userlist6);
}
/**
* Supoort function to get all the localised roles name
* in a simple array for testing.
+6 -6
View File
@@ -23,29 +23,29 @@
*/
$string['aim'] = 'This administration tool helps developers and test writers to create .feature files describing Moodle\'s functionalities and run them automatically. Step definitions available for use in .feature files are listed below.';
$string['allavailablesteps'] = 'All the available steps definitions';
$string['allavailablesteps'] = 'All available step definitions';
$string['errorbehatcommand'] = 'Error running behat CLI command. Try running "{$a} --help" manually from CLI to find out more about the problem.';
$string['errorcomposer'] = 'Composer dependencies are not installed.';
$string['errordataroot'] = '$CFG->behat_dataroot is not set or is invalid.';
$string['errorsetconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot need to be set in config.php.';
$string['erroruniqueconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot values need to be different than $CFG->dataroot, $CFG->prefix, $CFG->wwwroot, $CFG->phpunit_dataroot and $CFG->phpunit_prefix values.';
$string['fieldvalueargument'] = 'Field value arguments';
$string['fieldvalueargument_help'] = 'This argument should be completed by a field value, there are many field types, simple ones like checkboxes, selects or textareas or complex ones like date selectors. You can check <a href="http://docs.moodle.org/dev/Acceptance_testing#Providing_values_to_steps" target="_blank">Field values</a> to see the expected field value depending on the field type you provide.';
$string['fieldvalueargument_help'] = 'This argument should be completed by a field value. There are many field types, including simple ones like checkboxes, selects or textareas, or complex ones like date selectors. See the dev documentation <a href="http://docs.moodle.org/dev/Acceptance_testing" target="_blank">Acceptance_testing</a> for details of expected field values.';
$string['giveninfo'] = 'Given. Processes to set up the environment';
$string['infoheading'] = 'Info';
$string['installinfo'] = 'Read {$a} for installation and tests execution info';
$string['newstepsinfo'] = 'Read {$a} for info about how to add new steps definitions';
$string['newstepsinfo'] = 'Read {$a} for info about how to add new step definitions';
$string['newtestsinfo'] = 'Read {$a} for info about how to write new tests';
$string['nostepsdefinitions'] = 'There aren\'t steps definitions matching this filters';
$string['nostepsdefinitions'] = 'There aren\'t any step definitions matching this filter';
$string['pluginname'] = 'Acceptance testing';
$string['stepsdefinitionscomponent'] = 'Area';
$string['stepsdefinitionscontains'] = 'Contains';
$string['stepsdefinitionsfilters'] = 'Steps definitions';
$string['stepsdefinitionsfilters'] = 'Step definitions';
$string['stepsdefinitionstype'] = 'Type';
$string['theninfo'] = 'Then. Checkings to ensure the outcomes are the expected ones';
$string['unknownexceptioninfo'] = 'There was a problem with Selenium or your browser. Please ensure you are using the latest version of Selenium. Error:';
$string['viewsteps'] = 'Filter';
$string['wheninfo'] = 'When. Actions that provokes an event';
$string['wheninfo'] = 'When. Action that provokes an event';
$string['wrongbehatsetup'] = 'Something is wrong with the behat setup and so step definitions cannot be listed: <b>{$a->errormsg}</b><br/><br/>Please check:<ul>
<li>$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot are set in config.php with different values from $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.</li>
<li>You ran "{$a->behatinit}" from your Moodle root directory.</li>
@@ -11,7 +11,7 @@ Feature: List the system steps definitions
@javascript
Scenario: Accessing the list
Then I should see "Steps definitions"
Then I should see "Step definitions"
And I should not see "There aren't steps definitions matching this filter"
@javascript
@@ -30,6 +30,8 @@ use core_privacy\local\request\context;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\writer;
use core_privacy\local\request\userlist;
use \core_privacy\local\request\approved_userlist;
defined('MOODLE_INTERNAL') || die();
@@ -39,7 +41,10 @@ defined('MOODLE_INTERNAL') || die();
* @copyright 2018 Zig Tan <zig@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\plugin\provider {
/**
* Returns meta data about this system.
@@ -75,15 +80,25 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
public static function get_contexts_for_userid(int $userid) : contextlist {
$contextlist = new contextlist();
// Retrieve the User context associated with tool_cohortroles records.
$sql = "SELECT DISTINCT c.id
FROM {context} c
JOIN {tool_cohortroles} cr ON cr.userid = c.instanceid AND c.contextlevel = :contextuser
WHERE cr.userid = :userid";
// When we process user deletions and expiries, we always delete from the user context.
// As a result the cohort role assignments would be deleted, which has a knock-on effect with courses
// as roles may change and data may be removed earlier than it should be.
// Retrieve the context associated with tool_cohortroles records.
$sql = "SELECT DISTINCT c.contextid
FROM {tool_cohortroles} tc
JOIN {cohort} c
ON tc.cohortid = c.id
JOIN {context} ctx
ON ctx.id = c.contextid
WHERE tc.userid = :userid
AND (ctx.contextlevel = :contextlevel1
OR ctx.contextlevel = :contextlevel2)";
$params = [
'contextuser' => CONTEXT_USER,
'userid' => $userid
'userid' => $userid,
'contextlevel1' => CONTEXT_SYSTEM,
'contextlevel2' => CONTEXT_COURSECAT
];
$contextlist->add_from_sql($sql, $params);
@@ -91,6 +106,40 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
return $contextlist;
}
/**
* Get the list of users within a specific context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
$context = $userlist->get_context();
// When we process user deletions and expiries, we always delete from the user context.
// As a result the cohort role assignments would be deleted, which has a knock-on effect with courses
// as roles may change and data may be removed earlier than it should be.
$allowedcontextlevels = [
CONTEXT_SYSTEM,
CONTEXT_COURSECAT
];
if (!in_array($context->contextlevel, $allowedcontextlevels)) {
return;
}
$sql = "SELECT tc.userid as userid
FROM {tool_cohortroles} tc
JOIN {cohort} c
ON tc.cohortid = c.id
WHERE c.contextid = :contextid";
$params = [
'contextid' => $context->id
];
$userlist->add_from_sql('userid', $sql, $params);
}
/**
* Export all user data for the specified user, in the specified contexts.
*
@@ -99,24 +148,28 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
public static function export_user_data(approved_contextlist $contextlist) {
global $DB;
// If the user has tool_cohortroles data, then only the User context should be present so get the first context.
$contexts = $contextlist->get_contexts();
if (count($contexts) == 0) {
return;
}
$context = reset($contexts);
// Remove contexts different from SYSTEM or COURSECAT.
$contextids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
if ($context->contextlevel == CONTEXT_SYSTEM || $context->contextlevel == CONTEXT_COURSECAT) {
$carry[] = $context->id;
}
return $carry;
}, []);
// Sanity check that context is at the User context level, then get the userid.
if ($context->contextlevel !== CONTEXT_USER) {
if (empty($contextids)) {
return;
}
$userid = $context->instanceid;
$userid = $contextlist->get_user()->id;
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
// Retrieve the tool_cohortroles records created for the user.
$sql = 'SELECT cr.id as cohortroleid,
$sql = "SELECT cr.id as cohortroleid,
c.name as cohortname,
c.idnumber as cohortidnumber,
c.description as cohortdescription,
c.contextid as contextid,
r.shortname as roleshortname,
cr.userid as userid,
cr.timecreated as timecreated,
@@ -124,13 +177,13 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
FROM {tool_cohortroles} cr
JOIN {cohort} c ON c.id = cr.cohortid
JOIN {role} r ON r.id = cr.roleid
WHERE cr.userid = :userid';
WHERE cr.userid = :userid
AND c.contextid {$contextsql}";
$params = [
'userid' => $userid
];
$params = ['userid' => $userid] + $contextparams;
$cohortroles = $DB->get_records_sql($sql, $params);
foreach ($cohortroles as $cohortrole) {
// The tool_cohortroles data export is organised in:
// {User Context}/Cohort roles management/{cohort name}/{role shortname}/data.json.
@@ -150,6 +203,8 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
'timemodified' => transform::datetime($cohortrole->timemodified)
];
$context = \context::instance_by_id($cohortrole->contextid);
writer::with_context($context)->export_data($subcontext, $data);
}
}
@@ -162,14 +217,70 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
public static function delete_data_for_all_users_in_context(\context $context) {
global $DB;
// Sanity check that context is at the User context level, then get the userid.
if ($context->contextlevel !== CONTEXT_USER) {
// When we process user deletions and expiries, we always delete from the user context.
// As a result the cohort role assignments would be deleted, which has a knock-on effect with courses
// as roles may change and data may be removed earlier than it should be.
$allowedcontextlevels = [
CONTEXT_SYSTEM,
CONTEXT_COURSECAT
];
if (!in_array($context->contextlevel, $allowedcontextlevels)) {
return;
}
$userid = $context->instanceid;
// Delete the tool_cohortroles records created for the userid.
$DB->delete_records('tool_cohortroles', ['userid' => $userid]);
$cohortids = $DB->get_fieldset_select('cohort', 'id', 'contextid = :contextid',
['contextid' => $context->id]);
// Delete the tool_cohortroles records created in the specific context.
$DB->delete_records_list('tool_cohortroles', 'cohortid', $cohortids);
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
global $DB;
// When we process user deletions and expiries, we always delete from the user context.
// As a result the cohort role assignments would be deleted, which has a knock-on effect with courses
// as roles may change and data may be removed earlier than it should be.
$userids = $userlist->get_userids();
if (empty($userids)) {
return;
}
$context = $userlist->get_context();
$allowedcontextlevels = [
CONTEXT_SYSTEM,
CONTEXT_COURSECAT
];
if (!in_array($context->contextlevel, $allowedcontextlevels)) {
return;
}
$cohortids = $DB->get_fieldset_select('cohort', 'id', 'contextid = :contextid',
['contextid' => $context->id]);
if (empty($cohortids)) {
return;
}
list($cohortsql, $cohortparams) = $DB->get_in_or_equal($cohortids, SQL_PARAMS_NAMED);
list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
$params = $cohortparams + $userparams;
$select = "cohortid {$cohortsql} AND userid {$usersql}";
// Delete the tool_cohortroles records created in the specific context for an approved list of users.
$DB->delete_records_select('tool_cohortroles', $select, $params);
}
/**
@@ -180,21 +291,38 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
public static function delete_data_for_user(approved_contextlist $contextlist) {
global $DB;
// If the user has tool_cohortroles data, then only the User context should be present so get the first context.
$contexts = $contextlist->get_contexts();
if (count($contexts) == 0) {
return;
}
$context = reset($contexts);
// When we process user deletions and expiries, we always delete from the user context.
// As a result the cohort role assignments would be deleted, which has a knock-on effect with courses
// as roles may change and data may be removed earlier than it should be.
// Sanity check that context is at the User context level, then get the userid.
if ($context->contextlevel !== CONTEXT_USER) {
// Remove contexts different from SYSTEM or COURSECAT.
$contextids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
if ($context->contextlevel == CONTEXT_SYSTEM || $context->contextlevel == CONTEXT_COURSECAT) {
$carry[] = $context->id;
}
return $carry;
}, []);
if (empty($contextids)) {
return;
}
$userid = $context->instanceid;
$userid = $contextlist->get_user()->id;
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
$selectcontext = "contextid {$contextsql}";
// Get the cohorts in the specified contexts.
$cohortids = $DB->get_fieldset_select('cohort', 'id', $selectcontext, $contextparams);
if (empty($cohortids)) {
return;
}
list($cohortsql, $cohortparams) = $DB->get_in_or_equal($cohortids, SQL_PARAMS_NAMED);
$selectcohort = "cohortid {$cohortsql} AND userid = :userid";
$params = ['userid' => $userid] + $cohortparams;
// Delete the tool_cohortroles records created for the userid.
$DB->delete_records('tool_cohortroles', ['userid' => $userid]);
$DB->delete_records_select('tool_cohortroles', $selectcohort, $params);
}
}
+219 -46
View File
@@ -30,6 +30,7 @@ use \core_privacy\local\request\writer;
use \core_privacy\local\request\approved_contextlist;
use \tool_cohortroles\api;
use \tool_cohortroles\privacy\provider;
use core_privacy\local\request\approved_userlist;
/**
* Unit tests for the tool_cohortroles implementation of the privacy API.
@@ -57,22 +58,36 @@ class tool_cohortroles_privacy_testcase extends \core_privacy\tests\provider_tes
$this->setUser($user);
$this->setAdminUser();
$nocohortroles = 3;
$this->setup_test_scenario_data($user->id, $nocohortroles);
// Create course category.
$coursecategory = $this->getDataGenerator()->create_category();
$coursecategoryctx = \context_coursecat::instance($coursecategory->id);
$systemctx = context_system::instance();
// Create course.
$course = $this->getDataGenerator()->create_course();
$coursectx = \context_course::instance($course->id);
$this->setup_test_scenario_data($user->id, $systemctx, 1);
$this->setup_test_scenario_data($user->id, $coursecategoryctx, 1, 'Sausage roll 2',
'sausageroll2');
$this->setup_test_scenario_data($user->id, $coursectx, 1, 'Sausage roll 3',
'sausageroll3');
// Test the User's assigned cohortroles matches 3.
$cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
$this->assertCount($nocohortroles, $cohortroles);
$this->assertCount(3, $cohortroles);
// Test the User's retrieved contextlist contains only one context.
// Test the User's retrieved contextlist returns only the system and course category context.
$contextlist = provider::get_contexts_for_userid($user->id);
$contexts = $contextlist->get_contexts();
$this->assertCount(1, $contexts);
$this->assertCount(2, $contexts);
// Test the User's contexts equal the User's own context.
$context = reset($contexts);
$this->assertEquals(CONTEXT_USER, $context->contextlevel);
$this->assertEquals($user->id, $context->instanceid);
$contextlevels = array_column($contexts, 'contextlevel');
$expected = [
CONTEXT_SYSTEM,
CONTEXT_COURSECAT
];
// Test the User's contexts equal the system and course category context.
$this->assertEquals($expected, $contextlevels, '', 0, 10, true);
}
/**
@@ -84,26 +99,45 @@ class tool_cohortroles_privacy_testcase extends \core_privacy\tests\provider_tes
$this->setUser($user);
$this->setAdminUser();
$nocohortroles = 3;
$this->setup_test_scenario_data($user->id, $nocohortroles);
// Create course category.
$coursecategory = $this->getDataGenerator()->create_category();
$coursecategoryctx = \context_coursecat::instance($coursecategory->id);
$systemctx = context_system::instance();
// Create course.
$course = $this->getDataGenerator()->create_course();
$coursectx = \context_course::instance($course->id);
// Test the User's retrieved contextlist contains only one context.
$this->setup_test_scenario_data($user->id, $systemctx, 1);
$this->setup_test_scenario_data($user->id, $coursecategoryctx, 1, 'Sausage roll 2',
'sausageroll2');
$this->setup_test_scenario_data($user->id, $coursectx, 1, 'Sausage roll 3',
'sausageroll3');
// Test the User's retrieved contextlist contains two contexts.
$contextlist = provider::get_contexts_for_userid($user->id);
$contexts = $contextlist->get_contexts();
$this->assertCount(1, $contexts);
$this->assertCount(2, $contexts);
// Test the User's contexts equal the User's own context.
$context = reset($contexts);
$this->assertEquals(CONTEXT_USER, $context->contextlevel);
$this->assertEquals($user->id, $context->instanceid);
// Add a system, course category and course context to the approved context list.
$approvedcontextids = [
$systemctx->id,
$coursecategoryctx->id,
$coursectx->id
];
// Retrieve the User's tool_cohortroles data.
$approvedcontextlist = new approved_contextlist($user, 'tool_cohortroles', $contextlist->get_contextids());
$approvedcontextlist = new approved_contextlist($user, 'tool_cohortroles', $approvedcontextids);
provider::export_user_data($approvedcontextlist);
// Test the tool_cohortroles data is exported at the User context level.
$writer = writer::with_context($context);
// Test the tool_cohortroles data is exported at the system context level.
$writer = writer::with_context($systemctx);
$this->assertTrue($writer->has_any_data());
// Test the tool_cohortroles data is exported at the course category context level.
$writer = writer::with_context($coursecategoryctx);
$this->assertTrue($writer->has_any_data());
// Test the tool_cohortroles data is not exported at the course context level.
$writer = writer::with_context($coursectx);
$this->assertFalse($writer->has_any_data());
}
/**
@@ -117,29 +151,52 @@ class tool_cohortroles_privacy_testcase extends \core_privacy\tests\provider_tes
$this->setUser($user);
$this->setAdminUser();
$nocohortroles = 4;
$this->setup_test_scenario_data($user->id, $nocohortroles);
// Create course category.
$coursecategory = $this->getDataGenerator()->create_category();
$coursecategoryctx = \context_coursecat::instance($coursecategory->id);
$systemctx = context_system::instance();
// Test the User's assigned cohortroles matches 4.
$this->setup_test_scenario_data($user->id, $systemctx, 1);
$this->setup_test_scenario_data($user->id, $coursecategoryctx, 1, 'Sausage roll 2',
'sausageroll2');
// Test the User's assigned cohortroles matches 2.
$cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
$this->assertCount($nocohortroles, $cohortroles);
$this->assertCount(2, $cohortroles);
// Test the User's retrieved contextlist contains only one context.
// Test the User's retrieved contextlist contains two contexts.
$contextlist = provider::get_contexts_for_userid($user->id);
$contexts = $contextlist->get_contexts();
$this->assertCount(2, $contexts);
// Make sure the user data is only being deleted in within the system and course category context.
$usercontext = context_user::instance($user->id);
// Delete all the User's records in mdl_tool_cohortroles table by the user context.
provider::delete_data_for_all_users_in_context($usercontext);
// Test the cohort roles records in mdl_tool_cohortroles table is still present.
$cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
$this->assertCount(2, $cohortroles);
// Delete all the User's records in mdl_tool_cohortroles table by the specified system context.
provider::delete_data_for_all_users_in_context($systemctx);
// The user data in the system context should be deleted.
// Test the User's retrieved contextlist contains one context (course category).
$contextlist = provider::get_contexts_for_userid($user->id);
$contexts = $contextlist->get_contexts();
$this->assertCount(1, $contexts);
// Test the User's contexts equal the User's own context.
$context = reset($contexts);
$this->assertEquals(CONTEXT_USER, $context->contextlevel);
$this->assertEquals($user->id, $context->instanceid);
// Delete all the User's records in mdl_tool_cohortroles table by the specified User context.
provider::delete_data_for_all_users_in_context($context);
// Delete all the User's records in mdl_tool_cohortroles table by the specified course category context.
provider::delete_data_for_all_users_in_context($coursecategoryctx);
// Test the cohort roles records in mdl_tool_cohortroles table is equals zero.
$cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
$this->assertCount(0, $cohortroles);
$contextlist = provider::get_contexts_for_userid($user->id);
$contexts = $contextlist->get_contexts();
$this->assertCount(0, $contexts);
}
/**
@@ -153,24 +210,35 @@ class tool_cohortroles_privacy_testcase extends \core_privacy\tests\provider_tes
$this->setUser($user);
$this->setAdminUser();
$nocohortroles = 4;
$this->setup_test_scenario_data($user->id, $nocohortroles);
// Create course category.
$coursecategory = $this->getDataGenerator()->create_category();
$coursecategoryctx = \context_coursecat::instance($coursecategory->id);
$systemctx = context_system::instance();
// Test the User's assigned cohortroles matches 4.
$this->setup_test_scenario_data($user->id, $systemctx, 1);
$this->setup_test_scenario_data($user->id, $coursecategoryctx, 1, 'Sausage roll 2',
'sausageroll2');
// Test the User's assigned cohortroles matches 2.
$cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
$this->assertCount($nocohortroles, $cohortroles);
$this->assertCount(2, $cohortroles);
// Test the User's retrieved contextlist contains only one context.
// Test the User's retrieved contextlist contains two contexts.
$contextlist = provider::get_contexts_for_userid($user->id);
$contexts = $contextlist->get_contexts();
$this->assertCount(1, $contexts);
$this->assertCount(2, $contexts);
// Test the User's contexts equal the User's own context.
$context = reset($contexts);
$this->assertEquals(CONTEXT_USER, $context->contextlevel);
$this->assertEquals($user->id, $context->instanceid);
// Make sure the user data is only being deleted in within the system and the course category contexts.
$usercontext = context_user::instance($user->id);
// Delete all the User's records in mdl_tool_cohortroles table by the specified approved context list.
$approvedcontextlist = new approved_contextlist($user, 'tool_cohortroles', [$usercontext->id]);
provider::delete_data_for_user($approvedcontextlist);
// Delete all the User's records in mdl_tool_cohortroles table by the specified User approved context list.
// Test the cohort roles records in mdl_tool_cohortroles table are still present.
$cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
$this->assertCount(2, $cohortroles);
// Delete all the User's records in mdl_tool_cohortroles table by the specified approved context list.
$approvedcontextlist = new approved_contextlist($user, 'tool_cohortroles', $contextlist->get_contextids());
provider::delete_data_for_user($approvedcontextlist);
@@ -179,19 +247,124 @@ class tool_cohortroles_privacy_testcase extends \core_privacy\tests\provider_tes
$this->assertCount(0, $cohortroles);
}
/**
* Test that only users within a course context are fetched.
*/
public function test_get_users_in_context() {
$component = 'tool_cohortroles';
// Create a user.
$user = $this->getDataGenerator()->create_user();
$usercontext = context_user::instance($user->id);
// Create course category.
$coursecategory = $this->getDataGenerator()->create_category();
$coursecategoryctx = \context_coursecat::instance($coursecategory->id);
$systemctx = context_system::instance();
$this->setAdminUser();
$userlist = new \core_privacy\local\request\userlist($systemctx, $component);
provider::get_users_in_context($userlist);
$this->assertCount(0, $userlist);
$this->setup_test_scenario_data($user->id, $systemctx, 1);
$this->setup_test_scenario_data($user->id, $coursecategoryctx, 1, 'Sausage roll 2',
'sausageroll2');
// The list of users within the system context should contain user.
provider::get_users_in_context($userlist);
$this->assertCount(1, $userlist);
$this->assertTrue(in_array($user->id, $userlist->get_userids()));
// The list of users within the course category context should contain user.
$userlist = new \core_privacy\local\request\userlist($coursecategoryctx, $component);
provider::get_users_in_context($userlist);
$this->assertCount(1, $userlist);
$this->assertTrue(in_array($user->id, $userlist->get_userids()));
// The list of users within the user context should be empty.
$userlist2 = new \core_privacy\local\request\userlist($usercontext, $component);
provider::get_users_in_context($userlist2);
$this->assertCount(0, $userlist2);
}
/**
* Test that data for users in approved userlist is deleted.
*/
public function test_delete_data_for_users() {
$component = 'tool_cohortroles';
// Create user1.
$user1 = $this->getDataGenerator()->create_user();
// Create user2.
$user2 = $this->getDataGenerator()->create_user();
// Create user3.
$user3 = $this->getDataGenerator()->create_user();
$usercontext3 = context_user::instance($user3->id);
// Create course category.
$coursecategory = $this->getDataGenerator()->create_category();
$coursecategoryctx = \context_coursecat::instance($coursecategory->id);
$systemctx = context_system::instance();
$this->setAdminUser();
$this->setup_test_scenario_data($user1->id, $systemctx, 1);
$this->setup_test_scenario_data($user2->id, $systemctx, 1, 'Sausage roll 2',
'sausageroll2');
$this->setup_test_scenario_data($user3->id, $coursecategoryctx, 1, 'Sausage roll 3',
'sausageroll3');
$userlist1 = new \core_privacy\local\request\userlist($systemctx, $component);
provider::get_users_in_context($userlist1);
$this->assertCount(2, $userlist1);
$this->assertTrue(in_array($user1->id, $userlist1->get_userids()));
$this->assertTrue(in_array($user2->id, $userlist1->get_userids()));
// Convert $userlist1 into an approved_contextlist.
$approvedlist1 = new approved_userlist($systemctx, $component, [$user1->id]);
// Delete using delete_data_for_user.
provider::delete_data_for_users($approvedlist1);
// Re-fetch users in systemcontext.
$userlist1 = new \core_privacy\local\request\userlist($systemctx, $component);
provider::get_users_in_context($userlist1);
// The user data of user1in systemcontext should be deleted.
// The user data of user2 in systemcontext should be still present.
$this->assertCount(1, $userlist1);
$this->assertTrue(in_array($user2->id, $userlist1->get_userids()));
// Convert $userlist1 into an approved_contextlist in the user context.
$approvedlist2 = new approved_userlist($usercontext3, $component, $userlist1->get_userids());
// Delete using delete_data_for_user.
provider::delete_data_for_users($approvedlist2);
// Re-fetch users in systemcontext.
$userlist1 = new \core_privacy\local\request\userlist($systemctx, $component);
provider::get_users_in_context($userlist1);
// The user data in systemcontext should not be deleted.
$this->assertCount(1, $userlist1);
}
/**
* Helper function to setup tool_cohortroles records for testing a specific user.
*
* @param int $userid The ID of the user used for testing.
* @param int $nocohortroles The number of tool_cohortroles to create for the user.
* @param string $rolename The name of the role to be created.
* @param string $roleshortname The short name of the role to be created.
* @throws \core_competency\invalid_persistent_exception
* @throws coding_exception
*/
protected function setup_test_scenario_data($userid, $nocohortroles) {
$roleid = create_role('Sausage Roll', 'sausageroll', 'mmmm');
protected function setup_test_scenario_data($userid, $context, $nocohortroles, $rolename = 'Sausage Roll',
$roleshortname = 'sausageroll') {
$roleid = create_role($rolename, $roleshortname, 'mmmm');
$result = new \stdClass();
$result->contextid = $context->id;
for ($c = 0; $c < $nocohortroles; $c++) {
$cohort = $this->getDataGenerator()->create_cohort();
$cohort = $this->getDataGenerator()->create_cohort($result);
$params = (object)array(
'userid' => $userid,
@@ -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=g.render("tool_dataprivacy/request_details",a),c={approvedeny:a.approvedeny,canmarkcomplete:a.canmarkcomplete};return e.create({title:a.typename,body:b,type:h.TYPE,large:!0,templateContext:c})}).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=g.render("tool_dataprivacy/request_details",a),c={approvedeny:a.approvedeny,canmarkcomplete:a.canmarkcomplete};return e.create({title:a.typename,body:b,type:h.TYPE,large:!0,templateContext:c})}).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'
};
/**
@@ -103,12 +129,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.
@@ -137,34 +163,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 = [
@@ -177,7 +327,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 = [
@@ -190,7 +351,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 = [
@@ -203,7 +375,6 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
component: 'tool_dataprivacy'
}
];
wsfunction = 'tool_dataprivacy_mark_complete';
break;
}
@@ -221,7 +392,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);
+259 -83
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() : array {
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)";
@@ -425,8 +455,8 @@ class api {
self::DATAREQUEST_STATUS_EXPIRED,
self::DATAREQUEST_STATUS_DELETED,
];
list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED);
$select = 'type = :type AND userid = :userid AND status NOT ' . $insql;
list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false);
$select = "type = :type AND userid = :userid AND status {$insql}";
$params = array_merge([
'type' => $type,
'userid' => $userid
@@ -435,6 +465,48 @@ class api {
return data_request::record_exists_select($select, $params);
}
/**
* Find whether any ongoing requests exist for a set of users.
*
* @param array $userids
* @return array
*/
public static function find_ongoing_request_types_for_users(array $userids) : array {
global $DB;
if (empty($userids)) {
return [];
}
// Check if the user already has an incomplete data request of the same type.
$nonpendingstatuses = [
self::DATAREQUEST_STATUS_COMPLETE,
self::DATAREQUEST_STATUS_CANCELLED,
self::DATAREQUEST_STATUS_REJECTED,
self::DATAREQUEST_STATUS_DOWNLOAD_READY,
self::DATAREQUEST_STATUS_EXPIRED,
self::DATAREQUEST_STATUS_DELETED,
];
list($statusinsql, $statusparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false);
list($userinsql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'us');
$select = "userid {$userinsql} AND status {$statusinsql}";
$params = array_merge($statusparams, $userparams);
$requests = $DB->get_records_select(data_request::TABLE, $select, $params, 'userid', 'id, userid, type');
$returnval = [];
foreach ($userids as $userid) {
$returnval[$userid] = (object) [];
}
foreach ($requests as $request) {
$returnval[$request->userid]->{$request->type} = true;
}
return $returnval;
}
/**
* Determines whether a request is active or not based on its status.
*
@@ -619,6 +691,8 @@ class api {
'requestedby' => $requestedby->fullname,
'requesttype' => $typetext,
'requestdate' => userdate($requestdata->timecreated),
'requestorigin' => $SITE->fullname,
'requestoriginurl' => new moodle_url('/'),
'requestcomments' => $requestdata->messagehtml,
'datarequestsurl' => $datarequestsurl
];
@@ -644,16 +718,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.
*
@@ -709,8 +798,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();
@@ -724,8 +811,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 = '';
}
@@ -745,8 +830,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.');
@@ -760,8 +843,6 @@ class api {
* @return \tool_dataprivacy\purpose[]
*/
public static function get_purposes() {
self::check_can_manage_data_registry();
return purpose::get_records([], 'name', 'ASC');
}
@@ -772,8 +853,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();
@@ -787,8 +866,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);
@@ -804,8 +881,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.');
@@ -819,8 +894,6 @@ class api {
* @return \tool_dataprivacy\category[]
*/
public static function get_categories() {
self::check_can_manage_data_registry();
return category::get_records([], 'name', 'ASC');
}
@@ -831,8 +904,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);
@@ -859,7 +930,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();
}
@@ -873,9 +943,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');
@@ -906,8 +973,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;
}
@@ -922,8 +988,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;
}
@@ -935,16 +1000,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');
}
/**
@@ -955,7 +1018,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;
}
@@ -970,8 +1032,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,
@@ -989,8 +1049,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();
}
@@ -1003,8 +1061,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();
}
@@ -1018,6 +1074,7 @@ class api {
*/
public static function add_request_contexts_with_status(contextlist_collection $clcollection, int $requestid, int $status) {
$request = new data_request($requestid);
$user = \core_user::get_user($request->get('userid'));
foreach ($clcollection as $contextlist) {
// Convert the \core_privacy\local\request\contextlist into a contextlist persistent and store it.
$clp = \tool_dataprivacy\contextlist::from_contextlist($contextlist);
@@ -1028,10 +1085,14 @@ class api {
foreach ($contextlist->get_contextids() as $contextid) {
if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
$context = \context::instance_by_id($contextid);
if (($purpose = static::get_effective_context_purpose($context)) && !empty($purpose->get('protected'))) {
$purpose = static::get_effective_context_purpose($context);
// Data can only be deleted from it if the context is either expired, or unprotected.
if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $user)) {
continue;
}
}
$context = new contextlist_context();
$context->set('contextid', $contextid)
->set('contextlistid', $contextlistid)
@@ -1129,6 +1190,15 @@ class api {
$contexts = [];
}
if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
$context = \context::instance_by_id($record->contextid);
$purpose = static::get_effective_context_purpose($context);
// Data can only be deleted from it if the context is either expired, or unprotected.
if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $foruser)) {
continue;
}
}
$contexts[] = $record->contextid;
$lastcomponent = $record->component;
}
@@ -1141,4 +1211,110 @@ 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;
}
/**
* Format the supplied date interval as a retention period.
*
* @param \DateInterval $interval
* @return string
*/
public static function format_retention_period(\DateInterval $interval) : string {
// It is one or another.
if ($interval->y) {
$formattedtime = get_string('numyears', 'moodle', $interval->format('%y'));
} else if ($interval->m) {
$formattedtime = get_string('nummonths', 'moodle', $interval->format('%m'));
} else if ($interval->d) {
$formattedtime = get_string('numdays', 'moodle', $interval->format('%d'));
} else {
$formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy');
}
return $formattedtime;
}
}
+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
],
];
}
@@ -144,8 +160,6 @@ class data_request extends persistent {
return $result;
}
/**
* Fetch completed data requests which are due to expire.
*
@@ -210,4 +224,68 @@ class data_request extends persistent {
}
}
}
/**
* Whether this request is in a state appropriate for reset/resubmission.
*
* Note: This does not check whether any other completed requests exist for this user.
*
* @return bool
*/
public function is_resettable() : bool {
if (api::DATAREQUEST_TYPE_OTHERS == $this->get('type')) {
// It is not possible to reset 'other' reqeusts.
return false;
}
$resettable = [
api::DATAREQUEST_STATUS_AWAITING_APPROVAL => true,
api::DATAREQUEST_STATUS_APPROVED => true,
api::DATAREQUEST_STATUS_REJECTED => true,
];
return isset($resettable[$this->get('status')]);
}
/**
* Whether this request is 'active'.
*
* @return bool
*/
public function is_active() : bool {
$active = [
api::DATAREQUEST_STATUS_AWAITING_APPROVAL => true,
api::DATAREQUEST_STATUS_APPROVED => true,
];
return isset($active[$this->get('status')]);
}
/**
* Reject this request and resubmit it as a fresh request.
*
* Note: This does not check whether any other completed requests exist for this user.
*
* @return self
*/
public function resubmit_request() : data_request {
if ($this->is_active()) {
$this->set('status', api::DATAREQUEST_STATUS_REJECTED)->save();
}
if (!$this->is_resettable()) {
throw new \moodle_exception('cannotreset', 'tool_dataprivacy');
}
$currentdata = $this->to_record();
unset($currentdata->id);
$clone = api::create_data_request($this->get('userid'), $this->get('type'));
$clone->set('comments', $this->get('comments'));
$clone->set('dpo', $this->get('dpo'));
$clone->set('requestedby', $this->get('requestedby'));
$clone->save();
return $clone;
}
}
@@ -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);
}
}
}
}
@@ -60,12 +60,27 @@ class expired_context extends \core\persistent {
* @return array
*/
protected static function define_properties() {
return array(
'contextid' => array(
return [
'contextid' => [
'type' => PARAM_INT,
'description' => 'The context id.',
),
'status' => array(
],
'defaultexpired' => [
'type' => PARAM_INT,
'description' => 'Whether to default retention period for the purpose has been reached',
'default' => 1,
],
'expiredroles' => [
'type' => PARAM_TEXT,
'description' => 'This list of roles to include during deletion',
'default' => '',
],
'unexpiredroles' => [
'type' => PARAM_TEXT,
'description' => 'This list of roles to exclude during deletion',
'default' => '',
],
'status' => [
'choices' => [
self::STATUS_EXPIRED,
self::STATUS_APPROVED,
@@ -73,8 +88,8 @@ class expired_context extends \core\persistent {
],
'type' => PARAM_INT,
'description' => 'The deletion status of the context.',
),
);
],
];
}
/**
@@ -159,4 +174,205 @@ class expired_context extends \core\persistent {
return $DB->count_records_sql($sql, $params);
}
/**
* Set the list of role IDs for either expiredroles, or unexpiredroles.
*
* @param string $field
* @param int[] $roleids
* @return expired_context
*/
protected function set_roleids_for(string $field, array $roleids) : expired_context {
$roledata = json_encode($roleids);
$this->raw_set($field, $roledata);
return $this;
}
/**
* Get the list of role IDs for either expiredroles, or unexpiredroles.
*
* @param string $field
* @return int[]
*/
protected function get_roleids_for(string $field) {
$value = $this->raw_get($field);
if (empty($value)) {
return [];
}
return json_decode($value);
}
/**
* Set the list of unexpired role IDs.
*
* @param int[] $roleids
* @return expired_context
*/
protected function set_unexpiredroles(array $roleids) : expired_context {
$this->set_roleids_for('unexpiredroles', $roleids);
return $this;
}
/**
* Add a set of role IDs to the list of expired role IDs.
*
* @param int[] $roleids
* @return expired_context
*/
public function add_expiredroles(array $roleids) : expired_context {
$existing = $this->get('expiredroles');
$newvalue = array_merge($existing, $roleids);
$this->set('expiredroles', $newvalue);
return $this;
}
/**
* Add a set of role IDs to the list of unexpired role IDs.
*
* @param int[] $roleids
* @return unexpired_context
*/
public function add_unexpiredroles(array $roleids) : expired_context {
$existing = $this->get('unexpiredroles');
$newvalue = array_merge($existing, $roleids);
$this->set('unexpiredroles', $newvalue);
return $this;
}
/**
* Set the list of expired role IDs.
*
* @param int[] $roleids
* @return expired_context
*/
protected function set_expiredroles(array $roleids) : expired_context {
$this->set_roleids_for('expiredroles', $roleids);
return $this;
}
/**
* Get the list of expired role IDs.
*
* @return int[]
*/
protected function get_expiredroles() {
return $this->get_roleids_for('expiredroles');
}
/**
* Get the list of unexpired role IDs.
*
* @return int[]
*/
protected function get_unexpiredroles() {
return $this->get_roleids_for('unexpiredroles');
}
/**
* Create a new expired_context based on the context, and expiry_info object.
*
* @param \context $context
* @param expiry_info $info
* @param boolean $save
* @return expired_context
*/
public static function create_from_expiry_info(\context $context, expiry_info $info, bool $save = true) : expired_context {
$record = (object) [
'contextid' => $context->id,
'status' => self::STATUS_EXPIRED,
'defaultexpired' => (int) $info->is_default_expired(),
];
$expiredcontext = new static(0, $record);
$expiredcontext->set('expiredroles', $info->get_expired_roles());
$expiredcontext->set('unexpiredroles', $info->get_unexpired_roles());
if ($save) {
$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) : expired_context {
$save = false;
// Compare the expiredroles.
$thisexpired = $this->get('expiredroles');
$infoexpired = $info->get_expired_roles();
sort($thisexpired);
sort($infoexpired);
if ($infoexpired != $thisexpired) {
$this->set('expiredroles', $infoexpired);
$save = true;
}
// Compare the unexpiredroles.
$thisunexpired = $this->get('unexpiredroles');
$infounexpired = $info->get_unexpired_roles();
sort($thisunexpired);
sort($infounexpired);
if ($infounexpired != $thisunexpired) {
$this->set('unexpiredroles', $infounexpired);
$save = true;
}
if (empty($this->get('defaultexpired')) == $info->is_default_expired()) {
$this->set('defaultexpired', (int) $info->is_default_expired());
$save = true;
}
if ($save) {
$this->set('status', self::STATUS_EXPIRED);
$this->save();
}
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() : bool {
return ($this->get('status') == self::STATUS_APPROVED);
}
/**
* Check whether this expired_context record has already been cleaned.
*
* @return bool
*/
public function is_complete() : bool {
return ($this->get('status') == self::STATUS_CLEANED);
}
/**
* 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() : bool {
return $this->get('defaultexpired') && empty($this->get('unexpiredroles'));
}
}
File diff suppressed because it is too large Load Diff
@@ -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,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/>.
/**
* 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 $fullyexpired = false;
/** @var bool Whether the default expiry value of this purpose has been reached */
protected $defaultexpiryreached = false;
/** @var bool Whether the default purpose is protected */
protected $defaultprotected = false;
/** @var int[] List of expires roles */
protected $expired = [];
/** @var int[] List of unexpires roles */
protected $unexpired = [];
/** @var int[] List of unexpired roles which are also protected */
protected $protectedroles = [];
/**
* Constructor for the expiry_info class.
*
* @param bool $default Whether the default expiry period for this context has been reached.
* @param bool $defaultprotected Whether the default expiry is protected.
* @param int[] $expired A list of roles in this context which have explicitly expired.
* @param int[] $unexpired A list of roles in this context which have not yet expired.
* @param int[] $protectedroles A list of unexpired roles in this context which are protected.
*/
public function __construct(bool $default, bool $defaultprotected, array $expired, array $unexpired, array $protectedroles) {
$this->defaultexpiryreached = $default;
$this->defaultprotected = $defaultprotected;
$this->expired = $expired;
$this->unexpired = $unexpired;
$this->protectedroles = $protectedroles;
}
/**
* 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() : bool {
return $this->defaultexpiryreached && empty($this->unexpired);
}
/**
* Whether any part of this context has expired.
*
* @return bool
*/
public function is_any_expired() : bool {
if ($this->is_fully_expired()) {
return true;
}
if (!empty($this->get_expired_roles())) {
return true;
}
if ($this->is_default_expired()) {
return true;
}
return false;
}
/**
* Get the list of explicitly expired role IDs.
* Note: This does not list roles which have been expired via the default retention policy being reached.
*
* @return int[]
*/
public function get_expired_roles() : array {
if ($this->is_default_expired()) {
return [];
}
return $this->expired;
}
/**
* Check whether the specified role is explicitly expired.
* Note: This does not list roles which have been expired via the default retention policy being reached.
*
* @param int $roleid
* @return bool
*/
public function is_role_expired(int $roleid) : bool {
return false !== array_search($roleid, $this->expired);
}
/**
* Whether the default retention policy has been reached.
*
* @return bool
*/
public function is_default_expired() : bool {
return $this->defaultexpiryreached;
}
/**
* Whether the default purpose is protected.
*
* @return bool
*/
public function is_default_protected() : bool {
return $this->defaultprotected;
}
/**
* Get the list of unexpired role IDs.
*
* @return int[]
*/
public function get_unexpired_roles() : array {
return $this->unexpired;
}
/**
* Get the list of unexpired protected roles.
*
* @return int[]
*/
public function get_unexpired_protected_roles() : array {
return array_keys(array_filter($this->protectedroles));
}
/**
* Get a list of all overridden roles which are unprotected.
* @return int[]
*/
public function get_unprotected_overridden_roles() : array {
$allroles = array_merge($this->expired, $this->unexpired);
return array_diff($allroles, $this->protectedroles);
}
/**
* 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) : expiry_info {
if ($child->is_fully_expired()) {
return $this;
}
// If the child is not fully expired, then none of the parents can be either.
$this->fullyexpired = false;
// Remove any role in this node which is not expired in the child.
foreach ($this->expired as $key => $roleid) {
if (!$child->is_role_expired($roleid)) {
unset($this->expired[$key]);
}
}
array_merge($this->unexpired, $child->get_unexpired_roles());
if (!$child->is_default_expired()) {
$this->defaultexpiryreached = 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;
}
}
}
+28 -13
View File
@@ -26,9 +26,9 @@ defined('MOODLE_INTERNAL') || die();
use coding_exception;
use core\external\persistent_exporter;
use DateInterval;
use Exception;
use renderer_base;
use tool_dataprivacy\context_instance;
use tool_dataprivacy\purpose;
/**
@@ -78,6 +78,9 @@ class purpose_exporter extends persistent_exporter {
'multiple' => true,
'optional' => true
],
'roleoverrides' => [
'type' => PARAM_TEXT
],
];
}
@@ -124,23 +127,35 @@ class purpose_exporter extends persistent_exporter {
$retentionperiod = $this->persistent->get('retentionperiod');
if ($retentionperiod) {
$interval = new DateInterval($retentionperiod);
// It is one or another.
if ($interval->y) {
$formattedtime = get_string('numyears', 'moodle', $interval->format('%y'));
} else if ($interval->m) {
$formattedtime = get_string('nummonths', 'moodle', $interval->format('%m'));
} else if ($interval->d) {
$formattedtime = get_string('numdays', 'moodle', $interval->format('%d'));
} else {
$formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy');
}
$formattedtime = \tool_dataprivacy\api::format_retention_period(new \DateInterval($retentionperiod));
} else {
$formattedtime = get_string('retentionperiodnotdefined', 'tool_dataprivacy');
}
$values['formattedretentionperiod'] = $formattedtime;
$values['roleoverrides'] = !empty($this->persistent->get_purpose_overrides());
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;
}
}
}
@@ -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/>.
/**
* An implementation of a userlist which has been filtered and approved.
*
* @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;
defined('MOODLE_INTERNAL') || die();
/**
* An implementation of a userlist which can be filtered by role.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class filtered_userlist extends \core_privacy\local\request\approved_userlist {
/**
* Apply filters to only remove users in the expireduserids list, and to remove any who are in the unexpired list.
* The unexpired list wins where a user is in both lists.
*
* @param int[] $expireduserids The list of userids for users who should be expired.
* @param int[] $unexpireduserids The list of userids for those users who should not be expired.
* @return $this
*/
public function apply_expired_context_filters(array $expireduserids, array $unexpireduserids) : filtered_userlist {
// The current userlist content.
$userids = $this->get_userids();
if (!empty($expireduserids)) {
// Now remove any not on the list of expired users.
$userids = array_intersect($userids, $expireduserids);
}
if (!empty($unexpireduserids)) {
// Remove any on the list of unexpiredusers users.
$userids = array_diff($userids, $unexpireduserids);
}
$this->set_userids($userids);
return $this;
}
}
@@ -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'));
}
}
+456 -31
View File
@@ -41,6 +41,11 @@ class purpose extends persistent {
*/
protected static $persistentclass = 'tool_dataprivacy\\purpose';
/**
* @var array The list of current overrides.
*/
protected $existingoverrides = [];
/**
* Define the form - called by parent constructor
*/
@@ -56,41 +61,16 @@ class purpose extends persistent {
$mform->setType('description', PARAM_CLEANHTML);
// Field for selecting lawful bases (from GDPR Article 6.1).
$lawfulbases = [];
foreach (\tool_dataprivacy\purpose::GDPR_ART_6_1_ITEMS as $article) {
$key = 'gdpr_art_6_1_' . $article;
$lawfulbases[$key] = get_string($key . '_name', 'tool_dataprivacy');
}
$options = array(
'multiple' => true,
);
$mform->addElement('autocomplete', 'lawfulbases', get_string('lawfulbases', 'tool_dataprivacy'), $lawfulbases, $options);
$this->add_field($this->get_lawful_base_field());
$mform->addRule('lawfulbases', get_string('required'), 'required', null, 'server');
$mform->addHelpButton('lawfulbases', 'lawfulbases', 'tool_dataprivacy');
// Optional field for selecting reasons for collecting sensitive personal data (from GDPR Article 9.2).
$sensitivereasons = [];
foreach (\tool_dataprivacy\purpose::GDPR_ART_9_2_ITEMS as $article) {
$key = 'gdpr_art_9_2_' . $article;
$sensitivereasons[$key] = get_string($key . '_name', 'tool_dataprivacy');
}
$mform->addElement('autocomplete', 'sensitivedatareasons', get_string('sensitivedatareasons', 'tool_dataprivacy'),
$sensitivereasons, $options);
$mform->addHelpButton('sensitivedatareasons', 'sensitivedatareasons', 'tool_dataprivacy');
$this->add_field($this->get_sensitive_base_field());
$number = $mform->createElement('text', 'retentionperiodnumber', null, ['size' => 8]);
$unitoptions = [
'Y' => get_string('years'),
'M' => strtolower(get_string('months')),
'D' => strtolower(get_string('days'))
];
$unit = $mform->createElement('select', 'retentionperiodunit', '', $unitoptions);
$mform->addGroup(['number' => $number, 'unit' => $unit], 'retentionperiod',
get_string('retentionperiod', 'tool_dataprivacy'), null, false);
$mform->setType('retentionperiodnumber', PARAM_INT);
$this->add_field($this->get_retention_period_fields());
$this->add_field($this->get_protected_field());
$this->_form->addElement('advcheckbox', 'protected', get_string('protected', 'tool_dataprivacy'),
get_string('protectedlabel', 'tool_dataprivacy'));
$this->add_override_fields();
if (!empty($this->_customdata['showbuttons'])) {
if (!$this->get_persistent()->get('id')) {
@@ -102,16 +82,360 @@ class purpose extends persistent {
}
}
/**
* Add a fieldset to the current form.
*
* @param \stdClass $data
*/
protected function add_field(\stdClass $data) {
foreach ($data->fields as $field) {
$this->_form->addElement($field);
}
if (!empty($data->helps)) {
foreach ($data->helps as $fieldname => $helpdata) {
$help = array_merge([$fieldname], $helpdata);
call_user_func_array([$this->_form, 'addHelpButton'], $help);
}
}
if (!empty($data->types)) {
foreach ($data->types as $fieldname => $type) {
$this->_form->setType($fieldname, $type);
}
}
if (!empty($data->rules)) {
foreach ($data->rules as $fieldname => $ruledata) {
$rule = array_merge([$fieldname], $ruledata);
call_user_func_array([$this->_form, 'addRule'], $rule);
}
}
if (!empty($data->defaults)) {
foreach ($data->defaults as $fieldname => $default) {
$this->_form($fieldname, $default);
}
}
}
/**
* Handle addition of relevant repeated element fields for role overrides.
*/
protected function add_override_fields() {
$purpose = $this->get_persistent();
if (empty($purpose->get('id'))) {
// It is not possible to use repeated elements in a modal form yet.
return;
}
$fields = [
$this->get_role_override_id('roleoverride_'),
$this->get_role_field('roleoverride_'),
$this->get_retention_period_fields('roleoverride_'),
$this->get_protected_field('roleoverride_'),
$this->get_lawful_base_field('roleoverride_'),
$this->get_sensitive_base_field('roleoverride_'),
];
$options = [
'type' => [],
'helpbutton' => [],
];
// Start by adding the title.
$overrideelements = [
$this->_form->createElement('header', 'roleoverride', get_string('roleoverride', 'tool_dataprivacy')),
$this->_form->createElement(
'static',
'roleoverrideoverview',
'',
get_string('roleoverrideoverview', 'tool_dataprivacy')
),
];
foreach ($fields as $fielddata) {
foreach ($fielddata->fields as $field) {
$overrideelements[] = $field;
}
if (!empty($fielddata->helps)) {
foreach ($fielddata->helps as $name => $help) {
if (!isset($options[$name])) {
$options[$name] = [];
}
$options[$name]['helpbutton'] = $help;
}
}
if (!empty($fielddata->types)) {
foreach ($fielddata->types as $name => $type) {
if (!isset($options[$name])) {
$options[$name] = [];
}
$options[$name]['type'] = $type;
}
}
if (!empty($fielddata->rules)) {
foreach ($fielddata->rules as $name => $rule) {
if (!isset($options[$name])) {
$options[$name] = [];
}
$options[$name]['rule'] = $rule;
}
}
if (!empty($fielddata->defaults)) {
foreach ($fielddata->defaults as $name => $default) {
if (!isset($options[$name])) {
$options[$name] = [];
}
$options[$name]['default'] = $default;
}
}
if (!empty($fielddata->advanceds)) {
foreach ($fielddata->advanceds as $name => $advanced) {
if (!isset($options[$name])) {
$options[$name] = [];
}
$options[$name]['advanced'] = $advanced;
}
}
}
$this->existingoverrides = $purpose->get_purpose_overrides();
$existingoverridecount = count($this->existingoverrides);
$this->repeat_elements(
$overrideelements,
$existingoverridecount,
$options,
'overrides',
'addoverride',
1,
get_string('addroleoverride', 'tool_dataprivacy')
);
}
/**
* Converts fields.
*
* @param \stdClass $data
* @return \stdClass
*/
public function filter_data_for_persistent($data) {
$data = parent::filter_data_for_persistent($data);
$classname = static::$persistentclass;
$properties = $classname::properties_definition();
$data = (object) array_filter((array) $data, function($value, $key) use ($properties) {
return isset($properties[$key]);
}, ARRAY_FILTER_USE_BOTH);
return $data;
}
/**
* Get the field for the role name.
*
* @param string $prefix The prefix to apply to the field
* @return \stdClass
*/
protected function get_role_override_id(string $prefix = '') : \stdClass {
$fieldname = "{$prefix}id";
$fielddata = (object) [
'fields' => [],
];
$fielddata->fields[] = $this->_form->createElement('hidden', $fieldname);
$fielddata->types[$fieldname] = PARAM_INT;
return $fielddata;
}
/**
* Get the field for the role name.
*
* @param string $prefix The prefix to apply to the field
* @return \stdClass
*/
protected function get_role_field(string $prefix = '') : \stdClass {
$fieldname = "{$prefix}roleid";
$fielddata = (object) [
'fields' => [],
'helps' => [],
];
$roles = [
'' => get_string('none'),
];
foreach (role_get_names() as $roleid => $role) {
$roles[$roleid] = $role->localname;
}
$fielddata->fields[] = $this->_form->createElement('select', $fieldname, get_string('role'),
$roles,
[
'multiple' => false,
]
);
$fielddata->helps[$fieldname] = ['role', 'tool_dataprivacy'];
$fielddata->defaults[$fieldname] = null;
return $fielddata;
}
/**
* Get the mform field for lawful bases.
*
* @param string $prefix The prefix to apply to the field
* @return \stdClass
*/
protected function get_lawful_base_field(string $prefix = '') : \stdClass {
$fieldname = "{$prefix}lawfulbases";
$data = (object) [
'fields' => [],
];
$bases = [];
foreach (\tool_dataprivacy\purpose::GDPR_ART_6_1_ITEMS as $article) {
$key = 'gdpr_art_6_1_' . $article;
$bases[$key] = get_string("{$key}_name", 'tool_dataprivacy');
}
$data->fields[] = $this->_form->createElement('autocomplete', $fieldname, get_string('lawfulbases', 'tool_dataprivacy'),
$bases,
[
'multiple' => true,
]
);
$data->helps = [
$fieldname => ['lawfulbases', 'tool_dataprivacy'],
];
$data->advanceds = [
$fieldname => true,
];
return $data;
}
/**
* Get the mform field for sensitive bases.
*
* @param string $prefix The prefix to apply to the field
* @return \stdClass
*/
protected function get_sensitive_base_field(string $prefix = '') : \stdClass {
$fieldname = "{$prefix}sensitivedatareasons";
$data = (object) [
'fields' => [],
];
$bases = [];
foreach (\tool_dataprivacy\purpose::GDPR_ART_9_2_ITEMS as $article) {
$key = 'gdpr_art_9_2_' . $article;
$bases[$key] = get_string("{$key}_name", 'tool_dataprivacy');
}
$data->fields[] = $this->_form->createElement(
'autocomplete',
$fieldname,
get_string('sensitivedatareasons', 'tool_dataprivacy'),
$bases,
[
'multiple' => true,
]
);
$data->helps = [
$fieldname => ['sensitivedatareasons', 'tool_dataprivacy'],
];
$data->advanceds = [
$fieldname => true,
];
return $data;
}
/**
* Get the retention period fields.
*
* @param string $prefix The name of the main field, and prefix for the subfields.
* @return \stdClass
*/
protected function get_retention_period_fields(string $prefix = '') : \stdClass {
$prefix = "{$prefix}retentionperiod";
$data = (object) [
'fields' => [],
'types' => [],
];
$number = $this->_form->createElement('text', "{$prefix}number", null, ['size' => 8]);
$data->types["{$prefix}number"] = PARAM_INT;
$unitoptions = [
'Y' => get_string('years'),
'M' => strtolower(get_string('months')),
'D' => strtolower(get_string('days'))
];
$unit = $this->_form->createElement('select', "{$prefix}unit", '', $unitoptions);
$data->fields[] = $this->_form->createElement(
'group',
$prefix,
get_string('retentionperiod', 'tool_dataprivacy'),
[
'number' => $number,
'unit' => $unit,
],
null,
false
);
return $data;
}
/**
* Get the mform field for the protected flag.
*
* @param string $prefix The prefix to apply to the field
* @return \stdClass
*/
protected function get_protected_field(string $prefix = '') : \stdClass {
$fieldname = "{$prefix}protected";
return (object) [
'fields' => [
$this->_form->createElement(
'advcheckbox',
$fieldname,
get_string('protected', 'tool_dataprivacy'),
get_string('protectedlabel', 'tool_dataprivacy')
),
],
];
}
/**
* Converts data to data suitable for storage.
*
* @param \stdClass $data
* @return \stdClass
*/
protected static function convert_fields(\stdClass $data) {
$data = parent::convert_fields($data);
if (is_array($data->lawfulbases)) {
if (!empty($data->lawfulbases) && is_array($data->lawfulbases)) {
$data->lawfulbases = implode(',', $data->lawfulbases);
}
if (!empty($data->sensitivedatareasons) && is_array($data->sensitivedatareasons)) {
@@ -122,6 +446,7 @@ class purpose extends persistent {
$data->retentionperiod = 'P' . $data->retentionperiodnumber . $data->retentionperiodunit;
unset($data->retentionperiodnumber);
unset($data->retentionperiodunit);
return $data;
}
@@ -133,6 +458,16 @@ class purpose extends persistent {
protected function get_default_data() {
$data = parent::get_default_data();
return $this->convert_existing_data_to_values($data);
}
/**
* Normalise any values stored in existing data.
*
* @param \stdClass $data
* @return \stdClass
*/
protected function convert_existing_data_to_values(\stdClass $data) : \stdClass {
$data->lawfulbases = explode(',', $data->lawfulbases);
if (!empty($data->sensitivedatareasons)) {
$data->sensitivedatareasons = explode(',', $data->sensitivedatareasons);
@@ -146,4 +481,94 @@ class purpose extends persistent {
return $data;
}
/**
* Fetch the role override data from the list of submitted data.
*
* @param \stdClass $data The complete set of processed data
* @return \stdClass[] The list of overrides
*/
public function get_role_overrides_from_data(\stdClass $data) {
$overrides = [];
if (!empty($data->overrides)) {
$searchkey = 'roleoverride_';
for ($i = 0; $i < $data->overrides; $i++) {
$overridedata = (object) [];
foreach ((array) $data as $fieldname => $value) {
if (strpos($fieldname, $searchkey) !== 0) {
continue;
}
$overridefieldname = substr($fieldname, strlen($searchkey));
$overridedata->$overridefieldname = $value[$i];
}
if (empty($overridedata->roleid) || empty($overridedata->retentionperiodnumber)) {
// Skip this one.
// There is no value and it will be delete.
continue;
}
$override = static::convert_fields($overridedata);
$overrides[$i] = $override;
}
}
return $overrides;
}
/**
* Define extra validation mechanims.
*
* @param stdClass $data Data to validate.
* @param array $files Array of files.
* @param array $errors Currently reported errors.
* @return array of additional errors, or overridden errors.
*/
protected function extra_validation($data, $files, array &$errors) {
$overrides = $this->get_role_overrides_from_data($data);
// Check role overrides to ensure that:
// - roles are unique; and
// - specifeid retention periods are numeric.
$seenroleids = [];
foreach ($overrides as $id => $override) {
$override->purposeid = 0;
$persistent = new \tool_dataprivacy\purpose_override($override->id, $override);
if (isset($seenroleids[$persistent->get('roleid')])) {
$errors["roleoverride_roleid[{$id}]"] = get_string('duplicaterole');
}
$seenroleids[$persistent->get('roleid')] = true;
$errors = array_merge($errors, $persistent->get_errors());
}
return $errors;
}
/**
* Load in existing data as form defaults. Usually new entry defaults are stored directly in
* form definition (new entry form); this function is used to load in data where values
* already exist and data is being edited (edit entry form).
*
* @param stdClass $data
*/
public function set_data($data) {
$purpose = $this->get_persistent();
$count = 0;
foreach ($this->existingoverrides as $override) {
$overridedata = $this->convert_existing_data_to_values($override->to_record());
foreach ($overridedata as $key => $value) {
$keyname = "roleoverride_{$key}[{$count}]";
$data->$keyname = $value;
}
$count++;
}
parent::set_data($data);
}
}
@@ -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,31 @@ class metadata_registry {
if (isset($contributedplugins[$plugintype][$shortname])) {
$internaldata['external'] = true;
}
// Additional interface checks.
if (!$manager->is_empty_subsystem($component)) {
$classname = $manager->get_provider_classname_for_component($component);
if (class_exists($classname)) {
$componentclass = new $classname();
// Check if the interface is deprecated.
if ($componentclass instanceof \core_privacy\local\deprecated) {
$internaldata['deprecated'] = true;
}
// Check that the core_userlist_provider is implemented for all user data providers.
if ($componentclass instanceof \core_privacy\local\request\core_user_data_provider
&& !$componentclass instanceof \core_privacy\local\request\core_userlist_provider) {
$internaldata['userlistnoncompliance'] = true;
}
// Check that any type of userlist_provider is implemented for all shared data providers.
if ($componentclass instanceof \core_privacy\local\request\shared_data_provider
&& !$componentclass instanceof \core_privacy\local\request\userlist_provider) {
$internaldata['userlistnoncompliance'] = 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,41 @@ class data_requests_table extends table_sql {
/** @var \tool_dataprivacy\data_request[] Array of data request persistents. */
protected $datarequests = [];
/** @var \stdClass[] List of userids and whether they have any ongoing active requests. */
protected $ongoingrequests = [];
/** @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 +108,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)]);
}
}
/**
@@ -216,6 +252,20 @@ class data_requests_table extends table_sql {
break;
}
if ($this->manage) {
$persistent = $this->datarequests[$requestid];
$canreset = $persistent->is_active() || empty($this->ongoingrequests[$data->foruser->id]->{$data->type});
$canreset = $canreset && $persistent->is_resettable();
if ($canreset) {
$reseturl = new moodle_url('/admin/tool/dataprivacy/resubmitrequest.php', [
'requestid' => $requestid,
]);
$actiondata = ['data-action' => 'reset', 'data-requestid' => $requestid];
$actiontext = get_string('resubmitrequestasnew', 'tool_dataprivacy');
$actions[] = new action_menu_link_secondary($reseturl, null, $actiontext, $actiondata);
}
}
$actionsmenu = new action_menu($actions);
$actionsmenu->set_menu_trigger(get_string('actions'));
$actionsmenu->set_owner_selector('request-actions-' . $requestid);
@@ -242,23 +292,31 @@ 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 = [];
$context = \context_system::instance();
$renderer = $PAGE->get_renderer('tool_dataprivacy');
$forusers = [];
foreach ($datarequests as $persistent) {
$this->datarequests[$persistent->get('id')] = $persistent;
$exporter = new data_request_exporter($persistent, ['context' => $context]);
$this->rawdata[] = $exporter->export($renderer);
$forusers[] = $persistent->get('userid');
}
// Fetch the list of all ongoing requests for the users currently shown.
// This is used to determine whether any non-active request can be resubmitted.
// There can only be one ongoing request of a type for each user.
$this->ongoingrequests = api::find_ongoing_request_types_for_users($forusers);
// Set initial bars.
if ($useinitialsbar) {
$this->initialbars($total > $pagesize);
@@ -290,4 +348,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(int $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() : int {
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(array $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() : array {
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;
}
}
@@ -57,9 +57,15 @@ class expired_contexts_table extends table_sql {
*/
protected $selectall = true;
/** @var purpose[] Array of purposes mapped to the contexts. */
/** @var purpose[] Array of purposes by their id. */
protected $purposes = [];
/** @var purpose[] Map of context => purpose. */
protected $purposemap = [];
/** @var array List of roles. */
protected $roles = [];
/**
* expired_contexts_table constructor.
*
@@ -77,6 +83,7 @@ class expired_contexts_table extends table_sql {
'purpose' => get_string('purpose', 'tool_dataprivacy'),
'category' => get_string('category', 'tool_dataprivacy'),
'retentionperiod' => get_string('retentionperiod', 'tool_dataprivacy'),
'tobedeleted' => get_string('tobedeleted', 'tool_dataprivacy'),
'timecreated' => get_string('expiry', 'tool_dataprivacy'),
];
$checkboxattrs = [
@@ -93,21 +100,25 @@ class expired_contexts_table extends table_sql {
$this->no_sorting('purpose');
$this->no_sorting('category');
$this->no_sorting('retentionperiod');
$this->no_sorting('tobedeleted');
// Make this table sorted by first name by default.
$this->sortable(true, 'timecreated');
// We use roles in several places.
$this->roles = role_get_names();
}
/**
* The context name column.
*
* @param stdClass $data The row data.
* @param stdClass $expiredctx The row data.
* @return string
* @throws coding_exception
*/
public function col_name($data) {
public function col_name($expiredctx) {
global $OUTPUT;
$context = context_helper::instance_by_id($data->contextid);
$context = context_helper::instance_by_id($expiredctx->get('contextid'));
$parent = $context->get_parent_context();
$contextdata = (object)[
'name' => $context->get_context_name(false, true),
@@ -128,14 +139,14 @@ class expired_contexts_table extends table_sql {
/**
* The context information column.
*
* @param stdClass $data The row data.
* @param stdClass $expiredctx The row data.
* @return string
* @throws coding_exception
*/
public function col_info($data) {
public function col_info($expiredctx) {
global $OUTPUT;
$context = context_helper::instance_by_id($data->contextid);
$context = context_helper::instance_by_id($expiredctx->get('contextid'));
$children = $context->get_child_contexts();
if (empty($children)) {
@@ -156,13 +167,13 @@ class expired_contexts_table extends table_sql {
/**
* The category name column.
*
* @param stdClass $data The row data.
* @param stdClass $expiredctx The row data.
* @return mixed
* @throws coding_exception
* @throws dml_exception
*/
public function col_category($data) {
$context = context_helper::instance_by_id($data->contextid);
public function col_category($expiredctx) {
$context = context_helper::instance_by_id($expiredctx->get('contextid'));
$category = api::get_effective_context_category($context);
return s($category->get('name'));
@@ -171,12 +182,12 @@ class expired_contexts_table extends table_sql {
/**
* The purpose column.
*
* @param stdClass $data The row data.
* @param stdClass $expiredctx The row data.
* @return string
* @throws coding_exception
*/
public function col_purpose($data) {
$purpose = $this->purposes[$data->contextid];
public function col_purpose($expiredctx) {
$purpose = $this->get_purpose_for_expiry($expiredctx);
return s($purpose->get('name'));
}
@@ -184,42 +195,114 @@ class expired_contexts_table extends table_sql {
/**
* The retention period column.
*
* @param stdClass $data The row data.
* @param stdClass $expiredctx The row data.
* @return string
* @throws Exception
*/
public function col_retentionperiod($data) {
global $PAGE;
public function col_retentionperiod($expiredctx) {
$purpose = $this->get_purpose_for_expiry($expiredctx);
$purpose = $this->purposes[$data->contextid];
$expiries = [];
$exporter = new purpose_exporter($purpose, ['context' => \context_system::instance()]);
$exportedpurpose = $exporter->export($PAGE->get_renderer('core'));
$expiry = html_writer::tag('dt', get_string('default'), ['class' => 'col-sm-3']);
if ($expiredctx->get('defaultexpired')) {
$expiries[get_string('default')] = get_string('expiredrolewithretention', 'tool_dataprivacy', (object) [
'retention' => api::format_retention_period(new \DateInterval($purpose->get('retentionperiod'))),
]);
} else {
$expiries[get_string('default')] = get_string('unexpiredrolewithretention', 'tool_dataprivacy', (object) [
'retention' => api::format_retention_period(new \DateInterval($purpose->get('retentionperiod'))),
]);
}
return $exportedpurpose->formattedretentionperiod;
if (!$expiredctx->is_fully_expired()) {
$purposeoverrides = $purpose->get_purpose_overrides();
foreach ($expiredctx->get('unexpiredroles') as $roleid) {
$role = $this->roles[$roleid];
$override = $purposeoverrides[$roleid];
$expiries[$role->localname] = get_string('unexpiredrolewithretention', 'tool_dataprivacy', (object) [
'retention' => api::format_retention_period(new \DateInterval($override->get('retentionperiod'))),
]);
}
foreach ($expiredctx->get('expiredroles') as $roleid) {
$role = $this->roles[$roleid];
$override = $purposeoverrides[$roleid];
$expiries[$role->localname] = get_string('expiredrolewithretention', 'tool_dataprivacy', (object) [
'retention' => api::format_retention_period(new \DateInterval($override->get('retentionperiod'))),
]);
}
}
$output = array_map(function($rolename, $expiry) {
$return = html_writer::tag('dt', $rolename, ['class' => 'col-sm-3']);
$return .= html_writer::tag('dd', $expiry, ['class' => 'col-sm-9']);
return $return;
}, array_keys($expiries), $expiries);
return html_writer::tag('dl', implode($output), ['class' => 'row']);
}
/**
* The timecreated a.k.a. the context expiry date column.
*
* @param stdClass $data The row data.
* @param stdClass $expiredctx The row data.
* @return string
*/
public function col_timecreated($data) {
return userdate($data->timecreated);
public function col_timecreated($expiredctx) {
return userdate($expiredctx->get('timecreated'));
}
/**
* Generate the select column.
*
* @param stdClass $data The row data.
* @param stdClass $expiredctx The row data.
* @return string
*/
public function col_select($data) {
$id = $data->id;
public function col_select($expiredctx) {
$id = $expiredctx->get('id');
return html_writer::checkbox('expiredcontext_' . $id, $id, $this->selectall, '', ['class' => 'selectcontext']);
}
/**
* Formatting for the 'tobedeleted' column which indicates in a friendlier fashion whose data will be removed.
*
* @param stdClass $expiredctx The row data.
* @return string
*/
public function col_tobedeleted($expiredctx) {
if ($expiredctx->is_fully_expired()) {
return get_string('defaultexpired', 'tool_dataprivacy');
}
$purpose = $this->get_purpose_for_expiry($expiredctx);
$a = (object) [];
$expiredroles = [];
foreach ($expiredctx->get('expiredroles') as $roleid) {
$expiredroles[] = html_writer::tag('li', $this->roles[$roleid]->localname);
}
$a->expired = html_writer::tag('ul', implode($expiredroles));
$unexpiredroles = [];
foreach ($expiredctx->get('unexpiredroles') as $roleid) {
$unexpiredroles[] = html_writer::tag('li', $this->roles[$roleid]->localname);
}
$a->unexpired = html_writer::tag('ul', implode($unexpiredroles));
if ($expiredctx->get('defaultexpired')) {
return get_string('defaultexpiredexcept', 'tool_dataprivacy', $a);
} else if (empty($unexpiredroles)) {
return get_string('defaultunexpired', 'tool_dataprivacy', $a);
} else {
return get_string('defaultunexpiredwithexceptions', 'tool_dataprivacy', $a);
}
}
/**
* Query the database for results to display in the table.
*
@@ -241,17 +324,16 @@ class expired_contexts_table extends table_sql {
// Only load expired contexts that are awaiting confirmation.
$expiredcontexts = expired_context::get_records_by_contextlevel($this->contextlevel, expired_context::STATUS_EXPIRED,
$sort, $this->get_page_start(), $this->get_page_size());
$this->rawdata = [];
$contextids = [];
foreach ($expiredcontexts as $persistent) {
$data = $persistent->to_record();
$context = context_helper::instance_by_id($data->contextid);
$purpose = api::get_effective_context_purpose($context);
$this->purposes[$data->contextid] = $purpose;
$this->rawdata[] = $data;
$this->rawdata[] = $persistent;
$contextids[] = $persistent->get('contextid');
}
$this->preload_contexts($contextids);
// Set initial bars.
if ($useinitialsbar) {
$this->initialbars($total > $pagesize);
@@ -281,4 +363,48 @@ class expired_contexts_table extends table_sql {
}
return '';
}
/**
* Get the purpose for the specified expired context.
*
* @param expired_context $expiredcontext
* @return purpose
*/
protected function get_purpose_for_expiry(expired_context $expiredcontext) : purpose {
$context = context_helper::instance_by_id($expiredcontext->get('contextid'));
if (empty($this->purposemap[$context->id])) {
$purpose = api::get_effective_context_purpose($context);
$this->purposemap[$context->id] = $purpose->get('id');
if (empty($this->purposes[$purpose->get('id')])) {
$this->purposes[$purpose->get('id')] = $purpose;
}
}
return $this->purposes[$this->purposemap[$context->id]];
}
/**
* Preload context records given a set of contextids.
*
* @param array $contextids
*/
protected function preload_contexts(array $contextids) {
global $DB;
if (empty($contextids)) {
return;
}
$ctxfields = \context_helper::get_preload_record_columns_sql('ctx');
list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
$sql = "SELECT {$ctxfields} FROM {context} ctx WHERE ctx.id {$insql}";
$contextlist = $DB->get_recordset_sql($sql, $inparams);
foreach ($contextlist as $contextdata) {
\context_helper::preload_from_record($contextdata);
}
$contextlist->close();
}
}
@@ -139,4 +139,15 @@ class renderer extends plugin_renderer_base {
$data = $page->export_for_template($this);
return parent::render_from_template('tool_dataprivacy/data_deletion', $data);
}
/**
* Render the user data retention summary page.
*
* @param summary_page $page
* @return string html for the page.
*/
public function render_summary_page(summary_page $page) {
$data = $page->export_for_template($this);
return parent::render_from_template('tool_dataprivacy/summary', $data);
}
}
@@ -0,0 +1,132 @@
<?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/>.
/**
* Summary page renderable.
*
* @package tool_dataprivacy
* @copyright 2018 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy\output;
defined('MOODLE_INTERNAL') || die();
use renderable;
use renderer_base;
use templatable;
/**
* Class containing the summary page renderable.
*
* @copyright 2018 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class summary_page implements renderable, templatable {
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output
* @return array
*/
public function export_for_template(renderer_base $output) {
$contextlevels = [
'contextlevelname10' => CONTEXT_SYSTEM,
'contextlevelname30' => CONTEXT_USER,
'contextlevelname40' => CONTEXT_COURSECAT,
'contextlevelname50' => CONTEXT_COURSE,
'contextlevelname70' => CONTEXT_MODULE,
'contextlevelname80' => CONTEXT_BLOCK
];
$data = [];
$context = \context_system::instance();
foreach ($contextlevels as $levelname => $level) {
$classname = \context_helper::get_class_for_level($level);
list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
$purposeid = get_config('tool_dataprivacy', $purposevar);
$categoryid = get_config('tool_dataprivacy', $categoryvar);
$section = [];
$section['contextname'] = get_string($levelname, 'tool_dataprivacy');
if (empty($purposeid)) {
list($purposeid, $categoryid) =
\tool_dataprivacy\data_registry::get_effective_default_contextlevel_purpose_and_category($level);
}
if ($purposeid == -1) {
$purposeid = 0;
}
$purpose = new \tool_dataprivacy\purpose($purposeid);
$export = new \tool_dataprivacy\external\purpose_exporter($purpose, ['context' => $context]);
$purposedata = $export->export($output);
$section['purpose'] = $purposedata;
if (empty($categoryid)) {
list($purposeid, $categoryid) =
\tool_dataprivacy\data_registry::get_effective_default_contextlevel_purpose_and_category($level);
}
if ($categoryid == -1) {
$categoryid = 0;
}
$category = new \tool_dataprivacy\category($categoryid);
$export = new \tool_dataprivacy\external\category_exporter($category, ['context' => $context]);
$categorydata = $export->export($output);
$section['category'] = $categorydata;
$data['contexts'][] = $section;
}
// Get activity module plugin info.
$pluginmanager = \core_plugin_manager::instance();
$modplugins = $pluginmanager->get_enabled_plugins('mod');
foreach ($modplugins as $name) {
$classname = \context_helper::get_class_for_level($contextlevels['contextlevelname70']);
list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname, $name);
$categoryid = get_config('tool_dataprivacy', $categoryvar);
$purposeid = get_config('tool_dataprivacy', $purposevar);
if ($categoryid === false && $purposeid === false) {
// If no purpose and category has been set for this plugin, then there's no need to show this on the list.
continue;
}
$section = [];
$section['contextname'] = $pluginmanager->plugin_name('mod_' . $name);
if ($purposeid == -1) {
$purposeid = 0;
}
$purpose = new \tool_dataprivacy\purpose($purposeid);
$export = new \tool_dataprivacy\external\purpose_exporter($purpose, ['context' => $context]);
$purposedata = $export->export($output);
$section['purpose'] = $purposedata;
if ($categoryid == -1) {
$categoryid = 0;
}
$category = new \tool_dataprivacy\category($categoryid);
$export = new \tool_dataprivacy\external\category_exporter($category, ['context' => $context]);
$categorydata = $export->export($output);
$section['category'] = $categorydata;
$data['contexts'][] = $section;
}
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) {
@@ -30,9 +30,11 @@ use context;
use context_user;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use \core_privacy\local\request\approved_userlist;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\helper;
use core_privacy\local\request\transform;
use \core_privacy\local\request\userlist;
use core_privacy\local\request\writer;
use dml_exception;
use stdClass;
@@ -50,6 +52,9 @@ class provider implements
// This tool stores user data.
\core_privacy\local\metadata\provider,
// This plugin is capable of determining which users have data within it.
\core_privacy\local\request\core_userlist_provider,
// This tool may provide access to and deletion of user data.
\core_privacy\local\request\plugin\provider,
@@ -76,6 +81,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;
}
@@ -98,6 +105,32 @@ class provider implements
return $contextlist;
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*
*/
public static function get_users_in_context(userlist $userlist) {
$context = $userlist->get_context();
if (!is_a($context, \context_user::class)) {
return;
}
$params = [
'contextlevel' => CONTEXT_USER,
'contextid' => $context->id,
];
$sql = "SELECT instanceid AS userid
FROM {context}
WHERE id = :contextid
AND contextlevel = :contextlevel";
$userlist->add_from_sql('userid', $sql, $params);
}
/**
* Export all user data for the specified user, in the specified contexts.
*
@@ -134,6 +167,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.
@@ -170,6 +205,15 @@ class provider implements
public static function delete_data_for_user(approved_contextlist $contextlist) {
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*
*/
public static function delete_data_for_users(approved_userlist $userlist) {
}
/**
* Export all user preferences for the plugin.
*
@@ -192,6 +236,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 +248,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'));
}
}
}
+9 -1
View File
@@ -162,7 +162,6 @@ class purpose extends \core\persistent {
* @return null
*/
public function is_used() {
if (\tool_dataprivacy\contextlevel::is_purpose_used($this->get('id')) ||
\tool_dataprivacy\context_instance::is_purpose_used($this->get('id'))) {
return true;
@@ -180,4 +179,13 @@ class purpose extends \core\persistent {
return false;
}
/**
* Get a list of the role purpose overrides for this purpose.
*
* @return array
*/
public function get_purpose_overrides() : array {
return purpose_override::get_overrides_for_purpose($this);
}
}
@@ -0,0 +1,143 @@
<?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 for loading/storing data purpose overrides from the DB.
*
* @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 stdClass;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
/**
* Class for loading/storing data purpose overrides from the DB.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class purpose_override extends \core\persistent {
/**
* Database table.
*/
const TABLE = 'tool_dataprivacy_purposerole';
/**
* Return the definition of the properties of this model.
*
* @return array
*/
protected static function define_properties() {
return array(
'purposeid' => array(
'type' => PARAM_INT,
'description' => 'The purpose that that this override relates to',
),
'roleid' => array(
'type' => PARAM_INT,
'description' => 'The role that that this override relates to',
),
'lawfulbases' => array(
'type' => PARAM_TEXT,
'description' => 'Comma-separated IDs matching records in tool_dataprivacy_lawfulbasis.',
'null' => NULL_ALLOWED,
'default' => null,
),
'sensitivedatareasons' => array(
'type' => PARAM_TEXT,
'description' => 'Comma-separated IDs matching records in tool_dataprivacy_sensitive',
'null' => NULL_ALLOWED,
'default' => null,
),
'retentionperiod' => array(
'type' => PARAM_ALPHANUM,
'description' => 'Retention period. ISO_8601 durations format (as in DateInterval format).',
'default' => '',
),
'protected' => array(
'type' => PARAM_INT,
'description' => 'Data retention with higher precedent over user\'s request to be forgotten.',
'default' => '0',
),
);
}
/**
* Get all role overrides for the purpose.
*
* @param purpose $purpose
* @return array
*/
public static function get_overrides_for_purpose(purpose $purpose) : array {
$cache = \cache::make('tool_dataprivacy', 'purpose_overrides');
$overrides = [];
$alldata = $cache->get($purpose->get('id'));
if (false === $alldata) {
$tocache = [];
foreach (self::get_records(['purposeid' => $purpose->get('id')]) as $override) {
$tocache[] = $override->to_record();
$overrides[$override->get('roleid')] = $override;
}
$cache->set($purpose->get('id'), $tocache);
} else {
foreach ($alldata as $data) {
$override = new self(0, $data);
$overrides[$override->get('roleid')] = $override;
}
}
return $overrides;
}
/**
* Adds the new record to the cache.
*
* @return null
*/
protected function after_create() {
$cache = \cache::make('tool_dataprivacy', 'purpose_overrides');
$cache->delete($this->get('purposeid'));
}
/**
* Updates the cache record.
*
* @param bool $result
* @return null
*/
protected function after_update($result) {
$cache = \cache::make('tool_dataprivacy', 'purpose_overrides');
$cache->delete($this->get('purposeid'));
}
/**
* Removes unnecessary stuff from db.
*
* @return null
*/
protected function before_delete() {
$cache = \cache::make('tool_dataprivacy', 'purpose_overrides');
$cache->delete($this->get('purposeid'));
}
}
@@ -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(new \text_progress_trace());
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(new \text_progress_trace());
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);
}
}
}
}
@@ -74,16 +74,31 @@ class process_data_request_task extends adhoc_task {
return;
}
// If no site purpose is defined, reject requests since they cannot be processed.
if (!\tool_dataprivacy\data_registry::defaults_set()) {
api::update_request_status($requestid, api::DATAREQUEST_STATUS_REJECTED);
mtrace('No site purpose defined. Request ' . $requestid . ' rejected.');
return;
}
// 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) {
// 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);
@@ -117,6 +132,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.
@@ -191,12 +207,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.
@@ -242,7 +265,7 @@ class process_data_request_task extends adhoc_task {
}
}
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);
+7
View File
@@ -33,6 +33,13 @@ $definitions = array(
'staticacceleration' => true,
'staticaccelerationsize' => 30,
),
'purpose_overrides' => array(
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
'simpledata' => false,
'staticacceleration' => true,
'staticaccelerationsize' => 50,
),
'contextlevel' => array(
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
+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',
],
];
+27 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20180821" COMMENT="XMLDB file for Moodle tool/dataprivacy"
<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20180904" COMMENT="XMLDB file for Moodle tool/dataprivacy"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
>
@@ -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"/>
@@ -98,6 +99,9 @@
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="unexpiredroles" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Roles which have explicitly not expired yet."/>
<FIELD NAME="expiredroles" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Explicitly expires roles"/>
<FIELD NAME="defaultexpired" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false" COMMENT="The default retention period has passed."/>
<FIELD NAME="status" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
@@ -146,5 +150,27 @@
<KEY NAME="request_contextlist" TYPE="unique" FIELDS="requestid, contextlistid" COMMENT="Uniqueness constraint on request and contextlist"/>
</KEYS>
</TABLE>
<TABLE NAME="tool_dataprivacy_purposerole" COMMENT="Data purpose overrides for a specific role">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="purposeid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="roleid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="lawfulbases" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="sensitivedatareasons" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="retentionperiod" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="protected" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="purposepurposeid" TYPE="foreign" FIELDS="purposeid" REFTABLE="tool_dataprivacy_purpose" REFFIELDS="id"/>
<KEY NAME="puproseroleid" TYPE="foreign" FIELDS="roleid" REFTABLE="role" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="purposerole" UNIQUE="true" FIELDS="purposeid, roleid"/>
</INDEXES>
</TABLE>
</TABLES>
</XMLDB>
+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,
),
);
+78
View File
@@ -201,5 +201,83 @@ function xmldb_tool_dataprivacy_upgrade($oldversion) {
upgrade_plugin_savepoint(true, 2017111316, 'tool', 'dataprivacy');
}
if ($oldversion < 2017111354) {
// Define table tool_dataprivacy_purposerole to be created.
$table = new xmldb_table('tool_dataprivacy_purposerole');
// Adding fields to table tool_dataprivacy_purposerole.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('purposeid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('roleid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('lawfulbases', XMLDB_TYPE_TEXT, null, null, null, null, null);
$table->add_field('sensitivedatareasons', XMLDB_TYPE_TEXT, null, null, null, null, null);
$table->add_field('retentionperiod', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
$table->add_field('protected', XMLDB_TYPE_INTEGER, '1', null, null, null, null);
$table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
// Adding keys to table tool_dataprivacy_purposerole.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('purposepurposeid', XMLDB_KEY_FOREIGN, ['purposeid'], 'tool_dataprivacy_purpose', ['id']);
$table->add_key('puproseroleid', XMLDB_KEY_FOREIGN, ['roleid'], 'role', ['id']);
// Adding indexes to table tool_dataprivacy_purposerole.
$table->add_index('purposerole', XMLDB_INDEX_UNIQUE, ['purposeid', 'roleid']);
// Conditionally launch create table for tool_dataprivacy_purposerole.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Update the ctxexpired table.
$table = new xmldb_table('tool_dataprivacy_ctxexpired');
// Add the unexpiredroles field.
$field = new xmldb_field('unexpiredroles', XMLDB_TYPE_TEXT, null, null, null, null, null, 'contextid');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
$DB->set_field('tool_dataprivacy_ctxexpired', 'unexpiredroles', '');
// Add the expiredroles field.
$field = new xmldb_field('expiredroles', XMLDB_TYPE_TEXT, null, null, null, null, null, 'unexpiredroles');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
$DB->set_field('tool_dataprivacy_ctxexpired', 'expiredroles', '');
// Add the defaultexpired field.
$field = new xmldb_field('defaultexpired', XMLDB_TYPE_INTEGER, '1', null, null, null, '1', 'expiredroles');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Change the default for the expired field to be empty.
$field = new xmldb_field('defaultexpired', XMLDB_TYPE_INTEGER, '1', null, null, null, null, 'expiredroles');
$dbman->change_field_default($table, $field);
// Prevent hte field from being nullable.
$field = new xmldb_field('defaultexpired', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, null, 'expiredroles');
$dbman->change_field_notnull($table, $field);
// Dataprivacy savepoint reached.
upgrade_plugin_savepoint(true, 2017111354, 'tool', 'dataprivacy');
}
if ($oldversion < 2017111356) {
// 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, 2017111356, '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();
+36 -3
View File
@@ -44,14 +44,47 @@ $form = new \tool_dataprivacy\form\purpose($PAGE->url->out(false),
$returnurl = new \moodle_url('/admin/tool/dataprivacy/purposes.php');
if ($form->is_cancelled()) {
redirect($returnurl);
} else if ($data = $form->get_data()) {
} else if ($alldata = $form->get_data()) {
$data = $form->filter_data_for_persistent($alldata);
if (empty($data->id)) {
\tool_dataprivacy\api::create_purpose($data);
$purpose = \tool_dataprivacy\api::create_purpose($data);
$messagesuccess = get_string('purposecreated', 'tool_dataprivacy');
} else {
\tool_dataprivacy\api::update_purpose($data);
$purpose = \tool_dataprivacy\api::update_purpose($data);
$messagesuccess = get_string('purposeupdated', 'tool_dataprivacy');
}
$currentoverrides = [];
foreach ($purpose->get_purpose_overrides() as $override) {
$currentoverrides[$override->get('id')] = $override;
}
$overrides = $form->get_role_overrides_from_data($alldata);
$submittedoverrides = [];
$tosave = [];
foreach ($overrides as $overridedata) {
$overridedata->purposeid = $purpose->get('id');
$override = new \tool_dataprivacy\purpose_override($overridedata->id, $overridedata);
$tosave[] = $override;
if (!empty($overridedata->id)) {
$submittedoverrides[$overridedata->id] = true;
}
}
foreach ($currentoverrides as $id => $override) {
if (!isset($submittedoverrides[$id])) {
$override->delete();
}
}
foreach ($tosave as $override) {
$override->save();
}
redirect($returnurl, $messagesuccess, 0, \core\output\notification::NOTIFY_SUCCESS);
}
@@ -28,13 +28,20 @@ $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_purpose_overrides'] = 'Purpose overrides in the Data privacy tool';
$string['cachedef_contextlevel'] = 'Context levels purpose and category';
$string['cancelrequest'] = 'Cancel request';
$string['cancelrequestconfirmation'] = 'Do you really want cancel this data request?';
$string['cannotreset'] = 'Unable to reset this request. Only rejected requests can be reset.';
$string['categories'] = 'Categories';
$string['category'] = 'Category';
$string['category_help'] = 'A category in the data registry describes a type of data. A new category may be added, or if Inherit is selected, the data category from a higher context is applied. Contexts are (from low to high): Blocks > Activity modules > Courses > Course categories > Site.';
@@ -46,9 +53,12 @@ $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['confirmrequestresubmit'] = 'Are you sure you wish to cancel the current {$a->type} request for {$a->username} and resubmit it?';
$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 +70,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';
@@ -70,22 +83,33 @@ $string['dataprivacy:downloadownrequest'] = 'Download your own exported data';
$string['dataprivacy:downloadallrequests'] = 'Download exported data for everyone';
$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['dataretentionexplanation'] = 'This summary shows the default categories and purposes for retaining user data. Certain areas may have more specific categories and purposes than those listed here.';
$string['dataretentionsummary'] = 'Data retention summary';
$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['deleteexpireddatarequeststask'] = 'Delete files from completed data requests that have expired';
$string['deleteexpireddatarequeststask'] = 'Delete expired data request export files';
$string['deleteexistingdeleteduserstask'] = 'Create delete data request for pre-existing deleted users';
$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 +117,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}';
@@ -183,11 +210,14 @@ $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['notyetexpired'] = '{$a} (not yet expired)';
$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';
@@ -213,10 +243,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['requestfor'] = 'User';
$string['requestmarkedcomplete'] = 'The request has been marked as complete';
$string['requestorigin'] = 'Site';
$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';
@@ -228,8 +262,20 @@ $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 in a course, 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['resubmitrequestasnew'] = 'Resubmit as new request';
$string['resubmitrequest'] = 'Resubmit {$a->type} request for {$a->username}';
$string['resubmittedrequest'] = 'The existing {$a->type} request for {$a->username} was cancelled and resubmitted';
$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.';
$string['resultdownloadready'] = 'Your copy of your personal data in {$a} that you recently requested is now available for download. Please click on the link below to go to the download page.';
$string['reviewdata'] = 'Review data';
@@ -237,10 +283,16 @@ $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['roleoverrides'] = 'Role overrides';
$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>';
$string['setdefaults'] = 'Set defaults';
$string['showdataretentionsummary'] = 'Show data retention summary';
$string['showdataretentionsummary_desc'] = 'If enabled, a link to the data retention summary is shown in the page footer and in the user profile page.';
$string['statusapproved'] = 'Approved';
$string['statusawaitingapproval'] = 'Awaiting approval';
$string['statuscancelled'] = 'Cancelled';
@@ -255,6 +307,28 @@ $string['statuspending'] = 'Pending';
$string['statusrejected'] = 'Rejected';
$string['subjectscope'] = 'Subject scope';
$string['subjectscope_help'] = 'The subject scope lists the roles which may be assigned in this context.';
$string['summary'] = 'Registry configuration summary';
$string['user'] = 'User';
$string['userlistnoncompliant'] = 'Userlist provider missing';
$string['userlistexplanation'] = 'This plugin has the base provider but should also implement the userlist provider for full support of privacy functionality.';
$string['viewrequest'] = 'View the request';
$string['visible'] = 'Expand all';
$string['unexpiredrolewithretention'] = '{$a->retention} (Unexpired)';
$string['expiredrolewithretention'] = '{$a->retention} (Expired)';
$string['defaultexpired'] = 'Data for all users';
$string['defaultexpiredexcept'] = 'Data for all users, except those who hold any of the following roles:<br>
{$a->unexpired}';
$string['defaultunexpiredwithexceptions'] = 'Only data for users who hold any of the following roles:<br>
{$a->expired}
Unless they also hold any of the following roles:<br>
{$a->unexpired}';
$string['defaultunexpired'] = 'Only data for users holding any of the following roles:<br>
{$a->expired}';
$string['tobedeleted'] = 'Data to be deleted';
$string['addroleoverride'] = 'Add role override';
$string['roleoverride'] = 'Role override';
$string['role'] = 'Role';
$string['role_help'] = 'The role which the override should apply to.';
$string['duplicaterole'] = 'Role already specified';
$string['purposeoverview'] = 'A purpose describes the intended use and retention policy for stored data. The basis for storing and retaining that data is also described in the purpose.';
$string['roleoverrideoverview'] = 'The default retention policy can be overridden for specific user roles, allowing you to specify a longer, or a shorter, retention policy. A user is only expired when all of their roles have expired.';
+59
View File
@@ -63,6 +63,42 @@ 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);
}
}
// A returned 0 means that the setting was set and disabled, false means that there is no value for the provided setting.
$showsummary = get_config('tool_dataprivacy', 'showdataretentionsummary');
if ($showsummary === false) {
// This means that no value is stored in db. We use the default value in this case.
$showsummary = true;
}
if ($showsummary) {
$summaryurl = new moodle_url('/admin/tool/dataprivacy/summary.php');
$summarynode = new core_user\output\myprofile\node('privacyandpolicies', 'retentionsummary',
get_string('dataretentionsummary', 'tool_dataprivacy'), null, $summaryurl);
$category->add_node($summarynode);
}
// Add the Privacy category to the tree if it's not empty and it doesn't exist.
@@ -77,6 +113,29 @@ function tool_dataprivacy_myprofile_navigation(tree $tree, $user, $iscurrentuser
return false;
}
/**
* Callback to add footer elements.
*
* @return string HTML footer content
*/
function tool_dataprivacy_standard_footer_html() {
$output = '';
// A returned 0 means that the setting was set and disabled, false means that there is no value for the provided setting.
$showsummary = get_config('tool_dataprivacy', 'showdataretentionsummary');
if ($showsummary === false) {
// This means that no value is stored in db. We use the default value in this case.
$showsummary = true;
}
if ($showsummary) {
$url = new moodle_url('/admin/tool/dataprivacy/summary.php');
$output = html_writer::link($url, get_string('dataretentionsummary', 'tool_dataprivacy'));
$output = html_writer::div($output, 'tool_dataprivacy');
}
return $output;
}
/**
* Fragment to add a new purpose.
*
+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);
@@ -0,0 +1,60 @@
<?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/>.
/**
* Display the request reject + resubmit confirmation page.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package tool_dataprivacy
*/
require_once('../../../config.php');
$requestid = required_param('requestid', PARAM_INT);
$confirm = optional_param('confirm', null, PARAM_INT);
$PAGE->set_url(new moodle_url('/admin/tool/dataprivacy/resubmitrequest.php', ['requestid' => $requestid]));
require_login();
$PAGE->set_context(\context_system::instance());
require_capability('tool/dataprivacy:managedatarequests', $PAGE->context);
$manageurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
$originalrequest = \tool_dataprivacy\api::get_request($requestid);
$user = \core_user::get_user($originalrequest->get('userid'));
$stringparams = (object) [
'username' => fullname($user),
'type' => \tool_dataprivacy\local\helper::get_shortened_request_type_string($originalrequest->get('type')),
];
if (null !== $confirm && confirm_sesskey()) {
$originalrequest->resubmit_request();
redirect($manageurl, get_string('resubmittedrequest', 'tool_dataprivacy', $stringparams));
}
$heading = get_string('resubmitrequest', 'tool_dataprivacy', $stringparams);
$PAGE->set_title($heading);
$PAGE->set_heading($heading);
echo $OUTPUT->header();
$confirmstring = get_string('confirmrequestresubmit', 'tool_dataprivacy', $stringparams);
$confirmurl = new \moodle_url($PAGE->url, ['confirm' => 1]);
echo $OUTPUT->confirm($confirmstring, $confirmurl, $manageurl);
echo $OUTPUT->footer();
+20
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,18 @@ 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));
// Whether the data retention summary should be shown in the page footer and in the user profile page.
$privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/showdataretentionsummary',
new lang_string('showdataretentionsummary', 'tool_dataprivacy'),
new lang_string('showdataretentionsummary_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%;
}
+41
View File
@@ -0,0 +1,41 @@
<?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/>.
/**
* Prints the compliance data registry main page.
*
* @copyright 2018 onwards Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package tool_dataprivacy
*/
require_once(__DIR__ . '/../../../config.php');
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
$url = new moodle_url('/' . $CFG->admin . '/tool/dataprivacy/summary.php');
$title = get_string('summary', 'tool_dataprivacy');
$context = \context_system::instance();
$PAGE->set_url($url);
$PAGE->set_context($context);
$PAGE->set_title($title);
$PAGE->set_heading($SITE->fullname);
$output = $PAGE->get_renderer('tool_dataprivacy');
echo $output->header();
$summarypage = new \tool_dataprivacy\output\summary_page();
echo $output->render($summarypage);
echo $output->footer();
@@ -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,12 @@
{{#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}}
{{#userlistnoncompliance}}
<span class="badge badge-pill badge-warning">{{#str}}userlistnoncompliant, tool_dataprivacy{{/str}}</span>
{{/userlistnoncompliance}}
</div>
{{#compliant}}
@@ -45,6 +45,10 @@
<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>
<dt><span class="badge badge-pill badge-warning">{{#str}}userlistnoncompliant, tool_dataprivacy{{/str}}</span></dt>
<dd>{{#str}}userlistexplanation, 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>
@@ -54,36 +54,49 @@
}}
{{#navigation}}
{{> core/action_link}}
<div class="m-b-1">
{{> core/action_link}}
</div>
{{/navigation}}
<div data-region="purposes" class="m-t-3 m-b-1">
<h3>{{#str}}purposes, tool_dataprivacy{{/str}}</h3>
<p>
{{#str}}purposeoverview, tool_dataprivacy{{/str}}
</p>
<div data-region="purposes" class="m-b-1">
<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}}roleoverrides, 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 +104,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 +113,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 +122,15 @@
{{#str}}no{{/str}}
{{/protected}}
</td>
<td class="col-md-1">
<td>
{{#roleoverrides}}
{{#str}}yes{{/str}}
{{/roleoverrides}}
{{^roleoverrides}}
{{#str}}no{{/str}}
{{/roleoverrides}}
</td>
<td>
{{#actions}}
{{> core/action_menu}}
{{/actions}}
@@ -0,0 +1,123 @@
{{!
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/summary
Summary
Classes required for JS:
Data attributes required for JS:
Context variables required for this template:
Example context (json):
{
"contexts": [
{
"contextname": "Site",
"category":
{
"name": "Test category",
"description": "<p>Description for category</p>"
},
"purpose":
{
"name": "Test purpose",
"description": "<p>Description for purpose</p>",
"lawfulbases": "gdpr_art_6_1_c",
"sensitivedatareasons": "gdpr_art_9_2_f",
"formattedlawfulbases": [
{
"name": "Lawful base 1(a)",
"description": "We need your information"
},
{
"name": "Lawful base 1(b)",
"description": "We really do need your information"
}
],
"formattedsensitivedatareasons": [
{
"name": "Sensitive data reason number 1",
"description": "Number 1"
},
{
"name": "Sensitive data reason number 1",
"description": "Number 2"
}
],
"formattedretentionperiod": "10 Years"
}
}
]
}
}}
<h2>{{#str}}dataretentionsummary, tool_dataprivacy{{/str}}</h2>
<p>{{#str}}dataretentionexplanation, tool_dataprivacy{{/str}}</p>
<div>
{{#contexts}}
<div class="card mb-3">
<div class="card-header"><h3>{{contextname}}</h3></div>
<div class="card-body p-l-2 p-r-2">
{{#category.name}}
<h4>{{#str}}category, tool_dataprivacy{{/str}}</h4>
<dl>
<dt>{{category.name}}</dt>
<dd>{{{category.description}}}</dd>
</dl>
<hr />
{{/category.name}}
<h4>{{#str}}purpose, tool_dataprivacy{{/str}}</h4>
<dl>
<dt>{{purpose.name}}</dt>
<dd>{{{purpose.description}}}</dd>
<dt>{{#str}}retentionperiod, tool_dataprivacy{{/str}}</dt>
<dd>{{purpose.formattedretentionperiod}}</dd>
</dl>
{{#purpose.lawfulbases}}
<table class="table table-bordered">
<thead><tr><th colspan="2">{{#str}}lawfulbases, tool_dataprivacy{{/str}}</th></tr></thead>
<tbody>
{{#purpose.formattedlawfulbases}}
<tr>
<td>{{name}}</td>
<td>{{description}}</td>
</tr>
{{/purpose.formattedlawfulbases}}
</tbody>
</table>
{{/purpose.lawfulbases}}
{{#purpose.sensitivedatareasons}}
<table class="table table-bordered">
<thead><tr><th colspan="2">{{#str}}sensitivedatareasons, tool_dataprivacy{{/str}}</th></tr></thead>
<tbody>
{{#purpose.formattedsensitivedatareasons}}
<tr>
<td>{{name}}</td>
<td>{{description}}</td>
</tr>
{{/purpose.formattedsensitivedatareasons}}
</tbody>
</table>
{{/purpose.sensitivedatareasons}}
</div>
</div>
{{/contexts}}
</div>
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'),
]);
}
}
@@ -20,6 +20,13 @@ Feature: Data delete from the privacy API
| parent | tired | User | victim |
And the following config values are set as admin:
| contactdataprotectionofficer | 1 | tool_dataprivacy |
And the following data privacy "categories" exist:
| name |
| Site category |
And the following data privacy "purposes" exist:
| name | retentionperiod |
| Site purpose | P10Y |
And I set the site category and purpose to "Site category" and "Site purpose"
@javascript
Scenario: As admin, delete a user and their data
@@ -30,7 +37,7 @@ Feature: Data delete from the privacy API
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "New request"
And I set the field "Requesting for" to "Victim User 1"
And I set the field "User" to "Victim User 1"
And I set the field "Type" to "Delete all of my personal data"
And I press "Save changes"
Then I should see "Victim User 1"
@@ -38,7 +45,7 @@ Feature: Data delete from the privacy API
And I run all adhoc tasks
And I reload the page
And I should see "Awaiting approval" in the "Victim User 1" "table_row"
And I follow "Actions"
And I open the action menu in "Victim User 1" "table_row"
And I follow "Approve request"
And I press "Approve request"
And I should see "Approved" in the "Victim User 1" "table_row"
@@ -67,7 +74,7 @@ Feature: Data delete from the privacy API
And I log out
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "Actions"
And I open the action menu in "Victim User 1" "table_row"
And I follow "Approve request"
And I press "Approve request"
@@ -93,7 +100,7 @@ Feature: Data delete from the privacy API
And I follow "Profile" in the user menu
And I follow "Data requests"
And I follow "New request"
And I set the field "Requesting for" to "Victim User 1"
And I set the field "User" to "Victim User 1"
And I set the field "Type" to "Delete all of my personal data"
And I press "Save changes"
Then I should see "Victim User 1"
@@ -105,7 +112,7 @@ Feature: Data delete from the privacy API
And I log out
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "Actions"
And I open the action menu in "Victim User 1" "table_row"
And I follow "Approve request"
And I press "Approve request"
@@ -21,34 +21,41 @@ Feature: Data export from the privacy API
And the following config values are set as admin:
| contactdataprotectionofficer | 1 | tool_dataprivacy |
| privacyrequestexpiry | 55 | tool_dataprivacy |
And the following data privacy "categories" exist:
| name |
| Site category |
And the following data privacy "purposes" exist:
| name | retentionperiod |
| Site purpose | P10Y |
And I set the site category and purpose to "Site category" and "Site purpose"
@javascript
Scenario: As admin, export data for a user and download it, unless it has expired
Given I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "New request"
And I set the field "Requesting for" to "Victim User 1"
And I set the field "User" to "Victim User 1"
And I press "Save changes"
Then I should see "Victim User 1"
And I should see "Pending" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Awaiting approval" in the "Victim User 1" "table_row"
And I follow "Actions"
And I open the action menu in "Victim User 1" "table_row"
And I follow "Approve request"
And I press "Approve request"
And I should see "Approved" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Download ready" in the "Victim User 1" "table_row"
And I follow "Actions"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download between "1" and "100000" bytes
And the following config values are set as admin:
| privacyrequestexpiry | 1 | tool_dataprivacy |
And I wait "1" seconds
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I should see "Expired" in the "Victim User 1" "table_row"
And I follow "Actions"
And I open the action menu in "Victim User 1" "table_row"
And I should not see "Download"
@javascript
@@ -67,7 +74,7 @@ Feature: Data export from the privacy API
And I log out
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "Actions"
And I open the action menu in "Victim User 1" "table_row"
And I follow "Approve request"
And I press "Approve request"
@@ -79,7 +86,7 @@ Feature: Data export from the privacy API
And I run all adhoc tasks
And I reload the page
And I should see "Download ready" in the "Export all of my personal data" "table_row"
And I follow "Actions"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download between "1" and "100000" bytes
And the following config values are set as admin:
@@ -96,7 +103,7 @@ Feature: Data export from the privacy API
And I follow "Profile" in the user menu
And I follow "Data requests"
And I follow "New request"
And I set the field "Requesting for" to "Victim User 1"
And I set the field "User" to "Victim User 1"
And I press "Save changes"
Then I should see "Victim User 1"
And I should see "Pending" in the "Victim User 1" "table_row"
@@ -107,7 +114,7 @@ Feature: Data export from the privacy API
And I log out
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "Actions"
And I open the action menu in "Victim User 1" "table_row"
And I follow "Approve request"
And I press "Approve request"
@@ -119,7 +126,7 @@ Feature: Data export from the privacy API
And I run all adhoc tasks
And I reload the page
And I should see "Download ready" in the "Victim User 1" "table_row"
And I follow "Actions"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download between "1" and "100000" bytes
And the following config values are set as admin:
@@ -7,7 +7,7 @@ Feature: Manage data categories
Background:
Given I log in as "admin"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Edit" "link"
And I open the action menu in "region-main" "region"
And I choose "Categories" in the open action menu
And I press "Add category"
And I set the field "Name" to "Category 1"
@@ -17,7 +17,7 @@ Feature: Manage data categories
And I should see "Category 1 description" in the "Category 1" "table_row"
Scenario: Update a data category
Given I click on "Actions" "link" in the "Category 1" "table_row"
Given I open the action menu in "Category 1" "table_row"
And I choose "Edit" in the open action menu
And I set the field "Name" to "Category 1 edited"
And I set the field "Description" to "Category 1 description edited"
@@ -26,7 +26,7 @@ Feature: Manage data categories
And I should see "Category 1 description edited" in the "List of data categories" "table"
Scenario: Delete a data category
Given I click on "Actions" "link" in the "Category 1" "table_row"
Given I open the action menu in "Category 1" "table_row"
And I choose "Delete" in the open action menu
And I should see "Delete category"
And I should see "Are you sure you want to delete the category 'Category 1'?"
@@ -35,7 +35,7 @@ Feature: Manage data requests
And I navigate to "Users > Privacy and policies > Data requests" in site administration
Then I should see "Hi PO!" in the "John Doe" "table_row"
And I should see "Dear Mr. Privacy Officer" in the "Jane Doe" "table_row"
And I click on "Actions" "link" in the "John Doe" "table_row"
And I open the action menu in "John Doe" "table_row"
And I should see "View the request"
And I should see "Mark as complete"
And I choose "View the request" in the open action menu
@@ -43,16 +43,106 @@ Feature: Manage data requests
And I press "Mark as complete"
And I wait until the page is ready
And I should see "Complete" in the "John Doe" "table_row"
And I click on "Actions" "link" in the "John Doe" "table_row"
And I open the action menu in "John Doe" "table_row"
And I should see "View the request"
But I should not see "Mark as complete"
And I press key "27" in ".moodle-actionmenu" "css_element"
And I click on "Actions" "link" in the "Jane Doe" "table_row"
And I open the action menu in "Jane Doe" "table_row"
And I choose "Mark as complete" in the open action menu
And I should see "Do you really want to mark this user enquiry as complete?"
And I press "Mark as complete"
And I wait until the page is ready
And I should see "Complete" in the "Jane Doe" "table_row"
And I click on "Actions" "link" in the "Jane Doe" "table_row"
And I open the action menu in "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)"
@@ -7,7 +7,7 @@ Feature: Manage data storage purposes
Background:
Given I log in as "admin"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Edit" "link"
And I open the action menu in "region-main" "region"
And I choose "Purposes" in the open action menu
And I press "Add purpose"
And I set the field "Name" to "Purpose 1"
@@ -26,10 +26,10 @@ Feature: Manage data storage purposes
And I should see "Legal obligation (GDPR Art 6.1(c))" in the "Purpose 1" "table_row"
And I should see "Explicit consent (GDPR Art. 9.2(a))" in the "Purpose 1" "table_row"
And I should see "2 years" in the "Purpose 1" "table_row"
And I should see "No" in the "Purpose 1" "table_row"
And "Purpose 1 Purpose 1 description" row "5" column of "List of data purposes" table should contain "No"
Scenario: Update a data storage purpose
Given I click on "Actions" "link" in the "Purpose 1" "table_row"
Given I open the action menu in "Purpose 1" "table_row"
And I choose "Edit" in the open action menu
And I set the field "Name" to "Purpose 1 edited"
And I set the field "Description" to "Purpose 1 description edited"
@@ -45,10 +45,10 @@ Feature: Manage data storage purposes
And I should see "Vital interests (GDPR Art. 6.1(d))" in the "Purpose 1 edited" "table_row"
And I should see "3 years" in the "Purpose 1 edited" "table_row"
But I should not see "Legal obligation (GDPR Art 6.1(c))" in the "Purpose 1 edited" "table_row"
And I should not see "No" in the "Purpose 1 edited" "table_row"
And "Purpose 1 edited Purpose 1 description edited" row "5" column of "List of data purposes" table should not contain "No"
Scenario: Delete a data storage purpose
Given I click on "Actions" "link" in the "Purpose 1" "table_row"
Given I open the action menu in "Purpose 1" "table_row"
And I choose "Delete" in the open action menu
And I should see "Delete purpose"
And I should see "Are you sure you want to delete the purpose 'Purpose 1'?"
@@ -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');
}
}
@@ -0,0 +1,246 @@
<?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 data_request persistent.
*
* @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();
require_once('data_privacy_testcase.php');
use tool_dataprivacy\api;
/**
* Tests for the data_request persistent.
*
* @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_data_request_testcase extends data_privacy_testcase {
/**
* Data provider for testing is_resettable, and is_active.
*
* @return array
*/
public function status_state_provider() : array {
return [
[
'state' => api::DATAREQUEST_STATUS_PENDING,
'resettable' => false,
'active' => false,
],
[
'state' => api::DATAREQUEST_STATUS_PREPROCESSING,
'resettable' => false,
'active' => false,
],
[
'state' => api::DATAREQUEST_STATUS_AWAITING_APPROVAL,
'resettable' => true,
'active' => true,
],
[
'state' => api::DATAREQUEST_STATUS_APPROVED,
'resettable' => true,
'active' => true,
],
[
'state' => api::DATAREQUEST_STATUS_PROCESSING,
'resettable' => false,
'active' => false,
],
[
'state' => api::DATAREQUEST_STATUS_COMPLETE,
'resettable' => false,
'active' => false,
],
[
'state' => api::DATAREQUEST_STATUS_CANCELLED,
'resettable' => false,
'active' => false,
],
[
'state' => api::DATAREQUEST_STATUS_REJECTED,
'resettable' => true,
'active' => false,
],
[
'state' => api::DATAREQUEST_STATUS_DOWNLOAD_READY,
'resettable' => false,
'active' => false,
],
[
'state' => api::DATAREQUEST_STATUS_EXPIRED,
'resettable' => false,
'active' => false,
],
];
}
/**
* Test the pseudo states of a data request with an export request.
*
* @dataProvider status_state_provider
* @param int $status
* @param bool $resettable
* @param bool $active
*/
public function test_pseudo_states_export(int $status, bool $resettable, bool $active) {
$uut = new \tool_dataprivacy\data_request();
$uut->set('status', $status);
$uut->set('type', api::DATAREQUEST_TYPE_EXPORT);
$this->assertEquals($resettable, $uut->is_resettable());
$this->assertEquals($active, $uut->is_active());
}
/**
* Test the pseudo states of a data request with a delete request.
*
* @dataProvider status_state_provider
* @param int $status
* @param bool $resettable
* @param bool $active
*/
public function test_pseudo_states_delete(int $status, bool $resettable, bool $active) {
$uut = new \tool_dataprivacy\data_request();
$uut->set('status', $status);
$uut->set('type', api::DATAREQUEST_TYPE_DELETE);
$this->assertEquals($resettable, $uut->is_resettable());
$this->assertEquals($active, $uut->is_active());
}
/**
* Test the pseudo states of a data request.
*
* @dataProvider status_state_provider
* @param int $status
*/
public function test_can_reset_others($status) {
$uut = new \tool_dataprivacy\data_request();
$uut->set('status', $status);
$uut->set('type', api::DATAREQUEST_TYPE_OTHERS);
$this->assertFalse($uut->is_resettable());
}
/**
* Data provider for states which are not resettable.
*
* @return array
*/
public function non_resettable_provider() : array {
$states = [];
foreach ($this->status_state_provider() as $thisstatus) {
if (!$thisstatus['resettable']) {
$states[] = $thisstatus;
}
}
return $states;
}
/**
* Ensure that requests which are not resettable cause an exception to be thrown.
*
* @dataProvider non_resettable_provider
* @param int $status
*/
public function test_non_resubmit_request($status) {
$uut = new \tool_dataprivacy\data_request();
$uut->set('status', $status);
$this->expectException(\moodle_exception::class);
$this->expectExceptionMessage(get_string('cannotreset', 'tool_dataprivacy'));
$uut->resubmit_request();
}
/**
* Ensure that a rejected request can be reset.
*/
public function test_resubmit_request() {
$this->resetAfterTest();
$uut = new \tool_dataprivacy\data_request();
$uut->set('status', api::DATAREQUEST_STATUS_REJECTED);
$uut->set('type', api::DATAREQUEST_TYPE_DELETE);
$uut->set('comments', 'Foo');
$uut->set('requestedby', 42);
$uut->set('dpo', 98);
$newrequest = $uut->resubmit_request();
$this->assertEquals('Foo', $newrequest->get('comments'));
$this->assertEquals(42, $newrequest->get('requestedby'));
$this->assertEquals(98, $newrequest->get('dpo'));
$this->assertEquals(api::DATAREQUEST_STATUS_PENDING, $newrequest->get('status'));
$this->assertEquals(api::DATAREQUEST_TYPE_DELETE, $newrequest->get('type'));
$this->assertEquals(api::DATAREQUEST_STATUS_REJECTED, $uut->get('status'));
}
/**
* Ensure that an active request can be reset.
*/
public function test_resubmit_active_request() {
$this->resetAfterTest();
$uut = new \tool_dataprivacy\data_request();
$uut->set('status', api::DATAREQUEST_STATUS_APPROVED);
$uut->set('type', api::DATAREQUEST_TYPE_DELETE);
$uut->set('comments', 'Foo');
$uut->set('requestedby', 42);
$uut->set('dpo', 98);
$newrequest = $uut->resubmit_request();
$this->assertEquals('Foo', $newrequest->get('comments'));
$this->assertEquals(42, $newrequest->get('requestedby'));
$this->assertEquals(98, $newrequest->get('dpo'));
$this->assertEquals(api::DATAREQUEST_STATUS_PENDING, $newrequest->get('status'));
$this->assertEquals(api::DATAREQUEST_TYPE_DELETE, $newrequest->get('type'));
$this->assertEquals(api::DATAREQUEST_STATUS_REJECTED, $uut->get('status'));
}
/**
* Create a data request for the user.
*
* @param int $userid
* @param int $type
* @param int $status
* @return data_request
*/
public function create_request_for_user_with_status(int $userid, int $type, int $status) : data_request {
$request = new data_request(0, (object) [
'userid' => $userid,
'type' => $type,
'status' => $status,
]);
$request->save();
return $request;
}
}
File diff suppressed because it is too large Load Diff
@@ -23,7 +23,9 @@
*/
use tool_dataprivacy\api;
use tool_dataprivacy\category;
use tool_dataprivacy\data_request;
use tool_dataprivacy\purpose;
defined('MOODLE_INTERNAL') || die();
global $CFG;
@@ -62,6 +64,9 @@ class tool_dataprivacy_expired_data_requests_testcase extends data_privacy_testc
$dpouser = $this->getDataGenerator()->create_user();
$this->assign_site_dpo($dpouser);
// Set site purpose.
$this->create_system_purpose();
// Set request expiry to 5 minutes.
set_config('privacyrequestexpiry', 300, 'tool_dataprivacy');
@@ -130,6 +135,9 @@ class tool_dataprivacy_expired_data_requests_testcase extends data_privacy_testc
$admin = get_admin();
$this->setAdminUser();
// Set site purpose.
$this->create_system_purpose();
// Create export request.
$datarequest = api::create_data_request($admin->id, api::DATAREQUEST_TYPE_EXPORT);
$requestid = $datarequest->get('id');
@@ -170,4 +178,28 @@ class tool_dataprivacy_expired_data_requests_testcase extends data_privacy_testc
$result = data_request::is_expired($request);
$this->assertTrue($result);
}
/**
* Create a site (system context) purpose and category.
*
* @return void
*/
protected function create_system_purpose() {
$purpose = new purpose(0, (object) [
'name' => 'Test purpose ' . rand(1, 1000),
'retentionperiod' => 'P1D',
'lawfulbases' => 'gdpr_art_6_1_a',
]);
$purpose->create();
$cat = new category(0, (object) ['name' => 'Test category']);
$cat->create();
$record = (object) [
'purposeid' => $purpose->get('id'),
'categoryid' => $cat->get('id'),
'contextlevel' => CONTEXT_SYSTEM,
];
api::set_contextlevel($record);
}
}
+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,99 @@
<?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 filtered_userlist.
*
* @package tool_dataprivacy
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Unit tests for the filtered_userlist.
*
* @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_filtered_userlist_testcase extends advanced_testcase {
/**
* Test the apply_expired_contexts_filters function with arange of options.
*
* @dataProvider apply_expired_contexts_filters_provider
* @param array $initial The set of userids in the initial filterlist.
* @param array $expired The set of userids considered as expired.
* @param array $unexpired The set of userids considered as unexpired.
* @param array $expected The expected values.
*/
public function test_apply_expired_contexts_filters(array $initial, array $expired, array $unexpired, array $expected) {
$userlist = $this->getMockBuilder(\tool_dataprivacy\filtered_userlist::class)
->disableOriginalConstructor()
->setMethods(null)
->getMock();
$rc = new \ReflectionClass(\tool_dataprivacy\filtered_userlist::class);
$rcm = $rc->getMethod('set_userids');
$rcm->setAccessible(true);
$rcm->invoke($userlist, $initial);
$userlist->apply_expired_context_filters($expired, $unexpired);
$filtered = $userlist->get_userids();
sort($expected);
sort($filtered);
$this->assertEquals($expected, $filtered);
}
/**
* Data provider for the apply_expired_contexts_filters function.
*
* @return array
*/
public function apply_expired_contexts_filters_provider() : array {
return [
// Entire list should be preserved.
'No overrides' => [
'users' => [1, 2, 3, 4, 5],
'expired' => [],
'unexpired' => [],
[1, 2, 3, 4, 5],
],
// The list should be filtered to only keep the expired users.
'Expired only' => [
'users' => [1, 2, 3, 4, 5],
'expired' => [2, 3, 4],
'unexpired' => [],
'expected' => [2, 3, 4],
],
// The list should be filtered to remove any unexpired users.
'Unexpired only' => [
'users' => [1, 2, 3, 4, 5],
'expired' => [],
'unexpired' => [1, 5],
'expected' => [2, 3, 4],
],
// The list should be filtered to only keep expired users who are not on the unexpired list.
'Combination of expired and unexpired' => [
'users' => [1, 2, 3, 4, 5],
'expired' => [1, 2, 3],
'unexpired' => [1, 5],
'expected' => [2, 3],
],
];
}
}
@@ -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 = 2017111350;
$plugin->version = 2017111357;
$plugin->requires = 2017111304.00; // Moodle 3.4.4 (Build: 20180517) and upwards.
$plugin->component = 'tool_dataprivacy';
@@ -0,0 +1,60 @@
<?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/>.
/**
* Logstore userlist provider interface.
*
* @package tool_log
* @copyright 2018 Adrian Greeve
* @author Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_log\local\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Logstore userlist provider interface.
*
* Logstore subplugins providers must implement this interface.
*
* @package tool_log
* @copyright 2018 Adrian Greeve
* @author Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface logstore_userlist_provider extends
\core_privacy\local\request\plugin\subplugin_provider,
\core_privacy\local\request\shared_userlist_provider
{
/**
* Add user IDs that contain user information for the specified context.
*
* @param \core_privacy\local\request\userlist $userlist The userlist to add the users to.
* @return void
*/
public static function add_userids_for_context(\core_privacy\local\request\userlist $userlist);
/**
* Delete all data for a list of users in the specified context.
*
* @param \core_privacy\local\request\approved_userlist $userlist The specific context and users to delete data for.
* @return void
*/
public static function delete_data_for_userlist(\core_privacy\local\request\approved_userlist $userlist);
}
@@ -120,4 +120,19 @@ trait moodle_database_export_and_delete {
$db->delete_records_select($table, "userid = :userid AND contextid $insql", $params);
}
/**
* Delete all user data for the specified users, in the specified context.
*
* @param \core_privacy\local\request\approved_userlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_userlist(\core_privacy\local\request\approved_userlist $userlist) {
list($db, $table) = static::get_database_and_table();
if (!$db || !$table) {
return;
}
list($insql, $inparams) = $db->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
$params = array_merge($inparams, ['contextid' => $userlist->get_context()->id]);
$db->delete_records_select($table, "contextid = :contextid AND userid $insql", $params);
}
}

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