Merge branch 'MDL-63135-master' of git://github.com/rezaies/moodle
authorDavid Monllao <davidm@moodle.com>
Wed, 19 Sep 2018 11:12:40 +0000 (13:12 +0200)
committerDavid Monllao <davidm@moodle.com>
Wed, 19 Sep 2018 11:12:40 +0000 (13:12 +0200)
289 files changed:
.eslintignore
.stylelintignore
Gruntfile.js
admin/environment.xml
admin/settings/subsystems.php
admin/tool/cohortroles/lang/en/tool_cohortroles.php
admin/tool/customlang/lang/en/tool_customlang.php
admin/tool/dataprivacy/amd/build/defaultsactions.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/events.min.js
admin/tool/dataprivacy/amd/build/requestactions.min.js
admin/tool/dataprivacy/amd/src/defaultsactions.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/events.js
admin/tool/dataprivacy/amd/src/requestactions.js
admin/tool/dataprivacy/categories.php
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/data_registry.php
admin/tool/dataprivacy/classes/external.php
admin/tool/dataprivacy/classes/external/category_exporter.php
admin/tool/dataprivacy/classes/external/purpose_exporter.php
admin/tool/dataprivacy/classes/form/defaults.php [deleted file]
admin/tool/dataprivacy/classes/local/helper.php
admin/tool/dataprivacy/classes/output/data_registry_page.php
admin/tool/dataprivacy/classes/output/data_requests_page.php
admin/tool/dataprivacy/classes/output/data_requests_table.php
admin/tool/dataprivacy/classes/output/defaults_page.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/page_helper.php
admin/tool/dataprivacy/classes/privacy/provider.php
admin/tool/dataprivacy/createdatarequest.php
admin/tool/dataprivacy/datadeletion.php
admin/tool/dataprivacy/dataregistry.php
admin/tool/dataprivacy/datarequests.php
admin/tool/dataprivacy/db/services.php
admin/tool/dataprivacy/defaults.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/purposes.php
admin/tool/dataprivacy/styles.css
admin/tool/dataprivacy/templates/categories.mustache
admin/tool/dataprivacy/templates/category_purpose_form.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/data_request_email.mustache
admin/tool/dataprivacy/templates/data_requests_bulk_actions.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/defaults_display.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/defaults_page.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/delete_activity_defaults.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/purposes.mustache
admin/tool/dataprivacy/tests/api_test.php
admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php [new file with mode: 0644]
admin/tool/dataprivacy/tests/behat/manage_data_requests.feature
admin/tool/dataprivacy/tests/behat/manage_defaults.feature [new file with mode: 0644]
admin/tool/dataprivacy/tests/external_test.php
admin/tool/dataprivacy/tests/generator/lib.php [new file with mode: 0644]
admin/tool/dataprivacy/version.php
admin/tool/log/store/database/lang/en/logstore_database.php
admin/tool/log/store/legacy/lang/en/logstore_legacy.php
admin/tool/log/store/standard/lang/en/logstore_standard.php
admin/tool/messageinbound/lang/en/tool_messageinbound.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/policy/amd/build/acceptmodal.min.js
admin/tool/policy/amd/src/acceptmodal.js
admin/tool/policy/classes/acceptances_table.php
admin/tool/policy/classes/form/accept_policy.php
admin/tool/policy/lang/en/tool_policy.php
admin/tool/policy/tests/behat/acceptances.feature
admin/tool/usertours/lang/en/tool_usertours.php
auth/ldap/lang/en/auth_ldap.php
backup/util/ui/tests/behat/duplicate_activities.feature
blocks/calendar_month/block_calendar_month.php
blocks/rss_client/classes/output/item.php
blog/lib.php
cache/stores/memcache/addinstanceform.php [deleted file]
cache/stores/memcache/classes/privacy/provider.php [deleted file]
cache/stores/memcache/lang/en/cachestore_memcache.php [deleted file]
cache/stores/memcache/lib.php [deleted file]
cache/stores/memcache/settings.php [deleted file]
cache/stores/memcache/tests/memcache_test.php [deleted file]
cache/stores/memcache/version.php [deleted file]
cache/upgrade.txt
calendar/classes/external/event_exporter_base.php
calendar/lib.php
calendar/templates/event_item.mustache
completion/classes/api.php
completion/tests/behat/completion_no_calendar_capabilities.feature [new file with mode: 0644]
completion/tests/capabilities_test.php [new file with mode: 0644]
config-dist.php
course/amd/build/actions.min.js
course/amd/src/actions.js
course/changenumsections.php
course/classes/management_renderer.php
course/format/lib.php
course/format/renderer.php
course/lib.php
course/tests/behat/course_category_management_listing.feature
course/tests/courselib_test.php
enrol/flatfile/lang/en/enrol_flatfile.php
enrol/lti/lang/en/enrol_lti.php
filter/algebra/algebradebug.php
filter/tex/lib.php
filter/tex/texdebug.php
group/assign.php
install/lang/ca/admin.php
install/lang/el/admin.php
install/lang/gl/error.php
iplookup/tests/geoplugin_test.php
lang/en/admin.php
lang/en/auth.php
lang/en/backup.php
lang/en/badges.php
lang/en/blog.php
lang/en/cohort.php
lang/en/competency.php
lang/en/enrol.php
lang/en/error.php
lang/en/grades.php
lang/en/message.php
lang/en/moodle.php
lang/en/portfolio.php
lang/en/webservice.php
lib/amd/build/form-autocomplete.min.js
lib/amd/src/form-autocomplete.js
lib/classes/plugin_manager.php
lib/classes/session/memcache.php [deleted file]
lib/classes/session/memcached.php
lib/classes/session/util.php [deleted file]
lib/db/upgrade.php
lib/ddl/mssql_sql_generator.php
lib/ddl/mysql_sql_generator.php
lib/ddl/tests/ddl_test.php
lib/editor/atto/lib.php
lib/editor/atto/plugins/html/styles.css [new file with mode: 0644]
lib/editor/atto/plugins/html/thirdpartylibs.xml [new file with mode: 0644]
lib/editor/atto/plugins/html/version.php
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify-debug.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify-min.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-debug.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-min.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/moodle-atto_html-codemirror-core.css [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/skins/sam/moodle-atto_html-codemirror-skin.css [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/skins/sam/moodle-atto_html-codemirror.css [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-debug.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-min.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/LICENSE [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/build.json [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/js/beautify-css.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/js/beautify-html.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/js/beautify.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/js/wrapper.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/meta/beautify.json [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/readme_moodle.txt [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/button/build.json
lib/editor/atto/plugins/html/yui/src/button/js/button.js
lib/editor/atto/plugins/html/yui/src/button/meta/button.json
lib/editor/atto/plugins/html/yui/src/codemirror/LICENSE [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/assets/moodle-atto_html-codemirror-core.css [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/assets/skins/sam/moodle-atto_html-codemirror-skin.css [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/build.json [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/codemirror.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/css.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/htmlmixed.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/javascript.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/wrapper-end.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/wrapper-mid.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/wrapper.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/xml.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/meta/codemirror.json [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/readme_moodle.txt [new file with mode: 0644]
lib/form/filemanager.php
lib/messagelib.php
lib/outputrenderers.php
lib/pear/HTML/QuickForm.php
lib/pear/HTML/QuickForm/element.php
lib/pear/HTML/QuickForm/hierselect.php
lib/pear/HTML/QuickForm/utils.php [new file with mode: 0644]
lib/phpunit/tests/advanced_test.php
lib/templates/form_autocomplete_input.mustache
lib/templates/paging_bar.mustache [moved from theme/boost/templates/core/paging_bar.mustache with 85% similarity]
lib/tests/messagelib_test.php
lib/tests/tablelib_test.php
lib/upgrade.txt
lib/upgradelib.php
message/lib.php
message/output/airnotifier/lang/en/message_airnotifier.php
message/tests/externallib_test.php
message/tests/messagelib_test.php
mnet/service/enrol/lang/en/mnetservice_enrol.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/mod_form.php
mod/assign/tests/behat/assign_no_calendar_capabilities.feature [new file with mode: 0644]
mod/assign/tests/lib_test.php
mod/book/lib.php
mod/book/tests/lib_test.php
mod/chat/lib.php
mod/chat/tests/behat/chat_no_calendar_capabilities.feature [new file with mode: 0644]
mod/chat/tests/lib_test.php
mod/choice/locallib.php
mod/choice/tests/behat/choice_no_calendar_capabilities.feature [new file with mode: 0644]
mod/choice/tests/lib_test.php
mod/data/edit.php
mod/data/export.php
mod/data/field.php
mod/data/lib.php
mod/data/locallib.php
mod/data/preset.php
mod/data/templates.php
mod/data/tests/behat/data_no_calendar_capabilities.feature [new file with mode: 0644]
mod/data/tests/lib_test.php
mod/feedback/analysis_to_excel.php
mod/feedback/classes/complete_form.php
mod/feedback/classes/external.php
mod/feedback/item/multichoice/lib.php
mod/feedback/lib.php
mod/feedback/tests/behat/feedback_no_calendar_capabilities.feature [new file with mode: 0644]
mod/feedback/tests/external_test.php
mod/feedback/tests/lib_test.php
mod/feedback/upgrade.txt
mod/folder/lib.php
mod/folder/tests/lib_test.php
mod/forum/lib.php
mod/forum/tests/lib_test.php
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/tests/behat/lesson_no_calendar_capabilities.feature [new file with mode: 0644]
mod/lesson/tests/lib_test.php
mod/lti/lang/en/lti.php
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/report/attemptsreport.php
mod/quiz/tests/behat/quiz_no_calendar_capabilities.feature [new file with mode: 0644]
mod/quiz/tests/lib_test.php
mod/scorm/locallib.php
mod/scorm/tests/behat/scorm_no_calendar_capabilities.feature [new file with mode: 0644]
mod/scorm/tests/lib_test.php
mod/workshop/amd/build/modform.min.js [new file with mode: 0644]
mod/workshop/amd/src/modform.js [new file with mode: 0644]
mod/workshop/backup/moodle1/lib.php
mod/workshop/backup/moodle2/backup_workshop_stepslib.php
mod/workshop/backup/moodle2/restore_workshop_stepslib.php
mod/workshop/classes/external/workshop_summary_exporter.php
mod/workshop/db/install.xml
mod/workshop/db/upgrade.php
mod/workshop/lang/en/workshop.php
mod/workshop/lib.php
mod/workshop/locallib.php
mod/workshop/mod_form.php
mod/workshop/submission_form.php
mod/workshop/tests/behat/delete_submission.feature
mod/workshop/tests/behat/export_submission.feature
mod/workshop/tests/behat/file_type_restriction.feature
mod/workshop/tests/behat/submission_types.feature [new file with mode: 0644]
mod/workshop/tests/behat/workshop_assessment.feature
mod/workshop/tests/external_test.php
mod/workshop/upgrade.txt
mod/workshop/version.php
question/editlib.php
question/engine/questionattempt.php
question/format.php
question/format/gift/tests/behat/import_export.feature
question/format/upgrade.txt
question/format/xml/format.php
question/format/xml/tests/fixtures/categories_reverse_order.xml [new file with mode: 0644]
question/format/xml/tests/fixtures/category_with_description.xml [new file with mode: 0644]
question/format/xml/tests/fixtures/export_category.xml [new file with mode: 0644]
question/format/xml/tests/fixtures/nested_categories.xml [new file with mode: 0644]
question/format/xml/tests/fixtures/nested_categories_with_questions.xml [new file with mode: 0644]
question/format/xml/tests/fixtures/old_format_file.xml [new file with mode: 0644]
question/format/xml/tests/fixtures/truefalse.xml
question/format/xml/tests/qformat_xml_import_export_test.php [new file with mode: 0644]
question/format/xml/tests/xmlformat_test.php
question/type/ddwtos/questiontype.php
question/type/ddwtos/tests/questiontype_test.php
repository/url/lang/en/repository_url.php
theme/boost/classes/output/core_renderer.php
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/drawer.scss
theme/boost/style/moodle.css
theme/boost/templates/core/form_autocomplete_input.mustache
theme/boost/templates/core/navbar.mustache
user/lib.php
user/profile/field/checkbox/lang/en/profilefield_checkbox.php
user/profile/field/datetime/lang/en/profilefield_datetime.php
user/profile/field/menu/lang/en/profilefield_menu.php
user/profile/field/text/lang/en/profilefield_text.php
user/profile/field/textarea/lang/en/profilefield_textarea.php
user/tests/behat/filter_participants.feature
version.php

index 6345306..420eb21 100644 (file)
@@ -9,6 +9,8 @@ auth/cas/CAS/
 enrol/lti/ims-blti/
 filter/algebra/AlgParser.pm
 filter/tex/mimetex.*
+lib/editor/atto/plugins/html/yui/src/codemirror/
+lib/editor/atto/plugins/html/yui/src/beautify/
 lib/editor/atto/yui/src/rangy/js/*.*
 lib/editor/tinymce/plugins/pdw/tinymce/
 lib/editor/tinymce/plugins/spellchecker/rpc.php
index b0dc0dc..be8cec8 100644 (file)
@@ -1,4 +1,5 @@
 # Generated by "grunt ignorefiles"
+**/yui/build/*
 theme/bootstrapbase/style/
 theme/clean/style/custom.css
 theme/more/style/custom.css
@@ -11,6 +12,8 @@ auth/cas/CAS/
 enrol/lti/ims-blti/
 filter/algebra/AlgParser.pm
 filter/tex/mimetex.*
+lib/editor/atto/plugins/html/yui/src/codemirror/
+lib/editor/atto/plugins/html/yui/src/beautify/
 lib/editor/atto/yui/src/rangy/js/*.*
 lib/editor/tinymce/plugins/pdw/tinymce/
 lib/editor/tinymce/plugins/spellchecker/rpc.php
index 78583d0..29d271c 100644 (file)
@@ -222,6 +222,7 @@ module.exports = function(grunt) {
       // Generate .stylelintignore.
       var stylelintIgnores = [
           '# Generated by "grunt ignorefiles"',
+          '**/yui/build/*',
           'theme/bootstrapbase/style/',
           'theme/clean/style/custom.css',
           'theme/more/style/custom.css',
index 2b8d4ef..fbda3f3 100644 (file)
       </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
+  <MOODLE version="3.6" requires="3.1">
+    <UNICODE level="required">
+      <FEEDBACK>
+        <ON_ERROR message="unicoderequired" />
+      </FEEDBACK>
+    </UNICODE>
+    <DATABASE level="required">
+      <VENDOR name="mariadb" version="5.5.31" />
+      <VENDOR name="mysql" version="5.6" />
+      <VENDOR name="postgres" version="9.4" />
+      <VENDOR name="mssql" version="10.0" />
+      <VENDOR name="oracle" version="11.2" />
+    </DATABASE>
+    <PHP version="7.0.0" level="required">
+    </PHP>
+    <PCREUNICODE level="optional">
+      <FEEDBACK>
+        <ON_CHECK message="pcreunicodewarning" />
+      </FEEDBACK>
+    </PCREUNICODE>
+    <PHP_EXTENSIONS>
+      <PHP_EXTENSION name="iconv" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="iconvrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="mbstring" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="mbstringrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="curl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="curlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="openssl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="opensslrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="tokenizer" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="tokenizerrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlrpc" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrpcrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="soap" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="soaprecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="ctype" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ctyperequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zip" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ziprequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zlib" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="gd" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="gdrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="simplexml" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="simplexmlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="spl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="splrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="pcre" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="dom" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlreader" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="intl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="intlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="json" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="hash" level="required"/>
+      <PHP_EXTENSION name="fileinfo" level="required"/>
+    </PHP_EXTENSIONS>
+    <PHP_SETTINGS>
+      <PHP_SETTING name="memory_limit" value="96M" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="settingmemorylimit" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="file_uploads" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingfileuploads" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="opcache.enable" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opcacherecommended" />
+        </FEEDBACK>
+      </PHP_SETTING>
+    </PHP_SETTINGS>
+    <CUSTOM_CHECKS>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbstorageengine" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="quizattemptsupgradedmessage" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="slashargumentswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unsupporteddbtablerowformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unoconvwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="libcurlwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfileformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfilepertable" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddblargeprefix" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="ishttpswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="incompleteunicodesupport" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="sixtyfourbitswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+    </CUSTOM_CHECKS>
+  </MOODLE>
 </COMPATIBILITY_MATRIX>
index f2b50d5..db26385 100644 (file)
@@ -15,7 +15,11 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
 
     $optionalsubsystems->add(new admin_setting_configcheckbox('messaging', new lang_string('messaging', 'admin'), new lang_string('configmessaging','admin'), 1));
 
-    $optionalsubsystems->add(new admin_setting_configcheckbox('messaginghidereadnotifications', new lang_string('messaginghidereadnotifications', 'admin'), new lang_string('configmessaginghidereadnotifications','admin'), 0));
+    $optionalsubsystems->add(new admin_setting_configcheckbox('messagingallusers',
+        new lang_string('messagingallusers', 'admin'),
+        new lang_string('configmessagingallusers', 'admin'),
+        0)
+    );
 
     $options = array(DAYSECS=>new lang_string('secondstotime86400'), WEEKSECS=>new lang_string('secondstotime604800'), 2620800=>new lang_string('nummonths', 'moodle', 1), 15724800=>new lang_string('nummonths', 'moodle', 6),0=>new lang_string('never'));
     $optionalsubsystems->add(new admin_setting_configselect('messagingdeletereadnotificationsdelay', new lang_string('messagingdeletereadnotificationsdelay', 'admin'), new lang_string('configmessagingdeletereadnotificationsdelay', 'admin'), 604800, $options));
index a8afe83..2f6eb21 100644 (file)
@@ -43,10 +43,10 @@ $string['selectusers'] = 'Select users to assign role';
 $string['taskname'] = 'Sync cohort role assignments';
 $string['thisuserroles'] = 'Roles assigned relative to this user';
 $string['privacy:metadata:tool_cohortroles'] = 'The Cohort roles management plugin stores user cohort role mappings.';
-$string['privacy:metadata:tool_cohortroles:id'] = 'The ID of the cohort role mapping record.';
-$string['privacy:metadata:tool_cohortroles:cohortid'] = 'The ID of the cohort.';
-$string['privacy:metadata:tool_cohortroles:roleid'] = 'The ID of the role.';
-$string['privacy:metadata:tool_cohortroles:userid'] = 'The ID of the user.';
-$string['privacy:metadata:tool_cohortroles:timecreated'] = 'The date/time of when the cohort  role mapping was created.';
-$string['privacy:metadata:tool_cohortroles:timemodified'] = 'The date/time of when the cohort role mapping was modified.';
-$string['privacy:metadata:tool_cohortroles:usermodified'] = 'The ID of the user who last modified the cohort role mapping.';
+$string['privacy:metadata:tool_cohortroles:id'] = 'The ID of the cohort role mapping record';
+$string['privacy:metadata:tool_cohortroles:cohortid'] = 'The ID of the cohort';
+$string['privacy:metadata:tool_cohortroles:roleid'] = 'The ID of the role';
+$string['privacy:metadata:tool_cohortroles:userid'] = 'The ID of the user';
+$string['privacy:metadata:tool_cohortroles:timecreated'] = 'The time when the cohort role mapping was created';
+$string['privacy:metadata:tool_cohortroles:timemodified'] = 'The time when the cohort role mapping was modified';
+$string['privacy:metadata:tool_cohortroles:usermodified'] = 'The ID of the user who last modified the cohort role mapping';
index 7192622..9e7a2d6 100644 (file)
@@ -30,7 +30,7 @@ $string['checkin'] = 'Save strings to language pack';
 $string['checkout'] = 'Open language pack for editing';
 $string['checkoutdone'] = 'Language pack loaded';
 $string['checkoutinprogress'] = 'Loading language pack';
-$string['confirmcheckin'] = 'You are about to save modifications to your local language pack. This will export the customised strings from the translator into you Moodle data directory and Moodle will start using the modified strings. Press \'Continue\' to proceed with saving.';
+$string['confirmcheckin'] = 'You are about to save modifications to your local language pack. This will export the customised strings from the translator into your site data directory and your site will start using the modified strings. Press \'Continue\' to proceed with saving.';
 $string['customlang:edit'] = 'Edit local translation';
 $string['customlang:view'] = 'View local translation';
 $string['filter'] = 'Filter strings';
diff --git a/admin/tool/dataprivacy/amd/build/defaultsactions.min.js b/admin/tool/dataprivacy/amd/build/defaultsactions.min.js
new file mode 100644 (file)
index 0000000..b8f0bc3
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/defaultsactions.min.js differ
index 0ecae4c..6c94ab3 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/events.min.js and b/admin/tool/dataprivacy/amd/build/events.min.js differ
index c405d17..2260ee3 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/requestactions.min.js and b/admin/tool/dataprivacy/amd/build/requestactions.min.js differ
diff --git a/admin/tool/dataprivacy/amd/src/defaultsactions.js b/admin/tool/dataprivacy/amd/src/defaultsactions.js
new file mode 100644 (file)
index 0000000..76cbb89
--- /dev/null
@@ -0,0 +1,315 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * AMD module for data registry defaults actions.
+ *
+ * @module     tool_dataprivacy/defaultsactions
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+    'jquery',
+    'core/ajax',
+    'core/notification',
+    'core/str',
+    'core/modal_factory',
+    'core/modal_events',
+    'core/templates'],
+function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
+
+    /**
+     * List of action selectors.
+     *
+     * @type {{EDIT_LEVEL_DEFAULTS: string}}
+     * @type {{NEW_ACTIVITY_DEFAULTS: string}}
+     * @type {{EDIT_ACTIVITY_DEFAULTS: string}}
+     * @type {{DELETE_ACTIVITY_DEFAULTS: string}}
+     */
+    var ACTIONS = {
+        EDIT_LEVEL_DEFAULTS: '[data-action="edit-level-defaults"]',
+        NEW_ACTIVITY_DEFAULTS: '[data-action="new-activity-defaults"]',
+        EDIT_ACTIVITY_DEFAULTS: '[data-action="edit-activity-defaults"]',
+        DELETE_ACTIVITY_DEFAULTS: '[data-action="delete-activity-defaults"]'
+    };
+
+    /** @type {{INHERIT: Number}} **/
+    var INHERIT = -1;
+
+    /**
+     * DefaultsActions class.
+     */
+    var DefaultsActions = function() {
+        this.registerEvents();
+    };
+
+    /**
+     * Register event listeners.
+     */
+    DefaultsActions.prototype.registerEvents = function() {
+        $(ACTIONS.EDIT_LEVEL_DEFAULTS).click(function(e) {
+            e.preventDefault();
+
+            var button = $(this);
+            var contextLevel = button.data('contextlevel');
+            var category = button.data('category');
+            var purpose = button.data('purpose');
+
+            // Get options.
+            var requests = [
+                {methodname: 'tool_dataprivacy_get_category_options', args: {}},
+                {methodname: 'tool_dataprivacy_get_purpose_options', args: {}}
+            ];
+
+            var promises = Ajax.call(requests);
+            var titlePromise = Str.get_string('editdefaults', 'tool_dataprivacy', $('#defaults-header').text());
+            $.when(promises[0], promises[1], titlePromise).then(function(categoryResponse, purposeResponse, title) {
+                var categories = categoryResponse.options;
+                var purposes = purposeResponse.options;
+                showDefaultsFormModal(title, contextLevel, category, purpose, null, categories, purposes, null);
+
+                return true;
+            }).catch(Notification.exception);
+        });
+
+        $(ACTIONS.NEW_ACTIVITY_DEFAULTS).click(function(e) {
+            e.preventDefault();
+
+            var button = $(this);
+            var contextLevel = button.data('contextlevel');
+
+            // Get options.
+            var requests = [
+                {methodname: 'tool_dataprivacy_get_category_options', args: {}},
+                {methodname: 'tool_dataprivacy_get_purpose_options', args: {}},
+                {methodname: 'tool_dataprivacy_get_activity_options', args: {'nodefaults': true}}
+            ];
+
+            var promises = Ajax.call(requests);
+            var titlePromise = Str.get_string('addnewdefaults', 'tool_dataprivacy');
+
+            $.when(promises[0], promises[1], promises[2], titlePromise).then(
+                function(categoryResponse, purposeResponse, activityResponse, title) {
+                    var categories = categoryResponse.options;
+                    var purposes = purposeResponse.options;
+                    var activities = activityResponse.options;
+
+                    showDefaultsFormModal(title, contextLevel, null, null, null, categories, purposes, activities);
+
+                    return true;
+
+                }).catch(Notification.exception);
+            }
+        );
+
+        $(ACTIONS.EDIT_ACTIVITY_DEFAULTS).click(function(e) {
+            e.preventDefault();
+
+            var button = $(this);
+            var contextLevel = button.data('contextlevel');
+            var category = button.data('category');
+            var purpose = button.data('purpose');
+            var activity = button.data('activityname');
+
+            // Get options.
+            var requests = [
+                {methodname: 'tool_dataprivacy_get_category_options', args: {}},
+                {methodname: 'tool_dataprivacy_get_purpose_options', args: {}},
+                {methodname: 'tool_dataprivacy_get_activity_options', args: {}}
+            ];
+
+            var promises = Ajax.call(requests);
+            var titlePromise = Str.get_string('editmoduledefaults', 'tool_dataprivacy');
+
+            $.when(promises[0], promises[1], promises[2], titlePromise).then(
+                function(categoryResponse, purposeResponse, activityResponse, title) {
+                    var categories = categoryResponse.options;
+                    var purposes = purposeResponse.options;
+                    var activities = activityResponse.options;
+
+                    showDefaultsFormModal(title, contextLevel, category, purpose, activity, categories, purposes, activities);
+
+                    return true;
+
+                }).catch(Notification.exception);
+            }
+        );
+
+        $(ACTIONS.DELETE_ACTIVITY_DEFAULTS).click(function(e) {
+            e.preventDefault();
+
+            var button = $(this);
+            var contextLevel = button.data('contextlevel');
+            var activity = button.data('activityname');
+            var activityDisplayName = button.data('activitydisplayname');
+            // Set category and purpose to inherit (-1).
+            var category = INHERIT;
+            var purpose = INHERIT;
+
+            ModalFactory.create({
+                title: Str.get_string('deletedefaults', 'tool_dataprivacy', activityDisplayName),
+                body: Templates.render('tool_dataprivacy/delete_activity_defaults', {"activityname": activityDisplayName}),
+                type: ModalFactory.types.SAVE_CANCEL,
+                large: true
+            }).then(function(modal) {
+                modal.setSaveButtonText(Str.get_string('delete'));
+
+                // Handle save event.
+                modal.getRoot().on(ModalEvents.save, function() {
+                    setContextDefaults(contextLevel, category, purpose, activity, false);
+                });
+
+                // Handle hidden event.
+                modal.getRoot().on(ModalEvents.hidden, function() {
+                    // Destroy when hidden.
+                    modal.destroy();
+                });
+
+                modal.show();
+
+                return true;
+            }).catch(Notification.exception);
+        });
+    };
+
+    /**
+     * Prepares and renders the modal for setting the defaults for the given context level/plugin.
+     *
+     * @param {String} title The modal's title.
+     * @param {Number} contextLevel The context level to set defaults for.
+     * @param {Number} category The current category ID.
+     * @param {Number} purpose The current purpose ID.
+     * @param {String} activity The plugin name of the activity. Optional.
+     * @param {Array} categoryOptions The list of category options.
+     * @param {Array} purposeOptions The list of purpose options.
+     * @param {Array} activityOptions The list of activity options. Optional.
+     */
+    function showDefaultsFormModal(title, contextLevel, category, purpose, activity,
+                                   categoryOptions, purposeOptions, activityOptions) {
+
+        if (category !== null) {
+            categoryOptions.forEach(function(currentValue) {
+                if (currentValue.id === category) {
+                    currentValue.selected = true;
+                }
+            });
+        }
+
+        if (purpose !== null) {
+            purposeOptions.forEach(function(currentValue) {
+                if (currentValue.id === purpose) {
+                    currentValue.selected = true;
+                }
+            });
+        }
+
+        var templateContext = {
+            "contextlevel": contextLevel,
+            "categoryoptions": categoryOptions,
+            "purposeoptions": purposeOptions
+        };
+
+        // Check the activityOptions parameter that was passed.
+        if (activityOptions !== null && activityOptions.length) {
+            // Check the activity parameter that was passed.
+            if (activity === null) {
+                // We're setting a new defaults for a module.
+                templateContext.newactivitydefaults = true;
+
+            } else {
+                // Edit mode. Set selection.
+                activityOptions.forEach(function(currentValue) {
+                    if (activity === currentValue.name) {
+                        currentValue.selected = true;
+                    }
+                });
+            }
+
+            templateContext.modemodule = true;
+            templateContext.activityoptions = activityOptions;
+        }
+
+        ModalFactory.create({
+            title: title,
+            body: Templates.render('tool_dataprivacy/category_purpose_form', templateContext),
+            type: ModalFactory.types.SAVE_CANCEL,
+            large: true
+        }).then(function(modal) {
+
+            // Handle save event.
+            modal.getRoot().on(ModalEvents.save, function() {
+                var activity = $('#activity');
+                var activityVal = typeof activity !== 'undefined' ? activity.val() : null;
+                var override = $('#override');
+                var overrideVal = typeof override !== 'undefined' ? override.is(':checked') : false;
+
+                setContextDefaults($('#contextlevel').val(), $('#category').val(), $('#purpose').val(), activityVal, overrideVal);
+            });
+
+            // Handle hidden event.
+            modal.getRoot().on(ModalEvents.hidden, function() {
+                // Destroy when hidden.
+                modal.destroy();
+            });
+
+            modal.show();
+
+            return modal;
+        }).catch(Notification.exception);
+    }
+
+    /**
+     * Calls a the tool_dataprivacy_set_context_defaults WS function.
+     *
+     * @param {Number} contextLevel The context level.
+     * @param {Number} category The category ID.
+     * @param {Number} purpose The purpose ID.
+     * @param {String} activity The plugin name of the activity module.
+     * @param {Boolean} override Whether to override custom instances.
+     */
+    function setContextDefaults(contextLevel, category, purpose, activity, override) {
+        var request = {
+            methodname: 'tool_dataprivacy_set_context_defaults',
+            args: {
+                'contextlevel': contextLevel,
+                'category': category,
+                'purpose': purpose,
+                'override': override,
+                'activity': activity
+            }
+        };
+
+        Ajax.call([request])[0].done(function(data) {
+            if (data.result) {
+                window.location.reload();
+            }
+        });
+    }
+
+    return /** @alias module:tool_dataprivacy/defaultsactions */ {
+        // Public variables and functions.
+
+        /**
+         * Initialise the module.
+         *
+         * @method init
+         * @return {DefaultsActions}
+         */
+        'init': function() {
+            return new DefaultsActions();
+        }
+    };
+});
index 4e7ff77..cb1d9af 100644 (file)
@@ -25,7 +25,9 @@
 define([], function() {
     return {
         approve: 'tool_dataprivacy-data_request:approve',
+        bulkApprove: 'tool_dataprivacy-data_request:bulk_approve',
         deny: 'tool_dataprivacy-data_request:deny',
+        bulkDeny: 'tool_dataprivacy-data_request:bulk_deny',
         complete: 'tool_dataprivacy-data_request:complete'
     };
 });
index 4f3c406..37c4d92 100644 (file)
@@ -40,12 +40,38 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
      * @type {{DENY_REQUEST: string}}
      * @type {{VIEW_REQUEST: string}}
      * @type {{MARK_COMPLETE: string}}
+     * @type {{CHANGE_BULK_ACTION: string}}
+     * @type {{CONFIRM_BULK_ACTION: string}}
+     * @type {{SELECT_ALL: string}}
      */
     var ACTIONS = {
         APPROVE_REQUEST: '[data-action="approve"]',
         DENY_REQUEST: '[data-action="deny"]',
         VIEW_REQUEST: '[data-action="view"]',
-        MARK_COMPLETE: '[data-action="complete"]'
+        MARK_COMPLETE: '[data-action="complete"]',
+        CHANGE_BULK_ACTION: '[id="bulk-action"]',
+        CONFIRM_BULK_ACTION: '[id="confirm-bulk-action"]',
+        SELECT_ALL: '[data-action="selectall"]'
+    };
+
+    /**
+     * List of available bulk actions.
+     *
+     * @type {{APPROVE: number}}
+     * @type {{DENY: number}}
+     */
+    var BULK_ACTIONS = {
+        APPROVE: 1,
+        DENY: 2
+    };
+
+    /**
+     * List of selectors.
+     *
+     * @type {{SELECT_REQUEST: string}}
+     */
+    var SELECTORS = {
+        SELECT_REQUEST: '.selectrequests'
     };
 
     /**
@@ -103,12 +129,12 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
             }).then(function(modal) {
                 // Handle approve event.
                 modal.getRoot().on(DataPrivacyEvents.approve, function() {
-                    showConfirmation(DataPrivacyEvents.approve, requestId);
+                    showConfirmation(DataPrivacyEvents.approve, approveEventWsData(requestId));
                 });
 
                 // Handle deny event.
                 modal.getRoot().on(DataPrivacyEvents.deny, function() {
-                    showConfirmation(DataPrivacyEvents.deny, requestId);
+                    showConfirmation(DataPrivacyEvents.deny, denyEventWsData(requestId));
                 });
 
                 // Handle send event.
@@ -137,34 +163,158 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
             e.preventDefault();
 
             var requestId = $(this).data('requestid');
-            showConfirmation(DataPrivacyEvents.approve, requestId);
+            showConfirmation(DataPrivacyEvents.approve, approveEventWsData(requestId));
         });
 
         $(ACTIONS.DENY_REQUEST).click(function(e) {
             e.preventDefault();
 
             var requestId = $(this).data('requestid');
-            showConfirmation(DataPrivacyEvents.deny, requestId);
+            showConfirmation(DataPrivacyEvents.deny, denyEventWsData(requestId));
         });
 
         $(ACTIONS.MARK_COMPLETE).click(function(e) {
             e.preventDefault();
-            showConfirmation(DataPrivacyEvents.complete, $(this).data('requestid'));
+
+            var requestId = $(this).data('requestid');
+            showConfirmation(DataPrivacyEvents.complete, completeEventWsData(requestId));
+        });
+
+        $(ACTIONS.CONFIRM_BULK_ACTION).click(function() {
+            var requestIds = [];
+            var actionEvent = '';
+            var wsdata = {};
+            var bulkActionKeys = [
+                {
+                    key: 'selectbulkaction',
+                    component: 'tool_dataprivacy'
+                },
+                {
+                    key: 'selectdatarequests',
+                    component: 'tool_dataprivacy'
+                },
+                {
+                    key: 'ok'
+                }
+            ];
+
+            var bulkaction = parseInt($('#bulk-action').val());
+
+            if (bulkaction != BULK_ACTIONS.APPROVE && bulkaction != BULK_ACTIONS.DENY) {
+                Str.get_strings(bulkActionKeys).done(function(langStrings) {
+                    Notification.alert('', langStrings[0], langStrings[2]);
+                }).fail(Notification.exception);
+
+                return;
+            }
+
+            $(".selectrequests:checked").each(function() {
+                requestIds.push($(this).val());
+            });
+
+            if (requestIds.length < 1) {
+                Str.get_strings(bulkActionKeys).done(function(langStrings) {
+                    Notification.alert('', langStrings[1], langStrings[2]);
+                }).fail(Notification.exception);
+
+                return;
+            }
+
+            switch (bulkaction) {
+                case BULK_ACTIONS.APPROVE:
+                    actionEvent = DataPrivacyEvents.bulkApprove;
+                    wsdata = bulkApproveEventWsData(requestIds);
+                    break;
+                case BULK_ACTIONS.DENY:
+                    actionEvent = DataPrivacyEvents.bulkDeny;
+                    wsdata = bulkDenyEventWsData(requestIds);
+            }
+
+            showConfirmation(actionEvent, wsdata);
+        });
+
+        $(ACTIONS.SELECT_ALL).change(function(e) {
+            e.preventDefault();
+
+            var selectAll = $(this).is(':checked');
+            $(SELECTORS.SELECT_REQUEST).prop('checked', selectAll);
         });
     };
 
+    /**
+     * Return the webservice data for the approve request action.
+     *
+     * @param {Number} requestId The ID of the request.
+     * @return {Object}
+     */
+    function approveEventWsData(requestId) {
+        return {
+            'wsfunction': 'tool_dataprivacy_approve_data_request',
+            'wsparams': {'requestid': requestId}
+        };
+    }
+
+    /**
+     * Return the webservice data for the bulk approve request action.
+     *
+     * @param {Array} requestIds The array of request ID's.
+     * @return {Object}
+     */
+    function bulkApproveEventWsData(requestIds) {
+        return {
+            'wsfunction': 'tool_dataprivacy_bulk_approve_data_requests',
+            'wsparams': {'requestids': requestIds}
+        };
+    }
+
+    /**
+     * Return the webservice data for the deny request action.
+     *
+     * @param {Number} requestId The ID of the request.
+     * @return {Object}
+     */
+    function denyEventWsData(requestId) {
+        return {
+            'wsfunction': 'tool_dataprivacy_deny_data_request',
+            'wsparams': {'requestid': requestId}
+        };
+    }
+
+    /**
+     * Return the webservice data for the bulk deny request action.
+     *
+     * @param {Array} requestIds The array of request ID's.
+     * @return {Object}
+     */
+    function bulkDenyEventWsData(requestIds) {
+        return {
+            'wsfunction': 'tool_dataprivacy_bulk_deny_data_requests',
+            'wsparams': {'requestids': requestIds}
+        };
+    }
+
+    /**
+     * Return the webservice data for the complete request action.
+     *
+     * @param {Number} requestId The ID of the request.
+     * @return {Object}
+     */
+    function completeEventWsData(requestId) {
+        return {
+            'wsfunction': 'tool_dataprivacy_mark_complete',
+            'wsparams': {'requestid': requestId}
+        };
+    }
+
     /**
      * Show the confirmation dialogue.
      *
      * @param {String} action The action name.
-     * @param {Number} requestId The request ID.
+     * @param {Object} wsdata Object containing ws data.
      */
-    function showConfirmation(action, requestId) {
+    function showConfirmation(action, wsdata) {
         var keys = [];
-        var wsfunction = '';
-        var params = {
-            'requestid': requestId
-        };
+
         switch (action) {
             case DataPrivacyEvents.approve:
                 keys = [
@@ -177,7 +327,18 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
                         component: 'tool_dataprivacy'
                     }
                 ];
-                wsfunction = 'tool_dataprivacy_approve_data_request';
+                break;
+            case DataPrivacyEvents.bulkApprove:
+                keys = [
+                    {
+                        key: 'bulkapproverequests',
+                        component: 'tool_dataprivacy'
+                    },
+                    {
+                        key: 'confirmbulkapproval',
+                        component: 'tool_dataprivacy'
+                    }
+                ];
                 break;
             case DataPrivacyEvents.deny:
                 keys = [
@@ -190,7 +351,18 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
                         component: 'tool_dataprivacy'
                     }
                 ];
-                wsfunction = 'tool_dataprivacy_deny_data_request';
+                break;
+            case DataPrivacyEvents.bulkDeny:
+                keys = [
+                    {
+                        key: 'bulkdenyrequests',
+                        component: 'tool_dataprivacy'
+                    },
+                    {
+                        key: 'confirmbulkdenial',
+                        component: 'tool_dataprivacy'
+                    }
+                ];
                 break;
             case DataPrivacyEvents.complete:
                 keys = [
@@ -203,7 +375,6 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
                         component: 'tool_dataprivacy'
                     }
                 ];
-                wsfunction = 'tool_dataprivacy_mark_complete';
                 break;
         }
 
@@ -221,7 +392,7 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
 
             // Handle save event.
             modal.getRoot().on(ModalEvents.save, function() {
-                handleSave(wsfunction, params);
+                handleSave(wsdata.wsfunction, wsdata.wsparams);
             });
 
             // Handle hidden event.
index f323278..aa58917 100644 (file)
@@ -33,6 +33,7 @@ $title = get_string('editcategories', 'tool_dataprivacy');
 
 $output = $PAGE->get_renderer('tool_dataprivacy');
 echo $output->header();
+echo $output->heading($title);
 
 $categories = \tool_dataprivacy\api::get_categories();
 $renderable = new \tool_dataprivacy\output\categories($categories);
index 6ee9707..7e5f350 100644 (file)
@@ -24,7 +24,7 @@
 namespace tool_dataprivacy;
 
 use coding_exception;
-use context_course;
+use context_helper;
 use context_system;
 use core\invalid_persistent_exception;
 use core\message\message;
@@ -94,6 +94,12 @@ class api {
     /** Data delete request completed, account is removed. */
     const DATAREQUEST_STATUS_DELETED = 10;
 
+    /** Approve data request. */
+    const DATAREQUEST_ACTION_APPROVE = 1;
+
+    /** Reject data request. */
+    const DATAREQUEST_ACTION_REJECT = 2;
+
     /**
      * Determines whether the user can contact the site's Data Protection Officer via Moodle.
      *
@@ -105,18 +111,14 @@ class api {
     }
 
     /**
-     * Check's whether the current user has the capability to manage data requests.
+     * Checks whether the current user has the capability to manage data requests.
      *
      * @param int $userid The user ID.
      * @return bool
-     * @throws coding_exception
-     * @throws dml_exception
      */
     public static function can_manage_data_requests($userid) {
-        $context = context_system::instance();
-
-        // A user can manage data requests if he/she has the site DPO role and has the capability to manage data requests.
-        return self::is_site_dpo($userid) && has_capability('tool/dataprivacy:managedatarequests', $context, $userid);
+        // Privacy officers can manage data requests.
+        return self::is_site_dpo($userid);
     }
 
     /**
@@ -136,6 +138,31 @@ class api {
         require_capability('tool/dataprivacy:managedataregistry', $context);
     }
 
+    /**
+     * Fetches the list of configured privacy officer roles.
+     *
+     * Every time this function is called, it checks each role if they have the 'managedatarequests' capability and removes
+     * any role that doesn't have the required capability anymore.
+     *
+     * @return int[]
+     * @throws dml_exception
+     */
+    public static function get_assigned_privacy_officer_roles() {
+        $roleids = [];
+
+        // Get roles from config.
+        $configroleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
+        if (!empty($configroleids)) {
+            // Fetch roles that have the capability to manage data requests.
+            $capableroles = array_keys(get_roles_with_capability('tool/dataprivacy:managedatarequests'));
+
+            // Extract the configured roles that have the capability from the list of capable roles.
+            $roleids = array_intersect($capableroles, $configroleids);
+        }
+
+        return $roleids;
+    }
+
     /**
      * Fetches the role shortnames of Data Protection Officer roles.
      *
@@ -144,7 +171,7 @@ class api {
     public static function get_dpo_role_names() : array {
         global $DB;
 
-        $dporoleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
+        $dporoleids = self::get_assigned_privacy_officer_roles();
         $dponames = array();
 
         if (!empty($dporoleids)) {
@@ -156,20 +183,15 @@ class api {
     }
 
     /**
-     * Fetches the list of users with the Data Protection Officer role.
-     *
-     * @throws dml_exception
+     * Fetches the list of users with the Privacy Officer role.
      */
     public static function get_site_dpos() {
         // Get role(s) that can manage data requests.
-        $dporoles = explode(',', get_config('tool_dataprivacy', 'dporoles'));
+        $dporoles = self::get_assigned_privacy_officer_roles();
 
         $dpos = [];
         $context = context_system::instance();
         foreach ($dporoles as $roleid) {
-            if (empty($roleid)) {
-                continue;
-            }
             $allnames = get_all_user_name_fields(true, 'u');
             $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
                       'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
@@ -189,15 +211,14 @@ class api {
     }
 
     /**
-     * Checks whether a given user is a site DPO.
+     * Checks whether a given user is a site Privacy Officer.
      *
      * @param int $userid The user ID.
      * @return bool
-     * @throws dml_exception
      */
     public static function is_site_dpo($userid) {
         $dpos = self::get_site_dpos();
-        return array_key_exists($userid, $dpos);
+        return array_key_exists($userid, $dpos) || is_siteadmin();
     }
 
     /**
@@ -619,6 +640,8 @@ class api {
             'requestedby' => $requestedby->fullname,
             'requesttype' => $typetext,
             'requestdate' => userdate($requestdata->timecreated),
+            'requestorigin' => $SITE->fullname,
+            'requestoriginurl' => new moodle_url('/'),
             'requestcomments' => $requestdata->messagehtml,
             'datarequestsurl' => $datarequestsurl
         ];
@@ -1141,4 +1164,91 @@ class api {
 
         return $approvedcollection;
     }
+
+    /**
+     * Updates the default category and purpose for a given context level (and optionally, a plugin).
+     *
+     * @param int $contextlevel The context level.
+     * @param int $categoryid The ID matching the category.
+     * @param int $purposeid The ID matching the purpose record.
+     * @param int $activity The name of the activity that we're making a defaults configuration for.
+     * @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
+     * @return boolean True if set/unset config succeeds. Otherwise, it throws an exception.
+     */
+    public static function set_context_defaults($contextlevel, $categoryid, $purposeid, $activity = null, $override = false) {
+        global $DB;
+
+        self::check_can_manage_data_registry();
+
+        // Get the class name associated with this context level.
+        $classname = context_helper::get_class_for_level($contextlevel);
+        list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname, $activity);
+
+        // Check the default category to be set.
+        if ($categoryid == context_instance::INHERIT) {
+            unset_config($categoryvar, 'tool_dataprivacy');
+
+        } else {
+            // Make sure the given category ID exists first.
+            $categorypersistent = new category($categoryid);
+            $categorypersistent->read();
+
+            // Then set the new default value.
+            set_config($categoryvar, $categoryid, 'tool_dataprivacy');
+        }
+
+        // Check the default purpose to be set.
+        if ($purposeid == context_instance::INHERIT) {
+            // If the defaults is set to inherit, just unset the config value.
+            unset_config($purposevar, 'tool_dataprivacy');
+
+        } else {
+            // Make sure the given purpose ID exists first.
+            $purposepersistent = new purpose($purposeid);
+            $purposepersistent->read();
+
+            // Then set the new default value.
+            set_config($purposevar, $purposeid, 'tool_dataprivacy');
+        }
+
+        // Unset instances that have been assigned with custom purpose and category, if override was specified.
+        if ($override) {
+            // We'd like to find context IDs that we want to unset.
+            $statements = ["SELECT c.id as contextid FROM {context} c"];
+            // Based on this context level.
+            $params = ['contextlevel' => $contextlevel];
+
+            if ($contextlevel == CONTEXT_MODULE) {
+                // If we're deleting module context instances, we need to make sure the instance ID is in the course modules table.
+                $statements[] = "JOIN {course_modules} cm ON cm.id = c.instanceid";
+                // And that the module is listed on the modules table.
+                $statements[] = "JOIN {modules} m ON m.id = cm.module";
+
+                if ($activity) {
+                    // If we're overriding for an activity module, make sure that the context instance matches that activity.
+                    $statements[] = "AND m.name = :modname";
+                    $params['modname'] = $activity;
+                }
+            }
+            // Make sure this context instance exists in the tool_dataprivacy_ctxinstance table.
+            $statements[] = "JOIN {tool_dataprivacy_ctxinstance} tdc ON tdc.contextid = c.id";
+            // And that the context level of this instance matches the given context level.
+            $statements[] = "WHERE c.contextlevel = :contextlevel";
+
+            // Build our SQL query by gluing the statements.
+            $sql = implode("\n", $statements);
+
+            // Get the context records matching our query.
+            $contextids = $DB->get_fieldset_sql($sql, $params);
+
+            // Delete the matching context instances.
+            foreach ($contextids as $contextid) {
+                if ($instance = context_instance::get_record_by_contextid($contextid, false)) {
+                    self::unset_context_instance($instance);
+                }
+            }
+        }
+
+        return true;
+    }
 }
index 1fac2f5..7b46b4d 100644 (file)
 namespace tool_dataprivacy;
 
 use coding_exception;
-use tool_dataprivacy\purpose;
-use tool_dataprivacy\category;
-use tool_dataprivacy\contextlevel;
-use tool_dataprivacy\context_instance;
+use core\persistent;
 
 defined('MOODLE_INTERNAL') || die();
 
@@ -57,13 +54,22 @@ class data_registry {
     /**
      * Returns purpose and category var names from a context class name
      *
-     * @param string $classname
+     * @param string $classname The context level's class.
+     * @param string $pluginname The name of the plugin associated with the context level.
      * @return string[]
      */
-    public static function var_names_from_context($classname) {
+    public static function var_names_from_context($classname, $pluginname = '') {
+        $pluginname = trim($pluginname);
+        if (!empty($pluginname)) {
+            $categoryvar = $classname . '_' . $pluginname . '_category';
+            $purposevar = $classname . '_' . $pluginname . '_purpose';
+        } else {
+            $categoryvar = $classname . '_category';
+            $purposevar = $classname . '_purpose';
+        }
         return [
-            $classname . '_purpose',
-            $classname . '_category',
+            $purposevar,
+            $categoryvar
         ];
     }
 
@@ -72,17 +78,31 @@ class data_registry {
      *
      * The caller code is responsible of checking that $contextlevel is an integer.
      *
-     * @param int $contextlevel
-     * @return int|false[]
+     * @param int $contextlevel The context level.
+     * @param string $pluginname The name of the plugin associated with the context level.
+     * @return int[]|false[]
      */
-    public static function get_defaults($contextlevel) {
+    public static function get_defaults($contextlevel, $pluginname = '') {
 
         $classname = \context_helper::get_class_for_level($contextlevel);
-        list($purposevar, $categoryvar) = self::var_names_from_context($classname);
+        list($purposevar, $categoryvar) = self::var_names_from_context($classname, $pluginname);
 
         $purposeid = get_config('tool_dataprivacy', $purposevar);
         $categoryid = get_config('tool_dataprivacy', $categoryvar);
 
+        if (!empty($pluginname)) {
+            list($purposevar, $categoryvar) = self::var_names_from_context($classname);
+            // If the plugin-level doesn't have a default purpose set, try the context level.
+            if ($purposeid === false) {
+                $purposeid = get_config('tool_dataprivacy', $purposevar);
+            }
+
+            // If the plugin-level doesn't have a default category set, try the context level.
+            if ($categoryid === false) {
+                $categoryid = get_config('tool_dataprivacy', $categoryvar);
+            }
+        }
+
         if (empty($purposeid)) {
             $purposeid = false;
         }
@@ -197,9 +217,15 @@ class data_registry {
                 // Use the context level value as we don't allow people to set specific instances values.
                 return self::get_effective_contextlevel_value($context->contextlevel, $element);
             } else {
+                // Check if we need to pass the plugin name of an activity.
+                $forplugin = '';
+                if ($context->contextlevel == CONTEXT_MODULE) {
+                    list($course, $cm) = get_course_and_cm_from_cmid($context->instanceid);
+                    $forplugin = $cm->modname;
+                }
                 // Use the default context level value.
                 list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category(
-                    $context->contextlevel
+                    $context->contextlevel, false, false, $forplugin
                 );
                 return self::get_element_instance($element, $$fieldname);
             }
@@ -283,14 +309,15 @@ class data_registry {
      * Returns the effective default purpose and category for a context level.
      *
      * @param int $contextlevel
-     * @param int $forcedpurposevalue Use this value as if this was this context level purpose.
-     * @param int $forcedcategoryvalue Use this value as if this was this context level category.
+     * @param int|bool $forcedpurposevalue Use this value as if this was this context level purpose.
+     * @param int|bool $forcedcategoryvalue Use this value as if this was this context level category.
+     * @param string $activity The plugin name of the activity.
      * @return int[]
      */
     public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false,
-                                                                                   $forcedcategoryvalue = false) {
+                                                                                   $forcedcategoryvalue = false, $activity = '') {
 
-        list($purposeid, $categoryid) = self::get_defaults($contextlevel);
+        list($purposeid, $categoryid) = self::get_defaults($contextlevel, $activity);
 
         // Honour forced values.
         if ($forcedpurposevalue) {
index e14e072..c3f9f65 100644 (file)
@@ -421,6 +421,85 @@ class external extends external_api {
         ]);
     }
 
+    /**
+     * Parameter description for bulk_approve_data_requests().
+     *
+     * @since Moodle 3.5
+     * @return external_function_parameters
+     */
+    public static function bulk_approve_data_requests_parameters() {
+        return new external_function_parameters([
+            'requestids' => new external_multiple_structure(
+                new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
+            )
+        ]);
+    }
+
+    /**
+     * Bulk approve bulk data request.
+     *
+     * @since Moodle 3.5
+     * @param array $requestids Array consisting the request ID's.
+     * @return array
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws invalid_parameter_exception
+     * @throws restricted_context_exception
+     * @throws moodle_exception
+     */
+    public static function bulk_approve_data_requests($requestids) {
+        $warnings = [];
+        $result = false;
+        $params = external_api::validate_parameters(self::bulk_approve_data_requests_parameters(), [
+            'requestids' => $requestids
+        ]);
+        $requestids = $params['requestids'];
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+        require_capability('tool/dataprivacy:managedatarequests', $context);
+
+        foreach ($requestids as $requestid) {
+            // Ensure the request exists.
+            $requestexists = data_request::record_exists($requestid);
+
+            if ($requestexists) {
+                api::approve_data_request($requestid);
+            } else {
+                $warnings[] = [
+                    'item' => $requestid,
+                    'warningcode' => 'errorrequestnotfound',
+                    'message' => get_string('errorrequestnotfound', 'tool_dataprivacy')
+                ];
+            }
+        }
+
+        if (empty($warnings)) {
+            $result = true;
+            // Add notification in the session to be shown when the page is reloaded on the JS side.
+            notification::success(get_string('requestsapproved', 'tool_dataprivacy'));
+        }
+
+        return [
+            'result' => $result,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Parameter description for bulk_approve_data_requests().
+     *
+     * @since Moodle 3.5
+     * @return external_description
+     */
+    public static function bulk_approve_data_requests_returns() {
+        return new external_single_structure([
+            'result' => new external_value(PARAM_BOOL, 'The processing result'),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
     /**
      * Parameter description for deny_data_request().
      *
@@ -493,6 +572,85 @@ class external extends external_api {
         ]);
     }
 
+    /**
+     * Parameter description for bulk_deny_data_requests().
+     *
+     * @since Moodle 3.5
+     * @return external_function_parameters
+     */
+    public static function bulk_deny_data_requests_parameters() {
+        return new external_function_parameters([
+            'requestids' => new external_multiple_structure(
+                new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
+            )
+        ]);
+    }
+
+    /**
+     * Bulk deny data requests.
+     *
+     * @since Moodle 3.5
+     * @param array $requestids Array consisting of request ID's.
+     * @return array
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws invalid_parameter_exception
+     * @throws restricted_context_exception
+     * @throws moodle_exception
+     */
+    public static function bulk_deny_data_requests($requestids) {
+        $warnings = [];
+        $result = false;
+        $params = external_api::validate_parameters(self::bulk_deny_data_requests_parameters(), [
+            'requestids' => $requestids
+        ]);
+        $requestids = $params['requestids'];
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+        require_capability('tool/dataprivacy:managedatarequests', $context);
+
+        foreach ($requestids as $requestid) {
+            // Ensure the request exists.
+            $requestexists = data_request::record_exists($requestid);
+
+            if ($requestexists) {
+                api::deny_data_request($requestid);
+            } else {
+                $warnings[] = [
+                    'item' => $requestid,
+                    'warningcode' => 'errorrequestnotfound',
+                    'message' => get_string('errorrequestnotfound', 'tool_dataprivacy')
+                ];
+            }
+        }
+
+        if (empty($warnings)) {
+            $result = true;
+            // Add notification in the session to be shown when the page is reloaded on the JS side.
+            notification::success(get_string('requestsdenied', 'tool_dataprivacy'));
+        }
+
+        return [
+            'result' => $result,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Parameter description for bulk_deny_data_requests().
+     *
+     * @since Moodle 3.5
+     * @return external_description
+     */
+    public static function bulk_deny_data_requests_returns() {
+        return new external_single_structure([
+            'result' => new external_value(PARAM_BOOL, 'The processing result'),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
     /**
      * Parameter description for get_data_request().
      *
@@ -1110,6 +1268,287 @@ class external extends external_api {
         ]);
     }
 
+    /**
+     * Parameters for set_context_defaults().
+     *
+     * @return external_function_parameters
+     */
+    public static function set_context_defaults_parameters() {
+        return new external_function_parameters([
+            'contextlevel' => new external_value(PARAM_INT, 'The context level', VALUE_REQUIRED),
+            'category' => new external_value(PARAM_INT, 'The default category for the given context level', VALUE_REQUIRED),
+            'purpose' => new external_value(PARAM_INT, 'The default purpose for the given context level', VALUE_REQUIRED),
+            'activity' => new external_value(PARAM_PLUGIN, 'The plugin name of the activity', VALUE_DEFAULT, null),
+            'override' => new external_value(PARAM_BOOL, 'Whether to override existing instances with the defaults', VALUE_DEFAULT,
+                false),
+        ]);
+    }
+
+    /**
+     * Updates the default category and purpose for a given context level (and optionally, a plugin).
+     *
+     * @param int $contextlevel The context level.
+     * @param int $category The ID matching the category.
+     * @param int $purpose The ID matching the purpose record.
+     * @param int $activity The name of the activity that we're making a defaults configuration for.
+     * @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
+     * @return array
+     */
+    public static function set_context_defaults($contextlevel, $category, $purpose, $activity, $override) {
+        $warnings = [];
+
+        $params = external_api::validate_parameters(self::set_context_defaults_parameters(), [
+            'contextlevel' => $contextlevel,
+            'category' => $category,
+            'purpose' => $purpose,
+            'activity' => $activity,
+            'override' => $override,
+        ]);
+        $contextlevel = $params['contextlevel'];
+        $category = $params['category'];
+        $purpose = $params['purpose'];
+        $activity = $params['activity'];
+        $override = $params['override'];
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        // Set the context defaults.
+        $result = api::set_context_defaults($contextlevel, $category, $purpose, $activity, $override);
+
+        return [
+            'result' => $result,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Returns for set_context_defaults().
+     *
+     * @return external_single_structure
+     */
+    public static function set_context_defaults_returns() {
+        return new external_single_structure([
+            'result' => new external_value(PARAM_BOOL, 'Whether the context defaults were successfully set or not'),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
+    /**
+     * Parameters for get_category_options().
+     *
+     * @return external_function_parameters
+     */
+    public static function get_category_options_parameters() {
+        return new external_function_parameters([
+            'includeinherit' => new external_value(PARAM_BOOL, 'Include option "Inherit"', VALUE_DEFAULT, true),
+            'includenotset' => new external_value(PARAM_BOOL, 'Include option "Not set"', VALUE_DEFAULT, false),
+        ]);
+    }
+
+    /**
+     * Fetches a list of data category options containing category IDs as keys and the category name for the value.
+     *
+     * @param bool $includeinherit Whether to include the "Inherit" option.
+     * @param bool $includenotset Whether to include the "Not set" option.
+     * @return array
+     */
+    public static function get_category_options($includeinherit, $includenotset) {
+        $warnings = [];
+
+        $params = self::validate_parameters(self::get_category_options_parameters(), [
+            'includeinherit' => $includeinherit,
+            'includenotset' => $includenotset
+        ]);
+        $includeinherit = $params['includeinherit'];
+        $includenotset = $params['includenotset'];
+
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $categories = api::get_categories();
+        $options = data_registry_page::category_options($categories, $includenotset, $includeinherit);
+        $categoryoptions = [];
+        foreach ($options as $id => $name) {
+            $categoryoptions[] = [
+                'id' => $id,
+                'name' => $name,
+            ];
+        }
+
+        return [
+            'options' => $categoryoptions,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Returns for get_category_options().
+     *
+     * @return external_single_structure
+     */
+    public static function get_category_options_returns() {
+        $optiondefinition = new external_single_structure(
+            [
+                'id' => new external_value(PARAM_INT, 'The category ID'),
+                'name' => new external_value(PARAM_TEXT, 'The category name'),
+            ]
+        );
+
+        return new external_single_structure([
+            'options' => new external_multiple_structure($optiondefinition),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
+    /**
+     * Parameters for get_purpose_options().
+     *
+     * @return external_function_parameters
+     */
+    public static function get_purpose_options_parameters() {
+        return new external_function_parameters([
+            'includeinherit' => new external_value(PARAM_BOOL, 'Include option "Inherit"', VALUE_DEFAULT, true),
+            'includenotset' => new external_value(PARAM_BOOL, 'Include option "Not set"', VALUE_DEFAULT, false),
+        ]);
+    }
+
+    /**
+     * Fetches a list of data storage purposes containing purpose IDs as keys and the purpose name for the value.
+     *
+     * @param bool $includeinherit Whether to include the "Inherit" option.
+     * @param bool $includenotset Whether to include the "Not set" option.
+     * @return array
+     */
+    public static function get_purpose_options($includeinherit, $includenotset) {
+        $warnings = [];
+
+        $params = self::validate_parameters(self::get_category_options_parameters(), [
+            'includeinherit' => $includeinherit,
+            'includenotset' => $includenotset
+        ]);
+        $includeinherit = $params['includeinherit'];
+        $includenotset = $params['includenotset'];
+
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $purposes = api::get_purposes();
+        $options = data_registry_page::purpose_options($purposes, $includenotset, $includeinherit);
+        $purposeoptions = [];
+        foreach ($options as $id => $name) {
+            $purposeoptions[] = [
+                'id' => $id,
+                'name' => $name,
+            ];
+        }
+
+        return [
+            'options' => $purposeoptions,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Returns for get_purpose_options().
+     *
+     * @return external_single_structure
+     */
+    public static function get_purpose_options_returns() {
+        $optiondefinition = new external_single_structure(
+            [
+                'id' => new external_value(PARAM_INT, 'The purpose ID'),
+                'name' => new external_value(PARAM_TEXT, 'The purpose name'),
+            ]
+        );
+
+        return new external_single_structure([
+            'options' => new external_multiple_structure($optiondefinition),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
+    /**
+     * Parameters for get_activity_options().
+     *
+     * @return external_function_parameters
+     */
+    public static function get_activity_options_parameters() {
+        return new external_function_parameters([
+            'nodefaults' => new external_value(PARAM_BOOL, 'Whether to fetch all activities or only those without defaults',
+                VALUE_DEFAULT, false),
+        ]);
+    }
+
+    /**
+     * Fetches a list of activity options for setting data registry defaults.
+     *
+     * @param boolean $nodefaults If false, it will fetch all of the activities. Otherwise, it will only fetch the activities
+     *                            that don't have defaults yet (e.g. when adding a new activity module defaults).
+     * @return array
+     */
+    public static function get_activity_options($nodefaults) {
+        $warnings = [];
+
+        $params = self::validate_parameters(self::get_activity_options_parameters(), [
+            'nodefaults' => $nodefaults,
+        ]);
+        $nodefaults = $params['nodefaults'];
+
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        // Get activity module plugin info.
+        $pluginmanager = \core_plugin_manager::instance();
+        $modplugins = $pluginmanager->get_enabled_plugins('mod');
+        $modoptions = [];
+
+        // Get the module-level defaults. data_registry::get_defaults falls back to this when there are no activity defaults.
+        list($levelpurpose, $levelcategory) = data_registry::get_defaults(CONTEXT_MODULE);
+        foreach ($modplugins as $name) {
+            // Check if we have default purpose and category for this module if we want don't want to fetch everything.
+            if ($nodefaults) {
+                list($purpose, $category) = data_registry::get_defaults(CONTEXT_MODULE, $name);
+                // Compare this with the module-level defaults.
+                if ($purpose !== $levelpurpose || $category !== $levelcategory) {
+                    // If the defaults for this activity has been already set, there's no need to add this in the list of options.
+                    continue;
+                }
+            }
+
+            $displayname = $pluginmanager->plugin_name('mod_' . $name);
+            $modoptions[] = (object)[
+                'name' => $name,
+                'displayname' => $displayname
+            ];
+        }
+
+        return [
+            'options' => $modoptions,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Returns for get_category_options().
+     *
+     * @return external_single_structure
+     */
+    public static function get_activity_options_returns() {
+        $optionsdefinition = new external_single_structure(
+            [
+                'name' => new external_value(PARAM_TEXT, 'The plugin name of the activity'),
+                'displayname' => new external_value(PARAM_TEXT, 'The display name of the activity'),
+            ]
+        );
+
+        return new external_single_structure([
+            'options' => new external_multiple_structure($optionsdefinition),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
     /**
      * Gets the structure of a tree node (link + child branches).
      *
index c5c350c..06d2469 100644 (file)
@@ -25,6 +25,8 @@ namespace tool_dataprivacy\external;
 defined('MOODLE_INTERNAL') || die();
 
 use core\external\persistent_exporter;
+use tool_dataprivacy\category;
+use tool_dataprivacy\context_instance;
 
 /**
  * Class for exporting field data.
@@ -53,4 +55,25 @@ class category_exporter extends persistent_exporter {
             'context' => 'context',
         );
     }
+
+    /**
+     * Utility function that fetches a category name from the given ID.
+     *
+     * @param int $categoryid The category ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID.
+     * @return string The purpose name.
+     */
+    public static function get_name($categoryid) {
+        global $PAGE;
+        if ($categoryid === false || $categoryid == context_instance::INHERIT) {
+            return get_string('inherit', 'tool_dataprivacy');
+        } else if ($categoryid == context_instance::NOTSET) {
+            return get_string('notset', 'tool_dataprivacy');
+        } else {
+            $purpose = new category($categoryid);
+            $output = $PAGE->get_renderer('tool_dataprivacy');
+            $exporter = new self($purpose, ['context' => \context_system::instance()]);
+            $data = $exporter->export($output);
+            return $data->name;
+        }
+    }
 }
index f91e2f2..a6519c6 100644 (file)
@@ -29,6 +29,7 @@ use core\external\persistent_exporter;
 use DateInterval;
 use Exception;
 use renderer_base;
+use tool_dataprivacy\context_instance;
 use tool_dataprivacy\purpose;
 
 /**
@@ -143,4 +144,25 @@ class purpose_exporter extends persistent_exporter {
 
         return $values;
     }
+
+    /**
+     * Utility function that fetches a purpose name from the given ID.
+     *
+     * @param int $purposeid The purpose ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID.
+     * @return string The purpose name.
+     */
+    public static function get_name($purposeid) {
+        global $PAGE;
+        if ($purposeid === false || $purposeid == context_instance::INHERIT) {
+            return get_string('inherit', 'tool_dataprivacy');
+        } else if ($purposeid == context_instance::NOTSET) {
+            return get_string('notset', 'tool_dataprivacy');
+        } else {
+            $purpose = new purpose($purposeid);
+            $output = $PAGE->get_renderer('tool_dataprivacy');
+            $exporter = new self($purpose, ['context' => \context_system::instance()]);
+            $data = $exporter->export($output);
+            return $data->name;
+        }
+    }
 }
diff --git a/admin/tool/dataprivacy/classes/form/defaults.php b/admin/tool/dataprivacy/classes/form/defaults.php
deleted file mode 100644 (file)
index 4e599c3..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * This file contains the defaults form.
- *
- * @package   tool_dataprivacy
- * @copyright 2018 David Monllao
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace tool_dataprivacy\form;
-defined('MOODLE_INTERNAL') || die();
-
-use \tool_dataprivacy\output\data_registry_page;
-
-require_once($CFG->libdir . '/formslib.php');
-require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
-
-/**
- * Context levels defaults form.
- *
- * @package   tool_dataprivacy
- * @copyright 2018 David Monllao
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class defaults extends \moodleform {
-
-    /**
-     * Define the form.
-     */
-    public function definition() {
-        global $OUTPUT;
-
-        $mform = $this->_form;
-        $mform->setDisableShortforms();
-
-        $notification = $OUTPUT->notification(get_string('defaultsinfo', 'tool_dataprivacy'),
-            \core\output\notification::NOTIFY_INFO);
-        $mform->addElement('html', $notification);
-
-        foreach ($this->_customdata['levels'] as $level => $classname) {
-
-            $mform->addElement('header', $classname . '-header',
-                get_string('contextlevelname' . $level, 'tool_dataprivacy'));
-
-            list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
-
-            $includeinherit = true;
-            if ($level == CONTEXT_SYSTEM) {
-                $includeinherit = false;
-            }
-
-            $categoryoptions = data_registry_page::category_options($this->_customdata['categories'], false, $includeinherit);
-            $purposeoptions = data_registry_page::category_options($this->_customdata['purposes'], false, $includeinherit);
-
-            $mform->addElement('select', $categoryvar, get_string('category', 'tool_dataprivacy'), $categoryoptions);
-            $mform->addHelpButton($categoryvar, 'categorydefault', 'tool_dataprivacy');
-            $mform->setType($categoryvar, PARAM_INT);
-
-            $mform->addElement('select', $purposevar, get_string('purpose', 'tool_dataprivacy'), $purposeoptions);
-            $mform->addHelpButton($purposevar, 'purposedefault', 'tool_dataprivacy');
-            $mform->setType($purposevar, PARAM_INT);
-        }
-
-        $this->add_action_buttons(true, get_string('savechanges'));
-    }
-}
index 36dd93a..2609ef1 100644 (file)
@@ -47,6 +47,9 @@ class helper {
     /** The request filters preference key. */
     const PREF_REQUEST_FILTERS = 'tool_dataprivacy_request-filters';
 
+    /** The number of data request records per page preference key. */
+    const PREF_REQUEST_PERPAGE = 'tool_dataprivacy_request-perpage';
+
     /**
      * Retrieves the human-readable text value of a data request type.
      *
index 8d5fd35..be0e996 100644 (file)
@@ -425,7 +425,7 @@ class data_registry_page implements renderable, templatable {
     /**
      * From a list of purpose persistents to a list of id => name purposes.
      *
-     * @param \tool_dataprivacy\purpose $purposes
+     * @param \tool_dataprivacy\purpose[] $purposes
      * @param bool $includenotset
      * @param bool $includeinherit
      * @return string[]
@@ -442,7 +442,7 @@ class data_registry_page implements renderable, templatable {
     /**
      * From a list of category persistents to a list of id => name categories.
      *
-     * @param \tool_dataprivacy\category $categories
+     * @param \tool_dataprivacy\category[] $categories
      * @param bool $includenotset
      * @param bool $includeinherit
      * @return string[]
index 7ea4bf8..402a22d 100644 (file)
@@ -86,7 +86,7 @@ class data_requests_page implements renderable, templatable {
         $data->filter = $filter->export_for_template($output);
 
         ob_start();
-        $this->table->out(helper::DEFAULT_PAGE_SIZE, true);
+        $this->table->out($this->table->get_requests_per_page(), true);
         $requests = ob_get_contents();
         ob_end_clean();
 
index 477e503..b6fa957 100644 (file)
@@ -62,6 +62,12 @@ class data_requests_table extends table_sql {
     /** @var \tool_dataprivacy\data_request[] Array of data request persistents. */
     protected $datarequests = [];
 
+    /** @var int The number of data request to be displayed per page. */
+    protected $perpage;
+
+    /** @var int[] The available options for the number of data request to be displayed per page. */
+    protected $perpageoptions = [25, 50, 100, 250];
+
     /**
      * data_requests_table constructor.
      *
@@ -79,7 +85,13 @@ class data_requests_table extends table_sql {
         $this->types = $types;
         $this->manage = $manage;
 
+        $checkboxattrs = [
+            'title' => get_string('selectall'),
+            'data-action' => 'selectall'
+        ];
+
         $columnheaders = [
+            'select' => html_writer::checkbox('selectall', 1, false, null, $checkboxattrs),
             'type' => get_string('requesttype', 'tool_dataprivacy'),
             'userid' => get_string('user', 'tool_dataprivacy'),
             'timecreated' => get_string('daterequested', 'tool_dataprivacy'),
@@ -91,7 +103,26 @@ class data_requests_table extends table_sql {
 
         $this->define_columns(array_keys($columnheaders));
         $this->define_headers(array_values($columnheaders));
-        $this->no_sorting('actions');
+        $this->no_sorting('select', 'actions');
+    }
+
+    /**
+     * The select column.
+     *
+     * @param stdClass $data The row data.
+     * @return string
+     */
+    public function col_select($data) {
+        if ($data->status == \tool_dataprivacy\api::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
+            $stringdata = [
+                'username' => $data->foruser->fullname,
+                'requesttype' => \core_text::strtolower($data->typenameshort)
+            ];
+
+            return \html_writer::checkbox('requestids[]', $data->id, false, '',
+                    ['class' => 'selectrequests', 'title' => get_string('selectuserdatarequest',
+                    'tool_dataprivacy', $stringdata)]);
+        }
     }
 
     /**
@@ -290,4 +321,72 @@ class data_requests_table extends table_sql {
     protected function show_hide_link($column, $index) {
         return '';
     }
+
+    /**
+     * Override the table's wrap_html_finish method in order to render the bulk actions and
+     * records per page options.
+     */
+    public function wrap_html_finish() {
+        global $OUTPUT;
+
+        $data = new stdClass();
+        $data->options = [
+            [
+                'value' => 0,
+                'name' => ''
+            ],
+            [
+                'value' => \tool_dataprivacy\api::DATAREQUEST_ACTION_APPROVE,
+                'name' => get_string('approve', 'tool_dataprivacy')
+            ],
+            [
+                'value' => \tool_dataprivacy\api::DATAREQUEST_ACTION_REJECT,
+                'name' => get_string('deny', 'tool_dataprivacy')
+            ]
+        ];
+
+        $perpageoptions = array_combine($this->perpageoptions, $this->perpageoptions);
+        $perpageselect = new \single_select(new moodle_url(''), 'perpage',
+                $perpageoptions, get_user_preferences('tool_dataprivacy_request-perpage'), null, 'selectgroup');
+        $perpageselect->label = get_string('perpage', 'moodle');
+        $data->perpage = $OUTPUT->render($perpageselect);
+
+        echo $OUTPUT->render_from_template('tool_dataprivacy/data_requests_bulk_actions', $data);
+    }
+
+    /**
+     * Set the number of data request records to be displayed per page.
+     *
+     * @param int $perpage The number of data request records.
+     */
+    public function set_requests_per_page(int $perpage) {
+        $this->perpage = $perpage;
+    }
+
+    /**
+     * Get the number of data request records to be displayed per page.
+     *
+     * @return int The number of data request records.
+     */
+    public function get_requests_per_page() : int {
+        return $this->perpage;
+    }
+
+    /**
+     * Set the available options for the number of data request to be displayed per page.
+     *
+     * @param array $perpageoptions The available options for the number of data request to be displayed per page.
+     */
+    public function set_requests_per_page_options(array $perpageoptions) {
+        $this->$perpageoptions = $perpageoptions;
+    }
+
+    /**
+     * Get the available options for the number of data request to be displayed per page.
+     *
+     * @return array The available options for the number of data request to be displayed per page.
+     */
+    public function get_requests_per_page_options() : array {
+        return $this->perpageoptions;
+    }
 }
diff --git a/admin/tool/dataprivacy/classes/output/defaults_page.php b/admin/tool/dataprivacy/classes/output/defaults_page.php
new file mode 100644 (file)
index 0000000..166228f
--- /dev/null
@@ -0,0 +1,178 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class containing data for the data registry defaults.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy\output;
+defined('MOODLE_INTERNAL') || die();
+
+use action_menu_link_primary;
+use coding_exception;
+use moodle_exception;
+use moodle_url;
+use renderable;
+use renderer_base;
+use stdClass;
+use templatable;
+use tool_dataprivacy\data_registry;
+use tool_dataprivacy\external\category_exporter;
+use tool_dataprivacy\external\purpose_exporter;
+
+/**
+ * Class containing data for the data registry defaults.
+ *
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class defaults_page implements renderable, templatable {
+
+    /** @var int $mode The display mode. */
+    protected $mode = null;
+
+    /** @var int $category The default category for the given mode. */
+    protected $category = null;
+
+    /** @var int $purpose The default purpose for the given mode. */
+    protected $purpose = null;
+
+    /** @var stdClass[] $otherdefaults Other defaults for the given mode. */
+    protected $otherdefaults = [];
+
+    /** @var bool $canedit Whether editing is allowed. */
+    protected $canedit = false;
+
+    /**
+     * Construct this renderable.
+     *
+     * @param int $mode The display mode.
+     * @param int $category The default category for the given mode.
+     * @param int $purpose The default purpose for the given mode.
+     * @param stdClass[] $otherdefaults Other defaults for the given mode.
+     * @param bool $canedit Whether editing is allowed.
+     */
+    public function __construct($mode, $category, $purpose, $otherdefaults = [], $canedit = false) {
+        $this->mode = $mode;
+        $this->category = $category;
+        $this->purpose = $purpose;
+        $this->otherdefaults = $otherdefaults;
+        $this->canedit = $canedit;
+    }
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param renderer_base $output
+     * @return stdClass
+     * @throws coding_exception
+     * @throws moodle_exception
+     */
+    public function export_for_template(renderer_base $output) {
+        $data = new stdClass();
+
+        // Set tab URLs.
+        $coursecaturl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_COURSECAT]);
+        $courseurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_COURSE]);
+        $moduleurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_MODULE]);
+        $blockurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_BLOCK]);
+        $data->coursecaturl = $coursecaturl;
+        $data->courseurl = $courseurl;
+        $data->moduleurl = $moduleurl;
+        $data->blockurl = $blockurl;
+
+        // Set display mode.
+        switch ($this->mode) {
+            case CONTEXT_COURSECAT:
+                $data->modecoursecat = true;
+                break;
+            case CONTEXT_COURSE:
+                $data->modecourse = true;
+                break;
+            case CONTEXT_MODULE:
+                $data->modemodule = true;
+                break;
+            case CONTEXT_BLOCK:
+                $data->modeblock = true;
+                break;
+            default:
+                $data->modecoursecat = true;
+                break;
+        }
+
+        // Set config variables.
+        $configname = \context_helper::get_class_for_level($this->mode);
+        list($purposevar, $categoryvar) = data_registry::var_names_from_context($configname);
+        $data->categoryvar = $categoryvar;
+        $data->purposevar = $purposevar;
+
+        // Set default category.
+        $data->categoryid = $this->category;
+        $data->category = category_exporter::get_name($this->category);
+
+        // Set default purpose.
+        $data->purposeid = $this->purpose;
+        $data->purpose = purpose_exporter::get_name($this->purpose);
+
+        // Set other defaults.
+        $otherdefaults = [];
+        $url = new moodle_url('#');
+        foreach ($this->otherdefaults as $pluginname => $values) {
+            $defaults = [
+                'name' => $values->name,
+                'category' => category_exporter::get_name($values->category),
+                'purpose' => purpose_exporter::get_name($values->purpose),
+            ];
+            if ($this->canedit) {
+                $actions = [];
+                // Edit link.
+                $editattrs = [
+                    'data-action' => 'edit-activity-defaults',
+                    'data-contextlevel' => $this->mode,
+                    'data-activityname' => $pluginname,
+                    'data-category' => $values->category,
+                    'data-purpose' => $values->purpose,
+                ];
+                $editlink = new action_menu_link_primary($url, new \pix_icon('t/edit', get_string('edit')),
+                    get_string('edit'), $editattrs);
+                $actions[] = $editlink->export_for_template($output);
+
+                // Delete link.
+                $deleteattrs = [
+                    'data-action' => 'delete-activity-defaults',
+                    'data-contextlevel' => $this->mode,
+                    'data-activityname' => $pluginname,
+                    'data-activitydisplayname' => $values->name,
+                ];
+                $deletelink = new action_menu_link_primary($url, new \pix_icon('t/delete', get_string('delete')),
+                    get_string('delete'), $deleteattrs);
+                $actions[] = $deletelink->export_for_template($output);
+
+                $defaults['actions'] = $actions;
+            }
+            $otherdefaults[] = (object)$defaults;
+        }
+        $data->otherdefaults = $otherdefaults;
+
+        $data->canedit = $this->canedit;
+        $data->contextlevel = $this->mode;
+
+        return $data;
+    }
+}
index 0a8cf73..d7ec6f7 100644 (file)
@@ -47,7 +47,7 @@ class page_helper {
      */
     public static function setup(moodle_url $url, $title, $attachtoparentnode = '',
                                  $requiredcapability = 'tool/dataprivacy:managedataregistry') {
-        global $PAGE;
+        global $PAGE, $SITE;
 
         $context = context_system::instance();
 
@@ -65,7 +65,7 @@ class page_helper {
         $PAGE->set_context($context);
         $PAGE->set_pagelayout('admin');
         $PAGE->set_title($title);
-        $PAGE->set_heading($title);
+        $PAGE->set_heading($SITE->fullname);
 
         // If necessary, override the settings navigation to add this page into the breadcrumb navigation.
         if ($attachtoparentnode) {
index 4c2d8c4..4ddd411 100644 (file)
@@ -76,6 +76,8 @@ class provider implements
 
         $collection->add_user_preference(tool_helper::PREF_REQUEST_FILTERS,
             'privacy:metadata:preference:tool_dataprivacy_request-filters');
+        $collection->add_user_preference(tool_helper::PREF_REQUEST_PERPAGE,
+            'privacy:metadata:preference:tool_dataprivacy_request-perpage');
 
         return $collection;
     }
@@ -200,5 +202,11 @@ class provider implements
             $descriptionstext = implode(', ', $descriptions);
             writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_FILTERS, $values, $descriptionstext);
         }
+
+        $prefperpage = get_user_preferences(tool_helper::PREF_REQUEST_PERPAGE, null, $userid);
+        if ($prefperpage !== null) {
+            writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_PERPAGE, $prefperpage,
+                get_string('privacy:metadata:preference:tool_dataprivacy_request-perpage', 'tool_dataprivacy'));
+        }
     }
 }
index 83a5c90..2a2fb81 100644 (file)
@@ -78,8 +78,8 @@ if ($data = $mform->get_data()) {
     redirect($returnurl, $redirectmessage);
 }
 
-$title = get_string('contactdataprotectionofficer', 'tool_dataprivacy');
-$PAGE->set_heading($title);
+$title = get_string('createnewdatarequest', 'tool_dataprivacy');
+$PAGE->set_heading($SITE->fullname);
 $PAGE->set_title($title);
 echo $OUTPUT->header();
 echo $OUTPUT->heading($title);
index c92c676..df088ca 100644 (file)
@@ -36,6 +36,7 @@ $title = get_string('datadeletion', 'tool_dataprivacy');
 \tool_dataprivacy\page_helper::setup($url, $title);
 
 echo $OUTPUT->header();
+echo $OUTPUT->heading($title);
 
 if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
     $table = new \tool_dataprivacy\output\expired_contexts_table($filter);
index 50c0ab9..e11c47c 100644 (file)
@@ -37,6 +37,7 @@ $title = get_string('dataregistry', 'tool_dataprivacy');
 
 $output = $PAGE->get_renderer('tool_dataprivacy');
 echo $output->header();
+echo $OUTPUT->heading($title);
 
 if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
     $dataregistry = new tool_dataprivacy\output\data_registry_page($contextlevel, $contextid);
index 6a8140d..8b2b16f 100644 (file)
@@ -27,6 +27,8 @@ require_once('lib.php');
 
 require_login(null, false);
 
+$perpage = optional_param('perpage', 0, PARAM_INT);
+
 $url = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
 
 $title = get_string('datarequests', 'tool_dataprivacy');
@@ -66,6 +68,13 @@ if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
     }
 
     $table = new \tool_dataprivacy\output\data_requests_table(0, $statuses, $types, true);
+    if (!empty($perpage)) {
+        set_user_preference(\tool_dataprivacy\local\helper::PREF_REQUEST_PERPAGE, $perpage);
+    } else {
+        $prefperpage = get_user_preferences(\tool_dataprivacy\local\helper::PREF_REQUEST_PERPAGE);
+        $perpage = ($prefperpage) ? $prefperpage : $table->get_requests_per_page_options()[0];
+    }
+    $table->set_requests_per_page($perpage);
     $table->baseurl = $url;
 
     $requestlist = new tool_dataprivacy\output\data_requests_page($table, $filtersapplied);
index 9c71e8c..dac6ba4 100644 (file)
@@ -73,6 +73,16 @@ $functions = [
         'ajax'          => true,
         'loginrequired' => true,
     ],
+    'tool_dataprivacy_bulk_approve_data_requests' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'bulk_approve_data_requests',
+        'classpath'     => '',
+        'description'   => 'Bulk approve data requests',
+        'type'          => 'write',
+        'capabilities'  => 'tool/dataprivacy:managedatarequests',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
     'tool_dataprivacy_deny_data_request' => [
         'classname'     => 'tool_dataprivacy\external',
         'methodname'    => 'deny_data_request',
@@ -83,6 +93,16 @@ $functions = [
         'ajax'          => true,
         'loginrequired' => true,
     ],
+    'tool_dataprivacy_bulk_deny_data_requests' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'bulk_deny_data_requests',
+        'classpath'     => '',
+        'description'   => 'Bulk deny data requests',
+        'type'          => 'write',
+        'capabilities'  => 'tool/dataprivacy:managedatarequests',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
     'tool_dataprivacy_get_users' => [
         'classname'     => 'tool_dataprivacy\external',
         'methodname'    => 'get_users',
@@ -173,4 +193,44 @@ $functions = [
         'ajax'          => true,
         'loginrequired' => true,
     ],
+    'tool_dataprivacy_set_context_defaults' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'set_context_defaults',
+        'classpath'     => '',
+        'description'   => 'Updates the default category and purpose for a given context level (and optionally, a plugin)',
+        'type'          => 'write',
+        'capabilities'  => 'tool/dataprivacy:managedataregistry',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
+    'tool_dataprivacy_get_category_options' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'get_category_options',
+        'classpath'     => '',
+        'description'   => 'Fetches a list of data category options',
+        'type'          => 'read',
+        'capabilities'  => 'tool/dataprivacy:managedataregistry',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
+    'tool_dataprivacy_get_purpose_options' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'get_purpose_options',
+        'classpath'     => '',
+        'description'   => 'Fetches a list of data storage purpose options',
+        'type'          => 'read',
+        'capabilities'  => 'tool/dataprivacy:managedataregistry',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
+    'tool_dataprivacy_get_activity_options' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'get_activity_options',
+        'classpath'     => '',
+        'description'   => 'Fetches a list of activity options',
+        'type'          => 'read',
+        'capabilities'  => 'tool/dataprivacy:managedataregistry',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
 ];
index d936ba2..e920585 100644 (file)
@@ -32,47 +32,40 @@ $title = get_string('setdefaults', 'tool_dataprivacy');
 
 \tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry');
 
-$levels = \context_helper::get_all_levels();
-// They are set through the context level site and user.
-unset($levels[CONTEXT_SYSTEM]);
-unset($levels[CONTEXT_USER]);
+$mode = optional_param('mode', CONTEXT_COURSECAT, PARAM_INT);
+$classname = context_helper::get_class_for_level($mode);
+list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
+$purpose = get_config('tool_dataprivacy', $purposevar);
+$category = get_config('tool_dataprivacy', $categoryvar);
 
-$customdata = [
-    'levels' => $levels,
-    'purposes' => \tool_dataprivacy\api::get_purposes(),
-    'categories' => \tool_dataprivacy\api::get_categories(),
-];
-$form = new \tool_dataprivacy\form\defaults($PAGE->url->out(false), $customdata);
+$otherdefaults = [];
+if ($mode == CONTEXT_MODULE) {
+    // Get activity module plugin info.
+    $pluginmanager = core_plugin_manager::instance();
+    $modplugins = $pluginmanager->get_enabled_plugins('mod');
 
-$toform = new stdClass();
-foreach ($levels as $level => $classname) {
-    list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
-    $toform->{$purposevar} = get_config('tool_dataprivacy', $purposevar);
-    $toform->{$categoryvar} = get_config('tool_dataprivacy', $categoryvar);
-}
-$form->set_data($toform);
-
-$returnurl = new \moodle_url('/admin/tool/dataprivacy/dataregistry.php');
-if ($form->is_cancelled()) {
-    redirect($returnurl);
-} else if ($data = $form->get_data()) {
-
-    foreach ($levels as $level => $classname) {
-
-        list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
-
-        if (isset($data->{$purposevar})) {
-            set_config($purposevar, $data->{$purposevar}, 'tool_dataprivacy');
-        }
-        if (isset($data->{$categoryvar})) {
-            set_config($categoryvar, $data->{$categoryvar}, 'tool_dataprivacy');
+    foreach ($modplugins as $name) {
+        list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname, $name);
+        $plugincategory = get_config('tool_dataprivacy', $categoryvar);
+        $pluginpurpose = get_config('tool_dataprivacy', $purposevar);
+        if ($plugincategory === false && $pluginpurpose === false) {
+            // If no purpose and category has been set for this plugin, then there's no need to show this on the list.
+            continue;
         }
+
+        $displayname = $pluginmanager->plugin_name('mod_' . $name);
+        $otherdefaults[$name] = (object)[
+            'name' => $displayname,
+            'category' => $plugincategory,
+            'purpose' => $pluginpurpose,
+        ];
     }
-    redirect($returnurl, get_string('defaultssaved', 'tool_dataprivacy'),
-        0, \core\output\notification::NOTIFY_SUCCESS);
 }
 
+$defaultspage = new \tool_dataprivacy\output\defaults_page($mode, $category, $purpose, $otherdefaults, true);
+
 $output = $PAGE->get_renderer('tool_dataprivacy');
 echo $output->header();
-$form->display();
+echo $output->heading($title);
+echo $output->render_from_template('tool_dataprivacy/defaults_page', $defaultspage->export_for_template($output));
 echo $output->footer();
index 4fa38fa..7b8aa51 100644 (file)
@@ -28,9 +28,12 @@ $string['pluginname'] = 'Data privacy';
 $string['pluginname_help'] = 'Data privacy plugin';
 $string['activitiesandresources'] = 'Activities and resources';
 $string['addcategory'] = 'Add category';
+$string['addnewdefaults'] = 'Add a new module default';
 $string['addpurpose'] = 'Add purpose';
 $string['approve'] = 'Approve';
 $string['approverequest'] = 'Approve request';
+$string['bulkapproverequests'] = 'Approve requests';
+$string['bulkdenyrequests'] = 'Deny requests';
 $string['cachedef_purpose'] = 'Data purposes';
 $string['cachedef_contextlevel'] = 'Context levels purpose and category';
 $string['cancelrequest'] = 'Cancel request';
@@ -46,9 +49,11 @@ $string['categoryupdated'] = 'Category updated';
 $string['close'] = 'Close';
 $string['compliant'] = 'Compliant';
 $string['confirmapproval'] = 'Do you really want to approve this data request?';
+$string['confirmbulkapproval'] = 'Do you really want to bulk approve the selected data requests?';
 $string['confirmcompletion'] = 'Do you really want to mark this user enquiry as complete?';
 $string['confirmcontextdeletion'] = 'Do you really want to confirm the deletion of the selected contexts? This will also delete all of the user data for their respective sub-contexts.';
 $string['confirmdenial'] = 'Do you really want deny this data request?';
+$string['confirmbulkdenial'] = 'Do you really want to bulk deny the selected data requests?';
 $string['contactdataprotectionofficer'] = 'Contact the privacy officer';
 $string['contactdataprotectionofficer_desc'] = 'If enabled, users will be able to contact the privacy officer and make a data request via a link on their profile page.';
 $string['contextlevelname10'] = 'Site';
@@ -60,6 +65,7 @@ $string['contextlevelname80'] = 'Blocks';
 $string['contextpurposecategorysaved'] = 'Purpose and category saved.';
 $string['contactdpoviaprivacypolicy'] = 'Please contact the privacy officer as described in the privacy policy.';
 $string['createcategory'] = 'Create data category';
+$string['createnewdatarequest'] = 'Create a new data request';
 $string['createpurpose'] = 'Create data purpose';
 $string['datadeletion'] = 'Data deletion';
 $string['datadeletionpagehelp'] = 'Data for which the retention period has expired are listed here. Please review and confirm data deletion, which will then be executed by the "Delete expired contexts" scheduled task.';
@@ -76,9 +82,12 @@ $string['datarequests'] = 'Data requests';
 $string['datecomment'] = '[{$a->date}]: ' . PHP_EOL . ' {$a->comment}';
 $string['daterequested'] = 'Date requested';
 $string['daterequesteddetail'] = 'Date requested:';
-$string['defaultsinfo'] = 'Default categories and purposes are applied to all newly created instances.';
+$string['defaultsinfo'] = 'Default categories and purposes are applied to all new and existing instances where a value is not set.';
+$string['defaultswarninginfo'] = 'Warning: Changing these defaults may affect the retention period of existing instances.';
 $string['deletecategory'] = 'Delete category';
 $string['deletecategorytext'] = 'Are you sure you want to delete the category \'{$a}\'?';
+$string['deletedefaults'] = 'Delete defaults: {$a}';
+$string['deletedefaultsconfirmation'] = 'Are you sure you want to delete the default category and purpose for {$a} modules?';
 $string['deleteexpiredcontextstask'] = 'Delete expired contexts';
 $string['deleteexpireddatarequeststask'] = 'Delete files from completed data requests that have expired';
 $string['deletepurpose'] = 'Delete purpose';
@@ -95,6 +104,8 @@ $string['dporolemapping_desc'] = 'The privacy officer can manage data requests.
 $string['editcategories'] = 'Edit categories';
 $string['editcategory'] = 'Edit category';
 $string['editcategories'] = 'Edit categories';
+$string['editdefaults'] = 'Edit defaults: {$a}';
+$string['editmoduledefaults'] = 'Edit module defaults';
 $string['editpurpose'] = 'Edit purpose';
 $string['editpurposes'] = 'Edit purposes';
 $string['effectiveretentionperiodcourse'] = '{$a} (after the course end date)';
@@ -185,11 +196,13 @@ $string['nopurposes'] = 'There are no purposes yet';
 $string['nosubjectaccessrequests'] = 'There are no data requests that you need to act on';
 $string['nosystemdefaults'] = 'Site purpose and category have not yet been defined.';
 $string['notset'] = 'Not set (use the default value)';
+$string['overrideinstances'] = 'Reset instances with custom values';
 $string['pluginregistry'] = 'Plugin privacy registry';
 $string['pluginregistrytitle'] = 'Plugin privacy compliance registry';
 $string['privacy'] = 'Privacy';
 $string['privacyofficeronly'] = 'Only users who are assigned a privacy officer role ({$a}) have access to this content';
 $string['privacy:metadata:preference:tool_dataprivacy_request-filters'] = 'The filters currently applied to the data requests page.';
+$string['privacy:metadata:preference:tool_dataprivacy_request-perpage'] = 'The number of data requests the user prefers to see on one page';
 $string['privacy:metadata:request'] = 'Information from personal data requests (subject access and deletion requests) made for this site.';
 $string['privacy:metadata:request:comments'] = 'Any user comments accompanying the request.';
 $string['privacy:metadata:request:userid'] = 'The ID of the user to whom the request belongs';
@@ -197,7 +210,7 @@ $string['privacy:metadata:request:requestedby'] = 'The ID of the user making the
 $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['privacyrequestexpiry_desc'] = 'The time that approved data requests will be available for download before expiring. If set to zero, then there is no time limit.';
 $string['protected'] = 'Protected';
 $string['protectedlabel'] = 'The retention of this data has a higher legal precedent over a user\'s request to be forgotten. This data will only be deleted after the retention period has expired.';
 $string['purpose'] = 'Purpose';
@@ -219,11 +232,14 @@ $string['requestdenied'] = 'The request has been denied';
 $string['requestemailintro'] = 'You have received a data request:';
 $string['requestfor'] = 'Requesting for';
 $string['requestmarkedcomplete'] = 'The request has been marked as complete';
+$string['requestorigin'] = 'Request origin';
+$string['requestsapproved'] = 'The requests have been approved';
+$string['requestsdenied'] = 'The requests have been denied';
 $string['requeststatus'] = 'Status';
 $string['requestsubmitted'] = 'Your request has been submitted to the privacy officer';
 $string['requesttype'] = 'Type';
 $string['requesttypeuser'] = '{$a->typename} ({$a->user})';
-$string['requesttype_help'] = 'Select the reason why you would like to contact the privacy officer';
+$string['requesttype_help'] = 'Select the reason for contacting the privacy officer. Be aware that deletion of all personal  data will result in you no longer being able to log in to the site.';
 $string['requesttypedelete'] = 'Delete all of my personal data';
 $string['requesttypedeleteshort'] = 'Delete';
 $string['requesttypeexport'] = 'Export all of my personal data';
@@ -239,6 +255,9 @@ $string['retentionperiod'] = 'Retention period';
 $string['retentionperiod_help'] = 'The retention period specifies the length of time that data should be kept for. When the retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
 $string['retentionperiodnotdefined'] = 'No retention period was defined';
 $string['retentionperiodzero'] = 'No retention period';
+$string['selectbulkaction'] = 'Please select a bulk action.';
+$string['selectdatarequests'] = 'Please select data requests.';
+$string['selectuserdatarequest'] = 'Select {$a->username}\'s {$a->requesttype} data request.';
 $string['send'] = 'Send';
 $string['sensitivedatareasons'] = 'Sensitive personal data processing reasons';
 $string['sensitivedatareasons_help'] = 'Select one or more applicable reasons that exempts the prohibition of processing sensitive personal data tied to this purpose. For more information, please see  <a href="https://gdpr-info.eu/art-9-gdpr/" target="_blank">GDPR Art. 9.2</a>';
index 5fad922..fb1b0c5 100644 (file)
@@ -33,6 +33,7 @@ $title = get_string('editpurposes', 'tool_dataprivacy');
 
 $output = $PAGE->get_renderer('tool_dataprivacy');
 echo $output->header();
+echo $output->heading($title);
 
 $purposes = \tool_dataprivacy\api::get_purposes();
 $renderable = new \tool_dataprivacy\output\purposes($purposes);
index e6ddf93..7a7015a 100644 (file)
@@ -28,3 +28,7 @@ dd a.contactdpo {
 [data-region="data-requests-table"] .moodle-actionmenu {
     min-width: 150px;
 }
+
+.context-level-view {
+    margin: 1em;
+}
\ No newline at end of file
index ef31041..523a56b 100644 (file)
@@ -51,7 +51,6 @@
 {{/navigation}}
 
 <div data-region="categories" class="m-t-3 m-b-1">
-    <h3>{{#str}}categories, tool_dataprivacy{{/str}}</h3>
     <div class="m-y-1">
         <button class="btn btn-secondary" data-add-element="category" title="{{#str}}addcategory, tool_dataprivacy{{/str}}">
             {{#pix}}t/add, moodle, {{#str}}addcategory, tool_dataprivacy{{/str}}{{/pix}}
diff --git a/admin/tool/dataprivacy/templates/category_purpose_form.mustache b/admin/tool/dataprivacy/templates/category_purpose_form.mustache
new file mode 100644 (file)
index 0000000..c5d0ed4
--- /dev/null
@@ -0,0 +1,99 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_dataprivacy/category_purpose_form
+
+    Manage data registry defaults.
+
+    Classes required for JS:
+
+    Data attributes required for JS:
+
+    Context variables required for this template:
+    * actionurl String The action URL.
+    * contextlevel Number - The context level.
+    * modemodule Boolean - Whether to display the activity select element.
+    * activityoptions Array - An array of objects for the activity select element.
+    * categoryoptions Array - An array of objects for the category select element.
+    * purposeoptions Array - An array of objects for the purpose select element.
+
+    Example context (json):
+    {
+        "actionurl": "#",
+        "contextlevel": 70,
+        "newactivitydefaults": true,
+        "modemodule": true,
+        "activityoptions": [
+            { "name": "assign", "displayname": "Assignment" },
+            { "name": "forum", "displayname": "Forum", "selected": true },
+            { "name": "lesson", "displayname": "Lesson" },
+            { "name": "quiz", "displayname": "Quiz" }
+        ],
+        "categoryoptions": [
+            { "id": 1, "name": "Category 1" },
+            { "id": 2, "name": "Category 2", "selected": true },
+            { "id": 3, "name": "Category 3" }
+        ],
+        "purposeoptions": [
+            { "id": 1, "name": "Purpose 1" },
+            { "id": 2, "name": "Purpose 2" },
+            { "id": 3, "name": "Purpose 3", "selected": true }
+        ]
+    }
+}}
+
+<div class="alert alert-warning" role="alert">
+    {{#str}}defaultswarninginfo, tool_dataprivacy{{/str}}
+</div>
+<form method="post" action="{{actionurl}}" id="category_purpose_form">
+    <input type="hidden" value="{{contextlevel}}" id="contextlevel" />
+    {{#modemodule}}
+    <div class="form-group">
+        <label for="activity">{{#str}}activitymodule{{/str}}</label>
+        {{^newactivitydefaults}}
+            <input type="hidden" id="activity" value="{{#activityoptions}}{{#selected}}{{name}}{{/selected}}{{/activityoptions}}" />
+        {{/newactivitydefaults}}
+        <select class="form-control" {{#newactivitydefaults}}id="activity" {{/newactivitydefaults}}{{^newactivitydefaults}}disabled{{/newactivitydefaults}}>
+            {{#activityoptions}}
+                <option value="{{name}}" {{#selected}}selected{{/selected}}>{{displayname}}</option>
+            {{/activityoptions}}
+        </select>
+    </div>
+    {{/modemodule}}
+    <div class="form-group">
+        <label for="category">{{#str}}category, tool_dataprivacy{{/str}}</label>
+        <select class="form-control" id="category">
+        {{#categoryoptions}}
+            <option value="{{id}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+        {{/categoryoptions}}
+        </select>
+    </div>
+    <div class="form-group">
+        <label for="purpose">{{#str}}purpose, tool_dataprivacy{{/str}}</label>
+        <select class="form-control" id="purpose">
+        {{#purposeoptions}}
+            <option value="{{id}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+        {{/purposeoptions}}
+        </select>
+    </div>
+    <div class="form-check">
+        <input class="form-check-input" type="checkbox" value="1" id="override">
+        <label class="form-check-label" for="override">
+            {{#str}}overrideinstances, tool_dataprivacy{{/str}}
+        </label>
+    </div>
+</form>
index 1c993b2..3cfda8b 100644 (file)
@@ -31,6 +31,8 @@
     * string requestedby The one making the request.
     * string requesttype The request type.
     * string requestdate The date the request was made.
+    * string requestorigin The name of the site the request originates from.
+    * string requestoriginurl The homepage of the site the request originates from.
     * string requestcomments Additional details regarding the request.
     * bool forself Whether the request has been made on behalf of another user or not.
     * string datarequestsurl The URL to the data requests page.
@@ -42,6 +44,8 @@
         "requestedby": "Angus Zhang",
         "requesttype": "Export user data",
         "requestdate": "31 January 2018",
+        "requestorigin": "My Amazing Site",
+        "requestoriginurl": "https://www.bestmoodlesiteever.com",
         "requestcomments": "Dear admin,<br/> I would like to request a copy of my son's user data. Thanks!",
         "forself": true,
         "datarequestsurl": "#"
                     </td>
                 </tr>
             {{/forself}}
+            <tr>
+                <th scope="row">
+                    {{#str}}requestorigin, tool_dataprivacy{{/str}}
+                </th>
+                <td>
+                    <a href="{{requestoriginurl}}">{{{requestorigin}}}</a>
+                </td>
+            </tr>
             <tr>
                 <th scope="row">
                     {{#str}}requestcomments, tool_dataprivacy{{/str}}
diff --git a/admin/tool/dataprivacy/templates/data_requests_bulk_actions.mustache b/admin/tool/dataprivacy/templates/data_requests_bulk_actions.mustache
new file mode 100644 (file)
index 0000000..2ec0473
--- /dev/null
@@ -0,0 +1,59 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_dataprivacy/data_requests_bulk_actions
+
+    Moodle template for the bulk action select element in the data requests page.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * options - Array of options for the select with value and name.
+    * perpage - HTML content of the records per page select element.
+
+    Example context (json):
+    {
+        "options": [
+            {
+                "value": 1,
+                "name": "Approve"
+            },
+            {
+                "value": 2,
+                "name": "Deny"
+            }
+        ],
+        "perpage" : "<div class='singleselect'></div>"
+    }
+}}
+<div class="m-t-1 d-inline-block w-100">
+    <div class="pull-left">
+        <select id="bulk-action" class="select custom-select">
+        {{#options}}
+            <option value="{{ value }}">{{ name }}</option>
+        {{/options}}
+        </select>
+        <button class="btn btn-primary" id="confirm-bulk-action">{{# str}} confirm {{/ str}}</button>
+    </div>
+    <div class="pull-right">
+        {{{ perpage }}}
+    </div>
+</div>
diff --git a/admin/tool/dataprivacy/templates/defaults_display.mustache b/admin/tool/dataprivacy/templates/defaults_display.mustache
new file mode 100644 (file)
index 0000000..39a29aa
--- /dev/null
@@ -0,0 +1,74 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_dataprivacy/defaults_display
+
+    Renders the current default category and purpose.
+
+    Classes required for JS:
+
+    Data attributes required for JS:
+
+    Context variables required for this template:
+    * contextlevel Number - The context level.
+    * categoryid Number - The category ID.
+    * purposeid Number - The purpose ID.
+    * category String - The category name.
+    * purpose String - The purpose name.
+    * canedit Boolean - Whether this is being rendered for editing purposes.
+
+    Example context (json):
+    {
+        "category": "Awesome default category",
+        "categoryid": 1,
+        "purpose": "Awesome default purpose",
+        "purposeid": 2,
+        "canedit": true,
+        "contextlevel": 70
+    }
+}}
+<div class="row-fluid rtl-compatible m-t-1 m-b-1">
+    <div class="col-md-9 span9">
+        <div class="row-fluid rtl-compatible m-t-1 m-b-1">
+            <div class="col-md-3 span3">
+                <strong>{{#str}}category, tool_dataprivacy{{/str}}</strong>
+            </div>
+            <div class="col-md-9 span9">
+                {{category}}
+            </div>
+        </div>
+        <div class="row-fluid rtl-compatible m-t-1 m-b-1">
+            <div class="col-md-3 span3">
+                <strong>{{#str}}purpose, tool_dataprivacy{{/str}}</strong>
+            </div>
+            <div class="col-md-9 span9">
+                {{purpose}}
+            </div>
+        </div>
+    </div>
+    <div class="col-md-3 span3 align-self-center">
+    {{#canedit}}
+        <button class="btn btn-default" {{!
+            }}data-action="edit-level-defaults" {{!
+            }}data-contextlevel="{{contextlevel}}" {{!
+            }}data-category="{{categoryid}}" {{!
+            }}data-purpose="{{purposeid}}">
+            {{#str}}edit{{/str}}
+        </button>
+    {{/canedit}}
+    </div>
+</div>
diff --git a/admin/tool/dataprivacy/templates/defaults_page.mustache b/admin/tool/dataprivacy/templates/defaults_page.mustache
new file mode 100644 (file)
index 0000000..bb531da
--- /dev/null
@@ -0,0 +1,147 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_dataprivacy/defaults_page
+
+    Manage data registry defaults.
+
+    Classes required for JS:
+
+    Data attributes required for JS:
+
+    Context variables required for this template:
+    * contextlevel Number - The context level.
+    * modecoursecat Boolean - Whether we're displaying defaults for course categories.
+    * modecourse Boolean - Whether we're displaying defaults for courses.
+    * modemodule Boolean - Whether we're displaying defaults for activity modules.
+    * modeblock Boolean - Whether we're displaying defaults for blocks.
+    * coursecaturl String - The URL for the course category defaults tab.
+    * courseurl String - The URL for the course defaults tab.
+    * moduleurl String - The URL for the activity module defaults tab.
+    * blockurl String - The URL for the block defaults tab.
+    * purposeid Number - The purpose ID for this context level.
+    * canedit Boolean - Whether this is being rendered for editing purposes.
+    * categoryid Number - The ID of the default category for this context level.
+    * purposeid Number - The ID of the default purpose for this context level.
+    * category String - The category name.
+    * purpose String - The purpose name.
+    * otherdefaults Array - An array containing the defaults for the activity modules.
+
+    Example context (json):
+    {
+        "contextlevel": 70,
+        "modecoursecat": false,
+        "modecourse": false,
+        "modemodule": true,
+        "modeblock": false,
+        "coursecaturl": "#",
+        "courseurl": "#",
+        "moduleurl": "#",
+        "blockurl": "#",
+        "category": "Awesome default category",
+        "purpose": "Awesome default purpose",
+        "canedit": true,
+        "otherdefaults": [
+            {
+                "name": "Assignment",
+                "category": "Category for activity modules",
+                "purpose": "Assessments"
+            },
+            {
+                "name": "Forum",
+                "category": "Category for activity modules",
+                "purpose": "Social interactions"
+            }
+        ]
+    }
+}}
+<div class="card">
+    <div class="card-header">
+        <ul class="nav nav-tabs card-header-tabs">
+            <li class="nav-item">
+                <a class="nav-link {{#modecoursecat}}active{{/modecoursecat}}" href="{{coursecaturl}}">{{#str}}categories{{/str}}</a>
+            </li>
+            <li class="nav-item">
+                <a class="nav-link {{#modecourse}}active{{/modecourse}}" href="{{courseurl}}">{{#str}}courses{{/str}}</a>
+            </li>
+            <li class="nav-item">
+                <a class="nav-link {{#modemodule}}active{{/modemodule}}" href="{{moduleurl}}">{{#str}}activitymodules{{/str}}</a>
+            </li>
+            <li class="nav-item">
+                <a class="nav-link {{#modeblock}}active{{/modeblock}}" href="{{blockurl}}">{{#str}}blocks{{/str}}</a>
+            </li>
+        </ul>
+    </div>
+    <div class="card-body context-level-view">
+        <div class="alert alert-primary" role="alert">
+            {{#str}}defaultsinfo, tool_dataprivacy{{/str}}
+        </div>
+        <h4 class="card-title" id="defaults-header">
+            {{#modecoursecat}}{{#str}}categories{{/str}}{{/modecoursecat}}
+            {{#modecourse}}{{#str}}courses{{/str}}{{/modecourse}}
+            {{#modemodule}}{{#str}}activitymodules{{/str}}{{/modemodule}}
+            {{#modeblock}}{{#str}}blocks{{/str}}{{/modeblock}}
+        </h4>
+        <div>
+            {{> tool_dataprivacy/defaults_display}}
+            {{#canedit}}
+                {{#modemodule}}
+                <button class="btn btn-primary" data-action="new-activity-defaults" data-contextlevel="{{contextlevel}}">
+                    {{#str}}addnewdefaults, tool_dataprivacy{{/str}}
+                </button>
+                {{/modemodule}}
+            {{/canedit}}
+            {{#modemodule}}
+            <table class="m-t-1 table table-striped">
+                <thead>
+                    <tr>
+                        <th>&nbsp;</th>
+                        <th scope="col">{{#str}}category, tool_dataprivacy{{/str}}</th>
+                        <th scope="col">{{#str}}purpose, tool_dataprivacy{{/str}}</th>
+                        {{#canedit}}
+                            <th scope="col">{{#str}}actions{{/str}}</th>
+                        {{/canedit}}
+                    </tr>
+                </thead>
+                <tbody>
+                    {{#otherdefaults}}
+                    <tr>
+                        <th scope="row">{{name}}</th>
+                        <td>{{category}}</td>
+                        <td>{{purpose}}</td>
+                        {{#canedit}}
+                            <td>
+                                {{#actions}}
+                                    {{> core/action_menu_link}}
+                                {{/actions}}
+                            </td>
+                        {{/canedit}}
+                    </tr>
+                    {{/otherdefaults}}
+                </tbody>
+            </table>
+            {{/modemodule}}
+        </div>
+    </div>
+</div>
+
+{{#js}}
+// Initialise the JS.
+require(['tool_dataprivacy/defaultsactions'], function(ActionsMod) {
+    ActionsMod.init();
+});
+{{/js}}
diff --git a/admin/tool/dataprivacy/templates/delete_activity_defaults.mustache b/admin/tool/dataprivacy/templates/delete_activity_defaults.mustache
new file mode 100644 (file)
index 0000000..c0220d3
--- /dev/null
@@ -0,0 +1,41 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_dataprivacy/delete_activity_defaults
+
+    Renders the confirmation.
+
+    Classes required for JS:
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * activityname String - The activity name.
+
+    Example context (json):
+    {
+        "activityname": "Assignment"
+    }
+}}
+
+<div class="alert alert-warning" role="alert">
+    {{#str}}defaultswarninginfo, tool_dataprivacy{{/str}}
+</div>
+<div>
+    {{#str}}deletedefaultsconfirmation, tool_dataprivacy, {{activityname}}{{/str}}
+</div>
index 4461eab..fe25440 100644 (file)
@@ -58,7 +58,6 @@
 {{/navigation}}
 
 <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" title="{{#str}}addpurpose, tool_dataprivacy{{/str}}">
             {{#pix}}t/add, moodle, {{#str}}addpurpose, tool_dataprivacy{{/str}}{{/pix}}
index 9feebcf..820611c 100644 (file)
@@ -124,6 +124,47 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $this->assertEquals($u1->id, $dpo->id);
     }
 
+    /**
+     * Test for \tool_dataprivacy\api::get_assigned_privacy_officer_roles().
+     */
+    public function test_get_assigned_privacy_officer_roles() {
+        global $DB;
+
+        // Erroneously set the manager roles as the PO, even if it doesn't have the managedatarequests capability yet.
+        $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
+        set_config('dporoles', $managerroleid, 'tool_dataprivacy');
+        // Get the assigned PO roles when nothing has been set yet.
+        $roleids = api::get_assigned_privacy_officer_roles();
+        // Confirm that the returned list is empty.
+        $this->assertEmpty($roleids);
+
+        $context = context_system::instance();
+
+        // Give the manager role with the capability to manage data requests.
+        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
+
+        // Give the editing teacher role with the capability to manage data requests.
+        $editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
+        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $editingteacherroleid, $context->id, true);
+
+        // Get the non-editing teacher role ID.
+        $teacherroleid = $DB->get_field('role', 'id', array('shortname' => 'teacher'));
+
+        // Erroneously map the manager and the non-editing teacher roles to the PO role.
+        $badconfig = $managerroleid . ',' . $teacherroleid;
+        set_config('dporoles', $badconfig, 'tool_dataprivacy');
+
+        // Get the assigned PO roles.
+        $roleids = api::get_assigned_privacy_officer_roles();
+
+        // There should only be one PO role.
+        $this->assertCount(1, $roleids);
+        // Confirm it contains the manager role.
+        $this->assertContains($managerroleid, $roleids);
+        // And it does not contain the editing teacher role.
+        $this->assertNotContains($editingteacherroleid, $roleids);
+    }
+
     /**
      * Test for api::approve_data_request().
      */
@@ -1321,6 +1362,164 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $this->assertEquals($data->contexts->used, $contextids, '', 0.0, 10, true);
     }
 
+    /**
+     * Data provider for \tool_dataprivacy_api_testcase::test_set_context_defaults
+     */
+    public function set_context_defaults_provider() {
+        $contextlevels = [
+            [CONTEXT_COURSECAT],
+            [CONTEXT_COURSE],
+            [CONTEXT_MODULE],
+            [CONTEXT_BLOCK],
+        ];
+        $paramsets = [
+            [true, true, false, false], // Inherit category and purpose, Not for activity, Don't override.
+            [true, false, false, false], // Inherit category but not purpose, Not for activity, Don't override.
+            [false, true, false, false], // Inherit purpose but not category, Not for activity, Don't override.
+            [false, false, false, false], // Don't inherit both category and purpose, Not for activity, Don't override.
+            [false, false, false, true], // Don't inherit both category and purpose, Not for activity, Override instances.
+        ];
+        $data = [];
+        foreach ($contextlevels as $level) {
+            foreach ($paramsets as $set) {
+                $data[] = array_merge($level, $set);
+            }
+            if ($level == CONTEXT_MODULE) {
+                // Add a combination where defaults for activity is being set.
+                $data[] = [CONTEXT_MODULE, false, false, true, false];
+                $data[] = [CONTEXT_MODULE, false, false, true, true];
+            }
+        }
+        return $data;
+    }
+
+    /**
+     * Test for \tool_dataprivacy\api::set_context_defaults()
+     *
+     * @dataProvider set_context_defaults_provider
+     * @param int $contextlevel The context level
+     * @param bool $inheritcategory Whether to set category value as INHERIT.
+     * @param bool $inheritpurpose Whether to set purpose value as INHERIT.
+     * @param bool $foractivity Whether to set defaults for an activity.
+     * @param bool $override Whether to override instances.
+     */
+    public function test_set_context_defaults($contextlevel, $inheritcategory, $inheritpurpose, $foractivity, $override) {
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+
+        // Generate course cat, course, block, assignment, forum instances.
+        $coursecat = $generator->create_category();
+        $course = $generator->create_course(['category' => $coursecat->id]);
+        $block = $generator->create_block('online_users');
+        $assign = $generator->create_module('assign', ['course' => $course->id]);
+        $forum = $generator->create_module('forum', ['course' => $course->id]);
+
+        $coursecatcontext = context_coursecat::instance($coursecat->id);
+        $coursecontext = context_course::instance($course->id);
+        $blockcontext = context_block::instance($block->id);
+
+        list($course, $assigncm) = get_course_and_cm_from_instance($assign->id, 'assign');
+        list($course, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum');
+        $assigncontext = context_module::instance($assigncm->id);
+        $forumcontext = context_module::instance($forumcm->id);
+
+        // Generate purposes and categories.
+        $category1 = api::create_category((object)['name' => 'Test category 1']);
+        $category2 = api::create_category((object)['name' => 'Test category 2']);
+        $purpose1 = api::create_purpose((object)[
+            'name' => 'Test purpose 1', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+        ]);
+        $purpose2 = api::create_purpose((object)[
+            'name' => 'Test purpose 2', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+        ]);
+
+        // Assign purposes and categories to contexts.
+        $coursecatctxinstance = api::set_context_instance((object) [
+            'contextid' => $coursecatcontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+        $coursectxinstance = api::set_context_instance((object) [
+            'contextid' => $coursecontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+        $blockctxinstance = api::set_context_instance((object) [
+            'contextid' => $blockcontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+        $assignctxinstance = api::set_context_instance((object) [
+            'contextid' => $assigncontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+        $forumctxinstance = api::set_context_instance((object) [
+            'contextid' => $forumcontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+
+        $categoryid = $inheritcategory ? context_instance::INHERIT : $category2->get('id');
+        $purposeid = $inheritpurpose ? context_instance::INHERIT : $purpose2->get('id');
+        $activity = '';
+        if ($contextlevel == CONTEXT_MODULE && $foractivity) {
+            $activity = 'assign';
+        }
+        $result = api::set_context_defaults($contextlevel, $categoryid, $purposeid, $activity, $override);
+        $this->assertTrue($result);
+
+        $targetctxinstance = false;
+        switch ($contextlevel) {
+            case CONTEXT_COURSECAT:
+                $targetctxinstance = $coursecatctxinstance;
+                break;
+            case CONTEXT_COURSE:
+                $targetctxinstance = $coursectxinstance;
+                break;
+            case CONTEXT_MODULE:
+                $targetctxinstance = $assignctxinstance;
+                break;
+            case CONTEXT_BLOCK:
+                $targetctxinstance = $blockctxinstance;
+                break;
+        }
+        $this->assertNotFalse($targetctxinstance);
+
+        // Check the context instances.
+        $instanceexists = context_instance::record_exists($targetctxinstance->get('id'));
+        if ($override) {
+            // If overridden, context instances on this context level would have been deleted.
+            $this->assertFalse($instanceexists);
+
+            // Check forum context instance.
+            $forumctxexists = context_instance::record_exists($forumctxinstance->get('id'));
+            if ($contextlevel != CONTEXT_MODULE || $foractivity) {
+                // The forum context instance won't be affected in this test if:
+                // - The overridden defaults are not for context modules.
+                // - Only the defaults for assign have been set.
+                $this->assertTrue($forumctxexists);
+            } else {
+                // If we're overriding for the whole course module context level,
+                // then this forum context instance will be deleted as well.
+                $this->assertFalse($forumctxexists);
+            }
+        } else {
+            // Otherwise, the context instance record remains.
+            $this->assertTrue($instanceexists);
+        }
+
+        // Check defaults.
+        list($defaultpurpose, $defaultcategory) = data_registry::get_defaults($contextlevel, $activity);
+        if (!$inheritpurpose) {
+            $this->assertEquals($purposeid, $defaultpurpose);
+        }
+        if (!$inheritcategory) {
+            $this->assertEquals($categoryid, $defaultcategory);
+        }
+    }
+
     /**
      * Perform setup for the test_add_request_contexts_with_status_xxxxx tests.
      *
diff --git a/admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php b/admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php
new file mode 100644 (file)
index 0000000..857bb33
--- /dev/null
@@ -0,0 +1,284 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Step definitions to generate database fixtures for the data privacy tool.
+ *
+ * @package    tool_dataprivacy
+ * @category   test
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+
+use Behat\Gherkin\Node\TableNode as TableNode;
+use Behat\Behat\Tester\Exception\PendingException as PendingException;
+use tool_dataprivacy\api;
+
+/**
+ * Step definitions to generate database fixtures for the data privacy tool.
+ *
+ * @package    tool_dataprivacy
+ * @category   test
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_tool_dataprivacy extends behat_base {
+
+    /**
+     * Each element specifies:
+     * - The data generator suffix used.
+     * - The required fields.
+     * - The mapping between other elements references and database field names.
+     * @var array
+     */
+    protected static $elements = array(
+        'categories' => array(
+            'datagenerator' => 'category',
+            'required' => array()
+        ),
+        'purposes' => array(
+            'datagenerator' => 'purpose',
+            'required' => array()
+        ),
+    );
+
+    /**
+     * Creates the specified element. More info about available elements in http://docs.moodle.org/dev/Acceptance_testing#Fixtures.
+     *
+     * @Given /^the following data privacy "(?P<element_string>(?:[^"]|\\")*)" exist:$/
+     *
+     * @param string    $elementname The name of the entity to add
+     * @param TableNode $data
+     */
+    public function the_following_data_categories_exist($elementname, TableNode $data) {
+
+        // Now that we need them require the data generators.
+        require_once(__DIR__.'/../../../../../lib/phpunit/classes/util.php');
+
+        if (empty(self::$elements[$elementname])) {
+            throw new PendingException($elementname . ' data generator is not implemented');
+        }
+
+        $datagenerator = testing_util::get_data_generator();
+        $dataprivacygenerator = $datagenerator->get_plugin_generator('tool_dataprivacy');
+
+        $elementdatagenerator = self::$elements[$elementname]['datagenerator'];
+        $requiredfields = self::$elements[$elementname]['required'];
+        if (!empty(self::$elements[$elementname]['switchids'])) {
+            $switchids = self::$elements[$elementname]['switchids'];
+        }
+
+        foreach ($data->getHash() as $elementdata) {
+
+            // Check if all the required fields are there.
+            foreach ($requiredfields as $requiredfield) {
+                if (!isset($elementdata[$requiredfield])) {
+                    throw new Exception($elementname . ' requires the field ' . $requiredfield . ' to be specified');
+                }
+            }
+
+            // Switch from human-friendly references to ids.
+            if (isset($switchids)) {
+                foreach ($switchids as $element => $field) {
+                    $methodname = 'get_' . $element . '_id';
+
+                    // Not all the switch fields are required, default vars will be assigned by data generators.
+                    if (isset($elementdata[$element])) {
+                        // Temp $id var to avoid problems when $element == $field.
+                        $id = $this->{$methodname}($elementdata[$element]);
+                        unset($elementdata[$element]);
+                        $elementdata[$field] = $id;
+                    }
+                }
+            }
+
+            // Preprocess the entities that requires a special treatment.
+            if (method_exists($this, 'preprocess_' . $elementdatagenerator)) {
+                $elementdata = $this->{'preprocess_' . $elementdatagenerator}($elementdata);
+            }
+
+            // Creates element.
+            $methodname = 'create_' . $elementdatagenerator;
+            if (method_exists($dataprivacygenerator, $methodname)) {
+                // Using data generators directly.
+                $dataprivacygenerator->{$methodname}($elementdata);
+
+            } else if (method_exists($this, 'process_' . $elementdatagenerator)) {
+                // Using an alternative to the direct data generator call.
+                $this->{'process_' . $elementdatagenerator}($elementdata);
+            } else {
+                throw new PendingException($elementname . ' data generator is not implemented');
+            }
+        }
+    }
+
+    /**
+     * Sets the data category and data storage purpose for the site.
+     *
+     * @Given /^I set the site category and purpose to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     *
+     * @param string $category The ID of the category to be set for the instance.
+     * @param string $purpose The ID of the purpose to be set for the instance.
+     */
+    public function i_set_the_site_category_and_purpose($category, $purpose) {
+        $category = \tool_dataprivacy\category::get_record(['name' => $category]);
+        $purpose = \tool_dataprivacy\purpose::get_record(['name' => $purpose]);
+        $data = (object)[
+            'contextlevel' => CONTEXT_SYSTEM,
+            'categoryid' => $category->get('id'),
+            'purposeid' => $purpose->get('id'),
+        ];
+        api::set_contextlevel($data);
+    }
+
+    /**
+     * Sets the data category and data storage purpose for a course category instance.
+     *
+     * @Given /^I set the category and purpose for the course category "(?P<categoryname_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     *
+     * @param string $name The instance name. It should match the name or the idnumber.
+     * @param string $category The ID of the category to be set for the instance.
+     * @param string $purpose The ID of the purpose to be set for the instance.
+     */
+    public function i_set_the_category_and_purpose_for_course_category($name, $category, $purpose) {
+        global $DB;
+
+        $params = [
+            'name' => $name,
+            'idnumber' => $name,
+        ];
+        $select = 'name = :name OR idnumber = :idnumber';
+        $coursecatid = $DB->get_field_select('course_categories', 'id', $select, $params, MUST_EXIST);
+        $context = context_coursecat::instance($coursecatid);
+
+        $this->set_category_and_purpose($context->id, $category, $purpose);
+    }
+
+    /**
+     * Sets the data category and data storage purpose for a course instance.
+     *
+     * @Given /^I set the category and purpose for the course "(?P<coursename_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     *
+     * @param string $name The instance name. It should match the fullname or the shortname, or the idnumber.
+     * @param string $category The ID of the category to be set for the instance.
+     * @param string $purpose The ID of the purpose to be set for the instance.
+     */
+    public function i_set_the_category_and_purpose_for_course($name, $category, $purpose) {
+        global $DB;
+
+        $params = [
+            'shortname' => $name,
+            'fullname' => $name,
+            'idnumber' => $name,
+        ];
+        $select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
+        $courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
+        $context = context_course::instance($courseid);
+
+        $this->set_category_and_purpose($context->id, $category, $purpose);
+    }
+
+    /**
+     * Sets the data category and data storage purpose for a course instance.
+     *
+     * @Given /^I set the category and purpose for the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" in course "(?P<coursename_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     *
+     * @param string $name The instance name. It should match the name of the activity.
+     * @param string $type The activity type. E.g. assign, quiz, forum, etc.
+     * @param string $coursename The course name. It should match the fullname or the shortname, or the idnumber.
+     * @param string $category The ID of the category to be set for the instance.
+     * @param string $purpose The ID of the purpose to be set for the instance.
+     */
+    public function i_set_the_category_and_purpose_for_activity($name, $type, $coursename, $category, $purpose) {
+        global $DB;
+
+        $params = [
+            'shortname' => $coursename,
+            'fullname' => $coursename,
+            'idnumber' => $coursename,
+        ];
+        $select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
+        $courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
+
+        $cmid = null;
+        $cms = get_coursemodules_in_course($type, $courseid);
+        foreach ($cms as $cm) {
+            if ($cm->name === $name || $cm->idnumber === $name) {
+                $cmid = $cm->id;
+                break;
+            }
+        }
+        if ($cmid === null) {
+            throw new coding_exception("Activity module '{$name}' of type '{$type}' not found!");
+        }
+        $context = context_module::instance($cmid);
+
+        $this->set_category_and_purpose($context->id, $category, $purpose);
+    }
+
+    /**
+     * Sets the data category and data storage purpose for a course instance.
+     *
+     * @Given /^I set the category and purpose for the "(?P<blockname_string>(?:[^"]|\\")*)" block in the "(?P<coursename_string>(?:[^"]|\\")*)" course to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     *
+     * @param string $name The instance name. It should match the name of the block. (e.g. online_users)
+     * @param string $coursename The course name. It should match the fullname or the shortname, or the idnumber.
+     * @param string $category The ID of the category to be set for the instance.
+     * @param string $purpose The ID of the purpose to be set for the instance.
+     */
+    public function i_set_the_category_and_purpose_for_block($name, $coursename, $category, $purpose) {
+        global $DB;
+
+        $params = [
+            'shortname' => $coursename,
+            'fullname' => $coursename,
+            'idnumber' => $coursename,
+        ];
+        $select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
+        $courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
+
+        // Fetch the course context.
+        $coursecontext = context_course::instance($courseid);
+
+        // Fetch the block record and context.
+        $blockid = $DB->get_field('block_instances', 'id', ['blockname' => $name, 'parentcontextid' => $coursecontext->id]);
+        $context = context_block::instance($blockid);
+
+        // Set the category and purpose.
+        $this->set_category_and_purpose($context->id, $category, $purpose);
+    }
+
+    /**
+     * Sets the category and purpose for a context instance.
+     *
+     * @param int $contextid The context ID.
+     * @param int $categoryname The category name.
+     * @param int $purposename The purpose name.
+     * @throws coding_exception
+     */
+    protected function set_category_and_purpose($contextid, $categoryname, $purposename) {
+        $category = \tool_dataprivacy\category::get_record(['name' => $categoryname]);
+        $purpose = \tool_dataprivacy\purpose::get_record(['name' => $purposename]);
+
+        api::set_context_instance((object) [
+            'contextid' => $contextid,
+            'purposeid' => $purpose->get('id'),
+            'categoryid' => $category->get('id'),
+        ]);
+    }
+}
index bdc43e7..9eb2a8a 100644 (file)
@@ -56,3 +56,93 @@ Feature: Manage data requests
     And I click on "Actions" "link" in the "Jane Doe" "table_row"
     And I should see "View the request"
     But I should not see "Mark as complete"
+
+  @javascript
+  Scenario: Bulk accepting requests
+    Given I log in as "student1"
+    And I follow "Profile" in the user menu
+    And I should see "Data requests"
+    And I click on "Data requests" "link"
+    And I should see "New request"
+    And I click on "New request" "link"
+    And I should see "Type"
+    And I should see "Comments"
+    And I set the field "Type" to "Export all of my personal data"
+    And I set the field "Comments" to "Comment1"
+    And I press "Save changes"
+    And I should see "Your request has been submitted to the privacy officer"
+    And I log out
+    And I log in as "student2"
+    And I follow "Profile" in the user menu
+    And I should see "Data requests"
+    And I click on "Data requests" "link"
+    And I should see "New request"
+    And I click on "New request" "link"
+    And I should see "Type"
+    And I should see "Comments"
+    And I set the field "Type" to "Export all of my personal data"
+    And I set the field "Comments" to "Comment2"
+    And I press "Save changes"
+    And I should see "Your request has been submitted to the privacy officer"
+    And I log out
+    And I trigger cron
+    And I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I should see "Comment1" in the "John Doe" "table_row"
+    And I should see "Awaiting approval" in the "John Doe" "table_row"
+    And I should see "Comment2" in the "Jane Doe" "table_row"
+    And I should see "Awaiting approval" in the "Jane Doe" "table_row"
+    And I click on ".selectrequests" "css_element" in the "John Doe" "table_row"
+    And I click on ".selectrequests" "css_element" in the "Jane Doe" "table_row"
+    And I set the field with xpath "//select[@id='bulk-action']" to "Approve"
+    And I press "Confirm"
+    And I should see "Approve requests"
+    And I should see "Do you really want to bulk approve the selected data requests?"
+    When I press "Approve requests"
+    Then I should see "Approved" in the "John Doe" "table_row"
+    And I should see "Approved" in the "Jane Doe" "table_row"
+
+  @javascript
+  Scenario: Bulk denying requests
+    Given I log in as "student1"
+    And I follow "Profile" in the user menu
+    And I should see "Data requests"
+    And I click on "Data requests" "link"
+    And I should see "New request"
+    And I click on "New request" "link"
+    And I should see "Type"
+    And I should see "Comments"
+    And I set the field "Type" to "Export all of my personal data"
+    And I set the field "Comments" to "Comment1"
+    And I press "Save changes"
+    And I should see "Your request has been submitted to the privacy officer"
+    And I log out
+    And I log in as "student2"
+    And I follow "Profile" in the user menu
+    And I should see "Data requests"
+    And I click on "Data requests" "link"
+    And I should see "New request"
+    And I click on "New request" "link"
+    And I should see "Type"
+    And I should see "Comments"
+    And I set the field "Type" to "Export all of my personal data"
+    And I set the field "Comments" to "Comment2"
+    And I press "Save changes"
+    And I should see "Your request has been submitted to the privacy officer"
+    And I log out
+    And I trigger cron
+    And I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I should see "Comment1" in the "John Doe" "table_row"
+    And I should see "Awaiting approval" in the "John Doe" "table_row"
+    And I should see "Comment2" in the "Jane Doe" "table_row"
+    And I should see "Awaiting approval" in the "Jane Doe" "table_row"
+    And I click on ".selectrequests" "css_element" in the "John Doe" "table_row"
+    And I click on ".selectrequests" "css_element" in the "Jane Doe" "table_row"
+    And I set the field with xpath "//select[@id='bulk-action']" to "Deny"
+    And I press "Confirm"
+    And I should see "Deny requests"
+    And I should see "Do you really want to bulk deny the selected data requests?"
+    When I press "Deny requests"
+    Then I should see "Rejected" in the "John Doe" "table_row"
+    And I should see "Rejected" in the "Jane Doe" "table_row"
diff --git a/admin/tool/dataprivacy/tests/behat/manage_defaults.feature b/admin/tool/dataprivacy/tests/behat/manage_defaults.feature
new file mode 100644 (file)
index 0000000..a6287d5
--- /dev/null
@@ -0,0 +1,298 @@
+@tool @tool_dataprivacy @javascript
+Feature: Manage data registry defaults
+  As the privacy officer
+  In order to manage the data registry
+  I need to be able to manage the default data categories and data storage purposes for various context levels.
+
+  Background:
+    Given I log in as "admin"
+    And the following "categories" exist:
+      | name                   | idnumber | category |
+      | Science and technology | scitech  |          |
+      | Physics                | st-phys  | scitech  |
+    And the following "courses" exist:
+      | fullname                  | shortname   | category |
+      | Fundamentals of physics 1 | Physics 101 | st-phys  |
+    And the following "activities" exist:
+      | activity | name         | idnumber | course      |
+      | assign   | Assignment 1 | assign1  | Physics 101 |
+      | forum    | Forum 1      | forum1   | Physics 101 |
+    And the following "blocks" exist:
+      | blockname    | contextlevel | reference   | pagetypepattern | defaultregion |
+      | online_users | Course       | Physics 101 | course-view-*   | site-post     |
+    And the following data privacy "categories" exist:
+      | name          |
+      | Site category |
+      | Category 1    |
+      | Category 2    |
+    And the following data privacy "purposes" exist:
+      | name         | retentionperiod |
+      | Site purpose | P10Y           |
+      | Purpose 1    | P3Y            |
+      | Purpose 2    | P5Y            |
+    And I set the site category and purpose to "Site category" and "Site purpose"
+
+  Scenario: Set course category data registry defaults
+    Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Category 2"
+    And the field "purposeid" matches value "Purpose 2"
+    And I should see "5 years"
+
+  Scenario: Set course category data registry defaults with override
+    Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    And I click on "Reset instances with custom values" "checkbox"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I should see "3 years"
+
+  Scenario: Set course data registry defaults
+    Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Courses" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Category 2"
+    And the field "purposeid" matches value "Purpose 2"
+    And I should see "5 years (after the course end date)"
+
+  Scenario: Set course data registry defaults with override
+    Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Courses" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    And I click on "Reset instances with custom values" "checkbox"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I should see "3 years (after the course end date)"
+
+  Scenario: Set module level data registry defaults
+    Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Activity modules" "link"
+    And I should see "Inherit"
+    And I should see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Category 2"
+    And the field "purposeid" matches value "Purpose 2"
+    And I should see "5 years (after the course end date)"
+
+  Scenario: Set module level data registry defaults with override
+    Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Activity modules" "link"
+    And I should see "Inherit"
+    And I should see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    And I click on "Reset instances with custom values" "checkbox"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I click on "Forum 1 (Forum)" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I should see "3 years (after the course end date)"
+
+  Scenario: Set data registry defaults for an activity module
+    Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Activity modules" "link"
+    And I should see "Inherit"
+    And I should see "Add a new module default"
+    And I press "Add a new module default"
+    And I set the field "Activity module" to "Assignment"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    When I press "Save changes"
+    Then I should see "Category 1" in the "Assignment" "table_row"
+    And I should see "Purpose 1" in the "Assignment" "table_row"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Category 2"
+    And the field "purposeid" matches value "Purpose 2"
+    And I should see "5 years (after the course end date)"
+
+  Scenario: Set data registry defaults for an activity module with override
+    Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Activity modules" "link"
+    And I should see "Inherit"
+    And I should see "Add a new module default"
+    And I press "Add a new module default"
+    And I set the field "Activity module" to "Assignment"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    And I click on "Reset instances with custom values" "checkbox"
+    When I press "Save changes"
+    Then I should see "Category 1" in the "Assignment" "table_row"
+    And I should see "Purpose 1" in the "Assignment" "table_row"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I should see "3 years (after the course end date)"
+
+  Scenario: Set block category data registry defaults
+    Given I set the category and purpose for the "online_users" block in the "Physics 101" course to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Blocks" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Blocks" "link"
+    And I wait until the page is ready
+    And I click on "Online users" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Category 2"
+    And the field "purposeid" matches value "Purpose 2"
+    And I should see "5 years (after the course end date)"
+
+  Scenario: Set course category data registry defaults with override
+    Given I set the category and purpose for the "online_users" block in the "Physics 101" course to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Blocks" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    And I click on "Reset instances with custom values" "checkbox"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Blocks" "link"
+    And I wait until the page is ready
+    And I click on "Online users" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I should see "3 years (after the course end date)"
index 71d0111..b768786 100644 (file)
@@ -28,6 +28,7 @@ global $CFG;
 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
 
 use tool_dataprivacy\api;
+use tool_dataprivacy\context_instance;
 use tool_dataprivacy\external;
 
 /**
@@ -282,4 +283,383 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
         $this->expectException(dml_missing_record_exception::class);
         external::get_data_request($this->requestid + 1);
     }
+
+    /**
+     * Test for \tool_dataprivacy\external::set_context_defaults()
+     * when called by a user that doesn't have the manage registry capability.
+     */
+    public function test_set_context_defaults_no_capability() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $this->setUser($user);
+        $this->expectException(required_capability_exception::class);
+        external::set_context_defaults(CONTEXT_COURSECAT, context_instance::INHERIT, context_instance::INHERIT, '', false);
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::set_context_defaults().
+     *
+     * We're just checking the module context level here to test the WS function.
+     * More testing is done in \tool_dataprivacy_api_testcase::test_set_context_defaults().
+     *
+     * @dataProvider get_options_provider
+     * @param bool $modulelevel Whether defaults are to be applied on the module context level or for an activity only.
+     * @param bool $override Whether to override instances.
+     */
+    public function test_set_context_defaults($modulelevel, $override) {
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+
+        // Generate course cat, course, block, assignment, forum instances.
+        $coursecat = $generator->create_category();
+        $course = $generator->create_course(['category' => $coursecat->id]);
+        $assign = $generator->create_module('assign', ['course' => $course->id]);
+        list($course, $assigncm) = get_course_and_cm_from_instance($assign->id, 'assign');
+        $assigncontext = context_module::instance($assigncm->id);
+
+        // Generate purpose and category.
+        $category1 = api::create_category((object)['name' => 'Test category 1']);
+        $category2 = api::create_category((object)['name' => 'Test category 2']);
+        $purpose1 = api::create_purpose((object)[
+            'name' => 'Test purpose 1', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+        ]);
+        $purpose2 = api::create_purpose((object)[
+            'name' => 'Test purpose 2', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+        ]);
+
+        // Set a custom purpose and ID for this assignment instance.
+        $assignctxinstance = api::set_context_instance((object) [
+            'contextid' => $assigncontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+
+        $modulename = $modulelevel ? 'assign' : '';
+        $categoryid = $category2->get('id');
+        $purposeid = $purpose2->get('id');
+        $result = external::set_context_defaults(CONTEXT_MODULE, $categoryid, $purposeid, $modulename, $override);
+
+        // Extract the result.
+        $return = external_api::clean_returnvalue(external::set_context_defaults_returns(), $result);
+        $this->assertTrue($return['result']);
+
+        // Check the assignment context instance.
+        $instanceexists = context_instance::record_exists($assignctxinstance->get('id'));
+        if ($override) {
+            // The custom assign instance should have been deleted.
+            $this->assertFalse($instanceexists);
+        } else {
+            // The custom assign instance should still exist.
+            $this->assertTrue($instanceexists);
+        }
+
+        // Check the saved defaults.
+        list($savedpurpose, $savedcategory) = \tool_dataprivacy\data_registry::get_defaults(CONTEXT_MODULE, $modulename);
+        $this->assertEquals($categoryid, $savedcategory);
+        $this->assertEquals($purposeid, $savedpurpose);
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::get_category_options()
+     * when called by a user that doesn't have the manage registry capability.
+     */
+    public function test_get_category_options_no_capability() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $this->setUser($user);
+        $this->expectException(required_capability_exception::class);
+        external::get_category_options(true, true);
+    }
+
+    /**
+     * Data provider for \tool_dataprivacy_external_testcase::test_XX_options().
+     */
+    public function get_options_provider() {
+        return [
+            [false, false],
+            [false, true],
+            [true, false],
+            [true, true],
+        ];
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::get_category_options().
+     *
+     * @dataProvider get_options_provider
+     * @param bool $includeinherit Whether "Inherit" would be included to the options.
+     * @param bool $includenotset Whether "Not set" would be included to the options.
+     */
+    public function test_get_category_options($includeinherit, $includenotset) {
+        $this->setAdminUser();
+
+        // Prepare our expected options.
+        $expectedoptions = [];
+        if ($includeinherit) {
+            $expectedoptions[] = [
+                'id' => context_instance::INHERIT,
+                'name' => get_string('inherit', 'tool_dataprivacy'),
+            ];
+        }
+
+        if ($includenotset) {
+            $expectedoptions[] = [
+                'id' => context_instance::NOTSET,
+                'name' => get_string('notset', 'tool_dataprivacy'),
+            ];
+        }
+
+        for ($i = 1; $i <= 3; $i++) {
+            $category = api::create_category((object)['name' => 'Category ' . $i]);
+            $expectedoptions[] = [
+                'id' => $category->get('id'),
+                'name' => $category->get('name'),
+            ];
+        }
+
+        // Call the WS function.
+        $result = external::get_category_options($includeinherit, $includenotset);
+
+        // Extract the options.
+        $return = (object) external_api::clean_returnvalue(external::get_category_options_returns(), $result);
+        $options = $return->options;
+
+        // Make sure everything checks out.
+        $this->assertCount(count($expectedoptions), $options);
+        foreach ($options as $option) {
+            $this->assertContains($option, $expectedoptions);
+        }
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::get_purpose_options()
+     * when called by a user that doesn't have the manage registry capability.
+     */
+    public function test_get_purpose_options_no_capability() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $this->setUser($user);
+        $this->expectException(required_capability_exception::class);
+        external::get_category_options(true, true);
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::get_purpose_options().
+     *
+     * @dataProvider get_options_provider
+     * @param bool $includeinherit Whether "Inherit" would be included to the options.
+     * @param bool $includenotset Whether "Not set" would be included to the options.
+     */
+    public function test_get_purpose_options($includeinherit, $includenotset) {
+        $this->setAdminUser();
+
+        // Prepare our expected options.
+        $expectedoptions = [];
+        if ($includeinherit) {
+            $expectedoptions[] = [
+                'id' => context_instance::INHERIT,
+                'name' => get_string('inherit', 'tool_dataprivacy'),
+            ];
+        }
+
+        if ($includenotset) {
+            $expectedoptions[] = [
+                'id' => context_instance::NOTSET,
+                'name' => get_string('notset', 'tool_dataprivacy'),
+            ];
+        }
+
+        for ($i = 1; $i <= 3; $i++) {
+            $purpose = api::create_purpose((object)[
+                'name' => 'Purpose ' . $i, 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+            ]);
+            $expectedoptions[] = [
+                'id' => $purpose->get('id'),
+                'name' => $purpose->get('name'),
+            ];
+        }
+
+        // Call the WS function.
+        $result = external::get_purpose_options($includeinherit, $includenotset);
+
+        // Extract the options.
+        $return = (object) external_api::clean_returnvalue(external::get_purpose_options_returns(), $result);
+        $options = $return->options;
+
+        // Make sure everything checks out.
+        $this->assertCount(count($expectedoptions), $options);
+        foreach ($options as $option) {
+            $this->assertContains($option, $expectedoptions);
+        }
+    }
+
+    /**
+     * Data provider for \tool_dataprivacy_external_testcase::get_activity_options().
+     */
+    public function get_activity_options_provider() {
+        return [
+            [false, false, true],
+            [false, true, true],
+            [true, false, true],
+            [true, true, true],
+            [false, false, false],
+            [false, true, false],
+            [true, false, false],
+            [true, true, false],
+        ];
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::get_activity_options().
+     *
+     * @dataProvider get_activity_options_provider
+     * @param bool $inheritcategory Whether the category would be set to "Inherit".
+     * @param bool $inheritpurpose Whether the purpose would be set to "Inherit".
+     * @param bool $nodefaults Whether to fetch only activities that don't have defaults.
+     */
+    public function test_get_activity_options($inheritcategory, $inheritpurpose, $nodefaults) {
+        $this->setAdminUser();
+
+        $category = api::create_category((object)['name' => 'Test category']);
+        $purpose = api::create_purpose((object)[
+            'name' => 'Test purpose ', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+        ]);
+        $categoryid = $category->get('id');
+        $purposeid = $purpose->get('id');
+
+        if ($inheritcategory) {
+            $categoryid = context_instance::INHERIT;
+        }
+        if ($inheritpurpose) {
+            $purposeid = context_instance::INHERIT;
+        }
+
+        // Set the context default for the assignment module.
+        api::set_context_defaults(CONTEXT_MODULE, $categoryid, $purposeid, 'assign');
+
+        // Call the WS function.
+        $result = external::get_activity_options($nodefaults);
+
+        // Extract the options.
+        $return = (object) external_api::clean_returnvalue(external::get_activity_options_returns(), $result);
+        $options = $return->options;
+
+        // Make sure the options list is not empty.
+        $this->assertNotEmpty($options);
+
+        $pluginwithdefaults = [
+            'name' => 'assign',
+            'displayname' => get_string('pluginname', 'assign')
+        ];
+
+        // If we don't want plugins with defaults to be listed or if both of the category and purpose are set to inherit,
+        // the assign module should be listed.
+        if (!$nodefaults || ($inheritcategory && $inheritpurpose)) {
+            $this->assertContains($pluginwithdefaults, $options);
+        } else {
+            $this->assertNotContains($pluginwithdefaults, $options);
+        }
+    }
+
+    /**
+     * Test for external::bulk_approve_data_requests().
+     */
+    public function test_bulk_approve_data_requests() {
+        $generator = new testing_data_generator();
+        $requester1 = $generator->create_user();
+        $comment1 = 'sample comment';
+        // Login as requester2.
+        $this->setUser($requester1->id);
+        // Create delete data request.
+        $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
+
+        $requestid1 = $datarequest1->get('id');
+        $requestid2 = $this->requestid;
+
+        $this->setAdminUser();
+        api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        $result = external::bulk_approve_data_requests([$requestid1, $requestid2]);
+        $return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
+        $this->assertTrue($return->result);
+        $this->assertEmpty($return->warnings);
+    }
+
+    /**
+     * Test for external::bulk_approve_data_requests() for a non-existent request ID.
+     */
+    public function test_bulk_approve_data_requests_non_existent() {
+        $generator = new testing_data_generator();
+        $requester1 = $generator->create_user();
+        $comment1 = 'sample comment';
+        // Login as requester2.
+        $this->setUser($requester1->id);
+        // Create delete data request.
+        $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
+
+        $requestid1 = $datarequest1->get('id');
+        $requestid2 = $this->requestid;
+
+        $this->setAdminUser();
+        api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        $result = external::bulk_approve_data_requests([$requestid1 + 1, $requestid2]);
+        $return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
+        $this->assertFalse($return->result);
+        $this->assertCount(1, $return->warnings);
+        $warning = reset($return->warnings);
+        $this->assertEquals('errorrequestnotfound', $warning['warningcode']);
+        $this->assertEquals($requestid1 + 1, $warning['item']);
+    }
+
+    /**
+     * Test for external::bulk_deny_data_requests().
+     */
+    public function test_bulk_deny_data_requests() {
+        $generator = new testing_data_generator();
+        $requester1 = $generator->create_user();
+        $comment1 = 'sample comment';
+        // Login as requester2.
+        $this->setUser($requester1->id);
+        // Create delete data request.
+        $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
+
+        $requestid1 = $datarequest1->get('id');
+        $requestid2 = $this->requestid;
+
+        $this->setAdminUser();
+        api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        $result = external::bulk_deny_data_requests([$requestid1, $requestid2]);
+        $return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
+        $this->assertTrue($return->result);
+        $this->assertEmpty($return->warnings);
+    }
+
+    /**
+     * Test for external::bulk_deny_data_requests() for a non-existent request ID.
+     */
+    public function test_bulk_deny_data_requests_non_existent() {
+        $generator = new testing_data_generator();
+        $requester1 = $generator->create_user();
+        $comment1 = 'sample comment';
+        // Login as requester2.
+        $this->setUser($requester1->id);
+        // Create delete data request.
+        $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
+
+        $requestid1 = $datarequest1->get('id');
+        $requestid2 = $this->requestid;
+
+        $this->setAdminUser();
+        api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        $result = external::bulk_deny_data_requests([$requestid1 + 1, $requestid2]);
+        $return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
+        $this->assertFalse($return->result);
+        $this->assertCount(1, $return->warnings);
+        $warning = reset($return->warnings);
+        $this->assertEquals('errorrequestnotfound', $warning['warningcode']);
+        $this->assertEquals($requestid1 + 1, $warning['item']);
+    }
 }
diff --git a/admin/tool/dataprivacy/tests/generator/lib.php b/admin/tool/dataprivacy/tests/generator/lib.php
new file mode 100644 (file)
index 0000000..744e34a
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Data privacy tool data generator.
+ *
+ * @package    tool_dataprivacy
+ * @category   test
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use tool_dataprivacy\api;
+use tool_dataprivacy\category;
+use tool_dataprivacy\purpose;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Data privacy tool data generator class.
+ *
+ * @package    tool_dataprivacy
+ * @category   test
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_dataprivacy_generator extends component_generator_base {
+
+    /** @var int Number of created categories. */
+    protected $categorycount = 0;
+
+    /** @var int Number of created purposes. */
+    protected $purposecount = 0;
+
+    /**
+     * Reset process.
+     *
+     * Do not call directly.
+     *
+     * @return void
+     */
+    public function reset() {
+        $this->categorycount = 0;
+        $this->purposecount = 0;
+    }
+
+    /**
+     * Create a new category.
+     *
+     * @param array|stdClass $record
+     * @return category
+     */
+    public function create_category($record = null) {
+        $this->categorycount++;
+        $i = $this->categorycount;
+        $record = (object)$record;
+
+        if (!isset($record->name)) {
+            $record->name = "Test purpose $i";
+        }
+
+        if (!isset($record->description)) {
+            $record->description = "{$record->name} description";
+        }
+
+        $category = api::create_category($record);
+
+        return $category;
+    }
+
+    /**
+     * Create a new purpose.
+     *
+     * @param array|stdClass $record
+     * @return purpose
+     */
+    public function create_purpose($record = null) {
+        $this->purposecount++;
+        $i = $this->purposecount;
+        $record = (object)$record;
+
+        if (!isset($record->name)) {
+            $record->name = "Test purpose $i";
+        }
+
+        if (!isset($record->description)) {
+            $record->description = "{$record->name} $i description";
+        }
+
+        if (!isset($record->retentionperiod)) {
+            $record->retentionperiod = 'PT1M';
+        }
+
+        if (!isset($record->lawfulbases)) {
+            $record->lawfulbases = 'gdpr_art_6_1_a';
+        }
+
+        $purpose = api::create_purpose($record);
+
+        return $purpose;
+    }
+}
index ca666e3..a65e257 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2018082100;
+$plugin->version   = 2018091100;
 $plugin->requires  = 2018050800;        // Moodle 3.5dev (Build 2018031600) and upwards.
 $plugin->component = 'tool_dataprivacy';
index 8f49696..31edf3a 100644 (file)
@@ -52,7 +52,7 @@ $string['privacy:metadata:log:origin'] = 'The origin of the event';
 $string['privacy:metadata:log:other'] = 'Additional information about the event';
 $string['privacy:metadata:log:realuserid'] = 'The ID of the real user behind the event, when masquerading a user.';
 $string['privacy:metadata:log:relateduserid'] = 'The ID of a user related to this event';
-$string['privacy:metadata:log:timecreated'] = 'The time at which the event occurred';
+$string['privacy:metadata:log:timecreated'] = 'The time when the event occurred';
 $string['privacy:metadata:log:userid'] = 'The ID of the user who triggered this event';
 $string['read'] = 'Read';
 $string['tablenotfound'] = 'Specified table was not found';
index 5f35555..d40f4cf 100644 (file)
@@ -31,7 +31,7 @@ $string['privacy:metadata:log'] = 'A collection of past events';
 $string['privacy:metadata:log:action'] = 'A description of the action';
 $string['privacy:metadata:log:info'] = 'Additional information';
 $string['privacy:metadata:log:ip'] = 'The IP address used at the time of the event';
-$string['privacy:metadata:log:time'] = 'The date at wich the action took place';
+$string['privacy:metadata:log:time'] = 'The time when the action took place';
 $string['privacy:metadata:log:url'] = 'The URL related to the event';
 $string['privacy:metadata:log:userid'] = 'The ID of the user who performed the action';
 $string['taskcleanup'] = 'Legacy log table cleanup';
index 33c229f..14a1d2b 100644 (file)
@@ -33,6 +33,6 @@ $string['privacy:metadata:log:origin'] = 'The origin of the event';
 $string['privacy:metadata:log:other'] = 'Additional information about the event';
 $string['privacy:metadata:log:realuserid'] = 'The ID of the real user behind the event, when masquerading a user.';
 $string['privacy:metadata:log:relateduserid'] = 'The ID of a user related to this event';
-$string['privacy:metadata:log:timecreated'] = 'The time at which the event occurred';
+$string['privacy:metadata:log:timecreated'] = 'The time when the event occurred';
 $string['privacy:metadata:log:userid'] = 'The ID of the user who triggered this event';
 $string['taskcleanup'] = 'Log table cleanup';
index c847b03..a9c378d 100644 (file)
@@ -96,9 +96,9 @@ $string['oneyear'] = 'One year';
 $string['pluginname'] = 'Inbound message configuration';
 $string['privacy:metadata:coreuserkey'] = 'User\'s keys to validate the email received';
 $string['privacy:metadata:messagelist'] = 'A list of message identifiers which failed validation and requires further authorisation';
-$string['privacy:metadata:messagelist:address'] = 'The address at which the email was sent';
+$string['privacy:metadata:messagelist:address'] = 'The address where the email was sent';
 $string['privacy:metadata:messagelist:messageid'] = 'The message ID';
-$string['privacy:metadata:messagelist:timecreated'] = 'The time at which the record was made';
+$string['privacy:metadata:messagelist:timecreated'] = 'The time when the record was made';
 $string['privacy:metadata:messagelist:userid'] = 'The ID of user who need to approve the message';
 $string['replysubjectprefix'] = 'Re:';
 $string['requirevalidation'] = 'Validate sender address';
index 195af12..f2d08ac 100644 (file)
@@ -84,7 +84,7 @@ $string['mobilefeatures'] = 'Mobile features';
 $string['mobilenotificationsdisabledwarning'] = 'Mobile notifications are not enabled. They should be enabled in Manage message outputs.';
 $string['mobilesettings'] = 'Mobile settings';
 $string['offlineuse'] = 'Offline use';
-$string['pluginname'] = 'Moodle Mobile tools';
+$string['pluginname'] = 'Moodle app tools';
 $string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
 $string['remoteaddons'] = 'Remote add-ons';
 $string['selfsignedoruntrustedcertificatewarning'] = 'It seems that the HTTPS certificate is self-signed or not trusted. The mobile app will only work with trusted sites.';
index da1432a..020e5bf 100644 (file)
Binary files a/admin/tool/policy/amd/build/acceptmodal.min.js and b/admin/tool/policy/amd/build/acceptmodal.min.js differ
index a642ac1..b923629 100644 (file)
@@ -82,6 +82,21 @@ define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/n
             }
         ];
 
+        /**
+         * @var {object} currentTrigger The triggered HTML jQuery object
+         * @private
+         */
+        AcceptOnBehalf.prototype.currentTrigger = null;
+
+        /**
+         * @var {object} triggers The trigger selectors
+         * @private
+         */
+        AcceptOnBehalf.prototype.triggers = {
+            SINGLE: 'a[data-action=acceptmodal]',
+            BULK: 'input[data-action=acceptmodal]'
+        };
+
         /**
          * Initialise the class.
          *
@@ -89,21 +104,22 @@ define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/n
          */
         AcceptOnBehalf.prototype.init = function() {
             // Initialise for links accepting policies for individual users.
-            var triggers = $('a[data-action=acceptmodal]');
-            triggers.on('click', function(e) {
+            $(this.triggers.SINGLE).on('click', function(e) {
                 e.preventDefault();
+                this.currentTrigger = $(e.currentTarget);
                 var href = $(e.currentTarget).attr('href'),
                     formData = href.slice(href.indexOf('?') + 1);
                 this.showFormModal(formData);
             }.bind(this));
 
             // Initialise for multiple users acceptance form.
-            triggers = $('form[data-action=acceptmodal]');
-            triggers.on('submit', function(e) {
+            $(this.triggers.BULK).on('click', function(e) {
                 e.preventDefault();
-                if ($(e.currentTarget).find('input[type=checkbox][name="userids[]"]:checked').length) {
-                    var formData = $(e.currentTarget).serialize();
-                    this.showFormModal(formData, triggers);
+                this.currentTrigger = $(e.currentTarget);
+                var form = $(e.currentTarget).closest('form');
+                if (form.find('input[type=checkbox][name="userids[]"]:checked').length) {
+                    var formData = form.serialize();
+                    this.showFormModal(formData);
                 } else {
                     Str.get_strings(this.stringKeys).done(function(strings) {
                         Notification.alert('', strings[2], strings[3]);
@@ -116,9 +132,8 @@ define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/n
          * Show modal with a form
          *
          * @param {String} formData
-         * @param {object} triggerElement The trigger HTML jQuery object
          */
-        AcceptOnBehalf.prototype.showFormModal = function(formData, triggerElement) {
+        AcceptOnBehalf.prototype.showFormModal = function(formData) {
             var action;
             var params = formData.split('&');
             for (var i = 0; i < params.length; i++) {
@@ -143,7 +158,7 @@ define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/n
                     type: ModalFactory.types.SAVE_CANCEL,
                     title: title,
                     body: ''
-                }, triggerElement).done(function(modal) {
+                }).done(function(modal) {
                     this.modal = modal;
                     this.setupFormModal(formData, saveText);
                 }.bind(this));
@@ -250,6 +265,7 @@ define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/n
                 M.core_formchangechecker.reset_form_dirty_state();
             });
             this.modal.destroy();
+            this.currentTrigger.focus();
         };
 
         return /** @alias module:tool_policy/acceptmodal */ {
index a723519..a0efdcc 100644 (file)
@@ -415,7 +415,7 @@ class acceptances_table extends \table_sql {
      */
     public function wrap_html_start() {
         echo \html_writer::start_tag('form',
-            ['action' => new \moodle_url('/admin/tool/policy/accept.php'), 'data-action' => 'acceptmodal']);
+            ['action' => new \moodle_url('/admin/tool/policy/accept.php')]);
         echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]);
         echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'returnurl',
             'value' => $this->get_return_url()]);
@@ -433,8 +433,8 @@ class acceptances_table extends \table_sql {
     public function wrap_html_finish() {
         global $PAGE;
         if ($this->canagreeany) {
-            echo \html_writer::empty_tag('input', ['type' => 'submit',
-                'value' => get_string('consentbulk', 'tool_policy'), 'class' => 'btn btn-primary']);
+            echo \html_writer::empty_tag('input', ['type' => 'submit', 'data-action' => 'acceptmodal',
+                'value' => get_string('consentbulk', 'tool_policy'), 'class' => 'btn btn-primary m-t-1']);
             $PAGE->requires->js_call_amd('tool_policy/acceptmodal', 'getInstance', [\context_system::instance()->id]);
         }
         echo "</form>\n";
index f53af7a..3ff0daf 100644 (file)
@@ -71,10 +71,12 @@ class accept_policy extends \moodleform {
 
         $mform->addElement('hidden', 'returnurl');
         $mform->setType('returnurl', PARAM_LOCALURL);
-
-        $mform->addElement('static', 'user', get_string('acceptanceusers', 'tool_policy'), join(', ', $usernames));
-        $mform->addElement('static', 'policy', get_string('acceptancepolicies', 'tool_policy'),
-            join(', ', $versionnames));
+        $useracceptancelabel = (count($usernames) > 1) ? get_string('acceptanceusers', 'tool_policy') :
+                get_string('user');
+        $mform->addElement('static', 'user', $useracceptancelabel, join(', ', $usernames));
+        $policyacceptancelabel = (count($versionnames) > 1) ? get_string('acceptancepolicies', 'tool_policy') :
+                get_string('policydochdrpolicy', 'tool_policy');
+        $mform->addElement('static', 'policy', $policyacceptancelabel, join(', ', $versionnames));
 
         if ($revoke) {
             $mform->addElement('static', 'ack', '', get_string('revokeacknowledgement', 'tool_policy'));
index 8f2a1d5..3ede09b 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['acceptanceacknowledgement'] = 'I acknowledge that I have received a request to give consent on behalf of user(s).';
+$string['acceptanceacknowledgement'] = 'I acknowledge that I have received a request to give consent on behalf of the above user(s).';
 $string['acceptancecount'] = '{$a->agreedcount} of {$a->policiescount}';
 $string['acceptancenote'] = 'Remarks';
 $string['acceptancepolicies'] = 'Policies';
@@ -53,7 +53,7 @@ $string['agreepolicies'] = 'Please agree to the following policies';
 $string['backtoprevious'] = 'Go back to previous page';
 $string['backtotop'] = 'Back to top';
 $string['consentbulk'] = 'Consent';
-$string['consentdetails'] = 'Give consent on behalf of user';
+$string['consentdetails'] = 'Give consent on behalf of user(s)';
 $string['consentpagetitle'] = 'Consent';
 $string['contactdpo'] = 'For any questions about the policies please contact the privacy officer.';
 $string['dataproc'] = 'Personal data processing';
@@ -78,7 +78,7 @@ $string['filterpolicy'] = 'Policy: {$a}';
 $string['guestconsent:continue'] = 'Continue';
 $string['guestconsentmessage'] = 'If you continue browsing this website, you agree to our policies:';
 $string['iagree'] = 'I agree to the {$a}';
-$string['iagreetothepolicy'] = 'Give consent on behalf of user';
+$string['iagreetothepolicy'] = 'Give consent';
 $string['inactivate'] = 'Set status to "Inactive"';
 $string['inactivating'] = 'Inactivating a policy';
 $string['inactivatingconfirm'] = '<p>You are about to inactivate policy <em>\'{$a->name}\'</em> version <em>\'{$a->revision}\'</em>.</p>';
@@ -159,7 +159,7 @@ $string['privacy:metadata:versions:contentformat'] = 'The format of the content
 $string['privacysettings'] = 'Privacy settings';
 $string['readpolicy'] = 'Please read our {$a}';
 $string['refertofullpolicytext'] = 'Please refer to the full {$a} if you would like to review the text.';
-$string['revokeacknowledgement'] = 'I acknowledge that I have received a request to withdraw consent on behalf of user(s).';
+$string['revokeacknowledgement'] = 'I acknowledge that I have received a request to withdraw consent on behalf of the above user(s).';
 $string['revokedetails'] = 'Withdraw user consent';
 $string['save'] = 'Save';
 $string['saveasdraft'] = 'Save as draft';
index b8c6d6e..23198da 100644 (file)
@@ -58,12 +58,12 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     And I navigate to "Users > Privacy and policies > Manage policies" in site administration
     And I click on "1 of 4 (25%)" "link" in the "This site policy" "table_row"
     And I click on "Consent not given" "link" in the "User One" "table_row"
-    Then I should see "Give consent on behalf of user"
+    Then I should see "Give consent"
     And I should see "User One"
     And I should see "This site policy"
-    And I should see "I acknowledge that I have received a request to give consent on behalf of user(s)."
+    And I should see "I acknowledge that I have received a request to give consent on behalf of the above user(s)."
     And I set the field "Remarks" to "Consent received from a parent"
-    And I press "Give consent on behalf of user"
+    And I press "Give consent"
     And "Consent given on behalf of user" "icon" should exist in the "User One" "table_row"
     And "Max Manager" "link" should exist in the "User One" "table_row"
     And "Consent received from a parent" "text" should exist in the "User One" "table_row"
@@ -84,12 +84,12 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     And I navigate to "Users > Privacy and policies > Manage policies" in site administration
     And I click on "1 of 4 (25%)" "link" in the "This site policy" "table_row"
     And I click on "Consent not given" "link" in the "User One" "table_row"
-    Then I should see "Give consent on behalf of user"
+    Then I should see "Give consent"
     And I should see "User One"
     And I should see "This site policy"
-    And I should see "I acknowledge that I have received a request to give consent on behalf of user(s)."
+    And I should see "I acknowledge that I have received a request to give consent on behalf of the above user(s)."
     And I set the field "Remarks" to "Consent received from a parent"
-    And I press "Give consent on behalf of user"
+    And I press "Give consent"
     And "Consent given on behalf of user" "icon" should exist in the "User One" "table_row"
     And "Max Manager" "link" should exist in the "User One" "table_row"
     And "Consent received from a parent" "text" should exist in the "User One" "table_row"
@@ -151,12 +151,12 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     And I press "Next"
     And I navigate to "Users > Privacy and policies > User agreements" in site administration
     And I click on "Consent not given; click to give consent on behalf of user for This site policy" "link" in the "User One" "table_row"
-    Then I should see "Give consent on behalf of user"
+    Then I should see "Give consent"
     And I should see "User One"
     And I should see "This site policy"
-    And I should see "I acknowledge that I have received a request to give consent on behalf of user(s)."
+    And I should see "I acknowledge that I have received a request to give consent on behalf of the above user(s)."
     And I set the field "Remarks" to "Consent received from a parent"
-    And I press "Give consent on behalf of user"
+    And I press "Give consent"
     And "Consent given on behalf of user" "icon" should exist in the "User One" "table_row"
     And "Consent not given; click to give consent on behalf of user for This privacy policy" "icon" should exist in the "User One" "table_row"
     And I click on "1 of 2" "link" in the "User One" "table_row"
@@ -184,12 +184,12 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     And I press "Next"
     And I navigate to "Users > Privacy and policies > User agreements" in site administration
     And I click on "Consent not given; click to give consent on behalf of user for This site policy" "link" in the "User One" "table_row"
-    Then I should see "Give consent on behalf of user"
+    Then I should see "Give consent"
     And I should see "User One"
     And I should see "This site policy"
-    And I should see "I acknowledge that I have received a request to give consent on behalf of user(s)."
+    And I should see "I acknowledge that I have received a request to give consent on behalf of the above user(s)."
     And I set the field "Remarks" to "Consent received from a parent"
-    And I press "Give consent on behalf of user"
+    And I press "Give consent"
     And "Consent given on behalf of user" "icon" should exist in the "User One" "table_row"
     And "Consent not given; click to give consent on behalf of user for This privacy policy" "icon" should exist in the "User One" "table_row"
     And I click on "1 of 2" "link" in the "User One" "table_row"
@@ -249,14 +249,48 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     And I navigate to "Users > Privacy and policies > Manage policies" in site administration
     And I click on "1 of 4 (25%)" "link" in the "This site policy" "table_row"
     And I click on "Consent not given" "link" in the "User One" "table_row"
-    Then I should see "Give consent on behalf of user"
+    Then I should see "Give consent"
     And I should see "User One"
     And I should see "This site policy"
-    And I should see "I acknowledge that I have received a request to give consent on behalf of user(s)."
+    And I should see "I acknowledge that I have received a request to give consent on behalf of the above user(s)."
     And I set the field "Remarks" to "Consent received from a parent"
-    And I press "Give consent on behalf of user"
+    And I press "Give consent"
     And "Consent given on behalf of user" "icon" should exist in the "User One" "table_row"
     And "Max Manager" "link" should not exist in the "User One" "table_row"
     And "Admin User" "link" should exist in the "User One" "table_row"
     And "Consent received from a parent" "text" should exist in the "User One" "table_row"
     And "Consent not given" "icon" should exist in the "User Two" "table_row"
+
+  @javascript
+  Scenario: Bulk agree on behalf of another users as a manager, multiple policies, javascript on
+    Given I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Manage policies" in site administration
+    And I open the action menu in "This privacy policy" "table_row"
+    And I click on "Set status to \"Active\"" "link" in the "This privacy policy" "table_row"
+    And I press "Activate"
+    And I set the following system permissions of "Manager" role:
+      | capability | permission |
+      | tool/policy:acceptbehalf | Allow |
+    And I log out
+    When I log in as "manager"
+    And I press "Next"
+    And I press "Next"
+    And I set the field "I agree to the This site policy" to "1"
+    And I set the field "I agree to the This privacy policy" to "1"
+    And I press "Next"
+    And I navigate to "Users > Privacy and policies > User agreements" in site administration
+    And I click on "Select" "checkbox" in the "User One" "table_row"
+    And I press "Consent"
+    And I should see "Give consent on behalf of user(s)"
+    And I should see "One"
+    And I press "Cancel"
+    And I should not see "Give consent on behalf of user(s)"
+    And I click on "Select" "checkbox" in the "User Two" "table_row"
+    And I press "Consent"
+    And I should see "Give consent on behalf of user(s)"
+    And I should see "User One, User Two"
+    When I press "Give consent"
+    Then "Consent given on behalf of user" "icon" should exist in the "User One" "table_row"
+    And I should see "2 of 2" in the "User One" "table_row"
+    And "Consent given on behalf of user" "icon" should exist in the "User Two" "table_row"
+    And I should see "2 of 2" in the "User Two" "table_row"
index 4c4333c..078e046 100644 (file)
@@ -154,7 +154,7 @@ $string['tour1_content_customisation'] = 'To customise the look of your site and
 $string['tour1_title_blockregion'] = 'Block region';
 $string['tour1_content_blockregion'] = 'There is still a block region over here. We recommend removing the Navigation and Administration blocks completely, as all the functionality is elsewhere in the Boost theme.';
 $string['tour1_title_addingblocks'] = 'Adding blocks';
-$string['tour1_content_addingblocks'] = 'In fact, think carefully about including any blocks on your pages. Blocks are not shown on the Moodle Mobile app, so as a general rule it\'s much better to make sure your site works well without any blocks.';
+$string['tour1_content_addingblocks'] = 'In fact, think carefully about including any blocks on your pages. Blocks are not shown in the Moodle app, so as a general rule it\'s much better to make sure your site works well without any blocks.';
 $string['tour1_title_end'] = 'End of tour';
 $string['tour1_content_end'] = 'This is the end of your user tour. It won\'t show again unless you reset it using the link in the footer. As an admin you can also create your own tours like this!';
 
@@ -170,9 +170,9 @@ $string['tour2_content_opendrawer'] = 'Try opening the nav drawer now.';
 $string['tour2_title_participants'] = 'Course participants';
 $string['tour2_content_participants'] = 'View participants here. This is also where you go to add or remove students.';
 $string['tour2_title_addblock'] = 'Add a block';
-$string['tour2_content_addblock'] = 'If you turn editing on you can add blocks from the nav drawer. However, think carefully about including any blocks on your pages. Blocks are not shown on the Moodle Mobile app, so for the best user experience it is better to make sure your course works well without any blocks.';
+$string['tour2_content_addblock'] = 'If you turn editing on you can add blocks from the nav drawer. However, think carefully about including any blocks on your pages. Blocks are not shown in the Moodle app, so for the best user experience it is better to make sure your course works well without any blocks.';
 $string['tour2_title_addingblocks'] = 'Adding blocks';
-$string['tour2_content_addingblocks'] = 'You can add blocks to this page using this button. However, think carefully about including any blocks on your pages. Blocks are not shown on the Moodle Mobile app, so for the best user experience it is better to make sure your course works well without any blocks.';
+$string['tour2_content_addingblocks'] = 'You can add blocks to this page using this button. However, think carefully about including any blocks on your pages. Blocks are not shown in the Moodle app, so for the best user experience it is better to make sure your course works well without any blocks.';
 $string['tour2_title_end'] = 'End of tour';
 $string['tour2_content_end'] = 'This is the end of your user tour. It won\'t show again unless you reset it using the link in the footer. The site admin can also create further tours for this site if required.';
 $string['privacy:metadata:preference:requested'] = 'The time that a user last manually requested a user tour.';
index 5040996..6d92020 100644 (file)
@@ -149,7 +149,7 @@ $string['start_tls'] = 'Use regular LDAP service (port 389) with TLS encryption'
 $string['start_tls_key'] = 'Use TLS';
 $string['updateremfail'] = 'Error updating LDAP record. Error code: {$a->errno}; Error string: {$a->errstring}<br/>Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
 $string['updateremfailamb'] = 'Failed to update LDAP with ambiguous field {$a->key}; old moodle value: \'{$a->ouvalue}\', new value: \'{$a->nuvalue}\'';
-$string['updateremfailfield'] = 'Failed to update LDAP with non-existent field (\'{$a->ldapkey}\'). Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
+$string['updateremfailfield'] = 'Failed to update LDAP with non-existent field (\'{$a->ldapkey}\'). Key ({$a->key}) - old Moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
 $string['updatepasserror'] = 'Error in user_update_password(). Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiry time. Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expirationtime and/or gracelogins. Error code: {$a->errno}; Error string: {$a->errstring}';
index 1207eb7..40a65dc 100644 (file)
@@ -20,12 +20,13 @@ Feature: Duplicate activities
       | Name | Test database name |
       | Description | Test database description |
     And I duplicate "Test database name" activity
+    And I should see "Test database name (copy)"
     And I wait until section "1" is available
     And I click on "Edit settings" "link" in the "Test database name" activity
     And I set the following fields to these values:
       | Name | Original database name |
     And I press "Save and return to course"
-    And I click on "Edit settings" "link" in the "Test database name" activity
+    And I click on "Edit settings" "link" in the "Test database name (copy)" activity
     And I set the following fields to these values:
       | Name | Duplicated database name |
       | Description | Duplicated database description |
index 00547c6..64c2cd2 100644 (file)
@@ -49,7 +49,8 @@ class block_calendar_month extends block_base {
         $this->content->footer = '';
 
         $courseid = $this->page->course->id;
-        $categoryid = ($this->page->context->contextlevel === CONTEXT_COURSECAT) ? $this->page->category->id : null;
+        $categoryid = ($this->page->context->contextlevel === CONTEXT_COURSECAT && !empty($this->page->category)) ?
+            $this->page->category->id : null;
         $calendar = \calendar_information::create(time(), $courseid, $categoryid);
         list($data, $template) = calendar_get_view($calendar, 'mini', isloggedin(), isloggedin());
 
index e988304..4bb603a 100644 (file)
@@ -131,7 +131,7 @@ class item implements \renderable, \templatable {
         }
 
         // Allow the renderer to format the title and description.
-        $data['title']          = $output->format_title($title);
+        $data['title']          = strip_tags($output->format_title($title));
         $data['description']    = $this->showdescription ? $output->format_description($this->description) : null;
 
         return $data;
index 2e53d97..b81c645 100644 (file)
@@ -135,6 +135,25 @@ function blog_remove_associations_for_course($courseid) {
     $DB->delete_records('blog_association', array('contextid' => $context->id));
 }
 
+/**
+ * Remove module associated blogs and blog tag instances.
+ *
+ * @param  int $modcontextid Module context ID.
+ */
+function blog_remove_associations_for_module($modcontextid) {
+    global $DB;
+
+    if (!empty($assocblogids = $DB->get_fieldset_select('blog_association', 'blogid',
+        'contextid = :contextid', ['contextid' => $modcontextid]))) {
+        list($sql, $params) = $DB->get_in_or_equal($assocblogids, SQL_PARAMS_NAMED);
+
+        $DB->delete_records_select('tag_instance', "itemid $sql", $params);
+        $DB->delete_records_select('post', "id $sql AND module = :module",
+            array_merge($params, ['module' => 'blog']));
+        $DB->delete_records('blog_association', ['contextid' => $modcontextid]);
+    }
+}
+
 /**
  * Given a record in the {blog_external} table, checks the blog's URL
  * for new entries not yet copied into Moodle.
diff --git a/cache/stores/memcache/addinstanceform.php b/cache/stores/memcache/addinstanceform.php
deleted file mode 100644 (file)
index 27135ef..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * The library file for the memcache cache store.
- *
- * This file is part of the memcache cache store, it contains the API for interacting with an instance of the store.
- *
- * @package    cachestore_memcache
- * @copyright  2012 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-require_once($CFG->dirroot.'/cache/forms.php');
-
-/**
- * Form for adding a memcache instance.
- *
- * @copyright  2012 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class cachestore_memcache_addinstance_form extends cachestore_addinstance_form {
-
-    /**
-     * Add the desired form elements.
-     */
-    protected function configuration_definition() {
-        $form = $this->_form;
-        $form->addElement('textarea', 'servers', get_string('servers', 'cachestore_memcache'), array('cols' => 75, 'rows' => 5));
-        $form->addHelpButton('servers', 'servers', 'cachestore_memcache');
-        $form->addRule('servers', get_string('required'), 'required');
-        $form->setType('servers', PARAM_RAW);
-
-        $form->addElement('text', 'prefix', get_string('prefix', 'cachestore_memcache'),
-                array('maxlength' => 5, 'size' => 5));
-        $form->addHelpButton('prefix', 'prefix', 'cachestore_memcache');
-        $form->setType('prefix', PARAM_TEXT); // We set to text but we have a rule to limit to alphanumext.
-        $form->setDefault('prefix', 'mdl_');
-        $form->addRule('prefix', get_string('prefixinvalid', 'cachestore_memcache'), 'regex', '#^[a-zA-Z0-9\-_]+$#');
-        $form->setForceLtr('prefix');
-
-        $form->addElement('header', 'clusteredheader', get_string('clustered', 'cachestore_memcache'));
-
-        $form->addElement('checkbox', 'clustered', get_string('clustered', 'cachestore_memcache'));
-        $form->setDefault('checkbox', false);
-        $form->addHelpButton('clustered', 'clustered', 'cachestore_memcache');
-
-        $form->addElement('textarea', 'setservers', get_string('setservers', 'cachestore_memcache'),
-                array('cols' => 75, 'rows' => 5));
-        $form->addHelpButton('setservers', 'setservers', 'cachestore_memcache');
-        $form->disabledIf('setservers', 'clustered');
-        $form->setType('setservers', PARAM_RAW);
-    }
-
-    /**
-     * Perform minimal validation on the settings form.
-     *
-     * @param array $data
-     * @param array $files
-     */
-    public function validation($data, $files) {
-        $errors = parent::validation($data, $files);
-
-        if (isset($data['clustered']) && ($data['clustered'] == 1)) {
-            // Set servers is required with in cluster mode.
-            if (!isset($data['setservers'])) {
-                $errors['setservers'] = get_string('required');
-            } else {
-                $trimmed = trim($data['setservers']);
-                if (empty($trimmed)) {
-                    $errors['setservers'] = get_string('required');
-                }
-            }
-
-            $validservers = false;
-            if (isset($data['servers'])) {
-                $servers = trim($data['servers']);
-                $servers = explode("\n", $servers);
-                if (count($servers) === 1) {
-                    $validservers = true;
-                }
-            }
-
-            if (!$validservers) {
-                $errors['servers'] = get_string('serversclusterinvalid', 'cachestore_memcache');
-            }
-        }
-
-        return $errors;
-    }
-}
\ No newline at end of file
diff --git a/cache/stores/memcache/classes/privacy/provider.php b/cache/stores/memcache/classes/privacy/provider.php
deleted file mode 100644 (file)
index e7e3779..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Privacy Subsystem implementation for cachestore_memcache.
- *
- * @package    cachestore_memcache
- * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace cachestore_memcache\privacy;
-
-use core_privacy\local\metadata\collection;
-use core_privacy\local\request\contextlist;
-use core_privacy\local\request\approved_contextlist;
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * Privacy Subsystem for cachestore_memcache.
- *
- * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
-
-    /**
-     * Returns meta data about this system.
-     *
-     * @param   collection $collection The initialised collection to add items to.
-     * @return  collection A listing of user data stored through this system.
-     */
-    public static function get_metadata(collection $collection) : collection {
-        $collection->add_external_location_link('memcache', [
-                'data' => 'privacy:metadata:memcache:data',
-            ], 'privacy:metadata:memcache');
-        return $collection;
-    }
-
-    /**
-     * Get the list of contexts that contain user information for the specified user.
-     *
-     * @param   int         $userid     The user to search.
-     * @return  contextlist $contextlist  The contextlist containing the list of contexts used in this plugin.
-     */
-    public static function get_contexts_for_userid(int $userid) : contextlist {
-        return new contextlist();
-    }
-
-    /**
-     * Export all user data for the specified user, in the specified contexts.
-     *
-     * @param approved_contextlist $contextlist The approved contexts to export information for.
-     */
-    public static function export_user_data(approved_contextlist $contextlist) {
-    }
-
-    /**
-     * Delete all use data which matches the specified deletion_criteria.
-     *
-     * @param \context $context A user context.
-     */
-    public static function delete_data_for_all_users_in_context(\context $context) {
-    }
-
-    /**
-     * Delete all user data for the specified user, in the specified contexts.
-     *
-     * @param   approved_contextlist    $contextlist    The approved contexts and user information to delete information for.
-     */
-    public static function delete_data_for_user(approved_contextlist $contextlist) {
-    }
-}
diff --git a/cache/stores/memcache/lang/en/cachestore_memcache.php b/cache/stores/memcache/lang/en/cachestore_memcache.php
deleted file mode 100644 (file)
index 9095825..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * The library file for the memcache cache store.
- *
- * This file is part of the memcache cache store, it contains the API for interacting with an instance of the store.
- *
- * @package    cachestore_memcache
- * @copyright  2012 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-$string['clustered'] = 'Enable clustered servers';
-$string['clustered_help'] = 'This is used to allow read-one, set-multi functionality.
-
-The intended use case is to create an improved store for load-balanced configurations. The store will fetch from one server (usually localhost), but set to many (all the servers in the load-balance pool). For caches with very high read to set ratios, this saves a significant amount of network overhead.
-
-When this setting is enabled, the server listed above will be used for fetching.';
-$string['clusteredheader'] = 'Split servers';
-$string['pluginname'] = 'Memcache';
-$string['prefix'] = 'Key prefix';
-$string['prefix_help'] = 'This prefix is used for all key names on the memcache server.
-* If you only have one Moodle instance using this server, you can leave this value default.
-* Due to key length restrictions, a maximum of 5 characters is permitted.';
-$string['prefixinvalid'] = 'Invalid prefix. You can only use a-z A-Z 0-9-_.';
-$string['privacy:metadata:memcache'] = 'The Memcache cachestore plugin stores data briefly as part of its caching functionality. This data is stored on an Memcache server where data is regularly removed.';
-$string['privacy:metadata:memcache:data'] = 'The various data stored in the cache';
-$string['servers'] = 'Servers';
-$string['servers_help'] = 'This sets the servers that should be utilised by this memcache adapter.
-Servers should be defined one per line and consist of a server address and optionally a port and weight.
-If no port is provided then the default port (11211) is used.
-
-For example:
-<pre>
-server.url.com
-ipaddress:port
-servername:port:weight
-</pre>
-
-If *Enable clustered servers* is enabled below, there must be only one server listed here. This would usually be a name that always resolves to the local machine, like 127.0.0.1 or localhost.';
-$string['serversclusterinvalid'] = 'Exactly one server is required when clustering is enabled.';
-$string['setservers'] = 'Set Servers';
-$string['setservers_help'] = 'This is the list of servers that will updated when data is modified in the cache. Generally the fully qualified name of each server in the pool.
-It **must** include the server listed in *Servers* above, even if by a different hostname.
-Servers should be defined one per line and consist of a server address and optionally a port.
-If no port is provided then the default port (11211) is used.
-
-For example:
-<pre>
-server.url.com
-ipaddress:port
-</pre>';
-$string['sessionhandlerconflict'] = 'Warning: A memcache instance ({$a}) has being configured to use the same memcache server as sessions. Purging all caches will lead to sessions also being purged.';
-$string['testservers'] = 'Test servers';
-$string['testservers_desc'] = 'One or more connection strings for memcache servers to test against. If a test server