Merge branch 'MDL-63094-master' of git://github.com/andrewnicols/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 28 Aug 2018 21:53:58 +0000 (23:53 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 28 Aug 2018 21:53:58 +0000 (23:53 +0200)
333 files changed:
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/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_request.php
admin/tool/dataprivacy/classes/external/data_request_exporter.php
admin/tool/dataprivacy/classes/local/helper.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/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/settings.php
admin/tool/dataprivacy/templates/categories.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/monitor/tests/behat/disabled.feature
admin/tool/monitor/tests/behat/rule.feature
admin/tool/monitor/tests/behat/subscription.feature
admin/tool/policy/tests/behat/consent.feature
admin/tool/recyclebin/tests/behat/backup_user_data.feature
admin/tool/recyclebin/tests/behat/basic_functionality.feature
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/tests/behat/create.feature
admin/tool/uploadcourse/tests/behat/update.feature
admin/tool/uploaduser/tests/behat/upload_users.feature
admin/tool/usertours/tests/behat/behat_tool_usertours.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/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/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/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/tests/behat/blog_visibility.feature
blog/tests/behat/comment.feature
blog/tests/behat/delete.feature
cache/classes/helper.php
cache/classes/loaders.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
cache/upgrade.txt
calendar/tests/behat/category_events.feature
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
completion/tests/behat/behat_completion.php
completion/tests/behat/enable_manual_complete_mark.feature
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/tests/behat/behat_course.php
course/tests/behat/coursetags.feature
course/tests/behat/keyholder.feature
course/tests/behat/max_number_sections.feature
course/tests/behat/role_renaming.feature
enrol/guest/tests/behat/guest_access.feature
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/tests/behat/add_custom_file_type.feature
files/tests/behat/course_files.feature
grade/edit/scale/index.php
grade/edit/tree/lib.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
lang/en/error.php
lib/amd/build/ajax.min.js
lib/amd/src/ajax.js
lib/bennu/iCalendar_components.php
lib/bennu/iCalendar_properties.php
lib/bennu/readme_moodle.txt
lib/datalib.php
lib/editor/atto/tests/behat/autosave.feature
lib/externallib.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/phpunit/classes/advanced_testcase.php
lib/questionlib.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/questionlib_test.php
lib/upgrade.txt
media/player/videojs/tests/behat/modules.feature
message/tests/behat/update_messaging_preferences.feature
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/tests/behat/assign_course_reset.feature
mod/assign/tests/behat/group_submission.feature
mod/assign/tests/behat/outcome_grading.feature
mod/assign/tests/behat/quickgrading.feature
mod/chat/tests/behat/chat_calendar_events.feature
mod/chat/tests/behat/chat_course_reset.feature
mod/chat/tests/lib_test.php
mod/data/tests/behat/behat_mod_data.php
mod/forum/tests/behat/advanced_search.feature
mod/lesson/tests/behat/lesson_course_reset.feature
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/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_reset.feature
mod/wiki/tests/behat/reset_wiki_comments_tags_files.feature
my/tests/behat/reset_all_pages.feature
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/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/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
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/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/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
user/externallib.php
user/lib.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/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 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 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 bbf57da..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.
      *
@@ -319,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;
     }
 
@@ -400,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;
@@ -423,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 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 ab40140..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,14 +206,14 @@ 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);
@@ -236,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 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 3c1c31c..9ca89eb 100644 (file)
@@ -77,15 +77,17 @@ $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['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';
@@ -192,6 +194,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 amount of time that approved data requests will be available for download before expiring. 0 means 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';
@@ -241,7 +245,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 9210f75..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());
 
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 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 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 6a5f4c0..047ac5e 100644 (file)
@@ -364,7 +364,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I log out
     # Create new policy document.
     And I log in as "admin"
-    And I navigate to "Manage policies" node in "Site administration > Users > Privacy and policies"
+    And I navigate to "Users > Privacy and policies > Manage policies" in site administration
     And I should see "Policies and agreements"
     And I should see "New policy"
     And I follow "New policy"
@@ -419,7 +419,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I log out
     # Create new version of the policy document.
     And I log in as "admin"
-    And I navigate to "Manage policies" node in "Site administration > Users > Privacy and policies"
+    And I navigate to "Users > Privacy and policies > Manage policies" in site administration
     When I follow "Actions"
     Then I should see "View"
     And I should see "Edit"
index 7df1cb2..90af9cb 100644 (file)
@@ -57,7 +57,7 @@ Feature: Backup user data
     And I am on "Course 1" course homepage with editing mode on
     And I delete "Quiz 1" activity
     And I run all adhoc tasks
-    And I navigate to "Recycle bin" node in "Course administration"
+    And I navigate to "Recycle bin" in current page administration
     And I should see "Quiz 1"
     And I click on "Restore" "link" in the "region-main" "region"
     And I log out
index 679a0cf..5edb043 100644 (file)
@@ -48,7 +48,7 @@ Feature: Basic recycle bin functionality
       | Assignment name | Test assign |
       | Description | Test |
     And I delete "Test assign" activity
-    When I navigate to "Recycle bin" node in "Course administration"
+    When I navigate to "Recycle bin" in current page administration
     Then I should see "Test assign"
     And I should see "Contents will be permanently deleted after 7 days"
     And I click on "Restore" "link" in the "region-main" "region"
@@ -77,7 +77,7 @@ Feature: Basic recycle bin functionality
     And I go to the courses management page
     And I should see "Course 2" in the "#course-listing" "css_element"
     And I am on "Course 2" course homepage
-    And I navigate to "Groups" node in "Course administration > Users"
+    And I navigate to "Users > Groups" in current page administration
     And I follow "Overview"
     And "Student 1" "text" should exist in the "Group A" "table_row"
     And "Student 2" "text" should exist in the "Group A" "table_row"
@@ -92,7 +92,7 @@ Feature: Basic recycle bin functionality
       | Description | Test |
     And I delete "Test assign" activity
     And I run all adhoc tasks
-    And I navigate to "Recycle bin" node in "Course administration"
+    And I navigate to "Recycle bin" in current page administration
     When I click on "Delete" "link"
     Then I should see "Are you sure you want to delete the selected item from the recycle bin?"
     And I press "Cancel"
@@ -115,7 +115,7 @@ Feature: Basic recycle bin functionality
     And I delete "Test assign 1" activity
     And I delete "Test assign 2" activity
     And I run all adhoc tasks
-    And I navigate to "Recycle bin" node in "Course administration"
+    And I navigate to "Recycle bin" in current page administration
     And I should see "Test assign 1"
     And I should see "Test assign 2"
     When I click on "Delete all" "link"
index 474468d..bfa0e8d 100644 (file)
@@ -7,7 +7,7 @@ Feature: Clear scheduled task fail delay
   Background:
     Given the scheduled task "\core\task\send_new_user_passwords_task" has a fail delay of "60" seconds
     And I log in as "admin"
-    And I navigate to "Scheduled tasks" node in "Site administration > Server"
+    And I navigate to "Server > Scheduled tasks" in site administration
 
   Scenario: Clear fail delay
     When I click on "Clear" "text" in the "Send new user passwords" "table_row"
index 73fc27d..1f0a9df 100644 (file)
@@ -6,7 +6,7 @@ Feature: Manage scheduled tasks
 
   Background:
     Given I log in as "admin"
-    And I navigate to "Scheduled tasks" node in "Site administration > Server"
+    And I navigate to "Server > Scheduled tasks" in site administration
 
   Scenario: Disable scheduled task
     When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row"
index 4740732..ed8ff3e 100644 (file)
@@ -6,7 +6,7 @@ Feature: Run tasks from web interface
 
   Scenario: Run a task
     Given I log in as "admin"
-    When I navigate to "Scheduled tasks" node in "Site administration > Server"
+    When I navigate to "Server > Scheduled tasks" in site administration
     Then I should see "Never" in the "Log table cleanup" "table_row"
 
     And I click on "Run now" "text" in the "Log table cleanup" "table_row"
@@ -21,7 +21,7 @@ Feature: Run tasks from web interface
 
   Scenario: Cancel running a task
     Given I log in as "admin"
-    When I navigate to "Scheduled tasks" node in "Site administration > Server"
+    When I navigate to "Server > Scheduled tasks" in site administration
     And I click on "Run now" "text" in the "Log table cleanup" "table_row"
     And I press "Cancel"
     # Confirm we're back on the scheduled tasks page by looking for the table.
@@ -31,5 +31,5 @@ Feature: Run tasks from web interface
     Given the following config values are set as admin:
       | enablerunnow | 0 | tool_task |
     When I log in as "admin"
-    And I navigate to "Scheduled tasks" node in "Site administration > Server"
+    And I navigate to "Server > Scheduled tasks" in site administration
     Then I should not see "Run now"
index b1e26f3..9fbfa01 100644 (file)
@@ -9,7 +9,7 @@ Feature: An admin can create courses using a CSV file
       | fullname | shortname | category |
       | First course | C1 | 0 |
     And I log in as "admin"
