Merge branch 'm36_MDL-63789_Search_SOLR_Fixes_Proxy_Auth' of https://github.com/scara...
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 6 Nov 2018 23:08:12 +0000 (00:08 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 6 Nov 2018 23:08:12 +0000 (00:08 +0100)
321 files changed:
admin/roles/classes/privacy/provider.php
admin/roles/tests/privacy_test.php
admin/tool/cohortroles/classes/privacy/provider.php
admin/tool/cohortroles/tests/privacy_test.php
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/data_request.php
admin/tool/dataprivacy/classes/metadata_registry.php
admin/tool/dataprivacy/classes/task/delete_existing_deleted_users.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/task/initiate_data_request_task.php
admin/tool/dataprivacy/classes/task/process_data_request_task.php
admin/tool/dataprivacy/db/install.xml
admin/tool/dataprivacy/db/tasks.php
admin/tool/dataprivacy/db/upgrade.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/lib.php
admin/tool/dataprivacy/settings.php
admin/tool/dataprivacy/templates/component_status.mustache
admin/tool/dataprivacy/templates/data_registry_compliance.mustache
admin/tool/dataprivacy/tests/api_test.php
admin/tool/dataprivacy/tests/task_test.php [new file with mode: 0644]
admin/tool/dataprivacy/version.php
admin/tool/log/classes/local/privacy/logstore_userlist_provider.php [new file with mode: 0644]
admin/tool/log/classes/local/privacy/moodle_database_export_and_delete.php
admin/tool/log/classes/privacy/provider.php
admin/tool/log/store/database/classes/privacy/provider.php
admin/tool/log/store/database/tests/privacy_test.php
admin/tool/log/store/legacy/classes/privacy/provider.php
admin/tool/log/store/legacy/tests/privacy_test.php
admin/tool/log/store/standard/classes/privacy/provider.php
admin/tool/log/store/standard/tests/privacy_test.php
admin/tool/mobile/classes/api.php
admin/tool/oauth2/classes/form/issuer.php
admin/tool/oauth2/issuers.php
admin/tool/oauth2/lang/en/tool_oauth2.php
analytics/classes/model.php
analytics/classes/privacy/provider.php
analytics/tests/privacy_test.php
auth/cas/CAS/CAS/Client.php
auth/cas/CAS/moodle_readme.txt
badges/assertion.php
badges/badge_json.php [new file with mode: 0644]
badges/classes/assertion.php
badges/classes/privacy/provider.php
badges/competency.php [new file with mode: 0644]
badges/competency_action.php [new file with mode: 0644]
badges/competency_form.php [new file with mode: 0644]
badges/edit.php
badges/edit_form.php
badges/endorsement.php [new file with mode: 0644]
badges/endorsement_form.php [new file with mode: 0644]
badges/endorsement_json.php [new file with mode: 0644]
badges/image_author_json.php [new file with mode: 0644]
badges/newbadge.php
badges/related.php [new file with mode: 0644]
badges/related_action.php [new file with mode: 0644]
badges/related_form.php [new file with mode: 0644]
badges/renderer.php
badges/tests/badgeslib_test.php
badges/tests/behat/add_badge.feature
badges/tests/privacy_test.php
blocks/classes/privacy/provider.php
blocks/myoverview/amd/build/main.min.js
blocks/myoverview/amd/build/selectors.min.js [new file with mode: 0644]
blocks/myoverview/amd/build/view.min.js
blocks/myoverview/amd/build/view_nav.min.js
blocks/myoverview/amd/src/main.js
blocks/myoverview/amd/src/selectors.js [new file with mode: 0644]
blocks/myoverview/amd/src/view.js
blocks/myoverview/amd/src/view_nav.js
blocks/myoverview/classes/privacy/provider.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/lib.php
blocks/myoverview/templates/course-action-menu.mustache
blocks/myoverview/templates/courses-view.mustache
blocks/myoverview/templates/nav-grouping-selector.mustache
blocks/myoverview/templates/view-cards.mustache
blocks/myoverview/templates/view-list.mustache
blocks/myoverview/templates/view-summary.mustache
blocks/myoverview/tests/behat/block_myoverview_dashboard.feature
blocks/myoverview/tests/behat/block_myoverview_favourite.feature
blocks/myoverview/tests/behat/block_myoverview_hidden.feature [new file with mode: 0644]
blocks/myoverview/tests/privacy_test.php
blocks/recentlyaccessedcourses/amd/build/main.min.js [new file with mode: 0644]
blocks/recentlyaccessedcourses/amd/src/main.js [new file with mode: 0644]
blocks/recentlyaccessedcourses/block_recentlyaccessedcourses.php [new file with mode: 0644]
blocks/recentlyaccessedcourses/classes/output/main.php [new file with mode: 0644]
blocks/recentlyaccessedcourses/classes/output/renderer.php [new file with mode: 0644]
blocks/recentlyaccessedcourses/classes/privacy/provider.php [new file with mode: 0644]
blocks/recentlyaccessedcourses/db/access.php [new file with mode: 0644]
blocks/recentlyaccessedcourses/db/install.php [new file with mode: 0644]
blocks/recentlyaccessedcourses/lang/en/block_recentlyaccessedcourses.php [new file with mode: 0644]
blocks/recentlyaccessedcourses/pix/courses.svg [new file with mode: 0644]
blocks/recentlyaccessedcourses/templates/main.mustache [new file with mode: 0644]
blocks/recentlyaccessedcourses/templates/no-courses.mustache [new file with mode: 0644]
blocks/recentlyaccessedcourses/templates/placeholder-course.mustache [new file with mode: 0644]
blocks/recentlyaccessedcourses/templates/recentlyaccessedcourses-view.mustache [new file with mode: 0644]
blocks/recentlyaccessedcourses/templates/view-cards.mustache [new file with mode: 0644]
blocks/recentlyaccessedcourses/tests/behat/block_recentlyaccessedcourses_dashboard.feature [new file with mode: 0644]
blocks/recentlyaccessedcourses/version.php [new file with mode: 0644]
blocks/recentlyaccesseditems/amd/build/main.min.js [new file with mode: 0644]
blocks/recentlyaccesseditems/amd/build/repository.min.js [new file with mode: 0644]
blocks/recentlyaccesseditems/amd/src/main.js [new file with mode: 0644]
blocks/recentlyaccesseditems/amd/src/repository.js [new file with mode: 0644]
blocks/recentlyaccesseditems/block_recentlyaccesseditems.php [new file with mode: 0644]
blocks/recentlyaccesseditems/classes/external.php [new file with mode: 0644]
blocks/recentlyaccesseditems/classes/external/recentlyaccesseditems_item_exporter.php [new file with mode: 0644]
blocks/recentlyaccesseditems/classes/helper.php [new file with mode: 0644]
blocks/recentlyaccesseditems/classes/observer.php [new file with mode: 0644]
blocks/recentlyaccesseditems/classes/output/main.php [new file with mode: 0644]
blocks/recentlyaccesseditems/classes/output/renderer.php [new file with mode: 0644]
blocks/recentlyaccesseditems/classes/privacy/provider.php [new file with mode: 0644]
blocks/recentlyaccesseditems/db/access.php [new file with mode: 0644]
blocks/recentlyaccesseditems/db/events.php [new file with mode: 0644]
blocks/recentlyaccesseditems/db/install.xml [new file with mode: 0644]
blocks/recentlyaccesseditems/db/services.php [new file with mode: 0644]
blocks/recentlyaccesseditems/lang/en/block_recentlyaccesseditems.php [new file with mode: 0644]
blocks/recentlyaccesseditems/pix/items.svg [new file with mode: 0644]
blocks/recentlyaccesseditems/templates/main.mustache [new file with mode: 0644]
blocks/recentlyaccesseditems/templates/no-items.mustache [new file with mode: 0644]
blocks/recentlyaccesseditems/templates/placeholder-item.mustache [new file with mode: 0644]
blocks/recentlyaccesseditems/templates/recentlyaccesseditems-view.mustache [new file with mode: 0644]
blocks/recentlyaccesseditems/templates/view-cards.mustache [new file with mode: 0644]
blocks/recentlyaccesseditems/tests/behat/block_recentlyaccesseditems_dashboard.feature [new file with mode: 0644]
blocks/recentlyaccesseditems/tests/externallib_test.php [new file with mode: 0644]
blocks/recentlyaccesseditems/tests/observer_test.php [new file with mode: 0644]
blocks/recentlyaccesseditems/version.php [new file with mode: 0644]
blocks/starredcourses/amd/build/main.min.js [new file with mode: 0644]
blocks/starredcourses/amd/build/repository.min.js [new file with mode: 0644]
blocks/starredcourses/amd/src/main.js [new file with mode: 0644]
blocks/starredcourses/amd/src/repository.js [new file with mode: 0644]
blocks/starredcourses/block_starredcourses.php [new file with mode: 0644]
blocks/starredcourses/classes/external.php [new file with mode: 0644]
blocks/starredcourses/classes/output/main.php [new file with mode: 0644]
blocks/starredcourses/classes/output/renderer.php [new file with mode: 0644]
blocks/starredcourses/classes/privacy/provider.php [new file with mode: 0644]
blocks/starredcourses/db/access.php [new file with mode: 0644]
blocks/starredcourses/db/services.php [new file with mode: 0644]
blocks/starredcourses/lang/en/block_starredcourses.php [new file with mode: 0644]
blocks/starredcourses/pix/courses.svg [new file with mode: 0644]
blocks/starredcourses/templates/main.mustache [new file with mode: 0644]
blocks/starredcourses/templates/no-courses.mustache [new file with mode: 0644]
blocks/starredcourses/templates/placeholder-course.mustache [new file with mode: 0644]
blocks/starredcourses/templates/view-cards.mustache [new file with mode: 0644]
blocks/starredcourses/templates/view.mustache [new file with mode: 0644]
blocks/starredcourses/version.php [new file with mode: 0644]
blocks/tests/privacy_test.php
blog/classes/privacy/provider.php
blog/tests/privacy_test.php
calendar/classes/privacy/provider.php
calendar/tests/privacy_test.php
competency/classes/privacy/provider.php
competency/tests/privacy_test.php
course/amd/build/events.min.js [new file with mode: 0644]
course/amd/build/repository.min.js
course/amd/src/events.js [new file with mode: 0644]
course/amd/src/repository.js
course/classes/external/course_summary_exporter.php
course/externallib.php
course/lib.php
course/templates/favouriteicon.mustache [moved from blocks/myoverview/templates/favourite-icon.mustache with 82% similarity]
course/tests/courselib_test.php
course/tests/externallib_test.php
course/upgrade.txt
enrol/externallib.php
enrol/tests/enrollib_test.php
enrol/tests/externallib_test.php
enrol/upgrade.txt
grade/classes/privacy/provider.php
grade/grading/classes/privacy/provider.php
grade/querylib.php
grade/tests/privacy_test.php
group/classes/privacy/provider.php
group/externallib.php
group/tests/externallib_test.php
install/lang/el/install.php
install/lang/lt/langconfig.php
lang/en/badges.php
lang/en/course.php
lang/en/question.php
lib/accesslib.php
lib/amd/build/paged_content_factory.min.js
lib/amd/build/paged_content_pages.min.js
lib/amd/src/paged_content_factory.js
lib/amd/src/paged_content_pages.js
lib/badgeslib.php
lib/blocklib.php
lib/classes/oauth2/api.php
lib/classes/oauth2/issuer.php
lib/classes/plugin_manager.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/editor/atto/classes/privacy/provider.php
lib/editor/atto/plugins/recordrtc/db/access.php [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/lang/en/atto_recordrtc.php
lib/editor/atto/plugins/recordrtc/lib.php
lib/editor/atto/plugins/recordrtc/version.php
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-button/moodle-atto_recordrtc-button-debug.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-button/moodle-atto_recordrtc-button-min.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-button/moodle-atto_recordrtc-button.js
lib/editor/atto/plugins/recordrtc/yui/src/button/js/button.js
lib/editor/atto/tests/privacy_provider.php
lib/enrollib.php
lib/gradelib.php
lib/horde/framework/Horde/Imap/Client/Tokenize.php
lib/horde/framework/Horde/Mail/Rfc822.php
lib/horde/readme_moodle.txt
lib/lessphp/Exception/Chunk.php
lib/lessphp/moodle_readme.txt
lib/questionlib.php
lib/tcpdf/readme_moodle.txt
lib/tcpdf/tcpdf.php
lib/tests/oauth2_test.php
lib/tests/questionlib_test.php
login/confirm.php
message/classes/api.php
message/classes/helper.php
message/classes/privacy/provider.php
message/externallib.php
message/index.php
message/tests/api_test.php
message/tests/externallib_test.php
message/tests/privacy_provider_test.php
message/upgrade.txt
mod/assign/classes/privacy/provider.php
mod/assign/feedback/comments/locallib.php
mod/assign/feedback/editpdf/classes/combined_document.php
mod/assign/feedback/editpdf/classes/task/convert_submissions.php
mod/assign/lib.php
mod/assign/submission/comments/tests/privacy_test.php
mod/assign/tests/locallib_test.php
mod/assignment/classes/privacy/provider.php
mod/chat/chatd.php
mod/choice/lib.php
mod/data/lib.php
mod/feedback/lib.php
mod/forum/lib.php
mod/glossary/lib.php
mod/lesson/lib.php
mod/quiz/classes/privacy/legacy_quizaccess_polyfill.php
mod/quiz/classes/privacy/provider.php
mod/quiz/classes/privacy/quizaccess_user_provider.php [new file with mode: 0644]
mod/quiz/lib.php
mod/quiz/tests/privacy_legacy_quizaccess_polyfill_test.php
mod/quiz/tests/privacy_provider_test.php
mod/scorm/classes/event/sco_launched.php
mod/scorm/lib.php
mod/scorm/tests/externallib_test.php
mod/survey/lib.php
mod/workshop/classes/privacy/provider.php
mod/workshop/lang/en/workshop.php
mod/workshop/tests/privacy_provider_test.php
notes/classes/privacy/provider.php
notes/tests/privacy_test.php
privacy/classes/local/request/core_userlist_provider.php
privacy/classes/local/request/userlist.php
privacy/tests/provider_test.php
question/classes/privacy/provider.php
question/exportone.php [new file with mode: 0644]
question/format.php
question/format/gift/tests/behat/import_export.feature
question/format/gift/tests/fixtures/questions_in_category.gift.txt [new file with mode: 0644]
question/preview.php
question/tests/behat/preview_question.feature
question/tests/privacy_provider_test.php
question/type/ddimageortext/amd/build/form.min.js [new file with mode: 0644]
question/type/ddimageortext/amd/build/question.min.js [new file with mode: 0644]
question/type/ddimageortext/amd/src/form.js [new file with mode: 0644]
question/type/ddimageortext/amd/src/question.js [new file with mode: 0644]
question/type/ddimageortext/edit_ddimageortext_form.php
question/type/ddimageortext/rendererbase.php
question/type/ddimageortext/styles.css
question/type/ddimageortext/tests/behat/behat_qtype_ddimageortext.php
question/type/ddimageortext/tests/behat/preview.feature
question/type/ddimageortext/tests/walkthrough_test.php
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-dd/moodle-qtype_ddimageortext-dd-debug.js [deleted file]
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-dd/moodle-qtype_ddimageortext-dd-min.js [deleted file]
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-dd/moodle-qtype_ddimageortext-dd.js [deleted file]
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-form/moodle-qtype_ddimageortext-form-debug.js [deleted file]
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-form/moodle-qtype_ddimageortext-form-min.js [deleted file]
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-form/moodle-qtype_ddimageortext-form.js [deleted file]
question/type/ddimageortext/yui/src/ddimageortext/build.json [deleted file]
question/type/ddimageortext/yui/src/ddimageortext/js/ddimageortext.js [deleted file]
question/type/ddimageortext/yui/src/ddimageortext/meta/ddimageortext.json [deleted file]
question/type/ddimageortext/yui/src/form/build.json [deleted file]
question/type/ddimageortext/yui/src/form/js/form.js [deleted file]
question/type/ddimageortext/yui/src/form/meta/form.json [deleted file]
question/type/ddmarker/amd/build/shapes.min.js
question/type/ddmarker/amd/src/shapes.js
question/type/multianswer/edit_multianswer_form.php
question/type/multianswer/tests/behat/add.feature [new file with mode: 0644]
repository/googledocs/tests/generator/lib.php
repository/nextcloud/classes/access_controlled_link_manager.php [new file with mode: 0644]
repository/nextcloud/classes/configuration_exception.php [new file with mode: 0644]
repository/nextcloud/classes/issuer_management.php [new file with mode: 0644]
repository/nextcloud/classes/ocs_client.php [new file with mode: 0644]
repository/nextcloud/classes/privacy/provider.php [new file with mode: 0644]
repository/nextcloud/classes/request_exception.php [new file with mode: 0644]
repository/nextcloud/db/access.php [new file with mode: 0644]
repository/nextcloud/lang/en/repository_nextcloud.php [new file with mode: 0644]
repository/nextcloud/lib.php [new file with mode: 0644]
repository/nextcloud/pix/icon.svg [new file with mode: 0644]
repository/nextcloud/tests/access_controlled_link_manager_test.php [new file with mode: 0644]
repository/nextcloud/tests/fixtures/testable_access_controlled_link_manager.php [new file with mode: 0644]
repository/nextcloud/tests/generator/lib.php [new file with mode: 0644]
repository/nextcloud/tests/lib_test.php [new file with mode: 0644]
repository/nextcloud/tests/ocs_test.php [new file with mode: 0644]
repository/nextcloud/version.php [new file with mode: 0644]
repository/tests/generator_test.php
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/bs4alphacompat.scss
theme/boost/style/moodle.css
theme/boost/templates/core/action_menu_trigger.mustache
theme/boost/templates/core/filemanager_fileselect.mustache
theme/boost/templates/header.mustache
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_myoverview/course-action-menu.mustache
user/action_redir.php
user/index.php
version.php

index e420f9e..d711db6 100644 (file)
@@ -13,6 +13,7 @@
 //
 // 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 core_role.
  *
@@ -20,7 +21,9 @@
  * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
+
 namespace core_role\privacy;
+
 defined('MOODLE_INTERNAL') || die();
 
 use \core_privacy\local\metadata\collection;
@@ -28,6 +31,8 @@ use \core_privacy\local\request\contextlist;
 use \core_privacy\local\request\approved_contextlist;
 use \core_privacy\local\request\transform;
 use \core_privacy\local\request\writer;
+use \core_privacy\local\request\userlist;
+use \core_privacy\local\request\approved_userlist;
 
 /**
  * Privacy provider for core_role.
@@ -39,7 +44,8 @@ class provider implements
     \core_privacy\local\metadata\provider,
     \core_privacy\local\request\subsystem\provider,
     \core_privacy\local\request\subsystem\plugin_provider,
-    \core_privacy\local\request\user_preference_provider {
+    \core_privacy\local\request\user_preference_provider,
+    \core_privacy\local\request\core_userlist_provider {
 
     /**
      * Get information about the user data stored by this plugin.
@@ -151,6 +157,50 @@ class provider implements
 
         return $contextlist;
     }
+
+    /**
+     * Get the list of users within a specific context.
+     *
+     * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
+     */
+    public static function get_users_in_context(userlist $userlist) {
+
+        if (empty($userlist)) {
+            return;
+        }
+
+        $context = $userlist->get_context();
+
+        // Include users who created or modified role capabilities.
+        $sql = "SELECT modifierid as userid
+                  FROM {role_capabilities}
+                 WHERE contextid = :contextid";
+
+        $params = [
+            'contextid' => $context->id
+        ];
+
+        $userlist->add_from_sql('userid', $sql, $params);
+
+        // Include users that have a role assigned to them.
+        $sql = "SELECT userid
+                  FROM {role_assignments}
+                 WHERE contextid = :contextid";
+
+        $userlist->add_from_sql('userid', $sql, $params);
+
+        // Include users who created or modified the role assignment.
+        // Differentiate and exclude special cases where tool_cohortroles adds records through the
+        // "Assign user roles to cohort" feature into the role_assignments table.
+        // These records should be separately processed in tool_cohortroles.
+        $sql = "SELECT modifierid as userid
+                  FROM {role_assignments}
+                 WHERE contextid = :contextid
+                       AND component != 'tool_cohortroles'";
+
+        $userlist->add_from_sql('userid', $sql, $params);
+    }
+
     /**
      * Export all user data for the specified user, in the specified contexts.
      *
@@ -315,6 +365,32 @@ class provider implements
         // Remove data from role_assignments.
         $DB->delete_records('role_assignments', ['contextid' => $context->id]);
     }
+
+    /**
+     * Delete multiple users within a single context.
+     *
+     * @param approved_userlist $userlist The approved context and user information to delete information for.
+     */
+    public static function delete_data_for_users(approved_userlist $userlist) {
+        global $DB;
+
+        // Don't remove data from role_capabilities.
+        // Because this data affects the whole Moodle, there are override capabilities.
+        // Don't belong to the modifier user.
+        $context = $userlist->get_context();
+        $userids = $userlist->get_userids();
+
+        if (empty($userids)) {
+            return;
+        }
+        list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
+        $params = ['contextid' => $context->id] + $userparams;
+
+        // Remove data from role_assignments.
+        $DB->delete_records_select('role_assignments',
+            "contextid = :contextid AND userid {$usersql}", $params);
+    }
+
     /**
      * Delete all user data for this user only.
      *
@@ -332,10 +408,14 @@ class provider implements
             return;
         }
         $userid = $contextlist->get_user()->id;
-        foreach ($contextlist->get_contexts() as $context) {
-            // Only delete the roles assignments where the user is assigned in all contexts.
-            $DB->delete_records('role_assignments', ['userid' => $userid, 'contextid' => $context->id]);
-        }
+        $contextids = $contextlist->get_contextids();
+
+        list($contextsql, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
+        $params = ['userid' => $userid] + $contextparams;
+
+        // Only delete the roles assignments where the user is assigned in all contexts.
+        $DB->delete_records_select('role_assignments',
+            "userid = :userid AND contextid {$contextsql}", $params);
     }
     /**
      * Delete user entries in role_assignments related to the feature
index bffc919..258b5a7 100644 (file)
@@ -13,6 +13,7 @@
 //
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
 /**
  * Privacy test for core_role
  *
  * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
+
 defined('MOODLE_INTERNAL') || die();
+
 use \core_role\privacy\provider;
 use \core_privacy\local\request\approved_contextlist;
 use \core_privacy\local\request\writer;
 use \core_privacy\tests\provider_testcase;
 use \core_privacy\local\request\transform;
 use \tool_cohortroles\api;
+use \core_privacy\local\request\approved_userlist;
 
 /**
  * Privacy test for core_role
@@ -54,6 +58,7 @@ class core_role_privacy_testcase extends provider_testcase {
         $this->assertEquals(get_string('privacy:metadata:preference:showadvanced', 'core_role'),
             $prefs->definerole_showadvanced->description);
     }
+
     /**
      * Check all contexts are returned if there is any user data for this user.
      */
@@ -231,6 +236,7 @@ class core_role_privacy_testcase extends provider_testcase {
             }
         }
     }
+
     /**
      * Test for provider::delete_data_for_all_users_in_context().
      */
@@ -316,6 +322,7 @@ class core_role_privacy_testcase extends provider_testcase {
         $count = $DB->count_records('role_assignments', ['contextid' => $usercontext2->id]);
         $this->assertEquals(0, $count);
     }
+
     /**
      * Test for provider::delete_data_for_user().
      */
@@ -373,6 +380,7 @@ class core_role_privacy_testcase extends provider_testcase {
         $count = $DB->count_records('role_assignments', ['modifierid' => $user->id]);
         $this->assertEquals(2, $count);
     }
+
     /**
      * Export for a user with a key against a script where no instance is specified.
      */
@@ -426,6 +434,7 @@ class core_role_privacy_testcase extends provider_testcase {
         $exported = $writer->get_related_data($subcontextteacher, 'cohortroles');
         $this->assertEquals($user2->id, reset($exported)->userid);
     }
+
     /**
      * Test for provider::delete_user_role_to_cohort().
      */
@@ -460,6 +469,267 @@ class core_role_privacy_testcase extends provider_testcase {
         $count = $DB->count_records('role_assignments', ['userid' => $user->id, 'component' => 'tool_cohortroles']);
         $this->assertEquals(0, $count);
     }
