Merge branch 'MDL-62528-master' of git://github.com/bmbrands/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 11 Sep 2018 21:02:40 +0000 (23:02 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 11 Sep 2018 21:02:40 +0000 (23:02 +0200)
601 files changed:
admin/environment.xml
admin/roles/classes/define_role_table_advanced.php
admin/tests/behat/enable_multiple_accounts_use_same_email.feature
admin/tool/availabilityconditions/tests/behat/manage_conditions.feature
admin/tool/behat/tests/behat/data_generators.feature
admin/tool/behat/tests/behat/get_and_set_fields.feature
admin/tool/behat/tests/behat/list_steps.feature
admin/tool/cohortroles/lang/en/tool_cohortroles.php
admin/tool/customlang/lang/en/tool_customlang.php
admin/tool/dataprivacy/amd/build/categoriesactions.min.js
admin/tool/dataprivacy/amd/build/purposesactions.min.js
admin/tool/dataprivacy/amd/src/categoriesactions.js
admin/tool/dataprivacy/amd/src/purposesactions.js
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/data_registry.php
admin/tool/dataprivacy/classes/data_request.php
admin/tool/dataprivacy/classes/external/data_request_exporter.php
admin/tool/dataprivacy/classes/local/helper.php
admin/tool/dataprivacy/classes/metadata_registry.php
admin/tool/dataprivacy/classes/output/data_registry_page.php
admin/tool/dataprivacy/classes/output/data_requests_table.php
admin/tool/dataprivacy/classes/output/my_data_requests_page.php
admin/tool/dataprivacy/classes/task/delete_expired_requests.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/task/process_data_request_task.php
admin/tool/dataprivacy/datadeletion.php
admin/tool/dataprivacy/dataregistry.php
admin/tool/dataprivacy/datarequests.php
admin/tool/dataprivacy/db/install.xml
admin/tool/dataprivacy/db/tasks.php
admin/tool/dataprivacy/db/upgrade.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/lib.php
admin/tool/dataprivacy/mydatarequests.php
admin/tool/dataprivacy/pluginregistry.php
admin/tool/dataprivacy/settings.php
admin/tool/dataprivacy/styles.css
admin/tool/dataprivacy/templates/categories.mustache
admin/tool/dataprivacy/templates/component_status.mustache
admin/tool/dataprivacy/templates/data_registry_compliance.mustache
admin/tool/dataprivacy/templates/my_data_requests.mustache
admin/tool/dataprivacy/templates/purposes.mustache
admin/tool/dataprivacy/tests/api_test.php
admin/tool/dataprivacy/tests/behat/datadelete.feature [new file with mode: 0644]
admin/tool/dataprivacy/tests/behat/dataexport.feature
admin/tool/dataprivacy/tests/behat/manage_categories.feature [new file with mode: 0644]
admin/tool/dataprivacy/tests/behat/manage_purposes.feature [new file with mode: 0644]
admin/tool/dataprivacy/tests/data_privacy_testcase.php [new file with mode: 0644]
admin/tool/dataprivacy/tests/expired_data_requests_test.php [new file with mode: 0644]
admin/tool/dataprivacy/tests/manager_observer_test.php
admin/tool/dataprivacy/version.php
admin/tool/filetypes/tests/behat/add_filetypes.feature
admin/tool/httpsreplace/tests/behat/httpsreplace.feature
admin/tool/langimport/tests/behat/manage_langpacks.feature
admin/tool/log/store/database/lang/en/logstore_database.php
admin/tool/log/store/legacy/lang/en/logstore_legacy.php
admin/tool/log/store/standard/lang/en/logstore_standard.php
admin/tool/messageinbound/lang/en/tool_messageinbound.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/monitor/tests/behat/disabled.feature
admin/tool/monitor/tests/behat/rule.feature
admin/tool/monitor/tests/behat/subscription.feature
admin/tool/policy/amd/build/policyactions.min.js
admin/tool/policy/amd/src/policyactions.js
admin/tool/policy/classes/form/accept_policy.php
admin/tool/policy/classes/output/page_viewalldoc.php
admin/tool/policy/lang/en/tool_policy.php
admin/tool/policy/lib.php
admin/tool/policy/styles.css
admin/tool/policy/templates/page_viewalldoc.mustache
admin/tool/policy/tests/behat/acceptances.feature
admin/tool/policy/tests/behat/consent.feature
admin/tool/policy/tests/behat/managepolicies.feature
admin/tool/policy/viewall.php
admin/tool/recyclebin/tests/behat/backup_user_data.feature
admin/tool/recyclebin/tests/behat/basic_functionality.feature
admin/tool/recyclebin/tests/category_bin_test.php
admin/tool/task/tests/behat/clear_fail_delay.feature
admin/tool/task/tests/behat/manage_tasks.feature
admin/tool/task/tests/behat/run_task_now.feature
admin/tool/uploadcourse/classes/helper.php
admin/tool/uploadcourse/classes/step2_form.php
admin/tool/uploadcourse/cli/uploadcourse.php
admin/tool/uploadcourse/index.php
admin/tool/uploadcourse/tests/behat/create.feature
admin/tool/uploadcourse/tests/behat/update.feature
admin/tool/uploaduser/tests/behat/upload_users.feature
admin/tool/usertours/classes/local/filter/category.php
admin/tool/usertours/lang/en/tool_usertours.php
admin/tool/usertours/tests/behat/behat_tool_usertours.php
auth/ldap/lang/en/auth_ldap.php
auth/tests/behat/validateagedigitalconsentmap.feature
availability/condition/completion/tests/behat/conditional_bug.feature
availability/condition/profile/tests/behat/availability_profile.feature
backup/util/ui/tests/behat/import_groups.feature
backup/util/ui/tests/behat/restore_moodle2_courses.feature
backup/util/ui/tests/behat/restore_moodle2_courses_settings.feature
badges/criteria/award_criteria_courseset.php
badges/tests/behat/add_badge.feature
badges/tests/behat/award_badge.feature
badges/tests/behat/criteria_cohort.feature
badges/tests/behat/criteria_profile.feature
badges/tests/behat/role_visibility.feature
blocks/admin_bookmarks/tests/behat/bookmark_admin_pages.feature
blocks/badges/tests/behat/block_badges_course.feature
blocks/badges/tests/behat/block_badges_dashboard.feature
blocks/badges/tests/behat/block_badges_frontpage.feature
blocks/blog_menu/tests/behat/block_blog_menu_frontpage.feature
blocks/blog_recent/tests/behat/block_blog_recent_frontpage.feature
blocks/calendar_month/tests/behat/block_calendar_month.feature
blocks/calendar_upcoming/tests/behat/block_calendar_upcoming_frontpage.feature
blocks/comments/tests/behat/block_comment_frontpage.feature
blocks/completionstatus/tests/behat/block_completionstatus.feature
blocks/completionstatus/tests/behat/block_completionstatus_activity_completion.feature
blocks/completionstatus/tests/behat/block_completionstatus_manual_other.feature
blocks/completionstatus/tests/behat/block_completionstatus_manual_self.feature
blocks/course_list/block_course_list.php
blocks/course_list/tests/behat/block_course_list_frontpage.feature
blocks/course_summary/tests/behat/block_course_summary_frontpage.feature
blocks/login/tests/behat/login_block.feature
blocks/myoverview/amd/build/event_list.min.js
blocks/myoverview/amd/src/event_list.js
blocks/myoverview/classes/output/courses_view.php
blocks/myoverview/templates/timeline-view-courses.mustache
blocks/myprofile/tests/behat/block_myprofile_frontpage.feature
blocks/navigation/tests/behat/expand_courses_node.feature
blocks/online_users/tests/behat/block_online_users_frontpage.feature
blocks/participants/tests/behat/block_participants_frontpage.feature
blocks/private_files/tests/behat/block_private_files_frontpage.feature
blocks/recent_activity/classes/task/cleanup.php
blocks/rss_client/block_rss_client.php
blocks/rss_client/classes/task/refreshfeeds.php
blocks/rss_client/tests/cron_test.php
blocks/search_forums/tests/behat/block_search_forums_course.feature
blocks/search_forums/tests/behat/block_search_forums_frontpage.feature
blocks/site_main_menu/tests/behat/add_url.feature
blocks/site_main_menu/tests/behat/edit_activities.feature
blocks/tag_flickr/tests/behat/configuring_tag_flickr_block.feature
blocks/tests/behat/configure_block_throughout_site.feature
blog/lib.php
blog/tests/behat/blog_visibility.feature
blog/tests/behat/comment.feature
blog/tests/behat/delete.feature
cache/classes/helper.php
cache/classes/loaders.php
cache/stores/redis/lib.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
cache/upgrade.txt
calendar/classes/external/event_exporter_base.php
calendar/classes/local/event/container.php
calendar/classes/local/event/data_access/event_vault.php
calendar/classes/local/event/factories/event_abstract_factory.php
calendar/classes/local/event/forms/eventtype.php
calendar/classes/local/event/proxies/coursecat_proxy.php
calendar/externallib.php
calendar/lib.php
calendar/managesubscriptions.php
calendar/templates/event_item.mustache
calendar/tests/behat/category_events.feature
calendar/tests/calendar_information_test.php
calendar/tests/coursecat_proxy_test.php
cohort/edit.php
cohort/edit_form.php
cohort/index.php
cohort/lib.php
cohort/tests/behat/access_visible_cohorts.feature
cohort/tests/behat/add_cohort.feature
cohort/tests/behat/behat_cohort.php
cohort/tests/behat/upload_cohort_users.feature
cohort/tests/behat/upload_cohorts.feature
cohort/tests/behat/view_cohorts.feature
cohort/upload_form.php
completion/classes/api.php
completion/tests/behat/behat_completion.php
completion/tests/behat/completion_no_calendar_capabilities.feature [new file with mode: 0644]
completion/tests/behat/enable_manual_complete_mark.feature
completion/tests/capabilities_test.php [new file with mode: 0644]
config-dist.php
course/ajax/management.php
course/amd/build/actions.min.js
course/amd/src/actions.js
course/changenumsections.php
course/classes/category.php [new file with mode: 0644]
course/classes/deletecategory_form.php
course/classes/editcategory_form.php
course/classes/list_element.php [new file with mode: 0644]
course/classes/management/helper.php
course/classes/management_renderer.php
course/completion_form.php
course/edit_form.php
course/editcategory.php
course/externallib.php
course/format/lib.php
course/format/renderer.php
course/format/topics/db/upgradelib.php
course/format/topics/tests/format_topics_upgrade_test.php
course/format/weeks/db/upgradelib.php
course/format/weeks/tests/format_weeks_upgrade_test.php
course/index.php
course/lib.php
course/management.php
course/renderer.php
course/request_form.php
course/search.php
course/tests/behat/behat_course.php
course/tests/behat/course_category_management_listing.feature
course/tests/behat/coursetags.feature
course/tests/behat/keyholder.feature
course/tests/behat/max_number_sections.feature
course/tests/behat/role_renaming.feature
course/tests/category_test.php [moved from lib/tests/coursecatlib_test.php with 80% similarity]
course/tests/courselib_test.php
course/tests/management_helper_test.php
enrol/flatfile/lang/en/enrol_flatfile.php
enrol/guest/tests/behat/guest_access.feature
enrol/imsenterprise/lib.php
enrol/imsenterprise/tests/imsenterprise_test.php
enrol/lti/classes/manage_table.php
enrol/lti/lang/en/enrol_lti.php
enrol/lti/tests/behat/basic_settings.feature
enrol/lti/tests/behat/index_page.feature
enrol/meta/tests/behat/enrol_meta.feature
enrol/self/classes/deleteselectedusers_operation.php
enrol/self/tests/behat/key_holder.feature
enrol/tests/behat/behat_enrol.php
enrol/tests/behat/enrol_user.feature
enrol/tests/behat/role_visibility.feature
files/classes/privacy/provider.php
files/tests/behat/add_custom_file_type.feature
files/tests/behat/course_files.feature
filter/algebra/algebradebug.php
filter/tex/lib.php
filter/tex/texdebug.php
grade/edit/scale/index.php
grade/edit/tree/lib.php
grade/grading/classes/privacy/gradingform_legacy_polyfill.php
grade/grading/classes/privacy/gradingform_provider.php
grade/grading/classes/privacy/gradingform_provider_v2.php [new file with mode: 0644]
grade/grading/classes/privacy/provider.php
grade/grading/form/guide/classes/privacy/provider.php
grade/grading/form/guide/lang/en/gradingform_guide.php
grade/grading/form/guide/tests/privacy_test.php
grade/grading/form/rubric/classes/privacy/provider.php
grade/grading/form/rubric/lang/en/gradingform_rubric.php
grade/grading/form/rubric/tests/privacy_test.php [new file with mode: 0644]
grade/grading/form/upgrade.txt
grade/grading/tests/fixtures/marking_guide.php [new file with mode: 0644]
grade/grading/tests/privacy_legacy_polyfill_test.php
grade/grading/tests/privacy_test.php
grade/lib.php
grade/report/grader/index.php
grade/report/history/index.php
grade/report/lib.php
grade/report/overview/index.php
grade/report/overview/lib.php
grade/report/user/index.php
grade/tests/behat/grade_UI_settings.feature
grade/tests/behat/grade_average.feature
grade/tests/behat/grade_calculated_weights.feature
grade/tests/behat/grade_category_validation.feature
grade/tests/behat/grade_hidden_items.feature
grade/tests/behat/grade_item_validation.feature
grade/tests/behat/grade_letter_logging.feature
grade/tests/behat/grade_natural_normalisation.feature
grade/tests/behat/grade_point_maximum.feature
grade/tests/behat/grade_scales.feature
grade/tests/behat/grade_scales_aggregation.feature
grade/tests/behat/grade_scales_logging.feature
grade/tests/behat/grade_single_item_scales.feature
grade/upgrade.txt [new file with mode: 0644]
group/tests/behat/overview.feature
group/tests/behat/role_visibility.feature
install/lang/ca/admin.php
install/lang/ca/install.php
install/lang/el/admin.php
install/lang/el/error.php
install/lang/el/install.php
install/lang/gl/error.php
lang/en/admin.php
lang/en/auth.php
lang/en/backup.php
lang/en/badges.php
lang/en/blog.php
lang/en/cohort.php
lang/en/competency.php
lang/en/enrol.php
lang/en/error.php
lang/en/files.php
lang/en/grades.php
lang/en/message.php
lang/en/moodle.php
lang/en/portfolio.php
lang/en/webservice.php
lib/accesslib.php
lib/amd/build/ajax.min.js
lib/amd/build/form-autocomplete.min.js
lib/amd/build/modal.min.js
lib/amd/src/ajax.js
lib/amd/src/form-autocomplete.js
lib/amd/src/modal.js
lib/bennu/iCalendar_components.php
lib/bennu/iCalendar_properties.php
lib/bennu/readme_moodle.txt
lib/classes/event/course_category_deleted.php
lib/classes/external/coursecat_summary_exporter.php
lib/classes/session/redis.php
lib/coursecatlib.php
lib/datalib.php
lib/db/renamedclasses.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/editor/atto/lib.php
lib/editor/atto/tests/behat/autosave.feature
lib/enrollib.php
lib/externallib.php
lib/filebrowser/tests/file_browser_test.php
lib/filelib.php
lib/form/tests/behat/modgrade_validation.feature
lib/grade/grade_category.php
lib/grade/grade_object.php
lib/grade/grade_outcome.php
lib/grade/grade_scale.php
lib/messagelib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/pear/HTML/QuickForm.php
lib/pear/HTML/QuickForm/element.php
lib/pear/HTML/QuickForm/hierselect.php
lib/pear/HTML/QuickForm/utils.php [new file with mode: 0644]
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/tests/advanced_test.php
lib/questionlib.php
lib/templates/form_autocomplete_input.mustache
lib/templates/paging_bar.mustache [moved from theme/boost/templates/core/paging_bar.mustache with 85% similarity]
lib/testing/generator/data_generator.php
lib/tests/behat/alpha_chooser.feature
lib/tests/behat/behat_deprecated.php
lib/tests/behat/behat_filters.php
lib/tests/behat/behat_navigation.php
lib/tests/behat/behat_permissions.php
lib/tests/behat/timezone.feature
lib/tests/filelib_test.php
lib/tests/gradelib_test.php
lib/tests/messagelib_test.php
lib/tests/moodle_url_test.php [new file with mode: 0644]
lib/tests/questionlib_test.php
lib/tests/tablelib_test.php
lib/tests/weblib_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/weblib.php
lib/xhprof/xhprof_moodle.php
media/player/videojs/tests/behat/modules.feature
message/classes/api.php
message/output/airnotifier/lang/en/message_airnotifier.php
message/tests/behat/update_messaging_preferences.feature
message/tests/externallib_test.php
mnet/service/enrol/lang/en/mnetservice_enrol.php
mod/assign/classes/privacy/provider.php
mod/assign/feedback/editpdf/lib.php
mod/assign/feedback/editpdf/tests/behat/annotate_pdf.feature
mod/assign/feedback/file/lib.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/mod_form.php
mod/assign/submission/onlinetext/locallib.php
mod/assign/tests/behat/assign_course_reset.feature
mod/assign/tests/behat/assign_no_calendar_capabilities.feature [new file with mode: 0644]
mod/assign/tests/behat/group_submission.feature
mod/assign/tests/behat/outcome_grading.feature
mod/assign/tests/behat/quickgrading.feature
mod/assign/tests/lib_test.php
mod/chat/lib.php
mod/chat/tests/behat/chat_calendar_events.feature
mod/chat/tests/behat/chat_course_reset.feature
mod/chat/tests/behat/chat_no_calendar_capabilities.feature [new file with mode: 0644]
mod/chat/tests/lib_test.php
mod/choice/locallib.php
mod/choice/tests/behat/choice_no_calendar_capabilities.feature [new file with mode: 0644]
mod/choice/tests/lib_test.php
mod/data/edit.php
mod/data/export.php
mod/data/field.php
mod/data/field/radiobutton/field.class.php
mod/data/locallib.php
mod/data/preset.php
mod/data/templates.php
mod/data/tests/behat/behat_mod_data.php
mod/data/tests/behat/data_no_calendar_capabilities.feature [new file with mode: 0644]
mod/data/tests/lib_test.php
mod/feedback/classes/complete_form.php
mod/feedback/classes/external.php
mod/feedback/item/multichoice/lib.php
mod/feedback/lib.php
mod/feedback/tests/behat/feedback_no_calendar_capabilities.feature [new file with mode: 0644]
mod/feedback/tests/external_test.php
mod/feedback/tests/lib_test.php
mod/feedback/upgrade.txt
mod/forum/classes/output/email/renderer.php
mod/forum/classes/output/emaildigestbasic/renderer_textemail.php
mod/forum/classes/output/emaildigestfull/renderer_textemail.php
mod/forum/classes/output/forum_post.php
mod/forum/tests/behat/advanced_search.feature
mod/forum/tests/mail_test.php
mod/lesson/index.php
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/tests/behat/lesson_course_reset.feature
mod/lesson/tests/behat/lesson_no_calendar_capabilities.feature [new file with mode: 0644]
mod/lesson/tests/lib_test.php
mod/lesson/tests/locallib_test.php
mod/lti/lang/en/lti.php
mod/lti/locallib.php
mod/lti/tests/behat/addtool.feature
mod/lti/tests/behat/backup_restore.feature
mod/lti/tests/behat/contentitem.feature
mod/lti/tests/behat/contentitemregistration.feature
mod/lti/tests/behat/toolconfigure.feature
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/tests/behat/backup.feature
mod/quiz/tests/behat/behat_mod_quiz.php
mod/quiz/tests/behat/completion_condition_attempts_used.feature
mod/quiz/tests/behat/completion_condition_passing_grade.feature
mod/quiz/tests/behat/editing_add.feature
mod/quiz/tests/behat/editing_add_from_question_bank.feature
mod/quiz/tests/behat/editing_add_random.feature
mod/quiz/tests/behat/quiz_no_calendar_capabilities.feature [new file with mode: 0644]
mod/quiz/tests/behat/quiz_reset.feature
mod/quiz/tests/lib_test.php
mod/scorm/locallib.php
mod/scorm/tests/behat/scorm_no_calendar_capabilities.feature [new file with mode: 0644]
mod/scorm/tests/lib_test.php
mod/wiki/tests/behat/reset_wiki_comments_tags_files.feature
mod/workshop/amd/build/modform.min.js [new file with mode: 0644]
mod/workshop/amd/src/modform.js [new file with mode: 0644]
mod/workshop/backup/moodle1/lib.php
mod/workshop/backup/moodle2/backup_workshop_stepslib.php
mod/workshop/backup/moodle2/restore_workshop_stepslib.php
mod/workshop/classes/external/workshop_summary_exporter.php
mod/workshop/db/install.xml
mod/workshop/db/upgrade.php
mod/workshop/lang/en/workshop.php
mod/workshop/lib.php
mod/workshop/locallib.php
mod/workshop/mod_form.php
mod/workshop/submission_form.php
mod/workshop/tests/behat/delete_submission.feature
mod/workshop/tests/behat/export_submission.feature
mod/workshop/tests/behat/file_type_restriction.feature
mod/workshop/tests/behat/submission_types.feature [new file with mode: 0644]
mod/workshop/tests/behat/workshop_assessment.feature
mod/workshop/tests/external_test.php
mod/workshop/upgrade.txt
mod/workshop/version.php
my/tests/behat/reset_all_pages.feature
pluginfile.php
privacy/classes/local/deprecated.php [new file with mode: 0644]
privacy/classes/local/request/writer.php
question/format.php
question/format/gift/tests/behat/import_export.feature
question/format/webct/tests/behat/import.feature
question/format/webct/tests/behat/importcalculated.feature
question/format/xml/tests/behat/import_export.feature
question/import.php
question/tests/behat/copy_questions.feature
question/tests/behat/delete_questions.feature
question/tests/behat/edit_questions.feature
question/tests/behat/edit_questions_standard_tags.feature
question/tests/behat/filter_questions_by_tag.feature
question/tests/behat/move_question_categories.feature
question/tests/behat/preview_question.feature
question/tests/behat/question_categories.feature
question/tests/behat/sort_questions.feature
question/type/ddimageortext/tests/behat/add.feature
question/type/ddimageortext/tests/behat/backup_and_restore.feature
question/type/ddimageortext/tests/behat/edit.feature
question/type/ddimageortext/tests/behat/export.feature
question/type/ddimageortext/tests/behat/import.feature
question/type/ddimageortext/tests/behat/preview.feature
question/type/ddmarker/tests/behat/add.feature
question/type/ddmarker/tests/behat/backup_and_restore.feature
question/type/ddmarker/tests/behat/edit.feature
question/type/ddmarker/tests/behat/export.feature
question/type/ddmarker/tests/behat/import.feature
question/type/ddmarker/tests/behat/preview.feature
question/type/ddwtos/questiontype.php
question/type/ddwtos/tests/behat/add.feature
question/type/ddwtos/tests/behat/backup_and_restore.feature
question/type/ddwtos/tests/behat/edit.feature
question/type/ddwtos/tests/behat/export.feature
question/type/ddwtos/tests/behat/import.feature
question/type/ddwtos/tests/behat/preview.feature
question/type/ddwtos/tests/questiontype_test.php
question/type/description/tests/behat/add.feature
question/type/description/tests/behat/backup_and_restore.feature
question/type/description/tests/behat/edit.feature
question/type/description/tests/behat/export.feature
question/type/description/tests/behat/import.feature
question/type/description/tests/behat/preview.feature
question/type/essay/tests/behat/add.feature
question/type/essay/tests/behat/backup_and_restore.feature
question/type/essay/tests/behat/edit.feature
question/type/essay/tests/behat/export.feature
question/type/essay/tests/behat/import.feature
question/type/essay/tests/behat/preview.feature
question/type/gapselect/tests/behat/basic_test.feature
question/type/gapselect/tests/behat/import_test.feature
question/type/match/tests/behat/add.feature
question/type/match/tests/behat/backup_and_restore.feature
question/type/match/tests/behat/edit.feature
question/type/match/tests/behat/export.feature
question/type/match/tests/behat/import.feature
question/type/match/tests/behat/preview.feature
question/type/multichoice/tests/behat/add.feature
question/type/multichoice/tests/behat/backup_and_restore.feature
question/type/multichoice/tests/behat/edit.feature
question/type/multichoice/tests/behat/export.feature
question/type/multichoice/tests/behat/import.feature
question/type/multichoice/tests/behat/preview.feature
question/type/shortanswer/tests/behat/add.feature
question/type/shortanswer/tests/behat/backup_and_restore.feature
question/type/shortanswer/tests/behat/edit.feature
question/type/shortanswer/tests/behat/export.feature
question/type/shortanswer/tests/behat/import.feature
question/type/shortanswer/tests/behat/preview.feature
question/type/truefalse/tests/behat/add.feature
question/type/truefalse/tests/behat/backup_and_restore.feature
question/type/truefalse/tests/behat/edit.feature
question/type/truefalse/tests/behat/export.feature
question/type/truefalse/tests/behat/import.feature
question/type/truefalse/tests/behat/preview.feature
report/eventlist/tests/behat/mainsection.feature
report/log/tests/behat/filter_log.feature
report/log/tests/behat/filter_log_actions.feature
report/log/tests/behat/user_log.feature
report/loglive/tests/behat/loglive_report.feature
report/outline/tests/behat/filter.feature
report/outline/tests/behat/outline.feature
report/outline/tests/behat/user.feature
report/participation/tests/behat/filter_participation.feature
report/participation/tests/behat/message_participants.feature
report/progress/tests/behat/activity_completion_report.feature
repository/url/lang/en/repository_url.php
tag/classes/tag.php
tag/tests/behat/collections.feature
tag/tests/behat/delete_tag.feature
tag/tests/behat/edit_tag.feature
tag/tests/behat/flag_tags.feature
tag/tests/behat/standard_tags.feature
tag/tests/behat/tagindex.feature
tag/tests/taglib_test.php
theme/boost/classes/output/core_course/management/renderer.php
theme/boost/classes/output/core_renderer.php
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/sticky-footer.scss
theme/boost/style/moodle.css
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
theme/boost/templates/core/form_autocomplete_input.mustache
theme/boost/templates/core/navbar.mustache
theme/boost/templates/core_form/element-radio.mustache
theme/boost/templates/maintenance.mustache
theme/boost/templates/secure.mustache
theme/boost/tests/behat/behat_theme_boost_behat_deprecated.php [new file with mode: 0644]
theme/boost/tests/behat/behat_theme_boost_behat_mod_quiz.php
theme/boost/tests/behat/behat_theme_boost_behat_navigation.php
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/style/moodle.css
theme/upgrade.txt
tokenpluginfile.php [new file with mode: 0644]
user/externallib.php
user/filters/courserole.php
user/lib.php
user/profile/field/checkbox/lang/en/profilefield_checkbox.php
user/profile/field/datetime/lang/en/profilefield_datetime.php
user/profile/field/menu/lang/en/profilefield_menu.php
user/profile/field/text/lang/en/profilefield_text.php
user/profile/field/textarea/lang/en/profilefield_textarea.php
user/tests/behat/addnewuser.feature
user/tests/behat/custom_profile_fields.feature
user/tests/behat/delete_users.feature
user/tests/behat/edit_user_enrolment.feature
user/tests/behat/edituserpassword.feature
user/tests/behat/enrol_cohort_list.feature
user/tests/behat/filter_participants.feature
user/tests/behat/name_fields.feature
user/tests/behat/table_sorting.feature
user/tests/behat/user_grade_navigation.feature
user/tests/behat/view_full_profile.feature
user/tests/behat/view_preferences_page.feature
user/tests/externallib_test.php
user/tests/userlib_test.php
version.php
webservice/lib.php