-    And I navigate to "Upload courses" node in "Site administration > Courses"
+    And I navigate to "Courses > Upload courses" in site administration
 
   @javascript
   Scenario: Creation of unexisting courses
index 73d2394..dbdbef5 100644 (file)
@@ -9,7 +9,7 @@ Feature: An admin can update courses using a CSV file
       | fullname | shortname | category |
       | Some random name | C1 | 0 |
     And I log in as "admin"
-    And I navigate to "Upload courses" node in "Site administration > Courses"
+    And I navigate to "Courses > Upload courses" in site administration
 
   @javascript
   Scenario: Updating a course fullname
index 1ee26f4..e7d88a3 100644 (file)
@@ -14,7 +14,7 @@ Feature: Upload users
       | Section 1 | math102 | S1 |
       | Section 3 | math102 | S3 |
     And I log in as "admin"
-    And I navigate to "Upload users" node in "Site administration > Users > Accounts"
+    And I navigate to "Users > Accounts >Upload users" in site administration
     When I upload "lib/tests/fixtures/upload_users.csv" file to "File" filemanager
     And I press "Upload users"
     Then I should see "Upload users preview"
@@ -48,7 +48,7 @@ Feature: Upload users
       | Section 1 | math102 | S1 |
       | Section 3 | math102 | S3 |
     And I log in as "admin"
-    And I navigate to "Upload users" node in "Site administration > Users > Accounts"
+    And I navigate to "Users > Accounts > Upload users" in site administration
     When I upload "lib/tests/fixtures/upload_users.csv" file to "File" filemanager
     And I press "Upload users"
     And I set the following fields to these values:
@@ -67,19 +67,19 @@ Feature: Upload users
   Scenario: Upload users with custom profile fields
     # Create user profile field.
     Given I log in as "admin"
-    And I navigate to "User profile fields" node in "Site administration > Users > Accounts"
+    And I navigate to "Users > Accounts > User profile fields" in site administration
     And I set the field "datatype" to "Text area"
     And I set the following fields to these values:
       | Short name | superfield  |
       | Name       | Super field |
     And I click on "Save changes" "button"
     # Upload users.
-    When I navigate to "Upload users" node in "Site administration > Users > Accounts"
+    When I navigate to "Users > Accounts > Upload users" in site administration
     And I upload "lib/tests/fixtures/upload_users_profile.csv" file to "File" filemanager
     And I press "Upload users"
     And I press "Upload users"
     # Check that users were created and the superfield is filled.
-    And I navigate to "Browse list of users" node in "Site administration > Users > Accounts"
+    And I navigate to "Users > Accounts > Browse list of users" in site administration
     And I follow "Tom Jones"
     And I should see "Super field"
     And I should see "The big guy"
index b08bc14..ef00bd2 100644 (file)
@@ -90,12 +90,9 @@ class behat_tool_usertours extends behat_base {
      * @Given /^I open the User tour settings page$/
      */
     public function i_open_the_user_tour_settings_page() {
-        $this->execute('behat_navigation::i_navigate_to_node_in', [
-                get_string('usertours', 'tool_usertours'),
-                implode(' > ', [
-                    get_string('administrationsite', 'moodle'),
-                    get_string('appearance', 'admin'),
-                ])
-            ]);
+        $this->execute('behat_navigation::i_navigate_to_in_site_administration',
+                get_string('appearance', 'admin') . ' > ' .
+                get_string('usertours', 'tool_usertours')
+        );
     }
 }
index 7381991..2eefea9 100644 (file)
@@ -6,7 +6,7 @@ Feature: Test validation of 'Age of digital consent' setting.
 
   Background:
     Given I log in as "admin"
-    And I navigate to "Privacy settings" node in "Site administration > Users > Privacy and policies"
+    And I navigate to "Users > Privacy and policies > Privacy settings" in site administration
 
   Scenario: Admin provides valid value for 'Age of digital consent'.
     Given I set the field "s__agedigitalconsentmap" to multiline:
index f0b8094..918251e 100644 (file)
@@ -20,7 +20,7 @@ Feature: Confirm that conditions on completion no longer cause a bug
     # Set up course.
     Given I log in as "teacher1"
     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 expand all fieldsets
     And I set the field "Enable completion tracking" to "Yes"
     And I press "Save and display"
index 682f59f..816e1fe 100644 (file)
@@ -64,7 +64,7 @@ Feature: availability_profile
   Scenario: Test with custom user profile field
     # Add custom field.
     Given I log in as "admin"
-    And I navigate to "User profile fields" node in "Site administration > Users > Accounts"
+    And I navigate to "Users > Accounts > User profile fields" in site administration
     And I set the field "datatype" to "Text input"
     And I set the following fields to these values:
       | Short name | superfield  |
@@ -72,7 +72,7 @@ Feature: availability_profile
     And I click on "Save changes" "button"
 
     # Set field value for user.
-    And I navigate to "Browse list of users" node in "Site administration > Users > Accounts"
+    And I navigate to "Users > Accounts > Browse list of users" in site administration
     And I click on ".icon[title=Edit]" "css_element" in the "s@example.com" "table_row"
     And I expand all fieldsets
     And I set the field "Super field" to "Bananaman"
index 32ff5d1..3330fca 100644 (file)
@@ -30,7 +30,7 @@ Feature: Option to include groups and groupings when importing a course to anoth
   Scenario: Include groups and groupings when importing a course to another course
     Given I import "Course 1" course into "Course 2" course using this options:
       | Initial | Include groups and groupings | 1 |
-    When I navigate to "Groups" node in "Course administration > Users"
+    When I navigate to "Users > Groups" in current page administration
     Then I should see "Group 1"
     And I should see "Group 2"
     And I follow "Groupings"
@@ -40,7 +40,7 @@ Feature: Option to include groups and groupings when importing a course to anoth
   Scenario: Do not include groups and groupings when importing a course to another course
     Given I import "Course 1" course into "Course 2" course using this options:
       | Initial | Include groups and groupings | 0 |
-    When I navigate to "Groups" node in "Course administration > Users"
+    When I navigate to "Users > Groups" in current page administration
     Then I should not see "Group 1"
     And I should not see "Group 2"
     And I follow "Groupings"
index d34b8d5..bd24b15 100644 (file)
@@ -42,7 +42,7 @@ Feature: Restore Moodle 2 course backups
     And I should see "Test forum name"
     And I should see "Topic 15"
     And I should not see "Topic 16"
-    And I navigate to "Edit settings" node in "Course administration"
+    And I navigate to "Edit settings" in current page administration
     And I expand all fieldsets
     And the field "id_format" matches value "Topics format"
     And I press "Cancel"
@@ -66,7 +66,7 @@ Feature: Restore Moodle 2 course backups
     And I add a "Forum" to section "1" and I fill the form with:
       | Forum name | Test forum post backup name |
       | Description | Test forum post backup description |
-    And I navigate to "Restore" node in "Course administration"
+    And I navigate to "Restore" in current page administration
     And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
       | Schema | Section 3 | 0 |
     Then I should see "Course 1"
@@ -82,7 +82,7 @@ Feature: Restore Moodle 2 course backups
     When I restore "test_backup.mbz" backup into a new course using this options:
     Then I should see "Topic 1"
     And I should see "Test forum name"
-    And I navigate to "Edit settings" node in "Course administration"
+    And I navigate to "Edit settings" in current page administration
     And I expand all fieldsets
     And the field "id_format" matches value "Topics format"
     And I set the following fields to these values:
@@ -94,14 +94,14 @@ Feature: Restore Moodle 2 course backups
     And I press "Save and display"
     And I should see "1 January - 7 January"
     And I should see "Test forum name"
-    And I navigate to "Edit settings" node in "Course administration"
+    And I navigate to "Edit settings" in current page administration
     And I expand all fieldsets
     And the field "id_format" matches value "Weekly format"
     And I set the following fields to these values:
       | id_format | Social format |
     And I press "Save and display"
     And I should see "An open forum for chatting about anything you want to"
-    And I navigate to "Edit settings" node in "Course administration"
+    And I navigate to "Edit settings" in current page administration
     And I expand all fieldsets
     And the field "id_format" matches value "Social format"
     And I press "Cancel"
@@ -118,7 +118,7 @@ Feature: Restore Moodle 2 course backups
       | Confirmation | Filename | test_backup.mbz |
     And I restore "test_backup.mbz" backup into "Course 2" course using this options:
       | Schema | Overwrite course configuration | Yes |
-    And I navigate to "Edit settings" node in "Course administration"
+    And I navigate to "Edit settings" in current page administration
     And I expand all fieldsets
     Then the field "id_format" matches value "Topics format"
     And the field "Course layout" matches value "Show one section per page"
@@ -144,7 +144,7 @@ Feature: Restore Moodle 2 course backups
       | Confirmation | Filename | test_backup.mbz |
     And I restore "test_backup.mbz" backup into "Course 2" course using this options:
       | Schema | Overwrite course configuration | No |