+
+    /**
+     * Test that only users within a course context are fetched.
+     */
+    public function test_get_users_in_context() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $component = 'core_role';
+
+        $this->setAdminUser();
+        $admin = \core_user::get_user_by_username('admin');
+        // Create user1.
+        $user1 = $this->getDataGenerator()->create_user();
+        $usercontext1 = \context_user::instance($user1->id);
+        // Create user2.
+        $user2 = $this->getDataGenerator()->create_user();
+        $usercontext2 = \context_user::instance($user2->id);
+        // Create course1.
+        $course1 = $this->getDataGenerator()->create_course();
+        $coursecontext1 = \context_course::instance($course1->id);
+        // Create course category.
+        $coursecat = $this->getDataGenerator()->create_category();
+        $coursecatcontext = \context_coursecat::instance($coursecat->id);
+        // Create chat module.
+        $cm = $this->getDataGenerator()->create_module('chat', ['course' => $course1->id]);
+        $cmcontext = \context_module::instance($cm->cmid);
+
+        $systemcontext = \context_system::instance();
+        // Create a block.
+        $block = $this->getDataGenerator()->create_block('online_users');
+        $blockcontext = \context_block::instance($block->id);
+
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+        $managerrole = $DB->get_record('role', array('shortname' => 'manager'), '*', MUST_EXIST);
+
+        // Role assignments CONTEXT_COURSE.
+        role_assign($studentrole->id, $user1->id, $coursecontext1->id);
+        role_assign($studentrole->id, $user2->id, $coursecontext1->id);
+        // Role assignments CONTEXT_COURSECAT.
+        role_assign($studentrole->id, $user2->id, $coursecatcontext->id);
+        // Role assignments CONTEXT_SYSTEM.
+        role_assign($studentrole->id, $user1->id, $systemcontext->id);
+        // Role assignments CONTEXT_MODULE.
+        role_assign($studentrole->id, $user2->id, $cmcontext->id);
+        // Role assigments CONTEXT_BLOCK.
+        role_assign($studentrole->id, $user1->id, $blockcontext->id);
+        // Role assigments CONTEXT_USER.
+        role_assign($managerrole->id, $user1->id, $usercontext2->id);
+
+        // Role capabilities.
+        $this->setUser($user1);
+        assign_capability('moodle/backup:backupcourse', CAP_ALLOW, $studentrole->id, $cmcontext->id);
+
+        // The user list for usercontext1 should not return any users.
+        $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
+        provider::get_users_in_context($userlist1);
+        $this->assertCount(0, $userlist1);
+        // The user list for usercontext2 should user1 and admin (role creator).
+        $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
+        provider::get_users_in_context($userlist2);
+        $this->assertCount(2, $userlist2);
+        $expected = [
+            $user1->id,
+            $admin->id
+        ];
+        $this->assertEquals($expected, $userlist2->get_userids(), '', 0.0, 10, true);
+
+        // The user list for coursecontext1 should user1, user2 and admin (role creator).
+        $userlist3 = new \core_privacy\local\request\userlist($coursecontext1, $component);
+        provider::get_users_in_context($userlist3);
+        $this->assertCount(3, $userlist3);
+        $expected = [
+            $user1->id,
+            $user2->id,
+            $admin->id
+        ];
+        $this->assertEquals($expected, $userlist3->get_userids(), '', 0.0, 10, true);
+
+        // The user list for coursecatcontext should user2 and admin (role creator).
+        $userlist4 = new \core_privacy\local\request\userlist($coursecatcontext, $component);
+        provider::get_users_in_context($userlist4);
+        $this->assertCount(2, $userlist4);
+        $expected = [
+            $user2->id,
+            $admin->id
+        ];
+        $this->assertEquals($expected, $userlist4->get_userids(), '', 0.0, 10, true);
+
+        // The user list for systemcontext should user1 and admin (role creator).
+        $userlist6 = new \core_privacy\local\request\userlist($systemcontext, $component);
+        provider::get_users_in_context($userlist6);
+        $this->assertCount(2, $userlist6);
+        $expected = [
+            $user1->id,
+            $admin->id
+        ];
+        $this->assertEquals($expected, $userlist6->get_userids(), '', 0.0, 10, true);
+
+        // The user list for cmcontext should user1, user2 and admin (role creator).
+        $userlist7 = new \core_privacy\local\request\userlist($cmcontext, $component);
+        provider::get_users_in_context($userlist7);
+        $this->assertCount(3, $userlist7);
+        $expected = [
+            $user1->id,
+            $user2->id,
+            $admin->id
+        ];
+        $this->assertEquals($expected, $userlist7->get_userids(), '', 0.0, 10, true);
+
+        // The user list for blockcontext should user1 and admin (role creator).
+        $userlist8 = new \core_privacy\local\request\userlist($blockcontext, $component);
+        provider::get_users_in_context($userlist8);
+        $this->assertCount(2, $userlist8);
+        $expected = [
+            $user1->id,
+            $admin->id
+        ];
+        $this->assertEquals($expected, $userlist8->get_userids(), '', 0.0, 10, true);
+    }
+
+    /**
+     * Test that data for users in approved userlist is deleted.
+     */
+    public function test_delete_data_for_users() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $component = 'core_role';
+
+        $this->setAdminUser();
+        $admin = \core_user::get_user_by_username('admin');
+        // Create user1.
+        $user1 = $this->getDataGenerator()->create_user();
+        // Create user2.
+        $user2 = $this->getDataGenerator()->create_user();
+        $usercontext2 = \context_user::instance($user2->id);
+        // Create course1.
+        $course1 = $this->getDataGenerator()->create_course();
+        $coursecontext1 = \context_course::instance($course1->id);
+        // Create course category.
+        $coursecat = $this->getDataGenerator()->create_category();
+        $coursecatcontext = \context_coursecat::instance($coursecat->id);
+        // Create chat module.
+        $cm = $this->getDataGenerator()->create_module('chat', ['course' => $course1->id]);
+        $cmcontext = \context_module::instance($cm->cmid);
+
+        $systemcontext = \context_system::instance();
+        // Create a block.
+        $block = $this->getDataGenerator()->create_block('online_users');
+        $blockcontext = \context_block::instance($block->id);
+
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+        $managerrole = $DB->get_record('role', array('shortname' => 'manager'), '*', MUST_EXIST);
+
+        // Role assignments CONTEXT_COURSE.
+        role_assign($studentrole->id, $user1->id, $coursecontext1->id);
+        role_assign($studentrole->id, $user2->id, $coursecontext1->id);
+        // Role assignments CONTEXT_COURSECAT.
+        role_assign($studentrole->id, $user2->id, $coursecatcontext->id);
+        // Role assignments CONTEXT_SYSTEM.
+        role_assign($studentrole->id, $user1->id, $systemcontext->id);
+        // Role assignments CONTEXT_MODULE.
+        role_assign($studentrole->id, $user2->id, $cmcontext->id);
+        // Role assigments CONTEXT_BLOCK.
+        role_assign($studentrole->id, $user1->id, $blockcontext->id);
+        // Role assigments CONTEXT_USER.
+        role_assign($managerrole->id, $user1->id, $usercontext2->id);
+
+        // Role capabilities.
+        $this->setUser($user1);
+        assign_capability('moodle/backup:backupcourse', CAP_ALLOW, $studentrole->id, $cmcontext->id);
+
+        // The user list for usercontext2 should user1 and admin (role creator).
+        $userlist1 = new \core_privacy\local\request\userlist($usercontext2, $component);
+        provider::get_users_in_context($userlist1);
+        $this->assertCount(2, $userlist1);
+        // The user list for coursecontext1 should user1, user2 and admin (role creator).
+        $userlist2 = new \core_privacy\local\request\userlist($coursecontext1, $component);
+        provider::get_users_in_context($userlist2);
+        $this->assertCount(3, $userlist2);
+        // The user list for coursecatcontext should user2 and admin (role creator).
+        $userlist3 = new \core_privacy\local\request\userlist($coursecatcontext, $component);
+        provider::get_users_in_context($userlist3);
+        $this->assertCount(2, $userlist3);
+        // The user list for systemcontext should user1 and admin (role creator).
+        $userlist4 = new \core_privacy\local\request\userlist($systemcontext, $component);
+        provider::get_users_in_context($userlist4);
+        $this->assertCount(2, $userlist4);
+        // The user list for cmcontext should user1, user2 and admin (role creator).
+        $userlist5 = new \core_privacy\local\request\userlist($cmcontext, $component);
+        provider::get_users_in_context($userlist5);
+        $this->assertCount(3, $userlist5);
+        // The user list for blockcontext should user1 and admin (role creator).
+        $userlist6 = new \core_privacy\local\request\userlist($blockcontext, $component);
+        provider::get_users_in_context($userlist6);
+        $this->assertCount(2, $userlist6);
+
+        // Convert $userlist1 into an approved_contextlist.
+        $approvedlist1 = new approved_userlist($usercontext2, $component, $userlist1->get_userids());
+        // Delete using delete_data_for_user.
+        provider::delete_data_for_users($approvedlist1);
+        // Re-fetch users in usercontext2.
+        $userlist1 = new \core_privacy\local\request\userlist($usercontext2, $component);
+        provider::get_users_in_context($userlist1);
+        $this->assertCount(0, $userlist1);
+
+        // Convert $userlist2 into an approved_contextlist.
+        $approvedlist2 = new approved_userlist($coursecontext1, $component, $userlist2->get_userids());
+        // Delete using delete_data_for_user.
+        provider::delete_data_for_users($approvedlist2);
+        // Re-fetch users in coursecontext1.
+        $userlist2 = new \core_privacy\local\request\userlist($coursecontext1, $component);
+        provider::get_users_in_context($userlist2);
+        $this->assertCount(0, $userlist2);
+
+        // Convert $userlist3 into an approved_contextlist.
+        $approvedlist3 = new approved_userlist($coursecatcontext, $component, $userlist3->get_userids());
+        // Delete using delete_data_for_user.
+        provider::delete_data_for_users($approvedlist3);
+        // Re-fetch users in coursecatcontext.
+        $userlist3 = new \core_privacy\local\request\userlist($coursecatcontext, $component);
+        provider::get_users_in_context($userlist3);
+        $this->assertCount(0, $userlist3);
+
+        // Convert $userlist4 into an approved_contextlist.
+        $approvedlist4 = new approved_userlist($systemcontext, $component, $userlist4->get_userids());
+        // Delete using delete_data_for_user.
+        provider::delete_data_for_users($approvedlist4);
+        // Re-fetch users in systemcontext.
+        $userlist4 = new \core_privacy\local\request\userlist($systemcontext, $component);
+        provider::get_users_in_context($userlist4);
+        // The data from role_capabilities should still be present. The user list should return the admin user.
+        $this->assertCount(1, $userlist4);
+        $expected = [$admin->id];
+        $this->assertEquals($expected, $userlist4->get_userids());
+
+        // Convert $userlist5 into an approved_contextlist.
+        $approvedlist5 = new approved_userlist($cmcontext, $component, $userlist5->get_userids());
+        // Delete using delete_data_for_user.
+        provider::delete_data_for_users($approvedlist5);
+        // Re-fetch users in cmcontext.
+        $userlist5 = new \core_privacy\local\request\userlist($cmcontext, $component);
+        provider::get_users_in_context($userlist5);
+        // The data from role_capabilities should still be present. The user list should return user1.
+        $this->assertCount(1, $userlist5);
+        $expected = [$user1->id];
+        $this->assertEquals($expected, $userlist5->get_userids());
+
+        // Convert $userlist6 into an approved_contextlist.
+        $approvedlist6 = new approved_userlist($blockcontext, $component, $userlist6->get_userids());
+        // Delete using delete_data_for_user.
+        provider::delete_data_for_users($approvedlist6);
+        // Re-fetch users in blockcontext.
+        $userlist6 = new \core_privacy\local\request\userlist($blockcontext, $component);
+        provider::get_users_in_context($userlist6);
+        $this->assertCount(0, $userlist6);
+    }
+
     /**
      * Supoort function to get all the localised roles name
      * in a simple array for testing.
index bc38e7c..2cdfca6 100644 (file)
@@ -30,6 +30,8 @@ use core_privacy\local\request\context;
 use core_privacy\local\request\contextlist;
 use core_privacy\local\request\transform;
 use core_privacy\local\request\writer;
+use core_privacy\local\request\userlist;
+use \core_privacy\local\request\approved_userlist;
 
 defined('MOODLE_INTERNAL') || die();
 
@@ -39,7 +41,10 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2018 Zig Tan <zig@moodle.com>
  * @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 {
+class provider implements
+        \core_privacy\local\metadata\provider,
+        \core_privacy\local\request\core_userlist_provider,
+        \core_privacy\local\request\plugin\provider {
 
     /**
      * Returns meta data about this system.
@@ -91,6 +96,35 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
         return $contextlist;
     }
 
+    /**
+     * Get the list of users within a specific context.
+     *
+     * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
+     */
+    public static function get_users_in_context(userlist $userlist) {
+        $context = $userlist->get_context();
+
+        // We should process user data from the system context.
+        // When we process user deletions and expiries, we always delete from the user context.
+        // As a result the cohort role assignments would be deleted, which has a knock-on effect with courses
+        // as roles may change and data may be removed earlier than it should be.
+        if (!$context instanceof \context_system) {
+            return;
+        }
+
+        $params = [
+            'contextid' => $context->id
+        ];
+
+        $sql = "SELECT tc.userid as userid
+                  FROM {tool_cohortroles} tc
+                  JOIN {cohort} c
+                       ON tc.cohortid = c.id
+                 WHERE c.contextid = :contextid";
+
+        $userlist->add_from_sql('userid', $sql, $params);
+    }
+
     /**
      * Export all user data for the specified user, in the specified contexts.
      *
@@ -172,6 +206,25 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
         $DB->delete_records('tool_cohortroles', ['userid' => $userid]);
     }
 
+    /**
+     * Delete multiple users within a single context.
+     *
+     * @param approved_userlist $userlist The approved context and user information to delete information for.
+     */
+    public static function delete_data_for_users(approved_userlist $userlist) {
+        global $DB;
+
+        $context = $userlist->get_context();
+
+        // We should process user data from the system context.
+        // When we process user deletions and expiries, we always delete from the user context.
+        // As a result the cohort role assignments would be deleted, which has a knock-on effect with courses
+        // as roles may change and data may be removed earlier than it should be.
+        if ($context instanceof \context_system) {
+            $DB->delete_records_list('tool_cohortroles', 'userid', $userlist->get_userids());
+        }
+    }
+
     /**
      * Delete all user data for the specified user, in the specified contexts.
      *
index f118b74..26f99d3 100644 (file)
@@ -30,6 +30,7 @@ use \core_privacy\local\request\writer;
 use \core_privacy\local\request\approved_contextlist;
 use \tool_cohortroles\api;
 use \tool_cohortroles\privacy\provider;
+use core_privacy\local\request\approved_userlist;
 
 /**
  * Unit tests for the tool_cohortroles implementation of the privacy API.
@@ -179,16 +180,107 @@ class tool_cohortroles_privacy_testcase extends \core_privacy\tests\provider_tes
         $this->assertCount(0, $cohortroles);
     }
 
+    /**
+     * Test that only users within a course context are fetched.
+     */
+    public function test_get_users_in_context() {
+        $component = 'tool_cohortroles';
+
+        // Create a user.
+        $user = $this->getDataGenerator()->create_user();
+        $usercontext = context_user::instance($user->id);
+
+        $systemcontext = context_system::instance();
+
+        $this->setAdminUser();
+
+        $userlist = new \core_privacy\local\request\userlist($systemcontext, $component);
+        provider::get_users_in_context($userlist);
+        $this->assertCount(0, $userlist);
+
+        $nocohortroles = 3;
+        $this->setup_test_scenario_data($user->id, $nocohortroles);
+
+        // The list of users within the system context should contain user.
+        provider::get_users_in_context($userlist);
+        $this->assertCount(1, $userlist);
+        $this->assertTrue(in_array($user->id, $userlist->get_userids()));
+
+        // The list of users within the user context should be empty.
+        $userlist2 = new \core_privacy\local\request\userlist($usercontext, $component);
+        provider::get_users_in_context($userlist2);
+        $this->assertCount(0, $userlist2);
+    }
+
+    /**
+     * Test that data for users in approved userlist is deleted.
+     */
+    public function test_delete_data_for_users() {
+        $component = 'tool_cohortroles';
+
+        // Create user1.
+        $user1 = $this->getDataGenerator()->create_user();
+        // Create user2.
+        $user2 = $this->getDataGenerator()->create_user();
+        // Create user3.
+        $user3 = $this->getDataGenerator()->create_user();
+        $usercontext3 = context_user::instance($user3->id);
+
+        $systemcontext = context_system::instance();
+
+        $this->setAdminUser();
+
+        $nocohortroles = 3;
+        $this->setup_test_scenario_data($user1->id, $nocohortroles);
+        $this->setup_test_scenario_data($user2->id, $nocohortroles, 'Sausage roll 2',
+                'sausageroll2');
+        $this->setup_test_scenario_data($user3->id, $nocohortroles, 'Sausage roll 3',
+                'sausageroll3');
+
+        $userlist1 = new \core_privacy\local\request\userlist($systemcontext, $component);
+        provider::get_users_in_context($userlist1);
+        $this->assertCount(3, $userlist1);
+        $this->assertTrue(in_array($user1->id, $userlist1->get_userids()));
+        $this->assertTrue(in_array($user2->id, $userlist1->get_userids()));
+        $this->assertTrue(in_array($user3->id, $userlist1->get_userids()));
+
+        // Convert $userlist1 into an approved_contextlist.
+        $approvedlist1 = new approved_userlist($systemcontext, $component, [$user1->id, $user2->id]);
+        // Delete using delete_data_for_user.
+        provider::delete_data_for_users($approvedlist1);
+
+        // Re-fetch users in systemcontext.
+        $userlist1 = new \core_privacy\local\request\userlist($systemcontext, $component);
+        provider::get_users_in_context($userlist1);
+        // The user data of user1 and user2 in systemcontext should be deleted.
+        // The user data of user3 in systemcontext should be still present.
+        $this->assertCount(1, $userlist1);
+        $this->assertTrue(in_array($user3->id, $userlist1->get_userids()));
+
+        // Convert $userlist1 into an approved_contextlist in the user context.
+        $approvedlist2 = new approved_userlist($usercontext3, $component, $userlist1->get_userids());
+        // Delete using delete_data_for_user.
+        provider::delete_data_for_users($approvedlist2);
+        // Re-fetch users in systemcontext.
+        $userlist1 = new \core_privacy\local\request\userlist($systemcontext, $component);
+        provider::get_users_in_context($userlist1);
+        // The user data in systemcontext should not be deleted.
+        $this->assertCount(1, $userlist1);
+    }
+
     /**
      * Helper function to setup tool_cohortroles records for testing a specific user.
      *
      * @param int $userid           The ID of the user used for testing.
      * @param int $nocohortroles    The number of tool_cohortroles to create for the user.
+     * @param string $rolename      The name of the role to be created.
+     * @param string $roleshortname The short name of the role to be created.
      * @throws \core_competency\invalid_persistent_exception
      * @throws coding_exception
      */