index 2b8d4ef..fbda3f3 100644 (file)
       </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
+  <MOODLE version="3.6" requires="3.1">
+    <UNICODE level="required">
+      <FEEDBACK>
+        <ON_ERROR message="unicoderequired" />
+      </FEEDBACK>
+    </UNICODE>
+    <DATABASE level="required">
+      <VENDOR name="mariadb" version="5.5.31" />
+      <VENDOR name="mysql" version="5.6" />
+      <VENDOR name="postgres" version="9.4" />
+      <VENDOR name="mssql" version="10.0" />
+      <VENDOR name="oracle" version="11.2" />
+    </DATABASE>
+    <PHP version="7.0.0" level="required">
+    </PHP>
+    <PCREUNICODE level="optional">
+      <FEEDBACK>
+        <ON_CHECK message="pcreunicodewarning" />
+      </FEEDBACK>
+    </PCREUNICODE>
+    <PHP_EXTENSIONS>
+      <PHP_EXTENSION name="iconv" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="iconvrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="mbstring" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="mbstringrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="curl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="curlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="openssl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="opensslrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="tokenizer" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="tokenizerrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlrpc" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrpcrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="soap" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="soaprecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="ctype" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ctyperequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zip" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ziprequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zlib" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="gd" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="gdrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="simplexml" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="simplexmlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="spl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="splrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="pcre" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="dom" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlreader" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="intl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="intlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="json" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="hash" level="required"/>
+      <PHP_EXTENSION name="fileinfo" level="required"/>
+    </PHP_EXTENSIONS>
+    <PHP_SETTINGS>
+      <PHP_SETTING name="memory_limit" value="96M" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="settingmemorylimit" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="file_uploads" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingfileuploads" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="opcache.enable" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opcacherecommended" />
+        </FEEDBACK>
+      </PHP_SETTING>
+    </PHP_SETTINGS>
+    <CUSTOM_CHECKS>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbstorageengine" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="quizattemptsupgradedmessage" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="slashargumentswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unsupporteddbtablerowformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unoconvwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="libcurlwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfileformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfilepertable" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddblargeprefix" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="ishttpswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="incompleteunicodesupport" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="sixtyfourbitswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+    </CUSTOM_CHECKS>
+  </MOODLE>
 </COMPATIBILITY_MATRIX>