-    And I navigate to "Edit settings" node in "Course administration"
+    And I navigate to "Edit settings" in current page administration
     And I expand all fieldsets
     Then the field "id_format" matches value "Topics format"
     And the field "Course short name" matches value "C2"
@@ -170,10 +170,10 @@ Feature: Restore Moodle 2 course backups
       | Initial |  Include enrolled users | 0 |
       | Confirmation | Filename | test_backup.mbz |
     And I am on "Course 2" course homepage
-    And I navigate to "Restore" node in "Course administration"
+    And I navigate to "Restore" in current page administration
     And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
       | Schema | Overwrite course configuration | Yes |
-    And I navigate to "Edit settings" node in "Course administration"
+    And I navigate to "Edit settings" in current page administration
     And I expand all fieldsets
     Then the field "id_format" matches value "Topics format"
     And the field "Course layout" matches value "Show one section per page"
@@ -199,10 +199,10 @@ Feature: Restore Moodle 2 course backups
       | Initial |  Include enrolled users | 0 |
       | Confirmation | Filename | test_backup.mbz |
     And I am on "Course 2" course homepage
-    And I navigate to "Restore" node in "Course administration"
+    And I navigate to "Restore" in current page administration
     And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
       | Schema | Overwrite course configuration | No |
-    And I navigate to "Edit settings" node in "Course administration"
+    And I navigate to "Edit settings" in current page administration
     And I expand all fieldsets
     Then the field "id_format" matches value "Topics format"
     And the field "Course short name" matches value "C2"
@@ -228,10 +228,10 @@ Feature: Restore Moodle 2 course backups
       | Initial |  Include enrolled users | 0 |
       | Confirmation | Filename | test_backup.mbz |
     And I am on "Course 4" course homepage
-    And I navigate to "Restore" node in "Course administration"
+    And I navigate to "Restore" in current page administration
     And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
       | Schema | Overwrite course configuration | No |
-    And I navigate to "Edit settings" node in "Course administration"
+    And I navigate to "Edit settings" in current page administration
     And I expand all fieldsets
     Then the field "id_format" matches value "Topics format"
     And the field "Course short name" matches value "C4"
index b851957..d4cf469 100644 (file)
@@ -96,11 +96,11 @@ Feature: Restore Moodle 2 course backups with different user data settings
 
   @javascript
   Scenario: Restore a backup with user data with site config for including users set to 0
-    Given I navigate to "General restore defaults" node in "Site administration > Courses > Backups"
+    Given I navigate to "Courses > Backups > General restore defaults" in site administration
     And I set the field "s_restore_restore_general_users" to ""
     And I press "Save changes"
     And I am on "Course 1" course homepage
-    And I navigate to "Restore" node in "Course administration"
+    And I navigate to "Restore" in current page administration
     # "User data" marks the user data field for the section
     # "-" marks the user data field for the data activity
     And I restore "test_backup.mbz" backup into a new course using this options:
@@ -113,11 +113,11 @@ Feature: Restore Moodle 2 course backups with different user data settings
 
   @javascript
   Scenario: Restore a backup with user data with local and site config config for including users set to 0
-    Given I navigate to "General restore defaults" node in "Site administration > Courses > Backups"
+    Given I navigate to "Courses > Backups > General restore defaults" in site administration
     And I set the field "s_restore_restore_general_users" to ""
     And I press "Save changes"
     And I am on "Course 1" course homepage
-    And I navigate to "Restore" node in "Course administration"
+    And I navigate to "Restore" in current page administration
     When I restore "test_backup.mbz" backup into a new course using this options:
       | Settings |  Include enrolled users | 0 |
     Then I should see "Test database name"
index d228bbc..4a3c1c2 100644 (file)
@@ -10,7 +10,7 @@ Feature: Add badges to the system
 
   @javascript
   Scenario: Setting badges settings
-    Given I navigate to "Badges settings" node in "Site administration > Badges"
+    Given I navigate to "Badges > Badges settings" in site administration
     And I set the field "Default badge issuer name" to "Test Badge Site"
     And I set the field "Default badge issuer contact details" to "testuser@example.com"
     And I press "Save changes"
@@ -24,12 +24,13 @@ Feature: Add badges to the system
     And I press "Customise this page"
    # TODO MDL-57120 site "Badges" link not accessible without navigation block.
     And I add the "Navigation" block if not present
-    Given I navigate to "Site badges" node in "Site pages"
+    And I click on "Site pages" "list_item" in the "Navigation" "block"
+    Given I click on "Site badges" "link" in the "Navigation" "block"
     Then I should see "There are no badges available."
 
   @javascript @_file_upload
   Scenario: Add a badge
