Merge branch 'MDL-61970-master' of git://github.com/FMCorz/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 19 Apr 2018 03:12:35 +0000 (11:12 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Thu, 19 Apr 2018 03:12:35 +0000 (11:12 +0800)
711 files changed:
.eslintignore
.stylelintignore
admin/cli/install.php
admin/cli/upgrade.php
admin/tool/cohortroles/classes/output/cohort_role_assignments_table.php
admin/tool/cohortroles/templates/cohort-in-list.mustache
admin/tool/dataprivacy/amd/build/add_category.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/add_purpose.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/categoriesactions.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/data_deletion.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/data_registry.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/data_request_modal.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/effective_retention_period.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/events.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/expand_contract.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/form-user-selector.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/myrequestactions.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/purposesactions.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/requestactions.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/add_category.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/add_purpose.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/categoriesactions.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/data_deletion.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/data_registry.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/data_request_modal.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/effective_retention_period.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/events.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/expand_contract.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/form-user-selector.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/myrequestactions.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/purposesactions.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/requestactions.js [new file with mode: 0644]
admin/tool/dataprivacy/categories.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/api.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/category.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/context_instance.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/contextlevel.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/contextlist.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/contextlist_context.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/data_registry.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/data_request.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/expired_context.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/expired_contexts_manager.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/expired_course_related_contexts.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/expired_user_contexts.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/external.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/external/category_exporter.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/external/context_instance_exporter.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/external/data_request_exporter.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/external/name_description_exporter.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/external/purpose_exporter.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/form/category.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/form/context_instance.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/form/contextlevel.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/form/defaults.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/form/purpose.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/local/helper.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/metadata_registry.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/output/categories.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/output/crud_element.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/output/data_deletion_page.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/output/data_registry_compliance_page.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/output/data_registry_page.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/output/data_requests_page.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/output/expired_contexts_table.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/output/my_data_requests_page.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/output/purposes.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/output/renderer.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/page_helper.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/purpose.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/request_contextlist.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/task/delete_expired_contexts.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/task/expired_retention_period.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/task/initiate_data_request_task.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/task/process_data_request_task.php [new file with mode: 0644]
admin/tool/dataprivacy/createdatarequest.php [new file with mode: 0644]
admin/tool/dataprivacy/createdatarequest_form.php [new file with mode: 0644]
admin/tool/dataprivacy/datadeletion.php [new file with mode: 0644]
admin/tool/dataprivacy/dataregistry.php [new file with mode: 0644]
admin/tool/dataprivacy/datarequests.php [new file with mode: 0644]
admin/tool/dataprivacy/db/access.php [new file with mode: 0644]
admin/tool/dataprivacy/db/caches.php [new file with mode: 0644]
admin/tool/dataprivacy/db/install.xml [new file with mode: 0644]
admin/tool/dataprivacy/db/messages.php [new file with mode: 0644]
admin/tool/dataprivacy/db/services.php [new file with mode: 0644]
admin/tool/dataprivacy/db/tasks.php [new file with mode: 0644]
admin/tool/dataprivacy/defaults.php [new file with mode: 0644]
admin/tool/dataprivacy/editcategory.php [new file with mode: 0644]
admin/tool/dataprivacy/editpurpose.php [new file with mode: 0644]
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php [new file with mode: 0644]
admin/tool/dataprivacy/lib.php [new file with mode: 0644]
admin/tool/dataprivacy/mydatarequests.php [new file with mode: 0644]
admin/tool/dataprivacy/pluginregistry.php [new file with mode: 0644]
admin/tool/dataprivacy/purposes.php [new file with mode: 0644]
admin/tool/dataprivacy/settings.php [new file with mode: 0644]
admin/tool/dataprivacy/styles.css [new file with mode: 0644]
admin/tool/dataprivacy/templates/categories.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/component_status.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/contact_dpo.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/context_tree_branches.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/context_tree_node.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/data_deletion.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/data_registry.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/data_registry_compliance.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/data_request_email.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/data_request_modal.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/data_request_results_email.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/data_requests.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/form-user-selector-suggestion.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/my_data_requests.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/purposes.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/request_details.mustache [new file with mode: 0644]
admin/tool/dataprivacy/tests/api_test.php [new file with mode: 0644]
admin/tool/dataprivacy/tests/expired_contexts_test.php [new file with mode: 0644]
admin/tool/dataprivacy/tests/external_test.php [new file with mode: 0644]
admin/tool/dataprivacy/tests/metadata_registry_test.php [new file with mode: 0644]
admin/tool/dataprivacy/version.php [new file with mode: 0644]
admin/tool/lp/templates/manage_competencies_page.mustache
admin/tool/monitor/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/monitor/classes/rule.php
admin/tool/monitor/lang/en/tool_monitor.php
admin/tool/monitor/tests/privacy_test.php [new file with mode: 0644]
admin/tool/oauth2/lang/en/tool_oauth2.php
admin/tool/policy/accept.php [new file with mode: 0644]
admin/tool/policy/acceptances.php [new file with mode: 0644]
admin/tool/policy/amd/build/acceptances_filter.min.js [new file with mode: 0644]
admin/tool/policy/amd/build/acceptances_filter_datasource.min.js [new file with mode: 0644]
admin/tool/policy/amd/build/acceptmodal.min.js [new file with mode: 0644]
admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js [new file with mode: 0644]
admin/tool/policy/amd/build/managedocsactions.min.js [new file with mode: 0644]
admin/tool/policy/amd/build/policyactions.min.js [new file with mode: 0644]
admin/tool/policy/amd/src/acceptances_filter.js [new file with mode: 0644]
admin/tool/policy/amd/src/acceptances_filter_datasource.js [new file with mode: 0644]
admin/tool/policy/amd/src/acceptmodal.js [new file with mode: 0644]
admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js [new file with mode: 0644]
admin/tool/policy/amd/src/managedocsactions.js [new file with mode: 0644]
admin/tool/policy/amd/src/policyactions.js [new file with mode: 0644]
admin/tool/policy/classes/acceptances_table.php [new file with mode: 0644]
admin/tool/policy/classes/api.php [new file with mode: 0644]
admin/tool/policy/classes/event/acceptance_base.php [new file with mode: 0644]
admin/tool/policy/classes/event/acceptance_created.php [new file with mode: 0644]
admin/tool/policy/classes/event/acceptance_updated.php [new file with mode: 0644]
admin/tool/policy/classes/external.php [new file with mode: 0644]
admin/tool/policy/classes/form/accept_policy.php [new file with mode: 0644]
admin/tool/policy/classes/form/policydoc.php [new file with mode: 0644]
admin/tool/policy/classes/output/acceptances.php [new file with mode: 0644]
admin/tool/policy/classes/output/acceptances_filter.php [new file with mode: 0644]
admin/tool/policy/classes/output/guestconsent.php [new file with mode: 0644]
admin/tool/policy/classes/output/page_agreedocs.php [new file with mode: 0644]
admin/tool/policy/classes/output/page_managedocs_list.php [new file with mode: 0644]
admin/tool/policy/classes/output/page_nopermission.php [new file with mode: 0644]
admin/tool/policy/classes/output/page_viewalldoc.php [new file with mode: 0644]
admin/tool/policy/classes/output/page_viewdoc.php [new file with mode: 0644]
admin/tool/policy/classes/output/renderer.php [new file with mode: 0644]
admin/tool/policy/classes/output/user_agreement.php [new file with mode: 0644]
admin/tool/policy/classes/policy_exporter.php [new file with mode: 0644]
admin/tool/policy/classes/policy_version.php [new file with mode: 0644]
admin/tool/policy/classes/policy_version_exporter.php [new file with mode: 0644]
admin/tool/policy/classes/privacy/local/sitepolicy/handler.php [new file with mode: 0644]
admin/tool/policy/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/policy/db/access.php [new file with mode: 0644]
admin/tool/policy/db/events.php [new file with mode: 0644]
admin/tool/policy/db/install.xml [new file with mode: 0644]
admin/tool/policy/db/services.php [new file with mode: 0644]
admin/tool/policy/editpolicydoc.php [new file with mode: 0644]
admin/tool/policy/index.php [new file with mode: 0644]
admin/tool/policy/lang/en/tool_policy.php [new file with mode: 0644]
admin/tool/policy/lib.php [new file with mode: 0644]
admin/tool/policy/managedocs.php [new file with mode: 0644]
admin/tool/policy/pix/agreedno.png [new file with mode: 0644]
admin/tool/policy/pix/agreedno.svg [new file with mode: 0644]
admin/tool/policy/pix/agreedyes.png [new file with mode: 0644]
admin/tool/policy/pix/agreedyes.svg [new file with mode: 0644]
admin/tool/policy/pix/agreedyesonbehalf.png [new file with mode: 0644]
admin/tool/policy/pix/agreedyesonbehalf.svg [new file with mode: 0644]
admin/tool/policy/pix/level.png [new file with mode: 0644]
admin/tool/policy/pix/level.svg [new file with mode: 0644]
admin/tool/policy/readme_moodle.txt [new file with mode: 0644]
admin/tool/policy/settings.php [new file with mode: 0644]
admin/tool/policy/styles.css [new file with mode: 0644]
admin/tool/policy/templates/acceptances.mustache [new file with mode: 0644]
admin/tool/policy/templates/acceptances_filter.mustache [new file with mode: 0644]
admin/tool/policy/templates/guestconsent.mustache [new file with mode: 0644]
admin/tool/policy/templates/page_agreedocs.mustache [new file with mode: 0644]
admin/tool/policy/templates/page_managedocs_list.mustache [new file with mode: 0644]
admin/tool/policy/templates/page_nopermission.mustache [new file with mode: 0644]
admin/tool/policy/templates/page_viewalldoc.mustache [new file with mode: 0644]
admin/tool/policy/templates/page_viewdoc.mustache [new file with mode: 0644]
admin/tool/policy/templates/user_agreement.mustache [new file with mode: 0644]
admin/tool/policy/tests/api_test.php [new file with mode: 0644]
admin/tool/policy/tests/behat/acceptances.feature [new file with mode: 0644]
admin/tool/policy/tests/behat/behat_tool_policy.php [new file with mode: 0644]
admin/tool/policy/tests/behat/consent.feature [new file with mode: 0644]
admin/tool/policy/tests/behat/managepolicies.feature [new file with mode: 0644]
admin/tool/policy/tests/externallib_test.php [new file with mode: 0644]
admin/tool/policy/tests/privacy_provider_test.php [new file with mode: 0644]
admin/tool/policy/thirdpartylibs.xml [new file with mode: 0644]
admin/tool/policy/user.php [new file with mode: 0644]
admin/tool/policy/version.php [new file with mode: 0644]
admin/tool/policy/view.php [new file with mode: 0644]
admin/tool/policy/viewall.php [new file with mode: 0644]
admin/tool/profiling/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/profiling/lang/en/tool_profiling.php
admin/tool/recyclebin/classes/category_bin.php
admin/tool/recyclebin/classes/course_bin.php
admin/tool/uploadcourse/classes/course.php
admin/tool/uploadcourse/classes/helper.php
admin/tool/uploadcourse/classes/processor.php
auth/tests/behat/displayloginfailures.feature
backup/controller/restore_controller.class.php
backup/controller/tests/controller_test.php
backup/converter/convertlib.php
backup/converter/imscc11/backuplib.php
backup/converter/moodle1/lib.php
backup/converter/moodle1/tests/moodle1_converter_test.php
backup/import.php
backup/moodle2/tests/moodle2_test.php
backup/restorefile.php
backup/util/factories/backup_factory.class.php
backup/util/helper/backup_file_manager.class.php
backup/util/helper/backup_general_helper.class.php
backup/util/helper/backup_helper.class.php
backup/util/helper/convert_helper.class.php
backup/util/plan/backup_plan.class.php
backup/util/plan/restore_plan.class.php
backup/util/ui/base_moodleform.class.php
backup/util/ui/renderer.php
backup/util/ui/restore_ui_stage.class.php
blocks/community/communitycourse.php
blocks/myoverview/templates/course-event-list-item.mustache
blocks/myoverview/templates/course-summary.mustache
blocks/myoverview/templates/courses-view-course-item.mustache
blocks/myoverview/templates/courses-view.mustache
blocks/myoverview/templates/event-list-item.mustache
blocks/myoverview/templates/timeline-view.mustache
blocks/tests/behat/behat_blocks.php
calendar/templates/month_detailed.mustache
comment/lib.php
config-dist.php
course/classes/management_renderer.php
course/format/singleactivity/classes/privacy/provider.php [new file with mode: 0644]
course/format/singleactivity/lang/en/format_singleactivity.php
course/format/social/classes/privacy/provider.php [new file with mode: 0644]
course/format/social/lang/en/format_social.php
course/format/topics/classes/privacy/provider.php [new file with mode: 0644]
course/format/topics/lang/en/format_topics.php
course/format/weeks/classes/privacy/provider.php [new file with mode: 0644]
course/format/weeks/lang/en/format_weeks.php
course/tests/behat/category_change_visibility.feature
course/tests/behat/category_management.feature
course/tests/behat/course_category_management_listing.feature
course/tests/behat/course_change_visibility.feature
course/tests/behat/course_search.feature
course/tests/behat/navigate_course_list.feature
course/tests/restore_test.php
enrol/paypal/ipn.php
grade/export/ods/classes/privacy/provider.php [new file with mode: 0644]
grade/export/ods/lang/en/gradeexport_ods.php
grade/export/txt/classes/privacy/provider.php [new file with mode: 0644]
grade/export/txt/lang/en/gradeexport_txt.php
grade/export/xls/classes/privacy/provider.php [new file with mode: 0644]
grade/export/xls/lang/en/gradeexport_xls.php
grade/export/xml/classes/privacy/provider.php [new file with mode: 0644]
grade/export/xml/lang/en/gradeexport_xml.php
grade/import/csv/classes/privacy/provider.php [new file with mode: 0644]
grade/import/csv/lang/en/gradeimport_csv.php
grade/import/direct/classes/privacy/provider.php [new file with mode: 0644]
grade/import/direct/lang/en/gradeimport_direct.php
grade/import/xml/classes/privacy/provider.php [new file with mode: 0644]
grade/import/xml/lang/en/gradeimport_xml.php
grade/report/grader/classes/privacy/provider.php [new file with mode: 0644]
grade/report/grader/lang/en/gradereport_grader.php
grade/report/grader/tests/privacy_test.php [new file with mode: 0644]
grade/report/history/classes/privacy/provider.php [new file with mode: 0644]
grade/report/history/lang/en/gradereport_history.php
grade/report/outcomes/classes/privacy/provider.php [new file with mode: 0644]
grade/report/outcomes/lang/en/gradereport_outcomes.php
grade/report/overview/classes/privacy/provider.php [new file with mode: 0644]
grade/report/overview/lang/en/gradereport_overview.php
grade/report/singleview/classes/privacy/provider.php [new file with mode: 0644]
grade/report/singleview/lang/en/gradereport_singleview.php
grade/report/user/classes/privacy/provider.php [new file with mode: 0644]
grade/report/user/lang/en/gradereport_user.php
grade/report/user/tests/privacy_test.php [new file with mode: 0644]
install.php
install/lang/id/moodle.php
lang/en/admin.php
lang/en/editor.php
lang/en/install.php
lang/en/moodle.php
lang/en/portfolio.php
lang/en/repository.php
lang/en/rss.php [new file with mode: 0644]
lib/amd/build/adapter.min.js [new file with mode: 0644]
lib/amd/src/adapter.js [new file with mode: 0644]
lib/behat/classes/partial_named_selector.php
lib/blocklib.php
lib/classes/hub/publication.php
lib/classes/oauth2/user_field_mapping.php
lib/classes/plugin_manager.php
lib/classes/task/file_temp_cleanup_task.php
lib/editor/atto/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/atto/db/upgrade.php
lib/editor/atto/lang/en/editor_atto.php
lib/editor/atto/plugins/recordrtc/classes/privacy/provider.php [moved from theme/boost/classes/output/core/admin_renderer.php with 60% similarity]
lib/editor/atto/plugins/recordrtc/lang/en/atto_recordrtc.php [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/lib.php [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/pix/i/audiortc.png [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/pix/i/videortc.png [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/pix/icon.png [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/settings.php [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/styles.css [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/version.php [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-button/moodle-atto_recordrtc-button-debug.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-button/moodle-atto_recordrtc-button-min.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-button/moodle-atto_recordrtc-button.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-debug.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-min.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/button/build.json [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/button/js/button.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/button/meta/button.json [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/recording/build.json [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/abstractmodule.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/audiomodule.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/commonmodule.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/compatcheckmodule.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/videomodule.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/recording/meta/recording.json [new file with mode: 0644]
lib/editor/atto/settings.php
lib/editor/atto/tests/privacy_provider.php [new file with mode: 0644]
lib/editor/atto/version.php
lib/editor/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tests/privacy_provider_test.php [new file with mode: 0644]
lib/editor/textarea/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/textarea/lang/en/editor_textarea.php
lib/editor/tinymce/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/lang/en/editor_tinymce.php
lib/formslib.php
lib/outputrenderers.php
lib/setup.php
lib/setuplib.php
lib/tablelib.php
lib/templates/form_autocomplete_selection.mustache
lib/testing/classes/util.php
lib/tests/behat/alpha_chooser.feature
lib/tests/behat/behat_forms.php
lib/tests/behat/readonlyform.feature [new file with mode: 0644]
lib/tests/blocklib_test.php
lib/tests/cronlib_test.php
lib/tests/fixtures/readonlyform.php [new file with mode: 0644]
lib/tests/scheduled_task_test.php
lib/thirdpartylibs.xml
lib/upgradelib.php
media/player/html5audio/classes/privacy/provider.php [new file with mode: 0644]
media/player/html5audio/lang/en/media_html5audio.php
media/player/html5video/classes/privacy/provider.php [new file with mode: 0644]
media/player/html5video/lang/en/media_html5video.php
media/player/swf/classes/privacy/provider.php [new file with mode: 0644]
media/player/swf/lang/en/media_swf.php
media/player/videojs/classes/privacy/provider.php [new file with mode: 0644]
media/player/videojs/lang/en/media_videojs.php
media/player/vimeo/classes/privacy/provider.php [new file with mode: 0644]
media/player/vimeo/lang/en/media_vimeo.php
media/player/youtube/classes/privacy/provider.php [new file with mode: 0644]
media/player/youtube/lang/en/media_youtube.php
mod/assignment/type/offline/classes/privacy/provider.php [new file with mode: 0644]
mod/assignment/type/offline/lang/en/assignment_offline.php
mod/assignment/type/online/classes/privacy/provider.php [new file with mode: 0644]
mod/assignment/type/online/lang/en/assignment_online.php
mod/assignment/type/upload/classes/privacy/provider.php [new file with mode: 0644]
mod/assignment/type/upload/lang/en/assignment_upload.php
mod/assignment/type/uploadsingle/classes/privacy/provider.php [new file with mode: 0644]
mod/assignment/type/uploadsingle/lang/en/assignment_uploadsingle.php
mod/book/tool/exportimscp/classes/privacy/provider.php [new file with mode: 0644]
mod/book/tool/exportimscp/lang/en/booktool_exportimscp.php
mod/book/tool/importhtml/classes/privacy/provider.php [new file with mode: 0644]
mod/book/tool/importhtml/lang/en/booktool_importhtml.php
mod/book/tool/print/classes/privacy/provider.php [new file with mode: 0644]
mod/book/tool/print/lang/en/booktool_print.php
mod/choice/classes/privacy/provider.php
mod/feedback/classes/privacy/provider.php [new file with mode: 0644]
mod/feedback/lang/en/feedback.php
mod/feedback/tests/privacy_test.php [new file with mode: 0644]
mod/lti/locallib.php
mod/quiz/attemptlib.php
mod/quiz/backup/moodle2/backup_quiz_stepslib.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/quiz/classes/local/structure/slot_random.php
mod/quiz/classes/structure.php
mod/quiz/db/install.xml [changed mode: 0644->0755]
mod/quiz/db/upgrade.php
mod/quiz/editrandom.php
mod/quiz/lib.php
mod/quiz/locallib.php
mod/quiz/report/responses/last_responses_table.php
mod/quiz/report/statistics/report.php
mod/quiz/tests/fixtures/random_by_tag_quiz.mbz
mod/quiz/tests/local_structure_slot_random_test.php [new file with mode: 0755]
mod/quiz/tests/locallib_test.php
mod/quiz/tests/tags_test.php
mod/quiz/version.php
mod/scorm/db/install.xml
mod/scorm/db/upgrade.php
mod/scorm/version.php
mod/upgrade.txt
phpunit.xml.dist
portfolio/boxnet/classes/privacy/provider.php [new file with mode: 0644]
portfolio/boxnet/lang/en/portfolio_boxnet.php
portfolio/boxnet/tests/privacy_provider_test.php [new file with mode: 0644]
portfolio/classes/privacy/legacy_polyfill.php [new file with mode: 0644]
portfolio/classes/privacy/portfolio_provider.php [new file with mode: 0644]
portfolio/classes/privacy/provider.php [new file with mode: 0644]
portfolio/download/classes/privacy/provider.php [new file with mode: 0644]
portfolio/download/lang/en/portfolio_download.php
portfolio/flickr/classes/privacy/provider.php [new file with mode: 0644]
portfolio/flickr/lang/en/portfolio_flickr.php
portfolio/flickr/tests/privacy_provider_test.php [new file with mode: 0644]
portfolio/googledocs/classes/privacy/provider.php [new file with mode: 0644]
portfolio/googledocs/lang/en/portfolio_googledocs.php
portfolio/googledocs/tests/privacy_provider_test.php [new file with mode: 0644]
portfolio/mahara/classes/privacy/provider.php [new file with mode: 0644]
portfolio/mahara/lang/en/portfolio_mahara.php
portfolio/mahara/tests/privacy_provider_test.php [new file with mode: 0644]
portfolio/picasa/classes/privacy/provider.php [new file with mode: 0644]
portfolio/picasa/lang/en/portfolio_picasa.php
portfolio/picasa/tests/privacy_provider_test.php [new file with mode: 0644]
portfolio/tests/privacy_legacy_polyfill_test.php [new file with mode: 0644]
portfolio/tests/privacy_provider_test.php [new file with mode: 0644]
privacy/classes/tests/request/content_writer.php
privacy/tests/tests_content_writer_test.php
question/classes/statistics/questions/calculator.php
repository/areafiles/classes/privacy/provider.php [new file with mode: 0644]
repository/areafiles/lang/en/repository_areafiles.php
repository/boxnet/classes/privacy/provider.php [new file with mode: 0644]
repository/boxnet/lang/en/repository_boxnet.php
repository/classes/privacy/provider.php [new file with mode: 0644]
repository/coursefiles/classes/privacy/provider.php [new file with mode: 0644]
repository/coursefiles/lang/en/repository_coursefiles.php
repository/dropbox/classes/privacy/provider.php [new file with mode: 0644]
repository/dropbox/lang/en/repository_dropbox.php
repository/equella/classes/privacy/provider.php [new file with mode: 0644]
repository/equella/lang/en/repository_equella.php
repository/filesystem/classes/privacy/provider.php [new file with mode: 0644]
repository/filesystem/lang/en/repository_filesystem.php
repository/flickr/classes/privacy/provider.php [new file with mode: 0644]
repository/flickr/lang/en/repository_flickr.php
repository/flickr_public/classes/privacy/provider.php [new file with mode: 0644]
repository/flickr_public/lang/en/repository_flickr_public.php
repository/googledocs/classes/privacy/provider.php [new file with mode: 0644]
repository/googledocs/lang/en/repository_googledocs.php
repository/local/classes/privacy/provider.php [new file with mode: 0644]
repository/local/lang/en/repository_local.php
repository/merlot/classes/privacy/provider.php [new file with mode: 0644]
repository/merlot/lang/en/repository_merlot.php
repository/onedrive/classes/privacy/provider.php [new file with mode: 0644]
repository/onedrive/lang/en/repository_onedrive.php
repository/onedrive/tests/privacy_test.php [new file with mode: 0644]
repository/picasa/classes/privacy/provider.php [new file with mode: 0644]
repository/picasa/lang/en/repository_picasa.php
repository/recent/classes/privacy/provider.php [new file with mode: 0644]
repository/recent/lang/en/repository_recent.php
repository/s3/classes/privacy/provider.php [new file with mode: 0644]
repository/s3/lang/en/repository_s3.php
repository/skydrive/classes/privacy/provider.php [new file with mode: 0644]
repository/skydrive/lang/en/repository_skydrive.php
repository/tests/privacy_test.php [new file with mode: 0644]
repository/upload/classes/privacy/provider.php [new file with mode: 0644]
repository/upload/lang/en/repository_upload.php
repository/url/classes/privacy/provider.php [new file with mode: 0644]
repository/url/lang/en/repository_url.php
repository/user/classes/privacy/provider.php [new file with mode: 0644]
repository/user/lang/en/repository_user.php
repository/webdav/classes/privacy/provider.php [new file with mode: 0644]
repository/webdav/lang/en/repository_webdav.php
repository/wikimedia/classes/privacy/provider.php [new file with mode: 0644]
repository/wikimedia/lang/en/repository_wikimedia.php
repository/youtube/classes/privacy/provider.php [new file with mode: 0644]
repository/youtube/lang/en/repository_youtube.php
rss/classes/privacy/provider.php [new file with mode: 0644]
rss/renderer.php
rss/tests/privacy_test.php [new file with mode: 0644]
search/classes/output/form/search.php
search/index.php
search/tests/behat/search_query.feature
tag/classes/tag.php
tag/tests/behat/edit_tag.feature
theme/boost/amd/build/alert.min.js
theme/boost/amd/build/button.min.js
theme/boost/amd/build/carousel.min.js
theme/boost/amd/build/collapse.min.js
theme/boost/amd/build/dropdown.min.js
theme/boost/amd/build/form-display-errors.min.js
theme/boost/amd/build/modal.min.js
theme/boost/amd/build/popover.min.js
theme/boost/amd/build/scrollspy.min.js
theme/boost/amd/build/tab.min.js
theme/boost/amd/build/tooltip.min.js
theme/boost/amd/build/util.min.js
theme/boost/amd/src/alert.js
theme/boost/amd/src/button.js
theme/boost/amd/src/carousel.js
theme/boost/amd/src/collapse.js
theme/boost/amd/src/dropdown.js
theme/boost/amd/src/form-display-errors.js
theme/boost/amd/src/modal.js
theme/boost/amd/src/popover.js
theme/boost/amd/src/scrollspy.js
theme/boost/amd/src/tab.js
theme/boost/amd/src/tooltip.js
theme/boost/amd/src/util.js
theme/boost/classes/output/core_course/management/renderer.php [new file with mode: 0644]
theme/boost/classes/output/core_renderer.php
theme/boost/cli/import-bootswatch.php
theme/boost/lang/en/theme_boost.php
theme/boost/lib.php
theme/boost/readme_moodle.txt
theme/boost/scss/bootstrap.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/LICENSE
theme/boost/scss/bootstrap/_alert.scss
theme/boost/scss/bootstrap/_animation.scss [deleted file]
theme/boost/scss/bootstrap/_badge.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/_breadcrumb.scss
theme/boost/scss/bootstrap/_button-group.scss
theme/boost/scss/bootstrap/_buttons.scss
theme/boost/scss/bootstrap/_card.scss
theme/boost/scss/bootstrap/_carousel.scss
theme/boost/scss/bootstrap/_close.scss
theme/boost/scss/bootstrap/_code.scss
theme/boost/scss/bootstrap/_custom-forms.scss
theme/boost/scss/bootstrap/_custom.scss [deleted file]
theme/boost/scss/bootstrap/_dropdown.scss
theme/boost/scss/bootstrap/_forms.scss
theme/boost/scss/bootstrap/_functions.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/_grid.scss
theme/boost/scss/bootstrap/_images.scss
theme/boost/scss/bootstrap/_input-group.scss
theme/boost/scss/bootstrap/_jumbotron.scss
theme/boost/scss/bootstrap/_list-group.scss
theme/boost/scss/bootstrap/_media.scss
theme/boost/scss/bootstrap/_mixins.scss
theme/boost/scss/bootstrap/_modal.scss
theme/boost/scss/bootstrap/_nav.scss
theme/boost/scss/bootstrap/_navbar.scss
theme/boost/scss/bootstrap/_normalize.scss [deleted file]
theme/boost/scss/bootstrap/_pagination.scss
theme/boost/scss/bootstrap/_popover.scss
theme/boost/scss/bootstrap/_print.scss
theme/boost/scss/bootstrap/_progress.scss
theme/boost/scss/bootstrap/_reboot.scss
theme/boost/scss/bootstrap/_root.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/_tables.scss
theme/boost/scss/bootstrap/_tags.scss [deleted file]
theme/boost/scss/bootstrap/_tooltip.scss
theme/boost/scss/bootstrap/_transitions.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/_type.scss
theme/boost/scss/bootstrap/_utilities.scss
theme/boost/scss/bootstrap/_variables.scss
theme/boost/scss/bootstrap/bootstrap-flex.scss [deleted file]
theme/boost/scss/bootstrap/bootstrap-grid.scss
theme/boost/scss/bootstrap/bootstrap-reboot.scss
theme/boost/scss/bootstrap/bootstrap.scss
theme/boost/scss/bootstrap/mixins/_alert.scss
theme/boost/scss/bootstrap/mixins/_background-variant.scss
theme/boost/scss/bootstrap/mixins/_badge.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/mixins/_border-radius.scss
theme/boost/scss/bootstrap/mixins/_box-shadow.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/mixins/_breakpoints.scss
theme/boost/scss/bootstrap/mixins/_buttons.scss
theme/boost/scss/bootstrap/mixins/_cards.scss [deleted file]
theme/boost/scss/bootstrap/mixins/_caret.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/mixins/_clearfix.scss
theme/boost/scss/bootstrap/mixins/_float.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/mixins/_forms.scss
theme/boost/scss/bootstrap/mixins/_gradients.scss
theme/boost/scss/bootstrap/mixins/_grid-framework.scss
theme/boost/scss/bootstrap/mixins/_grid.scss
theme/boost/scss/bootstrap/mixins/_hover.scss
theme/boost/scss/bootstrap/mixins/_image.scss
theme/boost/scss/bootstrap/mixins/_list-group.scss
theme/boost/scss/bootstrap/mixins/_nav-divider.scss
theme/boost/scss/bootstrap/mixins/_navbar-align.scss
theme/boost/scss/bootstrap/mixins/_pagination.scss
theme/boost/scss/bootstrap/mixins/_progress.scss [deleted file]
theme/boost/scss/bootstrap/mixins/_pulls.scss [deleted file]
theme/boost/scss/bootstrap/mixins/_reset-filter.scss [deleted file]
theme/boost/scss/bootstrap/mixins/_reset-text.scss
theme/boost/scss/bootstrap/mixins/_resize.scss
theme/boost/scss/bootstrap/mixins/_screen-reader.scss
theme/boost/scss/bootstrap/mixins/_tab-focus.scss [deleted file]
theme/boost/scss/bootstrap/mixins/_tag.scss [deleted file]
theme/boost/scss/bootstrap/mixins/_text-emphasis.scss
theme/boost/scss/bootstrap/mixins/_text-hide.scss
theme/boost/scss/bootstrap/mixins/_text-truncate.scss
theme/boost/scss/bootstrap/mixins/_transition.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/mixins/_visibility.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_align.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_background.scss
theme/boost/scss/bootstrap/utilities/_borders.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_display.scss
theme/boost/scss/bootstrap/utilities/_embed.scss [moved from theme/boost/scss/bootstrap/_responsive-embed.scss with 60% similarity]
theme/boost/scss/bootstrap/utilities/_flex.scss
theme/boost/scss/bootstrap/utilities/_float.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_position.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_pulls.scss [deleted file]
theme/boost/scss/bootstrap/utilities/_sizing.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_spacing.scss
theme/boost/scss/bootstrap/utilities/_text.scss
theme/boost/scss/bootstrap/utilities/_visibility.scss
theme/boost/scss/fontawesome.scss [new file with mode: 0644]
theme/boost/scss/fontawesome/moodle-path.scss [deleted file]
theme/boost/scss/fontawesome/readme_moodle.txt
theme/boost/scss/moodle.scss
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/backup-restore.scss
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/bootswatch.scss
theme/boost/scss/moodle/bs2-compat.scss
theme/boost/scss/moodle/bs4alphacompat.scss [new file with mode: 0644]
theme/boost/scss/moodle/calendar.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/debug.scss
theme/boost/scss/moodle/drawer.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/icons.scss
theme/boost/scss/moodle/message.scss
theme/boost/scss/moodle/modal.scss
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/responsive-tabs.scss [deleted file]
theme/boost/scss/moodle/search.scss
theme/boost/scss/moodle/undo.scss
theme/boost/scss/moodle/user.scss
theme/boost/scss/preset/default.scss
theme/boost/scss/preset/plain.scss
theme/boost/templates/admin_setting_tabs.mustache
theme/boost/templates/block_search_forums/search_form.mustache
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
theme/boost/templates/core/action_menu.mustache
theme/boost/templates/core/action_menu_trigger.mustache
theme/boost/templates/core/block.mustache
theme/boost/templates/core/custom_menu_item.mustache
theme/boost/templates/core/dataformat_selector.mustache
theme/boost/templates/core/filemanager_fileselect.mustache
theme/boost/templates/core/filemanager_modal_generallayout.mustache
theme/boost/templates/core/filemanager_page_generallayout.mustache
theme/boost/templates/core/form_autocomplete_selection.mustache
theme/boost/templates/core/help_icon.mustache
theme/boost/templates/core/initials_bar.mustache [new file with mode: 0644]
theme/boost/templates/core/loginform.mustache
theme/boost/templates/core/modal.mustache
theme/boost/templates/core/preferences_groups.mustache
theme/boost/templates/core/settings_link_page.mustache
theme/boost/templates/core/settings_link_page_single.mustache
theme/boost/templates/core/signup_form_layout.mustache
theme/boost/templates/core/tabtree.mustache
theme/boost/templates/core_admin/setting.mustache
theme/boost/templates/core_form/element-advcheckbox-inline.mustache
theme/boost/templates/core_form/element-advcheckbox.mustache
theme/boost/templates/core_form/element-autocomplete-inline.mustache
theme/boost/templates/core_form/element-autocomplete.mustache
theme/boost/templates/core_form/element-button.mustache
theme/boost/templates/core_form/element-checkbox-inline.mustache
theme/boost/templates/core_form/element-checkbox.mustache
theme/boost/templates/core_form/element-date_time_selector-inline.mustache
theme/boost/templates/core_form/element-date_time_selector.mustache
theme/boost/templates/core_form/element-password.mustache
theme/boost/templates/core_form/element-passwordunmask.mustache
theme/boost/templates/core_form/element-radio-inline.mustache
theme/boost/templates/core_form/element-radio.mustache
theme/boost/templates/core_form/element-select-inline.mustache
theme/boost/templates/core_form/element-select.mustache
theme/boost/templates/core_form/element-selectgroups-inline.mustache
theme/boost/templates/core_form/element-selectgroups.mustache
theme/boost/templates/core_form/element-selectwithlink.mustache
theme/boost/templates/core_form/element-tags-inline.mustache
theme/boost/templates/core_form/element-tags.mustache
theme/boost/templates/core_form/element-template-inline.mustache
theme/boost/templates/core_form/element-template.mustache
theme/boost/templates/core_form/element-text-inline.mustache
theme/boost/templates/core_form/element-text.mustache
theme/boost/templates/core_form/element-textarea.mustache
theme/boost/templates/core_form/element-url.mustache
theme/boost/templates/core_grades/edit_tree.mustache
theme/boost/templates/custom_menu_footer.mustache
theme/boost/templates/flat_navigation.mustache
theme/boost/templates/footer.mustache [new file with mode: 0644]
theme/boost/templates/head.mustache [new file with mode: 0644]
theme/boost/templates/header.mustache
theme/boost/templates/login.mustache
theme/boost/templates/maintenance.mustache
theme/boost/templates/mod_forum/quick_search_form.mustache
theme/boost/templates/nav-drawer.mustache
theme/boost/templates/navbar-secure.mustache [moved from theme/boost/templates/header-secure.mustache with 66% similarity]
theme/boost/templates/navbar.mustache [new file with mode: 0644]
theme/boost/templates/secure.mustache
theme/boost/templates/tool_usertours/tourstep.mustache
theme/boost/tests/behat/behat_theme_boost_behat_action_menu.php
theme/boost/tests/behat/behat_theme_boost_behat_navigation.php
theme/boost/thirdpartylibs.xml
theme/boost/upgrade.txt
user/tests/behat/set_default_homepage.feature
version.php
webservice/rest/classes/privacy/provider.php [new file with mode: 0644]
webservice/rest/lang/en/webservice_rest.php
webservice/soap/classes/privacy/provider.php [new file with mode: 0644]
webservice/soap/lang/en/webservice_soap.php
webservice/xmlrpc/classes/privacy/provider.php [new file with mode: 0644]
webservice/xmlrpc/lang/en/webservice_xmlrpc.php

index 4cbb8e3..296343b 100644 (file)
@@ -3,6 +3,7 @@
 */**/build/
 node_modules/
 vendor/
+admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
 admin/tool/usertours/amd/src/tour.js
 auth/cas/CAS/
 enrol/lti/ims-blti/
@@ -56,6 +57,7 @@ lib/maxmind/MaxMind/
 lib/ltiprovider/
 lib/amd/src/truncate.js
 lib/fonts/
+lib/amd/src/adapter.js
 lib/validateurlsyntax.php
 lib/amd/src/popper.js
 media/player/videojs/amd/src/video-lazy.js
index 140b4aa..bdd33f8 100644 (file)
@@ -4,6 +4,7 @@ theme/clean/style/custom.css
 theme/more/style/custom.css
 node_modules/
 vendor/
+admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
 admin/tool/usertours/amd/src/tour.js
 auth/cas/CAS/
 enrol/lti/ims-blti/
@@ -57,6 +58,7 @@ lib/maxmind/MaxMind/
 lib/ltiprovider/
 lib/amd/src/truncate.js
 lib/fonts/
+lib/amd/src/adapter.js
 lib/validateurlsyntax.php
 lib/amd/src/popper.js
 media/player/videojs/amd/src/video-lazy.js
index 1ff43fe..1e5837f 100644 (file)
@@ -442,6 +442,7 @@ if ($interactive) {
     }
 }
 $CFG->tempdir       = $CFG->dataroot.'/temp';
+$CFG->backuptempdir = $CFG->tempdir.'/backup';
 $CFG->cachedir      = $CFG->dataroot.'/cache';
 $CFG->localcachedir = $CFG->dataroot.'/localcache';
 
index 6c10b9a..5915e24 100644 (file)
@@ -131,10 +131,11 @@ if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
     cli_error(get_string('pluginschecktodo', 'admin'));
 }
 
+$a = new stdClass();
+$a->oldversion = $oldversion;
+$a->newversion = $newversion;
+
 if ($interactive) {
-    $a = new stdClass();
-    $a->oldversion = $oldversion;
-    $a->newversion = $newversion;
     echo cli_heading(get_string('databasechecking', '', $a)) . PHP_EOL;
 }
 
@@ -193,5 +194,5 @@ admin_apply_default_settings(NULL, false);
 // to immediately start browsing the site.
 upgrade_themes();
 
-echo get_string('cliupgradefinished', 'admin')."\n";
+echo get_string('cliupgradefinished', 'admin', $a)."\n";
 exit(0); // 0 means success
index 29405c2..496a4ac 100644 (file)
@@ -94,7 +94,8 @@ class cohort_role_assignments_table extends table_sql {
             'idnumber' => $data->cohortidnumber,
             'description' => $data->cohortdescription,
             'visible' => $data->cohortvisible,
-            'name' => $data->cohortname
+            'name' => $data->cohortname,
+            'theme' => $data->cohorttheme
         );
         $context = context_helper::instance_by_id($data->cohortcontextid);
 
@@ -169,7 +170,7 @@ class cohort_role_assignments_table extends table_sql {
     protected function get_sql_and_params($count = false) {
         $fields = 'uca.id, uca.cohortid, uca.userid, uca.roleid, ';
         $fields .= 'c.name as cohortname, c.idnumber as cohortidnumber, c.contextid as cohortcontextid, ';
-        $fields .= 'c.visible as cohortvisible, c.description as cohortdescription, ';
+        $fields .= 'c.visible as cohortvisible, c.description as cohortdescription, c.theme as cohorttheme, ';
 
         // Add extra user fields that we need for the graded user.
         $extrafields = get_extra_user_fields($this->context);
index 971065a..ff2199c 100644 (file)
     * idnumber cohort idnumber field
     * description cohort description field
     * visible cohort visible field
+    * theme cohort theme field
 
     Example context (json):
     { "id": "1",
       "name": "Cohort 1",
       "visible": true,
       "idnumber": "014",
-      "description": "Some users"
+      "description": "Some users",
+      "theme": "clean"
     }
 }}
 {{> tool_lp/form-cohort-selector-suggestion }}
diff --git a/admin/tool/dataprivacy/amd/build/add_category.min.js b/admin/tool/dataprivacy/amd/build/add_category.min.js
new file mode 100644 (file)
index 0000000..92cd4ee
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/add_category.min.js differ
diff --git a/admin/tool/dataprivacy/amd/build/add_purpose.min.js b/admin/tool/dataprivacy/amd/build/add_purpose.min.js
new file mode 100644 (file)
index 0000000..dec27cc
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/add_purpose.min.js differ
diff --git a/admin/tool/dataprivacy/amd/build/categoriesactions.min.js b/admin/tool/dataprivacy/amd/build/categoriesactions.min.js
new file mode 100644 (file)
index 0000000..66e3b0a
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/categoriesactions.min.js differ
diff --git a/admin/tool/dataprivacy/amd/build/data_deletion.min.js b/admin/tool/dataprivacy/amd/build/data_deletion.min.js
new file mode 100644 (file)
index 0000000..5aa1e48
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/data_deletion.min.js differ
diff --git a/admin/tool/dataprivacy/amd/build/data_registry.min.js b/admin/tool/dataprivacy/amd/build/data_registry.min.js
new file mode 100644 (file)
index 0000000..f2964fe
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/data_registry.min.js differ
diff --git a/admin/tool/dataprivacy/amd/build/data_request_modal.min.js b/admin/tool/dataprivacy/amd/build/data_request_modal.min.js
new file mode 100644 (file)
index 0000000..7e9bda8
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/data_request_modal.min.js differ
diff --git a/admin/tool/dataprivacy/amd/build/effective_retention_period.min.js b/admin/tool/dataprivacy/amd/build/effective_retention_period.min.js
new file mode 100644 (file)
index 0000000..fd43e12
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/effective_retention_period.min.js differ
diff --git a/admin/tool/dataprivacy/amd/build/events.min.js b/admin/tool/dataprivacy/amd/build/events.min.js
new file mode 100644 (file)
index 0000000..1d4e973
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/events.min.js differ
diff --git a/admin/tool/dataprivacy/amd/build/expand_contract.min.js b/admin/tool/dataprivacy/amd/build/expand_contract.min.js
new file mode 100644 (file)
index 0000000..3419d58
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/expand_contract.min.js differ
diff --git a/admin/tool/dataprivacy/amd/build/form-user-selector.min.js b/admin/tool/dataprivacy/amd/build/form-user-selector.min.js
new file mode 100644 (file)
index 0000000..faa073f
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/form-user-selector.min.js differ
diff --git a/admin/tool/dataprivacy/amd/build/myrequestactions.min.js b/admin/tool/dataprivacy/amd/build/myrequestactions.min.js
new file mode 100644 (file)
index 0000000..937367c
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/myrequestactions.min.js differ
diff --git a/admin/tool/dataprivacy/amd/build/purposesactions.min.js b/admin/tool/dataprivacy/amd/build/purposesactions.min.js
new file mode 100644 (file)
index 0000000..0b15981
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/purposesactions.min.js differ
diff --git a/admin/tool/dataprivacy/amd/build/requestactions.min.js b/admin/tool/dataprivacy/amd/build/requestactions.min.js
new file mode 100644 (file)
index 0000000..586a2da
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/requestactions.min.js differ
diff --git a/admin/tool/dataprivacy/amd/src/add_category.js b/admin/tool/dataprivacy/amd/src/add_category.js
new file mode 100644 (file)
index 0000000..7ea22c8
--- /dev/null
@@ -0,0 +1,172 @@
+// 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/>.
+
+/**
+ * Module to add categories.
+ *
+ * @module     tool_dataprivacy/add_category
+ * @package    tool_dataprivacy
+ * @copyright  2018 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/str', 'core/ajax', 'core/notification', 'core/modal_factory', 'core/modal_events', 'core/fragment'],
+    function($, Str, Ajax, Notification, ModalFactory, ModalEvents, Fragment) {
+
+        var SELECTORS = {
+            CATEGORY_LINK: '[data-add-element="category"]',
+        };
+
+        var AddCategory = function(contextId) {
+            this.contextId = contextId;
+
+            var stringKeys = [
+                {
+                    key: 'addcategory',
+                    component: 'tool_dataprivacy'
+                },
+                {
+                    key: 'save',
+                    component: 'admin'
+                }
+            ];
+            this.strings = Str.get_strings(stringKeys);
+
+            this.registerEventListeners();
+        };
+
+        /**
+         * @var {int} contextId
+         * @private
+         */
+        AddCategory.prototype.contextId = 0;
+
+        /**
+         * @var {Promise}
+         * @private
+         */
+        AddCategory.prototype.strings = 0;
+
+        AddCategory.prototype.registerEventListeners = function() {
+
+            var trigger = $(SELECTORS.CATEGORY_LINK);
+            trigger.on('click', function() {
+                return this.strings.then(function(strings) {
+                    ModalFactory.create({
+                        type: ModalFactory.types.SAVE_CANCEL,
+                        title: strings[0],
+                        body: '',
+                    }, trigger).done(function(modal) {
+                        this.setupFormModal(modal, strings[1]);
+                    }.bind(this));
+                }.bind(this))
+                .fail(Notification.exception);
+            }.bind(this));
+
+        };
+
+        /**
+         * @method getBody
+         * @param {Object} formdata
+         * @private
+         * @return {Promise}
+         */
+        AddCategory.prototype.getBody = function(formdata) {
+
+            var params = null;
+            if (typeof formdata !== "undefined") {
+                params = {jsonformdata: JSON.stringify(formdata)};
+            }
+            // Get the content of the modal.
+            return Fragment.loadFragment('tool_dataprivacy', 'addcategory_form', this.contextId, params);
+        };
+
+        AddCategory.prototype.setupFormModal = function(modal, saveText) {
+            modal.setLarge();
+
+            modal.setSaveButtonText(saveText);
+
+            // We want to reset the form every time it is opened.
+            modal.getRoot().on(ModalEvents.hidden, this.destroy.bind(this));
+
+            modal.setBody(this.getBody());
+
+            // We catch the modal save event, and use it to submit the form inside the modal.
+            // Triggering a form submission will give JS validation scripts a chance to check for errors.
+            modal.getRoot().on(ModalEvents.save, this.submitForm.bind(this));
+            // We also catch the form submit event and use it to submit the form with ajax.
+            modal.getRoot().on('submit', 'form', this.submitFormAjax.bind(this));
+
+            this.modal = modal;
+
+            modal.show();
+        };
+
+        /**
+         * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed.
+         *
+         * @method submitForm
+         * @param {Event} e Form submission event.
+         * @private
+         */
+        AddCategory.prototype.submitForm = function(e) {
+            e.preventDefault();
+            this.modal.getRoot().find('form').submit();
+        };
+
+        AddCategory.prototype.submitFormAjax = function(e) {
+            // We don't want to do a real form submission.
+            e.preventDefault();
+
+            // Convert all the form elements values to a serialised string.
+            var formData = this.modal.getRoot().find('form').serialize();
+
+            Ajax.call([{
+                methodname: 'tool_dataprivacy_create_category_form',
+                args: {jsonformdata: JSON.stringify(formData)},
+                done: function(data) {
+                    if (data.validationerrors) {
+                        this.modal.setBody(this.getBody(formData));
+                    } else {
+                        this.close();
+                    }
+                }.bind(this),
+                fail: Notification.exception
+            }]);
+        };
+
+        AddCategory.prototype.close = function() {
+            this.destroy();
+            document.location.reload();
+        };
+
+        AddCategory.prototype.destroy = function() {
+            Y.use('moodle-core-formchangechecker', function() {
+                M.core_formchangechecker.reset_form_dirty_state();
+            });
+            this.modal.destroy();
+        };
+
+        AddCategory.prototype.removeListeners = function() {
+            $(SELECTORS.CATEGORY_LINK).off('click');
+        };
+
+        return /** @alias module:tool_dataprivacy/add_category */ {
+            getInstance: function(contextId) {
+                return new AddCategory(contextId);
+            }
+        };
+    }
+);
+
diff --git a/admin/tool/dataprivacy/amd/src/add_purpose.js b/admin/tool/dataprivacy/amd/src/add_purpose.js
new file mode 100644 (file)
index 0000000..f3c573c
--- /dev/null
@@ -0,0 +1,173 @@
+// 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/>.
+
+/**
+ * Module to add purposes.
+ *
+ * @module     tool_dataprivacy/add_purpose
+ * @package    tool_dataprivacy
+ * @copyright  2018 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/str', 'core/ajax', 'core/notification', 'core/modal_factory', 'core/modal_events', 'core/fragment'],
+    function($, Str, Ajax, Notification, ModalFactory, ModalEvents, Fragment) {
+
+        var SELECTORS = {
+            PURPOSE_LINK: '[data-add-element="purpose"]',
+        };
+
+        var AddPurpose = function(contextId) {
+            this.contextId = contextId;
+
+            var stringKeys = [
+                {
+                    key: 'addpurpose',
+                    component: 'tool_dataprivacy'
+                },
+                {
+                    key: 'save',
+                    component: 'admin'
+                }
+            ];
+            this.strings = Str.get_strings(stringKeys);
+
+            this.registerEventListeners();
+        };
+
+        /**
+         * @var {int} contextId
+         * @private
+         */
+        AddPurpose.prototype.contextId = 0;
+
+        /**
+         * @var {Promise}
+         * @private
+         */
+        AddPurpose.prototype.strings = 0;
+
+        AddPurpose.prototype.registerEventListeners = function() {
+
+            var trigger = $(SELECTORS.PURPOSE_LINK);
+            trigger.on('click', function() {
+                return this.strings.then(function(strings) {
+                    ModalFactory.create({
+                        type: ModalFactory.types.SAVE_CANCEL,
+                        title: strings[0],
+                        body: '',
+                    }, trigger).done(function(modal) {
+                        this.setupFormModal(modal, strings[1]);
+                    }.bind(this));
+                }.bind(this))
+                .fail(Notification.exception);
+            }.bind(this));
+
+        };
+
+        /**
+         * @method getBody
+         * @param {Object} formdata
+         * @private
+         * @return {Promise}
+         */
+        AddPurpose.prototype.getBody = function(formdata) {
+
+            var params = null;
+            if (typeof formdata !== "undefined") {
+                params = {jsonformdata: JSON.stringify(formdata)};
+            }
+            // Get the content of the modal.
+            return Fragment.loadFragment('tool_dataprivacy', 'addpurpose_form', this.contextId, params);
+        };
+
+        AddPurpose.prototype.setupFormModal = function(modal, saveText) {
+            modal.setLarge();
+
+            modal.setSaveButtonText(saveText);
+
+            // We want to reset the form every time it is opened.
+            modal.getRoot().on(ModalEvents.hidden, this.destroy.bind(this));
+
+            modal.setBody(this.getBody());
+
+            // We catch the modal save event, and use it to submit the form inside the modal.
+            // Triggering a form submission will give JS validation scripts a chance to check for errors.
+            modal.getRoot().on(ModalEvents.save, this.submitForm.bind(this));
+            // We also catch the form submit event and use it to submit the form with ajax.
+            modal.getRoot().on('submit', 'form', this.submitFormAjax.bind(this));
+
+            this.modal = modal;
+
+            modal.show();
+        };
+
+        /**
+         * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed.
+         *
+         * @method submitForm
+         * @param {Event} e Form submission event.
+         * @private
+         */
+        AddPurpose.prototype.submitForm = function(e) {
+            e.preventDefault();
+            this.modal.getRoot().find('form').submit();
+        };
+
+        AddPurpose.prototype.submitFormAjax = function(e) {
+            // We don't want to do a real form submission.
+            e.preventDefault();
+
+            // Convert all the form elements values to a serialised string.
+            var formData = this.modal.getRoot().find('form').serialize();
+
+            Ajax.call([{
+                methodname: 'tool_dataprivacy_create_purpose_form',
+                args: {jsonformdata: JSON.stringify(formData)},
+                done: function(data) {
+                    if (data.validationerrors) {
+                        this.modal.setBody(this.getBody(formData));
+                    } else {
+                        this.close();
+                    }
+                }.bind(this),
+
+                fail: Notification.exception
+            }]);
+        };
+
+        AddPurpose.prototype.close = function() {
+            this.destroy();
+            document.location.reload();
+        };
+
+        AddPurpose.prototype.destroy = function() {
+            Y.use('moodle-core-formchangechecker', function() {
+                M.core_formchangechecker.reset_form_dirty_state();
+            });
+            this.modal.destroy();
+        };
+
+        AddPurpose.prototype.removeListeners = function() {
+            $(SELECTORS.PURPOSE_LINK).off('click');
+        };
+
+        return /** @alias module:tool_dataprivacy/add_purpose */ {
+            getInstance: function(contextId) {
+                return new AddPurpose(contextId);
+            }
+        };
+    }
+);
+
diff --git a/admin/tool/dataprivacy/amd/src/categoriesactions.js b/admin/tool/dataprivacy/amd/src/categoriesactions.js
new file mode 100644 (file)
index 0000000..c40a1a7
--- /dev/null
@@ -0,0 +1,129 @@
+// 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 categories actions.
+ *
+ * @module     tool_dataprivacy/categoriesactions
+ * @package    tool_dataprivacy
+ * @copyright  2018 David Monllao
+ * @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'],
+function($, Ajax, Notification, Str, ModalFactory, ModalEvents) {
+
+    /**
+     * List of action selectors.
+     *
+     * @type {{DELETE: string}}
+     */
+    var ACTIONS = {
+        DELETE: '[data-action="deletecategory"]',
+    };
+
+    /**
+     * CategoriesActions class.
+     */
+    var CategoriesActions = function() {
+        this.registerEvents();
+    };
+
+    /**
+     * Register event listeners.
+     */
+    CategoriesActions.prototype.registerEvents = function() {
+        $(ACTIONS.DELETE).click(function(e) {
+            e.preventDefault();
+
+            var id = $(this).data('id');
+            var categoryname = $(this).data('name');
+            var stringkeys = [
+                {
+                    key: 'deletecategory',
+                    component: 'tool_dataprivacy',
+                    param: categoryname
+                },
+                {
+                    key: 'deletecategorytext',
+                    component: 'tool_dataprivacy',
+                    param: categoryname
+                }
+            ];
+
+            Str.get_strings(stringkeys).then(function(langStrings) {
+                var title = langStrings[0];
+                var confirmMessage = langStrings[1];
+                return ModalFactory.create({
+                    title: title,
+                    body: confirmMessage,
+                    type: ModalFactory.types.SAVE_CANCEL
+                }).then(function(modal) {
+                    modal.setSaveButtonText(title);
+
+                    // Handle save event.
+                    modal.getRoot().on(ModalEvents.save, function() {
+
+                        var request = {
+                            methodname: 'tool_dataprivacy_delete_category',
+                            args: {'id': id}
+                        };
+
+                        Ajax.call([request])[0].done(function(data) {
+                            if (data.result) {
+                                $('tr[data-categoryid="' + id + '"]').remove();
+                            } else {
+                                Notification.addNotification({
+                                    message: data.warnings[0].message,
+                                    type: 'error'
+                                });
+                            }
+                        }).fail(Notification.exception);
+                    });
+
+                    // Handle hidden event.
+                    modal.getRoot().on(ModalEvents.hidden, function() {
+                        // Destroy when hidden.
+                        modal.destroy();
+                    });
+
+                    return modal;
+                });
+            }).done(function(modal) {
+                modal.show();
+
+            }).fail(Notification.exception);
+        });
+    };
+
+    return /** @alias module:tool_dataprivacy/categoriesactions */ {
+        // Public variables and functions.
+
+        /**
+         * Initialise the module.
+         *
+         * @method init
+         * @return {CategoriesActions}
+         */
+        'init': function() {
+            return new CategoriesActions();
+        }
+    };
+});
diff --git a/admin/tool/dataprivacy/amd/src/data_deletion.js b/admin/tool/dataprivacy/amd/src/data_deletion.js
new file mode 100644 (file)
index 0000000..e36af21
--- /dev/null
@@ -0,0 +1,156 @@
+// 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/>.
+
+/**
+ * Request actions.
+ *
+ * @module     tool_dataprivacy/data_deletion
+ * @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'],
+function($, Ajax, Notification, Str, ModalFactory, ModalEvents) {
+
+    /**
+     * List of action selectors.
+     *
+     * @type {{MARK_FOR_DELETION: string}}
+     * @type {{SELECT_ALL: string}}
+     */
+    var ACTIONS = {
+        MARK_FOR_DELETION: '[data-action="markfordeletion"]',
+        SELECT_ALL: '[data-action="selectall"]',
+    };
+
+    /**
+     * List of selectors.
+     *
+     * @type {{SELECTCONTEXT: string}}
+     */
+    var SELECTORS = {
+        SELECTCONTEXT: '.selectcontext',
+    };
+
+    /**
+     * DataDeletionActions class.
+     */
+    var DataDeletionActions = function() {
+        this.registerEvents();
+    };
+
+    /**
+     * Register event listeners.
+     */
+    DataDeletionActions.prototype.registerEvents = function() {
+        $(ACTIONS.MARK_FOR_DELETION).click(function(e) {
+            e.preventDefault();
+
+            var selectedIds = [];
+            $(SELECTORS.SELECTCONTEXT).each(function() {
+                var checkbox = $(this);
+                if (checkbox.is(':checked')) {
+                    selectedIds.push(checkbox.val());
+                }
+            });
+            showConfirmation(selectedIds);
+        });
+
+        $(ACTIONS.SELECT_ALL).change(function(e) {
+            e.preventDefault();
+
+            var selectallnone = $(this);
+            if (selectallnone.is(':checked')) {
+                $(SELECTORS.SELECTCONTEXT).attr('checked', 'checked');
+            } else {
+                $(SELECTORS.SELECTCONTEXT).removeAttr('checked');
+            }
+        });
+    };
+
+    /**
+     * Show the confirmation dialogue.
+     *
+     * @param {Array} ids The array of expired context record IDs.
+     */
+    function showConfirmation(ids) {
+        var keys = [
+            {
+                key: 'confirm',
+                component: 'moodle'
+            },
+            {
+                key: 'confirmcontextdeletion',
+                component: 'tool_dataprivacy'
+            }
+        ];
+        var wsfunction = 'tool_dataprivacy_confirm_contexts_for_deletion';
+
+        var modalTitle = '';
+        Str.get_strings(keys).then(function(langStrings) {
+            modalTitle = langStrings[0];
+            var confirmMessage = langStrings[1];
+            return ModalFactory.create({
+                title: modalTitle,
+                body: confirmMessage,
+                type: ModalFactory.types.SAVE_CANCEL
+            });
+        }).then(function(modal) {
+            modal.setSaveButtonText(modalTitle);
+
+            // Handle save event.
+            modal.getRoot().on(ModalEvents.save, function() {
+                // Confirm the request.
+                var params = {
+                    'ids': ids
+                };
+
+                var request = {
+                    methodname: wsfunction,
+                    args: params
+                };
+
+                Ajax.call([request])[0].done(function(data) {
+                    if (data.result) {
+                        window.location.reload();
+                    } else {
+                        Notification.addNotification({
+                            message: data.warnings[0].message,
+                            type: 'error'
+                        });
+                    }
+                }).fail(Notification.exception);
+            });
+
+            // Handle hidden event.
+            modal.getRoot().on(ModalEvents.hidden, function() {
+                // Destroy when hidden.
+                modal.destroy();
+            });
+
+            return modal;
+        }).done(function(modal) {
+            modal.show();
+        }).fail(Notification.exception);
+    }
+
+    return DataDeletionActions;
+});
diff --git a/admin/tool/dataprivacy/amd/src/data_registry.js b/admin/tool/dataprivacy/amd/src/data_registry.js
new file mode 100644 (file)
index 0000000..76f00ee
--- /dev/null
@@ -0,0 +1,317 @@
+// 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/>.
+
+/**
+ * Request actions.
+ *
+ * @module     tool_dataprivacy/data_registry
+ * @package    tool_dataprivacy
+ * @copyright  2018 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/str', 'core/ajax', 'core/notification', 'core/templates', 'core/modal_factory',
+    'core/modal_events', 'core/fragment', 'tool_dataprivacy/add_purpose', 'tool_dataprivacy/add_category'],
+    function($, Str, Ajax, Notification, Templates, ModalFactory, ModalEvents, Fragment, AddPurpose, AddCategory) {
+
+        var SELECTORS = {
+            TREE_NODES: '[data-context-tree-node=1]',
+            FORM_CONTAINER: '#context-form-container',
+        };
+
+        var DataRegistry = function(systemContextId, initContextLevel, initContextId) {
+            this.systemContextId = systemContextId;
+            this.currentContextLevel = initContextLevel;
+            this.currentContextId = initContextId;
+            this.init();
+        };
+
+        /**
+         * @var {int} systemContextId
+         * @private
+         */
+        DataRegistry.prototype.systemContextId = 0;
+
+        /**
+         * @var {int} currentContextLevel
+         * @private
+         */
+        DataRegistry.prototype.currentContextLevel = 0;
+
+        /**
+         * @var {int} currentContextId
+         * @private
+         */
+        DataRegistry.prototype.currentContextId = 0;
+
+        /**
+         * @var {AddPurpose} addpurpose
+         * @private
+         */
+        DataRegistry.prototype.addpurpose = null;
+
+        /**
+         * @var {AddCategory} addcategory
+         * @private
+         */
+        DataRegistry.prototype.addcategory = null;
+
+        DataRegistry.prototype.init = function() {
+            // Add purpose and category modals always at system context.
+            this.addpurpose = AddPurpose.getInstance(this.systemContextId);
+            this.addcategory = AddCategory.getInstance(this.systemContextId);
+
+            var stringKeys = [
+                {
+                    key: 'changessaved',
+                    component: 'moodle'
+                }, {
+                    key: 'contextpurposecategorysaved',
+                    component: 'tool_dataprivacy'
+                }, {
+                    key: 'noblockstoload',
+                    component: 'tool_dataprivacy'
+                }, {
+                    key: 'noactivitiestoload',
+                    component: 'tool_dataprivacy'
+                }, {
+                    key: 'nocoursestoload',
+                    component: 'tool_dataprivacy'
+                }
+            ];
+            this.strings = Str.get_strings(stringKeys);
+
+            this.registerEventListeners();
+
+            // Load the default context level form.
+            if (this.currentContextId) {
+                this.loadForm('context_form', [this.currentContextId], this.submitContextFormAjax.bind(this));
+            } else {
+                this.loadForm('contextlevel_form', [this.currentContextLevel], this.submitContextLevelFormAjax.bind(this));
+            }
+        };
+
+        DataRegistry.prototype.registerEventListeners = function() {
+            $(SELECTORS.TREE_NODES).on('click', function(ev) {
+                ev.preventDefault();
+
+                var trigger = $(ev.currentTarget);
+
+                // Active node.
+                $(SELECTORS.TREE_NODES).removeClass('active');
+                trigger.addClass('active');
+
+                var contextLevel = trigger.data('contextlevel');
+                var contextId = trigger.data('contextid');
+                if (contextLevel) {
+                    // Context level level.
+
+                    window.history.pushState({}, null, '?contextlevel=' + contextLevel);
+
+                    // Remove previous add purpose and category listeners to avoid memory leaks.
+                    this.addpurpose.removeListeners();
+                    this.addcategory.removeListeners();
+
+                    // Load the context level form.
+                    this.currentContextLevel = contextLevel;
+                    this.loadForm('contextlevel_form', [this.currentContextLevel], this.submitContextLevelFormAjax.bind(this));
+                } else if (contextId) {
+                    // Context instance level.
+
+                    window.history.pushState({}, null, '?contextid=' + contextId);
+
+                    // Remove previous add purpose and category listeners to avoid memory leaks.
+                    this.addpurpose.removeListeners();
+                    this.addcategory.removeListeners();
+
+                    // Load the context level form.
+                    this.currentContextId = contextId;
+                    this.loadForm('context_form', [this.currentContextId], this.submitContextFormAjax.bind(this));
+                } else {
+                    // Expandable nodes.
+
+                    var expandContextId = trigger.data('expandcontextid');
+                    var expandElement = trigger.data('expandelement');
+                    var expanded = trigger.data('expanded');
+
+                    // Extra checking that there is an expandElement because we remove it after loading 0 branches.
+                    if (expandElement) {
+
+                        if (!expanded) {
+                            if (trigger.data('loaded') || !expandContextId || !expandElement) {
+                                this.expand(trigger);
+                            } else {
+
+                                trigger.find('> i').removeClass('fa-plus');
+                                trigger.find('> i').addClass('fa-circle-o-notch fa-spin');
+                                this.loadExtra(trigger, expandContextId, expandElement);
+                            }
+                        } else {
+                            this.collapse(trigger);
+                        }
+                    }
+                }
+
+            }.bind(this));
+        };
+
+        DataRegistry.prototype.removeListeners = function() {
+            $(SELECTORS.TREE_NODES).off('click');
+        };
+
+        DataRegistry.prototype.loadForm = function(fragmentName, fragmentArgs, formSubmitCallback) {
+
+            this.clearForm();
+
+            var fragment = Fragment.loadFragment('tool_dataprivacy', fragmentName, this.systemContextId, fragmentArgs);
+            fragment.done(function(html, js) {
+
+                $(SELECTORS.FORM_CONTAINER).html(html);
+                Templates.runTemplateJS(js);
+
+                this.addpurpose.registerEventListeners();
+                this.addcategory.registerEventListeners();
+
+                // We also catch the form submit event and use it to submit the form with ajax.
+                $(SELECTORS.FORM_CONTAINER).on('submit', 'form', formSubmitCallback);
+
+            }.bind(this)).fail(Notification.exception);
+        };
+
+        DataRegistry.prototype.clearForm = function() {
+            // For the previously loaded form.
+            Y.use('moodle-core-formchangechecker', function() {
+                M.core_formchangechecker.reset_form_dirty_state();
+            });
+
+            // Remove previous listeners.
+            $(SELECTORS.FORM_CONTAINER).off('submit', 'form');
+        };
+
+        /**
+         * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed.
+         *
+         * @method submitForm
+         * @param {Event} e Form submission event.
+         * @private
+         */
+        DataRegistry.prototype.submitForm = function(e) {
+            e.preventDefault();
+            $(SELECTORS.FORM_CONTAINER).find('form').submit();
+        };
+
+        DataRegistry.prototype.submitContextLevelFormAjax = function(e) {
+            this.submitFormAjax(e, 'tool_dataprivacy_set_contextlevel_form');
+        };
+
+        DataRegistry.prototype.submitContextFormAjax = function(e) {
+            this.submitFormAjax(e, 'tool_dataprivacy_set_context_form');
+        };
+
+        DataRegistry.prototype.submitFormAjax = function(e, saveMethodName) {
+            // We don't want to do a real form submission.
+            e.preventDefault();
+
+            // Convert all the form elements values to a serialised string.
+            var formData = $(SELECTORS.FORM_CONTAINER).find('form').serialize();
+            return this.strings.then(function(strings) {
+                Ajax.call([{
+                    methodname: saveMethodName,
+                    args: {jsonformdata: JSON.stringify(formData)},
+                    done: function() {
+                        Notification.alert(strings[0], strings[1]);
+                    },
+                    fail: Notification.exception
+                }]);
+            }).catch(Notification.exception);
+
+        };
+
+        DataRegistry.prototype.loadExtra = function(parentNode, expandContextId, expandElement) {
+
+            Ajax.call([{
+                methodname: 'tool_dataprivacy_tree_extra_branches',
+                args: {
+                    contextid: expandContextId,
+                    element: expandElement,
+                },
+                done: function(data) {
+                    if (data.branches.length == 0) {
+                        this.noElements(parentNode, expandElement);
+                        return;
+                    }
+                    Templates.render('tool_dataprivacy/context_tree_branches', data)
+                        .then(function(html) {
+                            parentNode.after(html);
+                            this.removeListeners();
+                            this.registerEventListeners();
+                            this.expand(parentNode);
+                            parentNode.data('loaded', 1);
+                            return;
+                        }.bind(this))
+                        .fail(Notification.exception);
+                }.bind(this),
+                fail: Notification.exception
+            }]);
+        };
+
+        DataRegistry.prototype.noElements = function(node, expandElement) {
+            node.data('expandcontextid', '');
+            node.data('expandelement', '');
+            this.strings.then(function(strings) {
+
+                // 2 = blocks, 3 = activities, 4 = courses (although courses is not likely really).
+                var key = 2;
+                if (expandElement == 'module') {
+                    key = 3;
+                } else if (expandElement == 'course') {
+                    key = 4;
+                }
+                node.text(strings[key]);
+                return;
+            }).fail(Notification.exception);
+        };
+
+        DataRegistry.prototype.collapse = function(node) {
+            node.data('expanded', 0);
+            node.siblings('nav').addClass('hidden');
+            node.find('> i').removeClass('fa-minus');
+            node.find('> i').addClass('fa-plus');
+        };
+
+        DataRegistry.prototype.expand = function(node) {
+            node.data('expanded', 1);
+            node.siblings('nav').removeClass('hidden');
+            node.find('> i').removeClass('fa-plus');
+            // Also remove the spinning one if data was just loaded.
+            node.find('> i').removeClass('fa-circle-o-notch fa-spin');
+            node.find('> i').addClass('fa-minus');
+        };
+        return /** @alias module:tool_dataprivacy/data_registry */ {
+
+            /**
+             * Initialise the page.
+             *
+             * @param {Number} systemContextId
+             * @param {Number} initContextLevel
+             * @param {Number} initContextId
+             * @return {DataRegistry}
+             */
+            init: function(systemContextId, initContextLevel, initContextId) {
+                return new DataRegistry(systemContextId, initContextLevel, initContextId);
+            }
+        };
+    }
+);
+
diff --git a/admin/tool/dataprivacy/amd/src/data_request_modal.js b/admin/tool/dataprivacy/amd/src/data_request_modal.js
new file mode 100644 (file)
index 0000000..abf717b
--- /dev/null
@@ -0,0 +1,93 @@
+// 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/>.
+
+/**
+ * Request actions.
+ *
+ * @module     tool_dataprivacy/data_request_modal
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/modal', 'core/modal_registry',
+        'tool_dataprivacy/events'],
+    function($, Notification, CustomEvents, Modal, ModalRegistry, DataPrivacyEvents) {
+
+        var registered = false;
+        var SELECTORS = {
+            APPROVE_BUTTON: '[data-action="approve"]',
+            DENY_BUTTON: '[data-action="deny"]',
+        };
+
+        /**
+         * Constructor for the Modal.
+         *
+         * @param {object} root The root jQuery element for the modal
+         */
+        var ModalDataRequest = function(root) {
+            Modal.call(this, root);
+
+            if (!this.getFooter().find(SELECTORS.APPROVE_BUTTON).length) {
+                Notification.exception({message: 'No approve button found'});
+            }
+
+            if (!this.getFooter().find(SELECTORS.DENY_BUTTON).length) {
+                Notification.exception({message: 'No deny button found'});
+            }
+        };
+
+        ModalDataRequest.TYPE = 'tool_dataprivacy-data_request';
+        ModalDataRequest.prototype = Object.create(Modal.prototype);
+        ModalDataRequest.prototype.constructor = ModalDataRequest;
+
+        /**
+         * Set up all of the event handling for the modal.
+         *
+         * @method registerEventListeners
+         */
+        ModalDataRequest.prototype.registerEventListeners = function() {
+            // Apply parent event listeners.
+            Modal.prototype.registerEventListeners.call(this);
+
+            this.getModal().on(CustomEvents.events.activate, SELECTORS.APPROVE_BUTTON, function(e, data) {
+                var approveEvent = $.Event(DataPrivacyEvents.approve);
+                this.getRoot().trigger(approveEvent, this);
+
+                if (!approveEvent.isDefaultPrevented()) {
+                    this.hide();
+                    data.originalEvent.preventDefault();
+                }
+            }.bind(this));
+
+            this.getModal().on(CustomEvents.events.activate, SELECTORS.DENY_BUTTON, function(e, data) {
+                var denyEvent = $.Event(DataPrivacyEvents.deny);
+                this.getRoot().trigger(denyEvent, this);
+
+                if (!denyEvent.isDefaultPrevented()) {
+                    this.hide();
+                    data.originalEvent.preventDefault();
+                }
+            }.bind(this));
+        };
+
+        // Automatically register with the modal registry the first time this module is imported so that you can create modals
+        // of this type using the modal factory.
+        if (!registered) {
+            ModalRegistry.register(ModalDataRequest.TYPE, ModalDataRequest, 'tool_dataprivacy/data_request_modal');
+            registered = true;
+        }
+
+        return ModalDataRequest;
+    });
\ No newline at end of file
diff --git a/admin/tool/dataprivacy/amd/src/effective_retention_period.js b/admin/tool/dataprivacy/amd/src/effective_retention_period.js
new file mode 100644 (file)
index 0000000..c73fb05
--- /dev/null
@@ -0,0 +1,89 @@
+// 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/>.
+
+/**
+ * Module to update the displayed retention period.
+ *
+ * @module     tool_dataprivacy/effective_retention_period
+ * @package    tool_dataprivacy
+ * @copyright  2018 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery'],
+    function($) {
+
+        var SELECTORS = {
+            PURPOSE_SELECT: '#id_purposeid',
+            RETENTION_FIELD: '.form-control-static',
+        };
+
+        /**
+         * Constructor for the retention period display.
+         *
+         * @param {Array} purposeRetentionPeriods Associative array of purposeids with effective retention period at this context
+         */
+        var EffectiveRetentionPeriod = function(purposeRetentionPeriods) {
+            this.purposeRetentionPeriods = purposeRetentionPeriods;
+            this.registerEventListeners();
+        };
+
+        /**
+         * Removes the current 'change' listeners.
+         *
+         * Useful when a new form is loaded.
+         */
+        var removeListeners = function() {
+            $(SELECTORS.PURPOSE_SELECT).off('change');
+        };
+
+        /**
+         * @var {Array} purposeRetentionPeriods
+         * @private
+         */
+        EffectiveRetentionPeriod.prototype.purposeRetentionPeriods = [];
+
+        /**
+         * Add purpose change listeners.
+         *
+         * @method registerEventListeners
+         */
+        EffectiveRetentionPeriod.prototype.registerEventListeners = function() {
+
+            $(SELECTORS.PURPOSE_SELECT).on('change', function(ev) {
+                var selected = $(ev.currentTarget).val();
+                var selectedPurpose = this.purposeRetentionPeriods[selected];
+
+                $(SELECTORS.RETENTION_FIELD).each(function() {
+                    var node = $(this);
+                    // Sucky way to select the text, but no id for static fields :(.
+                    var retentionField = node.siblings('#id_error_retention_current');
+                    if (retentionField.length > 0) {
+                        node.text(selectedPurpose);
+                    }
+                });
+
+            }.bind(this));
+        };
+
+        return /** @alias module:tool_dataprivacy/effective_retention_period */ {
+            init: function(purposeRetentionPeriods) {
+                // Remove previously attached listeners.
+                removeListeners();
+                return new EffectiveRetentionPeriod(purposeRetentionPeriods);
+            }
+        };
+    }
+);
+
diff --git a/admin/tool/dataprivacy/amd/src/events.js b/admin/tool/dataprivacy/amd/src/events.js
new file mode 100644 (file)
index 0000000..9398dc4
--- /dev/null
@@ -0,0 +1,30 @@
+// 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/>.
+
+/**
+ * Contain the events the data privacy tool can fire.
+ *
+ * @module     tool_dataprivacy/events
+ * @class      events
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([], function() {
+    return {
+        approve: 'tool_dataprivacy-data_request:approve',
+        deny: 'tool_dataprivacy-data_request:deny',
+    };
+});
diff --git a/admin/tool/dataprivacy/amd/src/expand_contract.js b/admin/tool/dataprivacy/amd/src/expand_contract.js
new file mode 100644 (file)
index 0000000..41b7e50
--- /dev/null
@@ -0,0 +1,89 @@
+// 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/>.
+
+/**
+ * Potential user selector module.
+ *
+ * @module     tool_dataprivacy/expand_contract
+ * @class      page-expand-contract
+ * @package    tool_dataprivacy
+ * @copyright  2018 Adrian Greeve
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery', 'core/url', 'core/str'], function($, url, str) {
+
+    var expandedImage = $('<img alt="" src="' + url.imageUrl('t/expanded') + '"/>');
+    var collapsedImage = $('<img alt="" src="' + url.imageUrl('t/collapsed') + '"/>');
+
+    return /** @alias module:tool_dataprivacy/expand-collapse */ {
+        /**
+         * Expand or collapse a selected node.
+         *
+         * @param  {object} targetnode The node that we want to expand / collapse
+         * @param  {object} thisnode The node that was clicked.
+         * @return {null}
+         */
+        expandCollapse: function(targetnode, thisnode) {
+            if (targetnode.hasClass('hide')) {
+                targetnode.removeClass('hide');
+                targetnode.addClass('visible');
+                targetnode.attr('aria-expanded', true);
+                thisnode.find(':header i.fa').removeClass('fa-plus-square');
+                thisnode.find(':header i.fa').addClass('fa-minus-square');
+                thisnode.find(':header img.icon').attr('src', expandedImage.attr('src'));
+            } else {
+                targetnode.removeClass('visible');
+                targetnode.addClass('hide');
+                targetnode.attr('aria-expanded', false);
+                thisnode.find(':header i.fa').removeClass('fa-minus-square');
+                thisnode.find(':header i.fa').addClass('fa-plus-square');
+                thisnode.find(':header img.icon').attr('src', collapsedImage.attr('src'));
+            }
+        },
+
+        /**
+         * Expand or collapse all nodes on this page.
+         *
+         * @param  {string} nextstate The next state to change to.
+         * @return {null}
+         */
+        expandCollapseAll: function(nextstate) {
+            var currentstate = (nextstate == 'visible') ? 'hide' : 'visible';
+            var ariaexpandedstate = (nextstate == 'visible') ? true : false;
+            var iconclassnow = (nextstate == 'visible') ? 'fa-plus-square' : 'fa-minus-square';
+            var iconclassnext = (nextstate == 'visible') ? 'fa-minus-square' : 'fa-plus-square';
+            var imagenow = (nextstate == 'visible') ? expandedImage.attr('src') : collapsedImage.attr('src');
+            $('.' + currentstate).each(function() {
+                $(this).removeClass(currentstate);
+                $(this).addClass(nextstate);
+                $(this).attr('aria-expanded', ariaexpandedstate);
+            });
+            $('.tool_dataprivacy-expand-all').data('visibilityState', currentstate);
+
+            str.get_string(currentstate, 'tool_dataprivacy').then(function(langString) {
+                $('.tool_dataprivacy-expand-all').html(langString);
+            }).catch(Notification.exception);
+
+            $(':header i.fa').each(function() {
+                $(this).removeClass(iconclassnow);
+                $(this).addClass(iconclassnext);
+            });
+            $(':header img.icon').each(function() {
+                $(this).attr('src', imagenow);
+            });
+        }
+    };
+});
diff --git a/admin/tool/dataprivacy/amd/src/form-user-selector.js b/admin/tool/dataprivacy/amd/src/form-user-selector.js
new file mode 100644 (file)
index 0000000..18c297a
--- /dev/null
@@ -0,0 +1,76 @@
+// 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/>.
+
+/**
+ * Potential user selector module.
+ *
+ * @module     tool_dataprivacy/form-user-selector
+ * @class      form-user-selector
+ * @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/templates'], function($, Ajax, Templates) {
+
+    return /** @alias module:tool_dataprivacy/form-user-selector */ {
+
+        processResults: function(selector, results) {
+            var users = [];
+            $.each(results, function(index, user) {
+                users.push({
+                    value: user.id,
+                    label: user._label
+                });
+            });
+            return users;
+        },
+
+        transport: function(selector, query, success, failure) {
+            var promise;
+
+            promise = Ajax.call([{
+                methodname: 'tool_dataprivacy_get_users',
+                args: {
+                    query: query
+                }
+            }]);
+
+            promise[0].then(function(results) {
+                var promises = [],
+                    i = 0;
+
+                // Render the label.
+                $.each(results, function(index, user) {
+                    promises.push(Templates.render('tool_dataprivacy/form-user-selector-suggestion', user));
+                });
+
+                // Apply the label to the results.
+                return $.when.apply($.when, promises).then(function() {
+                    var args = arguments;
+                    $.each(results, function(index, user) {
+                        user._label = args[i];
+                        i++;
+                    });
+                    success(results);
+                    return;
+                });
+
+            }).fail(failure);
+        }
+
+    };
+
+});
diff --git a/admin/tool/dataprivacy/amd/src/myrequestactions.js b/admin/tool/dataprivacy/amd/src/myrequestactions.js
new file mode 100644 (file)
index 0000000..5ec631e
--- /dev/null
@@ -0,0 +1,221 @@
+// 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 to enable users to manage their own data requests.
+ *
+ * @module     tool_dataprivacy/myrequestactions
+ * @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 {{CANCEL_REQUEST: string}}
+     * @type {{CONTACT_DPO: string}}
+     */
+    var ACTIONS = {
+        CANCEL_REQUEST: '[data-action="cancel"]',
+        CONTACT_DPO: '[data-action="contactdpo"]',
+    };
+
+    /**
+     * MyRequestActions class.
+     */
+    var MyRequestActions = function() {
+        this.registerEvents();
+    };
+
+    /**
+     * Register event listeners.
+     */
+    MyRequestActions.prototype.registerEvents = function() {
+        $(ACTIONS.CANCEL_REQUEST).click(function(e) {
+            e.preventDefault();
+
+            var requestId = $(this).data('requestid');
+            var stringkeys = [
+                {
+                    key: 'cancelrequest',
+                    component: 'tool_dataprivacy'
+                },
+                {
+                    key: 'cancelrequestconfirmation',
+                    component: 'tool_dataprivacy'
+                }
+            ];
+
+            Str.get_strings(stringkeys).then(function(langStrings) {
+                var title = langStrings[0];
+                var confirmMessage = langStrings[1];
+                return ModalFactory.create({
+                    title: title,
+                    body: confirmMessage,
+                    type: ModalFactory.types.SAVE_CANCEL
+                }).then(function(modal) {
+                    modal.setSaveButtonText(title);
+
+                    // Handle save event.
+                    modal.getRoot().on(ModalEvents.save, function() {
+                        // Cancel the request.
+                        var params = {
+                            'requestid': requestId
+                        };
+
+                        var request = {
+                            methodname: 'tool_dataprivacy_cancel_data_request',
+                            args: params
+                        };
+
+                        Ajax.call([request])[0].done(function(data) {
+                            if (data.result) {
+                                window.location.reload();
+                            } else {
+                                Notification.addNotification({
+                                    message: data.warnings[0].message,
+                                    type: 'error'
+                                });
+                            }
+                        }).fail(Notification.exception);
+                    });
+
+                    // Handle hidden event.
+                    modal.getRoot().on(ModalEvents.hidden, function() {
+                        // Destroy when hidden.
+                        modal.destroy();
+                    });
+
+                    return modal;
+                });
+            }).done(function(modal) {
+                // Show the modal!
+                modal.show();
+
+            }).fail(Notification.exception);
+        });
+
+        $(ACTIONS.CONTACT_DPO).click(function(e) {
+            e.preventDefault();
+
+            var replyToEmail = $(this).data('replytoemail');
+
+            var keys = [
+                {
+                    key: 'contactdataprotectionofficer',
+                    component: 'tool_dataprivacy'
+                },
+                {
+                    key: 'send',
+                    component: 'tool_dataprivacy'
+                },
+            ];
+
+            var sendButtonText = '';
+            Str.get_strings(keys).then(function(langStrings) {
+                var modalTitle = langStrings[0];
+                sendButtonText = langStrings[1];
+                var context = {
+                    'replytoemail': replyToEmail
+                };
+                return ModalFactory.create({
+                    title: modalTitle,
+                    body: Templates.render('tool_dataprivacy/contact_dpo', context),
+                    type: ModalFactory.types.SAVE_CANCEL,
+                    large: true
+                });
+            }).done(function(modal) {
+                modal.setSaveButtonText(sendButtonText);
+
+                // Handle send event.
+                modal.getRoot().on(ModalEvents.save, function(e) {
+                    var message = $('#message').val().trim();
+                    if (message.length === 0) {
+                        e.preventDefault();
+                        // Show validation error when the message is empty.
+                        $('[data-region="messageinput"]').addClass('has-danger notifyproblem');
+                        $('#id_error_message').removeAttr('hidden');
+                    } else {
+                        // Send the message.
+                        sendMessageToDPO(message);
+                    }
+                });
+
+                // Handle hidden event.
+                modal.getRoot().on(ModalEvents.hidden, function() {
+                    // Destroy when hidden.
+                    modal.destroy();
+                });
+
+                // Show the modal!
+                modal.show();
+            }).fail(Notification.exception);
+        });
+    };
+
+    /**
+     * Send message to the Data Protection Officer.
+     *
+     * @param {String} message The message to send.
+     */
+    function sendMessageToDPO(message) {
+        var request = {
+            methodname: 'tool_dataprivacy_contact_dpo',
+            args: {
+                message: message
+            }
+        };
+
+        var requestType = 'success';
+        Ajax.call([request])[0].then(function(data) {
+            if (data.result) {
+                return Str.get_string('requestsubmitted', 'tool_dataprivacy');
+            }
+            requestType = 'error';
+            return data.warnings.join('<br>');
+
+        }).done(function(message) {
+            Notification.addNotification({
+                message: message,
+                type: requestType
+            });
+
+        }).fail(Notification.exception);
+    }
+
+    return /** @alias module:tool_dataprivacy/myrequestactions */ {
+        // Public variables and functions.
+
+        /**
+         * Initialise the unified user filter.
+         *
+         * @method init
+         * @return {MyRequestActions}
+         */
+        'init': function() {
+            return new MyRequestActions();
+        }
+    };
+});
diff --git a/admin/tool/dataprivacy/amd/src/purposesactions.js b/admin/tool/dataprivacy/amd/src/purposesactions.js
new file mode 100644 (file)
index 0000000..fd92141
--- /dev/null
@@ -0,0 +1,129 @@
+// 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 purposes actions.
+ *
+ * @module     tool_dataprivacy/purposesactions
+ * @package    tool_dataprivacy
+ * @copyright  2018 David Monllao
+ * @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'],
+function($, Ajax, Notification, Str, ModalFactory, ModalEvents) {
+
+    /**
+     * List of action selectors.
+     *
+     * @type {{DELETE: string}}
+     */
+    var ACTIONS = {
+        DELETE: '[data-action="deletepurpose"]',
+    };
+
+    /**
+     * PurposesActions class.
+     */
+    var PurposesActions = function() {
+        this.registerEvents();
+    };
+
+    /**
+     * Register event listeners.
+     */
+    PurposesActions.prototype.registerEvents = function() {
+        $(ACTIONS.DELETE).click(function(e) {
+            e.preventDefault();
+
+            var id = $(this).data('id');
+            var purposename = $(this).data('name');
+            var stringkeys = [
+                {
+                    key: 'deletepurpose',
+                    component: 'tool_dataprivacy',
+                    param: purposename
+                },
+                {
+                    key: 'deletepurposetext',
+                    component: 'tool_dataprivacy',
+                    param: purposename
+                }
+            ];
+
+            Str.get_strings(stringkeys).then(function(langStrings) {
+                var title = langStrings[0];
+                var confirmMessage = langStrings[1];
+                return ModalFactory.create({
+                    title: title,
+                    body: confirmMessage,
+                    type: ModalFactory.types.SAVE_CANCEL
+                }).then(function(modal) {
+                    modal.setSaveButtonText(title);
+
+                    // Handle save event.
+                    modal.getRoot().on(ModalEvents.save, function() {
+
+                        var request = {
+                            methodname: 'tool_dataprivacy_delete_purpose',
+                            args: {'id': id}
+                        };
+
+                        Ajax.call([request])[0].done(function(data) {
+                            if (data.result) {
+                                $('tr[data-purposeid="' + id + '"]').remove();
+                            } else {
+                                Notification.addNotification({
+                                    message: data.warnings[0].message,
+                                    type: 'error'
+                                });
+                            }
+                        }).fail(Notification.exception);
+                    });
+
+                    // Handle hidden event.
+                    modal.getRoot().on(ModalEvents.hidden, function() {
+                        // Destroy when hidden.
+                        modal.destroy();
+                    });
+
+                    return modal;
+                });
+            }).done(function(modal) {
+                modal.show();
+
+            }).fail(Notification.exception);
+        });
+    };
+
+    return /** @alias module:tool_dataprivacy/purposesactions */ {
+        // Public variables and functions.
+
+        /**
+         * Initialise the module.
+         *
+         * @method init
+         * @return {PurposesActions}
+         */
+        'init': function() {
+            return new PurposesActions();
+        }
+    };
+});
diff --git a/admin/tool/dataprivacy/amd/src/requestactions.js b/admin/tool/dataprivacy/amd/src/requestactions.js
new file mode 100644 (file)
index 0000000..c0941f8
--- /dev/null
@@ -0,0 +1,227 @@
+// 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/>.
+
+/**
+ * Request actions.
+ *
+ * @module     tool_dataprivacy/requestactions
+ * @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',
+    'tool_dataprivacy/data_request_modal',
+    'tool_dataprivacy/events'],
+function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, ModalDataRequest, DataPrivacyEvents) {
+
+    /**
+     * List of action selectors.
+     *
+     * @type {{APPROVE_REQUEST: string}}
+     * @type {{DENY_REQUEST: string}}
+     * @type {{VIEW_REQUEST: string}}
+     */
+    var ACTIONS = {
+        APPROVE_REQUEST: '[data-action="approve"]',
+        DENY_REQUEST: '[data-action="deny"]',
+        VIEW_REQUEST: '[data-action="view"]'
+    };
+
+    /**
+     * RequestActions class.
+     */
+    var RequestActions = function() {
+        this.registerEvents();
+    };
+
+    /**
+     * Register event listeners.
+     */
+    RequestActions.prototype.registerEvents = function() {
+        $(ACTIONS.VIEW_REQUEST).click(function(e) {
+            e.preventDefault();
+
+            var requestId = $(this).data('requestid');
+
+            // Cancel the request.
+            var params = {
+                'requestid': requestId
+            };
+
+            var request = {
+                methodname: 'tool_dataprivacy_get_data_request',
+                args: params
+            };
+
+            var promises = Ajax.call([request]);
+            var modalTitle = '';
+            var modalType = ModalFactory.types.DEFAULT;
+            $.when(promises[0]).then(function(data) {
+                if (data.result) {
+                    // Check if the status is awaiting approval.
+                    if (data.result.status == 2) {
+                        modalType = ModalDataRequest.TYPE;
+                    }
+                    modalTitle = data.result.typename;
+                    return Templates.render('tool_dataprivacy/request_details', data.result);
+                }
+                // Fail.
+                Notification.addNotification({
+                    message: data.warnings[0].message,
+                    type: 'error'
+                });
+                return false;
+
+            }).then(function(html) {
+                return ModalFactory.create({
+                    title: modalTitle,
+                    body: html,
+                    type: modalType,
+                    large: true
+                }).then(function(modal) {
+                    // Handle approve event.
+                    modal.getRoot().on(DataPrivacyEvents.approve, function() {
+                        showConfirmation(DataPrivacyEvents.approve, requestId);
+                    });
+
+                    // Handle deny event.
+                    modal.getRoot().on(DataPrivacyEvents.deny, function() {
+                        showConfirmation(DataPrivacyEvents.deny, requestId);
+                    });
+
+                    // Handle hidden event.
+                    modal.getRoot().on(ModalEvents.hidden, function() {
+                        // Destroy when hidden.
+                        modal.destroy();
+                    });
+
+                    return modal;
+                });
+            }).done(function(modal) {
+                // Show the modal!
+                modal.show();
+            }).fail(Notification.exception);
+        });
+
+        $(ACTIONS.APPROVE_REQUEST).click(function(e) {
+            e.preventDefault();
+
+            var requestId = $(this).data('requestid');
+            showConfirmation(DataPrivacyEvents.approve, requestId);
+        });
+
+        $(ACTIONS.DENY_REQUEST).click(function(e) {
+            e.preventDefault();
+
+            var requestId = $(this).data('requestid');
+            showConfirmation(DataPrivacyEvents.deny, requestId);
+        });
+    };
+
+    /**
+     * Show the confirmation dialogue.
+     *
+     * @param {String} action The action name.
+     * @param {Number} requestId The request ID.
+     */
+    function showConfirmation(action, requestId) {
+        var keys = [];
+        var wsfunction = '';
+        switch (action) {
+            case DataPrivacyEvents.approve:
+                keys = [
+                    {
+                        key: 'approverequest',
+                        component: 'tool_dataprivacy'
+                    },
+                    {
+                        key: 'confirmapproval',
+                        component: 'tool_dataprivacy'
+                    }
+                ];
+                wsfunction = 'tool_dataprivacy_approve_data_request';
+                break;
+            case DataPrivacyEvents.deny:
+                keys = [
+                    {
+                        key: 'denyrequest',
+                        component: 'tool_dataprivacy'
+                    },
+                    {
+                        key: 'confirmdenial',
+                        component: 'tool_dataprivacy'
+                    }
+                ];
+                wsfunction = 'tool_dataprivacy_deny_data_request';
+                break;
+        }
+
+        var modalTitle = '';
+        Str.get_strings(keys).then(function(langStrings) {
+            modalTitle = langStrings[0];
+            var confirmMessage = langStrings[1];
+            return ModalFactory.create({
+                title: modalTitle,
+                body: confirmMessage,
+                type: ModalFactory.types.SAVE_CANCEL
+            });
+        }).then(function(modal) {
+            modal.setSaveButtonText(modalTitle);
+
+            // Handle save event.
+            modal.getRoot().on(ModalEvents.save, function() {
+                // Confirm the request.
+                var params = {
+                    'requestid': requestId
+                };
+
+                var request = {
+                    methodname: wsfunction,
+                    args: params
+                };
+
+                Ajax.call([request])[0].done(function(data) {
+                    if (data.result) {
+                        window.location.reload();
+                    } else {
+                        Notification.addNotification({
+                            message: data.warnings[0].message,
+                            type: 'error'
+                        });
+                    }
+                }).fail(Notification.exception);
+            });
+
+            // Handle hidden event.
+            modal.getRoot().on(ModalEvents.hidden, function() {
+                // Destroy when hidden.
+                modal.destroy();
+            });
+
+            return modal;
+        }).done(function(modal) {
+            modal.show();
+        }).fail(Notification.exception);
+    }
+
+    return RequestActions;
+});
diff --git a/admin/tool/dataprivacy/categories.php b/admin/tool/dataprivacy/categories.php
new file mode 100644 (file)
index 0000000..b160ff3
--- /dev/null
@@ -0,0 +1,39 @@
+<?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 page lets users manage categories.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../config.php');
+
+$url = new moodle_url("/admin/tool/dataprivacy/categories.php");
+$title = get_string('editcategories', 'tool_dataprivacy');
+
+\tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry');
+
+$output = $PAGE->get_renderer('tool_dataprivacy');
+echo $output->header();
+
+$categories = \tool_dataprivacy\api::get_categories();
+$renderable = new \tool_dataprivacy\output\categories($categories);
+
+echo $output->render($renderable);
+echo $output->footer();
diff --git a/admin/tool/dataprivacy/classes/api.php b/admin/tool/dataprivacy/classes/api.php
new file mode 100644 (file)
index 0000000..5038e98
--- /dev/null
@@ -0,0 +1,876 @@
+<?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 helper methods for processing data requests.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy;
+
+use coding_exception;
+use context_system;
+use core\invalid_persistent_exception;
+use core\message\message;
+use core\task\manager;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\contextlist_collection;
+use core_user;
+use dml_exception;
+use moodle_exception;
+use moodle_url;
+use required_capability_exception;
+use stdClass;
+use tool_dataprivacy\external\data_request_exporter;
+use tool_dataprivacy\task\initiate_data_request_task;
+use tool_dataprivacy\task\process_data_request_task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class containing helper methods for processing data requests.
+ *
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class api {
+
+    /** Data export request type. */
+    const DATAREQUEST_TYPE_EXPORT = 1;
+
+    /** Data deletion request type. */
+    const DATAREQUEST_TYPE_DELETE = 2;
+
+    /** Other request type. Usually of enquiries to the DPO. */
+    const DATAREQUEST_TYPE_OTHERS = 3;
+
+    /** Newly submitted and we haven't yet started finding out where they have data. */
+    const DATAREQUEST_STATUS_PENDING = 0;
+
+    /** Newly submitted and we have started to find the location of data. */
+    const DATAREQUEST_STATUS_PREPROCESSING = 1;
+
+    /** Metadata ready and awaiting review and approval by the Data Protection officer. */
+    const DATAREQUEST_STATUS_AWAITING_APPROVAL = 2;
+
+    /** Request approved and will be processed soon. */
+    const DATAREQUEST_STATUS_APPROVED = 3;
+
+    /** The request is now being processed. */
+    const DATAREQUEST_STATUS_PROCESSING = 4;
+
+    /** Data request completed. */
+    const DATAREQUEST_STATUS_COMPLETE = 5;
+
+    /** Data request cancelled by the user. */
+    const DATAREQUEST_STATUS_CANCELLED = 6;
+
+    /** Data request rejected by the DPO. */
+    const DATAREQUEST_STATUS_REJECTED = 7;
+
+    /**
+     * Determines whether the user can contact the site's Data Protection Officer via Moodle.
+     *
+     * @return boolean True when tool_dataprivacy|contactdataprotectionofficer is enabled.
+     * @throws dml_exception
+     */
+    public static function can_contact_dpo() {
+        return get_config('tool_dataprivacy', 'contactdataprotectionofficer') == 1;
+    }
+
+    /**
+     * Check's 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);
+    }
+
+    /**
+     * Checks if the current user can manage the data registry at the provided id.
+     *
+     * @param int $contextid Fallback to system context id.
+     * @throws \required_capability_exception
+     * @return null
+     */
+    public static function check_can_manage_data_registry($contextid = false) {
+        if ($contextid) {
+            $context = \context_helper::instance_by_id($contextid);
+        } else {
+            $context = \context_system::instance();
+        }
+
+        require_capability('tool/dataprivacy:managedataregistry', $context);
+    }
+
+    /**
+     * Fetches the list of users with the Data Protection Officer role.
+     *
+     * @throws dml_exception
+     */
+    public static function get_site_dpos() {
+        // Get role(s) that can manage data requests.
+        $dporoles = explode(',', get_config('tool_dataprivacy', 'dporoles'));
+
+        $dpos = [];
+        $context = context_system::instance();
+        foreach ($dporoles as $roleid) {
+            if (empty($roleid)) {
+                continue;
+            }
+            // Fetch users that can manage data requests.
+            $dpos += get_role_users($roleid, $context, false, 'u.*');
+        }
+
+        // If the site has no data protection officer, defer to site admin(s).
+        if (empty($dpos)) {
+            $dpos = get_admins();
+        }
+        return $dpos;
+    }
+
+    /**
+     * Checks whether a given user is a site DPO.
+     *
+     * @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);
+    }
+
+    /**
+     * Lodges a data request and sends the request details to the site Data Protection Officer(s).
+     *
+     * @param int $foruser The user whom the request is being made for.
+     * @param int $type The request type.
+     * @param string $comments Request comments.
+     * @return data_request
+     * @throws invalid_persistent_exception
+     * @throws coding_exception
+     */
+    public static function create_data_request($foruser, $type, $comments = '') {
+        global $USER;
+
+        $datarequest = new data_request();
+        // The user the request is being made for.
+        $datarequest->set('userid', $foruser);
+        // The user making the request.
+        $datarequest->set('requestedby', $USER->id);
+        // Set status.
+        $datarequest->set('status', self::DATAREQUEST_STATUS_PENDING);
+        // Set request type.
+        $datarequest->set('type', $type);
+        // Set request comments.
+        $datarequest->set('comments', $comments);
+
+        // Store subject access request.
+        $datarequest->create();
+
+        // Fire an ad hoc task to initiate the data request process.
+        $task = new initiate_data_request_task();
+        $task->set_custom_data(['requestid' => $datarequest->get('id')]);
+        manager::queue_adhoc_task($task, true);
+
+        return $datarequest;
+    }
+
+    /**
+     * Fetches the list of the data requests.
+     *
+     * If user ID is provided, it fetches the data requests for the user.
+     * Otherwise, it fetches all of the data requests, provided that the user has the capability to manage data requests.
+     * (e.g. Users with the Data Protection Officer roles)
+     *
+     * @param int $userid The User ID.
+     * @return data_request[]
+     * @throws dml_exception
+     */
+    public static function get_data_requests($userid = 0) {
+        global $USER;
+        $results = [];
+        if ($userid) {
+            // Get the data requests for the user or data requests made by the user.
+            $select = "userid = :userid OR requestedby = :requestedby";
+            $params = [
+                'userid' => $userid,
+                'requestedby' => $userid
+            ];
+            $results = data_request::get_records_select($select, $params, 'status DESC, timemodified DESC');
+        } else {
+            // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
+            if (self::is_site_dpo($USER->id)) {
+                $results = data_request::get_records(null, 'status DESC, timemodified DESC', '');
+            }
+        }
+
+        return $results;
+    }
+
+    /**
+     * Checks whether there is already an existing pending/in-progress data request for a user for a given request type.
+     *
+     * @param int $userid The user ID.
+     * @param int $type The request type.
+     * @return bool
+     * @throws coding_exception
+     * @throws dml_exception
+     */
+    public static function has_ongoing_request($userid, $type) {
+        global $DB;
+
+        // Check if the user already has an incomplete data request of the same type.
+        $nonpendingstatuses = [
+            self::DATAREQUEST_STATUS_COMPLETE,
+            self::DATAREQUEST_STATUS_CANCELLED,
+            self::DATAREQUEST_STATUS_REJECTED,
+        ];
+        list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED);
+        $select = 'type = :type AND userid = :userid AND status NOT ' . $insql;
+        $params = array_merge([
+            'type' => $type,
+            'userid' => $userid
+        ], $inparams);
+
+        return data_request::record_exists_select($select, $params);
+    }
+
+    /**
+     * Determines whether a request is active or not based on its status.
+     *
+     * @param int $status The request status.
+     * @return bool
+     */
+    public static function is_active($status) {
+        // List of statuses which doesn't require any further processing.
+        $finalstatuses = [
+            self::DATAREQUEST_STATUS_COMPLETE,
+            self::DATAREQUEST_STATUS_CANCELLED,
+            self::DATAREQUEST_STATUS_REJECTED,
+        ];
+
+        return !in_array($status, $finalstatuses);
+    }
+
+    /**
+     * Cancels the data request for a given request ID.
+     *
+     * @param int $requestid The request identifier.
+     * @param int $status The request status.
+     * @param int $dpoid The user ID of the Data Protection Officer
+     * @return bool
+     * @throws invalid_persistent_exception
+     * @throws coding_exception
+     */
+    public static function update_request_status($requestid, $status, $dpoid = 0) {
+        // Update the request.
+        $datarequest = new data_request($requestid);
+        $datarequest->set('status', $status);
+        if ($dpoid) {
+            $datarequest->set('dpo', $dpoid);
+        }
+        return $datarequest->update();
+    }
+
+    /**
+     * Fetches a request based on the request ID.
+     *
+     * @param int $requestid The request identifier
+     * @return data_request
+     */
+    public static function get_request($requestid) {
+        return new data_request($requestid);
+    }
+
+    /**
+     * Approves a data request based on the request ID.
+     *
+     * @param int $requestid The request identifier
+     * @return bool
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws invalid_persistent_exception
+     * @throws required_capability_exception
+     * @throws moodle_exception
+     */
+    public static function approve_data_request($requestid) {
+        global $USER;
+
+        // Check first whether the user can manage data requests.
+        if (!self::can_manage_data_requests($USER->id)) {
+            $context = context_system::instance();
+            throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
+        }
+
+        // Check if request is already awaiting for approval.
+        $request = new data_request($requestid);
+        if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
+            throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
+        }
+
+        // Update the status and the DPO.
+        $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id);
+
+        // Approve all the contexts attached to the request.
+        // Currently, approving the request implicitly approves all associated contexts, but this may change in future, allowing
+        // users to selectively approve certain contexts only.
+        self::update_request_contexts_with_status($requestid, contextlist_context::STATUS_APPROVED);
+
+        // Fire an ad hoc task to initiate the data request process.
+        $task = new process_data_request_task();
+        $task->set_custom_data(['requestid' => $requestid]);
+        if ($request->get('type') == self::DATAREQUEST_TYPE_EXPORT) {
+            $task->set_userid($request->get('userid'));
+        }
+        manager::queue_adhoc_task($task, true);
+
+        return $result;
+    }
+
+    /**
+     * Rejects a data request based on the request ID.
+     *
+     * @param int $requestid The request identifier
+     * @return bool
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws invalid_persistent_exception
+     * @throws required_capability_exception
+     * @throws moodle_exception
+     */
+    public static function deny_data_request($requestid) {
+        global $USER;
+
+        if (!self::can_manage_data_requests($USER->id)) {
+            $context = context_system::instance();
+            throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
+        }
+
+        // Check if request is already awaiting for approval.
+        $request = new data_request($requestid);
+        if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
+            throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
+        }
+
+        // Update the status and the DPO.
+        return self::update_request_status($requestid, self::DATAREQUEST_STATUS_REJECTED, $USER->id);
+    }
+
+    /**
+     * Sends a message to the site's Data Protection Officer about a request.
+     *
+     * @param stdClass $dpo The DPO user record
+     * @param data_request $request The data request
+     * @return int|false
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws moodle_exception
+     */
+    public static function notify_dpo($dpo, data_request $request) {
+        global $PAGE, $SITE;
+
+        $output = $PAGE->get_renderer('tool_dataprivacy');
+
+        $usercontext = \context_user::instance($request->get('requestedby'));
+        $requestexporter = new data_request_exporter($request, ['context' => $usercontext]);
+        $requestdata = $requestexporter->export($output);
+
+        // Create message to send to the Data Protection Officer(s).
+        $typetext = null;
+        $typetext = $requestdata->typename;
+        $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext);
+
+        $requestedby = $requestdata->requestedbyuser;
+        $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
+        $message = new message();
+        $message->courseid          = $SITE->id;
+        $message->component         = 'tool_dataprivacy';
+        $message->name              = 'contactdataprotectionofficer';
+        $message->userfrom          = $requestedby;
+        $message->replyto           = $requestedby->email;
+        $message->replytoname       = $requestedby->fullname;
+        $message->subject           = $subject;
+        $message->fullmessageformat = FORMAT_HTML;
+        $message->notification      = 1;
+        $message->contexturl        = $datarequestsurl;
+        $message->contexturlname    = get_string('datarequests', 'tool_dataprivacy');
+
+        // Prepare the context data for the email message body.
+        $messagetextdata = [
+            'requestedby' => $requestedby->fullname,
+            'requesttype' => $typetext,
+            'requestdate' => userdate($requestdata->timecreated),
+            'requestcomments' => $requestdata->messagehtml,
+            'datarequestsurl' => $datarequestsurl
+        ];
+        $requestingfor = $requestdata->foruser;
+        if ($requestedby->id == $requestingfor->id) {
+            $messagetextdata['requestfor'] = $messagetextdata['requestedby'];
+        } else {
+            $messagetextdata['requestfor'] = $requestingfor->fullname;
+        }
+
+        // Email the data request to the Data Protection Officer(s)/Admin(s).
+        $messagetextdata['dponame'] = fullname($dpo);
+        // Render message email body.
+        $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_email', $messagetextdata);
+        $message->userto = $dpo;
+        $message->fullmessage = html_to_text($messagehtml);
+        $message->fullmessagehtml = $messagehtml;
+
+        // Send message.
+        return message_send($message);
+    }
+
+    /**
+     * Creates a new data purpose.
+     *
+     * @param stdClass $record
+     * @return \tool_dataprivacy\purpose.
+     */
+    public static function create_purpose(stdClass $record) {
+        self::check_can_manage_data_registry();
+
+        $purpose = new purpose(0, $record);
+        $purpose->create();
+
+        return $purpose;
+    }
+
+    /**
+     * Updates an existing data purpose.
+     *
+     * @param stdClass $record
+     * @return \tool_dataprivacy\purpose.
+     */
+    public static function update_purpose(stdClass $record) {
+        self::check_can_manage_data_registry();
+
+        $purpose = new purpose($record->id);
+        $purpose->from_record($record);
+
+        $result = $purpose->update();
+
+        return $purpose;
+    }
+
+    /**
+     * Deletes a data purpose.
+     *
+     * @param int $id
+     * @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.');
+        }
+        return $purpose->delete();
+    }
+
+    /**
+     * Get all system data purposes.
+     *
+     * @return \tool_dataprivacy\purpose[]
+     */
+    public static function get_purposes() {
+        self::check_can_manage_data_registry();
+
+        return purpose::get_records([], 'name', 'ASC');
+    }
+
+    /**
+     * Creates a new data category.
+     *
+     * @param stdClass $record
+     * @return \tool_dataprivacy\category.
+     */
+    public static function create_category(stdClass $record) {
+        self::check_can_manage_data_registry();
+
+        $category = new category(0, $record);
+        $category->create();
+
+        return $category;
+    }
+
+    /**
+     * Updates an existing data category.
+     *
+     * @param stdClass $record
+     * @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);
+
+        $result = $category->update();
+
+        return $category;
+    }
+
+    /**
+     * Deletes a data category.
+     *
+     * @param int $id
+     * @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.');
+        }
+        return $category->delete();
+    }
+
+    /**
+     * Get all system data categories.
+     *
+     * @return \tool_dataprivacy\category[]
+     */
+    public static function get_categories() {
+        self::check_can_manage_data_registry();
+
+        return category::get_records([], 'name', 'ASC');
+    }
+
+    /**
+     * Sets the context instance purpose and category.
+     *
+     * @param \stdClass $record
+     * @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);
+
+            if (empty($record->purposeid) && empty($record->categoryid)) {
+                // We accept one of them to be null but we delete it if both are null.
+                self::unset_context_instance($instance);
+                return;
+            }
+
+        } else {
+            // Add.
+            $instance = new context_instance(0, $record);
+        }
+        $instance->save();
+
+        return $instance;
+    }
+
+    /**
+     * Unsets the context instance record.
+     *
+     * @param \tool_dataprivacy\context_instance $instance
+     * @return null
+     */
+    public static function unset_context_instance(context_instance $instance) {
+        self::check_can_manage_data_registry($instance->get('contextid'));
+        $instance->delete();
+    }
+
+    /**
+     * Sets the context level purpose and category.
+     *
+     * @throws \coding_exception
+     * @param \stdClass $record
+     * @return contextlevel
+     */
+    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');
+        }
+
+        if ($contextlevel = contextlevel::get_record_by_contextlevel($record->contextlevel, false)) {
+            // Update.
+            $contextlevel->from_record($record);
+        } else {
+            // Add.
+            $contextlevel = new contextlevel(0, $record);
+        }
+        $contextlevel->save();
+
+        // We sync with their defaults as we removed these options from the defaults page.
+        $classname = \context_helper::get_class_for_level($record->contextlevel);
+        list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname);
+        set_config($purposevar, $record->purposeid, 'tool_dataprivacy');
+        set_config($categoryvar, $record->categoryid, 'tool_dataprivacy');
+
+        return $contextlevel;
+    }
+
+    /**
+     * Returns the effective category given a context instance.
+     *
+     * @param \context $context
+     * @param int $forcedvalue Use this categoryid value as if this was this context instance category.
+     * @return category|false
+     */
+    public static function get_effective_context_category(\context $context, $forcedvalue=false) {
+        self::check_can_manage_data_registry($context->id);
+        if (!data_registry::defaults_set()) {
+            return false;
+        }
+
+        return data_registry::get_effective_context_value($context, 'category', $forcedvalue);
+    }
+
+    /**
+     * Returns the effective purpose given a context instance.
+     *
+     * @param \context $context
+     * @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);
+        if (!data_registry::defaults_set()) {
+            return false;
+        }
+
+        return data_registry::get_effective_context_value($context, 'purpose', $forcedvalue);
+    }
+
+    /**
+     * Returns the effective category given a context level.
+     *
+     * @param int $contextlevel
+     * @param int $forcedvalue Use this categoryid value as if this was this context level category.
+     * @return category|false
+     */
+    public static function get_effective_contextlevel_category($contextlevel, $forcedvalue=false) {
+        self::check_can_manage_data_registry(\context_system::instance()->id);
+        if (!data_registry::defaults_set()) {
+            return false;
+        }
+
+        return data_registry::get_effective_contextlevel_value($contextlevel, 'category', $forcedvalue);
+    }
+
+    /**
+     * Returns the effective purpose given a context level.
+     *
+     * @param int $contextlevel
+     * @param int $forcedvalue Use this purposeid value as if this was this context level purpose.
+     * @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;
+        }
+
+        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.
+     *
+     * @param \tool_dataprivacy\expired_context $expiredctx
+     * @param int $status
+     * @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();
+    }
+
+    /**
+     * Adds the contexts from the contextlist_collection to the request with the status provided.
+     *
+     * @param contextlist_collection $clcollection a collection of contextlists for all components.
+     * @param int $requestid the id of the request.
+     * @param int $status the status to set the contexts to.
+     */
+    public static function add_request_contexts_with_status(contextlist_collection $clcollection, int $requestid, int $status) {
+        foreach ($clcollection as $contextlist) {
+            // Convert the \core_privacy\local\request\contextlist into a contextlist persistent and store it.
+            $clp = \tool_dataprivacy\contextlist::from_contextlist($contextlist);
+            $clp->create();
+            $contextlistid = $clp->get('id');
+
+            // Store the associated contexts in the contextlist.
+            foreach ($contextlist->get_contextids() as $contextid) {
+                $context = new contextlist_context();
+                $context->set('contextid', $contextid)
+                    ->set('contextlistid', $contextlistid)
+                    ->set('status', $status)
+                    ->create();
+            }
+
+            // Create the relation to the request.
+            $requestcontextlist = request_contextlist::create_relation($requestid, $contextlistid);
+            $requestcontextlist->create();
+        }
+    }
+
+    /**
+     * Sets the status of all contexts associated with the request.
+     *
+     * @param int $requestid the requestid to which the contexts belong.
+     * @param int $status the status to set to.
+     * @throws \dml_exception if the requestid is invalid.
+     * @throws \moodle_exception if the status is invalid.
+     */
+    public static function update_request_contexts_with_status(int $requestid, int $status) {
+        // Validate contextlist_context status using the persistent's attribute validation.
+        $contextlistcontext = new contextlist_context();
+        $contextlistcontext->set('status', $status);
+        if (array_key_exists('status', $contextlistcontext->get_errors())) {
+            throw new moodle_exception("Invalid contextlist_context status: $status");
+        }
+
+        // Validate requestid using the persistent's record validation.
+        // A dml_exception is thrown if the record is missing.
+        $datarequest = new data_request($requestid);
+
+        // Bulk update the status of the request contexts.
+        global $DB;
+
+        $select = "SELECT ctx.id as id
+                     FROM {" . request_contextlist::TABLE . "} rcl
+                     JOIN {" . contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
+                     JOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid
+                    WHERE rcl.requestid = ?";
+
+        // Fetch records IDs to be updated and update by chunks, if applicable (limit of 1000 records per update).
+        $limit = 1000;
+        $idstoupdate = $DB->get_fieldset_sql($select, [$requestid]);
+        $count = count($idstoupdate);
+        $idchunks = $idstoupdate;
+        if ($count > $limit) {
+            $idchunks = array_chunk($idstoupdate, $limit);
+        }
+        $transaction = $DB->start_delegated_transaction();
+        $initialparams = [$status];
+        foreach ($idchunks as $chunk) {
+            list($insql, $inparams) = $DB->get_in_or_equal($chunk);
+            $update = "UPDATE {" . contextlist_context::TABLE . "}
+                          SET status = ?
+                        WHERE id $insql";
+            $params = array_merge($initialparams, $inparams);
+            $DB->execute($update, $params);
+        }
+        $transaction->allow_commit();
+    }
+
+    /**
+     * Finds all request contextlists having at least on approved context, and returns them as in a contextlist_collection.
+     *
+     * @param data_request $request the data request with which the contextlists are associated.
+     * @return contextlist_collection the collection of approved_contextlist objects.
+     */
+    public static function get_approved_contextlist_collection_for_request(data_request $request) : contextlist_collection {
+        $foruser = core_user::get_user($request->get('userid'));
+
+        // Fetch all approved contextlists and create the core_privacy\local\request\contextlist objects here.
+        global $DB;
+        $sql = "SELECT cl.component, ctx.contextid
+                  FROM {" . request_contextlist::TABLE . "} rcl
+                  JOIN {" . contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
+                  JOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid
+                 WHERE rcl.requestid = ?
+                   AND ctx.status = ?
+              ORDER BY cl.component, ctx.contextid";
+
+        // Create the approved contextlist collection object.
+        $lastcomponent = null;
+        $approvedcollection = new contextlist_collection($foruser->id);
+
+        $rs = $DB->get_recordset_sql($sql, [$request->get('id'), contextlist_context::STATUS_APPROVED]);
+        foreach ($rs as $record) {
+            // If we encounter a new component, and we've built up contexts for the last, then add the approved_contextlist for the
+            // last (the one we've just finished with) and reset the context array for the next one.
+            if ($lastcomponent != $record->component) {
+                if (!empty($contexts)) {
+                    $approvedcollection->add_contextlist(new approved_contextlist($foruser, $lastcomponent, $contexts));
+                }
+                $contexts = [];
+            }
+            $contexts[] = $record->contextid;
+            $lastcomponent = $record->component;
+        }
+        $rs->close();
+
+        // The data for the last component contextlist won't have been written yet, so write it now.
+        if (!empty($contexts)) {
+            $approvedcollection->add_contextlist(new approved_contextlist($foruser, $lastcomponent, $contexts));
+        }
+
+        return $approvedcollection;
+    }
+}
diff --git a/admin/tool/dataprivacy/classes/category.php b/admin/tool/dataprivacy/classes/category.php
new file mode 100644 (file)
index 0000000..8f334d5
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class for loading/storing data categories from the DB.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy;
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
+
+/**
+ * Class for loading/storing data categories from the DB.
+ *
+ * @copyright  2018 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class category extends \core\persistent {
+
+    /**
+     * Database table.
+     */
+    const TABLE = 'tool_dataprivacy_category';
+
+    /**
+     * Return the definition of the properties of this model.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return array(
+            'name' => array(
+                'type' => PARAM_TEXT,
+                'description' => 'The category name.',
+            ),
+            'description' => array(
+                'type' => PARAM_RAW,
+                'description' => 'The category description.',
+                'null' => NULL_ALLOWED,
+                'default' => '',
+            ),
+            'descriptionformat' => array(
+                'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN),
+                'type' => PARAM_INT,
+                'default' => FORMAT_HTML
+            ),
+        );
+    }
+
+    /**
+     * Is this category used?.
+     *
+     * @return null
+     */
+    public function is_used() {
+
+        if (\tool_dataprivacy\contextlevel::is_category_used($this->get('id')) ||
+                \tool_dataprivacy\context_instance::is_category_used($this->get('id'))) {
+            return true;
+        }
+
+        $pluginconfig = get_config('tool_dataprivacy');
+        $levels = \context_helper::get_all_levels();
+        foreach ($levels as $level => $classname) {
+
+            list($unused, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
+            if (!empty($pluginconfig->{$categoryvar}) && $pluginconfig->{$categoryvar} == $this->get('id')) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/admin/tool/dataprivacy/classes/context_instance.php b/admin/tool/dataprivacy/classes/context_instance.php
new file mode 100644 (file)
index 0000000..cd7e4f9
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class for loading/storing context instances data from the DB.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class for loading/storing context instances data from the DB.
+ *
+ * @copyright  2018 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class context_instance extends \core\persistent {
+
+    /**
+     * Database table.
+     */
+    const TABLE = 'tool_dataprivacy_ctxinstance';
+
+    /**
+     * Not set value.
+     */
+    const NOTSET = 0;
+
+    /**
+     * Inherit value.
+     */
+    const INHERIT = -1;
+
+    /**
+     * Return the definition of the properties of this model.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return array(
+            'contextid' => array(
+                'type' => PARAM_INT,
+                'description' => 'The context id.',
+            ),
+            'purposeid' => array(
+                'type' => PARAM_INT,
+                'description' => 'The purpose id.',
+                'null' => NULL_ALLOWED,
+            ),
+            'categoryid' => array(
+                'type' => PARAM_INT,
+                'description' => 'The category id.',
+                'null' => NULL_ALLOWED,
+            ),
+        );
+    }
+
+    /**
+     * Returns an instance by contextid.
+     *
+     * @param mixed $contextid
+     * @param mixed $exception
+     * @return null
+     */
+    public static function get_record_by_contextid($contextid, $exception = true) {
+        global $DB;
+
+        if (!$record = $DB->get_record(self::TABLE, array('contextid' => $contextid))) {
+            if (!$exception) {
+                return false;
+            } else {
+                throw new \dml_missing_record_exception(self::TABLE);
+            }
+        }
+
+        return new static(0, $record);
+    }
+
+    /**
+     * Is the provided purpose used by any context instance?
+     *
+     * @param int $purposeid
+     * @return bool
+     */
+    public static function is_purpose_used($purposeid) {
+        global $DB;
+        return $DB->record_exists(self::TABLE, array('purposeid' => $purposeid));
+    }
+
+    /**
+     * Is the provided category used by any context instance?
+     *
+     * @param int $categoryid
+     * @return bool
+     */
+    public static function is_category_used($categoryid) {
+        global $DB;
+        return $DB->record_exists(self::TABLE, array('categoryid' => $categoryid));
+    }
+}
diff --git a/admin/tool/dataprivacy/classes/contextlevel.php b/admin/tool/dataprivacy/classes/contextlevel.php
new file mode 100644 (file)
index 0000000..97af962
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class for loading/storing context level data from the DB.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class for loading/storing context level data from the DB.
+ *
+ * @copyright  2018 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class contextlevel extends \core\persistent {
+
+    /**
+     * Database table.
+     */
+    const TABLE = 'tool_dataprivacy_ctxlevel';
+
+    /**
+     * Return the definition of the properties of this model.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return array(
+            'contextlevel' => array(
+                'type' => PARAM_INT,
+                'description' => 'The context level.',
+            ),
+            'purposeid' => array(
+                'type' => PARAM_INT,
+                'description' => 'The purpose id.',
+            ),
+            'categoryid' => array(
+                'type' => PARAM_INT,
+                'description' => 'The category id.',
+            ),
+        );
+    }
+
+    /**
+     * Returns an instance by contextlevel.
+     *
+     * @param mixed $contextlevel
+     * @param mixed $exception
+     * @return null
+     */
+    public static function get_record_by_contextlevel($contextlevel, $exception = true) {
+        global $DB;
+
+        $cache = \cache::make('tool_dataprivacy', 'contextlevel');
+        if ($data = $cache->get($contextlevel)) {
+            return new static(0, $data);
+        }
+
+        if (!$record = $DB->get_record(self::TABLE, array('contextlevel' => $contextlevel))) {
+            if (!$exception) {
+                return false;
+            } else {
+                throw new \dml_missing_record_exception(self::TABLE);
+            }
+        }
+
+        return new static(0, $record);
+    }
+
+    /**
+     * Is the provided purpose used by any contextlevel?
+     *
+     * @param int $purposeid
+     * @return bool
+     */
+    public static function is_purpose_used($purposeid) {
+        global $DB;
+        return $DB->record_exists(self::TABLE, array('purposeid' => $purposeid));
+    }
+
+    /**
+     * Is the provided category used by any contextlevel?
+     *
+     * @param int $categoryid
+     * @return bool
+     */
+    public static function is_category_used($categoryid) {
+        global $DB;
+        return $DB->record_exists(self::TABLE, array('categoryid' => $categoryid));
+    }
+
+    /**
+     * Adds the new record to the cache.
+     *
+     * @return null
+     */
+    protected function after_create() {
+        $cache = \cache::make('tool_dataprivacy', 'contextlevel');
+        $cache->set($this->get('contextlevel'), $this->to_record());
+    }
+
+    /**
+     * Updates the cache record.
+     *
+     * @param bool $result
+     * @return null
+     */
+    protected function after_update($result) {
+        $cache = \cache::make('tool_dataprivacy', 'contextlevel');
+        $cache->set($this->get('contextlevel'), $this->to_record());
+    }
+
+    /**
+     * Removes unnecessary stuff from db.
+     *
+     * @return null
+     */
+    protected function before_delete() {
+        $cache = \cache::make('tool_dataprivacy', 'contextlevel');
+        $cache->delete($this->get('contextlevel'));
+    }
+}
diff --git a/admin/tool/dataprivacy/classes/contextlist.php b/admin/tool/dataprivacy/classes/contextlist.php
new file mode 100644 (file)
index 0000000..ef25c11
--- /dev/null
@@ -0,0 +1,64 @@
+<?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/>.
+
+/**
+ * Contains the contextlist persistent.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\persistent;
+
+/**
+ * The contextlist persistent.
+ *
+ * @copyright  2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class contextlist extends persistent {
+
+    /** The table name this persistent object maps to. */
+    const TABLE = 'tool_dataprivacy_contextlist';
+
+    /**
+     * Return the definition of the properties of this model.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return [
+            'component' => [
+                'type' => PARAM_TEXT
+            ]
+        ];
+    }
+
+    /**
+     * Create a new contextlist persistent from an instance of \core_privacy\local\request\contextlist.
+     *
+     * @param \core_privacy\local\request\contextlist $contextlist the core privacy contextlist.
+     * @return contextlist a contextlist persistent.
+     */
+    public static function from_contextlist(\core_privacy\local\request\contextlist $contextlist) : contextlist {
+        $contextlistpersistent = new contextlist();
+        return $contextlistpersistent->set('component', $contextlist->get_component());
+    }
+}
diff --git a/admin/tool/dataprivacy/classes/contextlist_context.php b/admin/tool/dataprivacy/classes/contextlist_context.php
new file mode 100644 (file)
index 0000000..0f4ca9f
--- /dev/null
@@ -0,0 +1,74 @@
+<?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/>.
+
+/**
+ * Contains the contextlist_context persistent.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\persistent;
+
+/**
+ * The contextlist_context persistent.
+ *
+ * @copyright  2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class contextlist_context extends persistent {
+
+    /** The table name this persistent object maps to. */
+    const TABLE = 'tool_dataprivacy_ctxlst_ctx';
+
+    /** This context is pending approval. */
+    const STATUS_PENDING = 0;
+
+    /** This context has been approved. */
+    const STATUS_APPROVED = 1;
+
+    /** This context has been rejected. */
+    const STATUS_REJECTED = 2;
+
+    /**
+     * Return the definition of the properties of this model.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return [
+            'contextid' => [
+                'type' => PARAM_INT
+            ],
+            'contextlistid' => [
+                'type' => PARAM_INT
+            ],
+            'status' => [
+                'choices' => [
+                    self::STATUS_PENDING,
+                    self::STATUS_APPROVED,
+                    self::STATUS_REJECTED,
+                ],
+                'default' => self::STATUS_PENDING,
+                'type' => PARAM_INT
+            ]
+        ];
+    }
+}
diff --git a/admin/tool/dataprivacy/classes/data_registry.php b/admin/tool/dataprivacy/classes/data_registry.php
new file mode 100644 (file)
index 0000000..cdf9de1
--- /dev/null
@@ -0,0 +1,353 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Data registry business logic methods. Mostly internal stuff.
+ *
+ * All methods should be considered part of the internal tool_dataprivacy API
+ * unless something different is specified.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_dataprivacy;
+
+use coding_exception;
+use tool_dataprivacy\purpose;
+use tool_dataprivacy\category;
+use tool_dataprivacy\contextlevel;
+use tool_dataprivacy\context_instance;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/coursecatlib.php');
+
+/**
+ * Data registry business logic methods. Mostly internal stuff.
+ *
+ * @copyright  2018 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class data_registry {
+
+    /**
+     * @var array Inheritance between context levels.
+     */
+    private static $contextlevelinheritance = [
+        CONTEXT_USER => [CONTEXT_SYSTEM],
+        CONTEXT_COURSECAT => [CONTEXT_SYSTEM],
+        CONTEXT_COURSE => [CONTEXT_COURSECAT, CONTEXT_SYSTEM],
+        CONTEXT_MODULE => [CONTEXT_COURSE, CONTEXT_COURSECAT, CONTEXT_SYSTEM],
+        CONTEXT_BLOCK => [CONTEXT_COURSE, CONTEXT_COURSECAT, CONTEXT_SYSTEM],
+    ];
+
+    /**
+     * Returns purpose and category var names from a context class name
+     *
+     * @param string $classname
+     * @return string[]
+     */
+    public static function var_names_from_context($classname) {
+        return [
+            $classname . '_purpose',
+            $classname . '_category',
+        ];
+    }
+
+    /**
+     * Returns the default purpose id and category id for the provided context level.
+     *
+     * The caller code is responsible of checking that $contextlevel is an integer.
+     *
+     * @param int $contextlevel
+     * @return int|false[]
+     */
+    public static function get_defaults($contextlevel) {
+
+        $classname = \context_helper::get_class_for_level($contextlevel);
+        list($purposevar, $categoryvar) = self::var_names_from_context($classname);
+
+        $purposeid = get_config('tool_dataprivacy', $purposevar);
+        $categoryid = get_config('tool_dataprivacy', $categoryvar);
+
+        if (empty($purposeid)) {
+            $purposeid = false;
+        }
+        if (empty($categoryid)) {
+            $categoryid = false;
+        }
+
+        return [$purposeid, $categoryid];
+    }
+
+    /**
+     * Are data registry defaults set?
+     *
+     * At least the system defaults need to be set.
+     *
+     * @return bool
+     */
+    public static function defaults_set() {
+        list($purposeid, $categoryid) = self::get_defaults(CONTEXT_SYSTEM);
+        if (empty($purposeid) || empty($categoryid)) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Returns all site categories that are visible to the current user.
+     *
+     * @return \coursecat[]
+     */
+    public static function get_site_categories() {
+        global $DB;
+
+        if (method_exists('\coursecat', 'get_all')) {
+            $categories = \coursecat::get_all(['returnhidden' => true]);
+        } else {
+            // Fallback (to be removed once this gets integrated into master).
+            $ids = $DB->get_fieldset_select('course_categories', 'id', '');
+            $categories = \coursecat::get_many($ids);
+        }
+
+        foreach ($categories as $key => $category) {
+            if (!$category->is_uservisible()) {
+                unset($categories[$key]);
+            }
+        }
+        return $categories;
+    }
+
+    /**
+     * Returns the roles assigned to the provided level.
+     *
+     * Important to note that it returns course-level assigned roles
+     * if the provided context level is below course.
+     *
+     * @param \context $context
+     * @return array
+     */
+    public static function get_subject_scope(\context $context) {
+
+        if ($contextcourse = $context->get_course_context(false)) {
+            // Below course level we only look at course-assigned roles.
+            $roles = get_user_roles($contextcourse, 0, false);
+        } else {
+            $roles = get_user_roles($context, 0, false);
+        }
+
+        return array_map(function($role) {
+            if ($role->name) {
+                return $role->name;
+            } else {
+                return $role->shortname;
+            }
+        }, $roles);
+    }
+
+    /**
+     * Returns the effective value given a context instance
+     *
+     * @param \context $context
+     * @param string $element 'category' or 'purpose'
+     * @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) {
+
+        if ($element !== 'purpose' && $element !== 'category') {
+            throw new coding_exception('Only \'purpose\' and \'category\' are supported.');
+        }
+        $fieldname = $element . 'id';
+
+        if ($forcedvalue === false) {
+            $instance = context_instance::get_record_by_contextid($context->id, false);
+
+            if (!$instance) {
+                // If the instance does not have a value defaults to not set, so we grab the context level default as its value.
+                $instancevalue = context_instance::NOTSET;
+            } else {
+                $instancevalue = $instance->get($fieldname);
+            }
+        } else {
+            $instancevalue = $forcedvalue;
+        }
+
+        // Not set.
+        if ($instancevalue == context_instance::NOTSET) {
+
+            // 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);
+            }
+        }
+
+        // Specific value for this context instance.
+        if ($instancevalue != context_instance::INHERIT) {
+            return self::get_element_instance($element, $instancevalue);
+        }
+
+        // This context is using inherited so let's return the parent effective value.
+        $parentcontext = $context->get_parent_context();
+        if (!$parentcontext) {
+            return false;
+        }
+
+        // The forced value should not be transmitted to parent contexts.
+        return self::get_effective_context_value($parentcontext, $element);
+    }
+
+    /**
+     * Returns the effective value for a context level.
+     *
+     * Note that this is different from the effective default context level
+     * (see get_effective_default_contextlevel_purpose_and_category) as this is returning
+     * the value set in the data registry, not in the defaults page.
+     *
+     * @param int $contextlevel
+     * @param string $element 'category' or 'purpose'
+     * @param int $forcedvalue Use this value as if this was this context level purpose.
+     * @return \tool_dataprivacy\purpose|false
+     */
+    public static function get_effective_contextlevel_value($contextlevel, $element, $forcedvalue = false) {
+
+        if ($element !== 'purpose' && $element !== 'category') {
+            throw new coding_exception('Only \'purpose\' and \'category\' are supported.');
+        }
+        $fieldname = $element . 'id';
+
+        if ($contextlevel != CONTEXT_SYSTEM && $contextlevel != CONTEXT_USER) {
+            throw new \coding_exception('Only context_system and context_user values can be retrieved, no other context levels ' .
+                'have a purpose or a category.');
+        }
+
+        if ($forcedvalue === false) {
+            $instance = contextlevel::get_record_by_contextlevel($contextlevel, false);
+            if (!$instance) {
+                // If the context level does not have a value defaults to not set, so we grab the context level default as
+                // its value.
+                $instancevalue = context_instance::NOTSET;
+            } else {
+                $instancevalue = $instance->get($fieldname);
+            }
+        } else {
+            $instancevalue = $forcedvalue;
+        }
+
+        // Not set -> Use the default context level value.
+        if ($instancevalue == context_instance::NOTSET) {
+            list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category($contextlevel);
+            return self::get_element_instance($element, $$fieldname);
+        }
+
+        // Specific value for this context instance.
+        if ($instancevalue != context_instance::INHERIT) {
+            return self::get_element_instance($element, $instancevalue);
+        }
+