index 354ba8c..d5f6c18 100644 (file)
@@ -434,7 +434,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
     }
 
     public function save_changes() {
-        global $DB, $CFG;
+        global $DB;
 
         if (!$this->roleid) {
             // Creating role.
@@ -448,8 +448,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
             // the UI. It would be better to do this only when we know that fields affected are
             // updated. But thats getting into the weeds of the coursecat cache and role edits
             // should not be that frequent, so here is the ugly brutal approach.
-            require_once($CFG->libdir . '/coursecatlib.php');
-            coursecat::role_assignment_changed($this->role->id, context_system::instance());
+            core_course_category::role_assignment_changed($this->role->id, context_system::instance());
         }
 
         // Assignable contexts.
index 30d21b9..65dffa3 100644 (file)
@@ -10,7 +10,7 @@ Feature: Enable multiple accounts to have the same email address
   Scenario: Enable registration of multiple accounts with the same email address
     Given the following config values are set as admin:
       | allowaccountssameemail | 1 |
-    When I navigate to "Add a new user" node in "Site administration>Users>Accounts"
+    When I navigate to "Users > Accounts > Add a new user" in site administration
     And I set the following fields to these values:
       | Username                        | testmultiemailuser1             |
       | Choose an authentication method | Manual accounts                 |
@@ -35,7 +35,7 @@ Feature: Enable multiple accounts to have the same email address
   Scenario: Disable registration of multiple accounts with the same email address
     Given the following config values are set as admin:
       | allowaccountssameemail | 0 |
-    When I navigate to "Add a new user" node in "Site administration>Users>Accounts"
+    When I navigate to "Users > Accounts > Add a new user" in site administration
     And I set the following fields to these values:
       | Username                        | testmultiemailuser1             |
       | Choose an authentication method | Manual accounts                 |
index 97b5b20..8fef761 100644 (file)
@@ -20,7 +20,7 @@ Feature: Manage availability conditions
     And the following config values are set as admin:
       | enableavailability | 1 |
     And I am on homepage
-    And I navigate to "Manage restrictions" node in "Site administration > Plugins > Availability restrictions"
+    And I navigate to "Plugins > Availability restrictions > Manage restrictions" in site administration
 
     # Having clicked on it, I should also see the list of plugins.
     And I should see "Restriction by date"
@@ -34,7 +34,7 @@ Feature: Manage availability conditions
       | Course 1 | C1        | topics |
     And I log in as "admin"
     And I am on site homepage
-    When I navigate to "Manage restrictions" node in "Site administration > Plugins > Availability restrictions"
+    When I navigate to "Plugins > Availability restrictions > Manage restrictions" in site administration
 
     # Check the icon is there (it should be a Hide icon, meaning is currently visible).
     Then "Hide" "icon" should exist in the "Restriction by date" "table_row"
index 0e62104..f078fab 100644 (file)
@@ -286,7 +286,7 @@ Feature: Set up contextual data for tests
       | student1 | CHSB   |
       | student1 | CHC    |
     When I log in as "admin"
-    And I navigate to "Cohorts" node in "Site administration > Users > Accounts"
+    And I navigate to "Users > Accounts > Cohorts" in site administration
     Then the following should exist in the "cohorts" table:
       | Name            | Cohort size |
       | System cohort A | 1           |
index 62a6fe8..9ade5e0 100644 (file)
@@ -34,7 +34,7 @@ Feature: Verify that all form fields values can be get and set
       | wiki | C1 | wiki1 | Test this one | Test this one | Test this one | collaborative | 0 |
     And I log in as "admin"
     And I am on "Course 1" course homepage
-    And I navigate to "Reset" node in "Course administration"
+    And I navigate to "Reset" in current page administration
     # Select (multi-select) - Checking "the select box should contain".
     And I expand all fieldsets
     And the "Unenrol users" select box should contain "No roles"
@@ -131,12 +131,12 @@ Feature: Verify that all form fields values can be get and set
     And the field "two" matches value ""
     # Check if field xpath set/match works.
     And I am on "Course 1" course homepage
-    And I navigate to "Edit settings" node in "Course administration"
+    And I navigate to "Edit settings" in current page administration
     And I set the field with xpath "//input[@id='id_idnumber']" to "Course id number"
     And the field with xpath "//input[@name='idnumber']" matches value "Course id number"
     And the field with xpath "//input[@name='idnumber']" does not match value ""
     And I press "Save and display"
-    And I navigate to "Edit settings" node in "Course administration"
+    And I navigate to "Edit settings" in current page administration
     And the field "Course ID number" matches value "Course id number"
 
   Scenario: with JS disabled all form fields getters and setters works as expected
index 708a0e7..2df5363 100644 (file)
@@ -7,7 +7,7 @@ Feature: List the system steps definitions
   Background:
     Given I am on homepage
     And I log in as "admin"
-    And I navigate to "Acceptance testing" node in "Site administration > Development"
+    And I navigate to "Development > Acceptance testing" in site administration
 
   @javascript
   Scenario: Accessing the list
index a8afe83..2f6eb21 100644 (file)
@@ -43,10 +43,10 @@ $string['selectusers'] = 'Select users to assign role';
 $string['taskname'] = 'Sync cohort role assignments';
 $string['thisuserroles'] = 'Roles assigned relative to this user';
 $string['privacy:metadata:tool_cohortroles'] = 'The Cohort roles management plugin stores user cohort role mappings.';
-$string['privacy:metadata:tool_cohortroles:id'] = 'The ID of the cohort role mapping record.';
-$string['privacy:metadata:tool_cohortroles:cohortid'] = 'The ID of the cohort.';
-$string['privacy:metadata:tool_cohortroles:roleid'] = 'The ID of the role.';
-$string['privacy:metadata:tool_cohortroles:userid'] = 'The ID of the user.';
-$string['privacy:metadata:tool_cohortroles:timecreated'] = 'The date/time of when the cohort  role mapping was created.';
-$string['privacy:metadata:tool_cohortroles:timemodified'] = 'The date/time of when the cohort role mapping was modified.';
-$string['privacy:metadata:tool_cohortroles:usermodified'] = 'The ID of the user who last modified the cohort role mapping.';
+$string['privacy:metadata:tool_cohortroles:id'] = 'The ID of the cohort role mapping record';
+$string['privacy:metadata:tool_cohortroles:cohortid'] = 'The ID of the cohort';
+$string['privacy:metadata:tool_cohortroles:roleid'] = 'The ID of the role';
+$string['privacy:metadata:tool_cohortroles:userid'] = 'The ID of the user';
+$string['privacy:metadata:tool_cohortroles:timecreated'] = 'The time when the cohort role mapping was created';
+$string['privacy:metadata:tool_cohortroles:timemodified'] = 'The time when the cohort role mapping was modified';
+$string['privacy:metadata:tool_cohortroles:usermodified'] = 'The ID of the user who last modified the cohort role mapping';
index 7192622..9e7a2d6 100644 (file)
@@ -30,7 +30,7 @@ $string['checkin'] = 'Save strings to language pack';
 $string['checkout'] = 'Open language pack for editing';
 $string['checkoutdone'] = 'Language pack loaded';
 $string['checkoutinprogress'] = 'Loading language pack';
-$string['confirmcheckin'] = 'You are about to save modifications to your local language pack. This will export the customised strings from the translator into you Moodle data directory and Moodle will start using the modified strings. Press \'Continue\' to proceed with saving.';
+$string['confirmcheckin'] = 'You are about to save modifications to your local language pack. This will export the customised strings from the translator into your site data directory and your site will start using the modified strings. Press \'Continue\' to proceed with saving.';
 $string['customlang:edit'] = 'Edit local translation';
 $string['customlang:view'] = 'View local translation';
 $string['filter'] = 'Filter strings';
index 66e3b0a..cac33a4 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/categoriesactions.min.js and b/admin/tool/dataprivacy/amd/build/categoriesactions.min.js differ
index 0b15981..33b773c 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/purposesactions.min.js and b/admin/tool/dataprivacy/amd/build/purposesactions.min.js differ
index c40a1a7..6d05977 100644 (file)
@@ -58,25 +58,28 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents) {
             var stringkeys = [
                 {
                     key: 'deletecategory',
-                    component: 'tool_dataprivacy',
-                    param: categoryname
+                    component: 'tool_dataprivacy'
                 },
                 {
                     key: 'deletecategorytext',
                     component: 'tool_dataprivacy',
                     param: categoryname
+                },
+                {
+                    key: 'delete'
                 }
             ];
 
             Str.get_strings(stringkeys).then(function(langStrings) {
                 var title = langStrings[0];
                 var confirmMessage = langStrings[1];
+                var buttonText = langStrings[2];
                 return ModalFactory.create({
                     title: title,
                     body: confirmMessage,
                     type: ModalFactory.types.SAVE_CANCEL
                 }).then(function(modal) {
-                    modal.setSaveButtonText(title);
+                    modal.setSaveButtonText(buttonText);
 
                     // Handle save event.
                     modal.getRoot().on(ModalEvents.save, function() {
index fd92141..05abf71 100644 (file)
@@ -58,25 +58,28 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents) {
             var stringkeys = [
                 {
                     key: 'deletepurpose',
-                    component: 'tool_dataprivacy',
-                    param: purposename
+                    component: 'tool_dataprivacy'
                 },
                 {
                     key: 'deletepurposetext',
                     component: 'tool_dataprivacy',
                     param: purposename
+                },
+                {
+                    key: 'delete'
                 }
             ];
 
             Str.get_strings(stringkeys).then(function(langStrings) {
                 var title = langStrings[0];
                 var confirmMessage = langStrings[1];
+                var buttonText = langStrings[2];
                 return ModalFactory.create({
                     title: title,
                     body: confirmMessage,
                     type: ModalFactory.types.SAVE_CANCEL
                 }).then(function(modal) {
-                    modal.setSaveButtonText(title);
+                    modal.setSaveButtonText(buttonText);
 
                     // Handle save event.
                     modal.getRoot().on(ModalEvents.save, function() {
index 8ec0bb5..6ee9707 100644 (file)
@@ -76,7 +76,7 @@ class api {
     /** The request is now being processed. */
     const DATAREQUEST_STATUS_PROCESSING = 4;
 
-    /** Data request completed. */
+    /** Information/other request completed. */
     const DATAREQUEST_STATUS_COMPLETE = 5;
 
     /** Data request cancelled by the user. */
@@ -85,6 +85,15 @@ class api {
     /** Data request rejected by the DPO. */
     const DATAREQUEST_STATUS_REJECTED = 7;
 
+    /** Data request download ready. */
+    const DATAREQUEST_STATUS_DOWNLOAD_READY = 8;
+
+    /** Data request expired. */
+    const DATAREQUEST_STATUS_EXPIRED = 9;
+
+    /** Data delete request completed, account is removed. */
+    const DATAREQUEST_STATUS_DELETED = 10;
+
     /**
      * Determines whether the user can contact the site's Data Protection Officer via Moodle.
      *
@@ -127,6 +136,25 @@ class api {
         require_capability('tool/dataprivacy:managedataregistry', $context);
     }
 
+    /**
+     * Fetches the role shortnames of Data Protection Officer roles.
+     *
+     * @return array An array of the DPO role shortnames
+     */
+    public static function get_dpo_role_names() : array {
+        global $DB;
+
+        $dporoleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
+        $dponames = array();
+
+        if (!empty($dporoleids)) {
+            list($insql, $inparams) = $DB->get_in_or_equal($dporoleids);
+            $dponames = $DB->get_fieldset_select('role', 'shortname', "id {$insql}", $inparams);
+        }
+
+        return $dponames;
+    }
+
     /**
      * Fetches the list of users with the Data Protection Officer role.
      *
@@ -300,6 +328,18 @@ class api {
             }
         }
 
+        // If any are due to expire, expire them and re-fetch updated data.
+        if (empty($statuses)
+                || in_array(self::DATAREQUEST_STATUS_DOWNLOAD_READY, $statuses)
+                || in_array(self::DATAREQUEST_STATUS_EXPIRED, $statuses)) {
+            $expiredrequests = data_request::get_expired_requests($userid);
+
+            if (!empty($expiredrequests)) {
+                data_request::expire($expiredrequests);
+                $results = self::get_data_requests($userid, $statuses, $types, $sort, $offset, $limit);
+            }
+        }
+
         return $results;
     }
 
@@ -381,6 +421,9 @@ class api {
             self::DATAREQUEST_STATUS_COMPLETE,
             self::DATAREQUEST_STATUS_CANCELLED,
             self::DATAREQUEST_STATUS_REJECTED,
+            self::DATAREQUEST_STATUS_DOWNLOAD_READY,
+            self::DATAREQUEST_STATUS_EXPIRED,
+            self::DATAREQUEST_STATUS_DELETED,
         ];
         list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED);
         $select = 'type = :type AND userid = :userid AND status NOT ' . $insql;
@@ -404,6 +447,9 @@ class api {
             self::DATAREQUEST_STATUS_COMPLETE,
             self::DATAREQUEST_STATUS_CANCELLED,
             self::DATAREQUEST_STATUS_REJECTED,
+            self::DATAREQUEST_STATUS_DOWNLOAD_READY,
+            self::DATAREQUEST_STATUS_EXPIRED,
+            self::DATAREQUEST_STATUS_DELETED,
         ];
 
         return !in_array($status, $finalstatuses);
index dbc8f32..1fac2f5 100644 (file)
@@ -35,8 +35,6 @@ use tool_dataprivacy\context_instance;
 
 defined('MOODLE_INTERNAL') || die();
 
-require_once($CFG->libdir . '/coursecatlib.php');
-
 /**
  * Data registry business logic methods. Mostly internal stuff.
  *
@@ -113,17 +111,17 @@ class data_registry {
     /**
      * Returns all site categories that are visible to the current user.
      *
-     * @return \coursecat[]
+     * @return \core_course_category[]
      */
     public static function get_site_categories() {
         global $DB;
 
-        if (method_exists('\coursecat', 'get_all')) {
-            $categories = \coursecat::get_all(['returnhidden' => true]);
+        if (method_exists('\core_course_category', 'get_all')) {
+            $categories = \core_course_category::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);
+            $categories = \core_course_category::get_many($ids);
         }
 
         foreach ($categories as $key => $category) {
index d5ab218..92c8c1f 100644 (file)
@@ -85,6 +85,9 @@ class data_request extends persistent {
                     api::DATAREQUEST_STATUS_COMPLETE,
                     api::DATAREQUEST_STATUS_CANCELLED,
                     api::DATAREQUEST_STATUS_REJECTED,
+                    api::DATAREQUEST_STATUS_DOWNLOAD_READY,
+                    api::DATAREQUEST_STATUS_EXPIRED,
+                    api::DATAREQUEST_STATUS_DELETED,
                 ],
                 'type' => PARAM_INT
             ],
@@ -110,4 +113,101 @@ class data_request extends persistent {
             ],
         ];
     }
+
+    /**
+     * Determines whether a completed data export request has expired.
+     * The response will be valid regardless of the expiry scheduled task having run.
+     *
+     * @param data_request $request the data request object whose expiry will be checked.
+     * @return bool true if the request has expired.
+     */
+    public static function is_expired(data_request $request) {
+        $result = false;
+
+        // Only export requests expire.
+        if ($request->get('type') == api::DATAREQUEST_TYPE_EXPORT) {
+            switch ($request->get('status')) {
+                // Expired requests are obviously expired.
+                case api::DATAREQUEST_STATUS_EXPIRED:
+                    $result = true;
+                    break;
+                // Complete requests are expired if the expiry time has elapsed.
+                case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
+                    $expiryseconds = get_config('tool_dataprivacy', 'privacyrequestexpiry');
+                    if ($expiryseconds > 0 && time() >= ($request->get('timemodified') + $expiryseconds)) {
+                        $result = true;
+                    }
+                    break;
+            }
+        }
+
+        return $result;
+    }
+
+
+
+    /**
+     * Fetch completed data requests which are due to expire.
+     *
+     * @param int $userid Optional user ID to filter by.
+     *
+     * @return array Details of completed requests which are due to expire.
+     */
+    public static function get_expired_requests($userid = 0) {
+        global $DB;
+
+        $expiryseconds = get_config('tool_dataprivacy', 'privacyrequestexpiry');
+        $expirytime = strtotime("-{$expiryseconds} second");
+        $table = self::TABLE;
+        $sqlwhere = 'type = :export_type AND status = :completestatus AND timemodified <= :expirytime';
+        $params = array(
+            'export_type' => api::DATAREQUEST_TYPE_EXPORT,
+            'completestatus' => api::DATAREQUEST_STATUS_DOWNLOAD_READY,
+            'expirytime' => $expirytime,
+        );
+        $sort = 'id';
+        $fields = 'id, userid';
+
+        // Filter by user ID if specified.
+        if ($userid > 0) {
+            $sqlwhere .= ' AND (userid = :userid OR requestedby = :requestedby)';
+            $params['userid'] = $userid;
+            $params['requestedby'] = $userid;
+        }
+
+        return $DB->get_records_select_menu($table, $sqlwhere, $params, $sort, $fields, 0, 2000);
+    }
+
+    /**
+     * Expire a given set of data requests.
+     * Update request status and delete the files.
+     *
+     * @param array $expiredrequests [requestid => userid]
+     *
+     * @return void
+     */
+    public static function expire($expiredrequests) {
+        global $DB;
+
+        $ids = array_keys($expiredrequests);
+
+        if (count($ids) > 0) {
+            list($insql, $inparams) = $DB->get_in_or_equal($ids);
+            $initialparams = array(api::DATAREQUEST_STATUS_EXPIRED, time());
+            $params = array_merge($initialparams, $inparams);
+
+            $update = "UPDATE {" . self::TABLE . "}
+                          SET status = ?, timemodified = ?
+                        WHERE id $insql";
+
+            if ($DB->execute($update, $params)) {
+                $fs = get_file_storage();
+
+                foreach ($expiredrequests as $id => $userid) {
+                    $usercontext = \context_user::instance($userid);
+                    $fs->delete_area_files($usercontext->id, 'tool_dataprivacy', 'export', $id);
+                }
+            }
+        }
+    }
 }
index 93b33e3..b7d483c 100644 (file)
@@ -160,7 +160,7 @@ class data_request_exporter extends persistent_exporter {
 
         switch ($this->persistent->get('status')) {
             case api::DATAREQUEST_STATUS_PENDING:
-                $values['statuslabelclass'] = 'label-default';
+                $values['statuslabelclass'] = 'label-info';
                 // Request can be manually completed for general enquiry requests.
                 $values['canmarkcomplete'] = $requesttype == api::DATAREQUEST_TYPE_OTHERS;
                 break;
@@ -181,6 +181,8 @@ class data_request_exporter extends persistent_exporter {
                 $values['statuslabelclass'] = 'label-info';
                 break;
             case api::DATAREQUEST_STATUS_COMPLETE:
+            case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
+            case api::DATAREQUEST_STATUS_DELETED:
                 $values['statuslabelclass'] = 'label-success';
                 break;
             case api::DATAREQUEST_STATUS_CANCELLED:
@@ -189,6 +191,9 @@ class data_request_exporter extends persistent_exporter {
             case api::DATAREQUEST_STATUS_REJECTED:
                 $values['statuslabelclass'] = 'label-important';
                 break;
+            case api::DATAREQUEST_STATUS_EXPIRED:
+                $values['statuslabelclass'] = 'label-default';
+                break;
         }
 
         return $values;
index f98362d..36dd93a 100644 (file)
@@ -117,6 +117,7 @@ class helper {
         if (!isset($statuses[$status])) {
             throw new moodle_exception('errorinvalidrequeststatus', 'tool_dataprivacy');
         }
+
         return $statuses[$status];
     }
 
@@ -133,8 +134,11 @@ class helper {
             api::DATAREQUEST_STATUS_APPROVED => get_string('statusapproved', 'tool_dataprivacy'),
             api::DATAREQUEST_STATUS_PROCESSING => get_string('statusprocessing', 'tool_dataprivacy'),
             api::DATAREQUEST_STATUS_COMPLETE => get_string('statuscomplete', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_DOWNLOAD_READY => get_string('statusready', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_EXPIRED => get_string('statusexpired', 'tool_dataprivacy'),
             api::DATAREQUEST_STATUS_CANCELLED => get_string('statuscancelled', 'tool_dataprivacy'),
             api::DATAREQUEST_STATUS_REJECTED => get_string('statusrejected', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_DELETED => get_string('statusdeleted', 'tool_dataprivacy'),
         ];
     }
 
index 6282cc4..e29174a 100644 (file)
@@ -70,11 +70,22 @@ class metadata_registry {
                     $internaldata['compliant'] = false;
                 }
                 // Check to see if we are an external plugin.
-                $componentshortname = explode('_', $component);
+                // Plugin names can contain _ characters, limit to 2 to just remove initial plugintype.
+                $componentshortname = explode('_', $component, 2);
                 $shortname = array_pop($componentshortname);
                 if (isset($contributedplugins[$plugintype][$shortname])) {
                     $internaldata['external'] = true;
                 }
+
+                // Check if the interface is deprecated.
+                if (!$manager->is_empty_subsystem($component)) {
+                    $classname = $manager->get_provider_classname_for_component($component);
+                    $componentclass = new $classname();
+                    if ($componentclass instanceof \core_privacy\local\deprecated) {
+                        $internaldata['deprecated'] = true;
+                    }
+                }
+
                 return $internaldata;
             }, $leaves['plugins']);
             $fullyrichtree[$branch]['plugin_type_raw'] = $plugintype;
index cfdad43..8d5fd35 100644 (file)
@@ -30,7 +30,6 @@ use stdClass;
 use templatable;
 use tool_dataprivacy\data_registry;
 
-require_once($CFG->libdir . '/coursecatlib.php');
 require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
 require_once($CFG->libdir . '/blocklib.php');
 
@@ -226,7 +225,7 @@ class data_registry_page implements renderable, templatable {
             throw new \coding_exception('A course category context should be provided');
         }
 
-        $coursecat = \coursecat::get($catcontext->instanceid);
+        $coursecat = \core_course_category::get($catcontext->instanceid);
         $courses = $coursecat->get_courses();
 
         $branches = [];
index 51bb133..477e503 100644 (file)
@@ -59,7 +59,7 @@ class data_requests_table extends table_sql {
     /** @var bool Whether this table is being rendered for managing data requests. */
     protected $manage = false;
 
-    /** @var stdClass[] Array of data request persistents. */
+    /** @var \tool_dataprivacy\data_request[] Array of data request persistents. */
     protected $datarequests = [];
 
     /**
@@ -206,20 +206,21 @@ class data_requests_table extends table_sql {
                 $actiontext = get_string('denyrequest', 'tool_dataprivacy');
                 $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
                 break;
-        }
-
-        if ($status == api::DATAREQUEST_STATUS_COMPLETE) {
-            $userid = $data->foruser->id;
-            $usercontext = \context_user::instance($userid, IGNORE_MISSING);
-            if ($usercontext && api::can_download_data_request_for_user($userid, $data->requestedbyuser->id)) {
-                $actions[] = api::get_download_link($usercontext, $requestid);
-            }
+            case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
+                $userid = $data->foruser->id;
+                $usercontext = \context_user::instance($userid, IGNORE_MISSING);
+                // If user has permission to view download link, show relevant action item.
+                if ($usercontext && api::can_download_data_request_for_user($userid, $data->requestedbyuser->id)) {
+                    $actions[] = api::get_download_link($usercontext, $requestid);
+                }
+                break;
         }
 
         $actionsmenu = new action_menu($actions);
         $actionsmenu->set_menu_trigger(get_string('actions'));
         $actionsmenu->set_owner_selector('request-actions-' . $requestid);
         $actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
+        $actionsmenu->set_constraint('[data-region=data-requests-table] > .no-overflow');
 
         return $OUTPUT->render($actionsmenu);
     }
@@ -235,19 +236,25 @@ class data_requests_table extends table_sql {
     public function query_db($pagesize, $useinitialsbar = true) {
         global $PAGE;
 
-        // Count data requests from the given conditions.
-        $total = api::get_data_requests_count($this->userid, $this->statuses, $this->types);
-        $this->pagesize($pagesize, $total);
+        // Set dummy page total until we fetch full result set.
+        $this->pagesize($pagesize, $pagesize + 1);
 
         $sort = $this->get_sql_sort();
 
         // Get data requests from the given conditions.
         $datarequests = api::get_data_requests($this->userid, $this->statuses, $this->types, $sort,
                 $this->get_page_start(), $this->get_page_size());
+
+        // Count data requests from the given conditions.
+        $total = api::get_data_requests_count($this->userid, $this->statuses, $this->types);
+        $this->pagesize($pagesize, $total);
+
         $this->rawdata = [];
         $context = \context_system::instance();
         $renderer = $PAGE->get_renderer('tool_dataprivacy');
+
         foreach ($datarequests as $persistent) {
+            $this->datarequests[$persistent->get('id')] = $persistent;
             $exporter = new data_request_exporter($persistent, ['context' => $context]);
             $this->rawdata[] = $exporter->export($renderer);
         }
index d82968c..729a7fe 100644 (file)
@@ -109,13 +109,29 @@ class my_data_requests_page implements renderable, templatable {
                     $item->statuslabelclass = 'label-success';
                     $item->statuslabel = get_string('statuscomplete', 'tool_dataprivacy');
                     $cancancel = false;
-                    // Show download links only for export-type data requests.
-                    $candownload = $type == api::DATAREQUEST_TYPE_EXPORT;
+                    break;
+                case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
+                    $item->statuslabelclass = 'label-success';
+                    $item->statuslabel = get_string('statusready', 'tool_dataprivacy');
+                    $cancancel = false;
+                    $candownload = true;
+
                     if ($usercontext) {
                         $candownload = api::can_download_data_request_for_user(
                                 $request->get('userid'), $request->get('requestedby'));
                     }
                     break;
+                case api::DATAREQUEST_STATUS_DELETED:
+                    $item->statuslabelclass = 'label-success';
+                    $item->statuslabel = get_string('statusdeleted', 'tool_dataprivacy');
+                    $cancancel = false;
+                    break;
+                case api::DATAREQUEST_STATUS_EXPIRED:
+                    $item->statuslabelclass = 'label-default';
+                    $item->statuslabel = get_string('statusexpired', 'tool_dataprivacy');
+                    $item->statuslabeltitle = get_string('downloadexpireduser', 'tool_dataprivacy');
+                    $cancancel = false;
+                    break;
                 case api::DATAREQUEST_STATUS_CANCELLED:
                 case api::DATAREQUEST_STATUS_REJECTED:
                     $cancancel = false;
diff --git a/admin/tool/dataprivacy/classes/task/delete_expired_requests.php b/admin/tool/dataprivacy/classes/task/delete_expired_requests.php
new file mode 100644 (file)
index 0000000..1ed3ac8
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Scheduled task to delete files and update statuses of expired data requests.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Michael Hawkins
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_dataprivacy\task;
+
+use coding_exception;
+use core\task\scheduled_task;
+use tool_dataprivacy\api;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
+
+/**
+ * Scheduled task to delete files and update request statuses once they expire.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Michael Hawkins
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class delete_expired_requests extends scheduled_task {
+
+    /**
+     * Returns the task name.
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('deleteexpireddatarequeststask', 'tool_dataprivacy');
+    }
+
+    /**
+     * Run the task to delete expired data request files and update request statuses.
+     *
+     */
+    public function execute() {
+        $expiredrequests = \tool_dataprivacy\data_request::get_expired_requests();
+        $deletecount = count($expiredrequests);
+
+        if ($deletecount > 0) {
+            \tool_dataprivacy\data_request::expire($expiredrequests);
+
+            mtrace($deletecount . ' expired completed data requests have been deleted');
+        }
+    }
+}
index 6db9252..c44b661 100644 (file)
@@ -81,6 +81,7 @@ class process_data_request_task extends adhoc_task {
         // Update the status of this request as pre-processing.
         mtrace('Processing request...');
         api::update_request_status($requestid, api::DATAREQUEST_STATUS_PROCESSING);
+        $completestatus = api::DATAREQUEST_STATUS_COMPLETE;
 
         if ($request->type == api::DATAREQUEST_TYPE_EXPORT) {
             // Get the collection of approved_contextlist objects needed for core_privacy data export.
@@ -105,7 +106,7 @@ class process_data_request_task extends adhoc_task {
             $filerecord->author    = fullname($foruser);
             // Save somewhere.
             $thing = $fs->create_file_from_pathname($filerecord, $exportedcontent);
-
+            $completestatus = api::DATAREQUEST_STATUS_DOWNLOAD_READY;
         } else if ($request->type == api::DATAREQUEST_TYPE_DELETE) {
             // Get the collection of approved_contextlist objects needed for core_privacy data deletion.
             $approvedclcollection = api::get_approved_contextlist_collection_for_request($requestpersistent);
@@ -115,10 +116,11 @@ class process_data_request_task extends adhoc_task {
             $manager->set_observer(new \tool_dataprivacy\manager_observer());
 
             $manager->delete_data_for_user($approvedclcollection);
+            $completestatus = api::DATAREQUEST_STATUS_DELETED;
         }
 
         // When the preparation of the metadata finishes, update the request status to awaiting approval.
-        api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE);
+        api::update_request_status($requestid, $completestatus);
         mtrace('The processing of the user data request has been completed...');
 
         // Create message to notify the user regarding the processing results.
index f622e5b..c92c676 100644 (file)
@@ -37,13 +37,19 @@ $title = get_string('datadeletion', 'tool_dataprivacy');
 
 echo $OUTPUT->header();
 
-$table = new \tool_dataprivacy\output\expired_contexts_table($filter);
-$table->baseurl = $url;
-$table->baseurl->param('filter', $filter);
-
-$datadeletionpage = new \tool_dataprivacy\output\data_deletion_page($filter, $table);
-
-$output = $PAGE->get_renderer('tool_dataprivacy');
-echo $output->render($datadeletionpage);
+if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
+    $table = new \tool_dataprivacy\output\expired_contexts_table($filter);
+    $table->baseurl = $url;
+    $table->baseurl->param('filter', $filter);
+
+    $datadeletionpage = new \tool_dataprivacy\output\data_deletion_page($filter, $table);
+
+    $output = $PAGE->get_renderer('tool_dataprivacy');
+    echo $output->render($datadeletionpage);
+} else {
+    $dponamestring = implode (',', tool_dataprivacy\api::get_dpo_role_names());
+    $message = get_string('privacyofficeronly', 'tool_dataprivacy', $dponamestring);
+    echo $OUTPUT->notification($message, 'error');
+}
 
 echo $OUTPUT->footer();
index 52eff0a..50c0ab9 100644 (file)
@@ -38,7 +38,12 @@ $title = get_string('dataregistry', 'tool_dataprivacy');
 $output = $PAGE->get_renderer('tool_dataprivacy');
 echo $output->header();
 
-$dataregistry = new tool_dataprivacy\output\data_registry_page($contextlevel, $contextid);
-
-echo $output->render($dataregistry);
+if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
+    $dataregistry = new tool_dataprivacy\output\data_registry_page($contextlevel, $contextid);
+    echo $output->render($dataregistry);
+} else {
+    $dponamestring = implode (', ', tool_dataprivacy\api::get_dpo_role_names());
+    $message = get_string('privacyofficeronly', 'tool_dataprivacy', $dponamestring);
+    echo $OUTPUT->notification($message, 'error');
+}
 echo $OUTPUT->footer();
index 0887134..6a8140d 100644 (file)
@@ -36,39 +36,45 @@ $title = get_string('datarequests', 'tool_dataprivacy');
 echo $OUTPUT->header();
 echo $OUTPUT->heading($title);
 
-$filtersapplied = optional_param_array('request-filters', [-1], PARAM_NOTAGS);
-$filterscleared = optional_param('filters-cleared', 0, PARAM_INT);
-if ($filtersapplied === [-1]) {
-    // If there are no filters submitted, check if there is a saved filters from the user preferences.
-    $filterprefs = get_user_preferences(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, null);
-    if ($filterprefs && empty($filterscleared)) {
-        $filtersapplied = json_decode($filterprefs);
-    } else {
-        $filtersapplied = [];
+if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
+    $filtersapplied = optional_param_array('request-filters', [-1], PARAM_NOTAGS);
+    $filterscleared = optional_param('filters-cleared', 0, PARAM_INT);
+    if ($filtersapplied === [-1]) {
+        // If there are no filters submitted, check if there is a saved filters from the user preferences.
+        $filterprefs = get_user_preferences(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, null);
+        if ($filterprefs && empty($filterscleared)) {
+            $filtersapplied = json_decode($filterprefs);
+        } else {
+            $filtersapplied = [];
+        }
     }
-}
-// Save the current applied filters to the user preferences.
-set_user_preference(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, json_encode($filtersapplied));
+    // Save the current applied filters to the user preferences.
+    set_user_preference(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, json_encode($filtersapplied));
 
-$types = [];
-$statuses = [];
-foreach ($filtersapplied as $filter) {
-    list($category, $value) = explode(':', $filter);
-    switch($category) {
-        case \tool_dataprivacy\local\helper::FILTER_TYPE:
-            $types[] = $value;
-            break;
-        case \tool_dataprivacy\local\helper::FILTER_STATUS:
-            $statuses[] = $value;
-            break;
+    $types = [];
+    $statuses = [];
+    foreach ($filtersapplied as $filter) {
+        list($category, $value) = explode(':', $filter);
+        switch($category) {
+            case \tool_dataprivacy\local\helper::FILTER_TYPE:
+                $types[] = $value;
+                break;
+            case \tool_dataprivacy\local\helper::FILTER_STATUS:
+                $statuses[] = $value;
+                break;
+        }
     }
-}
 
-$table = new \tool_dataprivacy\output\data_requests_table(0, $statuses, $types, true);
-$table->baseurl = $url;
+    $table = new \tool_dataprivacy\output\data_requests_table(0, $statuses, $types, true);
+    $table->baseurl = $url;
 
-$requestlist = new tool_dataprivacy\output\data_requests_page($table, $filtersapplied);
-$requestlistoutput = $PAGE->get_renderer('tool_dataprivacy');
-echo $requestlistoutput->render($requestlist);
+    $requestlist = new tool_dataprivacy\output\data_requests_page($table, $filtersapplied);
+    $requestlistoutput = $PAGE->get_renderer('tool_dataprivacy');
+    echo $requestlistoutput->render($requestlist);
+} else {
+    $dponamestring = implode (', ', tool_dataprivacy\api::get_dpo_role_names());
+    $message = get_string('privacyofficeronly', 'tool_dataprivacy', $dponamestring);
+    echo $OUTPUT->notification($message, 'error');
+}
 
 echo $OUTPUT->footer();
index 65d8926..98e852b 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20180313" COMMENT="XMLDB file for Moodle tool/dataprivacy"
+<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20180821" COMMENT="XMLDB file for Moodle tool/dataprivacy"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
 >
@@ -12,7 +12,7 @@
         <FIELD NAME="commentsformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID the request is being made for"/>
         <FIELD NAME="requestedby" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID of the one making the request"/>
-        <FIELD NAME="status" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The current status of the data request"/>
+        <FIELD NAME="status" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The current status of the data request"/>
         <FIELD NAME="dpo" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID of the Data Protection Officer who is reviewing th request"/>
         <FIELD NAME="dpocomment" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="DPO's comments (e.g. reason for rejecting the request, etc.)"/>
         <FIELD NAME="dpocommentformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
@@ -98,7 +98,7 @@
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="status" TYPE="int" LENGTH="2" DEFAULT="0" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="status" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
       </KEYS>
     </TABLE>
   </TABLES>
-</XMLDB>
+</XMLDB>
\ No newline at end of file
index 3a4915b..5ee3a19 100644 (file)
@@ -42,5 +42,13 @@ $tasks = array(
         'day' => '*',
         'dayofweek' => '*',
         'month' => '*'
+    ), array(
+        'classname' => 'tool_dataprivacy\task\delete_expired_requests',
+        'blocking' => 0,
+        'minute' => 'R',
+        'hour' => 'R',
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
     ),
 );
index 8c0b4fc..743185d 100644 (file)
@@ -145,5 +145,44 @@ function xmldb_tool_dataprivacy_upgrade($oldversion) {
         upgrade_plugin_savepoint(true, 2018051405, 'tool', 'dataprivacy');
     }
 
+    if ($oldversion < 2018051406) {
+        // Update completed delete requests to new delete status.
+        $query = "UPDATE {tool_dataprivacy_request}
+                     SET status = :setstatus
+                   WHERE type = :type
+                         AND status = :wherestatus";
+        $params = array(
+            'setstatus' => 10, // Request deleted.
+            'type' => 2, // Delete type.
+            'wherestatus' => 5, // Request completed.
+        );
+
+        $DB->execute($query, $params);
+
+        // Update completed data export requests to new download ready status.
+        $params = array(
+            'setstatus' => 8, // Request download ready.
+            'type' => 1, // export type.
+            'wherestatus' => 5, // Request completed.
+        );
+
+        $DB->execute($query, $params);
+
+        upgrade_plugin_savepoint(true, 2018051406, 'tool', 'dataprivacy');
+    }
+
+    if ($oldversion < 2018082100) {
+
+        // Changing precision of field status on table tool_dataprivacy_request to (2).
+        $table = new xmldb_table('tool_dataprivacy_request');
+        $field = new xmldb_field('status', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'requestedby');
+
+        // Launch change of precision for field status.
+        $dbman->change_field_precision($table, $field);
+
+        // Dataprivacy savepoint reached.
+        upgrade_plugin_savepoint(true, 2018082100, 'tool', 'dataprivacy');
+    }
+
     return true;
 }
index 4af32a8..eb2c97f 100644 (file)
@@ -77,15 +77,19 @@ $string['datecomment'] = '[{$a->date}]: ' . PHP_EOL . ' {$a->comment}';
 $string['daterequested'] = 'Date requested';
 $string['daterequesteddetail'] = 'Date requested:';
 $string['defaultsinfo'] = 'Default categories and purposes are applied to all newly created instances.';
-$string['deletecategory'] = 'Delete "{$a}" category';
-$string['deletecategorytext'] = 'Are you sure you want to delete "{$a}" category?';
+$string['deletecategory'] = 'Delete category';
+$string['deletecategorytext'] = 'Are you sure you want to delete the category \'{$a}\'?';
 $string['deleteexpiredcontextstask'] = 'Delete expired contexts';
-$string['deletepurpose'] = 'Delete "{$a}" purpose';
-$string['deletepurposetext'] = 'Are you sure you want to delete "{$a}" purpose?';
+$string['deleteexpireddatarequeststask'] = 'Delete files from completed data requests that have expired';
+$string['deletepurpose'] = 'Delete purpose';
+$string['deletepurposetext'] = 'Are you sure you want to delete the purpose \'{$a}\'?';
 $string['defaultssaved'] = 'Defaults saved';
 $string['deny'] = 'Deny';
 $string['denyrequest'] = 'Deny request';
+$string['deprecated'] = 'Deprecated';
+$string['deprecatedexplanation'] = 'This plugin is using an old version of one of the privacy interfaces and should be updated.';
 $string['download'] = 'Download';
+$string['downloadexpireduser'] = 'Download has expired. Submit a new request if you wish to export your personal data.';
 $string['dporolemapping'] = 'Privacy officer role mapping';
 $string['dporolemapping_desc'] = 'The privacy officer can manage data requests. The capability tool/dataprivacy:managedatarequests must be allowed for a role to be listed as a privacy officer role mapping option.';
 $string['editcategories'] = 'Edit categories';
@@ -184,6 +188,7 @@ $string['notset'] = 'Not set (use the default value)';
 $string['pluginregistry'] = 'Plugin privacy registry';
 $string['pluginregistrytitle'] = 'Plugin privacy compliance registry';
 $string['privacy'] = 'Privacy';
+$string['privacyofficeronly'] = 'Only users who are assigned a privacy officer role ({$a}) have access to this content';
 $string['privacy:metadata:preference:tool_dataprivacy_request-filters'] = 'The filters currently applied to the data requests page.';
 $string['privacy:metadata:request'] = 'Information from personal data requests (subject access and deletion requests) made for this site.';
 $string['privacy:metadata:request:comments'] = 'Any user comments accompanying the request.';
@@ -191,6 +196,8 @@ $string['privacy:metadata:request:userid'] = 'The ID of the user to whom the req
 $string['privacy:metadata:request:requestedby'] = 'The ID of the user making the request, if made on behalf of another user.';
 $string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s privacy officer regarding the request.';
 $string['privacy:metadata:request:timecreated'] = 'The timestamp indicating when the request was made by the user.';
+$string['privacyrequestexpiry'] = 'Data request expiry';
+$string['privacyrequestexpiry_desc'] = 'The time that approved data requests will be available for download before expiring. If set to zero, then there is no time limit.';
 $string['protected'] = 'Protected';
 $string['protectedlabel'] = 'The retention of this data has a higher legal precedent over a user\'s request to be forgotten. This data will only be deleted after the retention period has expired.';
 $string['purpose'] = 'Purpose';
@@ -216,7 +223,7 @@ $string['requeststatus'] = 'Status';
 $string['requestsubmitted'] = 'Your request has been submitted to the privacy officer';
 $string['requesttype'] = 'Type';
 $string['requesttypeuser'] = '{$a->typename} ({$a->user})';
-$string['requesttype_help'] = 'Select the reason why you would like to contact the privacy officer';
+$string['requesttype_help'] = 'Select the reason for contacting the privacy officer. Be aware that deletion of all personal  data will result in you no longer being able to log in to the site.';
 $string['requesttypedelete'] = 'Delete all of my personal data';
 $string['requesttypedeleteshort'] = 'Delete';
 $string['requesttypeexport'] = 'Export all of my personal data';
@@ -240,7 +247,10 @@ $string['statusapproved'] = 'Approved';
 $string['statusawaitingapproval'] = 'Awaiting approval';
 $string['statuscancelled'] = 'Cancelled';
 $string['statuscomplete'] = 'Complete';
+$string['statusready'] = 'Download ready';
+$string['statusdeleted'] = 'Deleted';
 $string['statusdetail'] = 'Status:';
+$string['statusexpired'] = 'Expired';
 $string['statuspreprocessing'] = 'Pre-processing';
 $string['statusprocessing'] = 'Processing';
 $string['statuspending'] = 'Pending';
index 73ffc14..fbeb61d 100644 (file)
@@ -199,6 +199,11 @@ function tool_dataprivacy_pluginfile($course, $cm, $context, $filearea, $args, $
             return false;
         }
 
+        // Make the file unavailable if it has expired.
+        if (\tool_dataprivacy\data_request::is_expired($datarequest)) {
+            send_file_not_found();
+        }
+
         // All good. Serve the exported data.
         $fs = get_file_storage();
         $relativepath = implode('/', $args);
index 2568096..f435d00 100644 (file)
@@ -55,7 +55,7 @@ $PAGE->set_title($title);
 echo $OUTPUT->header();
 echo $OUTPUT->heading($title);
 
-$requests = tool_dataprivacy\api::get_data_requests($USER->id);
+$requests = tool_dataprivacy\api::get_data_requests($USER->id, [], [], 'timecreated DESC');
 $requestlist = new tool_dataprivacy\output\my_data_requests_page($requests);
 $requestlistoutput = $PAGE->get_renderer('tool_dataprivacy');
 echo $requestlistoutput->render($requestlist);
index 3b3f1c4..5ae8839 100644 (file)
@@ -38,11 +38,18 @@ $title = get_string('pluginregistry', 'tool_dataprivacy');
 $output = $PAGE->get_renderer('tool_dataprivacy');
 echo $output->header();
 
-// Get data!
-$metadatatool = new \tool_dataprivacy\metadata_registry();
-$metadata = $metadatatool->get_registry_metadata();
+if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
+    // Get data!
+    $metadatatool = new \tool_dataprivacy\metadata_registry();
+    $metadata = $metadatatool->get_registry_metadata();
 
-$dataregistry = new tool_dataprivacy\output\data_registry_compliance_page($metadata);
+    $dataregistry = new tool_dataprivacy\output\data_registry_compliance_page($metadata);
+
+    echo $output->render($dataregistry);
+} else {
+    $dponamestring = implode (', ', tool_dataprivacy\api::get_dpo_role_names());
+    $message = get_string('privacyofficeronly', 'tool_dataprivacy', $dponamestring);
+    echo $OUTPUT->notification($message, 'error');
+}
 
-echo $output->render($dataregistry);
 echo $OUTPUT->footer();
index f04fc10..b902d52 100644 (file)
@@ -34,6 +34,12 @@ if ($hassiteconfig) {
                 new lang_string('contactdataprotectionofficer_desc', 'tool_dataprivacy'), 0)
         );
 
+        // Set days approved data requests will be accessible. 1 week default.
+        $privacysettings->add(new admin_setting_configduration('tool_dataprivacy/privacyrequestexpiry',
+                new lang_string('privacyrequestexpiry', 'tool_dataprivacy'),
+                new lang_string('privacyrequestexpiry_desc', 'tool_dataprivacy'),
+                WEEKSECS, 1));
+
         // Fetch roles that are assignable.
         $assignableroles = get_assignable_roles(context_system::instance());
 
@@ -57,22 +63,25 @@ if ($hassiteconfig) {
     }
 }
 
-// Link that leads to the data requests management page.
-$ADMIN->add('privacy', new admin_externalpage('datarequests', get_string('datarequests', 'tool_dataprivacy'),
-    new moodle_url('/admin/tool/dataprivacy/datarequests.php'), 'tool/dataprivacy:managedatarequests')
-);
+// Restrict config links to the DPO.
+if (tool_dataprivacy\api::is_site_dpo($USER->id)) {
+    // Link that leads to the data requests management page.
+    $ADMIN->add('privacy', new admin_externalpage('datarequests', get_string('datarequests', 'tool_dataprivacy'),
+        new moodle_url('/admin/tool/dataprivacy/datarequests.php'), 'tool/dataprivacy:managedatarequests')
+    );
 
-// Link that leads to the data registry management page.
-$ADMIN->add('privacy', new admin_externalpage('dataregistry', get_string('dataregistry', 'tool_dataprivacy'),
-    new moodle_url('/admin/tool/dataprivacy/dataregistry.php'), 'tool/dataprivacy:managedataregistry')
-);
+    // Link that leads to the data registry management page.
+    $ADMIN->add('privacy', new admin_externalpage('dataregistry', get_string('dataregistry', 'tool_dataprivacy'),
+        new moodle_url('/admin/tool/dataprivacy/dataregistry.php'), 'tool/dataprivacy:managedataregistry')
+    );
 
-// Link that leads to the review page of expired contexts that are up for deletion.
-$ADMIN->add('privacy', new admin_externalpage('datadeletion', get_string('datadeletion', 'tool_dataprivacy'),
-        new moodle_url('/admin/tool/dataprivacy/datadeletion.php'), 'tool/dataprivacy:managedataregistry')
-);
+    // Link that leads to the review page of expired contexts that are up for deletion.
+    $ADMIN->add('privacy', new admin_externalpage('datadeletion', get_string('datadeletion', 'tool_dataprivacy'),
+            new moodle_url('/admin/tool/dataprivacy/datadeletion.php'), 'tool/dataprivacy:managedataregistry')
+    );
 
-// Link that leads to the other data registry management page.
-$ADMIN->add('privacy', new admin_externalpage('pluginregistry', get_string('pluginregistry', 'tool_dataprivacy'),
-    new moodle_url('/admin/tool/dataprivacy/pluginregistry.php'), 'tool/dataprivacy:managedataregistry')
-);
+    // Link that leads to the other data registry management page.
+    $ADMIN->add('privacy', new admin_externalpage('pluginregistry', get_string('pluginregistry', 'tool_dataprivacy'),
+        new moodle_url('/admin/tool/dataprivacy/pluginregistry.php'), 'tool/dataprivacy:managedataregistry')
+    );
+}
index a351052..e6ddf93 100644 (file)
@@ -24,3 +24,7 @@ dd a.contactdpo {
     /* Reverting dd's left margin */
     margin-left: inherit;
 }
+
+[data-region="data-requests-table"] .moodle-actionmenu {
+    min-width: 150px;
+}
index 5bf63fb..ef31041 100644 (file)
@@ -53,7 +53,7 @@
 <div data-region="categories" class="m-t-3 m-b-1">
     <h3>{{#str}}categories, tool_dataprivacy{{/str}}</h3>
     <div class="m-y-1">
-        <button class="btn btn-secondary" data-add-element="category">
+        <button class="btn btn-secondary" data-add-element="category" title="{{#str}}addcategory, tool_dataprivacy{{/str}}">
             {{#pix}}t/add, moodle, {{#str}}addcategory, tool_dataprivacy{{/str}}{{/pix}}
         </button>
     </div>
index c62dc0b..3f7b691 100644 (file)
@@ -61,6 +61,9 @@
         {{#external}}
             <span class="badge badge-pill badge-notice">{{#str}}external, tool_dataprivacy{{/str}}</span>
         {{/external}}
+        {{#deprecated}}
+            <span class="badge badge-pill badge-warning">{{#str}}deprecated, tool_dataprivacy{{/str}}</span>
+        {{/deprecated}}
     </div>
 
     {{#compliant}}
index caadac1..c4a9c1b 100644 (file)
@@ -45,6 +45,8 @@
         <dd>{{#str}}requiresattentionexplanation, tool_dataprivacy{{/str}}</dd>
         <dt><span class="badge badge-pill badge-notice">{{#str}}external, tool_dataprivacy{{/str}}</span></dt>
         <dd>{{#str}}externalexplanation, tool_dataprivacy{{/str}}</dd>
+        <dt><span class="badge badge-pill badge-warning">{{#str}}deprecated, tool_dataprivacy{{/str}}</span></dt>
+        <dd>{{#str}}deprecatedexplanation, tool_dataprivacy{{/str}}</dd>
     </dl>
     <hr />
     <div class="clearfix"><a class="tool_dataprivacy-expand-all pull-right" href="#" data-visibility-state='visible'>{{#str}}visible, tool_dataprivacy{{/str}}</a></div>
index 2b654b2..6f00965 100644 (file)
@@ -60,7 +60,7 @@
                 "typename" : "Data deletion",
                 "comments": "Please delete all of my son's personal data.",
                 "statuslabelclass": "label-success",
-                "statuslabel": "Complete",
+                "statuslabel": "Deleted",
                 "timecreated" : 1517902087,
                 "requestedbyuser" : {
                     "fullname": "Martha Smith",
                     "fullname": "Martha Smith",
                     "profileurl": "#"
                 }
+            },
+            {
+                "id": 6,
+                "typename" : "Data export",
+                "comments": "Please let me download my data",
+                "statuslabelclass": "label",
+                "statuslabel": "Expired",
+                "statuslabeltitle": "Download has expired. Submit a new request if you wish to export your personal data.",
+                "timecreated" : 1517902087,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             }
         ]
     }
                 <td>{{#userdate}} {{timecreated}}, {{#str}} strftimedatetime {{/str}} {{/userdate}}</td>
                 <td><a href="{{requestedbyuser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{requestedbyuser.fullname}}</a></td>
                 <td>
-                    <span class="label {{statuslabelclass}}">{{statuslabel}}</span>
+                    <span class="label {{statuslabelclass}}" title="{{statuslabeltitle}}">{{statuslabel}}</span>
                 </td>
                 <td>{{comments}}</td>
                 <td>
index 6e6c855..4461eab 100644 (file)
@@ -60,7 +60,7 @@
 <div data-region="purposes" class="m-t-3 m-b-1">
     <h3>{{#str}}purposes, tool_dataprivacy{{/str}}</h3>
     <div class="m-y-1">
-        <button class="btn btn-secondary" data-add-element="purpose">
+        <button class="btn btn-secondary" data-add-element="purpose" title="{{#str}}addpurpose, tool_dataprivacy{{/str}}">
             {{#pix}}t/add, moodle, {{#str}}addpurpose, tool_dataprivacy{{/str}}{{/pix}}
         </button>
     </div>
index c341a63..9feebcf 100644 (file)
@@ -66,12 +66,12 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $requestid = $datarequest->get('id');
 
         // Update with a valid status.
-        $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE);
+        $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_DOWNLOAD_READY);
         $this->assertTrue($result);
 
         // Fetch the request record again.
         $datarequest = new data_request($requestid);
-        $this->assertEquals(api::DATAREQUEST_STATUS_COMPLETE, $datarequest->get('status'));
+        $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $datarequest->get('status'));
 
         // Update with an invalid status.
         $this->expectException(invalid_persistent_exception::class);
@@ -468,8 +468,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
      * @return array
      */
     public function get_data_requests_provider() {
-        $completeonly = [api::DATAREQUEST_STATUS_COMPLETE];
-        $completeandcancelled = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_CANCELLED];
+        $completeonly = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_DOWNLOAD_READY, api::DATAREQUEST_STATUS_DELETED];
+        $completeandcancelled = array_merge($completeonly, [api::DATAREQUEST_STATUS_CANCELLED]);
 
         return [
             // Own data requests.
@@ -612,6 +612,9 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
             [api::DATAREQUEST_STATUS_COMPLETE, false],
             [api::DATAREQUEST_STATUS_CANCELLED, false],
             [api::DATAREQUEST_STATUS_REJECTED, false],
+            [api::DATAREQUEST_STATUS_DOWNLOAD_READY, false],
+            [api::DATAREQUEST_STATUS_EXPIRED, false],
+            [api::DATAREQUEST_STATUS_DELETED, false],
         ];
     }
 
diff --git a/admin/tool/dataprivacy/tests/behat/datadelete.feature b/admin/tool/dataprivacy/tests/behat/datadelete.feature
new file mode 100644 (file)
index 0000000..c6454bd
--- /dev/null
@@ -0,0 +1,119 @@
+@tool @tool_dataprivacy
+Feature: Data delete from the privacy API
+  In order to delete data for users and meet legal requirements
+  As an admin, user, or parent
+  I need to be able to request a user and their data data be deleted
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname      | lastname |
+      | victim   | Victim User    | 1        |
+      | parent   | Long-suffering | Parent   |
+    And the following "roles" exist:
+      | shortname | name  | archetype |
+      | tired     | Tired |           |
+    And the following "permission overrides" exist:
+      | capability                                   | permission | role  | contextlevel | reference |
+      | tool/dataprivacy:makedatarequestsforchildren | Allow      | tired | System       |           |
+    And the following "role assigns" exist:
+      | user   | role  | contextlevel | reference |
+      | parent | tired | User         | victim    |
+    And the following config values are set as admin:
+      | contactdataprotectionofficer | 1  | tool_dataprivacy |
+
+  @javascript
+  Scenario: As admin, delete a user and their data
+    Given I log in as "victim"
+    And I should see "Victim User 1"
+    And I log out
+
+    And I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I follow "New request"
+    And I set the field "Requesting for" to "Victim User 1"
+    And I set the field "Type" to "Delete all of my personal data"
+    And I press "Save changes"
+    Then I should see "Victim User 1"
+    And I should see "Pending" in the "Victim User 1" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Awaiting approval" in the "Victim User 1" "table_row"
+    And I follow "Actions"
+    And I follow "Approve request"
+    And I press "Approve request"
+    And I should see "Approved" in the "Victim User 1" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Deleted" in the "Victim User 1" "table_row"
+
+    And I log out
+    And I log in as "victim"
+    And I should see "Invalid login"
+
+  @javascript
+  Scenario: As a student, request deletion of account and data
+    Given I log in as "victim"
+    And I follow "Profile" in the user menu
+    And I follow "Data requests"
+    And I follow "New request"
+    And I set the field "Type" to "Delete all of my personal data"
+    And I press "Save changes"
+    Then I should see "Delete all of my personal data"
+    And I should see "Pending" in the "Delete all of my personal data" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Awaiting approval" in the "Delete all of my personal data" "table_row"
+
+    And I log out
+    And I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I follow "Actions"
+    And I follow "Approve request"
+    And I press "Approve request"
+
+    And I log out
+    And I log in as "victim"
+    And I follow "Profile" in the user menu
+    And I follow "Data requests"
+    And I should see "Approved" in the "Delete all of my personal data" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Your session has timed out"
+    And I log in as "victim"
+    And I should see "Invalid login"
+
+    And I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I should see "Deleted"
+
+  @javascript
+  Scenario: As a parent, request account and data deletion for my child
+    Given I log in as "parent"
+    And I follow "Profile" in the user menu
+    And I follow "Data requests"
+    And I follow "New request"
+    And I set the field "Requesting for" to "Victim User 1"
+    And I set the field "Type" to "Delete all of my personal data"
+    And I press "Save changes"
+    Then I should see "Victim User 1"
+    And I should see "Pending" in the "Victim User 1" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Awaiting approval" in the "Victim User 1" "table_row"
+
+    And I log out
+    And I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I follow "Actions"
+    And I follow "Approve request"
+    And I press "Approve request"
+
+    And I log out
+    And I log in as "parent"
+    And I follow "Profile" in the user menu
+    And I follow "Data requests"
+    And I should see "Approved" in the "Victim User 1" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "You don't have any personal data requests"
index 3ab0467..50c58bc 100644 (file)
@@ -19,10 +19,11 @@ Feature: Data export from the privacy API
       | user   | role  | contextlevel | reference |
       | parent | tired | User         | victim    |
     And the following config values are set as admin:
-      | contactdataprotectionofficer | 1 | tool_dataprivacy |
+      | contactdataprotectionofficer | 1  | tool_dataprivacy |
+      | privacyrequestexpiry         | 55 | tool_dataprivacy |
 
   @javascript
-  Scenario: As admin, export data for a user and download it
+  Scenario: As admin, export data for a user and download it, unless it has expired
     Given I log in as "admin"
     And I navigate to "Users > Privacy and policies > Data requests" in site administration
     And I follow "New request"
@@ -39,12 +40,19 @@ Feature: Data export from the privacy API
     And I should see "Approved" in the "Victim User 1" "table_row"
     And I run all adhoc tasks
     And I reload the page
-    And I should see "Complete" in the "Victim User 1" "table_row"
+    And I should see "Download ready" in the "Victim User 1" "table_row"
     And I follow "Actions"
     And following "Download" should download between "1" and "100000" bytes
+    And the following config values are set as admin:
+      | privacyrequestexpiry | 1 | tool_dataprivacy |
+    And I wait "1" seconds
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I should see "Expired" in the "Victim User 1" "table_row"
+    And I follow "Actions"
+    And I should not see "Download"
 
   @javascript
-  Scenario: As a student, request data export and then download it when approved
+  Scenario: As a student, request data export and then download it when approved, unless it has expired
     Given I log in as "victim"
     And I follow "Profile" in the user menu
     And I follow "Data requests"
@@ -70,10 +78,18 @@ Feature: Data export from the privacy API
     And I should see "Approved" in the "Export all of my personal data" "table_row"
     And I run all adhoc tasks
     And I reload the page
-    And I should see "Complete" in the "Export all of my personal data" "table_row"
+    And I should see "Download ready" in the "Export all of my personal data" "table_row"
     And I follow "Actions"
     And following "Download" should download between "1" and "100000" bytes
 
+    And the following config values are set as admin:
+      | privacyrequestexpiry | 1 | tool_dataprivacy |
+    And I wait "1" seconds
+    And I reload the page
+
+    And I should see "Expired" in the "Export all of my personal data" "table_row"
+    And I should not see "Actions"
+
   @javascript
   Scenario: As a parent, request data export for my child because I don't trust the little blighter
     Given I log in as "parent"
@@ -102,6 +118,14 @@ Feature: Data export from the privacy API
     And I should see "Approved" in the "Victim User 1" "table_row"
     And I run all adhoc tasks
     And I reload the page
-    And I should see "Complete" in the "Victim User 1" "table_row"
+    And I should see "Download ready" in the "Victim User 1" "table_row"
     And I follow "Actions"
     And following "Download" should download between "1" and "100000" bytes
+
+    And the following config values are set as admin:
+      | privacyrequestexpiry | 1 | tool_dataprivacy |
+    And I wait "1" seconds
+    And I reload the page
+
+    And I should see "Expired" in the "Victim User 1" "table_row"
+    And I should not see "Actions"
diff --git a/admin/tool/dataprivacy/tests/behat/manage_categories.feature b/admin/tool/dataprivacy/tests/behat/manage_categories.feature
new file mode 100644 (file)
index 0000000..9952bbb
--- /dev/null
@@ -0,0 +1,34 @@
+@tool @tool_dataprivacy @javascript
+Feature: Manage data categories
+  As the privacy officer
+  In order to manage the data registry
+  I need to be able to manage the data categories for the data registry
+
+  Background:
+    Given I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Edit" "link"
+    And I choose "Categories" in the open action menu
+    And I press "Add category"
+    And I set the field "Name" to "Category 1"
+    And I set the field "Description" to "Category 1 description"
+    When I press "Save"
+    Then I should see "Category 1" in the "List of data categories" "table"
+    And I should see "Category 1 description" in the "Category 1" "table_row"
+
+  Scenario: Update a data category
+    Given I click on "Actions" "link" in the "Category 1" "table_row"
+    And I choose "Edit" in the open action menu
+    And I set the field "Name" to "Category 1 edited"
+    And I set the field "Description" to "Category 1 description edited"
+    When I press "Save changes"
+    Then I should see "Category 1 edited" in the "List of data categories" "table"
+    And I should see "Category 1 description edited" in the "List of data categories" "table"
+
+  Scenario: Delete a data category
+    Given I click on "Actions" "link" in the "Category 1" "table_row"
+    And I choose "Delete" in the open action menu
+    And I should see "Delete category"
+    And I should see "Are you sure you want to delete the category 'Category 1'?"
+    When I press "Delete"
+    Then I should not see "Category 1" in the "List of data categories" "table"
diff --git a/admin/tool/dataprivacy/tests/behat/manage_purposes.feature b/admin/tool/dataprivacy/tests/behat/manage_purposes.feature
new file mode 100644 (file)
index 0000000..b236d0a
--- /dev/null
@@ -0,0 +1,56 @@
+@tool @tool_dataprivacy @javascript
+Feature: Manage data storage purposes
+  As the privacy officer
+  In order to manage the data registry
+  I need to be able to manage the data storage purposes for the data registry
+
+  Background:
+    Given I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Edit" "link"
+    And I choose "Purposes" in the open action menu
+    And I press "Add purpose"
+    And I set the field "Name" to "Purpose 1"
+    And I set the field "Description" to "Purpose 1 description"
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "Lawful bases" "form_row"
+    And I click on "Contract (GDPR Art. 6.1(b))" "list_item"
+    And I click on "Legal obligation (GDPR Art 6.1(c))" "list_item"
+    And I press key "27" in the field "Lawful bases"
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "Sensitive personal data processing reasons" "form_row"
+    And I click on "Explicit consent (GDPR Art. 9.2(a))" "list_item"
+    And I press key "27" in the field "Sensitive personal data processing reasons"
+    And I set the field "retentionperiodnumber" to "2"
+    When I press "Save"
+    Then I should see "Purpose 1" in the "List of data purposes" "table"
+    And I should see "Contract (GDPR Art. 6.1(b))" in the "Purpose 1" "table_row"
+    And I should see "Legal obligation (GDPR Art 6.1(c))" in the "Purpose 1" "table_row"
+    And I should see "Explicit consent (GDPR Art. 9.2(a))" in the "Purpose 1" "table_row"
+    And I should see "2 years" in the "Purpose 1" "table_row"
+    And I should see "No" in the "Purpose 1" "table_row"
+
+  Scenario: Update a data storage purpose
+    Given I click on "Actions" "link" in the "Purpose 1" "table_row"
+    And I choose "Edit" in the open action menu
+    And I set the field "Name" to "Purpose 1 edited"
+    And I set the field "Description" to "Purpose 1 description edited"
+    And I click on "Legal obligation (GDPR Art 6.1(c))" "text" in the ".form-autocomplete-selection" "css_element"
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "Lawful bases" "form_row"
+    And I click on "Vital interests (GDPR Art. 6.1(d))" "list_item"
+    And I press key "27" in the field "Lawful bases"
+    And I set the field "retentionperiodnumber" to "3"
+    And I click on "protected" "checkbox"
+    When I press "Save changes"
+    Then I should see "Purpose 1 edited" in the "List of data purposes" "table"
+    And I should see "Purpose 1 description edited" in the "Purpose 1 edited" "table_row"
+    And I should see "Vital interests (GDPR Art. 6.1(d))" in the "Purpose 1 edited" "table_row"
+    And I should see "3 years" in the "Purpose 1 edited" "table_row"
+    But I should not see "Legal obligation (GDPR Art 6.1(c))" in the "Purpose 1 edited" "table_row"
+    And I should not see "No" in the "Purpose 1 edited" "table_row"
+
+  Scenario: Delete a data storage purpose
+    Given I click on "Actions" "link" in the "Purpose 1" "table_row"
+    And I choose "Delete" in the open action menu
+    And I should see "Delete purpose"
+    And I should see "Are you sure you want to delete the purpose 'Purpose 1'?"
+    When I press "Delete"
+    Then I should not see "Purpose 1" in the "List of data purposes" "table"
diff --git a/admin/tool/dataprivacy/tests/data_privacy_testcase.php b/admin/tool/dataprivacy/tests/data_privacy_testcase.php
new file mode 100644 (file)
index 0000000..16b48de
--- /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/>.
+
+/**
+ * Parent class for tests which need data privacy functionality.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Michael Hawkins
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Parent class for tests which need data privacy functionality.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Michael Hawkins
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class data_privacy_testcase extends advanced_testcase {
+
+    /**
+     * Assign one or more user IDs as site DPO
+     *
+     * @param stdClass|array $users User ID or array of user IDs to be assigned as site DPO
+     * @return void
+     */
+    protected function assign_site_dpo($users) {
+        global $DB;
+        $this->resetAfterTest();
+
+        if (!is_array($users)) {
+            $users = array($users);
+        }
+
+        $context = context_system::instance();
+
+        // Give the manager role with the capability to manage data requests.
+        $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
+        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
+
+        // Assign user(s) as manager.
+        foreach ($users as $user) {
+            role_assign($managerroleid, $user->id, $context->id);
+        }
+
+        // Only map the manager role to the DPO role.
+        set_config('dporoles', $managerroleid, 'tool_dataprivacy');
+    }
+}
diff --git a/admin/tool/dataprivacy/tests/expired_data_requests_test.php b/admin/tool/dataprivacy/tests/expired_data_requests_test.php
new file mode 100644 (file)
index 0000000..662d9ab
--- /dev/null
@@ -0,0 +1,173 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Expired data requests tests.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Michael Hawkins
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use tool_dataprivacy\api;
+use tool_dataprivacy\data_request;
+
+defined('MOODLE_INTERNAL') || die();
+global $CFG;
+
+require_once('data_privacy_testcase.php');
+
+/**
+ * Expired data requests tests.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Michael Hawkins
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_dataprivacy_expired_data_requests_testcase extends data_privacy_testcase {
+
+    /**
+     * Test tearDown.
+     */
+    public function tearDown() {
+        \core_privacy\local\request\writer::reset();
+    }
+
+    /**
+     * Test finding and deleting expired data requests
+     */
+    public function test_data_request_expiry() {
+        global $DB;
+        $this->resetAfterTest();
+        \core_privacy\local\request\writer::setup_real_writer_instance();
+
+        // Set up test users.
+        $this->setAdminUser();
+        $studentuser = $this->getDataGenerator()->create_user();
+        $studentusercontext = context_user::instance($studentuser->id);
+
+        $dpouser = $this->getDataGenerator()->create_user();
+        $this->assign_site_dpo($dpouser);
+
+        // Set request expiry to 5 minutes.
+        set_config('privacyrequestexpiry', 300, 'tool_dataprivacy');
+
+        // Create and approve data request.
+        $this->setUser($studentuser->id);
+        $datarequest = api::create_data_request($studentuser->id, api::DATAREQUEST_TYPE_EXPORT);
+        $this->setAdminUser();
+        ob_start();
+        $this->runAdhocTasks('\tool_dataprivacy\task\initiate_data_request_task');
+        $requestid = $datarequest->get('id');
+        $this->setUser($dpouser->id);
+        api::approve_data_request($requestid);
+        $this->setAdminUser();
+        $this->runAdhocTasks('\tool_dataprivacy\task\process_data_request_task');
+        ob_end_clean();
+
+        // Confirm approved and exported.
+        $request = new data_request($requestid);
+        $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
+        $fileconditions = array(
+            'userid' => $studentuser->id,
+            'component' => 'tool_dataprivacy',
+            'filearea' => 'export',
+            'itemid' => $requestid,
+            'contextid' => $studentusercontext->id,
+        );
+        $this->assertEquals(2, $DB->count_records('files', $fileconditions));
+
+        // Run expiry deletion - should not affect test export.
+        $expiredrequests = data_request::get_expired_requests();
+        $this->assertEquals(0, count($expiredrequests));
+        data_request::expire($expiredrequests);
+
+        // Confirm test export was not deleted.
+        $request = new data_request($requestid);
+        $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
+        $this->assertEquals(2, $DB->count_records('files', $fileconditions));
+
+        // Change request expiry to 1 second and allow it to elapse.
+        set_config('privacyrequestexpiry', 1, 'tool_dataprivacy');
+        $this->waitForSecond();
+
+        // Re-run expiry deletion, confirm the request expires and export is deleted.
+        $expiredrequests = data_request::get_expired_requests();
+        $this->assertEquals(1, count($expiredrequests));
+        data_request::expire($expiredrequests);
+
+        $request = new data_request($requestid);
+        $this->assertEquals(api::DATAREQUEST_STATUS_EXPIRED, $request->get('status'));
+        $this->assertEquals(0, $DB->count_records('files', $fileconditions));
+    }
+
+
+    /**
+     * Test for \tool_dataprivacy\data_request::is_expired()
+     * Tests for the expected request status to protect from false positive/negative,
+     * then tests is_expired() is returning the expected response.
+     */
+    public function test_is_expired() {
+        $this->resetAfterTest();
+        \core_privacy\local\request\writer::setup_real_writer_instance();
+
+        // Set request expiry beyond this test.
+        set_config('privacyrequestexpiry', 20, 'tool_dataprivacy');
+
+        $admin = get_admin();
+        $this->setAdminUser();
+
+        // Create export request.
+        $datarequest = api::create_data_request($admin->id, api::DATAREQUEST_TYPE_EXPORT);
+        $requestid = $datarequest->get('id');
+
+        // Approve the request.
+        ob_start();
+        $this->runAdhocTasks('\tool_dataprivacy\task\initiate_data_request_task');
+        $this->setAdminUser();
+        api::approve_data_request($requestid);
+        $this->runAdhocTasks('\tool_dataprivacy\task\process_data_request_task');
+        ob_end_clean();
+
+        // Test Download ready (not expired) response.
+        $request = new data_request($requestid);
+        $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
+        $result = data_request::is_expired($request);
+        $this->assertFalse($result);
+
+        // Let request expiry time lapse.
+        set_config('privacyrequestexpiry', 1, 'tool_dataprivacy');
+        $this->waitForSecond();
+
+        // Test Download ready (time expired) response.
+        $request = new data_request($requestid);
+        $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
+        $result = data_request::is_expired($request);
+        $this->assertTrue($result);
+
+        // Run the expiry task to properly expire the request.
+        ob_start();
+        $task = \core\task\manager::get_scheduled_task('\tool_dataprivacy\task\delete_expired_requests');
+        $task->execute();
+        ob_end_clean();
+
+        // Test Expired response status response.
+        $request = new data_request($requestid);
+        $this->assertEquals(api::DATAREQUEST_STATUS_EXPIRED, $request->get('status'));
+        $result = data_request::is_expired($request);
+        $this->assertTrue($result);
+    }
+}
index 6c71027..1c47153 100644 (file)
@@ -23,6 +23,7 @@
  */
 
 defined('MOODLE_INTERNAL') || die();
+require_once('data_privacy_testcase.php');
 
 /**
  * API tests.
@@ -31,35 +32,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class tool_dataprivacy_manager_observer_testcase extends advanced_testcase {
-
-    /**
-     * Helper to set andn return two users who are DPOs.
-     */
-    protected function setup_site_dpos() {
-        global $DB;
-        $this->resetAfterTest();
-
-        $generator = new testing_data_generator();
-        $u1 = $this->getDataGenerator()->create_user();
-        $u2 = $this->getDataGenerator()->create_user();
-
-        $context = context_system::instance();
-
-        // Give the manager role with the capability to manage data requests.
-        $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
-        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
-
-        // Assign both users as manager.
-        role_assign($managerroleid, $u1->id, $context->id);
-        role_assign($managerroleid, $u2->id, $context->id);
-
-        // Only map the manager role to the DPO role.
-        set_config('dporoles', $managerroleid, 'tool_dataprivacy');
-
-        return \tool_dataprivacy\api::get_site_dpos();
-    }
-
+class tool_dataprivacy_manager_observer_testcase extends data_privacy_testcase {
     /**
      * Ensure that when users are configured as DPO, they are sent an message upon failure.
      */
@@ -69,8 +42,11 @@ class tool_dataprivacy_manager_observer_testcase extends advanced_testcase {
         // Create another user who is not a DPO.
         $this->getDataGenerator()->create_user();
 
-        // Create the DPOs.
-        $dpos = $this->setup_site_dpos();
+        // Create two DPOs.
+        $dpo1 = $this->getDataGenerator()->create_user();
+        $dpo2 = $this->getDataGenerator()->create_user();
+        $this->assign_site_dpo(array($dpo1, $dpo2));
+        $dpos = \tool_dataprivacy\api::get_site_dpos();
 
         $observer = new \tool_dataprivacy\manager_observer();
 
index f5c7977..ca666e3 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2018051405;
+$plugin->version   = 2018082100;
 $plugin->requires  = 2018050800;        // Moodle 3.5dev (Build 2018031600) and upwards.
 $plugin->component = 'tool_dataprivacy';
index 5c464ba..90e8d21 100644 (file)
@@ -6,7 +6,7 @@ Feature: Add customised file types
 
   Scenario: Add a new file type
     Given I log in as "admin"
-    And I navigate to "File types" node in "Site administration > Server"
+    And I navigate to "Server > File types" in site administration
     And I press "Add"
     # Try setting all the form fields, not just the optional ones.
     And I set the following fields to these values:
@@ -24,7 +24,7 @@ Feature: Add customised file types
 
   Scenario: Update an existing file type
     Given I log in as "admin"
-    And I navigate to "File types" node in "Site administration > Server"
+    And I navigate to "Server > File types" in site administration
     When I click on "Edit 7z" "link"
     And I set the following fields to these values:
       | Extension | doc |
@@ -37,7 +37,7 @@ Feature: Add customised file types
 
   Scenario: Change the text option (was buggy)
     Given I log in as "admin"
-    And I navigate to "File types" node in "Site administration > Server"
+    And I navigate to "Server > File types" in site administration
     When I click on "Edit 7z" "link"
     And I set the following fields to these values:
       | Description type   | Custom description specified in this form |
@@ -51,7 +51,7 @@ Feature: Add customised file types
 
   Scenario: Try to select a text option without entering a value.
     Given I log in as "admin"
-    And I navigate to "File types" node in "Site administration > Server"
+    And I navigate to "Server > File types" in site administration
     When I click on "Edit dmg" "link"
     And I set the field "Description type" to "Custom description"
     And I press "Save changes"
@@ -66,7 +66,7 @@ Feature: Add customised file types
 
   Scenario: Delete an existing file type
     Given I log in as "admin"
-    And I navigate to "File types" node in "Site administration > Server"
+    And I navigate to "Server > File types" in site administration
     When I click on "Delete 7z" "link"
     Then I should see "Are you absolutely sure you want to remove .7z?"
     And I press "Yes"
@@ -74,7 +74,7 @@ Feature: Add customised file types
 
   Scenario: Delete a custom file type
     Given I log in as "admin"
-    And I navigate to "File types" node in "Site administration > Server"
+    And I navigate to "Server > File types" in site administration
     And I press "Add"
     And I set the following fields to these values:
       | Extension                  | frog                                      |
@@ -86,7 +86,7 @@ Feature: Add customised file types
 
   Scenario: Revert changes to deleted file type
     Given I log in as "admin"
-    And I navigate to "File types" node in "Site administration > Server"
+    And I navigate to "Server > File types" in site administration
     When I click on "Delete 7z" "link"
     And I press "Yes"
     And I follow "Restore 7z to Moodle defaults"
@@ -95,7 +95,7 @@ Feature: Add customised file types
 
   Scenario: Revert changes to updated file type
     Given I log in as "admin"
-    And I navigate to "File types" node in "Site administration > Server"
+    And I navigate to "Server > File types" in site administration
     And I click on "Edit 7z" "link"
     And I set the following fields to these values:
       | Type groups | document |
@@ -110,7 +110,7 @@ Feature: Add customised file types
       | fullname | shortname |
       | Course 1 | C1        |
     And I log in as "admin"
-    And I navigate to "File types" node in "Site administration > Server"
+    And I navigate to "Server > File types" in site administration
     And I press "Add"
     And I set the following fields to these values:
       | Extension          | frog                                      |
index 0b79d8b..76e77e0 100644 (file)
@@ -13,14 +13,14 @@ Feature: View the httpsreplace report
 
   @javascript
   Scenario: Go to the HTTPS replace report screen. Make sure broken domains are reported.
-    When I navigate to "HTTP security" node in "Site administration > Security"
+    When I navigate to "Security > HTTP security" in site administration
     And I follow "HTTPS conversion tool"
     And I press "Continue"
     Then I should see "intentionally.unavailable"
 
   @javascript
   Scenario: Use the find and replace tool.
-    When I navigate to "HTTP security" node in "Site administration > Security"
+    When I navigate to "Security > HTTP security" in site administration
     And I follow "HTTPS conversion tool"
     And I press "Continue"
     And I set the field "I understand the risks of this operation" to "1"
index e1b9170..63fcc53 100644 (file)
@@ -11,29 +11,29 @@ Feature: Manage language packs
 
   Scenario: Install language pack
     Given I log in as "admin"
-    And I navigate to "Language packs" node in "Site administration > Language"
+    And I navigate to "Language > Language packs" in site administration
     When I set the field "Available language packs" to "en_ar"
     And I press "Install selected language pack(s)"
     Then I should see "Language pack 'en_ar' was successfully installed"
     And the "Installed language packs" select box should contain "en_ar"
-    And I navigate to "Live logs" node in "Site administration > Reports"
+    And I navigate to "Reports > Live logs" in site administration
     And I should see "The language pack 'en_ar' was installed."
     And I log out
 
   Scenario: Update language pack
     Given outdated langpack 'en_ar' is installed
     And I log in as "admin"
-    And I navigate to "Language packs" node in "Site administration > Language"
+    And I navigate to "Language > Language packs" in site administration
     When I press "Update all installed language packs"
     Then I should see "Language pack 'en_ar' was successfully updated"
     And I should see "Language pack update completed"
-    And I navigate to "Live logs" node in "Site administration > Reports"
+    And I navigate to "Reports > Live logs" in site administration
     And I should see "The language pack 'en_ar' was updated."
     And I log out
 
   Scenario: Try to uninstall language pack
     Given I log in as "admin"
-    And I navigate to "Language packs" node in "Site administration > Language"
+    And I navigate to "Language > Language packs" in site administration
     And I set the field "Available language packs" to "en_ar"
     And I press "Install selected language pack(s)"
     When I set the field "Installed language packs" to "en_ar"
@@ -42,17 +42,17 @@ Feature: Manage language packs
     Then I should see "Language pack 'en_ar' was uninstalled"
     And the "Installed language packs" select box should not contain "en_ar"
     And the "Available language packs" select box should contain "en_ar"
-    And I navigate to "Live logs" node in "Site administration > Reports"
+    And I navigate to "Reports > Live logs" in site administration
     And I should see "The language pack 'en_ar' was removed."
     And I should see "Language pack uninstalled"
     And I log out
 
   Scenario: Try to uninstall English language pack
     Given I log in as "admin"
-    And I navigate to "Language packs" node in "Site administration > Language"
+    And I navigate to "Language > Language packs" in site administration
     When I set the field "Installed language packs" to "en"
     And I press "Uninstall selected language pack(s)"
     Then I should see "The English language pack cannot be uninstalled."
-    And I navigate to "Live logs" node in "Site administration > Reports"
+    And I navigate to "Reports > Live logs" in site administration
     And I should not see "Language pack uninstalled"
     And I log out
index 8f49696..31edf3a 100644 (file)
@@ -52,7 +52,7 @@ $string['privacy:metadata:log:origin'] = 'The origin of the event';
 $string['privacy:metadata:log:other'] = 'Additional information about the event';
 $string['privacy:metadata:log:realuserid'] = 'The ID of the real user behind the event, when masquerading a user.';
 $string['privacy:metadata:log:relateduserid'] = 'The ID of a user related to this event';
-$string['privacy:metadata:log:timecreated'] = 'The time at which the event occurred';
+$string['privacy:metadata:log:timecreated'] = 'The time when the event occurred';
 $string['privacy:metadata:log:userid'] = 'The ID of the user who triggered this event';
 $string['read'] = 'Read';
 $string['tablenotfound'] = 'Specified table was not found';
index 5f35555..d40f4cf 100644 (file)
@@ -31,7 +31,7 @@ $string['privacy:metadata:log'] = 'A collection of past events';
 $string['privacy:metadata:log:action'] = 'A description of the action';
 $string['privacy:metadata:log:info'] = 'Additional information';
 $string['privacy:metadata:log:ip'] = 'The IP address used at the time of the event';
-$string['privacy:metadata:log:time'] = 'The date at wich the action took place';
+$string['privacy:metadata:log:time'] = 'The time when the action took place';
 $string['privacy:metadata:log:url'] = 'The URL related to the event';
 $string['privacy:metadata:log:userid'] = 'The ID of the user who performed the action';
 $string['taskcleanup'] = 'Legacy log table cleanup';
index 33c229f..14a1d2b 100644 (file)
@@ -33,6 +33,6 @@ $string['privacy:metadata:log:origin'] = 'The origin of the event';
 $string['privacy:metadata:log:other'] = 'Additional information about the event';
 $string['privacy:metadata:log:realuserid'] = 'The ID of the real user behind the event, when masquerading a user.';
 $string['privacy:metadata:log:relateduserid'] = 'The ID of a user related to this event';
-$string['privacy:metadata:log:timecreated'] = 'The time at which the event occurred';
+$string['privacy:metadata:log:timecreated'] = 'The time when the event occurred';
 $string['privacy:metadata:log:userid'] = 'The ID of the user who triggered this event';
 $string['taskcleanup'] = 'Log table cleanup';
index c847b03..a9c378d 100644 (file)
@@ -96,9 +96,9 @@ $string['oneyear'] = 'One year';
 $string['pluginname'] = 'Inbound message configuration';
 $string['privacy:metadata:coreuserkey'] = 'User\'s keys to validate the email received';
 $string['privacy:metadata:messagelist'] = 'A list of message identifiers which failed validation and requires further authorisation';
-$string['privacy:metadata:messagelist:address'] = 'The address at which the email was sent';
+$string['privacy:metadata:messagelist:address'] = 'The address where the email was sent';
 $string['privacy:metadata:messagelist:messageid'] = 'The message ID';
-$string['privacy:metadata:messagelist:timecreated'] = 'The time at which the record was made';
+$string['privacy:metadata:messagelist:timecreated'] = 'The time when the record was made';
 $string['privacy:metadata:messagelist:userid'] = 'The ID of user who need to approve the message';
 $string['replysubjectprefix'] = 'Re:';
 $string['requirevalidation'] = 'Validate sender address';
index 195af12..f2d08ac 100644 (file)
@@ -84,7 +84,7 @@ $string['mobilefeatures'] = 'Mobile features';
 $string['mobilenotificationsdisabledwarning'] = 'Mobile notifications are not enabled. They should be enabled in Manage message outputs.';
 $string['mobilesettings'] = 'Mobile settings';
 $string['offlineuse'] = 'Offline use';
-$string['pluginname'] = 'Moodle Mobile tools';
+$string['pluginname'] = 'Moodle app tools';
 $string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
 $string['remoteaddons'] = 'Remote add-ons';
 $string['selfsignedoruntrustedcertificatewarning'] = 'It seems that the HTTPS certificate is self-signed or not trusted. The mobile app will only work with trusted sites.';
index 01c7619..d027a0f 100644 (file)
@@ -6,7 +6,7 @@ Feature: Enable/disable managment of the event monitor
 
   Scenario: Tool is disabled by default.
     Given I log in as "admin"
-    When I navigate to "Event monitoring rules" node in "Site administration > Reports"
+    When I navigate to "Reports > Event monitoring rules" in site administration
     Then I should see "Event monitoring is currently disabled"
     And I should see "Enable"
     And I should not see "Add a new rule"
index de818df..822cf96 100644 (file)
@@ -15,10 +15,10 @@ Feature: tool_monitor_rule
       | user | course | role |
       | teacher1 | C1 | editingteacher |
     And I log in as "admin"
-    And I navigate to "Event monitoring rules" node in "Site administration > Reports"
+    And I navigate to "Reports > Event monitoring rules" in site administration
     And I click on "Enable" "link"
     And I am on "Course 1" course homepage
-    And I navigate to "Event monitoring rules" node in "Course administration > Reports"
+    And I navigate to "Reports > Event monitoring rules" in current page administration
     And I press "Add a new rule"
     And I set the following fields to these values:
       | name                 | New rule course level                             |
@@ -29,7 +29,7 @@ Feature: tool_monitor_rule
       | minutes              | 1                                                 |
       | Notification message | The forum post was created. {modulelink}          |
     And I press "Save changes"
-    And I navigate to "Event monitoring rules" node in "Site administration > Reports"
+    And I navigate to "Reports > Event monitoring rules" in site administration
     And I press "Add a new rule"
     And I set the following fields to these values:
       | name                 | New rule site level                               |
@@ -45,7 +45,7 @@ Feature: tool_monitor_rule
   Scenario: Add a rule on course level
     Given I log in as "teacher1"
     And I am on "Course 1" course homepage
-    And I navigate to "Event monitoring rules" node in "Course administration > Reports"
+    And I navigate to "Reports > Event monitoring rules" in current page administration
     When I press "Add a new rule"
     And I set the following fields to these values:
       | name                 | New rule                                          |
@@ -65,7 +65,7 @@ Feature: tool_monitor_rule
   Scenario: Delete a rule on course level
     Given I log in as "teacher1"
     And I am on "Course 1" course homepage
-    And I navigate to "Event monitoring rules" node in "Course administration > Reports"
+    And I navigate to "Reports > Event monitoring rules" in current page administration
     When I click on "Delete rule" "link"
     Then I should see "Are you sure you want to delete the rule \"New rule course level\"?"
     And I press "Continue"
@@ -75,7 +75,7 @@ Feature: tool_monitor_rule
   Scenario: Edit a rule on course level
     Given I log in as "teacher1"
     And I am on "Course 1" course homepage
-    And I navigate to "Event monitoring rules" node in "Course administration > Reports"
+    And I navigate to "Reports > Event monitoring rules" in current page administration
     When I click on "Edit rule" "link"
     And I set the following fields to these values:
       | name                 | New rule quiz                                  |
@@ -94,7 +94,7 @@ Feature: tool_monitor_rule
   Scenario: Duplicate a rule on course level
     Given I log in as "teacher1"
     And I am on "Course 1" course homepage
-    And I navigate to "Event monitoring rules" node in "Course administration > Reports"
+    And I navigate to "Reports > Event monitoring rules" in current page administration
     When I click on "Duplicate rule" "link" in the "New rule course level" "table_row"
     Then I should see "Rule successfully duplicated"
     And "#toolmonitorrules_r1" "css_element" should appear before "#toolmonitorrules_r2" "css_element"
@@ -106,7 +106,7 @@ Feature: tool_monitor_rule
 
   Scenario: Add a rule on site level
     Given I log in as "admin"
-    And I navigate to "Event monitoring rules" node in "Site administration > Reports"
+    And I navigate to "Reports > Event monitoring rules" in site administration
     When I press "Add a new rule"
     And I set the following fields to these values:
       | name                 | New rule                                          |
@@ -125,7 +125,7 @@ Feature: tool_monitor_rule
 
   Scenario: Delete a rule on site level
     Given I log in as "admin"
-    And I navigate to "Event monitoring rules" node in "Site administration > Reports"
+    And I navigate to "Reports > Event monitoring rules" in site administration
     When I click on "Delete rule" "link"
     Then I should see "Are you sure you want to delete the rule \"New rule site level\"?"
     And I press "Continue"
@@ -134,7 +134,7 @@ Feature: tool_monitor_rule
 
   Scenario: Edit a rule on site level
     Given I log in as "admin"
-    And I navigate to "Event monitoring rules" node in "Site administration > Reports"
+    And I navigate to "Reports > Event monitoring rules" in site administration
     When I click on "Edit rule" "link"
     And I set the following fields to these values:
       | name                 | New Rule Quiz                                  |
@@ -153,7 +153,7 @@ Feature: tool_monitor_rule
   Scenario: Duplicate a rule on site level
     Given I log in as "teacher1"
     And I am on "Course 1" course homepage
-    And I navigate to "Event monitoring rules" node in "Course administration > Reports"
+    And I navigate to "Reports > Event monitoring rules" in current page administration
     When I click on "Duplicate rule" "link" in the "New rule site level" "table_row"
     Then I should see "Rule successfully duplicated"
     And "#toolmonitorrules_r2" "css_element" should appear after "#toolmonitorrules_r1" "css_element"
index 2766705..9b74a68 100644 (file)
@@ -20,10 +20,10 @@ Feature: tool_monitor_subscriptions
       | teacher2 | C1 | teacher |
       | teacher2 | C2 | editingteacher |
     And I log in as "admin"
-    And I navigate to "Event monitoring rules" node in "Site administration > Reports"
+    And I navigate to "Reports > Event monitoring rules" in site administration
     And I click on "Enable" "link"
     And I am on "Course 1" course homepage
-    And I navigate to "Event monitoring rules" node in "Course administration > Reports"
+    And I navigate to "Reports > Event monitoring rules" in current page administration
     And I press "Add a new rule"
     And I set the following fields to these values:
       | name                 | New rule course level                             |
@@ -34,7 +34,7 @@ Feature: tool_monitor_subscriptions
       | minutes              | 1                                                 |
       | Notification message | The course was viewed. {modulelink}               |
     And I press "Save changes"
-    And I navigate to "Event monitoring rules" node in "Site administration > Reports"
+    And I navigate to "Reports > Event monitoring rules" in site administration
     And I press "Add a new rule"
     And I set the following fields to these values:
       | name                 | New rule site level                               |
@@ -45,7 +45,7 @@ Feature: tool_monitor_subscriptions
       | minutes              | 1                                                 |
       | Notification message | The course was viewed. {modulelink}               |
     And I press "Save changes"
-    And I navigate to "Define roles" node in "Site administration > Users > Permissions"
+    And I navigate to "Users > Permissions > Define roles" in site administration
     And I follow "Non-editing teacher"
     And I press "Edit"
     And I click on "tool/monitor:managerules" "checkbox"
index 9a87001..2e54773 100644 (file)
Binary files a/admin/tool/policy/amd/build/policyactions.min.js and b/admin/tool/policy/amd/build/policyactions.min.js differ
index ae70f48..b3de54a 100644 (file)
@@ -65,43 +65,55 @@ function($, Ajax, Notification, ModalFactory, ModalEvents) {
                 args: params
             };
 
+            var modalTitle = $.Deferred();
+            var modalBody = $.Deferred();
+
+            var modal = ModalFactory.create({
+                title: modalTitle,
+                body: modalBody,
+                large: true
+            })
+            .then(function(modal) {
+                // Handle hidden event.
+                modal.getRoot().on(ModalEvents.hidden, function() {
+                    // Destroy when hidden.
+                    modal.destroy();
+                });
+
+                return modal;
+            })
+            .then(function(modal) {
+                modal.show();
+
+                return modal;
+            })
+            .catch(Notification.exception);
+
+            // Make the request now that the modal is configured.
             var promises = Ajax.call([request]);
-            var modalTitle = '';
-            var modalType = ModalFactory.types.DEFAULT;
             $.when(promises[0]).then(function(data) {
                 if (data.result.policy) {
-                    modalTitle = data.result.policy.name;
-                    return data.result.policy.content;
+                    modalTitle.resolve(data.result.policy.name);
+                    modalBody.resolve(data.result.policy.content);
+
+                    return data;
+                } else {
+                    throw new Error(data.warnings[0].message);
                 }
-                // Fail.
-                Notification.addNotification({
-                    message: data.warnings[0].message,
+            }).catch(function(message) {
+                modal.then(function(modal) {
+                    modal.hide();
+                    modal.destroy();
+
+                    return modal;
+                })
+                .catch(Notification.exception);
+
+                return Notification.addNotification({
+                    message: message,
                     type: 'error'
                 });
-                return false;
-
-            }).then(function(html) {
-                if (html != false) {
-                    return ModalFactory.create({
-                        title: modalTitle,
-                        body: html,
-                        type: modalType,
-                        large: true
-                    }).then(function(modal) {
-                        // Handle hidden event.
-                        modal.getRoot().on(ModalEvents.hidden, function() {
-                            // Destroy when hidden.
-                            modal.destroy();
-                        });
-
-                        return modal;
-                    });
-                }
-                return false;
-            }).done(function(modal) {
-                // Show the modal.
-                modal.show();
-            }).fail(Notification.exception);
+            });
         });
 
     };
index f53af7a..3ff0daf 100644 (file)
@@ -71,10 +71,12 @@ class accept_policy extends \moodleform {
 
         $mform->addElement('hidden', 'returnurl');
         $mform->setType('returnurl', PARAM_LOCALURL);
-
-        $mform->addElement('static', 'user', get_string('acceptanceusers', 'tool_policy'), join(', ', $usernames));
-        $mform->addElement('static', 'policy', get_string('acceptancepolicies', 'tool_policy'),
-            join(', ', $versionnames));
+        $useracceptancelabel = (count($usernames) > 1) ? get_string('acceptanceusers', 'tool_policy') :
+                get_string('user');
+        $mform->addElement('static', 'user', $useracceptancelabel, join(', ', $usernames));
+        $policyacceptancelabel = (count($versionnames) > 1) ? get_string('acceptancepolicies', 'tool_policy') :
+                get_string('policydochdrpolicy', 'tool_policy');
+        $mform->addElement('static', 'policy', $policyacceptancelabel, join(', ', $versionnames));
 
         if ($revoke) {
             $mform->addElement('static', 'ack', '', get_string('revokeacknowledgement', 'tool_policy'));
index 66ea3aa..801aac0 100644 (file)
@@ -48,12 +48,15 @@ use tool_policy\policy_version;
  */
 class page_viewalldoc implements renderable, templatable {
 
+    /** @var string Return url */
+    private $returnurl;
+
     /**
      * Prepare the page for rendering.
      *
      */
-    public function __construct() {
-
+    public function __construct($returnurl) {
+        $this->returnurl = $returnurl;
         $this->prepare_global_page_access();
         $this->prepare_policies();
     }
@@ -99,6 +102,9 @@ class page_viewalldoc implements renderable, templatable {
         ];
 
         $data->policies = array_values($this->policies);
+        if (!empty($this->returnurl)) {
+            $data->returnurl = $this->returnurl;
+        }
 
         array_walk($data->policies, function($item, $key) {
             $item->policytypestr = get_string('policydoctype'.$item->type, 'tool_policy');
index 3a75285..3ede09b 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['acceptanceacknowledgement'] = 'I acknowledge that I have received a request to give consent on behalf of user(s).';
+$string['acceptanceacknowledgement'] = 'I acknowledge that I have received a request to give consent on behalf of the above user(s).';
 $string['acceptancecount'] = '{$a->agreedcount} of {$a->policiescount}';
 $string['acceptancenote'] = 'Remarks';
 $string['acceptancepolicies'] = 'Policies';
@@ -50,9 +50,10 @@ $string['agreedyesonbehalfwithlinkall'] = 'Consent given on behalf of user; clic
 $string['agreedyeswithlink'] = 'Consent given; click to withdraw user consent for {$a}';
 $string['agreedyeswithlinkall'] = 'Consent given; click to withdraw user consent for all policies';
 $string['agreepolicies'] = 'Please agree to the following policies';
+$string['backtoprevious'] = 'Go back to previous page';
 $string['backtotop'] = 'Back to top';
 $string['consentbulk'] = 'Consent';
-$string['consentdetails'] = 'Give consent on behalf of user';
+$string['consentdetails'] = 'Give consent on behalf of user(s)';
 $string['consentpagetitle'] = 'Consent';
 $string['contactdpo'] = 'For any questions about the policies please contact the privacy officer.';
 $string['dataproc'] = 'Personal data processing';
@@ -77,7 +78,7 @@ $string['filterpolicy'] = 'Policy: {$a}';
 $string['guestconsent:continue'] = 'Continue';
 $string['guestconsentmessage'] = 'If you continue browsing this website, you agree to our policies:';
 $string['iagree'] = 'I agree to the {$a}';
-$string['iagreetothepolicy'] = 'Give consent on behalf of user';
+$string['iagreetothepolicy'] = 'Give consent';
 $string['inactivate'] = 'Set status to "Inactive"';
 $string['inactivating'] = 'Inactivating a policy';
 $string['inactivatingconfirm'] = '<p>You are about to inactivate policy <em>\'{$a->name}\'</em> version <em>\'{$a->revision}\'</em>.</p>';
@@ -158,7 +159,7 @@ $string['privacy:metadata:versions:contentformat'] = 'The format of the content
 $string['privacysettings'] = 'Privacy settings';
 $string['readpolicy'] = 'Please read our {$a}';
 $string['refertofullpolicytext'] = 'Please refer to the full {$a} if you would like to review the text.';
-$string['revokeacknowledgement'] = 'I acknowledge that I have received a request to withdraw consent on behalf of user(s).';
+$string['revokeacknowledgement'] = 'I acknowledge that I have received a request to withdraw consent on behalf of the above user(s).';
 $string['revokedetails'] = 'Withdraw user consent';
 $string['save'] = 'Save';
 $string['saveasdraft'] = 'Save as draft';
index 62919d3..563254d 100644 (file)
@@ -96,17 +96,17 @@ function tool_policy_before_standard_html_head() {
 /**
  * Callback to add footer elements.
  *
- * @return str valid html footer content
+ * @return string HTML footer content
  */
 function tool_policy_standard_footer_html() {
-    global $CFG;
+    global $CFG, $PAGE;
 
     $output = '';
     if (!empty($CFG->sitepolicyhandler)
             && $CFG->sitepolicyhandler == 'tool_policy') {
         $policies = api::get_current_versions_ids();
         if (!empty($policies)) {
-            $url = (new moodle_url('/admin/tool/policy/viewall.php'))->out();
+            $url = new moodle_url('/admin/tool/policy/viewall.php', ['returnurl' => $PAGE->url]);
             $output .= html_writer::link($url, get_string('userpolicysettings', 'tool_policy'));
             $output = html_writer::div($output, 'policiesfooter');
         }
index 5bebc1a..52bf795 100644 (file)
     z-index: 9999999;
 }
 
+.behat-site .eupopup-container-bottom {
+    position: relative;
+}
+
 .eupopup-container-bottom {
     position: fixed;
     bottom: 0;
index 2f9df36..3e4664c 100644 (file)
     -
 
     Context variables required for this template:
+    * returnurl - url to the previous page
     * policies - policy array
 
     Example context (json):
     {
+        "returnurl": "#",
         "policies": [
             {
                 "id": "2",
     }
 }}
 
+{{#returnurl}}
+<div class="text-right m-b-1">
+    <a href="{{returnurl}}">{{# str }} backtoprevious, tool_policy {{/ str }}</a>
+</div>
+{{/returnurl}}
+
 <a id="top"></a>
 <div id="policies_index">
 <h1>{{# str }} listactivepolicies, tool_policy {{/ str }}</h1>
index b8c6d6e..3d436d4 100644 (file)
@@ -58,12 +58,12 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     And I navigate to "Users > Privacy and policies > Manage policies" in site administration
     And I click on "1 of 4 (25%)" "link" in the "This site policy" "table_row"
     And I click on "Consent not given" "link" in the "User One" "table_row"
-    Then I should see "Give consent on behalf of user"
+    Then I should see "Give consent"
     And I should see "User One"
     And I should see "This site policy"
-    And I should see "I acknowledge that I have received a request to give consent on behalf of user(s)."
+    And I should see "I acknowledge that I have received a request to give consent on behalf of the above user(s)."
     And I set the field "Remarks" to "Consent received from a parent"
-    And I press "Give consent on behalf of user"
+    And I press "Give consent"
     And "Consent given on behalf of user" "icon" should exist in the "User One" "table_row"
     And "Max Manager" "link" should exist in the "User One" "table_row"
     And "Consent received from a parent" "text" should exist in the "User One" "table_row"
@@ -84,12 +84,12 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     And I navigate to "Users > Privacy and policies > Manage policies" in site administration
     And I click on "1 of 4 (25%)" "link" in the "This site policy" "table_row"
     And I click on "Consent not given" "link" in the "User One" "table_row"
-    Then I should see "Give consent on behalf of user"
+    Then I should see "Give consent"
     And I should see "User One"
     And I should see "This site policy"
-    And I should see "I acknowledge that I have received a request to give consent on behalf of user(s)."
+    And I should see "I acknowledge that I have received a request to give consent on behalf of the above user(s)."
     And I set the field "Remarks" to "Consent received from a parent"
-    And I press "Give consent on behalf of user"
+    And I press "Give consent"
     And "Consent given on behalf of user" "icon" should exist in the "User One" "table_row"
     And "Max Manager" "link" should exist in the "User One" "table_row"
     And "Consent received from a parent" "text" should exist in the "User One" "table_row"
@@ -151,12 +151,12 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     And I press "Next"
     And I navigate to "Users > Privacy and policies > User agreements" in site administration
     And I click on "Consent not given; click to give consent on behalf of user for This site policy" "link" in the "User One" "table_row"
-    Then I should see "Give consent on behalf of user"
+    Then I should see "Give consent"
     And I should see "User One"
     And I should see "This site policy"
-    And I should see "I acknowledge that I have received a request to give consent on behalf of user(s)."
+    And I should see "I acknowledge that I have received a request to give consent on behalf of the above user(s)."
     And I set the field "Remarks" to "Consent received from a parent"
-    And I press "Give consent on behalf of user"
+    And I press "Give consent"
     And "Consent given on behalf of user" "icon" should exist in the "User One" "table_row"
     And "Consent not given; click to give consent on behalf of user for This privacy policy" "icon" should exist in the "User One" "table_row"
     And I click on "1 of 2" "link" in the "User One" "table_row"
@@ -184,12 +184,12 @@ Feature: Viewing acceptances reports and accep