-    Given I navigate to "Add a new badge" node in "Site administration > Badges"
+    Given I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Test badge with 'apostrophe' and other friends (<>&@#) |
       | Description | Test badge description |
index cd64b32..79ae795 100644 (file)
@@ -20,7 +20,7 @@ Feature: Award badges
     And I log in as "teacher1"
     And I am on "Course 1" course homepage
     # Create course badge 1.
-    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I navigate to "Badges > Add a new badge" in current page administration
     And I follow "Add a new badge"
     And I set the following fields to these values:
       | Name | Course Badge 1 |
@@ -37,7 +37,8 @@ Feature: Award badges
     And I press "Enable access"
     And I press "Continue"
     # Badge #2
-    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I am on "Course 1" course homepage
+    And I navigate to "Badges > Add a new badge" in current page administration
     And I follow "Add a new badge"
     And I set the following fields to these values:
       | Name | Course Badge 2 |
@@ -72,7 +73,7 @@ Feature: Award badges
   @javascript
   Scenario: Award profile badge
     Given I log in as "admin"
-    And I navigate to "Add a new badge" node in "Site administration > Badges"
+    And I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Profile Badge |
       | Description | Test badge description |
@@ -110,7 +111,7 @@ Feature: Award badges
       | teacher | teacher | 1 | teacher1@example.com |
       | student | student | 1 | student1@example.com |
     And I log in as "admin"
-    And I navigate to "Add a new badge" node in "Site administration > Badges"
+    And I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
@@ -152,7 +153,7 @@ Feature: Award badges
       | student2 | C1 | student |
     And I log in as "teacher1"
     And I am on "Course 1" course homepage
-    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I navigate to "Badges > Add a new badge" in current page administration
     And I follow "Add a new badge"
     And I set the following fields to these values:
       | Name | Course Badge |
@@ -194,7 +195,7 @@ Feature: Award badges
       | student1 | C1 | student |
     And I log in as "teacher1"
     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 following fields to these values:
       | Enable completion tracking | Yes |
     And I press "Save and display"
@@ -204,7 +205,7 @@ Feature: Award badges
       | Description | Submit your online text |
       | id_completion | 1                     |
     And I am on "Course 1" course homepage
-    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I navigate to "Badges > Add a new badge" in current page administration
     And I follow "Add a new badge"
     And I set the following fields to these values:
       | Name | Course Badge |
@@ -243,7 +244,7 @@ Feature: Award badges
       | student1 | C1 | student |
     And I log in as "teacher1"
     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 following fields to these values:
       | Enable completion tracking | Yes |
     And I press "Save and display"
@@ -253,13 +254,13 @@ Feature: Award badges
       | Description | Submit your online text |
       | assignsubmission_onlinetext_enabled | 1 |
       | id_completion | 1                     |
-    And I navigate to "Course completion" node in "Course administration"
+    And I navigate to "Course completion" in current page administration
     And I set the field "id_overall_aggregation" to "2"
     And I click on "Condition: Activity completion" "link"
     And I set the field "Assignment - Test assignment name" to "1"
     And I press "Save changes"
     And I am on "Course 1" course homepage
-    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I navigate to "Badges > Add a new badge" in current page administration
     And I follow "Add a new badge"
     And I set the following fields to these values:
       | Name | Course Badge |
@@ -309,7 +310,7 @@ Feature: Award badges
     And I log in as "teacher1"
     And I am on "Course 1" course homepage
     # Create course badge 1.
-    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I navigate to "Badges > Add a new badge" in current page administration
     And I follow "Add a new badge"
     And I set the following fields to these values:
       | Name | Course Badge 1 |
@@ -334,7 +335,8 @@ Feature: Award badges
     And I follow "Recipients (1)"
     Then I should see "Recipients (1)"
     # Add course badge 2.
-    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I am on "Course 1" course homepage
+    And I navigate to "Badges > Add a new badge" in current page administration
     And I follow "Add a new badge"
     And I set the following fields to these values:
       | Name | Course Badge 2 |
@@ -391,7 +393,7 @@ Feature: Award badges
       | student2 | C1 | student |
     And I log in as "teacher1"
     And I am on "Course 1" course homepage
-    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I navigate to "Badges > Add a new badge" in current page administration
     And I follow "Add a new badge"
     And I set the following fields to these values:
       | Name | Course Badge |
index 6f5f816..e497144 100644 (file)
@@ -19,7 +19,7 @@ Feature: Award badges based on cohort
       | user1 | CH1   |
       | user2 | CH2   |
     And I log in as "admin"
-    And I navigate to "Add a new badge" node in "Site administration > Badges"
+    And I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
@@ -55,7 +55,7 @@ Feature: Award badges based on cohort
       | user2 | CH1   |
       | user2 | CH3   |
     And I log in as "admin"
-    And I navigate to "Add a new badge" node in "Site administration > Badges"
+    And I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
@@ -96,7 +96,7 @@ Feature: Award badges based on cohort
       | user3 | CH2   |
       | user3 | CH3   |
     And I log in as "admin"
-    And I navigate to "Add a new badge" node in "Site administration > Badges"
+    And I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
@@ -133,7 +133,7 @@ Feature: Award badges based on cohort
       | user1 | CH1   |
       | user2 | CH2   |
     And I log in as "admin"
-    And I navigate to "Add a new badge" node in "Site administration > Badges"
+    And I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
@@ -184,7 +184,7 @@ Feature: Award badges based on cohort
       | user2 | CH2   |
       | user3 | CH2   |
     And I log in as "admin"
-    And I navigate to "Add a new badge" node in "Site administration > Badges"
+    And I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
@@ -241,7 +241,7 @@ Feature: Award badges based on cohort
       | user2 | CH2   |
       | user2 | CH2   |
     And I log in as "admin"
-    And I navigate to "Add a new badge" node in "Site administration > Badges"
+    And I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
@@ -298,7 +298,7 @@ Feature: Award badges based on cohort
       | user2 | CH1   |
       | user3 | CH2   |
     And I log in as "admin"
-    And I navigate to "Add a new badge" node in "Site administration > Badges"
+    And I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
@@ -356,7 +356,7 @@ Feature: Award badges based on cohort
       | user1 | CH2   |
       | user2 | CH2   |
     And I log in as "admin"
-    And I navigate to "Add a new badge" node in "Site administration > Badges"
+    And I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Site Badge 1 |
       | Description | Site badge description |
@@ -369,7 +369,7 @@ Feature: Award badges based on cohort
     And I press "Enable access"
     When I press "Continue"
     And I should see "Recipients (1)"
-    And I navigate to "Add a new badge" node in "Site administration > Badges"
+    And I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Site Badge 2 |
       | Description | Site badge description |
@@ -411,7 +411,7 @@ Feature: Award badges based on cohort
       | user2    | Second    | User     | second@example.com |
       | user3    | Third     | User     | third@example.com  |
     And I log in as "admin"
-    And I navigate to "Add a new badge" node in "Site administration > Badges"
+    And I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Site Badge 1 |
       | Description | Site badge description |
@@ -426,7 +426,7 @@ Feature: Award badges based on cohort
     And I press "Enable access"
     When I press "Continue"
     And I should see "Recipients (0)"
-    And I navigate to "Add a new badge" node in "Site administration > Badges"
+    And I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Site Badge 2 |
       | Description | Site badge description |
@@ -440,7 +440,7 @@ Feature: Award badges based on cohort
     And I press "Save"
     And I press "Enable access"
     And I press "Continue"
-    Then I navigate to "Cohorts" node in "Site administration > Users > Accounts"
+    Then I navigate to "Users > Accounts >Cohorts" in site administration
     And I add "First User (first@example.com)" user to "CH1" cohort members
     And I add "First User (first@example.com)" user to "CH2" cohort members
     And I add "Second User (second@example.com)" user to "CH2" cohort members
index abdb166..14cc5b5 100644 (file)
@@ -10,7 +10,7 @@ Feature: Award badges based on user profile field
       | username | firstname | lastname | email           |
       | user1    | First     | User     | first@example.com  |
     And I log in as "admin"
-    And I navigate to "Add a new badge" node in "Site administration > Badges"
+    And I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
index bb3a49d..e89489b 100644 (file)
@@ -21,7 +21,7 @@ Feature: Test role visibility for the badge administration page
   Scenario: Check the default roles are visible
     Given I log in as "manager1"
     And I am on "Course 1" course homepage
-    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I navigate to "Badges > Add a new badge" in current page administration
     And I follow "Add a new badge"
     And I set the following fields to these values:
       | Name | Course Badge |
@@ -37,7 +37,7 @@ Feature: Test role visibility for the badge administration page
   Scenario: Check hidden roles are not visible
     Given I log in as "teacher1"
     And I am on "Course 1" course homepage
-    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I navigate to "Badges > Add a new badge" in current page administration
     And I follow "Add a new badge"
     And I set the following fields to these values:
       | Name | Course Badge |
index 1574fe8..faa02a4 100644 (file)
@@ -6,14 +6,14 @@ Feature: Add a bookmarks to an admin pages
 
   Background:
     Given I log in as "admin"
-    And I navigate to "Scheduled tasks" node in "Site administration > Server"
+    And I navigate to "Server > Scheduled tasks" in site administration
     And I click on "Bookmark this page" "link" in the "Admin bookmarks" "block"
     And I log out
 
   # Test bookmark functionality using the "User profile fields" page as our bookmark.
   Scenario: Admin page can be bookmarked
     Given I log in as "admin"
-    And I navigate to "User profile fields" node in "Site administration > Users > Accounts"
+    And I navigate to "Users > Accounts > User profile fields" in site administration
     When I click on "Bookmark this page" "link" in the "Admin bookmarks" "block"
     Then I should see "User profile fields" in the "Admin bookmarks" "block"
     # See the existing bookmark is there too.
@@ -21,14 +21,14 @@ Feature: Add a bookmarks to an admin pages
 
   Scenario: Admin page can be accessed through bookmarks block
     Given I log in as "admin"
-    And I navigate to "Notifications" node in "Site administration"
+    And I navigate to "Notifications" in site administration
     And I click on "Scheduled tasks" "link" in the "Admin bookmarks" "block"
     # Verify that we are on the right page.
     Then I should see "Scheduled tasks" in the "h1" "css_element"
 
   Scenario: Admin page can be removed from bookmarks
     Given I log in as "admin"
-    And I navigate to "Notifications" node in "Site administration"
+    And I navigate to "Notifications" in site administration
     And I click on "Scheduled tasks" "link" in the "Admin bookmarks" "block"
     When I click on "Unbookmark this page" "link" in the "Admin bookmarks" "block"
     Then I should see "Bookmark deleted"
index 33fe576..cc739af 100644 (file)
@@ -17,7 +17,7 @@ Feature: Enable Block Badges in a course
     And I log in as "teacher1"
     And I am on "Course 1" course homepage
     # Issue badge 1 of 2
-    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I navigate to "Badges > Add a new badge" in current page administration
     And I set the following fields to these values:
       | id_name | Badge 1 |
       | id_description | Badge 1 |
@@ -34,7 +34,8 @@ Feature: Enable Block Badges in a course
     And I set the field "potentialrecipients[]" to "Teacher 1 (teacher1@example.com)"
     And I press "Award badge"
     # Issue Badge 2 of 2
-    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I am on "Course 1" course homepage
+    And I navigate to "Badges > Add a new badge" in current page administration
     And I set the following fields to these values:
       | id_name | Badge 2 |
       | id_description | Badge 2 |
index 2928bf3..45d10c7 100644 (file)
@@ -17,7 +17,7 @@ Feature: Enable Block Badges on the dashboard and view awarded badges
     And I log in as "teacher1"
     And I am on "Course 1" course homepage
     # Issue badge 1 of 2
-    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I navigate to "Badges > Add a new badge" in current page administration
     And I set the following fields to these values:
       | id_name | Badge 1 |
       | id_description | Badge 1 |
index d1daf80..89a99de 100644 (file)
@@ -16,13 +16,13 @@ Feature: Enable Block Badges on the frontpage and view awarded badges
       | teacher1 | C1 | editingteacher |
     And I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Latest badges" block
     And I log out
     And I log in as "teacher1"
     And I am on "Course 1" course homepage
     # Issue badge 1 of 2
-    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I navigate to "Badges > Add a new badge" in current page administration
     And I set the following fields to these values:
       | id_name | Badge 1 |
       | id_description | Badge 1 |
index 3c2936a..8e1df82 100644 (file)
@@ -10,7 +10,7 @@ Feature: Enable Block blog menu on the frontpage
       | student1 | Student | 1 | student1@example.com | S1 |
     And I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Blog menu" block
     And I log out
 
index 3b838df..3864042 100644 (file)
@@ -10,7 +10,7 @@ Feature: Feature: Students can use the recent blog entries block to view recent
       | student1 | Student | 1 | student1@example.com | S1 |
     And I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Recent blog entries" block
     # TODO MDL-57120 site "Blogs" link not accessible without navigation block.
     And I add the "Navigation" block if not present
@@ -19,7 +19,7 @@ Feature: Feature: Students can use the recent blog entries block to view recent
   Scenario: Students use the recent blog entries block to view blogs
     Given I log in as "student1"
     And I am on site homepage
-    And I navigate to "Site blogs" node in "Site pages"
+    And I click on "Site blogs" "link" in the "Navigation" "block"
     And I follow "Add a new entry"
     When I set the following fields to these values:
       | Entry title | S1 First Blog |
@@ -35,7 +35,7 @@ Feature: Feature: Students can use the recent blog entries block to view recent
   Scenario: Students only see a few entries in the recent blog entries block
     Given I log in as "student1"
     And I am on site homepage
-    And I navigate to "Site blogs" node in "Site pages"
+    And I click on "Site blogs" "link" in the "Navigation" "block"
     And I follow "Add a new entry"
     # Blog 1 of 5
     And I set the following fields to these values:
@@ -89,7 +89,7 @@ Feature: Feature: Students can use the recent blog entries block to view recent
     Then I log out
     And I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I configure the "Recent blog entries" block
     And I set the following fields to these values:
       | id_config_numberofrecentblogentries | 2 |
index bc34346..f8f1597 100644 (file)
@@ -129,7 +129,7 @@ Feature: Enable the calendar block in a course and test it's functionality
       | student2 | G2 |
     When I log in as "teacher1"
     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 following fields to these values:
       | id_groupmode | Separate groups |
       | id_groupmodeforce | Yes |
@@ -167,7 +167,7 @@ Feature: Enable the calendar block in a course and test it's functionality
       | student2 | G2 |
     When I log in as "teacher1"
     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 following fields to these values:
       | id_groupmode | Separate groups |
       | id_groupmodeforce | Yes |
index 448b710..dd31e37 100644 (file)
@@ -16,7 +16,7 @@ Feature: View a site event on the frontpage
       | id_eventtype | Site |
       | id_name | My Site Event |
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Upcoming events" block
     And I log out
     When I log in as "teacher1"
index 5d949ab..6b883f2 100644 (file)
@@ -10,7 +10,7 @@ Feature: Enable Block comments on the frontpage and view comments
       | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
     And I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Comments" block
     And I follow "Show comments"
     And I add "I'm a comment from admin" comment to comments block
index f5e24a1..318be30 100644 (file)
@@ -20,7 +20,7 @@ Feature: Enable Block Completion in a course
   Scenario: Add the block to a the course where completion is disabled
     Given I log in as "teacher1"
     And I am on "Course 1" course homepage with editing mode on
-    And I navigate to "Edit settings" node in "Course administration"
+    And I navigate to "Edit settings" in current page administration
     And I set the following fields to these values:
       | Enable completion tracking | No |
     And I press "Save and display"
@@ -46,7 +46,7 @@ Feature: Enable Block Completion in a course
       | Require view | 1 |
     And I press "Save and return to course"
     When I add the "Course completion status" block
-    And I navigate to "Course completion" node in "Course administration"
+    And I navigate to "Course completion" in current page administration
     And I expand all fieldsets
     And I set the following fields to these values:
       | Test page name | 1 |
index d41fe98..397afe0 100644 (file)
@@ -30,7 +30,7 @@ Feature: Enable Block Completion in a course using activity completion
       | Require view | 1 |
     And I press "Save and return to course"
     And I add the "Course completion status" block
-    And I navigate to "Course completion" node in "Course administration"
+    And I navigate to "Course completion" in current page administration
     And I expand all fieldsets
     And I set the following fields to these values:
       | Test page name | 1 |
@@ -51,7 +51,7 @@ Feature: Enable Block Completion in a course using activity completion
       | Require view | 1 |
     And I press "Save and return to course"
     And I add the "Course completion status" block
-    And I navigate to "Course completion" node in "Course administration"
+    And I navigate to "Course completion" in current page administration
     And I expand all fieldsets
     And I set the following fields to these values:
       | Test page name | 1 |
index c021914..ebe242f 100644 (file)
@@ -23,7 +23,7 @@ Feature: Enable Block Completion in a course using manual completion by others
     Given I log in as "teacher1"
     And I am on "Course 1" course homepage with editing mode on
     And I add the "Course completion status" block
-    And I navigate to "Course completion" node in "Course administration"
+    And I navigate to "Course completion" in current page administration
     And I expand all fieldsets
     And I set the following fields to these values:
       | Teacher | 1 |
@@ -36,7 +36,7 @@ Feature: Enable Block Completion in a course using manual completion by others
     And I log out
     And I log in as "teacher1"
     And I am on "Course 1" course homepage
-    And I navigate to "Course completion" node in "Course administration > Reports"
+    And I navigate to "Reports > Course completion" in current page administration
     And I follow "Click to mark user complete"
     # Running completion task just after clicking sometimes fail, as record
     # should be created before the task runs.
@@ -55,7 +55,7 @@ Feature: Enable Block Completion in a course using manual completion by others
     Given I log in as "teacher1"
     And I am on "Course 1" course homepage with editing mode on
     And I add the "Course completion status" block
-    And I navigate to "Course completion" node in "Course administration"
+    And I navigate to "Course completion" in current page administration
     And I expand all fieldsets
     And I set the following fields to these values:
       | Teacher             | 1 |
@@ -71,7 +71,7 @@ Feature: Enable Block Completion in a course using manual completion by others
     And I log out
     And I log in as "teacher1"
     And I am on "Course 1" course homepage
-    And I navigate to "Course completion" node in "Course administration > Reports"
+    And I navigate to "Reports > Course completion" in current page administration
     And I follow "Click to mark user complete"
     And I log out
     And I log in as "student1"
@@ -85,7 +85,7 @@ Feature: Enable Block Completion in a course using manual completion by others
     And I log out
     And I log in as "teacher2"
     And I am on "Course 1" course homepage
-    And I navigate to "Course completion" node in "Course administration > Reports"
+    And I navigate to "Reports > Course completion" in current page administration
     And I follow "Click to mark user complete"
     # Running completion task just after clicking sometimes fail, as record
     # should be created before the task runs.
index d9b73c5..32a30ef 100644 (file)
@@ -20,7 +20,7 @@ Feature: Enable Block Completion in a course using manual self completion
     And I am on "Course 1" course homepage with editing mode on
     And I add the "Course completion status" block
     And I add the "Self completion" block
-    And I navigate to "Course completion" node in "Course administration"
+    And I navigate to "Course completion" in current page administration
     And I expand all fieldsets
     And I set the following fields to these values:
       | id_criteria_self | 1 |
index b10cf27..93dfa54 100644 (file)
@@ -28,7 +28,7 @@ Feature: Enable the course_list block on the frontpage and view it's contents
   Scenario: Add the course list block on the frontpage and navigate to the course listing
     Given I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Courses" block
     And I log out
     When I log in as "teacher1"
@@ -43,7 +43,7 @@ Feature: Enable the course_list block on the frontpage and view it's contents
   Scenario: Add the course list block on the frontpage page and navigate to another course
     Given I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Courses" block
     And I log out
     When I log in as "teacher1"
@@ -58,7 +58,7 @@ Feature: Enable the course_list block on the frontpage and view it's contents
   Scenario: Add the course list block on the frontpage page and view as an admin
     Given I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     When I add the "Courses" block
     Then I should see "Miscellaneous" in the "Course categories" "block"
     And I should see "Category 1" in the "Course categories" "block"
@@ -72,7 +72,7 @@ Feature: Enable the course_list block on the frontpage and view it's contents
   Scenario: Add the course list block on the frontpage page and view as a guest
     Given I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Courses" block
     And I log out
     When I log in as "guest"
index a48fb70..ac91d6d 100644 (file)
@@ -9,7 +9,7 @@ Feature: Course summary block used on the frontpage
     And I am on site homepage
     And I turn editing mode on
     And I add the "Course/site summary" block
-    And I navigate to "Edit settings" node in "Front page settings"
+    And I navigate to "Edit settings" in current page administration
     And I set the following fields to these values:
       | summary | Proved the summary block works! |
     And I press "Save changes"
index caaa06b..2f86023 100644 (file)
@@ -10,7 +10,7 @@ Feature: Login from a block
       | testuser | testpass | Test      | User     | student1@example.com |
     And I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Login" block
 
   Scenario: Login block visible to non-logged in users
index 5b14df3..1e67078 100644 (file)
@@ -10,7 +10,7 @@ Feature: The logged in user block allows users to view their profile information
       | teacher1 | Teacher   | One      | teacher1@example.com | T1       |
     And I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Logged in user" block
     And I log out
 
index 683c34a..b63a587 100644 (file)
@@ -46,7 +46,7 @@ Feature: Expand the courses nodes within the navigation block
     And I press "Save changes"
     And I turn editing mode off
     And I am on "Course 2" course homepage
-    And I navigate to "Enrolment methods" node in "Course administration > Users"
+    And I navigate to "Users > Enrolment methods" in current page administration
     And I click on "Edit" "link" in the "Guest access" "table_row"
     And I set the following fields to these values:
       | Allow guest access | Yes |
index 0c5bbdd..097c9e7 100644 (file)
@@ -13,7 +13,7 @@ Feature: The online users block allow you to see who is currently online on fron
   Scenario: View the online users block on the front page and see myself
     Given I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     When I add the "Online users" block
     Then I should see "Admin User" in the "Online users" "block"
     And I should see "1 online user" in the "Online users" "block"
@@ -21,7 +21,7 @@ Feature: The online users block allow you to see who is currently online on fron
   Scenario: View the online users block on the front page as a logged in user
     Given I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Online users" block
     And I log out
     And I log in as "student2"
@@ -36,7 +36,7 @@ Feature: The online users block allow you to see who is currently online on fron
   Scenario: View the online users block on the front page as a guest
     Given I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Online users" block
     And I log out
     And I log in as "student2"
@@ -54,7 +54,7 @@ Feature: The online users block allow you to see who is currently online on fron
   Scenario: Hide/show user's online status from/to other users in the online users block on front page
     Given I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Online users" block
     And I log out
     When I log in as "student1"
index 93b6a57..3e47bd2 100644 (file)
@@ -10,7 +10,7 @@ Feature: People Block used on frontpage
       | student1    | Sam       | Student  | student1@example.com |
     And I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "People" block
     And I log out
 
index 7816060..0e79d0a 100644 (file)
@@ -13,7 +13,7 @@ Feature: The private files block allows users to store files privately in moodle
       | teacher1 | Teacher | 1 | teacher1@example.com |
     And I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Private files" block
     And I log out
 
index c3f34c7..13dc420 100644 (file)
@@ -50,9 +50,12 @@ class cleanup extends \core\task\scheduled_task {
      * Remove old entries from table block_recent_activity
      */
     public function execute() {
-        global $DB;
+        global $CFG, $DB;
+        require_once("{$CFG->dirroot}/course/lib.php");
+
         // Those entries will never be displayed as RECENT anyway.
-        $DB->delete_records_select('block_recent_activity', 'timecreated < ?',
-            array(time() - COURSE_MAX_RECENT_PERIOD));
+        $DB->delete_records_select('block_recent_activity', 'timecreated < ?', [
+                time() - COURSE_MAX_RECENT_PERIOD,
+            ]);
     }
 }
index 2a2fd78..3b33049 100644 (file)
@@ -30,9 +30,6 @@
  */
 
  class block_rss_client extends block_base {
-    /** The maximum time in seconds that cron will wait between attempts to retry failing RSS feeds. */
-    const CLIENT_MAX_SKIPTIME = 43200; // 60 * 60 * 12 seconds.
-
     /** @var bool track whether any of the output feeds have recorded failures */
     private $hasfailedfeeds = false;
 
             return core_text::substr($title, 0, $max - 3) . '...';
         }
     }
