Merge branch 'MDL-29895' of https://github.com/enovation/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 9 Oct 2018 22:29:16 +0000 (00:29 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 9 Oct 2018 22:29:16 +0000 (00:29 +0200)
510 files changed:
.eslintignore
.stylelintignore
.travis.yml
Gruntfile.js
admin/roles/allow.php
admin/roles/classes/capability_table_with_risks.php
admin/roles/manage.php
admin/settings/appearance.php
admin/settings/courses.php
admin/settings/subsystems.php
admin/templates/settings_search_results.mustache
admin/tool/dataprivacy/amd/build/defaultsactions.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/events.min.js
admin/tool/dataprivacy/amd/build/requestactions.min.js
admin/tool/dataprivacy/amd/src/defaultsactions.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/events.js
admin/tool/dataprivacy/amd/src/requestactions.js
admin/tool/dataprivacy/categories.php
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/data_registry.php
admin/tool/dataprivacy/classes/expired_context.php
admin/tool/dataprivacy/classes/expired_contexts_manager.php
admin/tool/dataprivacy/classes/expired_course_related_contexts.php [deleted file]
admin/tool/dataprivacy/classes/expired_user_contexts.php [deleted file]
admin/tool/dataprivacy/classes/expiry_info.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/external.php
admin/tool/dataprivacy/classes/external/category_exporter.php
admin/tool/dataprivacy/classes/external/purpose_exporter.php
admin/tool/dataprivacy/classes/form/defaults.php [deleted file]
admin/tool/dataprivacy/classes/local/helper.php
admin/tool/dataprivacy/classes/metadata_registry.php
admin/tool/dataprivacy/classes/output/data_registry_page.php
admin/tool/dataprivacy/classes/output/data_requests_page.php
admin/tool/dataprivacy/classes/output/data_requests_table.php
admin/tool/dataprivacy/classes/output/defaults_page.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/output/renderer.php
admin/tool/dataprivacy/classes/output/summary_page.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/page_helper.php
admin/tool/dataprivacy/classes/privacy/provider.php
admin/tool/dataprivacy/classes/task/delete_expired_contexts.php
admin/tool/dataprivacy/classes/task/expired_retention_period.php
admin/tool/dataprivacy/createdatarequest.php
admin/tool/dataprivacy/datadeletion.php
admin/tool/dataprivacy/dataregistry.php
admin/tool/dataprivacy/datarequests.php
admin/tool/dataprivacy/db/services.php
admin/tool/dataprivacy/defaults.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/lib.php
admin/tool/dataprivacy/purposes.php
admin/tool/dataprivacy/settings.php
admin/tool/dataprivacy/styles.css
admin/tool/dataprivacy/summary.php [new file with mode: 0644]
admin/tool/dataprivacy/templates/categories.mustache
admin/tool/dataprivacy/templates/category_purpose_form.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/data_request_email.mustache
admin/tool/dataprivacy/templates/data_requests_bulk_actions.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/defaults_display.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/defaults_page.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/delete_activity_defaults.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/purposes.mustache
admin/tool/dataprivacy/templates/summary.mustache [new file with mode: 0644]
admin/tool/dataprivacy/tests/api_test.php
admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php [new file with mode: 0644]
admin/tool/dataprivacy/tests/behat/manage_data_requests.feature
admin/tool/dataprivacy/tests/behat/manage_defaults.feature [new file with mode: 0644]
admin/tool/dataprivacy/tests/expired_contexts_test.php
admin/tool/dataprivacy/tests/external_test.php
admin/tool/dataprivacy/tests/generator/lib.php [new file with mode: 0644]
admin/tool/dataprivacy/version.php
admin/tool/policy/amd/build/acceptmodal.min.js
admin/tool/policy/amd/src/acceptmodal.js
admin/tool/policy/classes/acceptances_table.php
admin/tool/policy/classes/form/policydoc.php
admin/tool/policy/classes/output/page_agreedocs.php
admin/tool/policy/classes/output/page_viewdoc.php
admin/tool/policy/classes/policy_version.php
admin/tool/policy/db/install.xml
admin/tool/policy/db/upgrade.php [new file with mode: 0644]
admin/tool/policy/index.php
admin/tool/policy/lang/en/tool_policy.php
admin/tool/policy/lib.php
admin/tool/policy/templates/page_agreedocs.mustache
admin/tool/policy/templates/page_viewdoc.mustache
admin/tool/policy/tests/behat/acceptances.feature
admin/tool/policy/tests/behat/behat_tool_policy.php
admin/tool/policy/tests/behat/consent.feature
admin/tool/policy/version.php
admin/tool/usertours/tests/behat/create_tour.feature
auth/classes/output/login.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
backup/util/ui/tests/behat/duplicate_activities.feature
blocks/calendar_month/block_calendar_month.php
blocks/myoverview/amd/build/event_list.min.js [deleted file]
blocks/myoverview/amd/build/event_list_by_course.min.js [deleted file]
blocks/myoverview/amd/build/tab_preferences.min.js [deleted file]
blocks/myoverview/amd/src/event_list.js [deleted file]
blocks/myoverview/amd/src/event_list_by_course.js [deleted file]
blocks/myoverview/amd/src/tab_preferences.js [deleted file]
blocks/myoverview/block_myoverview.php
blocks/myoverview/classes/output/main.php
blocks/myoverview/classes/privacy/provider.php
blocks/myoverview/db/upgrade.php [moved from cache/stores/memcache/settings.php with 53% similarity]
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/templates/course-event-list-item.mustache [deleted file]
blocks/myoverview/templates/course-event-list.mustache [deleted file]
blocks/myoverview/templates/course-summary.mustache [deleted file]
blocks/myoverview/templates/courses-view-nav-grouping-display-filter.mustache [new file with mode: 0644]
blocks/myoverview/templates/courses-view.mustache
blocks/myoverview/templates/event-list-group.mustache [deleted file]
blocks/myoverview/templates/event-list-item.mustache [deleted file]
blocks/myoverview/templates/event-list.mustache [deleted file]
blocks/myoverview/templates/main.mustache
blocks/myoverview/templates/timeline-view-courses.mustache [deleted file]
blocks/myoverview/templates/timeline-view.mustache [deleted file]
blocks/myoverview/tests/behat/block_myoverview_dashboard.feature
blocks/myoverview/tests/behat/block_myoverview_progress.feature
blocks/myoverview/tests/privacy_test.php [deleted file]
blocks/myoverview/version.php
blocks/myprofile/block_myprofile.php
blocks/myprofile/classes/output/myprofile.php [new file with mode: 0644]
blocks/myprofile/classes/output/renderer.php [new file with mode: 0644]
blocks/myprofile/styles.css
blocks/myprofile/templates/myprofile.mustache [new file with mode: 0644]
blocks/rss_client/classes/output/item.php
blocks/timeline/amd/build/calendar_events_repository.min.js [moved from blocks/myoverview/amd/build/calendar_events_repository.min.js with 100% similarity]
blocks/timeline/amd/build/event_list.min.js [new file with mode: 0644]
blocks/timeline/amd/build/main.min.js [new file with mode: 0644]
blocks/timeline/amd/build/view.min.js [new file with mode: 0644]
blocks/timeline/amd/build/view_courses.min.js [new file with mode: 0644]
blocks/timeline/amd/build/view_dates.min.js [new file with mode: 0644]
blocks/timeline/amd/build/view_nav.min.js [new file with mode: 0644]
blocks/timeline/amd/src/calendar_events_repository.js [moved from blocks/myoverview/amd/src/calendar_events_repository.js with 96% similarity]
blocks/timeline/amd/src/event_list.js [new file with mode: 0644]
blocks/timeline/amd/src/main.js [new file with mode: 0644]
blocks/timeline/amd/src/view.js [new file with mode: 0644]
blocks/timeline/amd/src/view_courses.js [new file with mode: 0644]
blocks/timeline/amd/src/view_dates.js [new file with mode: 0644]
blocks/timeline/amd/src/view_nav.js [new file with mode: 0644]
blocks/timeline/block_timeline.php [new file with mode: 0644]
blocks/timeline/classes/output/main.php [new file with mode: 0644]
blocks/timeline/classes/output/renderer.php [moved from blocks/myoverview/settings.php with 52% similarity]
blocks/timeline/classes/privacy/provider.php [moved from blocks/myoverview/lib.php with 52% similarity]
blocks/timeline/db/access.php [new file with mode: 0644]
blocks/timeline/db/install.php [new file with mode: 0644]
blocks/timeline/lang/en/block_timeline.php [new file with mode: 0644]
blocks/timeline/pix/activities.svg [moved from blocks/myoverview/pix/activities.svg with 100% similarity]
blocks/timeline/pix/courses.svg [new file with mode: 0644]
blocks/timeline/templates/course-item-loading-placeholder.mustache [new file with mode: 0644]
blocks/timeline/templates/course-item.mustache [moved from blocks/myoverview/templates/course-item.mustache with 64% similarity]
blocks/timeline/templates/course-items.mustache [moved from theme/bootstrapbase/templates/block_myoverview/course-item.mustache with 63% similarity]
blocks/timeline/templates/event-list-content.mustache [moved from theme/bootstrapbase/templates/block_myoverview/event-list-group.mustache with 66% similarity]
blocks/timeline/templates/event-list-item.mustache [new file with mode: 0644]
blocks/timeline/templates/event-list-items.mustache [moved from blocks/myoverview/templates/course-event-list-items.mustache with 82% similarity]
blocks/timeline/templates/event-list.mustache [new file with mode: 0644]
blocks/timeline/templates/main.mustache [new file with mode: 0644]
blocks/timeline/templates/nav-day-filter.mustache [new file with mode: 0644]
blocks/timeline/templates/nav-view-selector.mustache [new file with mode: 0644]
blocks/timeline/templates/placeholder-event-list-item.mustache [new file with mode: 0644]
blocks/timeline/templates/view-courses.mustache [new file with mode: 0644]
blocks/timeline/templates/view-dates.mustache [moved from blocks/myoverview/templates/timeline-view-dates.mustache with 57% similarity]
blocks/timeline/templates/view.mustache [new file with mode: 0644]
blocks/timeline/tests/behat/block_timeline_courses.feature [new file with mode: 0644]
blocks/timeline/tests/behat/block_timeline_dates.feature [new file with mode: 0644]
blocks/timeline/version.php [moved from cache/stores/memcache/version.php with 64% similarity]
blocks/upgrade.txt
blog/classes/external.php [new file with mode: 0644]
blog/classes/external/post_exporter.php [new file with mode: 0644]
blog/index.php
blog/lib.php
blog/tests/external_test.php [new file with mode: 0644]
cache/stores/memcache/addinstanceform.php [deleted file]
cache/stores/memcache/classes/privacy/provider.php [deleted file]
cache/stores/memcache/lang/en/cachestore_memcache.php [deleted file]
cache/stores/memcache/lib.php [deleted file]
cache/stores/memcache/tests/memcache_test.php [deleted file]
cache/upgrade.txt
calendar/classes/external/event_exporter.php
calendar/lib.php
calendar/templates/event_summary_body.mustache
config-dist.php
course/amd/build/repository.min.js [new file with mode: 0644]
course/amd/src/repository.js [new file with mode: 0644]
course/classes/category.php
course/classes/list_element.php
course/classes/output/activity_navigation.php
course/edit.php
course/externallib.php
course/lib.php
course/renderer.php
course/tests/behat/behat_course.php
course/tests/behat/course_contact.feature [new file with mode: 0644]
course/tests/category_test.php
course/tests/courselib_test.php
course/tests/externallib_test.php
course/upgrade.txt
enrol/category/tests/plugin_test.php
enrol/tests/enrollib_test.php
filter/glossary/filter.php
filter/glossary/lang/en/filter_glossary.php
filter/glossary/tests/filter_test.php
filter/mathjaxloader/filter.php
filter/mathjaxloader/tests/filtermath_test.php [new file with mode: 0644]
filter/upgrade.txt
grade/edit/tree/lib.php
grade/tests/report_graderlib_test.php
group/assign.php
install/lang/el/admin.php
install/lang/el/error.php
install/lang/el/install.php
iplookup/tests/geoplugin_test.php
lang/en/admin.php
lang/en/deprecated.txt
lang/en/message.php
lang/en/moodle.php
lang/en/privacy.php
lang/en/question.php
lib/accesslib.php
lib/adminlib.php
lib/amd/build/autoscroll.min.js [new file with mode: 0644]
lib/amd/build/page_global.min.js [new file with mode: 0644]
lib/amd/build/paged_content.min.js [new file with mode: 0644]
lib/amd/build/paged_content_events.min.js
lib/amd/build/paged_content_factory.min.js
lib/amd/build/paged_content_pages.min.js
lib/amd/build/paged_content_paging_bar.min.js
lib/amd/build/paged_content_paging_bar_limit_selector.min.js [new file with mode: 0644]
lib/amd/build/paged_content_paging_dropdown.min.js
lib/amd/build/pubsub.min.js [new file with mode: 0644]
lib/amd/build/sortable_list.min.js [new file with mode: 0644]
lib/amd/build/tree.min.js
lib/amd/build/user_date.min.js
lib/amd/src/autoscroll.js [new file with mode: 0644]
lib/amd/src/page_global.js [new file with mode: 0644]
lib/amd/src/paged_content.js [new file with mode: 0644]
lib/amd/src/paged_content_events.js
lib/amd/src/paged_content_factory.js
lib/amd/src/paged_content_pages.js
lib/amd/src/paged_content_paging_bar.js
lib/amd/src/paged_content_paging_bar_limit_selector.js [new file with mode: 0644]
lib/amd/src/paged_content_paging_dropdown.js
lib/amd/src/pubsub.js [new file with mode: 0644]
lib/amd/src/sortable_list.js [new file with mode: 0644]
lib/amd/src/tree.js
lib/amd/src/user_date.js
lib/badgeslib.php
lib/behat/behat_base.php
lib/behat/classes/util.php
lib/blocklib.php
lib/classes/event/message_contact_blocked.php
lib/classes/event/message_contact_unblocked.php
lib/classes/event/message_user_blocked.php [new file with mode: 0644]
lib/classes/event/message_user_unblocked.php [new file with mode: 0644]
lib/classes/output/icon_system_fontawesome.php
lib/classes/plugin_manager.php
lib/classes/session/memcache.php [deleted file]
lib/classes/session/memcached.php
lib/classes/session/util.php [deleted file]
lib/db/install.xml
lib/db/messages.php
lib/db/services.php
lib/db/upgrade.php
lib/ddl/mssql_sql_generator.php
lib/ddl/mysql_sql_generator.php
lib/ddl/oracle_sql_generator.php
lib/ddl/sql_generator.php
lib/ddl/tests/ddl_test.php
lib/deprecatedlib.php
lib/dml/moodle_database.php
lib/dml/tests/dml_test.php
lib/editor/atto/plugins/html/styles.css [new file with mode: 0644]
lib/editor/atto/plugins/html/thirdpartylibs.xml [new file with mode: 0644]
lib/editor/atto/plugins/html/version.php
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify-debug.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify-min.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-debug.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-min.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/moodle-atto_html-codemirror-core.css [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/skins/sam/moodle-atto_html-codemirror-skin.css [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/skins/sam/moodle-atto_html-codemirror.css [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-debug.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-min.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/LICENSE [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/build.json [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/js/beautify-css.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/js/beautify-html.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/js/beautify.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/js/wrapper.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/meta/beautify.json [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/readme_moodle.txt [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/button/build.json
lib/editor/atto/plugins/html/yui/src/button/js/button.js
lib/editor/atto/plugins/html/yui/src/button/meta/button.json
lib/editor/atto/plugins/html/yui/src/codemirror/LICENSE [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/assets/moodle-atto_html-codemirror-core.css [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/assets/skins/sam/moodle-atto_html-codemirror-skin.css [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/build.json [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/codemirror.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/css.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/htmlmixed.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/javascript.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/wrapper-end.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/wrapper-mid.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/wrapper.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/xml.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/meta/codemirror.json [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/readme_moodle.txt [new file with mode: 0644]
lib/editor/tinymce/module.js
lib/enrollib.php
lib/filterlib.php
lib/form/filemanager.php
lib/grouplib.php
lib/jquery/readme_moodle.txt
lib/moodlelib.php
lib/navigationlib.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/phpunit/classes/util.php
lib/questionlib.php
lib/requirejs/readme_moodle.txt
lib/templates/paged_content.mustache
lib/templates/paged_content_pages.mustache
lib/templates/paged_content_paging_bar.mustache
lib/templates/paged_content_paging_dropdown.mustache
lib/tests/accesslib_test.php
lib/tests/grouplib_test.php
lib/tests/moodlelib_test.php
lib/tests/questionlib_test.php
lib/upgrade.txt
lib/xmldb/xmldb_index.php
lib/xmldb/xmldb_table.php
message/amd/build/message_area_profile.min.js
message/amd/src/message_area_profile.js
message/classes/api.php
message/classes/output/messagearea/messages.php
message/classes/privacy/provider.php
message/externallib.php
message/lib.php
message/pendingcontactrequests.php [new file with mode: 0644]
message/tests/api_test.php
message/tests/behat/manage_contacts.feature
message/tests/events_test.php
message/tests/externallib_test.php
message/tests/messagelib_test.php
message/tests/privacy_provider_test.php
message/upgrade.txt
mod/assign/feedback/editpdf/classes/event/observer.php
mod/assign/feedback/editpdf/classes/task/convert_submissions.php
mod/assign/feedback/editpdf/db/install.xml
mod/assign/feedback/editpdf/db/upgrade.php
mod/assign/feedback/editpdf/tests/editpdf_test.php
mod/assign/feedback/editpdf/version.php
mod/assign/locallib.php
mod/assign/submission/file/locallib.php
mod/assign/submission/onlinetext/classes/privacy/provider.php
mod/assign/tests/locallib_test.php
mod/book/lib.php
mod/book/tests/lib_test.php
mod/choice/lib.php
mod/choice/tests/lib_test.php
mod/data/lib.php
mod/data/tests/lib_test.php
mod/feedback/analysis_to_excel.php
mod/feedback/tests/lib_test.php
mod/folder/lib.php
mod/folder/tests/lib_test.php
mod/forum/lib.php
mod/forum/styles.css
mod/forum/tests/behat/posts_ordering_blog.feature
mod/forum/tests/lib_test.php
mod/forum/tests/subscriptions_test.php
mod/forum/upgrade.txt
mod/glossary/lib.php
mod/glossary/tests/lib_test.php
mod/imscp/lib.php
mod/imscp/tests/lib_test.php
mod/label/lib.php
mod/label/tests/lib_test.php
mod/lesson/lib.php
mod/lesson/tests/lib_test.php
mod/lti/classes/local/ltiservice/resource_base.php
mod/quiz/attemptlib.php
mod/quiz/locallib.php
mod/quiz/report/attemptsreport.php
mod/quiz/report/grading/tests/behat/grading.feature
mod/quiz/report/overview/tests/behat/basic.feature
mod/quiz/report/reportlib.php
mod/quiz/report/responses/tests/behat/basic.feature
mod/quiz/report/statistics/lang/en/quiz_statistics.php
mod/quiz/report/statistics/statistics_table.php
mod/quiz/report/statistics/tests/behat/basic.feature [new file with mode: 0644]
mod/quiz/report/statistics/tests/statistics_table_test.php [new file with mode: 0644]
mod/quiz/tests/behat/attempt_basic.feature
mod/quiz/tests/behat/behat_mod_quiz.php
mod/quiz/tests/behat/completion_condition_attempts_used.feature
mod/quiz/tests/behat/completion_condition_passing_grade.feature
mod/quiz/tests/behat/preview.feature
mod/quiz/tests/behat/quiz_reset.feature
mod/quiz/tests/generator/lib.php
mod/quiz/tests/reportlib_test.php
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-debug.js
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-min.js
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop.js
mod/quiz/yui/src/dragdrop/js/resource.js
mod/workshop/form/numerrors/edit_form.php
mod/workshop/form/numerrors/lang/en/workshopform_numerrors.php
npm-shrinkwrap.json
privacy/classes/local/request/moodle_content_writer.php
privacy/classes/output/exported_html_page.php [new file with mode: 0644]
privacy/classes/output/exported_navigation_page.php [new file with mode: 0644]
privacy/classes/output/renderer.php [new file with mode: 0644]
privacy/export_files/general.css [new file with mode: 0644]
privacy/export_files/general.js [new file with mode: 0644]
privacy/templates/htmlpage.mustache [new file with mode: 0644]
privacy/templates/navigation.mustache [new file with mode: 0644]
privacy/tests/moodle_content_writer_test.php
question/behaviour/interactive/behaviour.php
question/category.php
question/category_class.php
question/category_form.php
question/classes/statistics/questions/all_calculated_for_qubaid_condition.php
question/classes/statistics/questions/calculated_question_summary.php [new file with mode: 0644]
question/editlib.php
question/engine/questionattempt.php
question/engine/tests/helpers.php
question/format.php
question/format/gift/tests/behat/import_export.feature
question/format/upgrade.txt
question/format/xml/format.php
question/format/xml/tests/fixtures/categories_reverse_order.xml [new file with mode: 0644]
question/format/xml/tests/fixtures/category_with_description.xml [new file with mode: 0644]
question/format/xml/tests/fixtures/export_category.xml [new file with mode: 0644]
question/format/xml/tests/fixtures/nested_categories.xml [new file with mode: 0644]
question/format/xml/tests/fixtures/nested_categories_with_questions.xml [new file with mode: 0644]
question/format/xml/tests/fixtures/old_format_file.xml [new file with mode: 0644]
question/format/xml/tests/fixtures/truefalse.xml
question/format/xml/tests/qformat_xml_import_export_test.php [new file with mode: 0644]
question/format/xml/tests/xmlformat_test.php
question/tests/backup_test.php
question/tests/behat/question_categories_idnumber.feature [new file with mode: 0644]
question/tests/calculated_question_summary_test.php [new file with mode: 0644]
question/tests/generator/lib.php
question/tests/generator_test.php
question/type/ddwtos/tests/questiontype_test.php
question/type/description/tests/behat/export.feature
question/type/edit_question_form.php
question/type/essay/question.php
question/type/essay/tests/behat/export.feature
question/type/gapselect/tests/questiontype_test.php
question/type/match/tests/questiontype_test.php
question/type/missingtype/question.php
question/type/missingtype/tests/missingtype_test.php
question/type/numerical/question.php
question/type/questionbase.php
question/type/questiontypebase.php
question/type/shortanswer/question.php
question/type/shortanswer/tests/behat/export.feature
question/type/truefalse/question.php
question/type/truefalse/tests/behat/export.feature
repository/dropbox/pix/icon.png
repository/dropbox/pix/icon.svg [new file with mode: 0644]
tag/tests/external_test.php
theme/boost/classes/output/core_renderer.php
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/bs4alphacompat.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/drawer.scss
theme/boost/scss/moodle/icons.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/boost/templates/core/initials_bar.mustache
theme/boost/templates/core/navbar.mustache
theme/boost/templates/core_admin/settings_search_results.mustache
theme/bootstrapbase/less/moodle/admin.less
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/bs4-compat.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_myoverview/course-event-list-item.mustache [deleted file]
theme/bootstrapbase/templates/block_myoverview/course-event-list.mustache [deleted file]
theme/bootstrapbase/templates/block_myoverview/course-summary.mustache [deleted file]
theme/bootstrapbase/templates/block_myoverview/courses-view-nav-grouping-display-filter.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_myoverview/courses-view.mustache
theme/bootstrapbase/templates/block_myoverview/event-list-item.mustache [deleted file]
theme/bootstrapbase/templates/block_myoverview/main.mustache
theme/bootstrapbase/templates/block_myoverview/timeline-view.mustache [deleted file]
theme/bootstrapbase/templates/block_timeline/course-item-loading-placeholder.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/event-list-item.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/event-list-items.mustache [moved from blocks/myoverview/templates/event-list-items.mustache with 91% similarity]
theme/bootstrapbase/templates/block_timeline/event-list.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/main.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/nav-day-filter.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/nav-view-selector.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/placeholder-event-list-item.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/view.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/core/paged_content_paging_bar.mustache
theme/bootstrapbase/templates/core/paged_content_paging_dropdown.mustache
user/classes/participants_table.php
user/index.php
user/lib.php
user/renderer.php
user/tests/behat/filter_participants.feature
user/tests/userlib_test.php
version.php

index 6345306..420eb21 100644 (file)
@@ -9,6 +9,8 @@ auth/cas/CAS/
 enrol/lti/ims-blti/
 filter/algebra/AlgParser.pm
 filter/tex/mimetex.*
+lib/editor/atto/plugins/html/yui/src/codemirror/
+lib/editor/atto/plugins/html/yui/src/beautify/
 lib/editor/atto/yui/src/rangy/js/*.*
 lib/editor/tinymce/plugins/pdw/tinymce/
 lib/editor/tinymce/plugins/spellchecker/rpc.php
index b0dc0dc..be8cec8 100644 (file)
@@ -1,4 +1,5 @@
 # Generated by "grunt ignorefiles"
+**/yui/build/*
 theme/bootstrapbase/style/
 theme/clean/style/custom.css
 theme/more/style/custom.css
@@ -11,6 +12,8 @@ auth/cas/CAS/
 enrol/lti/ims-blti/
 filter/algebra/AlgParser.pm
 filter/tex/mimetex.*
+lib/editor/atto/plugins/html/yui/src/codemirror/
+lib/editor/atto/plugins/html/yui/src/beautify/
 lib/editor/atto/yui/src/rangy/js/*.*
 lib/editor/tinymce/plugins/pdw/tinymce/
 lib/editor/tinymce/plugins/spellchecker/rpc.php
index 0b6690a..aa0db16 100644 (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
 
index 78583d0..29d271c 100644 (file)
@@ -222,6 +222,7 @@ module.exports = function(grunt) {
       // Generate .stylelintignore.
       var stylelintIgnores = [
           '# Generated by "grunt ignorefiles"',
+          '**/yui/build/*',
           'theme/bootstrapbase/style/',
           'theme/clean/style/custom.css',
           'theme/more/style/custom.css',
index 71e9bcb..88609cf 100644 (file)
@@ -46,7 +46,6 @@ $controller = new $classformode[$mode]();
 
 if (optional_param('submit', false, PARAM_BOOL) && data_submitted() && confirm_sesskey()) {
     $controller->process_submission();
-    $syscontext->mark_dirty();
     $event = null;
     // Create event depending on mode.
     switch ($mode) {
index db7e16b..4e597f4 100644 (file)
@@ -123,9 +123,6 @@ abstract class core_role_capability_table_with_risks extends core_role_capabilit
             assign_capability($changedcap, $this->permissions[$changedcap],
                 $this->roleid, $this->context->id, true);
         }
-
-        // Force accessinfo refresh for users visiting this context.
-        $this->context->mark_dirty();
     }
 
     public function display() {
index e32f398..9da57eb 100644 (file)
@@ -84,12 +84,10 @@ switch ($action) {
             die;
         }
         if (!delete_role($roleid)) {
-            // The delete failed, but mark the context dirty in case.
-            $systemcontext->mark_dirty();
+            // The delete failed.
             print_error('cannotdeleterolewithid', 'error', $baseurl, $roleid);
         }
         // Deleted a role sitewide...
-        $systemcontext->mark_dirty();
         redirect($baseurl);
         break;
 
index db05c1d..57a230e 100644 (file)
@@ -32,9 +32,9 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) { // sp
         'customusermenuitems',
         new lang_string('customusermenuitems', 'admin'),
         new lang_string('configcustomusermenuitems', 'admin'),
-        'grades,grades|/grade/report/mygrades.php|grades
-messages,message|/message/index.php|message
-preferences,moodle|/user/preferences.php|preferences',
+        'grades,grades|/grade/report/mygrades.php|t/grades
+messages,message|/message/index.php|t/message
+preferences,moodle|/user/preferences.php|t/preferences',
         PARAM_RAW,
         '50',
         '10'
@@ -218,6 +218,9 @@ preferences,moodle|/user/preferences.php|preferences',
     // coursecontact is the person responsible for course - usually manages enrolments, receives notification, etc.
     $temp = new admin_settingpage('coursecontact', new lang_string('courses'));
     $temp->add(new admin_setting_special_coursecontact());
+    $temp->add(new admin_setting_configcheckbox('coursecontactduplicates',
+            new lang_string('coursecontactduplicates', 'admin'),
+            new lang_string('coursecontactduplicates_desc', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('courselistshortnames',
             new lang_string('courselistshortnames', 'admin'),
             new lang_string('courselistshortnames_desc', 'admin'), 0));
@@ -227,6 +230,10 @@ preferences,moodle|/user/preferences.php|preferences',
             new lang_string('configcourseoverviewfileslimit', 'admin'), 1, PARAM_INT));
     $temp->add(new admin_setting_configtext('courseoverviewfilesext', new lang_string('courseoverviewfilesext'),
             new lang_string('configcourseoverviewfilesext', 'admin'), '.jpg,.gif,.png'));
+    $temp->add(new admin_setting_configtext('coursegraceperiodbefore', new lang_string('coursegraceperiodbefore', 'admin'),
+        new lang_string('configcoursegraceperiodbefore', 'admin'), 0, PARAM_INT));
+    $temp->add(new admin_setting_configtext('coursegraceperiodafter', new lang_string('coursegraceperiodafter', 'admin'),
+        new lang_string('configcoursegraceperiodafter', 'admin'), 0, PARAM_INT));
     $ADMIN->add('appearance', $temp);
 
     $temp = new admin_settingpage('ajax', new lang_string('ajaxuse'));
@@ -252,4 +259,3 @@ preferences,moodle|/user/preferences.php|preferences',
     $ADMIN->add('appearance', $temp);
 
 } // end of speedup
-
index 64ef150..cf9b286 100644 (file)
@@ -43,6 +43,12 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
             array('moodle/category:manage')
         )
     );
+    $ADMIN->add('courses',
+        new admin_externalpage('addnewcourse', new lang_string('addnewcourse'),
+            new moodle_url('/course/edit.php', array('category' => 0)),
+            array('moodle/category:manage')
+        )
+    );
     $ADMIN->add('courses',
         new admin_externalpage('restorecourse', new lang_string('restorecourse', 'admin'),
             new moodle_url('/backup/restorefile.php', array('contextid' => context_system::instance()->id)),
index f2b50d5..db26385 100644 (file)
@@ -15,7 +15,11 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
 
     $optionalsubsystems->add(new admin_setting_configcheckbox('messaging', new lang_string('messaging', 'admin'), new lang_string('configmessaging','admin'), 1));
 
-    $optionalsubsystems->add(new admin_setting_configcheckbox('messaginghidereadnotifications', new lang_string('messaginghidereadnotifications', 'admin'), new lang_string('configmessaginghidereadnotifications','admin'), 0));
+    $optionalsubsystems->add(new admin_setting_configcheckbox('messagingallusers',
+        new lang_string('messagingallusers', 'admin'),
+        new lang_string('configmessagingallusers', 'admin'),
+        0)
+    );
 
     $options = array(DAYSECS=>new lang_string('secondstotime86400'), WEEKSECS=>new lang_string('secondstotime604800'), 2620800=>new lang_string('nummonths', 'moodle', 1), 15724800=>new lang_string('nummonths', 'moodle', 6),0=>new lang_string('never'));
     $optionalsubsystems->add(new admin_setting_configselect('messagingdeletereadnotificationsdelay', new lang_string('messagingdeletereadnotificationsdelay', 'admin'), new lang_string('configmessagingdeletereadnotificationsdelay', 'admin'), 604800, $options));
index 78b6205..df82b0f 100644 (file)
@@ -22,7 +22,7 @@
     Context variables required for this template:
     * actionurl - Url to post to
     * hasresults - True if there are results
-    * results - List of results containing url, title, settings (array of raw html)
+    * results - List of results containing url, title, path (array of strings), settings (array of raw html)
     * showsave - Show save buttons
 
     Example context (json):
@@ -30,7 +30,7 @@
         "actionurl": "/",
         "hasresults": true,
         "results": [
-            { "url": "/", "title": "Match!", "settings": [ "blah blah blah" ] }
+            { "url": "/", "title": "Match!", "path": ["Administration", "Match!"], "settings": [ "blah blah blah" ] }
         ]
     }
 }}
     </div>
     <fieldset>
         <div class="clearer"></div>
+        <h2 class="main">{{#str}}searchresults, admin{{/str}}</h2>
         {{#hasresults}}
             {{#results}}
-                <h2 class="main">{{#str}}searchresults, admin{{/str}} - <a href="{{url}}">{{{title}}}</a></h2>
+                <h3 class="adminpagetitle"><a href="{{url}}">{{{title}}}</a></h3>
+                <ul class="adminpagepath" aria-label="{{#str}} pagepath, core {{/str}}">
+                    {{#path}}
+                    <li>{{.}}</li>
+                    {{/path}}
+                </ul>
                 <fieldset class="adminsettings">
                     {{#settings}}
                         <div class="clearer"></div>
diff --git a/admin/tool/dataprivacy/amd/build/defaultsactions.min.js b/admin/tool/dataprivacy/amd/build/defaultsactions.min.js
new file mode 100644 (file)
index 0000000..b8f0bc3
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/defaultsactions.min.js differ
index 0ecae4c..6c94ab3 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/events.min.js and b/admin/tool/dataprivacy/amd/build/events.min.js differ
index c405d17..2260ee3 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/requestactions.min.js and b/admin/tool/dataprivacy/amd/build/requestactions.min.js differ
diff --git a/admin/tool/dataprivacy/amd/src/defaultsactions.js b/admin/tool/dataprivacy/amd/src/defaultsactions.js
new file mode 100644 (file)
index 0000000..76cbb89
--- /dev/null
@@ -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();
+        }
+    };
+});
index 4e7ff77..cb1d9af 100644 (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'
     };
 });
index 4f3c406..37c4d92 100644 (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.
index f323278..aa58917 100644 (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);
index 6ee9707..f626371 100644 (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;
@@ -94,6 +94,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 +111,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 +138,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 +171,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 +183,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 +211,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();
     }
 
     /**
@@ -223,14 +244,6 @@ class api {
             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 user making the request.
@@ -619,6 +632,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 +659,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 +739,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 +752,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 +771,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 +784,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 +794,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 +807,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 +822,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 +835,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 +845,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 +871,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 +884,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');
@@ -907,7 +915,6 @@ class api {
      * @return category|false
      */
     public static function get_effective_context_category(\context $context, $forcedvalue=false) {
-        self::check_can_manage_data_registry($context->id);
         if (!data_registry::defaults_set()) {
             return false;
         }
@@ -922,8 +929,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;
         }
@@ -939,7 +945,6 @@ class api {
      * @return category|false
      */
     public static function get_effective_contextlevel_category($contextlevel, $forcedvalue=false) {
-        self::check_can_manage_data_registry(\context_system::instance()->id);
         if (!data_registry::defaults_set()) {
             return false;
         }
@@ -955,7 +960,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;
         }
@@ -963,38 +967,6 @@ class api {
         return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue);
     }
 
-    /**
-     * Creates an expired context record for the provided context id.
-     *
-     * @param int $contextid
-     * @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,
-        ];
-        $expiredctx = new expired_context(0, $record);
-        $expiredctx->save();
-
-        return $expiredctx;
-    }
-
-    /**
-     * Deletes an expired context record.
-     *
-     * @param int $id The tool_dataprivacy_ctxexpire id.
-     * @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();
-    }
-
     /**
      * Updates the status of an expired context.
      *
@@ -1003,8 +975,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();
     }
@@ -1141,4 +1111,89 @@ class api {
 
         return $approvedcollection;
     }
+
+    /**
+     * Updates the default category and purpose for a given context level (and optionally, a plugin).
+     *
+     * @param int $contextlevel The context level.
+     * @param int $categoryid The ID matching the category.
+     * @param int $purposeid The ID matching the purpose record.
+     * @param int $activity The name of the activity that we're making a defaults configuration for.
+     * @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
+     * @return boolean True if set/unset config succeeds. Otherwise, it throws an exception.
+     */
+    public static function set_context_defaults($contextlevel, $categoryid, $purposeid, $activity = null, $override = false) {
+        global $DB;
+
+        // Get the class name associated with this context level.
+        $classname = context_helper::get_class_for_level($contextlevel);
+        list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname, $activity);
+
+        // Check the default category to be set.
+        if ($categoryid == context_instance::INHERIT) {
+            unset_config($categoryvar, 'tool_dataprivacy');
+
+        } else {
+            // Make sure the given category ID exists first.
+            $categorypersistent = new category($categoryid);
+            $categorypersistent->read();
+
+            // Then set the new default value.
+            set_config($categoryvar, $categoryid, 'tool_dataprivacy');
+        }
+
+        // Check the default purpose to be set.
+        if ($purposeid == context_instance::INHERIT) {
+            // If the defaults is set to inherit, just unset the config value.
+            unset_config($purposevar, 'tool_dataprivacy');
+
+        } else {
+            // Make sure the given purpose ID exists first.
+            $purposepersistent = new purpose($purposeid);
+            $purposepersistent->read();
+
+            // Then set the new default value.
+            set_config($purposevar, $purposeid, 'tool_dataprivacy');
+        }
+
+        // Unset instances that have been assigned with custom purpose and category, if override was specified.
+        if ($override) {
+            // We'd like to find context IDs that we want to unset.
+            $statements = ["SELECT c.id as contextid FROM {context} c"];
+            // Based on this context level.
+            $params = ['contextlevel' => $contextlevel];
+
+            if ($contextlevel == CONTEXT_MODULE) {
+                // If we're deleting module context instances, we need to make sure the instance ID is in the course modules table.
+                $statements[] = "JOIN {course_modules} cm ON cm.id = c.instanceid";
+                // And that the module is listed on the modules table.
+                $statements[] = "JOIN {modules} m ON m.id = cm.module";
+
+                if ($activity) {
+                    // If we're overriding for an activity module, make sure that the context instance matches that activity.
+                    $statements[] = "AND m.name = :modname";
+                    $params['modname'] = $activity;
+                }
+            }
+            // Make sure this context instance exists in the tool_dataprivacy_ctxinstance table.
+            $statements[] = "JOIN {tool_dataprivacy_ctxinstance} tdc ON tdc.contextid = c.id";
+            // And that the context level of this instance matches the given context level.
+            $statements[] = "WHERE c.contextlevel = :contextlevel";
+
+            // Build our SQL query by gluing the statements.
+            $sql = implode("\n", $statements);
+
+            // Get the context records matching our query.
+            $contextids = $DB->get_fieldset_sql($sql, $params);
+
+            // Delete the matching context instances.
+            foreach ($contextids as $contextid) {
+                if ($instance = context_instance::get_record_by_contextid($contextid, false)) {
+                    self::unset_context_instance($instance);
+                }
+            }
+        }
+
+        return true;
+    }
 }
index 1fac2f5..d10ab83 100644 (file)
 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();
 
@@ -57,13 +54,22 @@ class data_registry {
     /**
      * 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
         ];
     }
 
@@ -72,17 +78,31 @@ 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;
         }
@@ -169,14 +189,14 @@ 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) {
 
         if ($element !== 'purpose' && $element !== 'category') {
             throw new coding_exception('Only \'purpose\' and \'category\' are supported.');
         }
         $fieldname = $element . 'id';
 
-        if ($forcedvalue === false) {
+        if (empty($forcedvalue)) {
             $instance = context_instance::get_record_by_contextid($context->id, false);
 
             if (!$instance) {
@@ -195,14 +215,29 @@ class data_registry {
             // 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);
+                return self::get_effective_contextlevel_value(CONTEXT_USER, $element);
+            }
+
+            $parents = $context->get_parent_contexts(true);
+            foreach ($parents as $parent) {
+                if ($parent->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_USER, $element);
+                }
             }
+
+            // Check if we need to pass the plugin name of an activity.
+            $forplugin = '';
+            if ($context->contextlevel == CONTEXT_MODULE) {
+                list($course, $cm) = get_course_and_cm_from_cmid($context->instanceid);
+                $forplugin = $cm->modname;
+            }
+            // Use the default context level value.
+            list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category(
+                $context->contextlevel, false, false, $forplugin
+            );
+
+            return self::get_element_instance($element, $$fieldname);
         }
 
         // Specific value for this context instance.
@@ -283,14 +318,15 @@ class data_registry {
      * Returns the effective default purpose and category for a context level.
      *
      * @param int $contextlevel
-     * @param int $forcedpurposevalue Use this value as if this was this context level purpose.
-     * @param int $forcedcategoryvalue Use this value as if this was this context level category.
+     * @param int|bool $forcedpurposevalue Use this value as if this was this context level purpose.
+     * @param int|bool $forcedcategoryvalue Use this value as if this was this context level category.
+     * @param string $activity The plugin name of the activity.
      * @return int[]
      */
     public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false,
-                                                                                   $forcedcategoryvalue = false) {
+                                                                                   $forcedcategoryvalue = false, $activity = '') {
 
-        list($purposeid, $categoryid) = self::get_defaults($contextlevel);
+        list($purposeid, $categoryid) = self::get_defaults($contextlevel, $activity);
 
         // Honour forced values.
         if ($forcedpurposevalue) {
index 3010438..5ac228d 100644 (file)
@@ -159,4 +159,51 @@ class expired_context extends \core\persistent {
 
         return $DB->count_records_sql($sql, $params);
     }
+
+    /**
+     * Create a new expired_context based on the context, and expiry_info object.
+     *
+     * @param   \context        $context
+     * @param   expiry_info     $info
+     * @return  expired_context
+     */
+    public static function create_from_expiry_info(\context $context, expiry_info $info) : expired_context {
+        $record = (object) [
+            'contextid' => $context->id,
+            'status' => self::STATUS_EXPIRED,
+        ];
+
+        $expiredcontext = new static(0, $record);
+        $expiredcontext->save();
+
+        return $expiredcontext;
+    }
+
+    /**
+     * Update the expired_context from an expiry_info object which relates to this context.
+     *
+     * @param   expiry_info     $info
+     * @return  $this
+     */
+    public function update_from_expiry_info(expiry_info $info) : expired_context {
+        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);
+    }
 }
index 539fc28..be8d731 100644 (file)
@@ -34,121 +34,680 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2018 David Monllao
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-abstract class expired_contexts_manager {
+class expired_contexts_manager {
 
     /**
      * Number of deleted contexts for each scheduled task run.
      */
     const DELETE_LIMIT = 200;
 
+    /** @var progress_trace The log progress tracer */
+    protected $progresstracer = null;
+
+    /** @var manager The privacy manager */
+    protected $manager = null;
+
     /**
-     * Returns the list of expired context instances.
+     * Flag expired contexts as expired.
      *
-     * @return \stdClass[]
+     * @return  int[]   The number of contexts flagged as expired for courses, and users.
+     */
+    public function flag_expired_contexts() : array {
+        if (!$this->check_requirements()) {
+            return [0, 0];
+        }
+
+        // Clear old and stale records first.
+        static::clear_old_records();
+
+        $data = static::get_nested_expiry_info_for_courses();
+        $coursecount = 0;
+        foreach ($data as $expiryrecord) {
+            if ($this->update_from_expiry_info($expiryrecord)) {
+                $coursecount++;
+            }
+        }
+
+        $data = static::get_nested_expiry_info_for_user();
+        $usercount = 0;
+        foreach ($data as $expiryrecord) {
+            if ($this->update_from_expiry_info($expiryrecord)) {
+                $usercount++;
+            }
+        }
+
+        return [$coursecount, $usercount];
+    }
+
+    /**
+     * Clear old and stale records.
      */
-    abstract protected function get_expired_contexts();
+    protected static function clear_old_records() {
+        global $DB;
+
+        $sql = "SELECT dpctx.*
+                  FROM {tool_dataprivacy_ctxexpired} dpctx
+             LEFT JOIN {context} ctx ON ctx.id = dpctx.contextid
+                 WHERE ctx.id IS NULL";
+
+        $orphaned = $DB->get_recordset_sql($sql);
+        foreach ($orphaned as $orphan) {
+            $expiredcontext = new expired_context(0, $orphan);
+            $expiredcontext->delete();
+        }
+
+        // Delete any child of a user context.
+        $parentpath = $DB->sql_concat('ctxuser.path', "'/%'");
+        $params = [
+            'contextuser' => CONTEXT_USER,
+        ];
+
+        $sql = "SELECT dpctx.*
+                  FROM {tool_dataprivacy_ctxexpired} dpctx
+                 WHERE dpctx.contextid IN (
+                    SELECT ctx.id
+                        FROM {context} ctxuser
+                        JOIN {context} ctx ON ctx.path LIKE {$parentpath}
+                       WHERE ctxuser.contextlevel = :contextuser
+                    )";
+        $userchildren = $DB->get_recordset_sql($sql, $params);
+        foreach ($userchildren as $child) {
+            $expiredcontext = new expired_context(0, $child);
+            $expiredcontext->delete();
+        }
+    }
 
     /**
-     * Specify with context levels this expired contexts manager is deleting.
+     * Get the full nested set of expiry data relating to all contexts.
      *
-     * @return int[]
+     * @param   string      $contextpath A contexpath to restrict results to
+     * @return  \stdClass[]
      */
-    abstract protected function get_context_levels();
+    protected static function get_nested_expiry_info($contextpath = '') : array {
+        $coursepaths = self::get_nested_expiry_info_for_courses($contextpath);
+        $userpaths = self::get_nested_expiry_info_for_user($contextpath);
+
+        return array_merge($coursepaths, $userpaths);
+    }
 
     /**
-     * Flag expired contexts as expired.
+     * Get the full nested set of expiry data relating to course-related contexts.
      *
-     * @return int The number of contexts flagged as expired.
+     * @param   string      $contextpath A contexpath to restrict results to
+     * @return  \stdClass[]
      */
-    public function flag_expired() {
+    protected static function get_nested_expiry_info_for_courses($contextpath = '') : array {
+        global $DB;
 
-        if (!$this->check_requirements()) {
-            return 0;
-        }
+        $contextfields = \context_helper::get_preload_record_columns_sql('ctx');
+        $expiredfields = expired_context::get_sql_fields('expiredctx', 'expiredctx');
+        $purposefields = 'dpctx.purposeid';
+        $coursefields = 'ctxcourse.expirydate AS expirydate';
+        $fields = implode(', ', ['ctx.id', $contextfields, $expiredfields, $coursefields, $purposefields]);
+
+        // We want all contexts at course-dependant levels.
+        $parentpath = $DB->sql_concat('ctxcourse.path', "'/%'");
 
-        $contexts = $this->get_expired_contexts();
-        foreach ($contexts as $context) {
-            api::create_expired_context($context->id);
+        // This SQL query returns all course-dependant contexts (including the course context)
+        // which course end date already passed.
+        // This is ordered by the context path in reverse order, which will give the child nodes before any parent node.
+        $params = [
+            'contextlevel' => CONTEXT_COURSE,
+        ];
+        $where = '';
+
+        if (!empty($contextpath)) {
+            $where = "WHERE (ctx.path = :pathmatchexact OR ctx.path LIKE :pathmatchchildren)";
+            $params['pathmatchexact'] = $contextpath;
+            $params['pathmatchchildren'] = "{$contextpath}/%";
         }
 
-        return count($contexts);
+        $sql = "SELECT $fields
+                  FROM {context} ctx
+                  JOIN (
+                        SELECT c.enddate AS expirydate, subctx.path
+                          FROM {context} subctx
+                          JOIN {course} c
+                            ON subctx.contextlevel = :contextlevel
+                           AND subctx.instanceid = c.id
+                           AND c.format != 'site'
+                       ) ctxcourse
+                    ON ctx.path LIKE {$parentpath} OR ctx.path = ctxcourse.path
+             LEFT JOIN {tool_dataprivacy_ctxinstance} dpctx
+                    ON dpctx.contextid = ctx.id
+             LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx
+                    ON ctx.id = expiredctx.contextid
+                 {$where}
+              ORDER BY ctx.path DESC";
+
+        return self::get_nested_expiry_info_from_sql($sql, $params);
     }
 
     /**
-     * Deletes the expired contexts.
+     * Get the full nested set of expiry data.
      *
-     * @return int The number of deleted contexts.
+     * @param   string      $contextpath A contexpath to restrict results to
+     * @return  \stdClass[]
      */
-    public function delete() {
+    protected static function get_nested_expiry_info_for_user($contextpath = '') : array {
+        global $DB;
 
-        $numprocessed = 0;
+        $contextfields = \context_helper::get_preload_record_columns_sql('ctx');
+        $expiredfields = expired_context::get_sql_fields('expiredctx', 'expiredctx');
+        $purposefields = 'dpctx.purposeid';
+        $userfields = 'u.lastaccess AS expirydate';
+        $fields = implode(', ', ['ctx.id', $contextfields, $expiredfields, $userfields, $purposefields]);
 
-        if (!$this->check_requirements()) {
-            return $numprocessed;
+        // We want all contexts at user-dependant levels.
+        $parentpath = $DB->sql_concat('ctxuser.path', "'/%'");
+
+        // This SQL query returns all user-dependant contexts (including the user context)
+        // This is ordered by the context path in reverse order, which will give the child nodes before any parent node.
+        $params = [
+            'contextlevel' => CONTEXT_USER,
+        ];
+        $where = '';
+
+        if (!empty($contextpath)) {
+            $where = "AND ctx.path = :pathmatchexact";
+            $params['pathmatchexact'] = $contextpath;
         }
 
-        $privacymanager = new manager();
-        $privacymanager->set_observer(new \tool_dataprivacy\manager_observer());
+        $sql = "SELECT $fields, u.deleted AS userdeleted
+                  FROM {context} ctx
+                  JOIN {user} u ON ctx.instanceid = u.id
+             LEFT JOIN {tool_dataprivacy_ctxinstance} dpctx
+                    ON dpctx.contextid = ctx.id
+             LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx
+                    ON ctx.id = expiredctx.contextid
+                 WHERE ctx.contextlevel = :contextlevel {$where}
+              ORDER BY ctx.path DESC";
 
-        foreach ($this->get_context_levels() as $level) {
+        return self::get_nested_expiry_info_from_sql($sql, $params);
+    }
+
+    /**
+     * Get the full nested set of expiry data given appropriate SQL.
+     * Only contexts which have expired will be included.
+     *
+     * @param   string      $sql The SQL used to select the nested information.
+     * @param   array       $params The params required by the SQL.
+     * @return  \stdClass[]
+     */
+    protected static function get_nested_expiry_info_from_sql(string $sql, array $params) : array {
+        global $DB;
+
+        $fulllist = $DB->get_recordset_sql($sql, $params);
+        $datalist = [];
+        $expiredcontents = [];
+        $pathstoskip = [];
+        foreach ($fulllist as $record) {
+            \context_helper::preload_from_record($record);
+            $context = \context::instance_by_id($record->id, false);
 
-            $expiredcontexts = expired_context::get_records_by_contextlevel($level, expired_context::STATUS_APPROVED);
+            if (!self::is_eligible_for_deletion($pathstoskip, $context)) {
+                // We should skip this context, and therefore all of it's children.
+                $datalist = array_filter($datalist, function($data, $path) use ($context) {
+                    // Remove any child of this context.
+                    // Technically this should never be fulfilled because the query is ordered in path DESC, but is kept
+                    // in to be certain.
+                    return (false === strpos($path, "{$context->path}/"));
+                }, ARRAY_FILTER_USE_BOTH);
 
-            foreach ($expiredcontexts as $expiredctx) {
+                if ($record->expiredctxid) {
+                    // There was previously an expired context record.
+                    // Delete it to be on the safe side.
+                    $expiredcontext = new expired_context(null, expired_context::extract_record($record, 'expiredctx'));
+                    $expiredcontext->delete();
+                }
+                continue;
+            }
+
+            $purposevalue = $record->purposeid !== null ? $record->purposeid : context_instance::NOTSET;
+            $purpose = api::get_effective_context_purpose($context, $purposevalue);
 
-                if (!$this->delete_expired_context($privacymanager, $expiredctx)) {
+            if ($context instanceof \context_user && !empty($record->userdeleted)) {
+                $expiryinfo = static::get_expiry_info($purpose, $record->userdeleted);
+            } else {
+                $expiryinfo = static::get_expiry_info($purpose, $record->expirydate);
+            }
+            foreach ($datalist as $path => $data) {
+                // Merge with already-processed children.
+                if (strpos($path, $context->path) !== 0) {
                     continue;
                 }
 
-                $numprocessed += 1;
-                if ($numprocessed == self::DELETE_LIMIT) {
-                    // Close the recordset.
-                    $expiredcontexts->close();
-                    break 2;
+                $expiryinfo->merge_with_child($data->info);
+            }
+            $datalist[$context->path] = (object) [
+                'context' => $context,
+                'record' => $record,
+                'purpose' => $purpose,
+                'info' => $expiryinfo,
+            ];
+        }
+        $fulllist->close();
+
+        return $datalist;
+    }
+
+    /**
+     * Check whether the supplied context would be elible for deletion.
+     *
+     * @param   array       $pathstoskip A set of paths which should be skipped
+     * @param   \context    $context
+     * @return  bool
+     */
+    protected static function is_eligible_for_deletion(array &$pathstoskip, \context $context) : bool {
+        $shouldskip = false;
+        // Check whether any of the child contexts are ineligble.
+        $shouldskip = !empty(array_filter($pathstoskip, function($path) use ($context) {
+            // If any child context has already been skipped then it will appear in this list.
+            // Since paths include parents, test if the context under test appears as the haystack in the skipped
+            // context's needle.
+            return false !== (strpos($context->path, $path));
+        }));
+
+        if (!$shouldskip && $context instanceof \context_user) {
+            // The context instanceid is the user's ID.
+            if (isguestuser($context->instanceid) || is_siteadmin($context->instanceid)) {
+                // This is an admin, or the guest and cannot be deleted.
+                $shouldskip = true;
+            }
+
+            if (!$shouldskip) {
+                $courses = enrol_get_users_courses($context->instanceid, false, ['enddate']);
+                $requireenddate = self::require_all_end_dates_for_user_deletion();
+
+                foreach ($courses as $course) {
+                    if (empty($course->enddate)) {
+                        // This course has no end date.
+                        if ($requireenddate) {
+                            // Course end dates are required, and this course has no end date.
+                            $shouldskip = true;
+                            break;
+                        }
+
+                        // Course end dates are not required. The subsequent checks are pointless at this time so just
+                        // skip them.
+                        continue;
+                    }
+
+                    if ($course->enddate >= time()) {
+                        // This course is still in the future.
+                        $shouldskip = true;
+                        break;
+                    }
+
+                    // This course has an end date which is in the past.
+                    if (!self::is_course_expired($course)) {
+                        // This course has not expired yet.
+                        $shouldskip = true;
+                        break;
+                    }
+                }
+            }
+        }
+
+        if ($shouldskip) {
+            // Add this to the list of contexts to skip for parentage checks.
+            $pathstoskip[] = $context->path;
+        }
+
+        return !$shouldskip;
+    }
+
+    /**
+     * Deletes the expired contexts.
+     *
+     * @return  int[]       The number of deleted contexts.
+     */
+    public function process_approved_deletions() : array {
+        if (!$this->check_requirements()) {
+            return [0, 0];
+        }
+
+        $expiredcontexts = expired_context::get_records(['status' => expired_context::STATUS_APPROVED]);
+        $totalprocessed = 0;
+        $usercount = 0;
+        $coursecount = 0;
+        foreach ($expiredcontexts as $expiredctx) {
+            $context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
+            if (empty($context)) {
+                // Unable to process this request further.
+                // We have no context to delete.
+                $expiredctx->delete();
+                continue;
+            }
+
+            if ($this->delete_expired_context($expiredctx)) {
+                if ($context instanceof \context_user) {
+                    $usercount++;
+                } else {
+                    $coursecount++;
+                }
+
+                $totalprocessed++;
+                if ($totalprocessed >= $this->get_delete_limit()) {
+                    break;
                 }
             }
         }
 
-        return $numprocessed;
+        return [$coursecount, $usercount];
     }
 
     /**
      * Deletes user data from the provided context.
      *
-     * @param manager $privacymanager
      * @param expired_context $expiredctx
      * @return \context|false
      */
-    protected function delete_expired_context(manager $privacymanager, expired_context $expiredctx) {
+    protected function delete_expired_context(expired_context $expiredctx) {
+        $context = \context::instance_by_id($expiredctx->get('contextid'));
+
+        $this->get_progress()->output("Deleting context {$context->id} - " . $context->get_context_name(true, true));
 
-        $context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
-        if (!$context) {
-            api::delete_expired_context($expiredctx->get('contextid'));
+        // Update the expired_context and verify that it is still ready for deletion.
+        $expiredctx = $this->update_expired_context($expiredctx);
+        if (empty($expiredctx)) {
+            $this->get_progress()->output("Context has changed since approval and is no longer pending approval. Skipping", 1);
             return false;
         }
 
-        if (!PHPUNIT_TEST) {
-            mtrace('Deleting context ' . $context->id . ' - ' .
-                shorten_text($context->get_context_name(true, true)));
+        if (!$expiredctx->can_process_deletion()) {
+            // This only happens if the record was updated after being first fetched.
+            $this->get_progress()->output("Context has changed since approval and must be re-approved. Skipping", 1);
+            $expiredctx->set('status', expired_context::STATUS_EXPIRED);
+            $expiredctx->save();
+
+            return false;
         }
 
-        $privacymanager->delete_data_for_all_users_in_context($context);
-        api::set_expired_context_status($expiredctx, expired_context::STATUS_CLEANED);
+        $privacymanager = $this->get_privacy_manager();
+        if ($context instanceof \context_user) {
+            $this->delete_expired_user_context($expiredctx);
+        } else {
+            // This context is fully expired - that is that the default retention period has been reached.
+            $privacymanager->delete_data_for_all_users_in_context($context);
+        }
+
+        // Mark the record as cleaned.
+        $expiredctx->set('status', expired_context::STATUS_CLEANED);
+        $expiredctx->save();
 
         return $context;
     }
 
+    /**
+     * Deletes user data from the provided user context.
+     *
+     * @param expired_context $expiredctx
+     */
+    protected function delete_expired_user_context(expired_context $expiredctx) {
+        global $DB;
+
+        $contextid = $expiredctx->get('contextid');
+        $context = \context::instance_by_id($contextid);
+        $user = \core_user::get_user($context->instanceid, '*', MUST_EXIST);
+
+        $privacymanager = $this->get_privacy_manager();
+
+        // Delete all child contexts of the user context.
+        $parentpath = $DB->sql_concat('ctxuser.path', "'/%'");
+
+        $params = [
+            'contextlevel'  => CONTEXT_USER,
+            'contextid'     => $expiredctx->get('contextid'),
+        ];
+
+        $fields = \context_helper::get_preload_record_columns_sql('ctx');
+        $sql = "SELECT ctx.id, $fields
+                  FROM {context} ctxuser
+                  JOIN {context} ctx ON ctx.path LIKE {$parentpath}
+                 WHERE ctxuser.contextlevel = :contextlevel AND ctxuser.id = :contextid
+              ORDER BY ctx.path DESC";
+
+        $children = $DB->get_recordset_sql($sql, $params);
+        foreach ($children as $child) {
+            \context_helper::preload_from_record($child);
+            $context = \context::instance_by_id($child->id);
+
+            $privacymanager->delete_data_for_all_users_in_context($context);
+        }
+        $children->close();
+
+        // Delete all unprotected data that the user holds.
+        $approvedlistcollection = new \core_privacy\local\request\contextlist_collection($user->id);
+        $contextlistcollection = $privacymanager->get_contexts_for_userid($user->id);
+
+        foreach ($contextlistcollection as $contextlist) {
+            $contextids = [];
+            $approvedlistcollection->add_contextlist(new \core_privacy\local\request\approved_contextlist(
+                    $user,
+                    $contextlist->get_component(),
+                    $contextlist->get_contextids()
+                ));
+        }
+        $privacymanager->delete_data_for_user($approvedlistcollection, $this->get_progress());
+
+        // Delete the user context.
+        $context = \context::instance_by_id($expiredctx->get('contextid'));
+        $privacymanager->delete_data_for_all_users_in_context($context);
+
+        // This user is now fully expired - finish by deleting the user.
+        delete_user($user);
+    }
+
+    /**
+     * Whether end dates are required on all courses in order for a user to be expired from them.
+     *
+     * @return bool
+     */
+    protected static function require_all_end_dates_for_user_deletion() : bool {
+        $requireenddate = get_config('tool_dataprivacy', 'requireallenddatesforuserdeletion');
+
+        return !empty($requireenddate);
+    }
+
     /**
      * Check that the requirements to start deleting contexts are satisified.
      *
      * @return bool
      */
     protected function check_requirements() {
-        api::check_can_manage_data_registry(\context_system::instance()->id);
-
         if (!data_registry::defaults_set()) {
             return false;
         }
         return true;
     }
+
+    /**
+     * Check whether a date is beyond the specified period.
+     *
+     * @param   string      $period The Expiry Period
+     * @param   int         $comparisondate The date for comparison
+     * @return  bool
+     */
+    protected static function has_expired(string $period, int $comparisondate) : bool {
+        $dt = new \DateTime();
+        $dt->setTimestamp($comparisondate);
+        $dt->add(new \DateInterval($period));
+
+        return (time() >= $dt->getTimestamp());
+    }
+
+    /**
+     * Get the expiry info object for the specified purpose and comparison date.
+     *
+     * @param   purpose     $purpose The purpose of this context
+     * @param   int         $comparisondate The date for comparison
+     * @return  expiry_info
+     */
+    protected static function get_expiry_info(purpose $purpose, int $comparisondate = 0) : expiry_info {
+        if (empty($comparisondate)) {
+            // The date is empty, therefore this context cannot be considered for automatic expiry.
+            $defaultexpired = false;
+        } else {
+            $defaultexpired = static::has_expired($purpose->get('retentionperiod'), $comparisondate);
+        }
+
+        return new expiry_info($defaultexpired);
+    }
+
+    /**
+     * Update or delete the expired_context from the expiry_info object.
+     * This function depends upon the data structure returned from get_nested_expiry_info.
+     *
+     * If the context is expired in any way, then an expired_context will be returned, otherwise null will be returned.
+     *
+     * @param   \stdClass   $expiryrecord
+     * @return  expired_context|null
+     */
+    protected function update_from_expiry_info(\stdClass $expiryrecord) {
+        if ($expiryrecord->info->is_any_expired()) {
+            // The context is expired in some fashion.
+            // Create or update as required.
+            if ($expiryrecord->record->expiredctxid) {
+                $expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx'));
+                $expiredcontext->update_from_expiry_info($expiryrecord->info);
+
+                if ($expiredcontext->is_complete()) {
+                    return null;
+                }
+            } else {
+                $expiredcontext = expired_context::create_from_expiry_info($expiryrecord->context, $expiryrecord->info);
+            }
+
+            return $expiredcontext;
+        } else {
+            // The context is not expired.
+            if ($expiryrecord->record->expiredctxid) {
+                // There was previously an expired context record, but it is no longer relevant.
+                // Delete it to be on the safe side.
+                $expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx'));
+                $expiredcontext->delete();
+            }
+
+            return null;
+        }
+    }
+
+    /**
+     * Update the expired context record.
+     *
+     * Note: You should use the return value as the provided value will be used to fetch data only.
+     *
+     * @param   expired_context $expiredctx The record to update
+     * @return  expired_context|null
+     */
+    protected function update_expired_context(expired_context $expiredctx) {
+        // Fetch the context from the expired_context record.
+        $context = \context::instance_by_id($expiredctx->get('contextid'));
+
+        // Fetch the current nested expiry data.
+        $expiryrecords = self::get_nested_expiry_info($context->path);
+
+        // Find the current record.
+        if (empty($expiryrecords[$context->path])) {
+            $expiredctx->delete();
+            return null;
+        }
+
+        // Refresh the record.
+        // Note: Use the returned expiredctx.
+        $expiredctx = $this->update_from_expiry_info($expiryrecords[$context->path]);
+        if (empty($expiredctx)) {
+            return null;
+        }
+
+        if (!$context instanceof \context_user) {
+            // Where the target context is not a user, we check all children of the context.
+            // The expiryrecords array only contains children, fetched from the get_nested_expiry_info call above.
+            // No need to check that these _are_ children.
+            foreach ($expiryrecords as $expiryrecord) {
+                if ($expiryrecord->context->id === $context->id) {
+                    // This is record for the context being tested that we checked earlier.
+                    continue;
+                }
+
+                if (empty($expiryrecord->record->expiredctxid)) {
+                    // There is no expired context record for this context.
+                    // If there is no record, then this context cannot have been approved for removal.
+                    return null;
+                }
+
+                // Fetch the expired_context object for this record.
+                // This needs to be updated from the expiry_info data too as there may be child changes to consider.
+                $expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx'));
+                $expiredcontext->update_from_expiry_info($expiryrecord->info);
+                if (!$expiredcontext->is_complete()) {
+                    return null;
+                }
+            }
+        }
+
+        return $expiredctx;
+    }
+
+    /**
+     * Check whether the course has expired.
+     *
+     * @param   \stdClass   $course
+     * @return  bool
+     */
+    protected static function is_course_expired(\stdClass $course) : bool {
+        $context = \context_course::instance($course->id);
+        $expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
+
+        return !empty($expiryrecords[$context->path]) && $expiryrecords[$context->path]->info->is_fully_expired();
+    }
+
+    /**
+     * Create a new instance of the privacy manager.
+     *
+     * @return  manager
+     */
+    protected function get_privacy_manager() : manager {
+        if (null === $this->manager) {
+            $this->manager = new manager();
+            $this->manager->set_observer(new \tool_dataprivacy\manager_observer());
+        }
+
+        return $this->manager;
+    }
+
+    /**
+     * Fetch the limit for the maximum number of contexts to delete in one session.
+     *
+     * @return  int
+     */
+    protected function get_delete_limit() : int {
+        return self::DELETE_LIMIT;
+    }
+
+    /**
+     * Get the progress tracer.
+     *
+     * @return  \progress_trace
+     */
+    protected function get_progress() : \progress_trace {
+        if (null === $this->progresstracer) {
+            $this->set_progress(new \text_progress_trace());
+        }
+
+        return $this->progresstracer;
+    }
+
+    /**
+     * Set a specific tracer for the task.
+     *
+     * @param   \progress_trace $trace
+     * @return  $this
+     */
+    public function set_progress(\progress_trace $trace) : expired_contexts_manager {
+        $this->progresstracer = $trace;
+
+        return $this;
+    }
 }
diff --git a/admin/tool/dataprivacy/classes/expired_course_related_contexts.php b/admin/tool/dataprivacy/classes/expired_course_related_contexts.php
deleted file mode 100644 (file)
index f878edf..0000000
+++ /dev/null
@@ -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;
-    }
-}
diff --git a/admin/tool/dataprivacy/classes/expired_user_contexts.php b/admin/tool/dataprivacy/classes/expired_user_contexts.php
deleted file mode 100644 (file)
index 924d565..0000000
+++ /dev/null
@@ -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;
-    }
-}
diff --git a/admin/tool/dataprivacy/classes/expiry_info.php b/admin/tool/dataprivacy/classes/expiry_info.php
new file mode 100644 (file)
index 0000000..508214f
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Expiry Data.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy;
+
+use core_privacy\manager;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Expiry Data.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class expiry_info {
+
+    /** @var bool Whether this context is fully expired */
+    protected $isexpired = false;
+
+    /**
+     * Constructor for the expiry_info class.
+     *
+     * @param   bool    $isexpired Whether the retention period for this context has expired yet.
+     */
+    public function __construct(bool $isexpired) {
+        $this->isexpired = $isexpired;
+    }
+
+    /**
+     * Whether this context has 'fully' expired.
+     * That is to say that the default retention period has been reached, and that there are no unexpired roles.
+     *
+     * @return  bool
+     */
+    public function is_fully_expired() : bool {
+        return $this->isexpired;
+    }
+
+    /**
+     * Whether any part of this context has expired.
+     *
+     * @return  bool
+     */
+    public function is_any_expired() : bool {
+        if ($this->is_fully_expired()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Merge this expiry_info object with another belonging to a child context in order to set the 'safest' heritage.
+     *
+     * It is not possible to delete any part of a context that is not deleted by a parent.
+     * So if a course's retention policy has been reached, then only parts where the children have also expired can be
+     * deleted.
+     *
+     * @param   expiry_info $child The child record to merge with.
+     * @return  $this
+     */
+    public function merge_with_child(expiry_info $child) : 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->isexpired = false;
+
+        return $this;
+    }
+}
index e14e072..3dd839e 100644 (file)
@@ -92,17 +92,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 {
@@ -257,9 +270,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.
@@ -421,6 +435,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().
      *
@@ -493,6 +586,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().
      *
@@ -590,7 +762,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();
@@ -658,6 +832,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 [
@@ -707,7 +885,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();
@@ -775,6 +955,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 [
@@ -824,8 +1008,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();
@@ -839,7 +1024,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) {
@@ -894,8 +1078,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();
@@ -905,6 +1090,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);
@@ -1034,9 +1220,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)) {
@@ -1046,24 +1232,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;
+                        }
                     }
                 }
 
@@ -1110,6 +1299,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).
      *
index c5c350c..06d2469 100644 (file)
@@ -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;
+        }
+    }
 }
index f91e2f2..a6519c6 100644 (file)
@@ -29,6 +29,7 @@ use core\external\persistent_exporter;
 use DateInterval;
 use Exception;
 use renderer_base;
+use tool_dataprivacy\context_instance;
 use tool_dataprivacy\purpose;
 
 /**
@@ -143,4 +144,25 @@ class purpose_exporter extends persistent_exporter {
 
         return $values;
     }
+
+    /**
+     * Utility function that fetches a purpose name from the given ID.
+     *
+     * @param int $purposeid The purpose ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID.
+     * @return string The purpose name.
+     */
+    public static function get_name($purposeid) {
+        global $PAGE;
+        if ($purposeid === false || $purposeid == context_instance::INHERIT) {
+            return get_string('inherit', 'tool_dataprivacy');
+        } else if ($purposeid == context_instance::NOTSET) {
+            return get_string('notset', 'tool_dataprivacy');
+        } else {
+            $purpose = new purpose($purposeid);
+            $output = $PAGE->get_renderer('tool_dataprivacy');
+            $exporter = new self($purpose, ['context' => \context_system::instance()]);
+            $data = $exporter->export($output);
+            return $data->name;
+        }
+    }
 }
diff --git a/admin/tool/dataprivacy/classes/form/defaults.php b/admin/tool/dataprivacy/classes/form/defaults.php
deleted file mode 100644 (file)
index 4e599c3..0000000
+++ /dev/null
@@ -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'));
-    }
-}
index 36dd93a..2609ef1 100644 (file)
@@ -47,6 +47,9 @@ class helper {
     /** 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.
      *
index e29174a..96c9e55 100644 (file)
@@ -80,9 +80,11 @@ class metadata_registry {
                 // Check if the interface is deprecated.
                 if (!$manager->is_empty_subsystem($component)) {
                     $classname = $manager->get_provider_classname_for_component($component);
-                    $componentclass = new $classname();
-                    if ($componentclass instanceof \core_privacy\local\deprecated) {
-                        $internaldata['deprecated'] = true;
+                    if (class_exists($classname)) {
+                        $componentclass = new $classname();
+                        if ($componentclass instanceof \core_privacy\local\deprecated) {
+                            $internaldata['deprecated'] = true;
+                        }
                     }
                 }
 
index 8d5fd35..be0e996 100644 (file)
@@ -425,7 +425,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[]
@@ -442,7 +442,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[]
index 7ea4bf8..402a22d 100644 (file)
@@ -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();
 
index 477e503..b6fa957 100644 (file)
@@ -62,6 +62,12 @@ class data_requests_table extends table_sql {
     /** @var \tool_dataprivacy\data_request[] Array of data request persistents. */
     protected $datarequests = [];
 
+    /** @var int The number of data request to be displayed per page. */
+    protected $perpage;
+
+    /** @var int[] The available options for the number of data request to be displayed per page. */
+    protected $perpageoptions = [25, 50, 100, 250];
+
     /**
      * data_requests_table constructor.
      *
@@ -79,7 +85,13 @@ class data_requests_table extends table_sql {
         $this->types = $types;
         $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 +103,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)]);
+        }
     }
 
     /**
@@ -290,4 +321,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;
+    }
 }
diff --git a/admin/tool/dataprivacy/classes/output/defaults_page.php b/admin/tool/dataprivacy/classes/output/defaults_page.php
new file mode 100644 (file)
index 0000000..166228f
--- /dev/null
@@ -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;
+    }
+}
index 09bcb1c..341bc8a 100644 (file)
@@ -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);
+    }
 }
diff --git a/admin/tool/dataprivacy/classes/output/summary_page.php b/admin/tool/dataprivacy/classes/output/summary_page.php
new file mode 100644 (file)
index 0000000..4b93b82
--- /dev/null
@@ -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;
+    }
+}
index 0a8cf73..d7ec6f7 100644 (file)
@@ -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) {
index 4c2d8c4..4ddd411 100644 (file)
@@ -76,6 +76,8 @@ class provider implements
 
         $collection->add_user_preference(tool_helper::PREF_REQUEST_FILTERS,
             'privacy:metadata:preference:tool_dataprivacy_request-filters');
+        $collection->add_user_preference(tool_helper::PREF_REQUEST_PERPAGE,
+            'privacy:metadata:preference:tool_dataprivacy_request-perpage');
 
         return $collection;
     }
@@ -200,5 +202,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'));
+        }
     }
 }
index 55deeec..8a4f96b 100644 (file)
@@ -52,18 +52,10 @@ class delete_expired_contexts extends scheduled_task {
 
     /**
      * Run the task to delete context instances based on their retention periods.
-     *
      */
     public function execute() {
-        $manager = new \tool_dataprivacy\expired_course_related_contexts();
-        $deleted = $manager->delete();
-        if ($deleted > 0) {
-            mtrace($deleted . ' course-related contexts have been deleted');
-        }
-        $manager = new \tool_dataprivacy\expired_user_contexts();
-        $deleted = $manager->delete();
-        if ($deleted > 0) {
-            mtrace($deleted . ' user contexts have been deleted');
-        }
+        $manager = new \tool_dataprivacy\expired_contexts_manager();
+        list($courses, $users) = $manager->process_approved_deletions();
+        mtrace("Processed deletions for {$courses} course contexts, and {$users} user contexts as expired");
     }
 }
index 1123067..f00b51a 100644 (file)
@@ -54,15 +54,8 @@ class expired_retention_period extends scheduled_task {
      * Run the task to flag context instances as expired.
      */
     public function execute() {
-        $manager = new \tool_dataprivacy\expired_course_related_contexts();
-        $flagged = $manager->flag_expired();
-        if ($flagged > 0) {
-            mtrace($flagged . ' course-related contexts have been flagged as expired');
-        }
-        $manager = new \tool_dataprivacy\expired_user_contexts();
-        $flagged = $manager->flag_expired();
-        if ($flagged > 0) {
-            mtrace($flagged . ' user contexts have been flagged as expired');
-        }
+        $manager = new \tool_dataprivacy\expired_contexts_manager();
+        list($courses, $users) = $manager->flag_expired_contexts();
+        mtrace("Flagged {$courses} course contexts, and {$users} user contexts as expired");
     }
 }
index 83a5c90..c0ea7ac 100644 (file)
@@ -24,7 +24,6 @@
 
 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);
@@ -67,6 +66,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 +85,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);
index c92c676..df088ca 100644 (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);
index 50c0ab9..e11c47c 100644 (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);
index 6a8140d..8b2b16f 100644 (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');
@@ -66,6 +68,13 @@ if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
     }
 
     $table = new \tool_dataprivacy\output\data_requests_table(0, $statuses, $types, 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);
index 9c71e8c..dac6ba4 100644 (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,
+    ],
 ];