-    protected function setup_test_scenario_data($userid, $nocohortroles) {
-        $roleid = create_role('Sausage Roll', 'sausageroll', 'mmmm');
+    protected function setup_test_scenario_data($userid, $nocohortroles, $rolename = 'Sausage Roll',
+                                                $roleshortname = 'sausageroll') {
+        $roleid = create_role($rolename, $roleshortname, 'mmmm');
 
         for ($c = 0; $c < $nocohortroles; $c++) {
             $cohort = $this->getDataGenerator()->create_cohort();
index e73a34c..10889a9 100644 (file)
@@ -227,25 +227,24 @@ class api {
      * @param int $foruser The user whom the request is being made for.
      * @param int $type The request type.
      * @param string $comments Request comments.
+     * @param int $creationmethod The creation method of the data request.
      * @return data_request
      * @throws invalid_persistent_exception
      * @throws coding_exception
      */
-    public static function create_data_request($foruser, $type, $comments = '') {
-        global $USER;
+    public static function create_data_request($foruser, $type, $comments = '',
+                                               $creationmethod = data_request::DATAREQUEST_CREATION_MANUAL) {
+        global $USER, $ADMIN;
 
         $datarequest = new data_request();
         // The user the request is being made for.
         $datarequest->set('userid', $foruser);
 
-        $requestinguser = $USER->id;
-        // Check when the user is making a request on behalf of another.
-        if ($requestinguser != $foruser) {
-            if (self::is_site_dpo($requestinguser)) {
-                // The user making the request is a DPO. Should be fine.
-                $datarequest->set('dpo', $requestinguser);
-            }
-        }
+        // The cron is considered to be a guest user when it creates a data request.
+        // NOTE: This should probably be changed. We should leave the default value for $requestinguser if
+        // the request is not explicitly created by a specific user.
+        $requestinguser = (isguestuser() && $creationmethod == data_request::DATAREQUEST_CREATION_AUTO) ?
+                $ADMIN->id : $USER->id;
         // The user making the request.
         $datarequest->set('requestedby', $requestinguser);
         // Set status.
index 92c8c1f..9976422 100644 (file)
@@ -37,6 +37,12 @@ class data_request extends persistent {
     /** The table name this persistent object maps to. */
     const TABLE = 'tool_dataprivacy_request';
 
+    /** Data request created manually. */
+    const DATAREQUEST_CREATION_MANUAL = 0;
+
+    /** Data request created automatically. */
+    const DATAREQUEST_CREATION_AUTO = 1;
+
     /**
      * Return the definition of the properties of this model.
      *
@@ -111,6 +117,14 @@ class data_request extends persistent {
                 'type' => PARAM_INT,
                 'default' => FORMAT_PLAIN
             ],
+            'creationmethod' => [
+                'default' => self::DATAREQUEST_CREATION_MANUAL,
+                'choices' => [
+                    self::DATAREQUEST_CREATION_MANUAL,
+                    self::DATAREQUEST_CREATION_AUTO
+                ],
+                'type' => PARAM_INT
+            ],
         ];
     }
 
index 96c9e55..3d25904 100644 (file)
@@ -77,14 +77,21 @@ class metadata_registry {
                     $internaldata['external'] = true;
                 }
 
-                // Check if the interface is deprecated.
+                // Additional interface checks.
                 if (!$manager->is_empty_subsystem($component)) {
                     $classname = $manager->get_provider_classname_for_component($component);
                     if (class_exists($classname)) {
                         $componentclass = new $classname();
+                        // Check if the interface is deprecated.
                         if ($componentclass instanceof \core_privacy\local\deprecated) {
                             $internaldata['deprecated'] = true;
                         }
+
+                        // Check that the userlist provider is implemented.
+                        if ($componentclass instanceof \core_privacy\local\request\core_user_data_provider
+                                && !$componentclass instanceof \core_privacy\local\request\core_userlist_provider) {
+                            $internaldata['userlistnoncompliance'] = true;
+                        }
                     }
                 }
 
diff --git a/admin/tool/dataprivacy/classes/task/delete_existing_deleted_users.php b/admin/tool/dataprivacy/classes/task/delete_existing_deleted_users.php
new file mode 100644 (file)
index 0000000..a078747
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Scheduled task to create delete data request for pre-existing deleted users.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Mihail Geshoski
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_dataprivacy\task;
+
+use core\task\scheduled_task;
+use tool_dataprivacy\api;
+use tool_dataprivacy\data_request;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
+
+/**
+ * Scheduled task to create delete data request for pre-existing deleted users.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Mihail Geshoski
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class delete_existing_deleted_users extends scheduled_task {
+
+    /**
+     * Returns the task name.
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('deleteexistingdeleteduserstask', 'tool_dataprivacy');
+    }
+
+    /**
+     * Run the task to delete expired data request files and update request statuses.
+     *
+     */
+    public function execute() {
+        global $DB;
+
+        // Select all deleted users that do not have any delete data requests created for them.
+        $sql = "SELECT DISTINCT(u.id)
+                  FROM {user} u
+             LEFT JOIN {tool_dataprivacy_request} r
+                       ON u.id = r.userid
+                 WHERE u.deleted = ?
+                       AND (r.id IS NULL
+                           OR r.type != ?)";
+
+        $params = [
+            1,
+            api::DATAREQUEST_TYPE_DELETE
+        ];
+
+        $deletedusers = $DB->get_records_sql($sql, $params);
+        $createdrequests = 0;
+
+        foreach ($deletedusers as $user) {
+            api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE,
+                    get_string('datarequestcreatedfromscheduledtask', 'tool_dataprivacy'),
+                    data_request::DATAREQUEST_CREATION_AUTO);
+            $createdrequests++;
+        }
+
+        if ($createdrequests > 0) {
+            mtrace($createdrequests . ' delete data request(s) created for existing deleted users');
+        }
+    }
+}
index 70402f4..d35f917 100644 (file)
@@ -70,27 +70,6 @@ class initiate_data_request_task extends adhoc_task {
             return;
         }
 
-        $requestedby = $datarequest->get('requestedby');
-        $valid = true;
-        $comment = '';
-        $foruser = $datarequest->get('userid');
-        if ($foruser != $requestedby) {
-            if (!$valid = api::can_create_data_request_for_user($foruser, $requestedby)) {
-                $params = (object)[
-                    'requestedby' => $requestedby,
-                    'userid' => $foruser
-                ];
-                $comment = get_string('errornocapabilitytorequestforothers', 'tool_dataprivacy', $params);
-                mtrace($comment);
-            }
-        }
-        // Reject the request outright if it's invalid.
-        if (!$valid) {
-            $dpo = $datarequest->get('dpo');
-            api::update_request_status($requestid, api::DATAREQUEST_STATUS_REJECTED, $dpo, $comment);
-            return;
-        }
-
         // Update the status of this request as pre-processing.
         mtrace('Generating the contexts containing personal data for the user...');
         api::update_request_status($requestid, api::DATAREQUEST_STATUS_PREPROCESSING);
index c44b661..802f61a 100644 (file)
@@ -76,7 +76,6 @@ class process_data_request_task extends adhoc_task {
 
         // Get the user details now. We might not be able to retrieve it later if it's a deletion processing.
         $foruser = core_user::get_user($request->userid);
-        $usercontext = \context_user::instance($foruser->id);
 
         // Update the status of this request as pre-processing.
         mtrace('Processing request...');
@@ -84,6 +83,14 @@ class process_data_request_task extends adhoc_task {
         $completestatus = api::DATAREQUEST_STATUS_COMPLETE;
 
         if ($request->type == api::DATAREQUEST_TYPE_EXPORT) {
+            // Get the user context.
+            $usercontext = \context_user::instance($foruser->id, IGNORE_MISSING);
+            if (!$usercontext) {
+                mtrace("Request {$requestid} cannot be processed due to a missing user context instance for the user
+                    with ID {$foruser->id}. Skipping...");
+                return;
+            }
+
             // Get the collection of approved_contextlist objects needed for core_privacy data export.
             $approvedclcollection = api::get_approved_contextlist_collection_for_request($requestpersistent);
 
@@ -191,12 +198,19 @@ class process_data_request_task extends adhoc_task {
 
         // Send message to the user involved.
         if ($notifyuser) {
+            $messagesent = false;
             if ($emailonly) {
-                email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
+                // Do not sent an email if the user has been deleted. The user email has been previously deleted.
+                if (!$foruser->deleted) {
+                    $messagesent = email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
+                }
             } else {
-                message_send($message);
+                $messagesent = message_send($message);
+            }
+
+            if ($messagesent) {
+                mtrace('Message sent to user: ' . $messagetextdata['username']);
             }
-            mtrace('Message sent to user: ' . $messagetextdata['username']);
         }
 
         // Send to requester as well in some circumstances.
index e340269..2d4a01a 100644 (file)
@@ -19,6 +19,7 @@
         <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user who created/modified this request object"/>
         <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time this data request was created"/>
         <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The last time this data request was updated"/>
+        <FIELD NAME="creationmethod" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The type of the creation method of the data request"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index 5ee3a19..e3bedd6 100644 (file)
@@ -50,5 +50,13 @@ $tasks = array(
         'day' => '*',
         'dayofweek' => '*',
         'month' => '*'
+    ), array(
+        'classname' => 'tool_dataprivacy\task\delete_existing_deleted_users',
+        'blocking' => 0,
+        'minute' => 'R',
+        'hour' => 'R',
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
     ),
 );
index 17d38b0..5449638 100644 (file)
@@ -248,5 +248,17 @@ function xmldb_tool_dataprivacy_upgrade($oldversion) {
         upgrade_plugin_savepoint(true, 2018100401, 'tool', 'dataprivacy');
     }
 
+    if ($oldversion < 2018100406) {
+        // Define field sensitivedatareasons to be added to tool_dataprivacy_purpose.
+        $table = new xmldb_table('tool_dataprivacy_request');
+        $field = new xmldb_field('creationmethod', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, 0, 'timemodified');
+        // Conditionally launch add field sensitivedatareasons.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+        // Dataprivacy savepoint reached.
+        upgrade_plugin_savepoint(true, 2018100406, 'tool', 'dataprivacy');
+    }
+
     return true;
 }
index 0c5ac5f..84ca013 100644 (file)
@@ -80,6 +80,7 @@ $string['dataregistryinfo'] = 'The data registry enables categories (types of da
 $string['dataretentionexplanation'] = 'This summary shows the default categories and purposes for retaining user data. Certain areas may have more specific categories and purposes than those listed here.';
 $string['dataretentionsummary'] = 'Data retention summary';
 $string['datarequestcreatedforuser'] = 'Data request created for {$a}';
+$string['datarequestcreatedfromscheduledtask'] = 'Automatically created from a scheduled task (pre-existing deleted user).';
 $string['datarequestemailsubject'] = 'Data request: {$a}';
 $string['datarequests'] = 'Data requests';
 $string['datecomment'] = '[{$a->date}]: ' . PHP_EOL . ' {$a->comment}';
@@ -93,6 +94,7 @@ $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 expired data request export files';
+$string['deleteexistingdeleteduserstask'] = 'Create delete data request for pre-existing deleted users';
 $string['deletemyaccount'] = 'Delete my account';
 $string['deletepurpose'] = 'Delete purpose';
 $string['deletepurposetext'] = 'Are you sure you want to delete the purpose \'{$a}\'?';
@@ -277,6 +279,8 @@ $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>';
 $string['setdefaults'] = 'Set defaults';
+$string['showdataretentionsummary'] = 'Show data retention summary';
+$string['showdataretentionsummary_desc'] = 'If enabled, a link to the data retention summary is shown in the page footer and in the user profile page.';
 $string['statusapproved'] = 'Approved';
 $string['statusawaitingapproval'] = 'Awaiting approval';
 $string['statuscancelled'] = 'Cancelled';
@@ -293,6 +297,8 @@ $string['subjectscope'] = 'Subject scope';
 $string['subjectscope_help'] = 'The subject scope lists the roles which may be assigned in this context.';
 $string['summary'] = 'Registry configuration summary';
 $string['user'] = 'User';
+$string['userlistnoncompliant'] = 'Userlist provider missing';
+$string['userlistexplanation'] = 'This plugin has the base provider but should also implement the userlist provider for full support of privacy functionality.';
 $string['viewrequest'] = 'View the request';
 $string['visible'] = 'Expand all';
 $string['unexpiredrolewithretention'] = '{$a->retention} (Unexpired)';
index 276850e..8655f1c 100644 (file)
@@ -87,10 +87,19 @@ function tool_dataprivacy_myprofile_navigation(tree $tree, $user, $iscurrentuser
         }
     }
 
-    $summaryurl = new moodle_url('/admin/tool/dataprivacy/summary.php');
-    $summarynode = new core_user\output\myprofile\node('privacyandpolicies', 'retentionsummary',
+    // A returned 0 means that the setting was set and disabled, false means that there is no value for the provided setting.
+    $showsummary = get_config('tool_dataprivacy', 'showdataretentionsummary');
+    if ($showsummary === false) {
+        // This means that no value is stored in db. We use the default value in this case.
+        $showsummary = true;
+    }
+
+    if ($showsummary) {
+        $summaryurl = new moodle_url('/admin/tool/dataprivacy/summary.php');
+        $summarynode = new core_user\output\myprofile\node('privacyandpolicies', 'retentionsummary',
             get_string('dataretentionsummary', 'tool_dataprivacy'), null, $summaryurl);
-    $category->add_node($summarynode);
+        $category->add_node($summarynode);
+    }
 
     // Add the Privacy category to the tree if it's not empty and it doesn't exist.
     $nodes = $category->nodes;
@@ -110,11 +119,20 @@ function tool_dataprivacy_myprofile_navigation(tree $tree, $user, $iscurrentuser
  * @return string HTML footer content
  */
 function tool_dataprivacy_standard_footer_html() {
+    $output = '';
 
-    $url = new moodle_url('/admin/tool/dataprivacy/summary.php');
-    $output = html_writer::link($url, get_string('dataretentionsummary', 'tool_dataprivacy'));
-    $output = html_writer::div($output, 'summaryfooter');
+    // A returned 0 means that the setting was set and disabled, false means that there is no value for the provided setting.
+    $showsummary = get_config('tool_dataprivacy', 'showdataretentionsummary');
+    if ($showsummary === false) {
+        // This means that no value is stored in db. We use the default value in this case.
+        $showsummary = true;
+    }
 
+    if ($showsummary) {
+        $url = new moodle_url('/admin/tool/dataprivacy/summary.php');
+        $output = html_writer::link($url, get_string('dataretentionsummary', 'tool_dataprivacy'));
+        $output = html_writer::div($output, 'tool_dataprivacy');
+    }
     return $output;
 }
 
index d3abf47..a434384 100644 (file)
@@ -66,6 +66,12 @@ if ($hassiteconfig) {
                 new lang_string('requireallenddatesforuserdeletion', 'tool_dataprivacy'),
                 new lang_string('requireallenddatesforuserdeletion_desc', 'tool_dataprivacy'),
                 1));
+
+        // Whether the data retention summary should be shown in the page footer and in the user profile page.
+        $privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/showdataretentionsummary',
+            new lang_string('showdataretentionsummary', 'tool_dataprivacy'),
+            new lang_string('showdataretentionsummary_desc', 'tool_dataprivacy'),
+            1));
     }
 }
 
index 3f7b691..e55a30b 100644 (file)
@@ -64,6 +64,9 @@
         {{#deprecated}}
             <span class="badge badge-pill badge-warning">{{#str}}deprecated, tool_dataprivacy{{/str}}</span>
         {{/deprecated}}
+        {{#userlistnoncompliance}}
+            <span class="badge badge-pill badge-warning">{{#str}}userlistnoncompliant, tool_dataprivacy{{/str}}</span>
+        {{/userlistnoncompliance}}
     </div>
 
     {{#compliant}}
index c4a9c1b..bbca4ff 100644 (file)
@@ -47,6 +47,8 @@
         <dd>{{#str}}externalexplanation, tool_dataprivacy{{/str}}</dd>
         <dt><span class="badge badge-pill badge-warning">{{#str}}deprecated, tool_dataprivacy{{/str}}</span></dt>
         <dd>{{#str}}deprecatedexplanation, tool_dataprivacy{{/str}}</dd>
+        <dt><span class="badge badge-pill badge-warning">{{#str}}userlistnoncompliant, tool_dataprivacy{{/str}}</span></dt>
+        <dd>{{#str}}userlistexplanation, tool_dataprivacy{{/str}}</dd>
     </dl>
     <hr />
     <div class="clearfix"><a class="tool_dataprivacy-expand-all pull-right" href="#" data-visibility-state='visible'>{{#str}}visible, tool_dataprivacy{{/str}}</a></div>
index f0617d9..ec6c171 100644 (file)
@@ -608,7 +608,6 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
         $this->assertEquals($user->id, $datarequest->get('userid'));
         $this->assertEquals($USER->id, $datarequest->get('requestedby'));
-        $this->assertEquals($USER->id, $datarequest->get('dpo'));
         $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $datarequest->get('type'));
         $this->assertEquals(api::DATAREQUEST_STATUS_PENDING, $datarequest->get('status'));
         $this->assertEquals($comment, $datarequest->get('comments'));
diff --git a/admin/tool/dataprivacy/tests/task_test.php b/admin/tool/dataprivacy/tests/task_test.php
new file mode 100644 (file)
index 0000000..e6be1b8
--- /dev/null
@@ -0,0 +1,189 @@
+<?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/>.
+
+/**
+ * Tests for scheduled tasks.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Mihail Geshoski <mihail@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once('data_privacy_testcase.php');
+
+use tool_dataprivacy\api;
+
+/**
+ * Tests for scheduled tasks.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Mihail Geshoski <mihail@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_dataprivacy_task_testcase extends data_privacy_testcase {
+
+    /**
+     * Test tearDown.
+     */
+    public function tearDown() {
+        \core_privacy\local\request\writer::reset();
+    }
+
+    /**
+     * Ensure that a delete data request for pre-existing deleted users
+     * is created when there are not any existing data requests
+     * for that particular user.
+     */
+    public function test_delete_existing_deleted_users_task_no_previous_requests() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+        // Create a user.
+        $user = $this->getDataGenerator()->create_user();
+        // Mark the user as deleted.
+        $user->deleted = 1;
+        $DB->update_record('user', $user);
+
+        // The user should not have a delete data request.
+        $this->assertCount(0, api::get_data_requests($user->id, [],
+                [api::DATAREQUEST_TYPE_DELETE]));
+
+        $this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
+        // After running the scheduled task, the deleted user should have a delete data request.
+        $this->assertCount(1, api::get_data_requests($user->id, [],
+                [api::DATAREQUEST_TYPE_DELETE]));
+    }
+
+    /**
+     * Ensure that a delete data request for pre-existing deleted users
+     * is created when there are existing non-delete data requests
+     * for that particular user.
+     */
+    public function test_delete_existing_deleted_users_task_existing_export_data_requests() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+        // Create a user.
+        $user = $this->getDataGenerator()->create_user();
+        // Create export data request for the user.
+        api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT);
+        // Mark the user as deleted.
+        $user->deleted = 1;
+        $DB->update_record('user', $user);
+
+        // The user should have a export data request.
+        $this->assertCount(1, api::get_data_requests($user->id, [],
+                [api::DATAREQUEST_TYPE_EXPORT]));
+        // The user should not have a delete data request.
+        $this->assertCount(0, api::get_data_requests($user->id, [],
+                [api::DATAREQUEST_TYPE_DELETE]));
+
+        $this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
+        // After running the scheduled task, the deleted user should have a delete data request.
+        $this->assertCount(1, api::get_data_requests($user->id, [],
+                [api::DATAREQUEST_TYPE_DELETE]));
+    }
+
+    /**
+     * Ensure that a delete data request for pre-existing deleted users
+     * is not created when there are existing ongoing delete data requests
+     * for that particular user.
+     */
+    public function test_delete_existing_deleted_users_task_existing_ongoing_delete_data_requests() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+        // Create a user.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+        // Create delete data request for the user.
+        $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
+        $requestid = $datarequest->get('id');
+        api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+
+        // The user should have an ongoing delete data request.
+        $this->assertCount(1, api::get_data_requests($user->id,
+                [api::DATAREQUEST_STATUS_AWAITING_APPROVAL], [api::DATAREQUEST_TYPE_DELETE]));
+
+        $this->setAdminUser();
+        // Delete the user.
+        delete_user($user);
+        // The user should still have the existing ongoing delete data request.
+        $this->assertCount(1, \tool_dataprivacy\api::get_data_requests($user->id,
+                [api::DATAREQUEST_STATUS_AWAITING_APPROVAL], [api::DATAREQUEST_TYPE_DELETE]));
+
+        $this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
+        // After running the scheduled task, the user should have only one delete data request.
+        $this->assertCount(1, api::get_data_requests($user->id, [],
+                [api::DATAREQUEST_TYPE_DELETE]));
+        // The user should not have a newly created delete data request.
+        $this->assertCount(0, api::get_data_requests($user->id,
+                [api::DATAREQUEST_STATUS_PENDING], [api::DATAREQUEST_TYPE_DELETE]));
+    }
+
+    /**
+     * Ensure that a delete data request for pre-existing deleted users
+     * is not created when there are existing finished delete data requests
+     * for that particular user.
+     */
+    public function test_delete_existing_deleted_users_task_existing_finished_delete_data_requests() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+        // Create a user.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+        // Create delete data request for the user.
+        $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
+        $requestid = $datarequest->get('id');
+        api::update_request_status($requestid, api::DATAREQUEST_STATUS_CANCELLED);
+
+        // The user should have a delete data request.
+        $this->assertCount(1, api::get_data_requests($user->id, [],
+                [api::DATAREQUEST_TYPE_DELETE]));
+        // The user should not have an ongoing data requests.
+        $this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
+
+        $this->setAdminUser();
+        // Delete the user.
+        delete_user($user);
+        // The user should still have the existing finished delete data request.
+        $this->assertCount(1, \tool_dataprivacy\api::get_data_requests($user->id,
+                [api::DATAREQUEST_STATUS_CANCELLED], [api::DATAREQUEST_TYPE_DELETE]));
+
+        $this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
+        // After running the scheduled task, the user should still have one delete data requests.
+        $this->assertCount(1, api::get_data_requests($user->id, [],
+                [api::DATAREQUEST_TYPE_DELETE]));
+        // The user should still have the existing finished delete data request.
+        $this->assertCount(1, \tool_dataprivacy\api::get_data_requests($user->id,
+                [api::DATAREQUEST_STATUS_CANCELLED], [api::DATAREQUEST_TYPE_DELETE]));
+    }
+
+    /**
+     * Helper to execute a particular task.
+     *
+     * @param string $task The task.
+     */
+    private function execute_task($task) {
+        // Run the scheduled task.
+        ob_start();
+        $task = \core\task\manager::get_scheduled_task($task);
+        $task->execute();
+        ob_end_clean();
+    }
+}
index 3efc437..2658ecb 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2018100403;
+$plugin->version   = 2018100406;
 $plugin->requires  = 2018050800;        // Moodle 3.5dev (Build 2018031600) and upwards.
 $plugin->component = 'tool_dataprivacy';
diff --git a/admin/tool/log/classes/local/privacy/logstore_userlist_provider.php b/admin/tool/log/classes/local/privacy/logstore_userlist_provider.php
new file mode 100644 (file)
index 0000000..44ace57
--- /dev/null
@@ -0,0 +1,57 @@
+<?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/>.
+
+/**
+ * Logstore userlist provider interface.
+ *
+ * @package    tool_log
+ * @copyright  2018 Adrian Greeve
+ * @author     Adrian Greeve <adriangreeve.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_log\local\privacy;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Logstore userlist provider interface.
+ *
+ * Logstore subplugins providers must implement this interface.
+ *
+ * @package    tool_log
+ * @copyright  2018 Adrian Greeve
+ * @author     Adrian Greeve <adriangreeve.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface logstore_userlist_provider extends \core_privacy\local\request\plugin\subplugin_provider {
+
+    /**
+     * Add user IDs that contain user information for the specified context.
+     *
+     * @param \core_privacy\local\request\userlist $userlist The userlist to add the users to.
+     * @return void
+     */
+    public static function add_userids_for_context(\core_privacy\local\request\userlist $userlist);
+
+
+    /**
+     * Delete all data for a list of users in the specified context.
+     *
+     * @param \core_privacy\local\request\approved_userlist $userlist The specific context and users to delete data for.
+     * @return void
+     */
+    public static function delete_data_for_userlist(\core_privacy\local\request\approved_userlist $userlist);
+}
index da973ea..27a3ee5 100644 (file)
@@ -120,4 +120,19 @@ trait moodle_database_export_and_delete {
         $db->delete_records_select($table, "userid = :userid AND contextid $insql", $params);
     }
 
+    /**
+     * Delete all user data for the specified users, in the specified context.
+     *
+     * @param \core_privacy\local\request\approved_userlist $contextlist The approved contexts and user information to delete information for.
+     */
+    public static function delete_data_for_userlist(\core_privacy\local\request\approved_userlist $userlist) {
+        list($db, $table) = static::get_database_and_table();
+        if (!$db || !$table) {
+            return;
+        }
+        list($insql, $inparams) = $db->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
+        $params = array_merge($inparams, ['contextid' => $userlist->get_context()->id]);
+        $db->delete_records_select($table, "contextid = :contextid AND userid $insql", $params);
+    }
+
 }
index 252ba50..9968e44 100644 (file)
@@ -43,7 +43,8 @@ use tool_log\log\manager;
  */
 class provider implements
     \core_privacy\local\metadata\provider,
-    \core_privacy\local\request\subsystem\provider {
+    \core_privacy\local\request\subsystem\provider,
+    \core_privacy\local\request\core_userlist_provider {
 
     /**
      * Returns metadata.
@@ -68,6 +69,17 @@ class provider implements
         return $contextlist;
     }
 
+    /**
+     * Get the list of contexts that contain user information for the specified user.
+     *
+     * @param   \core_privacy\local\request\userlist    $userlist   The userlist containing the list of users who have data in
+     * this context/plugin combination.
+     */
+    public static function get_users_in_context(\core_privacy\local\request\userlist $userlist) {
+        $interface = \tool_log\local\privacy\logstore_userlist_provider::class;
+        static::call_subplugins_method_with_args('add_userids_for_context', [$userlist], $interface);
+    }
+
     /**
      * Export all user data for the specified user, in the specified contexts.
      *
@@ -97,16 +109,29 @@ class provider implements
         static::call_subplugins_method_with_args('delete_data_for_user', [$contextlist]);
     }
 
+    /**
+     * Delete multiple users within a single context.
+     *
+     * @param \core_privacy\local\request\approved_userlist $userlist The approved context and user information to delete
+     * information for.
+     */
+    public static function delete_data_for_users(\core_privacy\local\request\approved_userlist $userlist) {
+        $interface = \tool_log\local\privacy\logstore_userlist_provider::class;
+        static::call_subplugins_method_with_args('delete_data_for_userlist', [$userlist], $interface);
+    }
+
     /**
      * Invoke the subplugins method with arguments.
      *
      * @param string $method The method name.
      * @param array $args The arguments.
+     * @param string $interface The interface to use. By default uses the logstore_provider.
      * @return void
      */
-    protected static function call_subplugins_method_with_args($method, array $args = []) {
-        $interface = \tool_log\local\privacy\logstore_provider::class;
+    protected static function call_subplugins_method_with_args($method, array $args = [], string $interface = null) {
+        if (!isset($interface)) {
+            $interface = \tool_log\local\privacy\logstore_provider::class;
+        }
         \core_privacy\manager::plugintype_class_callback('logstore', $interface, $method, $args);
     }
-
 }
index b5e8eac..c9b31af 100644 (file)
@@ -40,7 +40,8 @@ use core_privacy\local\request\contextlist;
  */
 class provider implements
     \core_privacy\local\metadata\provider,
-    \tool_log\local\privacy\logstore_provider {
+    \tool_log\local\privacy\logstore_provider,
+    \tool_log\local\privacy\logstore_userlist_provider {
 
     use \tool_log\local\privacy\moodle_database_export_and_delete;
 
@@ -91,6 +92,38 @@ class provider implements
         $contextlist->add_from_sql($sql, []);
     }
 
+    /**
+     * Add user IDs that contain user information for the specified context.
+     *
+     * @param \core_privacy\local\request\userlist $userlist The userlist to add the users to.
+     * @return void
+     */
+    public static function add_userids_for_context(\core_privacy\local\request\userlist $userlist) {
+        list($db, $table) = static::get_database_and_table();
+        if (!$db || !$table) {
+            return;
+        }
+
+        $userids = [];
+        $records = $db->get_records($table, ['contextid' => $userlist->get_context()->id], '',
+                'id, userid, relateduserid, realuserid');
+        if (empty($records)) {
+            return;
+        }
+
+        foreach ($records as $record) {
+            $userids[] = $record->userid;
+            if (!empty($record->relateduserid)) {
+                $userids[] = $record->relateduserid;
+            }
+            if (!empty($record->realuserid)) {
+                $userids[] = $record->realuserid;
+            }
+        }
+        $userids = array_unique($userids);
+        $userlist->add_users($userids);
+    }
+
     /**
      * Get the database object.
      *
index 4a1e37e..085ce1b 100644 (file)
@@ -151,6 +151,55 @@ class logstore_database_privacy_testcase extends provider_testcase {
         $this->assert_contextlist_equals($this->get_contextlist_for_user($admin), [$sysctx, $c1ctx, $c2ctx]);
     }
 
+    /**
+     * Check that user IDs are returned for a given context.
+     */
+    public function test_add_userids_for_context() {
+        $admin = \core_user::get_user(2);
+        $u1 = $this->getDataGenerator()->create_user();
+        $u2 = $this->getDataGenerator()->create_user();
+        $u3 = $this->getDataGenerator()->create_user();
+        $u4 = $this->getDataGenerator()->create_user();
+
+        $c1 = $this->getDataGenerator()->create_course();
+
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($c1->id);
+
+        $this->enable_logging();
+        $manager = get_log_manager(true);
+
+        $userlist = new \core_privacy\local\request\userlist($sysctx, 'logstore_database');
+        $userids = $userlist->get_userids();
+        $this->assertEmpty($userids);
+        provider::add_userids_for_context($userlist);
+        $userids = $userlist->get_userids();
+        $this->assertEmpty($userids);
+        // User one should be added (userid).
+        $this->setUser($u1);
+        $e = \logstore_database\event\unittest_executed::create(['context' => $sysctx]);
+        $e->trigger();
+        // User two (userids) and three (relateduserid) should be added.
+        $this->setUser($u2);
+        $e = \logstore_database\event\unittest_executed::create(['context' => $sysctx, 'relateduserid' => $u3->id]);
+        $e->trigger();
+        // The admin user should be added (realuserid).
+        $this->setAdminUser();
+        \core\session\manager::loginas($u2->id, context_system::instance());
+        $e = \logstore_database\event\unittest_executed::create(['context' => $sysctx]);
+        $e->trigger();
+        // Set off an event in a different context. User 4 should not be returned below.
+        $this->setUser($u4);
+        $e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx]);
+        $e->trigger();
+
+        provider::add_userids_for_context($userlist);
+        $userids = $userlist->get_userids();
+        $this->assertCount(4, $userids);
+        $expectedresult = [$admin->id, $u1->id, $u2->id, $u3->id];
+        $this->assertEmpty(array_diff($expectedresult, $userids));
+    }
+
     public function test_delete_data_for_user() {
         global $DB;
         $u1 = $this->getDataGenerator()->create_user();
@@ -233,6 +282,51 @@ class logstore_database_privacy_testcase extends provider_testcase {
         $this->assertEquals(1, $DB->count_records('logstore_standard_log', ['userid' => $u2->id]));
     }
 
+    /**
+     * Check that data is removed for the listed users in a given context.
+     */
+    public function test_delete_data_for_userlist() {
+        global $DB;
+
+        $u1 = $this->getDataGenerator()->create_user();
+        $u2 = $this->getDataGenerator()->create_user();
+        $u3 = $this->getDataGenerator()->create_user();
+        $u4 = $this->getDataGenerator()->create_user();
+
+        $course = $this->getDataGenerator()->create_course();
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($course->id);
+
+        $this->enable_logging();
+        $manager = get_log_manager(true);
+
+        $this->setUser($u1);
+        $e = \logstore_database\event\unittest_executed::create(['context' => $sysctx]);
+        $e->trigger();
+        $this->setUser($u2);
+        $e = \logstore_database\event\unittest_executed::create(['context' => $sysctx]);
+        $e->trigger();
+        $this->setUser($u3);
+        $e = \logstore_database\event\unittest_executed::create(['context' => $sysctx]);
+        $e->trigger();
+        $this->setUser($u4);
+        $e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx]);
+        $e->trigger();
+
+        // Check that four records were created.
+        $this->assertEquals(4, $DB->count_records('logstore_standard_log'));
+
+        $userlist = new \core_privacy\local\request\approved_userlist($sysctx, 'logstore_database', [$u1->id, $u3->id]);
+        provider::delete_data_for_userlist($userlist);
+        // We should have a record for u2 and u4.
+        $this->assertEquals(2, $DB->count_records('logstore_standard_log'));
+
+        $records = $DB->get_records('logstore_standard_log', ['contextid' => $sysctx->id]);
+        $this->assertCount(1, $records);
+        $currentrecord = array_shift($records);
+        $this->assertEquals($u2->id, $currentrecord->userid);
+    }
+
     public function test_export_data_for_user() {
         $admin = \core_user::get_user(2);
         $u1 = $this->getDataGenerator()->create_user();
index 298d821..271f7ac 100644 (file)
@@ -44,7 +44,8 @@ use tool_log\local\privacy\helper;
  */
 class provider implements
     \core_privacy\local\metadata\provider,
-    \tool_log\local\privacy\logstore_provider {
+    \tool_log\local\privacy\logstore_provider,
+    \tool_log\local\privacy\logstore_userlist_provider {
 
     /**
      * Returns metadata.
@@ -89,6 +90,22 @@ class provider implements
         $contextlist->add_from_sql($sql, $params);
     }
 
+    /**
+     * Add user IDs that contain user information for the specified context.
+     *
+     * @param \core_privacy\local\request\userlist $userlist The userlist to add the users to.
+     * @return void
+     */
+    public static function add_userids_for_context(\core_privacy\local\request\userlist $userlist) {
+        $context = $userlist->get_context();
+        list($insql, $params) = static::get_sql_where_from_contexts([$context]);
+
+        $sql = "SELECT l.userid
+                  FROM {log} l
+                 WHERE $insql";
+        $userlist->add_from_sql('userid', $sql, $params);
+    }
+
     /**
      * Export all user data for the specified user, in the specified contexts.
      *
@@ -168,6 +185,24 @@ class provider implements
         $DB->delete_records_select('log', "$sql AND userid = :userid", array_merge($params, ['userid' => $userid]));
     }
 
+
+    /**
+     * Delete all data for a list of users in the specified context.
+     *
+     * @param \core_privacy\local\request\approved_userlist $userlist The specific context and users to delete data for.
+     * @return void
+     */
+    public static function delete_data_for_userlist(\core_privacy\local\request\approved_userlist $userlist) {
+        global $DB;
+        list($sql, $params) = static::get_sql_where_from_contexts([$userlist->get_context()]);
+        if (empty($sql)) {
+            return;
+        }
+        list($usersql, $userparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
+        $params = array_merge($params, $userparams);
+        $DB->delete_records_select('log', "$sql AND userid $usersql", $params);
+    }
+
     /**
      * Get an SQL where statement from a list of contexts.
      *
index a60960a..d6d2a6c 100644 (file)
@@ -87,6 +87,71 @@ class logstore_legacy_privacy_testcase extends provider_testcase {
         $this->assert_contextlist_equals($this->get_contextlist_for_user($u3), [$sysctx]);
     }
 
+    /**
+     * Test returning user IDs for a given context.
+     */
+    public function test_add_userids_for_context() {
+        $u1 = $this->getDataGenerator()->create_user();
+        $u2 = $this->getDataGenerator()->create_user();
+        $u3 = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $module = $this->getDataGenerator()->create_module('url', ['course' => $course]);
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($course->id);
+        $cm1ctx = context_module::instance($module->cmid);
+
+        $userctx = context_user::instance($u1->id);
+
+        $this->enable_logging();
+        $manager = get_log_manager(true);
+
+        $this->setUser($u1);
+        $e = unittest_executed::create(['context' => $sysctx, 'other' => ['sample' => 1]]);
+        $e->trigger();
+        $this->setUser($u2);
+        $e = unittest_executed::create(['context' => $sysctx, 'other' => ['sample' => 2]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $sysctx, 'other' => ['sample' => 3]]);
+        $e->trigger();
+        $this->setUser($u3);
+        $e = unittest_executed::create(['context' => $c1ctx, 'other' => ['sample' => 4]]);
+        $e->trigger();
+        $this->setUser($u1);
+        $e = unittest_executed::create(['context' => $c1ctx, 'other' => ['sample' => 5]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 6]]);
+        $e->trigger();
+        $this->setUser($u2);
+        $e = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 7]]);
+        $e->trigger();
+        $this->setUser($u3);
+        $e = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 8]]);
+        $e->trigger();
+
+        // Start with system and check that each of the contexts returns what we expected.
+        $userlist = new \core_privacy\local\request\userlist($sysctx, 'logstore_legacy');
+        provider::add_userids_for_context($userlist);
+        $systemuserids = $userlist->get_userids();
+        $this->assertCount(2, $systemuserids);
+        $this->assertNotFalse(array_search($u1->id, $systemuserids));
+        $this->assertNotFalse(array_search($u2->id, $systemuserids));
+        // Check the course context.
+        $userlist = new \core_privacy\local\request\userlist($c1ctx, 'logstore_legacy');
+        provider::add_userids_for_context($userlist);
+        $courseuserids = $userlist->get_userids();
+        $this->assertCount(2, $courseuserids);
+        $this->assertNotFalse(array_search($u1->id, $courseuserids));
+        $this->assertNotFalse(array_search($u3->id, $courseuserids));
+        // Check the module context.
+        $userlist = new \core_privacy\local\request\userlist($cm1ctx, 'logstore_legacy');
+        provider::add_userids_for_context($userlist);
+        $moduleuserids = $userlist->get_userids();
+        $this->assertCount(3, $moduleuserids);
+        $this->assertNotFalse(array_search($u1->id, $moduleuserids));
+        $this->assertNotFalse(array_search($u2->id, $moduleuserids));
+        $this->assertNotFalse(array_search($u3->id, $moduleuserids));
+    }
+
     public function test_delete_data_for_user() {
         global $DB;
 
@@ -241,6 +306,68 @@ class logstore_legacy_privacy_testcase extends provider_testcase {
         $this->assertEquals(0, $DB->count_records('log', ['userid' => $u2->id]));
     }
 
+    /**
+     * Test the deletion of data for a list of users in a context.
+     */
+    public function test_delete_data_for_userlist() {
+        global $DB;
+
+        $u1 = $this->getDataGenerator()->create_user();
+        $u2 = $this->getDataGenerator()->create_user();
+        $u3 = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $module = $this->getDataGenerator()->create_module('url', ['course' => $course]);
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($course->id);
+        $cm1ctx = context_module::instance($module->cmid);
+
+        $userctx = context_user::instance($u1->id);
+
+        $this->enable_logging();
+        $manager = get_log_manager(true);
+
+        $this->setUser($u1);
+        $e = unittest_executed::create(['context' => $sysctx, 'other' => ['sample' => 1]]);
+        $e->trigger();
+        $this->setUser($u2);
+        $e = unittest_executed::create(['context' => $sysctx, 'other' => ['sample' => 2]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $sysctx, 'other' => ['sample' => 3]]);
+        $e->trigger();
+        $this->setUser($u3);
+        $e = unittest_executed::create(['context' => $c1ctx, 'other' => ['sample' => 4]]);
+        $e->trigger();
+        $this->setUser($u1);
+        $e = unittest_executed::create(['context' => $c1ctx, 'other' => ['sample' => 5]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 6]]);
+        $e->trigger();
+        $this->setUser($u2);
+        $e = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 7]]);
+        $e->trigger();
+        $this->setUser($u3);
+        $e = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 8]]);
+        $e->trigger();
+
+        // System context deleting one user.
+        $this->assertEquals(3, $DB->count_records('log', ['cmid' => 0, 'course' => 0]));
+        $userlist = new \core_privacy\local\request\approved_userlist($sysctx, 'logstore_legacy', [$u2->id]);
+        provider::delete_data_for_userlist($userlist);
+        $this->assertEquals(1, $DB->count_records('log', ['cmid' => 0, 'course' => 0]));
+
+        // Course context deleting one user.
+        $this->assertEquals(2, $DB->count_records('log', ['cmid' => 0, 'course' => $course->id]));
+        $userlist = new \core_privacy\local\request\approved_userlist($c1ctx, 'logstore_legacy', [$u1->id]);
+        provider::delete_data_for_userlist($userlist);
+        $this->assertEquals(1, $DB->count_records('log', ['cmid' => 0, 'course' => $course->id]));
+
+        // Module context deleting two users.
+        $this->assertEquals(3, $DB->count_records('log', ['cmid' => $module->cmid, 'course' => $course->id]));
+        $userlist = new \core_privacy\local\request\approved_userlist($cm1ctx, 'logstore_legacy', [$u1->id, $u3->id]);
+        provider::delete_data_for_userlist($userlist);
+        $this->assertEquals(1, $DB->count_records('log', ['cmid' => $module->cmid, 'course' => $course->id]));
+    }
+
     public function test_export_data_for_user() {
         global $DB;
 
index 4ee8007..76a4e1f 100644 (file)
@@ -40,7 +40,8 @@ use core_privacy\local\request\contextlist;
  */
 class provider implements
     \core_privacy\local\metadata\provider,
-    \tool_log\local\privacy\logstore_provider {
+    \tool_log\local\privacy\logstore_provider,
+    \tool_log\local\privacy\logstore_userlist_provider {
 
     use \tool_log\local\privacy\moodle_database_export_and_delete;
 
@@ -86,6 +87,22 @@ class provider implements
         ]);
     }
 
+    /**
+     * Add user IDs that contain user information for the specified context.
+     *
+     * @param \core_privacy\local\request\userlist $userlist The userlist to add the users to.
+     * @return void
+     */
+    public static function add_userids_for_context(\core_privacy\local\request\userlist $userlist) {
+        $params = ['contextid' => $userlist->get_context()->id];
+        $sql = "SELECT userid, relateduserid, realuserid
+                  FROM {logstore_standard_log}
+                 WHERE contextid = :contextid";
+        $userlist->add_from_sql('userid', $sql, $params);
+        $userlist->add_from_sql('relateduserid', $sql, $params);
+        $userlist->add_from_sql('realuserid', $sql, $params);
+    }
+
     /**
      * Get the database object.
      *
index 5103b3a..1e23599 100644 (file)
@@ -109,6 +109,52 @@ class logstore_standard_privacy_testcase extends provider_testcase {
         $this->assert_contextlist_equals($this->get_contextlist_for_user($admin), [$sysctx, $c1ctx, $c2ctx]);
     }
 
+    public function test_add_userids_for_context() {
+        $admin = \core_user::get_user(2);
+        $u1 = $this->getDataGenerator()->create_user();
+        $u2 = $this->getDataGenerator()->create_user();
+        $u3 = $this->getDataGenerator()->create_user();
+        $u4 = $this->getDataGenerator()->create_user();
+
+        $c1 = $this->getDataGenerator()->create_course();
+
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($c1->id);
+
+        $this->enable_logging();
+        $manager = get_log_manager(true);
+
+        $userlist = new \core_privacy\local\request\userlist($sysctx, 'logstore_standard_log');
+        $userids = $userlist->get_userids();
+        $this->assertEmpty($userids);
+        provider::add_userids_for_context($userlist);
+        $userids = $userlist->get_userids();
+        $this->assertEmpty($userids);
+        // User one should be added (userid).
+        $this->setUser($u1);
+        $e = \logstore_standard\event\unittest_executed::create(['context' => $sysctx]);
+        $e->trigger();
+        // User two (userids) and three (relateduserid) should be added.
+        $this->setUser($u2);
+        $e = \logstore_standard\event\unittest_executed::create(['context' => $sysctx, 'relateduserid' => $u3->id]);
+        $e->trigger();
+        // The admin user should be added (realuserid).
+        $this->setAdminUser();
+        \core\session\manager::loginas($u2->id, context_system::instance());
+        $e = \logstore_standard\event\unittest_executed::create(['context' => $sysctx]);
+        $e->trigger();
+        // Set off an event in a different context. User 4 should not be returned below.
+        $this->setUser($u4);
+        $e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
+        $e->trigger();
+
+        provider::add_userids_for_context($userlist);
+        $userids = $userlist->get_userids();
+        $this->assertCount(4, $userids);
+        $expectedresult = [$admin->id, $u1->id, $u2->id, $u3->id];
+        $this->assertEmpty(array_diff($expectedresult, $userids));
+    }
+
     public function test_delete_data_for_user() {
         global $DB;
         $u1 = $this->getDataGenerator()->create_user();
@@ -191,6 +237,48 @@ class logstore_standard_privacy_testcase extends provider_testcase {
         $this->assertEquals(1, $DB->count_records('logstore_standard_log', ['userid' => $u2->id]));
     }
 
+    public function test_delete_data_for_userlist() {
+        global $DB;
+
+        $u1 = $this->getDataGenerator()->create_user();
+        $u2 = $this->getDataGenerator()->create_user();
+        $u3 = $this->getDataGenerator()->create_user();
+        $u4 = $this->getDataGenerator()->create_user();
+
+        $course = $this->getDataGenerator()->create_course();
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($course->id);
+
+        $this->enable_logging();
+        $manager = get_log_manager(true);
+
+        $this->setUser($u1);
+        $e = \logstore_standard\event\unittest_executed::create(['context' => $sysctx]);
+        $e->trigger();
+        $this->setUser($u2);
+        $e = \logstore_standard\event\unittest_executed::create(['context' => $sysctx]);
+        $e->trigger();
+        $this->setUser($u3);
+        $e = \logstore_standard\event\unittest_executed::create(['context' => $sysctx]);
+        $e->trigger();
+        $this->setUser($u4);
+        $e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
+        $e->trigger();
+
+        // Check that four records were created.
+        $this->assertEquals(4, $DB->count_records('logstore_standard_log'));
+
+        $userlist = new \core_privacy\local\request\approved_userlist($sysctx, 'logstore_standard_log', [$u1->id, $u3->id]);
+        provider::delete_data_for_userlist($userlist);
+        // We should have a record for u2 and u4.
+        $this->assertEquals(2, $DB->count_records('logstore_standard_log'));
+
+        $records = $DB->get_records('logstore_standard_log', ['contextid' => $sysctx->id]);
+        $this->assertCount(1, $records);
+        $currentrecord = array_shift($records);
+        $this->assertEquals($u2->id, $currentrecord->userid);
+    }
+
     public function test_export_data_for_user() {
         $admin = \core_user::get_user(2);
         $u1 = $this->getDataGenerator()->create_user();
index d6c88c3..5fcd7f7 100644 (file)
@@ -194,10 +194,10 @@ class api {
 
         // Check that we are receiving a moodle_url object, themes can override get_logo_url and may return incorrect values.
         if (($logourl = $OUTPUT->get_logo_url()) && $logourl instanceof moodle_url) {
-            $settings['logourl'] = $logourl->out(false);
+            $settings['logourl'] = clean_param($logourl->out(false), PARAM_URL);
         }
         if (($compactlogourl = $OUTPUT->get_compact_logo_url()) && $compactlogourl instanceof moodle_url) {
-            $settings['compactlogourl'] = $compactlogourl->out(false);
+            $settings['compactlogourl'] = clean_param($compactlogourl->out(false), PARAM_URL);
         }
 
         // Identity providers.
index 23874bb..5efe02b 100644 (file)
@@ -41,7 +41,38 @@ class issuer extends persistent {
     protected static $persistentclass = 'core\\oauth2\\issuer';
 
     /** @var array $fieldstoremove */
-    protected static $fieldstoremove = array('submitbutton', 'action');
+    protected static $fieldstoremove = array('type', 'submitbutton', 'action');
+
+    /** @var string $type */
+    protected $type;
+
+    /**
+     * Constructor.
+     *
+     * The 'persistent' has to be passed as custom data when 'editing'.
+     * If a standard issuer is created the type can be passed as custom data, which alters the form according to the
+     * type.
+     *
+     * Note that in order for your persistent to be reloaded after form submission you should
+     * either override the URL to include the ID to your resource, or add the ID to the form
+     * fields.
+     *
+     * @param mixed $action
+     * @param mixed $customdata
+     * @param string $method
+     * @param string $target
+     * @param mixed $attributes
+     * @param bool $editable
+     * @param array $ajaxformdata
+     */
+    public function __construct($action = null, $customdata = null, $method = 'post', $target = '', $attributes = null,
+                                $editable = true, array $ajaxformdata = null) {
+        // The type variable defines, if we are in the creation process of a standard issuer.
+        if (array_key_exists('type', $customdata)) {
+            $this->type = $customdata['type'];
+        }
+        parent::__construct($action, $customdata, $method, $target, $attributes, $editable, $ajaxformdata);
+    }
 
     /**
      * Define the form - called by parent constructor
@@ -108,6 +139,9 @@ class issuer extends persistent {
         $mform->addElement('text', 'baseurl', get_string('issuerbaseurl', 'tool_oauth2'));
         $mform->addRule('baseurl', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client');
         $mform->addHelpButton('baseurl', 'issuerbaseurl', 'tool_oauth2');
+        if ($this->type && $this->type == 'nextcloud') {
+            $mform->addRule('baseurl', null, 'required', null, 'client');
+        }
 
         // Allowed Domains.
         $mform->addElement('text', 'alloweddomains', get_string('issueralloweddomains', 'tool_oauth2'));
@@ -130,8 +164,16 @@ class issuer extends persistent {
         $mform->addElement('hidden', 'sortorder');
         $mform->setType('sortorder', PARAM_INT);
 
-        $mform->addElement('hidden', 'action', 'edit');
-        $mform->setType('action', PARAM_ALPHA);
+        if ($this->type) {
+            $mform->addElement('hidden', 'action', 'savetemplate');
+            $mform->setType('action', PARAM_ALPHA);
+
+            $mform->addElement('hidden', 'type', $this->_customdata['type']);
+            $mform->setType('type', PARAM_ALPHA);
+        } else {
+            $mform->addElement('hidden', 'action', 'edit');
+            $mform->setType('action', PARAM_ALPHA);
+        }
 
         $mform->addElement('hidden', 'enabled', $issuer->get('enabled'));
         $mform->setType('enabled', PARAM_BOOL);
index 5bb4c4f..87fb17d 100644 (file)
@@ -86,15 +86,36 @@ if ($mform && $mform->is_cancelled()) {
         $mform->display();
         echo $OUTPUT->footer();
     }
+} else if ($action == 'savetemplate') {
+
+    $type = required_param('type', PARAM_ALPHA);
+    $mform = new \tool_oauth2\form\issuer(null, ['persistent' => $issuer, 'type' => $type]);
+    if ($mform->is_cancelled()) {
+        redirect(new moodle_url('/admin/tool/oauth2/issuers.php'));
+    }
+    if ($mform->is_submitted() && $data = $mform->get_data()) {
+        $issuer = new core\oauth2\issuer(0, $data);
+        $issuer->create();
+        $issuer = core\oauth2\api::create_endpoints_for_standard_issuer($type, $issuer);
+        redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
+    } else {
+        echo $OUTPUT->header();
+        $mform->display();
+        echo $OUTPUT->footer();
+    }
+
 } else if ($action == 'edittemplate') {
 
     $type = required_param('type', PARAM_ALPHA);
     $docs = required_param('docslink', PARAM_ALPHAEXT);
     require_sesskey();
-    $issuer = core\oauth2\api::create_standard_issuer($type);
-    $params = ['action' => 'edit', 'id' => $issuer->get('id'), 'docslink' => $docs];
-    $editurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params);
-    redirect($editurl, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
+    $issuer = core\oauth2\api::init_standard_issuer($type);
+    $mform = new \tool_oauth2\form\issuer(null, ['persistent' => $issuer, 'type' => $type]);
+
+    echo $OUTPUT->header();
+    $mform->display();
+    echo $OUTPUT->footer();
+
 } else if ($action == 'enable') {
 
     require_sesskey();
@@ -171,6 +192,11 @@ if ($mform && $mform->is_cancelled()) {
     $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params);
     echo $renderer->single_button($addurl, get_string('createnewfacebookissuer', 'tool_oauth2'));
     $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['action' => 'edit']);
+    $docs = 'admin/tool/oauth2/issuers/nextcloud';
+    $params = ['action' => 'edittemplate', 'type' => 'nextcloud', 'sesskey' => sesskey(), 'docslink' => $docs];
+    $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params);
+    echo $renderer->single_button($addurl, get_string('createnewnextcloudissuer', 'tool_oauth2'));
+    $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['action' => 'edit']);
     echo $renderer->single_button($addurl, get_string('createnewissuer', 'tool_oauth2'));
     echo $OUTPUT->footer();
 
index 395587a..6e5545f 100644 (file)
@@ -35,6 +35,7 @@ $string['createnewfacebookissuer'] = 'Create new Facebook service';
 $string['createnewgoogleissuer'] = 'Create new Google service';
 $string['createnewissuer'] = 'Create new custom service';
 $string['createnewmicrosoftissuer'] = 'Create new Microsoft service';
+$string['createnewnextcloudissuer'] = 'Create new Nextcloud service';
 $string['createnewuserfieldmapping'] = 'Create new user field mapping for issuer "{$a}"';
 $string['deleteconfirm'] = 'Are you sure you want to delete the identity issuer "{$a}"? Any plugins relying on this issuer will stop working.';
 $string['deleteendpointconfirm'] = 'Are you sure you want to delete the endpoint "{$a->endpoint}" for issuer "{$a->issuer}"? Any plugins relying on this endpoint will stop working.';
index 199baf4..fa6e91e 100644 (file)
@@ -795,7 +795,7 @@ class model {
                         // skip it and do nothing with it.
                         debugging($this->model->id . ' model predictions processor could not process the sample with id ' .
                             $sampleinfo[0], DEBUG_DEVELOPER);
-                        continue;
+                        continue 2;
                     case 2:
                         // Prediction processors that do not return a prediction score will have the maximum prediction
                         // score.
index ac5ae72..36f6069 100644 (file)
@@ -28,8 +28,10 @@ use core_privacy\local\request\transform;
 use core_privacy\local\request\writer;
 use core_privacy\local\metadata\collection;
 use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\approved_userlist;
 use core_privacy\local\request\context;
 use core_privacy\local\request\contextlist;
+use core_privacy\local\request\userlist;
 
 defined('MOODLE_INTERNAL') || die();
 
@@ -39,7 +41,10 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2018 David MonllaĆ³
  * @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 {
+class provider implements
+        \core_privacy\local\metadata\provider,
+        \core_privacy\local\request\core_userlist_provider,
+        \core_privacy\local\request\plugin\provider {
 
     /**
      * Returns meta data about this system.
@@ -126,14 +131,62 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
             $contextlist->add_from_sql($sql, ['userid' => $userid, 'analysersamplesorigin' => $analyser->get_samples_origin()]);
         }
 
-        // We can leave this out of the loop as there is no analyser-dependant stuff.
-        list($sql, $params) = self::analytics_prediction_actions_sql($userid, array_keys($models));
+        // We can leave this out of the loop as there is no analyser-dependent stuff.
+        list($sql, $params) = self::analytics_prediction_actions_user_sql($userid, array_keys($models));
         $sql = "SELECT DISTINCT ap.contextid" . $sql;
         $contextlist->add_from_sql($sql, $params);
 
         return $contextlist;
     }
 
+    /**
+     * Get the list of users who have data within a context.
+     *
+     * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
+     */
+    public static function get_users_in_context(userlist $userlist) {
+        global $DB;
+
+        $context = $userlist->get_context();
+        $models = self::get_models_with_user_data();
+
+        foreach ($models as $modelid => $model) {
+
+            $analyser = $model->get_analyser(['notimesplitting' => true]);
+
+            // Analytics predictions.
+            $params = [
+                'contextid' => $context->id,
+                'modelid' => $modelid,
+            ];
+            $joinusersql = $analyser->join_sample_user('ap');
+            $sql = "SELECT u.id AS userid
+                      FROM {analytics_predictions} ap
+                           {$joinusersql}
+                     WHERE ap.contextid = :contextid
+                       AND ap.modelid = :modelid";
+            $userlist->add_from_sql('userid', $sql, $params);
+
+            // Indicator calculations.
+            $params = [
+                'contextid' => $context->id,
+                'analysersamplesorigin' => $analyser->get_samples_origin(),
+            ];
+            $joinusersql = $analyser->join_sample_user('aic');
+            $sql = "SELECT u.id AS userid
+                      FROM {analytics_indicator_calc} aic
+                           {$joinusersql}
+                     WHERE aic.contextid = :contextid
+                       AND aic.sampleorigin = :analysersamplesorigin";
+            $userlist->add_from_sql('userid', $sql, $params);
+        }
+
+        // We can leave this out of the loop as there is no analyser-dependent stuff.
+        list($sql, $params) = self::analytics_prediction_actions_context_sql($context->id, array_keys($models));
+        $sql = "SELECT apa.userid" . $sql;
+        $userlist->add_from_sql('userid', $sql, $params);
+    }
+
     /**
      * Export all user data for the specified user, in the specified contexts.
      *
@@ -215,7 +268,7 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
 
         // Analytics predictions.
         // Provided contexts are ignored as we export all user-related stuff.
-        list($sql, $params) = self::analytics_prediction_actions_sql($userid, $modelids, $contextsql);
+        list($sql, $params) = self::analytics_prediction_actions_user_sql($userid, $modelids, $contextsql);
         $sql = "SELECT apa.*, ap.modelid, ap.contextid, $ctxfields" . $sql;
         $predictionactions = $DB->get_recordset_sql($sql, $params + $contextparams);
         foreach ($predictionactions as $predictionaction) {
@@ -282,7 +335,7 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
         list ($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
 
         // Analytics prediction actions.
-        list($sql, $apaparams) = self::analytics_prediction_actions_sql($userid, $modelids, $contextsql);
+        list($sql, $apaparams) = self::analytics_prediction_actions_user_sql($userid, $modelids, $contextsql);
         $sql = "SELECT apa.id " . $sql;
 
         $predictionactionids = $DB->get_fieldset_sql($sql, $apaparams + $contextparams);
@@ -322,6 +375,70 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
         }
     }
 
+    /**
+     * Delete multiple users within a single context.
+     *
+     * @param   approved_userlist       $userlist The approved context and user information to delete information for.
+     */
+    public static function delete_data_for_users(approved_userlist $userlist) {
+        global $DB;
+
+        $context = $userlist->get_context();
+        $models = self::get_models_with_user_data();
+        $modelids = array_keys($models);
+        list($usersinsql, $baseparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
+
+        // Analytics prediction actions.
+        list($sql, $apaparams) = self::analytics_prediction_actions_context_sql($context->id, $modelids, $usersinsql);
+        $sql = "SELECT apa.id" . $sql;
+        $predictionactionids = $DB->get_fieldset_sql($sql, $baseparams + $apaparams);
+
+        if ($predictionactionids) {
+            list ($predictionactionidssql, $params) = $DB->get_in_or_equal($predictionactionids);
+            $DB->delete_records_select('analytics_prediction_actions', "id {$predictionactionidssql}", $params);
+        }
+
+        $baseparams['contextid'] = $context->id;
+
+        foreach ($models as $modelid => $model) {
+            $analyser = $model->get_analyser(['notimesplitting' => true]);
+
+            // Analytics predictions.
+            $joinusersql = $analyser->join_sample_user('ap');
+            $sql = "SELECT DISTINCT ap.id
+                      FROM {analytics_predictions} ap
+                           {$joinusersql}
+                     WHERE ap.contextid = :contextid
+                       AND ap.modelid = :modelid
+                       AND u.id {$usersinsql}";
+            $params = $baseparams;
+            $params['modelid'] = $modelid;
+            $predictionids = $DB->get_fieldset_sql($sql, $params);
+
+            if ($predictionids) {
+                list($predictionidssql, $params) = $DB->get_in_or_equal($predictionids, SQL_PARAMS_NAMED);
+                $DB->delete_records_select('analytics_predictions', "id {$predictionidssql}", $params);
+            }
+
+            // Indicator calculations.
+            $joinusersql = $analyser->join_sample_user('aic');
+            $sql = "SELECT DISTINCT aic.id
+                      FROM {analytics_indicator_calc} aic
+                           {$joinusersql}
+                     WHERE aic.contextid = :contextid
+                       AND aic.sampleorigin = :analysersamplesorigin
+                       AND u.id {$usersinsql}";
+            $params = $baseparams;
+            $params['analysersamplesorigin'] = $analyser->get_samples_origin();
+            $indicatorcalcids = $DB->get_fieldset_sql($sql, $params);
+
+            if ($indicatorcalcids) {
+                list ($indicatorcalcidssql, $params) = $DB->get_in_or_equal($indicatorcalcids, SQL_PARAMS_NAMED);
+                $DB->delete_records_select('analytics_indicator_calc', "id $indicatorcalcidssql", $params);
+            }
+        }
+    }
+
     /**
      * Returns a list of models with user data.
      *
@@ -339,14 +456,14 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
     }
 
     /**
-     * Returns the sql query to query analytics_prediction_actions table.
+     * Returns the sql query to query analytics_prediction_actions table by user ID.
      *
-     * @param int $userid
-     * @param int[] $modelids
-     * @param string $contextsql
-     * @return array sql string in [0] and params in [1]
+     * @param int $userid The user ID of the analytics prediction.
+     * @param int[] $modelids Model IDs to include in the SQL.
+     * @param string $contextsql Optional "in or equal" SQL to also query by context ID(s).
+     * @return array sql string in [0] and params in [1].
      */
-    private static function analytics_prediction_actions_sql($userid, $modelids, $contextsql = false) {
+    private static function analytics_prediction_actions_user_sql($userid, $modelids, $contextsql = false) {
         global $DB;
 
         list($insql, $params) = $DB->get_in_or_equal($modelids, SQL_PARAMS_NAMED);
@@ -363,4 +480,29 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
 
         return [$sql, $params];
     }
+
+    /**
+     * Returns the sql query to query analytics_prediction_actions table by context ID.
+     *
+     * @param int $contextid The context ID of the analytics prediction.
+     * @param int[] $modelids Model IDs to include in the SQL.
+     * @param string $usersql Optional "in or equal" SQL to also query by user ID(s).
+     * @return array sql string in [0] and params in [1].
+     */
+    private static function analytics_prediction_actions_context_sql($contextid, $modelids, $usersql = false) {
+        global $DB;
+
+        list($insql, $params) = $DB->get_in_or_equal($modelids, SQL_PARAMS_NAMED);
+        $sql = " FROM {analytics_predictions} ap
+                  JOIN {analytics_prediction_actions} apa ON apa.predictionid = ap.id
+                 WHERE ap.contextid = :contextid
+                   AND ap.modelid {$insql}";
+        $params['contextid'] = $contextid;
+
+        if ($usersql) {
+            $sql .= " AND apa.userid {$usersql}";
+        }
+
+        return [$sql, $params];
+    }
 }
index 026d839..4e7e2ae 100644 (file)
@@ -26,6 +26,7 @@ use \core_analytics\privacy\provider;
 use core_privacy\local\request\transform;
 use core_privacy\local\request\writer;
 use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\approved_userlist;
 
 defined('MOODLE_INTERNAL') || die();
 
@@ -99,6 +100,42 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
         $this->setAdminUser();
     }
 
+    /**
+     * Test fetching users within a context.
+     */
+    public function test_get_users_in_context() {
+        global $CFG;
+
+        $component = 'core_analytics';
+        $course1context = \context_course::instance($this->c1->id);
+        $course2context = \context_course::instance($this->c2->id);
+        $systemcontext = \context_system::instance();
+        $expected = [$this->u1->id, $this->u2->id, $this->u3->id, $this->u4->id];
+
+        // Check users exist in the relevant contexts.
+        $userlist = new \core_privacy\local\request\userlist($course1context, $component);
+        provider::get_users_in_context($userlist);
+        $actual = $userlist->get_userids();
+        sort($actual);
+        $this->assertEquals($expected, $actual);
+
+        $userlist = new \core_privacy\local\request\userlist($course2context, $component);
+        provider::get_users_in_context($userlist);
+        $actual = $userlist->get_userids();
+        sort($actual);
+        $this->assertEquals($expected, $actual);
+
+        // System context will also find guest and admin user, add to expected before testing.
+        $expected = array_merge($expected, [$CFG->siteguest, get_admin()->id]);
+        sort($expected);
+
+        $userlist = new \core_privacy\local\request\userlist($systemcontext, $component);
+        provider::get_users_in_context($userlist);
+        $actual = $userlist->get_userids();
+        sort($actual);
+        $this->assertEquals($expected, $actual);
+    }
+
     /**
      * Test delete a context.
      *
@@ -160,6 +197,130 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
         $this->assertEquals(0, $DB->count_records('analytics_predictions'));
     }
 
+    /**
+     * Test deleting multiple users in a context.
+     */
+    public function test_delete_data_for_users() {
+        global $DB;
+
+        $component = 'core_analytics';
+        $course1context = \context_course::instance($this->c1->id);
+        $course2context = \context_course::instance($this->c2->id);
+        $systemcontext = \context_system::instance();
+
+        // Ensure all records exist in expected contexts.
+        $expectedcontexts = [$course1context->id, $course2context->id, $systemcontext->id];
+        sort($expectedcontexts);
+
+        $actualcontexts = [
+            $this->u1->id => provider::get_contexts_for_userid($this->u1->id)->get_contextids(),
+            $this->u2->id => provider::get_contexts_for_userid($this->u2->id)->get_contextids(),
+            $this->u3->id => provider::get_contexts_for_userid($this->u3->id)->get_contextids(),
+            $this->u4->id => provider::get_contexts_for_userid($this->u4->id)->get_contextids(),
+        ];
+
+        foreach ($actualcontexts as $userid => $unused) {
+            sort($actualcontexts[$userid]);
+            $this->assertEquals($expectedcontexts, $actualcontexts[$userid]);
+        }
+
+        // Test initial record counts are as expected.
+        $this->assertEquals(6, $DB->count_records('analytics_predictions'));
+        $this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
+        $this->assertEquals(14, $DB->count_records('analytics_indicator_calc'));
+
+        // Delete u1 and u3 from system context.
+        $approveduserids = [$this->u1->id, $this->u3->id];
+        $approvedlist = new approved_userlist($systemcontext, $component, $approveduserids);
+        provider::delete_data_for_users($approvedlist);
+
+        // Ensure u1 and u3 system context data deleted only.
+        $expectedcontexts = [
+            $this->u1->id => [$course1context->id, $course2context->id],
+            $this->u2->id => [$systemcontext->id, $course1context->id, $course2context->id],
+            $this->u3->id => [$course1context->id, $course2context->id],
+            $this->u4->id => [$systemcontext->id, $course1context->id, $course2context->id],
+        ];
+
+        $actualcontexts = [
+            $this->u1->id => provider::get_contexts_for_userid($this->u1->id)->get_contextids(),
+            $this->u2->id => provider::get_contexts_for_userid($this->u2->id)->get_contextids(),
+            $this->u3->id => provider::get_contexts_for_userid($this->u3->id)->get_contextids(),
+            $this->u4->id => provider::get_contexts_for_userid($this->u4->id)->get_contextids(),
+        ];
+
+        foreach ($actualcontexts as $userid => $unused) {
+            sort($expectedcontexts[$userid]);
+            sort($actualcontexts[$userid]);
+            $this->assertEquals($expectedcontexts[$userid], $actualcontexts[$userid]);
+        }
+
+        // Test expected number of records have been deleted.
+        $this->assertEquals(5, $DB->count_records('analytics_predictions'));
+        $this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
+        $this->assertEquals(12, $DB->count_records('analytics_indicator_calc'));
+
+        // Delete for all 4 users in course 2 context.
+        $approveduserids = [$this->u1->id, $this->u2->id, $this->u3->id, $this->u4->id];
+        $approvedlist = new approved_userlist($course2context, $component, $approveduserids);
+        provider::delete_data_for_users($approvedlist);
+
+        // Ensure all course 2 context data deleted for all 4 users.
+        $expectedcontexts = [
+            $this->u1->id => [$course1context->id],
+            $this->u2->id => [$systemcontext->id, $course1context->id],
+            $this->u3->id => [$course1context->id],
+            $this->u4->id => [$systemcontext->id, $course1context->id],
+        ];
+
+        $actualcontexts = [
+            $this->u1->id => provider::get_contexts_for_userid($this->u1->id)->get_contextids(),
+            $this->u2->id => provider::get_contexts_for_userid($this->u2->id)->get_contextids(),
+            $this->u3->id => provider::get_contexts_for_userid($this->u3->id)->get_contextids(),
+            $this->u4->id => provider::get_contexts_for_userid($this->u4->id)->get_contextids(),
+        ];
+
+        foreach ($actualcontexts as $userid => $unused) {
+            sort($actualcontexts[$userid]);
+            sort($expectedcontexts[$userid]);
+            $this->assertEquals($expectedcontexts[$userid], $actualcontexts[$userid]);
+        }
+
+        // Test expected number of records have been deleted.
+        $this->assertEquals(3, $DB->count_records('analytics_predictions'));
+        $this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
+        $this->assertEquals(8, $DB->count_records('analytics_indicator_calc'));
+
+        $approveduserids = [$this->u3->id];
+        $approvedlist = new approved_userlist($course1context, $component, $approveduserids);
+        provider::delete_data_for_users($approvedlist);
+
+        // Ensure all course 1 context data deleted for u3.
+        $expectedcontexts = [
+            $this->u1->id => [$course1context->id],
+            $this->u2->id => [$systemcontext->id, $course1context->id],
+            $this->u3->id => [],
+            $this->u4->id => [$systemcontext->id, $course1context->id],
+        ];
+
+        $actualcontexts = [
+            $this->u1->id => provider::get_contexts_for_userid($this->u1->id)->get_contextids(),
+            $this->u2->id => provider::get_contexts_for_userid($this->u2->id)->get_contextids(),
+            $this->u3->id => provider::get_contexts_for_userid($this->u3->id)->get_contextids(),
+            $this->u4->id => provider::get_contexts_for_userid($this->u4->id)->get_contextids(),
+        ];
+        foreach ($actualcontexts as $userid => $unused) {
+            sort($actualcontexts[$userid]);
+            sort($expectedcontexts[$userid]);
+            $this->assertEquals($expectedcontexts[$userid], $actualcontexts[$userid]);
+        }
+
+        // Test expected number of records have been deleted.
+        $this->assertEquals(2, $DB->count_records('analytics_predictions'));
+        $this->assertEquals(0, $DB->count_records('analytics_prediction_actions'));
+        $this->assertEquals(7, $DB->count_records('analytics_indicator_calc'));
+    }
+
     /**
      * Test export user data.
      *
index 8cce608..b850955 100644 (file)
@@ -3380,7 +3380,7 @@ class CAS_Client
                 case 'user':
                 case 'proxies':
                 case 'proxyGrantingTicket':
-                    continue;
+                    break;
                 default:
                     if (strlen(trim($attr_node->nodeValue))) {
                         phpCas :: trace(
index b368b32..15a8917 100644 (file)
@@ -1,4 +1,5 @@
 Description of phpCAS 1.3.5 library import
 
 * downloaded from http://downloads.jasig.org/cas-clients/php/current/
-* applied patch https://github.com/apereo/phpCAS/pull/247 for PHP 7.2 compatibility (MDL-60280)
\ No newline at end of file
+* applied patch https://github.com/apereo/phpCAS/pull/247 for PHP 7.2 compatibility (MDL-60280)
+* applied patch https://github.com/apereo/phpCAS/pull/278 for PHP 7.3 compatibility (MDL-63422)
index 3c3ae4a..67b4144 100644 (file)
@@ -35,8 +35,9 @@ if (empty($CFG->enablebadges)) {
 
 $hash = required_param('b', PARAM_ALPHANUM); // Issued badge unique hash for badge assertion.
 $action = optional_param('action', null, PARAM_BOOL); // Generates badge class if true.
+$obversion = optional_param('obversion', OPEN_BADGES_V1, PARAM_INT); // For control format OB specification version.
 
-$assertion = new core_badges_assertion($hash);
+$assertion = new core_badges_assertion($hash, $obversion);
 
 if (!is_null($action)) {
     // Get badge class or issuer information depending on $action.
@@ -48,11 +49,16 @@ if (!is_null($action)) {
         $json = $assertion->get_badge_assertion();
     } else { // Revoked badge.
         header("HTTP/1.0 410 Gone");
-        echo json_encode(array("revoked" => true));
+        $assertion = array();
+        if ($obversion == OPEN_BADGES_V2) {
+            $assertionurl = new moodle_url('/badges/assertion.php', array('b' => $hash));
+            $assertion['id'] = $assertionurl->out();
+        }
+        $assertion['revoked'] = true;
+        echo json_encode($assertion);
         die();
     }
 }
 
-
 echo $OUTPUT->header();
 echo json_encode($json);
diff --git a/badges/badge_json.php b/badges/badge_json.php
new file mode 100644 (file)
index 0000000..907f0f1
--- /dev/null
@@ -0,0 +1,126 @@
+<?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/>.
+
+/**
+ * Serve BadgeClass JSON for related badge.
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2018 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+define('AJAX_SCRIPT', true);
+define('NO_MOODLE_COOKIES', true); // No need for a session here.
+
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+
+$id = required_param('id', PARAM_INT);
+$action = optional_param('action', null, PARAM_INT); // Generates badge class if true.
+$json = array();
+$badge = new badge($id);
+if ($badge->status != BADGE_STATUS_INACTIVE) {
+    if (is_null($action)) {
+        // Get the content of badge class.
+        if (empty($badge->courseid)) {
+            $context = context_system::instance();
+        } else {
+            $context = context_course::instance($badge->courseid);
+        }
+        $urlimage = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/', 'f1')->out(false);
+
+        $url = new moodle_url('/badges/badge_json.php', array('id' => $badge->id));
+
+        $json['name'] = $badge->name;
+        $json['description'] = $badge->description;
+        if ($badge->imageauthorname ||
+                $badge->imageauthoremail ||
+                $badge->imageauthorurl ||
+                $badge->imagecaption) {
+            $urlimage = moodle_url::make_pluginfile_url($context->id,
+                'badges', 'badgeimage', $badge->id, '/', 'f1')->out(false);
+            $json['image'] = array();
+            $json['image']['id'] = $urlimage;
+            if ($badge->imageauthorname || $badge->imageauthoremail || $badge->imageauthorurl) {
+                $authorimage = new moodle_url('/badges/image_author_json.php', array('id' => $badge->id));
+                $json['image']['author'] = $authorimage->out(false);
+            }
+            if ($badge->imagecaption) {
+                $json['image']['caption'] = $badge->imagecaption;
+            }
+        } else {
+            $json['image'] = $urlimage;
+        }
+
+        $json['criteria']['id'] = $url->out(false);
+        $json['criteria']['narrative'] = $badge->markdown_badge_criteria();
+        $json['issuer'] = $badge->get_badge_issuer();
+        $json['@context'] = OPEN_BADGES_V2_CONTEXT;
+        $json['id'] = $url->out();
+        $json['type'] = OPEN_BADGES_V2_TYPE_BADGE;
+        if (!empty($badge->version)) {
+            $json['version'] = $badge->version;
+        }
+        if (!empty($badge->language)) {
+            $json['@language'] = $badge->language;
+        }
+
+        $relatedbadges = $badge->get_related_badges(true);
+        if (!empty($relatedbadges)) {
+            foreach ($relatedbadges as $related) {
+                $relatedurl = new moodle_url('/badges/badge_json.php', array('id' => $related->id));
+                $relateds[] = array('id' => $relatedurl->out(false),
+                    'version' => $related->version, '@language' => $related->language);
+            }
+             $json['related'] = $relateds;
+        }
+
+        $endorsement = $badge->get_endorsement();
+        if (!empty($endorsement)) {
+            $endorsementurl = new moodle_url('/badges/endorsement_json.php', array('id' => $badge->id));
+            $json['endorsement'] = $endorsementurl->out(false);
+        }
+
+        $competencies = $badge->get_alignment();
+        if (!empty($competencies)) {
+            foreach ($competencies as $item) {
+                $alignment = array('targetName' => $item->targetname, 'targetUrl' => $item->targeturl);
+                if ($item->targetdescription) {
+                    $alignment['targetDescription'] = $item->targetdescription;
+                }
+                if ($item->targetframework) {
+                    $alignment['targetFramework'] = $item->targetframework;
+                }
+                if ($item->targetcode) {
+                    $alignment['targetCode'] = $item->targetcode;
+                }
+                $json['alignment'][] = $alignment;
+            }
+        }
+    } else if ($action == 0) {
+        // Get the content for issuer.
+        $json = $badge->get_badge_issuer();
+    }
+} else {
+    // The badge doen't exist or not accessible for the users.
+    header("HTTP/1.0 410 Gone");
+    $badgeurl = new moodle_url('/badges/badge_json.php', array('id' => $id));
+    $json['id'] = $badgeurl->out();
+    $json['error'] = get_string('error:relatedbadgedoesntexist', 'badges');
+}
+echo $OUTPUT->header();
+echo json_encode($json);
\ No newline at end of file
index c5dba46..417594d 100644 (file)
@@ -34,6 +34,8 @@ defined('MOODLE_INTERNAL') || die();
  * - Badge Class (general information about a badge and what it is intended to represent)
  * - Issuer Class (general information of an issuing organisation)
  */
+require_once($CFG->libdir . '/badgeslib.php');
+require_once($CFG->dirroot . '/badges/renderer.php');
 
 /**
  * Class that represents badge assertion.
@@ -46,12 +48,16 @@ class core_badges_assertion {
     /** @var moodle_url Issued badge url */
     private $_url;
 
+    /** @var int $obversion to control version JSON-LD. */
+    private $_obversion = OPEN_BADGES_V1;
+
     /**
      * Constructs with issued badge unique hash.
      *
      * @param string $hash Badge unique hash from badge_issued table.
+     * @param int $obversion to control version JSON-LD.
      */
-    public function __construct($hash) {
+    public function __construct($hash, $obversion = OPEN_BADGES_V1) {
         global $DB;
 
         $this->_data = $DB->get_record_sql('
@@ -78,6 +84,7 @@ class core_badges_assertion {
         } else {
             $this->_url = new moodle_url('/badges/badge.php');
         }
+        $this->_obversion = $obversion;
     }
 
     /**
@@ -111,6 +118,7 @@ class core_badges_assertion {
             if (!empty($this->_data->dateexpire)) {
                 $assertion['expires'] = $this->_data->dateexpire;
             }
+            $this->embed_data_badge_version2($assertion, OPEN_BADGES_V2_TYPE_ASSERTION);
         }
         return $assertion;
     }
@@ -136,6 +144,7 @@ class core_badges_assertion {
             $class['image'] = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $this->_data->id, '/', 'f1')->out(false);
             $class['criteria'] = $this->_url->out(false); // Currently issued badge URL.
             $class['issuer'] = $issuerurl->out(false);
+            $this->embed_data_badge_version2($class, OPEN_BADGES_V2_TYPE_BADGE);
         }
         return $class;
     }
@@ -156,7 +165,169 @@ class core_badges_assertion {
                 $issuer['email'] = $this->_data->issuercontact;
             }
         }
+        $this->embed_data_badge_version2($issuer, OPEN_BADGES_V2_TYPE_ISSUER);
         return $issuer;
     }
 
+    /**
+     * Get related badges of the badge.
+     *
+     * @param badge $badge Badge object.
+     * @return array|bool List related badges.
+     */
+    public function get_related_badges(badge $badge) {
+        global $DB;
+        $arraybadges = array();
+        $relatedbadges = $badge->get_related_badges(true);
+        if ($relatedbadges) {
+            foreach ($relatedbadges as $rb) {
+                $url = new moodle_url('/badges/badge_json.php', array('id' => $rb->id));
+                $arraybadges[] = array(
+                    'id'        => $url->out(false),
+                    'version'   => $rb->version,
+                    '@language' => $rb->language
+                );
+            }
+        }
+        return $arraybadges;
+    }
+
+    /**
+     * Get endorsement of the badge.
+     *
+     * @return false|stdClass Endorsement information.
+     */
+    public function get_endorsement() {
+        global $DB;
+        $endorsement = array();
+        $record = $DB->get_record_select('badge_endorsement', 'badgeid = ?', array($this->_data->id));
+        return $record;
+    }
+
+    /**
+     * Get criteria of badge class.
+     *
+     * @return array|string Criteria information.
+     */
+    public function get_criteria_badge_class() {
+        $badge = new badge($this->_data->id);
+        $narrative = $badge->markdown_badge_criteria();
+        if (!empty($narrative)) {
+            $criteria = array();
+            $criteria['id'] = $this->_url->out(false);
+            $criteria['narrative'] = $narrative;
+            return $criteria;
+        } else {
+            return $this->_url->out(false);
+        }
+    }
+
+    /**
+     * Get competencies alignment of the badge.
+     *
+     * @return array competencies information.
+     */
+    public function get_competencies_alignment() {
+        global $DB;
+        $badgeid = $this->_data->id;
+        $alignments = array();
+        $items = $DB->get_records_select('badge_competencies', 'badgeid = ?', array($badgeid));
+        foreach ($items as $item) {
+            $alignment = array('targetName' => $item->targetname, 'targetUrl' => $item->targeturl);
+            if ($item->targetdescription) {
+                $alignment['targetDescription'] = $item->targetdescription;
+            }
+            if ($item->targetframework) {
+                $alignment['targetFramework'] = $item->targetframework;
+            }
+            if ($item->targetcode) {
+                $alignment['targetCode'] = $item->targetcode;
+            }
+            $alignments[] = $alignment;
+        }
+        return $alignments;
+    }
+
+    /**
+     * Embed data of Open Badges Specification Version 2.0 to json.
+     *
+     * @param array $json for assertion, badges, issuer.
+     * @param string $type Content type.
+     */
+    protected function embed_data_badge_version2 (&$json, $type = OPEN_BADGES_V2_TYPE_ASSERTION) {
+        // Specification Version 2.0.
+        if ($this->_obversion == OPEN_BADGES_V2) {
+            $badge = new badge($this->_data->id);
+            if (empty($this->_data->courseid)) {
+                $context = context_system::instance();
+            } else {
+                $context = context_course::instance($this->_data->courseid);
+            }
+
+            $hash = $this->_data->uniquehash;
+            $assertionsurl = new moodle_url('/badges/assertion.php', array('b' => $hash, 'obversion' => $this->_obversion));
+            $classurl = new moodle_url(
+                '/badges/assertion.php',
+                array('b' => $hash, 'action' => 1, 'obversion' => $this->_obversion)
+            );
+            $issuerurl = new moodle_url('/badges/assertion.php', array('b' => $this->_data->uniquehash, 'action' => 0,
+                'obversion' => $this->_obversion));
+            // For assertion.
+            if ($type == OPEN_BADGES_V2_TYPE_ASSERTION) {
+                $json['@context'] = OPEN_BADGES_V2_CONTEXT;
+                $json['type'] = OPEN_BADGES_V2_TYPE_ASSERTION;
+                $json['id'] = $assertionsurl->out(false);
+                $json['badge'] = $this->get_badge_class();
+                $json['issuedOn'] = date('c', $this->_data->dateissued);
+                if (!empty($this->_data->dateexpire)) {
+                    $json['expires'] = date('c', $this->_data->dateexpire);
+                }
+                unset($json['uid']);
+            }
+
+            // For Badge.
+            if ($type == OPEN_BADGES_V2_TYPE_BADGE) {
+                $json['@context'] = OPEN_BADGES_V2_CONTEXT;
+                $json['id'] = $classurl->out(false);
+                $json['type'] = OPEN_BADGES_V2_TYPE_BADGE;
+                $json['version'] = $this->_data->version;
+                $json['criteria'] = $this->get_criteria_badge_class();
+                $json['issuer'] = $this->get_issuer();
+                $json['@language'] = $this->_data->language;
+                if (!empty($relatedbadges = $this->get_related_badges($badge))) {
+                    $json['related'] = $relatedbadges;
+                }
+                if ($endorsement = $this->get_endorsement()) {
+                    $endorsementurl = new moodle_url('/badges/endorsement_json.php', array('id' => $this->_data->id));
+                    $json['endorsement'] = $endorsementurl->out(false);
+                }
+                if ($competencies = $this->get_competencies_alignment()) {
+                    $json['alignment'] = $competencies;
+                }
+                if ($this->_data->imageauthorname ||
+                        $this->_data->imageauthoremail ||
+                        $this->_data->imageauthorurl ||
+                        $this->_data->imagecaption) {
+                    $urlimage = moodle_url::make_pluginfile_url($context->id,
+                        'badges', 'badgeimage', $this->_data->id, '/', 'f1')->out(false);
+                    $json['image'] = array();
+                    $json['image']['id'] = $urlimage;
+                    if ($this->_data->imageauthorname || $this->_data->imageauthoremail || $this->_data->imageauthorurl) {
+                        $authorimage = new moodle_url('/badges/image_author_json.php', array('id' => $this->_data->id));
+                        $json['image']['author'] = $authorimage->out(false);
+                    }
+                    if ($this->_data->imagecaption) {
+                        $json['image']['caption'] = $this->_data->imagecaption;
+                    }
+                }
+            }
+
+            // For issuer.
+            if ($type == OPEN_BADGES_V2_TYPE_ISSUER) {
+                $json['@context'] = OPEN_BADGES_V2_CONTEXT;
+                $json['id'] = $issuerurl->out(false);
+                $json['type'] = OPEN_BADGES_V2_TYPE_ISSUER;
+            }
+        }
+    }
 }
index 1b198cb..ab3c83d 100644 (file)
@@ -24,6 +24,7 @@
  */
 
 namespace core_badges\privacy;
+
 defined('MOODLE_INTERNAL') || die();
 
 use badge;
@@ -37,6 +38,8 @@ use core_privacy\local\metadata\collection;
 use core_privacy\local\request\approved_contextlist;
 use core_privacy\local\request\transform;
 use core_privacy\local\request\writer;
+use core_privacy\local\request\userlist;
+use core_privacy\local\request\approved_userlist;
 
 require_once($CFG->libdir . '/badgeslib.php');
 
@@ -50,6 +53,7 @@ require_once($CFG->libdir . '/badgeslib.php');
  */
 class provider implements
     \core_privacy\local\metadata\provider,
+    \core_privacy\local\request\core_userlist_provider,
     \core_privacy\local\request\subsystem\provider {
 
     /**
@@ -173,6 +177,78 @@ class provider implements
         return $contextlist;
     }
 
+    /**
+     * Get the list of users within a specific context.
+     *
+     * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
+     */
+    public static function get_users_in_context(userlist $userlist) {
+        $context = $userlist->get_context();
+
+        $allowedcontexts = [
+            CONTEXT_COURSE,
+            CONTEXT_SYSTEM,
+            CONTEXT_USER
+        ];
+
+        if (!in_array($context->contextlevel, $allowedcontexts)) {
+            return;
+        }
+
+        if ($context->contextlevel == CONTEXT_COURSE || $context->contextlevel == CONTEXT_SYSTEM) {
+            // Find the modifications we made on badges (course & system).
+            $params = [
+                'courselevel' => CONTEXT_COURSE,
+                'syscontextid' => SYSCONTEXTID,
+                'typecourse' => BADGE_TYPE_COURSE,
+                'typesite' => BADGE_TYPE_SITE,
+                'contextid' => $context->id,
+            ];
+
+            $sql = "SELECT b.usermodified, b.usercreated
+                      FROM {badge} b
+                      JOIN {context} ctx
+                           ON (b.type = :typecourse AND b.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel)
+                           OR (b.type = :typesite AND ctx.id = :syscontextid)
+                     WHERE ctx.id = :contextid";
+
+            $userlist->add_from_sql('usermodified', $sql, $params);
+            $userlist->add_from_sql('usercreated', $sql, $params);
+        }
+
+        if ($context->contextlevel == CONTEXT_USER) {
+            // Find where we've manually awarded a badge (recipient user context).
+            $params = [
+                'instanceid' => $context->instanceid
+            ];
+
+            $sql = "SELECT issuerid, recipientid
+                      FROM {badge_manual_award}
+                     WHERE recipientid = :instanceid";
+
+            $userlist->add_from_sql('issuerid', $sql, $params);
+            $userlist->add_from_sql('recipientid', $sql, $params);
+
+            $sql = "SELECT userid
+                      FROM {badge_issued}
+                     WHERE userid = :instanceid";
+
+            $userlist->add_from_sql('userid', $sql, $params);
+
+            $sql = "SELECT userid
+                      FROM {badge_criteria_met}
+                     WHERE userid = :instanceid";
+
+            $userlist->add_from_sql('userid', $sql, $params);
+
+            $sql = "SELECT userid
+                      FROM {badge_backpack}
+                     WHERE userid = :instanceid";
+
+            $userlist->add_from_sql('userid', $sql, $params);
+        }
+    }
+
     /**
      * Export all user data for the specified user, in the specified contexts.
      *
@@ -439,6 +515,24 @@ class provider implements
         static::delete_user_data($context->instanceid);
     }
 
+    /**
+     * Delete multiple users within a single context.
+     *
+     * @param approved_userlist $userlist The approved context and user information to delete information for.
+     */
+    public static function delete_data_for_users(approved_userlist $userlist) {
+        $context = $userlist->get_context();
+
+        if (!in_array($context->instanceid, $userlist->get_userids())) {
+            return;
+        }
+
+        if ($context->contextlevel == CONTEXT_USER) {
+            // We can only delete our own data in the user context, nothing in course or system.
+            static::delete_user_data($context->instanceid);
+        }
+    }
+
     /**
      * Delete all user data for the specified user, in the specified contexts.
      *
diff --git a/badges/competency.php b/badges/competency.php
new file mode 100644 (file)
index 0000000..1eae429
--- /dev/null
@@ -0,0 +1,109 @@
+<?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/>.
+/**
+ * List competencies, skills, or standards are targeted by a BadgeClass.
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2018 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+require_once($CFG->dirroot . '/badges/competency_form.php');
+
+$badgeid = required_param('id', PARAM_INT);
+$alignmentid = optional_param('alignmentid', 0, PARAM_INT);
+$action = optional_param('action', '', PARAM_TEXT);
+$lang = current_language();
+
+require_login();
+if (empty($CFG->enablebadges)) {
+    print_error('badgesdisabled', 'badges');
+}
+$badge = new badge($badgeid);
+$context = $badge->get_context();
+$navurl = new moodle_url('/badges/index.php', array('type' => $badge->type));
+require_capability('moodle/badges:configuredetails', $context);
+
+if ($badge->type == BADGE_TYPE_COURSE) {
+    if (empty($CFG->badges_allowcoursebadges)) {
+        print_error('coursebadgesdisabled', 'badges');
+    }
+    require_login($badge->courseid);
+    $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type, 'id' => $badge->courseid));
+    $PAGE->set_pagelayout('standard');
+    navigation_node::override_active_url($navurl);
+} else {
+    $PAGE->set_pagelayout('admin');
+    navigation_node::override_active_url($navurl, true);
+}
+
+$currenturl = new moodle_url('/badges/competency.php', array('id' => $badge->id));
+$PAGE->set_context($context);
+$PAGE->set_url($currenturl);
+$PAGE->set_heading($badge->name);
+$PAGE->set_title($badge->name);
+$PAGE->navbar->add($badge->name);
+
+$output = $PAGE->get_renderer('core', 'badges');
+$msg = optional_param('msg', '', PARAM_TEXT);
+$emsg = optional_param('emsg', '', PARAM_TEXT);
+$url = new moodle_url('/badges/competency.php', array('id' => $badge->id, 'action' => $action, 'alignmentid' => $alignmentid));
+$mform = new competency_alignment_form($url, array('badge' => $badge, 'action' => $action, 'alignmentid' => $alignmentid));
+if ($mform->is_cancelled()) {
+    redirect($currenturl);
+} else if ($mform->is_submitted() && $mform->is_validated() && ($data = $mform->get_data())) {
+    $alignment = new stdClass();
+    $alignment->badgeid = $badgeid;
+    $alignment->targetname = $data->targetname;
+    $alignment->targeturl = $data->targeturl;
+    $alignment->targetframework = $data->targetframework;
+    $alignment->targetcode = $data->targetcode;
+    $alignment->targetdescription = trim($data->targetdescription);
+    $badge->save_alignment($alignment, $alignmentid);
+    redirect($currenturl);
+}
+
+echo $OUTPUT->header();
+echo $OUTPUT->heading(print_badge_image($badge, $context, 'small') . ' ' . $badge->name);
+echo $output->print_badge_status_box($badge);
+$output->print_badge_tabs($badgeid, $context, 'alignment');
+if ($emsg !== '') {
+    echo $OUTPUT->notification($emsg);
+} else if ($msg !== '') {
+    echo $OUTPUT->notification(get_string($msg, 'badges'), 'notifysuccess');
+}
+echo $output->notification(get_string('notealignment', 'badges'), 'info');
+
+if ($alignmentid || $action == 'add' || $action == 'edit') {
+    $mform->display();
+} else if (empty($action)) {
+    if (!$badge->is_active() && !$badge->is_locked()) {
+        $urlbutton = new moodle_url('/badges/competency.php', array('id' => $badge->id, 'action' => 'add'));
+        echo $OUTPUT->box($OUTPUT->single_button($urlbutton, get_string('addalignment', 'badges')), 'clearfix mdl-align');
+    }
+    $alignment = $badge->get_alignment();
+    if (count($alignment) > 0) {
+        $renderrelated = new badge_competencies_alignment($alignment, $badgeid);
+        echo $output->render($renderrelated);
+    } else {
+        echo $output->notification(get_string('noalignment', 'badges'));
+    }
+}
+
+echo $OUTPUT->footer();
diff --git a/badges/competency_action.php b/badges/competency_action.php
new file mode 100644 (file)
index 0000000..f56da92
--- /dev/null
@@ -0,0 +1,41 @@
+<?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/>.
+/**
+ * Processing actions with competencies alignment.
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2018 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+
+$alignmentid = required_param('alignmentid', PARAM_INT); // Related badge ID.
+$badgeid = required_param('id', PARAM_INT); // Badge ID.
+$action = optional_param('action', 'remove', PARAM_TEXT); // Remove.
+
+require_login();
+$return = new moodle_url('/badges/competency.php', array('id' => $badgeid));
+$badge = new badge($badgeid);
+$context = $badge->get_context();
+require_capability('moodle/badges:configuredetails', $context);
+
+if ($action == 'remove') {
+    $badge->delete_alignment($alignmentid);
+}
+redirect($return);
\ No newline at end of file
diff --git a/badges/competency_form.php b/badges/competency_form.php
new file mode 100644 (file)
index 0000000..7150eb9
--- /dev/null
@@ -0,0 +1,79 @@
+<?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/>.
+/**
+ * Form competency for editing.
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2018 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/formslib.php');
+require_once($CFG->libdir . '/badgeslib.php');
+
+/**
+ * Form to edit competency alignment.
+ *
+ * @copyright  2018 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+class competency_alignment_form extends moodleform {
+
+    /**
+     * Defines the form.
+     */
+    public function definition() {
+        global $DB;
+        $mform = $this->_form;
+        $badge = $this->_customdata['badge'];
+        $action = $this->_customdata['action'];
+        $alignmentid = $this->_customdata['alignmentid'];
+        $mform->addElement('header', 'alignment', get_string('alignment', 'badges'));
+        $mform->addElement('text', 'targetname', get_string('targetname', 'badges'), array('size' => '70'));
+        $mform->setType('targetname', PARAM_TEXT);
+        $mform->addRule('targetname', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
+        $mform->addRule('targetname', null, 'required');
+        $mform->addHelpButton('targetname', 'targetname', 'badges');
+        $mform->addElement('text', 'targeturl', get_string('targeturl', 'badges'), array('size' => '70'));
+        $mform->setType('targeturl', PARAM_URL);
+        $mform->addRule('targeturl', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
+        $mform->addRule('targeturl', null, 'required');
+        $mform->addHelpButton('targeturl', 'targeturl', 'badges');
+        $mform->addElement('text', 'targetframework', get_string('targetframework', 'badges'), array('size' => '70'));
+        $mform->setType('targetframework', PARAM_TEXT);
+        $mform->addRule('targetframework', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
+        $mform->addHelpButton('targetframework', 'targetframework', 'badges');
+        $mform->addElement('text', 'targetcode', get_string('targetcode', 'badges'), array('size' => '70'));
+        $mform->setType('targetcode', PARAM_TEXT);
+        $mform->addRule('targetcode', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
+        $mform->addHelpButton('targetcode', 'targetcode', 'badges');
+        $mform->addElement('textarea', 'targetdescription', get_string('targetdescription', 'badges'),
+            'wrap="virtual" rows="8" cols="70"');
+        $this->add_action_buttons();
+        if ($action == 'edit' || $alignmentid) {
+            $alignment = new stdClass();
+            $alignment = $DB->get_record_select('badge_competencies', 'id = ?', array($alignmentid));
+            $this->set_data($alignment);
+            // Freeze all elements if badge is active or locked.
+            if ($badge->is_active() || $badge->is_locked()) {
+                $mform->hardFreezeAllVisibleExcept(array());
+            }
+        }
+    }
+}
\ No newline at end of file
index c89aba4..2521dc1 100644 (file)
@@ -84,15 +84,21 @@ $editoroptions = array(
         );
 $badge = file_prepare_standard_editor($badge, 'message', $editoroptions, $context);
 
-$form_class = 'edit_' . $action . '_form';
-$form = new $form_class($currenturl, array('badge' => $badge, 'action' => $action, 'editoroptions' => $editoroptions));
+$formclass = 'edit_' . $action . '_form';
+$form = new $formclass($currenturl, array('badge' => $badge, 'action' => $action, 'editoroptions' => $editoroptions));
 
 if ($form->is_cancelled()) {
     redirect(new moodle_url('/badges/overview.php', array('id' => $badgeid)));
 } else if ($form->is_submitted() && $form->is_validated() && ($data = $form->get_data())) {
     if ($action == 'details') {
         $badge->name = $data->name;
+        $badge->version = trim($data->version);
+        $badge->language = $data->language;
         $badge->description = $data->description;
+        $badge->imageauthorname = $data->imageauthorname;
+        $badge->imageauthoremail = $data->imageauthoremail;
+        $badge->imageauthorurl = $data->imageauthorurl;
+        $badge->imagecaption = $data->imagecaption;
         $badge->usermodified = $USER->id;
         $badge->issuername = $data->issuername;
         $badge->issuerurl = $data->issuerurl;
index 1c2a40a..c21bf5a 100644 (file)
@@ -45,6 +45,7 @@ class edit_details_form extends moodleform {
         $mform = $this->_form;
         $badge = (isset($this->_customdata['badge'])) ? $this->_customdata['badge'] : false;
         $action = $this->_customdata['action'];
+        $languages = get_string_manager()->get_list_of_languages();
 
         $mform->addElement('header', 'badgedetails', get_string('badgedetails', 'badges'));
         $mform->addElement('text', 'name', get_string('name'), array('size' => '70'));
@@ -53,6 +54,12 @@ class edit_details_form extends moodleform {
         $mform->addRule('name', null, 'required');
         $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
 
+        $mform->addElement('text', 'version', get_string('version', 'badges'), array('size' => '70'));
+        $mform->setType('version', PARAM_TEXT);
+        $mform->addHelpButton('version', 'version', 'badges');
+        $mform->addElement('select', 'language', get_string('language'), $languages);
+        $mform->addHelpButton('language', 'language', 'badges');
+
         $mform->addElement('textarea', 'description', get_string('description', 'badges'), 'wrap="virtual" rows="8" cols="70"');
         $mform->setType('description', PARAM_NOTAGS);
         $mform->addRule('description', null, 'required');
@@ -68,6 +75,18 @@ class edit_details_form extends moodleform {
             $mform->insertElementBefore($currentimage, 'image');
         }
         $mform->addHelpButton('image', 'badgeimage', 'badges');
+        $mform->addElement('text', 'imageauthorname', get_string('imageauthorname', 'badges'), array('size' => '70'));
+        $mform->setType('imageauthorname', PARAM_TEXT);
+        $mform->addHelpButton('imageauthorname', 'imageauthorname', 'badges');
+        $mform->addElement('text', 'imageauthoremail', get_string('imageauthoremail', 'badges'), array('size' => '70'));
+        $mform->setType('imageauthoremail', PARAM_TEXT);
+        $mform->addHelpButton('imageauthoremail', 'imageauthoremail', 'badges');
+        $mform->addElement('text', 'imageauthorurl', get_string('imageauthorurl', 'badges'), array('size' => '70'));
+        $mform->setType('imageauthorurl', PARAM_URL);
+        $mform->addHelpButton('imageauthorurl', 'imageauthorurl', 'badges');
+        $mform->addElement('text', 'imagecaption', get_string('imagecaption', 'badges'), array('size' => '70'));
+        $mform->setType('imagecaption', PARAM_TEXT);
+        $mform->addHelpButton('imagecaption', 'imagecaption', 'badges');
 
         $mform->addElement('header', 'issuerdetails', get_string('issuerdetails', 'badges'));
 
@@ -118,6 +137,7 @@ class edit_details_form extends moodleform {
         $mform->setType('action', PARAM_TEXT);
 
         if ($action == 'new') {
+            $mform->setDefault('language', $CFG->lang);
             $this->add_action_buttons(true, get_string('createbutton', 'badges'));
         } else {
             // Add hidden fields.
@@ -174,6 +194,10 @@ class edit_details_form extends moodleform {
             $errors['expirydategr'] = get_string('error:invalidexpiredate', 'badges');
         }
 
+        if ($data['imageauthoremail'] && !validate_email($data['imageauthoremail'])) {
+            $errors['imageauthoremail'] = get_string('invalidemail');
+        }
+
         // Check for duplicate badge names.
         if ($data['action'] == 'new') {
             $duplicate = $DB->record_exists_select('badge', 'name = :name AND status != :deleted',
diff --git a/badges/endorsement.php b/badges/endorsement.php
new file mode 100644 (file)
index 0000000..ec3e145
--- /dev/null
@@ -0,0 +1,100 @@
+<?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/>.
+/**
+ * Endorsement information
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2018 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+require_once($CFG->dirroot . '/badges/endorsement_form.php');
+
+$badgeid = required_param('id', PARAM_INT);
+
+require_login();
+
+if (empty($CFG->enablebadges)) {
+    print_error('badgesdisabled', 'badges');
+}
+
+$badge = new badge($badgeid);
+$context = $badge->get_context();
+$navurl = new moodle_url('/badges/index.php', array('type' => $badge->type));
+require_capability('moodle/badges:configuredetails', $context);
+
+if ($badge->type == BADGE_TYPE_COURSE) {
+    if (empty($CFG->badges_allowcoursebadges)) {
+        print_error('coursebadgesdisabled', 'badges');
+    }
+    require_login($badge->courseid);
+    $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type, 'id' => $badge->courseid));
+    $PAGE->set_pagelayout('standard');
+    navigation_node::override_active_url($navurl);
+} else {
+    $PAGE->set_pagelayout('admin');
+    navigation_node::override_active_url($navurl, true);
+}
+
+$currenturl = new moodle_url('/badges/endorsement.php', array('id' => $badgeid));
+$PAGE->set_context($context);
+$PAGE->set_url($currenturl);
+$PAGE->set_heading($badge->name);
+$PAGE->set_title($badge->name);
+$PAGE->navbar->add($badge->name);
+
+$output = $PAGE->get_renderer('core', 'badges');
+$msg = optional_param('msg', '', PARAM_TEXT);
+$emsg = optional_param('emsg', '', PARAM_TEXT);
+
+echo $OUTPUT->header();
+echo $OUTPUT->heading(print_badge_image($badge, $context, 'small') . ' ' . $badge->name);
+
+echo $output->print_badge_status_box($badge);
+$output->print_badge_tabs($badgeid, $context, 'bendorsement');
+
+$form = new endorsement_form($currenturl, array('badge' => $badge));
+if ($form->is_cancelled()) {
+    redirect(new moodle_url('/badges/overview.php', array('id' => $badgeid)));
+} else if ($form->is_submitted() && $form->is_validated() && ($data = $form->get_data())) {
+    $endorsement = new stdClass();
+    $endorsement->badgeid = $badgeid;
+    $endorsement->issuername = $data->issuername;
+    $endorsement->issueremail = $data->issueremail;
+    $endorsement->issuerurl = $data->issuerurl;
+    $endorsement->claimid = $data->claimid;
+    $endorsement->claimcomment = strip_tags($data->claimcomment);
+    $endorsement->dateissued = $data->dateissued;
+
+    if ($badge->save_endorsement($endorsement)) {
+        $msg = get_string('changessaved');
+    } else {
+        $emsg = get_string('error:save', 'badges');
+    }
+}
+
+if ($emsg !== '') {
+    echo $OUTPUT->notification($emsg);
+
+} else if ($msg !== '') {
+    echo $OUTPUT->notification($msg, 'notifysuccess');
+}
+echo $output->notification(get_string('noteendorsement', 'badges'), 'info');
+$form->display();
+echo $OUTPUT->footer();
diff --git a/badges/endorsement_form.php b/badges/endorsement_form.php
new file mode 100644 (file)
index 0000000..f4bc231
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Form endorsement for editing.
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2018 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/formslib.php');
+require_once($CFG->libdir . '/badgeslib.php');
+/**
+ * Form to edit endorsement.
+ *
+ * @copyright  2018 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai
+ */
+class endorsement_form extends moodleform {
+
+    /**
+     * Defines the form.
+     */
+    public function definition() {
+        $mform = $this->_form;
+        $badge = $this->_customdata['badge'];
+        $mform->addElement('header', 'endorsement', get_string('issuerdetails', 'badges'));
+        $mform->addElement('text', 'issuername', get_string('issuername_endorsement', 'badges'), array('size' => '70'));
+        $mform->setType('issuername', PARAM_TEXT);
+        $mform->addRule('issuername', null, 'required');
+        $mform->addHelpButton('issuername', 'issuername_endorsement', 'badges');
+        $mform->addElement('text', 'issueremail', get_string('issueremail', 'badges'), array('size' => '70'));
+        $mform->addRule('issueremail', null, 'required');
+        $mform->setType('issueremail', PARAM_RAW);
+        $mform->addHelpButton('issueremail', 'issueremail', 'badges');
+        $mform->addElement('text', 'issuerurl', get_string('issuerurl', 'badges'), array('size' => '70'));
+        $mform->setType('issuerurl', PARAM_URL);
+        $mform->addRule('issuerurl', null, 'required');
+        $mform->addHelpButton('issuerurl', 'issuerurl', 'badges');
+        $mform->addElement('date_time_selector', 'dateissued',
+            get_string('dateawarded', 'badges'));
+        $mform->addElement('header', 'claim', get_string('claim', 'badges'));
+        $mform->addElement('text', 'claimid', get_string('claimid', 'badges'), array('size' => '70'));
+        $mform->setType('claimid', PARAM_URL);
+        $mform->addRule('claimid', null, 'required');
+        $mform->addElement('textarea', 'claimcomment', get_string('claimcomment', 'badges'), 'wrap="virtual" rows="8" cols="70"');
+        $mform->setType('claimcomment', PARAM_NOTAGS);
+        $endorsement = new stdClass();
+        $endorsement = $badge->get_endorsement();
+        if ($endorsement) {
+            $mform->setDefault('dateissued', $endorsement->dateissued);
+            $this->set_data($endorsement);
+        }
+        $this->add_action_buttons();
+        // Freeze all elements if badge is active or locked.
+        if ($badge->is_active() || $badge->is_locked()) {
+            $mform->hardFreezeAllVisibleExcept(array());
+        }
+    }
+
+    /**
+     * Validates form data.
+     *
+     * @param array $data submitted data.
+     * @param array $files submitted files.
+     * @return array $errors An array of errors.
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+        if ($data['issueremail'] && !validate_email($data['issueremail'])) {
+            $errors['issueremail'] = get_string('invalidemail');
+        }
+        return $errors;
+    }
+}
\ No newline at end of file
diff --git a/badges/endorsement_json.php b/badges/endorsement_json.php
new file mode 100644 (file)
index 0000000..9ecd3ac
--- /dev/null
@@ -0,0 +1,69 @@
+<?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/>.
+/**
+ * Serve endorsement JSON for assertion.
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2018 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+define('AJAX_SCRIPT', true);
+define('NO_MOODLE_COOKIES', true); // No need for a session here.
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+
+if (empty($CFG->enablebadges)) {
+    print_error('badgesdisabled', 'badges');
+}
+
+global $DB;
+$id = required_param('id', PARAM_INT);
+$action = optional_param('action', null, PARAM_BOOL); // Generates endorsement issuer class if true.
+$badge = new badge($id);
+
+$json = array();
+$endorsement = $badge->get_endorsement();
+$endorsementurl = new moodle_url('/badges/endorsement_json.php', array('id' => $id));
+
+if ($endorsement) {
+    $issuer = array();
+    $issuerurl = new moodle_url('/badges/endorsement_json.php', array('id' => $id, 'action' => 1));
+    $issuer['id'] = $issuerurl->out(false);
+    $issuer['name'] = $endorsement->issuername;
+    $issuer['email'] = $endorsement->issueremail;
+    $issuer['url'] = $endorsement->issuerurl;
+    if ($action) {
+        $json = $issuer;
+    } else {
+        $json['@context'] = OPEN_BADGES_V2_CONTEXT;
+        $json['type'] = OPEN_BADGES_V2_TYPE_ENDORSEMENT;
+        $json['id'] = $endorsementurl->out(false);
+        $json['issuer'] = $issuer;
+        if (!empty($endorsement->claimcomment)) {
+            $json['claim']['id'] = $endorsement->claimid;
+            $json['claim']['endorsementComment'] = $endorsement->claimcomment;
+        } else {
+            $json['claim'] = $endorsement->claimid;
+        }
+        $json['issuedOn'] = date('c', $endorsement->dateissued);
+        $json['verification'] = array('type' => 'hosted');
+    }
+}
+
+echo $OUTPUT->header();
+echo json_encode($json);
diff --git a/badges/image_author_json.php b/badges/image_author_json.php
new file mode 100644 (file)
index 0000000..1cfb92e
--- /dev/null
@@ -0,0 +1,52 @@
+<?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/>.
+/**
+ * Serve profile image author JSON for assertion.
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2018 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+
+define('AJAX_SCRIPT', true);
+define('NO_MOODLE_COOKIES', true); // No need for a session here.
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+
+if (empty($CFG->enablebadges)) {
+    print_error('badgesdisabled', 'badges');
+}
+
+$id = required_param('id', PARAM_INT); // Unique hash of badge assertion.
+$badge = new badge($id);
+
+$json = array();
+$authorimage = new moodle_url('/badges/image_author_json.php', array('id' => $badge->id));
+$json['id'] = $authorimage->out(false);
+$json['type'] = OPEN_BADGES_V2_TYPE_AUTHOR;
+if (!empty($badge->imageauthorname)) {
+    $json['name'] = $badge->imageauthorname;
+}
+if (!empty($badge->imageauthoremail)) {
+    $json['email'] = $badge->imageauthoremail;
+}
+if (!empty($badge->imageauthorurl)) {
+    $json['url'] = $badge->imageauthorurl;
+}
+echo $OUTPUT->header();
+echo json_encode($json);
\ No newline at end of file
index 60f3951..45081bc 100644 (file)
@@ -77,7 +77,13 @@ if ($form->is_cancelled()) {
     $now = time();
 
     $fordb->name = $data->name;
+    $fordb->version = $data->version;
+    $fordb->language = $data->language;
     $fordb->description = $data->description;
+    $fordb->imageauthorname = $data->imageauthorname;
+    $fordb->imageauthoremail = $data->imageauthoremail;
+    $fordb->imageauthorurl = $data->imageauthorurl;
+    $fordb->imagecaption = $data->imagecaption;
     $fordb->timecreated = $now;
     $fordb->timemodified = $now;
     $fordb->usercreated = $USER->id;
diff --git a/badges/related.php b/badges/related.php
new file mode 100644 (file)
index 0000000..d8af01b
--- /dev/null
@@ -0,0 +1,106 @@
+<?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/>.
+
+/**
+ * Related badges information
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2018 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+require_once($CFG->dirroot . '/badges/related_form.php');
+
+$badgeid = required_param('id', PARAM_INT);
+$action = optional_param('action', null, PARAM_TEXT);
+$lang = current_language();
+
+require_login();
+
+if (empty($CFG->enablebadges)) {
+    print_error('badgesdisabled', 'badges');
+}
+
+$badge = new badge($badgeid);
+$context = $badge->get_context();
+$navurl = new moodle_url('/badges/index.php', array('type' => $badge->type));
+require_capability('moodle/badges:configuredetails', $context);
+
+if ($badge->type == BADGE_TYPE_COURSE) {
+    if (empty($CFG->badges_allowcoursebadges)) {
+        print_error('coursebadgesdisabled', 'badges');
+    }
+    require_login($badge->courseid);
+    $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type, 'id' => $badge->courseid));
+    $PAGE->set_pagelayout('standard');
+    navigation_node::override_active_url($navurl);
+} else {
+    $PAGE->set_pagelayout('admin');
+    navigation_node::override_active_url($navurl, true);
+}
+
+$currenturl = new moodle_url('/badges/related.php', array('id' => $badge->id));
+$PAGE->set_context($context);
+$PAGE->set_url($currenturl);
+$PAGE->set_heading($badge->name);
+$PAGE->set_title($badge->name);
+$PAGE->navbar->add($badge->name);
+$output = $PAGE->get_renderer('core', 'badges');
+$msg = optional_param('msg', '', PARAM_TEXT);
+$emsg = optional_param('emsg', '', PARAM_TEXT);
+$url = new moodle_url('/badges/related.php', array('id' => $badge->id, 'action' => 'add'));
+
+$mform = new edit_relatedbadge_form($url, array('badge' => $badge));
+if ($mform->is_cancelled()) {
+    redirect($currenturl);
+} else if ($mform->is_submitted() && $mform->is_validated() && ($data = $mform->get_data())) {
+
+    if (isset($data->relatedbadgeids)) {
+        $badge->add_related_badges($data->relatedbadgeids);
+    }
+    redirect($currenturl);
+}
+echo $OUTPUT->header();
+echo $OUTPUT->heading(print_badge_image($badge, $context, 'small') . ' ' . $badge->name);
+echo $output->print_badge_status_box($badge);
+
+$output->print_badge_tabs($badgeid, $context, 'brelated');
+if ($emsg !== '') {
+    echo $OUTPUT->notification($emsg);
+} else if ($msg !== '') {
+    echo $OUTPUT->notification(get_string($msg, 'badges'), 'notifysuccess');
+}
+
+echo $output->notification(get_string('noterelated', 'badges'), 'info');
+if (is_null($action)) {
+    if (!$badge->is_active() && !$badge->is_locked()) {
+        echo $OUTPUT->box($OUTPUT->single_button($url, get_string('addrelated', 'badges')), 'clearfix mdl-align');
+    }
+    if ($badge->has_related()) {
+        $badgerelated = $badge->get_related_badges();
+        $renderrelated = new badge_related($badgerelated, $badgeid);
+        echo $output->render($renderrelated);
+    } else {
+        echo $output->notification(get_string('norelated', 'badges'));
+    }
+} else if ($action == 'add') {
+    $mform->display();
+}
+echo $OUTPUT->footer();
diff --git a/badges/related_action.php b/badges/related_action.php
new file mode 100644 (file)
index 0000000..8011aed
--- /dev/null
@@ -0,0 +1,42 @@
+<?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/>.
+/**
+ * Action related badges.
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2018 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+
+$relatedid = optional_param('relatedid', 0, PARAM_INT); // Related badge ID.
+$badgeid = optional_param('badgeid', 0, PARAM_INT); // Badge ID.
+$action = optional_param('action', 'remove', PARAM_TEXT); // Add, remove option.
+
+require_login();
+$return = new moodle_url('/badges/related.php', array('id' => $badgeid));
+$badge = new badge($badgeid);
+$context = $badge->get_context();
+require_capability('moodle/badges:configuredetails', $context);
+
+if ($action == 'remove') {
+    $badge->delete_related_badge($relatedid);
+}
+
+redirect($return);
diff --git a/badges/related_form.php b/badges/related_form.php
new file mode 100644 (file)
index 0000000..8e7cd56
--- /dev/null
@@ -0,0 +1,107 @@
+<?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/>.
+/**
+ * Form related badges.
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2018 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/formslib.php');
+require_once($CFG->libdir . '/badgeslib.php');
+
+/**
+ * Form to edit badge details.
+ *
+ * @copyright 2018 Tung Thai
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+class edit_relatedbadge_form extends moodleform {
+
+    /**
+     * Defines the form.
+     */
+    public function definition() {
+        $mform = $this->_form;
+        $badge = $this->_customdata['badge'];
+        $badgesarray = $this->get_badges_option($badge);
+        $mform->addElement('header', 'alignment', get_string('relatedbages', 'badges'));
+        if (!$badgesarray) {
+            $badgesarray = array(get_string('none'));
+            $attributes = array('size' => '3', 'disabled' => true, 'style' => 'min-width: 200px');
+        } else {
+            $attributes = array('size' => '10');
+        }
+        $mform->addElement('select', 'relatedbadgeids', get_string('relatedbages', 'badges'), $badgesarray, $attributes);
+        $mform->getElement('relatedbadgeids')->setMultiple(true);
+        $this->add_action_buttons();
+
+        // Freeze all elements if badge is active or locked.
+        if ($badge->is_active() || $badge->is_locked()) {
+            $mform->hardFreezeAllVisibleExcept(array());
+        }
+    }
+
+    /**
+     * Validates form data.
+     *
+     * @param array $data submitted data.
+     * @param array $files submitted files.
+     * @return array $errors An array of errors.
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+        return $errors;
+    }
+
+    /**
+     * Return list badge of a course or list badge site.
+     *
+     * @param object $badge Badge object.
+     * @return array $options An array the badges.
+     */
+    public function get_badges_option($badge) {
+        global $DB;
+        $sql = "SELECT b.id, b.name, b.version, b.language, b.type
+                  FROM {badge} b
+                 WHERE b.id <> :badgeid
+                       AND b.id NOT IN (SELECT br.relatedbadgeid
+                  FROM {badge_related} br WHERE br.badgeid = :badgeid1)";
+        $params = array('badgeid' => $badge->id, 'badgeid1' => $badge->id);
+        if ($badge->type == BADGE_TYPE_COURSE) {
+            $sql .= " AND (b.courseid = :courseid OR b.type = :badgetype)";
+            $params['courseid'] = $badge->courseid;
+            $params['badgetype'] = BADGE_TYPE_SITE;
+        }
+
+        $records = $DB->get_records_sql($sql, $params);
+        $languages = get_string_manager()->get_list_of_languages();
+        $options = array();
+        foreach ($records as $record) {
+            $language = isset($languages[$record->language]) ? $languages[$record->language] : '';
+            $options[$record->id] = $record->name .
+                ' (version: ' . $record->version . ', language: ' . $language . ', ' .
+                ($record->type == BADGE_TYPE_COURSE ? get_string('badgesview', 'badges') : get_string('sitebadges', 'badges')) .
+                ')';
+        }
+        return $options;
+    }
+
+}
index 175cea9..5810e57 100644 (file)
@@ -139,15 +139,24 @@ class core_badges_renderer extends plugin_renderer_base {
     // Prints a badge overview infomation.
     public function print_badge_overview($badge, $context) {
         $display = "";
+        $languages = get_string_manager()->get_list_of_languages();
 
         // Badge details.
-
         $display .= $this->heading(get_string('badgedetails', 'badges'), 3);
         $dl = array();
         $dl[get_string('name')] = $badge->name;
+        $dl[get_string('version', 'badges')] = $badge->version;
+        $dl[get_string('language')] = $languages[$badge->language];
         $dl[get_string('description', 'badges')] = $badge->description;
         $dl[get_string('createdon', 'search')] = userdate($badge->timecreated);
         $dl[get_string('badgeimage', 'badges')] = print_badge_image($badge, $context, 'large');
+        $dl[get_string('imageauthorname', 'badges')] =
+            html_writer::link($badge->imageauthorname, $badge->imageauthorname, array('target' => '_blank'));
+        $dl[get_string('imageauthoremail', 'badges')] =
+            html_writer::tag('a', $badge->imageauthoremail, array('href' => 'mailto:' . $badge->imageauthoremail));
+        $dl[get_string('imageauthorurl', 'badges')] =
+            html_writer::link($badge->imageauthorurl, $badge->imageauthorurl, array('target' => '_blank'));
+        $dl[get_string('imagecaption', 'badges')] = $badge->imagecaption;
         $display .= $this->definition_list($dl);
 
         // Issuer details.
@@ -212,6 +221,10 @@ class core_badges_renderer extends plugin_renderer_base {
             }
         }
 
+        $display .= self::print_badge_endorsement($badge);
+        $display .= self::print_badge_related($badge);
+        $display .= self::print_badge_competencies($badge);
+
         return html_writer::div($display, null, array('id' => 'badge-overview'));
     }
 
@@ -277,11 +290,13 @@ class core_badges_renderer extends plugin_renderer_base {
         $badge = new badge($ibadge->badgeid);
         $now = time();
         $expiration = isset($issued['expires']) ? $issued['expires'] : $now + 86400;
+        $badgeimage = is_array($badgeclass['image']) ? $badgeclass['image']['id'] : $badgeclass['image'];
+        $languages = get_string_manager()->get_list_of_languages();
 
         $output = '';
         $output .= html_writer::start_tag('div', array('id' => 'badge'));
         $output .= html_writer::start_tag('div', array('id' => 'badge-image'));
-        $output .= html_writer::empty_tag('img', array('src' => $badgeclass['image'], 'alt' => $badge->name));
+        $output .= html_writer::empty_tag('img', array('src' => $badgeimage, 'alt' => $badge->name));
         if ($expiration < $now) {
             $output .= $this->output->pix_icon('i/expired',
             get_string('expireddate', 'badges', userdate($issued['expires'])),
@@ -334,7 +349,16 @@ class core_badges_renderer extends plugin_renderer_base {
         $output .= $this->output->heading(get_string('badgedetails', 'badges'), 3);
         $dl = array();
         $dl[get_string('name')] = $badge->name;
+        $dl[get_string('version', 'badges')] = $badge->version;
+        $dl[get_string('language')] = $languages[$badge->language];
         $dl[get_string('description', 'badges')] = $badge->description;
+        $dl[get_string('imageauthorname', 'badges')] =
+            html_writer::link($badge->imageauthorname, $badge->imageauthorname, array('target' => '_blank'));
+        $dl[get_string('imageauthoremail', 'badges')] =
+            html_writer::tag('a', $badge->imageauthoremail, array('href' => 'mailto:' . $badge->imageauthoremail));
+        $dl[get_string('imageauthorurl', 'badges')] =
+            html_writer::link($badge->imageauthorurl, $badge->imageauthorurl, array('target' => '_blank'));
+        $dl[get_string('imagecaption', 'badges')] = $badge->imagecaption;
 
         if ($badge->type == BADGE_TYPE_COURSE && isset($badge->courseid)) {
             $coursename = $DB->get_field('course', 'fullname', array('id' => $badge->courseid));
@@ -345,8 +369,12 @@ class core_badges_renderer extends plugin_renderer_base {
 
         $output .= $this->output->heading(get_string('issuancedetails', 'badges'), 3);
         $dl = array();
+        $issued['issuedOn'] = !preg_match( '~^[1-9][0-9]*$~', $issued['issuedOn'] ) ?
+            strtotime($issued['issuedOn']) : $issued['issuedOn'];
         $dl[get_string('dateawarded', 'badges')] = userdate($issued['issuedOn']);
         if (isset($issued['expires'])) {
+            $issued['expires'] = !preg_match( '~^[1-9][0-9]*$~', $issued['expires'] ) ?
+                strtotime($issued['expires']) : $issued['expires'];
             if ($issued['expires'] < $now) {
                 $dl[get_string('expirydate', 'badges')] = userdate($issued['expires']) . get_string('warnexpired', 'badges');
 
@@ -377,6 +405,29 @@ class core_badges_renderer extends plugin_renderer_base {
 
         $dl[get_string('evidence', 'badges')] = get_string('completioninfo', 'badges') . html_writer::alist($items, array(), 'ul');
         $output .= $this->definition_list($dl);
+        $output .= self::print_badge_endorsement($badge);
+        $relatedbadges = $badge->get_related_badges();
+        $output .= $this->heading(get_string('relatedbages', 'badges'), 3);
+        if (!empty($relatedbadges)) {
+            $items = array();
+            foreach ($relatedbadges as $related) {
+                $items[] = $related->name;
+            }
+            $output .= html_writer::alist($items, array(), 'ul');
+        } else {
+            $output .= get_string('norelated', 'badges');
+        }
+        $output .= $this->heading(get_string('alignment', 'badges'), 3);
+        $competencies = $badge->get_alignment();
+        if (!empty($competencies)) {
+            $items = array();
+            foreach ($competencies as $competency) {
+                $items[] = html_writer::link($competency->targeturl, $competency->targetname, array('target' => '_blank'));
+            }
+            $output .= html_writer::alist($items, array(), 'ul');
+        } else {
+            $output .= get_string('noalignment', 'badges');
+        }
         $output .= html_writer::end_tag('div');
 
         return $output;
@@ -627,6 +678,7 @@ class core_badges_renderer extends plugin_renderer_base {
     public function print_badge_tabs($badgeid, $context, $current = 'overview') {
         global $DB;
 
+        $badge = new badge($badgeid);
         $row = array();
 
         $row[] = new tabobject('overview',
@@ -665,6 +717,31 @@ class core_badges_renderer extends plugin_renderer_base {
                     );
         }
 
+        if (has_capability('moodle/badges:configuredetails', $context)) {
+            $row[] = new tabobject('bendorsement',
+                new moodle_url('/badges/endorsement.php', array('id' => $badgeid)),
+                get_string('bendorsement', 'badges')
+            );
+        }
+
+        if (has_capability('moodle/badges:configuredetails', $context)) {
+            $related = $DB->count_records_sql("SELECT COUNT(br.badgeid)
+                      FROM {badge_related} br WHERE br.badgeid = :badgeid", array('badgeid' => $badgeid));
+            $row[] = new tabobject('brelated',
+                new moodle_url('/badges/related.php', array('id' => $badgeid)),
+                get_string('brelated', 'badges', $related)
+            );
+        }
+
+        if (has_capability('moodle/badges:configuredetails', $context)) {
+            $competencies = $DB->count_records_sql("SELECT COUNT(bc.id)
+                      FROM {badge_competencies} bc WHERE bc.badgeid = :badgeid", array('badgeid' => $badgeid));
+            $row[] = new tabobject('balignment',
+                new moodle_url('/badges/competency.php', array('id' => $badgeid)),
+                get_string('balignment', 'badges', $competencies)
+            );
+        }
+
         echo $this->tabtree($row, $current);
     }
 
@@ -957,6 +1034,193 @@ class core_badges_renderer extends plugin_renderer_base {
         $output .= html_writer::end_tag('dl');
         return $output;
     }
+
+    /**
+     * Outputs list en badges.
+     *
+     * @param badge $badge Badge object.
+     * @return string $output content endorsement to output.
+     */
+    protected function print_badge_endorsement(badge $badge) {
+        $output = '';
+        $endorsement = $badge->get_endorsement();
+        $dl = array();
+        $output .= $this->heading(get_string('endorsement', 'badges'), 3);
+        if (!empty($endorsement)) {
+            $dl[get_string('issuername', 'badges')] = $endorsement->issuername;
+            $dl[get_string('issueremail', 'badges')] = $endorsement->issueremail;
+            $dl[get_string('issuerurl', 'badges')] = html_writer::link($endorsement->issuerurl, $endorsement->issuerurl,
+                array('target' => '_blank'));
+            $dl[get_string('dateawarded', 'badges')] = date('c', $endorsement->dateissued);
+            $dl[get_string('claimid', 'badges')] = html_writer::link($endorsement->claimid, $endorsement->claimid,
+            array('target' => '_blank'));
+            $dl[get_string('claimcomment', 'badges')] = $endorsement->claimcomment;
+            $output .= $this->definition_list($dl);
+        } else {
+            $output .= get_string('noendorsement', 'badges');
+        }
+        return $output;
+    }
+
+    /**
+     * Print list badges related.
+     *
+     * @param badge $badge Badge objects.
+     * @return string $output List related badges to output.
+     */
+    protected function print_badge_related(badge $badge) {
+        $output = '';
+        $relatedbadges = $badge->get_related_badges();
+        $output .= $this->heading(get_string('relatedbages', 'badges'), 3);
+        if (!empty($relatedbadges)) {
+            $items = array();
+            foreach ($relatedbadges as $related) {
+                $relatedurl = new moodle_url('/badges/overview.php', array('id' => $related->id));
+                $items[] = html_writer::link($relatedurl->out(), $related->name, array('target' => '_blank'));
+            }
+            $output .= html_writer::alist($items, array(), 'ul');
+        } else {
+            $output .= get_string('norelated', 'badges');
+        }
+        return $output;
+    }
+
+    /**
+     * Print list badge competencies.
+     *
+     * @param badge $badge Badge objects.
+     * @return string $output List competencies to output.
+     */
+    protected function print_badge_competencies(badge $badge) {
+        $output = '';
+        $output .= $this->heading(get_string('alignment', 'badges'), 3);
+        $competencies = $badge->get_alignment();
+        if (!empty($competencies)) {
+            $items = array();
+            foreach ($competencies as $competency) {
+                $urlaligment = new moodle_url('competency.php',
+                    array('id' => $badge->id, 'alignmentid' => $competency->id)
+                );
+                $items[] = html_writer::link($urlaligment, $competency->targetname, array('target' => '_blank'));
+            }
+            $output .= html_writer::alist($items, array(), 'ul');
+        } else {
+            $output .= get_string('noalignment', 'badges');
+        }
+        return $output;
+    }
+
+    /**
+     * Renders a table for related badges.
+     *
+     * @param badge_related $related list related badges.
+     * @return string list related badges to output.
+     */
+    protected function render_badge_related(badge_related $related) {
+        $currentbadge = new badge($related->currentbadgeid);
+        $languages = get_string_manager()->get_list_of_languages();
+        $paging = new paging_bar($related->totalcount, $related->page, $related->perpage, $this->page->url, 'page');
+        $htmlpagingbar = $this->render($paging);
+        $table = new html_table();
+        $table->attributes['class'] = 'generaltable boxaligncenter boxwidthwide';
+        $table->head = array(
+            get_string('name'),
+            get_string('version', 'badges'),
+            get_string('language', 'badges'),
+            get_string('type', 'badges')
+        );
+        if (!$currentbadge->is_active() && !$currentbadge->is_locked()) {
+            array_push($table->head, '');
+        }
+
+        foreach ($related->badges as $badge) {
+            $badgeobject = new badge($badge->id);
+            $style = array('title' => $badgeobject->name);
+            if (!$badgeobject->is_active()) {
+                $style['class'] = 'dimmed';
+            }
+            $context = ($badgeobject->type == BADGE_TYPE_SITE) ?
+                context_system::instance() : context_course::instance($badgeobject->courseid);
+            $forlink = print_badge_image($badgeobject, $context) . ' ' .
+                html_writer::start_tag('span') . $badgeobject->name . html_writer::end_tag('span');
+            $name = html_writer::link(new moodle_url('/badges/overview.php', array('id' => $badgeobject->id)), $forlink, $style);
+
+            $row = array(
+                $name,
+                $badge->version,
+                $badge->language ? $languages[$badge->language] : '',
+                $badge->type == BADGE_TYPE_COURSE ? get_string('badgesview', 'badges') : get_string('sitebadges', 'badges')
+            );
+            if (!$currentbadge->is_active() && !$currentbadge->is_locked()) {
+                $action = $this->output->action_icon(
+                    new moodle_url('related_action.php',
+                        array(
+                            'badgeid' => $related->currentbadgeid,
+                            'relatedid' => $badge->id,
+                            'action' => 'remove'
+                        )
+                    ), new pix_icon('t/delete', get_string('delete')));
+                $actions = html_writer::tag('div', $action, array('class' => 'badge-actions'));
+                array_push($row, $actions);
+            }
+            $table->data[] = $row;
+        }
+        $htmltable = html_writer::table($table);
+
+        return $htmlpagingbar . $htmltable . $htmlpagingbar;
+    }
+
+    /**
+     * Renders a table with competencies alignment.
+     *
+     * @param badge_competencies_alignment $alignment List competencies alignment.
+     * @return string List competencies aligment to output.
+     */
+    protected function render_badge_competencies_alignment(badge_competencies_alignment $alignment) {
+        $currentbadge = new badge($alignment->currentbadgeid);
+        $paging = new paging_bar($alignment->totalcount, $alignment->page, $alignment->perpage, $this->page->url, 'page');
+        $htmlpagingbar = $this->render($paging);
+        $table = new html_table();
+        $table->attributes['class'] = 'generaltable boxaligncenter boxwidthwide';
+        $table->head = array('Name', 'URL', '');
+
+        foreach ($alignment->alignments as $item) {
+            $urlaligment = new moodle_url('competency.php',
+                array(
+                    'id' => $currentbadge->id,
+                    'alignmentid' => $item->id,
+                )
+            );
+            $row = array(
+                html_writer::link($urlaligment, $item->targetname),
+                html_writer::link($item->targeturl, $item->targeturl, array('target' => '_blank'))
+            );
+            if (!$currentbadge->is_active() && !$currentbadge->is_locked()) {
+                $delete = $this->output->action_icon(
+                    new moodle_url('competency_action.php',
+                        array(
+                            'id' => $currentbadge->id,
+                            'alignmentid' => $item->id,
+                            'action' => 'remove'
+                        )
+                    ), new pix_icon('t/delete', get_string('delete')));
+                $edit = $this->output->action_icon(
+                    new moodle_url('competency.php',
+                        array(
+                            'id' => $currentbadge->id,
+                            'alignmentid' => $item->id,
+                            'action' => 'edit'
+                        )
+                    ), new pix_icon('t/edit', get_string('edit')));
+                $actions = html_writer::tag('div', $edit . $delete, array('class' => 'badge-actions'));
+                array_push($row, $actions);
+            }
+            $table->data[] = $row;
+        }
+        $htmltable = html_writer::table($table);
+
+        return $htmlpagingbar . $htmltable . $htmlpagingbar;
+    }
 }
 
 /**
@@ -1157,3 +1421,87 @@ class badge_user_collection extends badge_collection implements renderable {
         }
     }
 }
+
+/**
+ * Collection of all related badges.
+ *
+ * @copyright  2018 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+class badge_related implements renderable {
+
+    /** @var string how are the data sorted. */
+    public $sort = 'name';
+
+    /** @var string how are the data sorted. */
+    public $dir = 'ASC';
+
+    /** @var int page number to display. */
+    public $page = 0;
+
+    /** @var int number of badges to display per page. */
+    public $perpage = BADGE_PERPAGE;
+
+    /** @var int the total number of badges to display. */
+    public $totalcount = null;
+
+    /** @var int the current badge. */
+    public $currentbadgeid = 0;
+
+    /** @var array list of badges. */
+    public $badges = array();
+
+    /**
+     * Initializes the list of badges to display.
+     *
+     * @param array $badges related badges to render.
+     * @param int $currentbadgeid ID current badge.
+     */
+    public function __construct($badges, $currentbadgeid) {
+        $this->badges = $badges;
+        $this->currentbadgeid = $currentbadgeid;
+    }
+}
+
+/**
+ * Collection of all competencies.
+ *
+ * @copyright  2018 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+class badge_competencies_alignment implements renderable
+{
+    /** @var string how are the data sorted. */
+    public $sort = 'name';
+
+    /** @var string how are the data sorted. */
+    public $dir = 'ASC';
+
+    /** @var int page number to display. */
+    public $page = 0;
+
+    /** @var int number of badges to display per page. */
+    public $perpage = BADGE_PERPAGE;
+
+    /** @var int the total number of badges to display. */
+    public $totalcount = null;
+
+    /** @var array list of badges. */
+    public $alignments = array();
+
+    /** @var array list of badges. */
+    public $currentbadgeid = 0;
+
+    /**
+     * Initializes the list of competencies to display.
+     *
+     * @param array $alignments List competencies alignment to render.
+     * @param int $currentbadgeid ID current badge.
+     */
+    public function __construct($alignments, $currentbadgeid) {
+        $this->alignments = $alignments;
+        $this->currentbadgeid = $currentbadgeid;
+    }
+}
\ No newline at end of file
index 722a5d4..d577c78 100644 (file)
@@ -38,14 +38,14 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
     protected $coursebadge;
     protected $assertion;
 
+    /** @var $assertion2 to define json format for Open badge version 2 */
+    protected $assertion2;
+
     protected function setUp() {
         global $DB, $CFG;
         $this->resetAfterTest(true);
-
         $CFG->enablecompletion = true;
-
         $user = $this->getDataGenerator()->create_user();
-
         $fordb = new stdClass();
         $fordb->id = null;
         $fordb->name = "Test badge with 'apostrophe' and other friends (<>&@#)";
@@ -60,11 +60,17 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
         $fordb->expiredate = null;
         $fordb->expireperiod = null;
         $fordb->type = BADGE_TYPE_SITE;
+        $fordb->version = 1;
+        $fordb->language = 'en';
         $fordb->courseid = null;
         $fordb->messagesubject = "Test message subject";
         $fordb->message = "Test message body";
         $fordb->attachment = 1;
         $fordb->notification = 0;
+        $fordb->imageauthorname = "Image Author 1";
+        $fordb->imageauthoremail = "author@example.com";
+        $fordb->imageauthorurl = "http://author-url.example.com";
+        $fordb->imagecaption = "Test caption image";
         $fordb->status = BADGE_STATUS_INACTIVE;
 
         $this->badgeid = $DB->insert_record('badge', $fordb, true);
@@ -81,7 +87,6 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
         $maninstance = $DB->get_record('enrol', array('courseid' => $this->course->id, 'enrol' => 'manual'), '*', MUST_EXIST);
         $manplugin->enrol_user($maninstance, $this->user->id, $studentrole->id);
         $this->assertEquals(1, $DB->count_records('user_enrolments'));
-
         $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
         $this->module = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
 
@@ -89,12 +94,65 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
         $fordb->type = BADGE_TYPE_COURSE;
         $fordb->courseid = $this->course->id;
         $fordb->status = BADGE_STATUS_ACTIVE;
-
         $this->coursebadge = $DB->insert_record('badge', $fordb, true);
+
+        // Insert Endorsement.
+        $endorsement = new stdClass();
+        $endorsement->badgeid = $this->coursebadge;
+        $endorsement->issuername = "Issuer 123";
+        $endorsement->issueremail = "issuer123@email.com";
+        $endorsement->issuerurl = "https://example.org/issuer-123";
+        $endorsement->dateissued = 1524567747;
+        $endorsement->claimid = "https://example.org/robotics-badge.json";
+        $endorsement->claimcomment = "Test endorser comment";
+        $DB->insert_record('badge_endorsement', $endorsement, true);
+
+        // Insert related badges.
+        $badge = new badge($this->coursebadge);
+        $clonedid = $badge->make_clone();
+        $badgeclone = new badge($clonedid);
+        $badgeclone->status = BADGE_STATUS_ACTIVE;
+        $badgeclone->save();
+
+        $relatebadge = new stdClass();
+        $relatebadge->badgeid = $this->coursebadge;
+        $relatebadge->relatedbadgeid = $clonedid;
+        $relatebadge->relatedid = $DB->insert_record('badge_related', $relatebadge, true);
+
+        // Insert a competency aligment.
+        $competency = new stdClass();
+        $competency->badgeid = $this->coursebadge;
+        $competency->targetname = 'CCSS.ELA-Literacy.RST.11-12.3';
+        $competency->targeturl = 'http://www.corestandards.org/ELA-Literacy/RST/11-12/3';
+        $competency->targetdescription = 'Test target description';
+        $competency->targetframework = 'CCSS.RST.11-12.3';
+        $competency->targetcode = 'CCSS.RST.11-12.3';
+        $DB->insert_record('badge_competencies', $competency, true);
+
         $this->assertion = new stdClass();
         $this->assertion->badge = '{"uid":"%s","recipient":{"identity":"%s","type":"email","hashed":true,"salt":"%s"},"badge":"%s","verify":{"type":"hosted","url":"%s"},"issuedOn":"%d","evidence":"%s"}';
         $this->assertion->class = '{"name":"%s","description":"%s","image":"%s","criteria":"%s","issuer":"%s"}';
         $this->assertion->issuer = '{"name":"%s","url":"%s","email":"%s"}';
+        // Format JSON-LD for Openbadge specification version 2.0.
+        $this->assertion2 = new stdClass();
+        $this->assertion2->badge = '{"recipient":{"identity":"%s","type":"email","hashed":true,"salt":"%s"},' .
+            '"badge":{"name":"%s","description":"%s","image":{"id":"%s","author":"%s","caption":"%s"},' .
+            '"criteria":{"id":"%s","narrative":"%s"},"issuer":{"name":"%s","url":"%s","email":"%s",' .
+            '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"Issuer"},' .
+            '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"BadgeClass","version":"%s",' .
+            '"@language":"%s","related":[{"id":"%s","version":"%s","@language":"%s"}],"endorsement":"%s",' .
+            '"alignment":[{"targetName":"%s","targetUrl":"%s","targetDescription":"%s","targetFramework":"%s",' .
+            '"targetCode":"%s"}]},"verify":{"type":"hosted","url":"%s"},"issuedOn":"%s","evidence":"%s",' .
+            '"@context":"https:\/\/w3id.org\/openbadges\/v2","type":"Assertion","id":"%s"}';
+        $this->assertion2->class = '{"name":"%s","description":"%s","image":{"id":"%s","author":"%s","caption":"%s"},' .
+            '"criteria":{"id":"%s","narrative":"%s"},"issuer":{"name":"%s","url":"%s","email":"%s",' .
+            '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"Issuer"},' .
+            '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"BadgeClass","version":"%s",' .
+            '"@language":"%s","related":[{"id":"%s","version":"%s","@language":"%s"}],"endorsement":"%s",' .
+            '"alignment":[{"targetName":"%s","targetUrl":"%s","targetDescription":"%s","targetFramework":"%s",' .
+            '"targetCode":"%s"}]}';
+        $this->assertion2->issuer = '{"name":"%s","url":"%s","email":"%s",' .
+            '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"Issuer"}';
     }
 
     public function test_create_badge() {
@@ -107,21 +165,27 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
     public function test_clone_badge() {
         $badge = new badge($this->badgeid);
         $newid = $badge->make_clone();
-        $cloned_badge = new badge($newid);
-
-        $this->assertEquals($badge->description, $cloned_badge->description);
-        $this->assertEquals($badge->issuercontact, $cloned_badge->issuercontact);
-        $this->assertEquals($badge->issuername, $cloned_badge->issuername);
-        $this->assertEquals($badge->issuercontact, $cloned_badge->issuercontact);
-        $this->assertEquals($badge->issuerurl, $cloned_badge->issuerurl);
-        $this->assertEquals($badge->expiredate, $cloned_badge->expiredate);
-        $this->assertEquals($badge->expireperiod, $cloned_badge->expireperiod);
-        $this->assertEquals($badge->type, $cloned_badge->type);
-        $this->assertEquals($badge->courseid, $cloned_badge->courseid);
-        $this->assertEquals($badge->message, $cloned_badge->message);
-        $this->assertEquals($badge->messagesubject, $cloned_badge->messagesubject);
-        $this->assertEquals($badge->attachment, $cloned_badge->attachment);
-        $this->assertEquals($badge->notification, $cloned_badge->notification);
+        $clonedbadge = new badge($newid);
+
+        $this->assertEquals($badge->description, $clonedbadge->description);
+        $this->assertEquals($badge->issuercontact, $clonedbadge->issuercontact);
+        $this->assertEquals($badge->issuername, $clonedbadge->issuername);
+        $this->assertEquals($badge->issuercontact, $clonedbadge->issuercontact);
+        $this->assertEquals($badge->issuerurl, $clonedbadge->issuerurl);
+        $this->assertEquals($badge->expiredate, $clonedbadge->expiredate);
+        $this->assertEquals($badge->expireperiod, $clonedbadge->expireperiod);
+        $this->assertEquals($badge->type, $clonedbadge->type);
+        $this->assertEquals($badge->courseid, $clonedbadge->courseid);
+        $this->assertEquals($badge->message, $clonedbadge->message);
+        $this->assertEquals($badge->messagesubject, $clonedbadge->messagesubject);
+        $this->assertEquals($badge->attachment, $clonedbadge->attachment);
+        $this->assertEquals($badge->notification, $clonedbadge->notification);
+        $this->assertEquals($badge->version, $clonedbadge->version);
+        $this->assertEquals($badge->language, $clonedbadge->language);
+        $this->assertEquals($badge->imagecaption, $clonedbadge->imagecaption);
+        $this->assertEquals($badge->imageauthorname, $clonedbadge->imageauthorname);
+        $this->assertEquals($badge->imageauthoremail, $clonedbadge->imageauthoremail);
+        $this->assertEquals($badge->imageauthorurl, $clonedbadge->imageauthorurl);
     }
 
     public function test_badge_status() {
@@ -139,6 +203,45 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
         $this->assertAttributeEquals(BADGE_STATUS_ARCHIVED, 'status', $badge);
     }
 
+    /**
+     * Really delete the badge.
+     */
+    public function test_delete_badge_for_real() {
+        global $DB;
+
+        $badge = new badge($this->badgeid);
+
+        $newid1 = $badge->make_clone();
+        $newid2 = $badge->make_clone();
+        $newid3 = $badge->make_clone();
+
+        // Insert related badges to badge 1.
+        $badge->add_related_badges([$newid1, $newid2, $newid3]);
+
+        // Another badge.
+        $badge2 = new badge($newid2);
+        // Make badge 1 related for badge 2.
+        $badge2->add_related_badges([$this->badgeid]);
+
+        // Confirm that the records about this badge about its relations have been removed as well.
+        $relatedsql = 'badgeid = :badgeid OR relatedbadgeid = :relatedbadgeid';
+        $relatedparams = array(
+            'badgeid' => $this->badgeid,
+            'relatedbadgeid' => $this->badgeid
+        );
+        // Badge 1 has 4 related records. 3 where it's the badgeid, 1 where it's the relatedbadgeid.
+        $this->assertEquals(4, $DB->count_records_select('badge_related', $relatedsql, $relatedparams));
+
+        // Delete the badge for real.
+        $badge->delete(false);
+
+        // Confirm that the badge itself has been removed.
+        $this->assertFalse($DB->record_exists('badge', ['id' => $this->badgeid]));
+
+        // Confirm that the records about this badge about its relations have been removed as well.
+        $this->assertFalse($DB->record_exists_select('badge_related', $relatedsql, $relatedparams));
+    }
+
     public function test_create_badge_criteria() {
         $badge = new badge($this->badgeid);
         $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
@@ -239,6 +342,12 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
             $badge->attachment = 1;
             $badge->notification = 0;
             $badge->status = BADGE_STATUS_INACTIVE;
+            $badge->version = "Version $i";
+            $badge->language = "en";
+            $badge->imagecaption = "Image caption $i";
+            $badge->imageauthorname = "Image author's name $i";
+            $badge->imageauthoremail = "author$i@example.com";
+            $badge->imageauthorname = "Image author's name $i";
 
             $badgeid = $DB->insert_record('badge', $badge, true);
             $badges[$badgeid] = new badge($badgeid);
@@ -314,6 +423,12 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
         $badge->attachment = 1;
         $badge->notification = 0;
         $badge->status = BADGE_STATUS_ACTIVE;
+        $badge->version = "Version $i";
+        $badge->language = "en";
+        $badge->imagecaption = "Image caption";
+        $badge->imageauthorname = "Image author's name";
+        $badge->imageauthoremail = "author@example.com";
+        $badge->imageauthorname = "Image author's name";
 
         $badgeid = $DB->insert_record('badge', $badge, true);
         $badges[$badgeid] = new badge($badgeid);
@@ -545,6 +660,17 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
         $this->assertStringMatchesFormat($testassertion->badge, json_encode($assertion->get_badge_assertion()));
         $this->assertStringMatchesFormat($testassertion->class, json_encode($assertion->get_badge_class()));
         $this->assertStringMatchesFormat($testassertion->issuer, json_encode($assertion->get_issuer()));
+
+        // Test Openbadge specification version 2.
+        // Get assertion version 2.
+        $award = reset($awards);
+        $assertion2 = new core_badges_assertion($award->uniquehash, OPEN_BADGES_V2);
+        $testassertion2 = $this->assertion2;
+
+        // Make sure JSON strings have the same structure.
+        $this->assertStringMatchesFormat($testassertion2->badge, json_encode($assertion2->get_badge_assertion()));
+        $this->assertStringMatchesFormat($testassertion2->class, json_encode($assertion2->get_badge_class()));
+        $this->assertStringMatchesFormat($testassertion2->issuer, json_encode($assertion2->get_issuer()));
     }
 
     /**
@@ -611,4 +737,101 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
         $nodes->setAccessible(true);
         $this->assertArrayHasKey('localbadges', $nodes->getValue($tree));
     }
+
+    /**
+     * Test insert and update endorsement with a site badge.
+     */
+    public function test_badge_endorsement() {
+        $badge = new badge($this->badgeid);
+
+        // Insert Endorsement.
+        $endorsement = new stdClass();
+        $endorsement->badgeid = $this->badgeid;
+        $endorsement->issuername = "Issuer 123";
+        $endorsement->issueremail = "issuer123@email.com";
+        $endorsement->issuerurl = "https://example.org/issuer-123";
+        $endorsement->dateissued = 1524567747;
+        $endorsement->claimid = "https://example.org/robotics-badge.json";
+        $endorsement->claimcomment = "Test endorser comment";
+
+        $badge->save_endorsement($endorsement);
+        $endorsement1 = $badge->get_endorsement();
+        $this->assertEquals($endorsement->badgeid, $endorsement1->badgeid);
+        $this->assertEquals($endorsement->issuername, $endorsement1->issuername);
+        $this->assertEquals($endorsement->issueremail, $endorsement1->issueremail);
+        $this->assertEquals($endorsement->issuerurl, $endorsement1->issuerurl);
+        $this->assertEquals($endorsement->dateissued, $endorsement1->dateissued);
+        $this->assertEquals($endorsement->claimid, $endorsement1->claimid);
+        $this->assertEquals($endorsement->claimcomment, $endorsement1->claimcomment);
+
+        // Update Endorsement.
+        $endorsement1->issuername = "Issuer update";
+        $badge->save_endorsement($endorsement1);
+        $endorsement2 = $badge->get_endorsement();
+        $this->assertEquals($endorsement1->id, $endorsement2->id);
+        $this->assertEquals($endorsement1->issuername, $endorsement2->issuername);
+    }
+
+    /**
+     * Test insert and delete related badge with a site badge.
+     */
+    public function test_badge_related() {
+        $badge = new badge($this->badgeid);
+        $newid1 = $badge->make_clone();
+        $newid2 = $badge->make_clone();
+        $newid3 = $badge->make_clone();
+
+        // Insert an related badge.
+        $badge->add_related_badges([$newid1, $newid2, $newid3]);
+        $this->assertCount(3, $badge->get_related_badges());
+
+        // Only get related is active.
+        $clonedbage1 = new badge($newid1);
+        $clonedbage1->status = BADGE_STATUS_ACTIVE;
+        $clonedbage1->save();
+        $this->assertCount(1, $badge->get_related_badges(true));
+
+        // Delete an related badge.
+        $badge->delete_related_badge($newid2);
+        $this->assertCount(2, $badge->get_related_badges());
+    }
+
+    /**
+     * Test insert, update, delete competencies alignment with a site badge.
+     */
+    public function test_competencies_alignment() {
+        $badge = new badge($this->badgeid);
+
+        // Insert a competency alignment.
+        $competency1 = new stdClass();
+        $competency1->badgeid = $this->badgeid;
+        $competency1->targetname = 'CCSS.ELA-Literacy.RST.11-12.3';
+        $competency1->targeturl = 'http://www.corestandards.org/ELA-Literacy/RST/11-12/3';
+        $competency1->targetdescription = 'Test target description';
+        $competency1->targetframework = 'CCSS.RST.11-12.3';
+        $competency1->targetcode = 'CCSS.RST.11-12.3';
+        $competency2 = clone $competency1;
+        $newid1 = $badge->save_alignment($competency1);
+        $newid2 = $badge->save_alignment($competency2);
+        $competencies1 = $badge->get_alignment();
+        $this->assertCount(2, $competencies1);
+
+        $this->assertEquals($competency1->badgeid, $competencies1[$newid1]->badgeid);
+        $this->assertEquals($competency1->targetname, $competencies1[$newid1]->targetname);
+        $this->assertEquals($competency1->targeturl, $competencies1[$newid1]->targeturl);
+        $this->assertEquals($competency1->targetdescription, $competencies1[$newid1]->targetdescription);
+        $this->assertEquals($competency1->targetframework, $competencies1[$newid1]->targetframework);
+        $this->assertEquals($competency1->targetcode, $competencies1[$newid1]->targetcode);
+
+        // Update competency aligment.
+        $competencies1[$newid1]->targetname = 'CCSS.ELA-Literacy.RST.11-12.3 update';
+        $badge->save_alignment($competencies1[$newid1], $competencies1[$newid1]->id);
+        $competencies2 = $badge->get_alignment();
+        $this->assertEquals($competencies1[$newid1]->id, $competencies2[$newid1]->id);
+        $this->assertEquals($competencies1[$newid1]->targetname, $competencies2[$newid1]->targetname);
+
+        // Delete competency alignment.
+        $badge->delete_alignment($competencies1[$newid2]->id);
+        $this->assertCount(1, $badge->get_alignment());
+    }
 }
index 4a3c1c2..f11c510 100644 (file)
@@ -33,14 +33,115 @@ Feature: Add badges to the system
     Given I navigate to "Badges > Add a new badge" in site administration
     And I set the following fields to these values:
       | Name | Test badge with 'apostrophe' and other friends (<>&@#) |
+      | Version | v1 |
+      | Language | English |
       | Description | Test badge description |
+      | Image author | http://author.example.com |
+      | Image caption | Test caption image |
       | issuername | Test Badge Site |
       | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     When I press "Create badge"
     Then I should see "Edit details"
     And I should see "Test badge with 'apostrophe' and other friends (&@#)"
+    And I should see "Endorsement"
+    And I should see "Related badges (0)"
+    And I should see "Competencies (0)"
     And I should not see "Create badge"
     And I follow "Manage badges"
     And I should see "Number of badges available: 1"
     And I should not see "There are no badges available."
+
+  @javascript @_file_upload
+  Scenario: Add a badge related
+    Given I navigate to "Badges > Add a new badge" in site administration
+    And I set the following fields to these values:
+      | Name | Test Badge 1 |
+      | Version | v1 |
+      | Language | French |
+      | Description | Test badge related description |
+      | Image author | http://author.example.com |
+      | Image caption | Test caption image |
+      | issuername | Test Badge Site |
+      | issuercontact | testuser@example.com |
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    And I wait until the page is ready
+    And I follow "Manage badges"
+    And I should see "Number of badges available: 1"
+    And I press "Add a new badge"
+    And I set the following fields to these values:
+      | Name | Test Badge 2 |
+      | Version | v2 |
+      | Language | English |
+      | Description | Test badge description |
+      | Image author | http://author.example.com |
+      | Image caption | Test caption image |
+      | issuername | Test Badge Site |
+      | issuercontact | testuser@example.com |
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    And I follow "Related badges (0)"
+    And I should see "This badge does not have related badges."
+    And I press "Add related badge"
+    And I follow "Related badges"
+    And I wait until the page is ready
+    And I follow "Related badges"
+    And I set the field "relatedbadgeids[]" to "Test Badge 1 (version: v1, language: French, Site badges)"
+    When I press "Save changes"
+    Then I should see "Related badges (1)"
+
+  @javascript @_file_upload
+  Scenario: Endorsement for Badge
+    Given I navigate to "Badges > Add a new badge" in site administration
+    And I set the following fields to these values:
+      | Name | Test Badge Enrolment |
+      | Version | v1 |
+      | Language | English |
+      | Description | Test badge description |
+      | Image author | http://author.example.com |
+      | Image caption | Test caption image |
+      | issuername | Test Badge Site |
+      | issuercontact | testuser@example.com |
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
+    When I press "Create badge"
+    Then I should see "Edit details"
+    And I should see "Endorsement"
+    And I follow "Endorsement"
+    And I set the following fields to these values:
+      | Name | Endorser |
+      | Email | endorsement@example.com |
+      | Issuer URL | http://example.com  |
+      | Claim URL | http://claimurl.example.com |
+      | Endorsement Comment | Test Endorsement Comment |
+    And I press "Save changes"
+    Then I should see "Changes saved"
+
+  @javascript @_file_upload
+  Scenario: Competencies alignment for Badge
+    Given I navigate to "Badges > Add a new badge" in site administration
+    And I set the following fields to these values:
+      | Name | Test Badge |
+      | Version | v1 |
+      | Language | English |
+      | Description | Test badge description |
+      | Image author | http://author.example.com |
+      | Image caption | Test caption image |
+      | issuername | Test Badge Site |
+      | issuercontact | testuser@example.com |
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
+    When I press "Create badge"
+    Then I should see "Test Badge"
+    And I should see "Endorsement"
+    And I follow "Competencies (0)"
+    And I should see "This badge does not have a competencies alignment."
+    And I press "Add competency alignment"
+    And I follow "Competencies alignment"
+    And I wait until the page is ready
+    And I follow "Competencies alignment"
+    And I set the following fields to these values:
+      | Name | Test Badge Competencies |
+      | URL | https://competencies.example.com |
+      | Description | Test Badge Competencies description |
+    When I press "Save changes"
+    And I should see "Competencies (1)"
index bf3b084..1bfb93a 100644 (file)
@@ -32,6 +32,7 @@ use core_privacy\local\request\approved_contextlist;
 use core_privacy\local\request\transform;
 use core_privacy\local\request\writer;
 use core_badges\privacy\provider;
+use core_privacy\local\request\approved_userlist;
 
 require_once($CFG->libdir . '/badgeslib.php');
 
@@ -399,6 +400,177 @@ class core_badges_privacy_testcase extends provider_testcase {
         $this->assertEquals('Manager', $data->badges[0]['issuer_role']);
     }
 
+    /**
+     * Test that only users within a user, system and course context are fetched.
+     */
+    public function test_get_users_in_context() {
+        $component = 'core_badges';
+
+        // Create course1.
+        $course1 = $this->getDataGenerator()->create_course();
+        $coursecontext1 = context_course::instance($course1->id);
+        // Create course2.
+        $course2 = $this->getDataGenerator()->create_course();
+        $coursecontext2 = context_course::instance($course2->id);
+        // Create user1.
+        $user1 = $this->getDataGenerator()->create_user();
+        $usercontext1 = context_user::instance($user1->id);
+        // Create user2.
+        $user2 = $this->getDataGenerator()->create_user();
+        $usercontext2 = context_user::instance($user2->id);
+        // Create user3.
+        $user3 = $this->getDataGenerator()->create_user();
+        $usercontext3 = context_user::instance($user3->id);
+
+        // The list of users in usercontext1 should not return anything yet (related data still haven't been created).
+        $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
+        provider::get_users_in_context($userlist1);
+        $this->assertCount(0, $userlist1);
+        // The list of users in coursecontext1 should not return anything yet (related data still haven't been created).
+        $userlist2 = new \core_privacy\local\request\userlist($coursecontext1, $component);
+        provider::get_users_in_context($userlist2);
+        $this->assertCount(0, $userlist2);
+        // The list of users in systemcontext should not return anything yet (related data still haven't been created).
+        $systemcontext = context_system::instance();
+        $userlist3 = new \core_privacy\local\request\userlist($systemcontext, $component);
+        provider::get_users_in_context($userlist3);
+        $this->assertCount(0, $userlist3);
+
+        // Assert that we find contexts where we created/modified a badge.
+        $this->create_badge(['usercreated' => $user1->id, 'usermodified' => $user2->id]);
+        $badge1 = $this->create_badge(['usercreated' => $user2->id, 'type' => BADGE_TYPE_COURSE, 'courseid' => $course1->id]);
+        $badge2 = $this->create_badge(['usercreated' => $user3->id, 'usermodified' => $user1->id]);
+
+        $this->create_manual_award(['recipientid' => $user2->id, 'issuerid' => $user1->id, 'badgeid' => $badge1->id]);
+        $this->create_manual_award(['recipientid' => $user3->id, 'issuerid' => $user2->id, 'badgeid' => $badge1->id]);
+        $this->create_manual_award(['recipientid' => $user1->id, 'issuerid' => $user2->id, 'badgeid' => $badge2->id]);
+
+        $this->create_backpack(['userid' => $user2->id]);
+        $this->create_issued(['badgeid' => $badge2->id, 'userid' => $user3->id]);
+
+        $crit = $this->create_criteria_manual($badge1->id);
+        $crit->mark_complete($user3->id);
+
+        // The list of users for user context should return user1 and user2.
+        provider::get_users_in_context($userlist1);
+        $this->assertCount(2, $userlist1);
+        $this->assertTrue(in_array($user1->id, $userlist1->get_userids()));
+        $this->assertTrue(in_array($user2->id, $userlist1->get_userids()));
+
+        // The list of users for course context should return user2.
+        provider::get_users_in_context($userlist2);
+        $this->assertCount(1, $userlist2);
+        $this->assertTrue(in_array($user2->id, $userlist2->get_userids()));
+
+        // The list of users for system context should return user1, user2 and user3.
+        provider::get_users_in_context($userlist3);
+        $this->assertCount(3, $userlist3);
+        $this->assertTrue(in_array($user1->id, $userlist3->get_userids()));
+        $this->assertTrue(in_array($user2->id, $userlist3->get_userids()));
+        $this->assertTrue(in_array($user3->id, $userlist3->get_userids()));
+    }
+
+    /**
+     * Test that data for users in approved userlist is deleted.
+     */
+    public function test_delete_data_for_users() {
+        $component = 'core_badges';
+
+        // Create course1.
+        $course1 = $this->getDataGenerator()->create_course();
+        $coursecontext1 = context_course::instance($course1->id);
+        // Create course2.
+        $course2 = $this->getDataGenerator()->create_course();
+        $coursecontext2 = context_course::instance($course2->id);
+        // Create user1.
+        $user1 = $this->getDataGenerator()->create_user();
+        $usercontext1 = context_user::instance($user1->id);
+        // Create user2.
+        $user2 = $this->getDataGenerator()->create_user();
+        $usercontext2 = context_user::instance($user2->id);
+        // Create user3.
+        $user3 = $this->getDataGenerator()->create_user();
+        $usercontext3 = context_user::instance($user3->id);
+
+        $this->create_badge(['usercreated' => $user1->id, 'usermodified' => $user2->id]);
+        $badge1 = $this->create_badge(['usercreated' => $user2->id, 'type' => BADGE_TYPE_COURSE, 'courseid' => $course1->id]);
+        $badge2 = $this->create_badge(['usercreated' => $user3->id, 'type' => BADGE_TYPE_COURSE, 'courseid' => $course2->id,
+            'usermodified' => $user1->id]);
+
+        $this->create_manual_award(['recipientid' => $user2->id, 'issuerid' => $user1->id, 'badgeid' => $badge1->id]);
+        $this->create_manual_award(['recipientid' => $user3->id, 'issuerid' => $user2->id, 'badgeid' => $badge1->id]);
+        $this->create_manual_award(['recipientid' => $user1->id, 'issuerid' => $user2->id, 'badgeid' => $badge2->id]);
+
+        $this->create_backpack(['userid' => $user2->id]);
+        $this->create_issued(['badgeid' => $badge2->id, 'userid' => $user3->id]);
+
+        $crit = $this->create_criteria_manual($badge1->id);
+        $crit->mark_complete($user3->id);
+
+        // The list of users for usercontext2 context should return users.
+        $userlist1 = new \core_privacy\local\request\userlist($usercontext2, $component);
+        provider::get_users_in_context($userlist1);
+        $this->assertCount(2, $userlist1);
+        $this->assertTrue(in_array($user1->id, $userlist1->get_userids()));
+        $this->assertTrue(in_array($user2->id, $userlist1->get_userids()));
+
+        // The list of users for coursecontext2 context should return users.
+        $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component);
+        provider::get_users_in_context($userlist2);
+        $this->assertCount(2, $userlist2);
+        $this->assertTrue(in_array($user1->id, $userlist2->get_userids()));
+        $this->assertTrue(in_array($user3->id, $userlist2->get_userids()));
+
+        // The list of users for system context should return users.
+        $systemcontext = context_system::instance();
+        $userlist3 = new \core_privacy\local\request\userlist($systemcontext, $component);
+        provider::get_users_in_context($userlist3);
+        $this->assertCount(2, $userlist3);
+        $this->assertTrue(in_array($user1->id, $userlist3->get_userids()));
+        $this->assertTrue(in_array($user2->id, $userlist3->get_userids()));
+
+        // Delete the data for user1 in usercontext2.
+        $approvedlist = new approved_userlist($usercontext2, $component, [$user1->id]);
+        // Delete using delete_data_for_user. No data for users in usercontext2 should be removed.
+        provider::delete_data_for_users($approvedlist);
+        // The list of users for usercontext2 context should still return user1, user2.
+        $userlist1 = new \core_privacy\local\request\userlist($usercontext2, $component);
+        provider::get_users_in_context($userlist1);
+        $this->assertCount(2, $userlist1);
+        $this->assertTrue(in_array($user1->id, $userlist1->get_userids()));
+        $this->assertTrue(in_array($user2->id, $userlist1->get_userids()));
+
+        // Delete the data for user2 in usercontext2.
+        $approvedlist = new approved_userlist($usercontext2, $component, [$user2->id]);
+        // Delete using delete_data_for_user. The user data in usercontext2 should be removed.
+        provider::delete_data_for_users($approvedlist);
+        // The list of users for usercontext2 context should not return any users.
+        $userlist1 = new \core_privacy\local\request\userlist($usercontext2, $component);
+        provider::get_users_in_context($userlist1);
+        $this->assertCount(0, $userlist1);
+
+        // The list of users for coursecontext2 context should return the previous users.
+        $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component);
+        provider::get_users_in_context($userlist2);
+        $this->assertCount(2, $userlist2);
+
+        // The list of users for system context should return the previous users.
+        $systemcontext = context_system::instance();
+        $userlist3 = new \core_privacy\local\request\userlist($systemcontext, $component);
+        provider::get_users_in_context($userlist3);
+        $this->assertCount(2, $userlist3);
+
+        // Make sure data is only deleted in the user context, nothing in course or system.
+        // Convert $userlist2 into an approved_contextlist.
+        $approvedlist = new approved_userlist($coursecontext2, $component, $userlist2->get_userids());
+        provider::delete_data_for_users($approvedlist);
+
+        // The list of users for coursecontext2 context should still return the user data.
+        $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component);
+        provider::get_users_in_context($userlist2);
+        $this->assertCount(2, $userlist2);
+    }
+
     /**
      * Create a badge.
      *
index cbee644..d756dac 100644 (file)
@@ -44,7 +44,8 @@ use core_privacy\local\request\writer;
 class provider implements
     \core_privacy\local\metadata\provider,
     \core_privacy\local\request\subsystem\provider,
-    \core_privacy\local\request\user_preference_provider {
+    \core_privacy\local\request\user_preference_provider,
+    \core_privacy\local\request\core_userlist_provider {
 
     /**
      * Returns metadata.
@@ -103,6 +104,28 @@ class provider implements
         return $contextlist;
     }
 
+    /**
+     * Get the list of users who have data within a context.
+     *
+     * @param   \core_privacy\local\request\userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
+     */
+    public static function get_users_in_context(\core_privacy\local\request\userlist $userlist) {
+        global $DB;
+
+        $context = $userlist->get_context();
+        if ($context->contextlevel != CONTEXT_BLOCK) {
+            return;
+        }
+
+        $params = ['docked' => 'docked_block_instance_' . $context->instanceid,
+                   'hidden' => 'block' . $context->instanceid . 'hidden'];
+
+        $sql = "SELECT userid
+                  FROM {user_preferences}
+                 WHERE name = :hidden OR name = :docked";
+        $userlist->add_from_sql('userid', $sql, $params);
+    }
+
     /**
      * Export all user data for the specified user, in the specified contexts.
      *
@@ -224,4 +247,24 @@ class provider implements
         $DB->delete_records_select('user_preferences', $sql, $params);
     }
 
+
+    /**
+     * Delete multiple users within a single context.
+     *
+     * @param \core_privacy\local\request\approved_userlist $userlist The approved context and user information to delete
+     * information for.
+     */
+    public static function delete_data_for_users(\core_privacy\local\request\approved_userlist $userlist) {
+        global $DB;
+        $context = $userlist->get_context();
+        if ($context->contextlevel != CONTEXT_BLOCK) {
+            return;
+        }
+
+        list($insql, $params) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
+        $params['hidden'] = 'block' . $context->instanceid . 'hidden';
+        $params['docked'] = 'docked_block_instance_' . $context->instanceid;
+
+        $DB->delete_records_select('user_preferences', "(name = :hidden OR name = :docked) AND userid $insql", $params);
+    }
 }
index ce2592d..5ca1a22 100644 (file)
Binary files a/blocks/myoverview/amd/build/main.min.js and b/blocks/myoverview/amd/build/main.min.js differ
diff --git a/blocks/myoverview/amd/build/selectors.min.js b/blocks/myoverview/amd/build/selectors.min.js
new file mode 100644 (file)
index 0000000..a07864c
Binary files /dev/null and b/blocks/myoverview/amd/build/selectors.min.js differ
index 5f3fb07..0212118 100644 (file)
Binary files a/blocks/myoverview/amd/build/view.min.js and b/blocks/myoverview/amd/build/view.min.js differ
index 7af70de..66a3cba 100644 (file)
Binary files a/blocks/myoverview/amd/build/view_nav.min.js and b/blocks/myoverview/amd/build/view_nav.min.js differ
index e59e62e..2582cbd 100644 (file)
@@ -32,12 +32,6 @@ function(
     View,
     ViewNav
 ) {
-
-    var SELECTORS = {
-        COURSES_VIEW: '[data-region="courses-view"]',
-        COURSES_VIEW_CONTENT: '[data-region="course-view-content"]'
-    };
-
     /**
      * Initialise all of the modules for the overview block.
      *
@@ -45,12 +39,10 @@ function(
      */
     var init = function(root) {
         root = $(root);
-        var coursesViewRoot = root.find(SELECTORS.COURSES_VIEW);
-        var coursesViewContent = root.find(SELECTORS.COURSES_VIEW_CONTENT);
         // Initialise the course navigation elements.
-        ViewNav.init(root, coursesViewRoot, coursesViewContent);
+        ViewNav.init(root);
         // Initialise the courses view modules.
-        View.init(coursesViewRoot, coursesViewContent);
+        View.init(root);
     };
 
     return {
diff --git a/blocks/myoverview/amd/src/selectors.js b/blocks/myoverview/amd/src/selectors.js
new file mode 100644 (file)
index 0000000..21e6a92
--- /dev/null
@@ -0,0 +1,31 @@
+// 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/>.
+
+/**
+ * Javascript to initialise the selectors for the myoverview block.
+ *
+ * @package    block_myoverview
+ * @copyright  2018 Peter Dias <peter@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define([], function() {
+    return {
+        courseView: {
+            region: '[data-region="courses-view"]',
+            regionContent: '[data-region="course-view-content"]'
+        }
+    };
+});
index f4304fa..62f44a8 100644 (file)
@@ -26,20 +26,29 @@ define(
     'jquery',
     'block_myoverview/repository',
     'core/paged_content_factory',
+    'core/pubsub',
     'core/custom_interaction_events',
     'core/notification',
     'core/templates',
+    'core_course/events',
+    'block_myoverview/selectors'
 ],
 function(
     $,
     Repository,
     PagedContentFactory,
+    PubSub,
     CustomEvents,
     Notification,
-    Templates
+    Templates,
+    CourseEvents,
+    Selectors
 ) {
 
     var SELECTORS = {
+        COURSE_REGION: '[data-region="course-view-content"]',
+        ACTION_HIDE_COURSE: '[data-action="hide-course"]',
+        ACTION_SHOW_COURSE: '[data-action="show-course"]',
         ACTION_ADD_FAVOURITE: '[data-action="add-favourite"]',
         ACTION_REMOVE_FAVOURITE: '[data-action="remove-favourite"]',
         FAVOURITE_ICON: '[data-region="favourite-icon"]',
@@ -60,6 +69,12 @@ function(
&nb