-
-    /**
-     * Calculates a new skip time for a record based on the current skip time.
-     *
-     * @param int $currentskip The curreent skip time of a record.
-     * @return int A new skip time that should be set.
-     */
-    public function calculate_skiptime($currentskip) {
-        // The default time to skiptime.
-        $newskiptime = $this->cron * 1.1;
-        if ($currentskip > 0) {
-            // Double the last time.
-            $newskiptime = $currentskip * 2;
-        }
-        if ($newskiptime > self::CLIENT_MAX_SKIPTIME) {
-            // Do not allow the skip time to increase indefinatly.
-            $newskiptime = self::CLIENT_MAX_SKIPTIME;
-        }
-        return $newskiptime;
-    }
 }
index 3e34e1b..3e12ab2 100644 (file)
@@ -37,6 +37,9 @@ defined('MOODLE_INTERNAL') || die();
  */
 class refreshfeeds extends \core\task\scheduled_task {
 
+    /** The maximum time in seconds that cron will wait between attempts to retry failing RSS feeds. */
+    const CLIENT_MAX_SKIPTIME = HOURSECS * 12;
+
     /**
      * Name for this task.
      *
@@ -65,7 +68,7 @@ class refreshfeeds extends \core\task\scheduled_task {
      */
     public function execute() {
         global $CFG, $DB;
-        require_once($CFG->libdir.'/simplepie/moodle_simplepie.php');
+        require_once("{$CFG->libdir}/simplepie/moodle_simplepie.php");
 
         // We are going to measure execution times.
         $starttime = microtime();
@@ -84,22 +87,11 @@ class refreshfeeds extends \core\task\scheduled_task {
                 continue;
             }
 
-            // Fetch the rss feed, using standard simplepie caching
-            // so feeds will be renewed only if cache has expired.
-            \core_php_time_limit::raise(60);
-
-            $feed = new \moodle_simplepie();
-            // Set timeout for longer than normal to be agressive at
-            // fetching feeds if possible..
-            $feed->set_timeout(40);
-            $feed->set_cache_duration(0);
-            $feed->set_feed_url($rec->url);
-            $feed->init();
+            $feed = $this->fetch_feed($rec->url);
 
             if ($feed->error()) {
                 // Skip this feed (for an ever-increasing time if it keeps failing).
-                $block = new \block_rss_client();
-                $rec->skiptime = $block->calculate_skiptime($rec->skiptime);
+                $rec->skiptime = $this->calculate_skiptime($rec->skiptime);
                 $rec->skipuntil = time() + $rec->skiptime;
                 $DB->update_record('block_rss_client', $rec);
                 mtrace("Error: could not load/find the RSS feed - skipping for {$rec->skiptime} seconds.");
@@ -119,6 +111,41 @@ class refreshfeeds extends \core\task\scheduled_task {
 
         // Show times.
         mtrace($counter . ' feeds refreshed (took ' . microtime_diff($starttime, microtime()) . ' seconds)');
+    }
+
+    /**
+     * Fetch a feed for the specified URL.
+     *
+     * @param   string  $url The URL to fetch
+     * @return  \moodle_simplepie
+     */
+    protected function fetch_feed(string $url) : \moodle_simplepie {
+        // Fetch the rss feed, using standard simplepie caching so feeds will be renewed only if cache has expired.
+        \core_php_time_limit::raise(60);
+
+        $feed = new \moodle_simplepie();
+
+        // Set timeout for longer than normal to be agressive at fetching feeds if possible..
+        $feed->set_timeout(40);
+        $feed->set_cache_duration(0);
+        $feed->set_feed_url($url);
+        $feed->init();
+
+        return $feed;
+    }
+
+    /**
+     * Calculates a new skip time for a record based on the current skip time.
+     *
+     * @param   int     $currentskip The current skip time of a record.
+     * @return  int     The newly calculated skip time.
+     */
+    protected function calculate_skiptime(int $currentskip) : int {
+        // If the feed has never failed, then the initial skiptime will be 0. We use a default of 5 minutes in this case.
+        // If the feed has previously failed then we double that time.
+        $newskiptime = max(MINSECS * 5, ($currentskip * 2));
 
+        // Max out at the CLIENT_MAX_SKIPTIME.
+        return min($newskiptime, self::CLIENT_MAX_SKIPTIME);
     }
 }
index bae4cde..3b27084 100644 (file)
@@ -68,82 +68,74 @@ class block_rss_client_cron_testcase extends advanced_testcase {
         $this->assertContains('0 feeds refreshed (took ', $cronoutput);
     }
 
+    /**
+     * Data provider for skip time tests.
+     *
+     * @return  array
+     */
+    public function skip_time_increase_provider() : array {
+        return [
+            'Never failed' => [
+                'skiptime' => 0,
+                'skipuntil' => 0,
+                'newvalue' => MINSECS * 5,
+            ],
+            'Failed before' => [
+                // This should just double the time.
+                'skiptime' => 330,
+                'skipuntil' => time(),
+                'newvalue' => 660,
+            ],
+            'Near max' => [
+                'skiptime' => \block_rss_client\task\refreshfeeds::CLIENT_MAX_SKIPTIME - 5,
+                'skipuntil' => time(),
+                'newvalue' => \block_rss_client\task\refreshfeeds::CLIENT_MAX_SKIPTIME,
+            ],
+        ];
+    }
+
     /**
      * Test that when a feed has an error the skip time is increased correctly.
+     *
+     * @dataProvider    skip_time_increase_provider
      */
-    public function test_error() {
+    public function test_error($skiptime, $skipuntil, $newvalue) {
         global $DB, $CFG;
         $this->resetAfterTest();
+
+        require_once("{$CFG->libdir}/simplepie/moodle_simplepie.php");
+
         $time = time();
         // A record that has failed before.
-        $record = (object) array(
+        $record = (object) [
             'userid' => 1,
             'title' => 'Skip test feed',
             'preferredtitle' => '',
             'description' => 'A feed to test the skip time.',
             'shared' => 0,
             'url' => 'http://example.com/rss',
-            'skiptime' => 330,
-            'skipuntil' => $time - 300,
-        );
+            'skiptime' => $skiptime,
+            'skipuntil' => $skipuntil,
+        ];
         $record->id = $DB->insert_record('block_rss_client', $record);
 
-        // A record that has not failed before.
-        $record2 = (object) array(
-            'userid' => 1,
-            'title' => 'Skip test feed',
-            'preferredtitle' => '',
-            'description' => 'A feed to test the skip time.',
-            'shared' => 0,
-            'url' => 'http://example.com/rss2',
-            'skiptime' => 0,
-            'skipuntil' => 0,
-        );
-        $record2->id = $DB->insert_record('block_rss_client', $record2);
+        // Run the scheduled task and have it fail.
+        $task = $this->getMockBuilder(\block_rss_client\task\refreshfeeds::class)
+            ->setMethods(['fetch_feed'])
+            ->getMock();
 
-        // A record that is near the maximum wait time.
-        $record3 = (object) array(
-            'userid' => 1,
-            'title' => 'Skip test feed',
-            'preferredtitle' => '',
-            'description' => 'A feed to test the skip time.',
-            'shared' => 0,
-            'url' => 'http://example.com/rss3',
-            'skiptime' => block_rss_client::CLIENT_MAX_SKIPTIME - 5,
-            'skipuntil' => $time - 1,
-        );
-        $record3->id = $DB->insert_record('block_rss_client', $record3);
+        $piemock = $this->getMockBuilder(\moodle_simplepie::class)
+            ->setMethods(['error'])
+            ->getMock();
 
-        // Run the scheduled task.
-        $task = new \block_rss_client\task\refreshfeeds();
-        ob_start();
+        $piemock->method('error')
+            ->willReturn(true);
 
-        // Silence SimplePie php notices.
-        $errorlevel = error_reporting($CFG->debug & ~E_USER_NOTICE);
-        $task->execute();
-        error_reporting($errorlevel);
+        $task->method('fetch_feed')
+            ->willReturn($piemock);
 
-        $cronoutput = ob_get_clean();
-        $skiptime1 = $record->skiptime * 2;
-        $message1 = 'http://example.com/rss Error: could not load/find the RSS feed - skipping for ' . $skiptime1 . ' seconds.';
-        $this->assertContains($message1, $cronoutput);
-        $skiptime2 = 0;
-        $message2 = 'http://example.com/rss2 Error: could not load/find the RSS feed - skipping for ' . $skiptime2 . ' seconds.';
-        $this->assertContains($message2, $cronoutput);
-        $skiptime3 = block_rss_client::CLIENT_MAX_SKIPTIME;
-        $message3 = 'http://example.com/rss3 Error: could not load/find the RSS feed - skipping for ' . $skiptime3 . ' seconds.';
-        $this->assertContains($message3, $cronoutput);
-        $this->assertContains('0 feeds refreshed (took ', $cronoutput);
-
-        // Test that the records have been correctly updated.
-        $newrecord = $DB->get_record('block_rss_client', array('id' => $record->id));
-        $this->assertAttributeEquals($skiptime1, 'skiptime', $newrecord);
-        $this->assertAttributeGreaterThanOrEqual($time + $skiptime1, 'skipuntil', $newrecord);
-        $newrecord2 = $DB->get_record('block_rss_client', array('id' => $record2->id));
-        $this->assertAttributeEquals($skiptime2, 'skiptime', $newrecord2);
-        $this->assertAttributeGreaterThanOrEqual($time + $skiptime2, 'skipuntil', $newrecord2);
-        $newrecord3 = $DB->get_record('block_rss_client', array('id' => $record3->id));
-        $this->assertAttributeEquals($skiptime3, 'skiptime', $newrecord3);
-        $this->assertAttributeGreaterThanOrEqual($time + $skiptime3, 'skipuntil', $newrecord3);
+        // Run the cron and capture its output.
+        $this->expectOutputRegex("/.*Error: could not load\/find the RSS feed - skipping for {$newvalue} seconds.*/");
+        $task->execute();
     }
 }
index e2e37d2..dfdeb6f 100644 (file)
@@ -18,7 +18,7 @@ Feature: The search forums block allows users to search for forum posts on cours
       | student1 | C1 | student |
     And I log in as "teacher1"
     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 "id_newsitems" to "1"
     And I press "Save and display"
     And I turn editing mode on
index 1f76d15..53f3ba9 100644 (file)
@@ -10,7 +10,7 @@ Feature: The search forums block allows users to search for forum posts on front
       | student1 | Student | 1 | student1@example.com | S1 |
     And I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Search forums" block
     And I log out
 
index a9cfb61..dbd6386 100644 (file)
@@ -8,7 +8,7 @@ Feature: Add URL to main menu block
   Scenario: Add a URL in menu block and ensure it appears
     Given I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Main menu" block
     When I add a "URL" to section "0" and I fill the form with:
       | Name | google |
index c577b9f..3b5d632 100644 (file)
@@ -8,7 +8,7 @@ Feature: Edit activities in main menu block
   Scenario: Edit name of acitivity in-place in site main menu block
     Given I log in as "admin"
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Main menu" block
     When I add a "Forum" to section "0" and I fill the form with:
       | Forum name | My forum name |
@@ -27,7 +27,7 @@ Feature: Edit activities in main menu block
     And I set the following administration settings values:
       | allowstealth | 1 |
     And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
+    And I navigate to "Turn editing on" in current page administration
     And I add the "Main menu" block
     When I add a "Forum" to section "0" and I fill the form with:
       | Forum name | Visible forum |
@@ -60,7 +60,7 @@ Feature: Edit activities in main menu block
     And the field "Availability" matches value "Make available but not shown on course page"
     And I press "Save and return to course"
     And "My forum name" activity in site main menu block should be available but hidden from course page
-    And I navigate to "Turn editing off" node in "Front page settings"
+    And I navigate to "Turn editing off" in current page administration
     And "My forum name" activity in site main menu block should be available but hidden from course page
     And I log out
     And I should not see "My forum name" in the "Main menu" "block"
index f213f66..c927827 100644 (file)
@@ -10,7 +10,8 @@ Feature: Adding and configuring Flickr block
     And I press "Customise this page"
     # TODO MDL-57120 site "Tags" link not accessible without navigation block.
     And I add the "Navigation" block if not present
-    And I navigate to "Tags" node in "Site pages"
+    And I click on "Site pages" "list_item" in the "Navigation" "block"
+    And I click on "Tags" "link" in the "Navigation" "block"
     And I add the "Flickr" block
     And I configure the "Flickr" block
     Then I should see "Flickr block title"
index 84fefa7..3df500d 100644 (file)
@@ -20,7 +20,7 @@ Feature: Add and configure blocks throughout the site
       | manager1 | Acceptance test site | manager |
     # Allow at least one role assignment in the block context:
     And I log in as "admin"
-    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 "Edit Non-editing teacher role"
     And I set the following fields to these values:
       | Block | 1 |
index f4bae30..ce213bd 100644 (file)
@@ -18,7 +18,7 @@ Feature: Blogs can be set to be only visible by the author.
       | testuser2 | C1     | student |
     And I log in as "admin"
     And I am on site homepage
-    And I navigate to "Blog" node in "Site administration > Appearance"
+    And I navigate to "Appearance > Blog" in site administration
     And I set the following fields to these values:
       | Blog visibility | Users can only see their own blog |
     And I press "Save changes"
index 86099b0..456ac5d 100644 (file)
@@ -20,7 +20,8 @@ Feature: Comment on a blog entry
     And I press "Save changes"
     And I log out
     And I log in as "testuser"
-    And I navigate to "Site blogs" node in "Site pages"
+    And I click on "Site pages" "list_item" in the "Navigation" "block"
+    And I click on "Site blogs" "link" in the "Navigation" "block"
     And I follow "Add a new entry"
     And I set the following fields to these values:
       | Entry title | Blog post from user 1 |
@@ -32,7 +33,8 @@ Feature: Comment on a blog entry
   Scenario: Commenting on my own blog entry
     Given I am on site homepage
     And I log in as "testuser"
-    And I navigate to "Site blogs" node in "Site pages"
+    And I click on "Site pages" "list_item" in the "Navigation" "block"
+    And I click on "Site blogs" "link" in the "Navigation" "block"
     And I follow "Blog post from user 1"
     And I should see "User 1 blog post content"
     And I follow "Comments (0)"
@@ -47,7 +49,8 @@ Feature: Comment on a blog entry
   Scenario: Deleting my own comment
     Given I am on site homepage
     And I log in as "testuser"
-    And I navigate to "Site blogs" node in "Site pages"
+    And I click on "Site pages" "list_item" in the "Navigation" "block"
+    And I click on "Site blogs" "link" in the "Navigation" "block"
     And I follow "Blog post from user 1"
     And I should see "User 1 blog post content"
     And I follow "Comments (0)"
@@ -67,7 +70,8 @@ Feature: Comment on a blog entry
     Given I am on site homepage
     And I log in as "testuser2"
     And I am on site homepage
-    And I navigate to "Site blogs" node in "Site pages"
+    And I click on "Site pages" "list_item" in the "Navigation" "block"
+    And I click on "Site blogs" "link" in the "Navigation" "block"
     And I follow "Blog post from user 1"
     When I follow "Comments (0)"
     And I set the field "content" to "$My own >nasty< \"string\"!"
index 950577b..0119820 100644 (file)
@@ -19,7 +19,7 @@ Feature: Delete a blog entry
     And I press "Save changes"
     And I log out
     And I log in as "testuser"
-    And I navigate to "Site blogs" node in "Site pages"
+    And I click on "Site blogs" "link" in the "Navigation" "block"
     And I follow "Add a new entry"
     And I set the following fields to these values:
       | Entry title | Blog post one |
@@ -31,7 +31,7 @@ Feature: Delete a blog entry
       | Blog entry body | User 1 blog post content |
     And I press "Save changes"
     And I am on site homepage
-    And I navigate to "Site blogs" node in "Site pages"
+    And I click on "Site blogs" "link" in the "Navigation" "block"
 
   Scenario: Delete blog post results in post deleted
     Given I follow "Blog post one"
index c2aa399..2776339 100644 (file)
@@ -240,6 +240,7 @@ class cache_helper {
         $invalidationeventset = false;
         $factory = cache_factory::instance();
         $inuse = $factory->get_caches_in_use();
+        $purgetoken = null;
         foreach ($instance->get_definitions() as $name => $definitionarr) {
             $definition = cache_definition::load($name, $definitionarr);
             if ($definition->invalidates_on_event($event)) {
@@ -266,8 +267,11 @@ class cache_helper {
                         $data = array();
                     }
                     // Add our keys to them with the current cache timestamp.
+                    if (null === $purgetoken) {
+                        $purgetoken = cache::get_purge_token(true);
+                    }
                     foreach ($keys as $key) {
-                        $data[$key] = cache::now();
+                        $data[$key] = $purgetoken;
                     }
                     // Set that data back to the cache.
                     $cache->set($event, $data);
@@ -315,6 +319,7 @@ class cache_helper {
         $invalidationeventset = false;
         $factory = cache_factory::instance();
         $inuse = $factory->get_caches_in_use();
+        $purgetoken = null;
         foreach ($instance->get_definitions() as $name => $definitionarr) {
             $definition = cache_definition::load($name, $definitionarr);
             if ($definition->invalidates_on_event($event)) {
@@ -338,8 +343,11 @@ class cache_helper {
                     // Get the event invalidation cache.
                     $cache = cache::make('core', 'eventinvalidation');
                     // Create a key to invalidate all.
+                    if (null === $purgetoken) {
+                        $purgetoken = cache::get_purge_token(true);
+                    }
                     $data = array(
-                        'purged' => cache::now()
+                        'purged' => $purgetoken,
                     );
                     // Set that data back to the cache.
                     $cache->set($event, $data);
index 168124d..691d2fb 100644 (file)
@@ -50,6 +50,14 @@ class cache implements cache_loader {
      */
     protected static $now;
 
+    /**
+     * A purge token used to distinguish between multiple cache purges in the same second.
+     * This is in the format <microtime>-<random string>.
+     *
+     * @var string
+     */
+    protected static $purgetoken;
+
     /**
      * The definition used when loading this cache if there was one.
      * @var cache_definition
@@ -286,33 +294,58 @@ class cache implements cache_loader {
             return;
         }
 
+        // Each cache stores the current 'lastinvalidation' value within the cache itself.
         $lastinvalidation = $this->get('lastinvalidation');
         if ($lastinvalidation === false) {
-            // This is a new cache or purged globally, there won't be anything to invalidate.
-            // Set the time of the last invalidation and move on.
-            $this->set('lastinvalidation', self::now());
+            // There is currently no  value for the lastinvalidation token, therefore the token is not set, and there
+            // can be nothing to invalidate.
+            // Set the lastinvalidation value to the current purge token and return early.
+            $this->set('lastinvalidation', self::get_purge_token());
             return;
-        } else if ($lastinvalidation == self::now()) {
-            // We've already invalidated during this request.
+        } else if ($lastinvalidation == self::get_purge_token()) {
+            // The current purge request has already been fully handled by this cache.
             return;
         }
 
-        // Get the event invalidation cache.
+        /*
+         * Now that the whole cache check is complete, we check the meaning of any specific cache invalidation events.
+         * These are stored in the core/eventinvalidation cache as an multi-dimensinoal array in the form:
+         *  [
+         *      eventname => [
+         *          keyname => purgetoken,
+         *      ]
+         *  ]
+         *
+         * The 'keyname' value is used to delete a specific key in the cache.
+         * If the keyname is set to the special value 'purged', then the whole cache is purged instead.
+         *
+         * The 'purgetoken' is the token that this key was last purged.
+         * a) If the purgetoken matches the last invalidation, then the key/cache is not purged.
+         * b) If the purgetoken is newer than the last invalidation, then the key/cache is not purged.
+         * c) If the purge token is older than the last invalidation, or it has a different token component, then the
+         *    cache is purged.
+         *
+         * Option b should not happen under normal operation, but may happen in race condition whereby a long-running
+         * request's cache is cleared in another process during that request, and prior to that long-running request
+         * creating the cache. In such a condition, it would be incorrect to clear that cache.
+         */
         $cache = self::make('core', 'eventinvalidation');
         $events = $cache->get_many($this->definition->get_invalidation_events());
         $todelete = array();
         $purgeall = false;
+
         // Iterate the returned data for the events.
         foreach ($events as $event => $keys) {
             if ($keys === false) {
                 // No data to be invalidated yet.
                 continue;
             }
+
             // Look at each key and check the timestamp.
-            foreach ($keys as $key => $timestamp) {
+            foreach ($keys as $key => $purgetoken) {
                 // If the timestamp of the event is more than or equal to the last invalidation (happened between the last
-                // invalidation and now)then we need to invaliate the key.
-                if ($timestamp >= $lastinvalidation) {
+                // invalidation and now)then we need to invaliate the key.
+                if (self::compare_purge_tokens($purgetoken, $lastinvalidation) > 0) {
                     if ($key === 'purged') {
                         $purgeall = true;
                         break;
@@ -330,7 +363,7 @@ class cache implements cache_loader {
         }
         // Set the time of the last invalidation.
         if ($purgeall || !empty($todelete)) {
-            $this->set('lastinvalidation', self::now());
+            $this->set('lastinvalidation', self::get_purge_token(true));
         }
     }
 
@@ -1186,13 +1219,70 @@ class cache implements cache_loader {
      * This stamp needs to be used for all ttl and time based operations to ensure that we don't end up with
      * timing issues.
      *
-     * @return int
+     * @param   bool    $float Whether to use floating precision accuracy.
+     * @return  int|float
      */
-    public static function now() {
+    public static function now($float = false) {
         if (self::$now === null) {
-            self::$now = time();
+            self::$now = microtime(true);
+        }
+
+        if ($float) {
+            return self::$now;
+        } else {
+            return (int) self::$now;
+        }
+    }
+
+    /**
+     * Get a 'purge' token used for cache invalidation handling.
+     *
+     * Note: This function is intended for use from within the Cache API only and not by plugins, or cache stores.
+     *
+     * @param   bool    $reset  Whether to reset the token and generate a new one.
+     * @return  string
+     */
+    public static function get_purge_token($reset = false) {
+        if (self::$purgetoken === null || $reset) {
+            self::$now = null;
+            self::$purgetoken = self::now(true) . '-' . uniqid('', true);
+        }
+
+        return self::$purgetoken;
+    }
+
+    /**
+     * Compare a pair of purge tokens.
+     *
+     * If the two tokens are identical, then the return value is 0.
+     * If the time component of token A is newer than token B, then a positive value is returned.
+     * If the time component of token B is newer than token A, then a negative value is returned.
+     *
+     * Note: This function is intended for use from within the Cache API only and not by plugins, or cache stores.
+     *
+     * @param   string  $tokena
+     * @param   string  $tokenb
+     * @return  int
+     */
+    public static function compare_purge_tokens($tokena, $tokenb) {
+        if ($tokena === $tokenb) {
+            // There is an exact match.
+            return 0;
+        }
+
+        // The token for when the cache was last invalidated.
+        list($atime) = explode('-', "{$tokena}-", 2);
+
+        // The token for this cache.
+        list($btime) = explode('-', "{$tokenb}-", 2);
+
+        if ($atime >= $btime) {
+            // Token A is newer.
+            return 1;
+        } else {
+            // Token A is older.
+            return -1;
         }
-        return self::$now;
     }
 }
 
index 8d04cd1..6aa268f 100644 (file)
@@ -1003,7 +1003,7 @@ class core_cache_testcase extends advanced_testcase {
         $timefile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las-cache/lastinvalidation-$hash.cache";
         // Make sure the file is correct.
         $this->assertTrue(file_exists($timefile));
-        $timecont = serialize(cache::now() - 60); // Back 60sec in the past to force it to re-invalidate.
+        $timecont = serialize(cache::now(true) - 60); // Back 60sec in the past to force it to re-invalidate.
         make_writable_directory(dirname($timefile));
         file_put_contents($timefile, $timecont);
         $this->assertTrue(file_exists($timefile));
@@ -1029,6 +1029,7 @@ class core_cache_testcase extends advanced_testcase {
 
         // Test 2: Rebuild and test the invalidation of the event via the invalidation cache.
         cach