Merge branch 'MDL-62277-master' of git://github.com/bmbrands/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 10 May 2018 18:50:20 +0000 (20:50 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 10 May 2018 18:50:20 +0000 (20:50 +0200)
12 files changed:
admin/settings/users.php
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/tests/api_test.php
lib/db/upgrade.php
lib/upgrade.txt
mod/assign/classes/privacy/provider.php
mod/assign/tests/privacy_test.php
mod/upgrade.txt
theme/boost/scss/moodle/core.scss
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/style/moodle.css
version.php

index 465a9d7..ac9c15b 100644 (file)
@@ -228,27 +228,18 @@ if ($hassiteconfig) {
     $setting->set_force_ltr(true);
     $temp->add($setting);
 
+    // See {@link https://gdpr-info.eu/art-8-gdpr/}.
+    $ageofdigitalconsentmap = implode(PHP_EOL, [
+        '*, 16',
+        'AT, 14',
+        'ES, 14',
+        'SI, 14',
+        'US, 13'
+    ]);
     $setting = new admin_setting_agedigitalconsentmap('agedigitalconsentmap',
         new lang_string('ageofdigitalconsentmap', 'admin'),
         new lang_string('ageofdigitalconsentmap_desc', 'admin'),
-        // See {@link https://gdpr-info.eu/art-8-gdpr/}.
-        implode(PHP_EOL, [
-            '*, 16',
-            'AT, 14',
-            'CZ, 13',
-            'DE, 14',
-            'DK, 13',
-            'ES, 13',
-            'FI, 15',
-            'GB, 13',
-            'HU, 14',
-            'IE, 13',
-            'LT, 16',
-            'LU, 16',
-            'NL, 16',
-            'PL, 13',
-            'SE, 13',
-        ]),
+        $ageofdigitalconsentmap,
         PARAM_RAW
     );
     $temp->add($setting);
index 09583b2..d1d6ef4 100644 (file)
@@ -765,6 +765,7 @@ class api {
      * @param int $status the status to set the contexts to.
      */
     public static function add_request_contexts_with_status(contextlist_collection $clcollection, int $requestid, int $status) {
+        $request = new data_request($requestid);
         foreach ($clcollection as $contextlist) {
             // Convert the \core_privacy\local\request\contextlist into a contextlist persistent and store it.
             $clp = \tool_dataprivacy\contextlist::from_contextlist($contextlist);
@@ -773,6 +774,12 @@ class api {
 
             // Store the associated contexts in the contextlist.
             foreach ($contextlist->get_contextids() as $contextid) {
+                if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
+                    $context = \context::instance_by_id($contextid);
+                    if (($purpose = static::get_effective_context_purpose($context)) && !empty($purpose->get('protected'))) {
+                        continue;
+                    }
+                }
                 $context = new contextlist_context();
                 $context->set('contextid', $contextid)
                     ->set('contextlistid', $contextlistid)
@@ -869,6 +876,7 @@ class api {
                 }
                 $contexts = [];
             }
+
             $contexts[] = $record->contextid;
             $lastcomponent = $record->component;
         }
index 62d29c1..2589607 100644 (file)
@@ -1073,4 +1073,126 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
             [$module1, $module2]
         ];
     }
+
+    /**
+     * Test that delete requests filter out protected purpose contexts.
+     */
+    public function test_add_request_contexts_with_status_delete() {
+        $data = $this->setup_test_add_request_contexts_with_status(api::DATAREQUEST_TYPE_DELETE);
+        $contextids = $data->list->get_contextids();
+
+        $this->assertCount(1, $contextids);
+        $this->assertEquals($data->contexts->unprotected, $contextids);
+    }
+
+    /**
+     * Test that export requests don't filter out protected purpose contexts.
+     */
+    public function test_add_request_contexts_with_status_export() {
+        $data = $this->setup_test_add_request_contexts_with_status(api::DATAREQUEST_TYPE_EXPORT);
+        $contextids = $data->list->get_contextids();
+
+        $this->assertCount(2, $contextids);
+        $this->assertEquals($data->contexts->used, $contextids, '', 0.0, 10, true);
+    }
+
+    /**
+     * Perform setup for the test_add_request_contexts_with_status_xxxxx tests.
+     *
+     * @param       int $type The type of request to create
+     * @return      \stdClass
+     */
+    protected function setup_test_add_request_contexts_with_status($type) {
+        $this->setAdminUser();
+
+        // User under test.
+        $s1 = $this->getDataGenerator()->create_user();
+
+        // Create three sample contexts.
+        // 1 which should not be returned; and
+        // 1 which will be returned and is not protected; and
+        // 1 which will be returned and is protected.
+
+        $c1 = $this->getDataGenerator()->create_course();
+        $c2 = $this->getDataGenerator()->create_course();
+        $c3 = $this->getDataGenerator()->create_course();
+
+        $ctx1 = \context_course::instance($c1->id);
+        $ctx2 = \context_course::instance($c2->id);
+        $ctx3 = \context_course::instance($c3->id);
+
+        $unprotected = api::create_purpose((object)[
+            'name' => 'Unprotected', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a']);
+        $protected = api::create_purpose((object) [
+            'name' => 'Protected', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a', 'protected' => true]);
+
+        $cat1 = api::create_category((object)['name' => 'a']);
+
+        // Set the defaults.
+        list($purposevar, $categoryvar) = data_registry::var_names_from_context(
+            \context_helper::get_class_for_level(CONTEXT_SYSTEM)
+        );
+        set_config($purposevar, $unprotected->get('id'), 'tool_dataprivacy');
+        set_config($categoryvar, $cat1->get('id'), 'tool_dataprivacy');
+
+        $contextinstance1 = api::set_context_instance((object) [
+                'contextid' => $ctx1->id,
+                'purposeid' => $unprotected->get('id'),
+                'categoryid' => $cat1->get('id'),
+            ]);
+
+        $contextinstance2 = api::set_context_instance((object) [
+                'contextid' => $ctx2->id,
+                'purposeid' => $unprotected->get('id'),
+                'categoryid' => $cat1->get('id'),
+            ]);
+
+        $contextinstance3 = api::set_context_instance((object) [
+                'contextid' => $ctx3->id,
+                'purposeid' => $protected->get('id'),
+                'categoryid' => $cat1->get('id'),
+            ]);
+
+        $collection = new \core_privacy\local\request\contextlist_collection($s1->id);
+        $contextlist = new \core_privacy\local\request\contextlist();
+        $contextlist->set_component('tool_dataprivacy');
+        $contextlist->add_from_sql('SELECT id FROM {context} WHERE id IN(:ctx2, :ctx3)', [
+                'ctx2' => $ctx2->id,
+                'ctx3' => $ctx3->id,
+            ]);
+
+        $collection->add_contextlist($contextlist);
+
+        // Create the sample data request.
+        $datarequest = api::create_data_request($s1->id, $type);
+        $requestid = $datarequest->get('id');
+
+        // Add the full collection with contexts 2, and 3.
+        api::add_request_contexts_with_status($collection, $requestid, \tool_dataprivacy\contextlist_context::STATUS_PENDING);
+
+        // Mark it as approved.
+        api::update_request_contexts_with_status($requestid, \tool_dataprivacy\contextlist_context::STATUS_APPROVED);
+
+        // Fetch the list.
+        $approvedcollection = api::get_approved_contextlist_collection_for_request($datarequest);
+
+        return (object) [
+            'contexts' => (object) [
+                'unused' => [
+                    $ctx1->id,
+                ],
+                'used' => [
+                    $ctx2->id,
+                    $ctx3->id,
+                ],
+                'unprotected' => [
+                    $ctx2->id,
+                ],
+                'protected' => [
+                    $ctx3->id,
+                ],
+            ],
+            'list' => $approvedcollection->get_contextlist_for_component('tool_dataprivacy'),
+        ];
+    }
 }
index edafff2..5e8fcd3 100644 (file)
@@ -2216,5 +2216,20 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2018040500.01);
     }
 
+    if ($oldversion < 2018050900.01) {
+        // Update default digital age consent map according to the current legislation on each country.
+        $ageofdigitalconsentmap = implode(PHP_EOL, [
+            '*, 16',
+            'AT, 14',
+            'ES, 14',
+            'SI, 14',
+            'US, 13'
+        ]);
+        set_config('agedigitalconsentmap', $ageofdigitalconsentmap);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018050900.01);
+    }
+
     return true;
 }
index 437c0dc..5d29d4c 100644 (file)
@@ -3,6 +3,10 @@ information provided here is intended especially for developers.
 
 === 3.5 ===
 
+* There is a new privacy API that every subsystem and plugin has to implement so that the site can become GDPR
+  compliant. Plugins use this API to report what information they store or process regarding users, and provide ability
+  to export and delete personal data. See https://docs.moodle.org/dev/Privacy_API for guidelines on how to implement the
+  privacy API in your plugin.
 * The cron runner now sets up a fresh PAGE and OUTPUT between each task.
 * The core_renderer methods notify_problem(), notify_success(), notify_message() and notify_redirect() that were
   deprecated in Moodle 3.1 have been removed. Use \core\notification::add(), or \core\output\notification as required.
index bbe0713..c706711 100644 (file)
@@ -376,7 +376,7 @@ class provider implements metadataprovider, pluginprovider, preference_provider
             'timemodified' => transform::datetime($grade->timemodified),
             'grader' => transform::user($grade->grader),
             'grade' => $grade->grade,
-            'attemptnumber' => $grade->attemptnumber
+            'attemptnumber' => ($grade->attemptnumber + 1)
         ];
         writer::with_context($context)
                 ->export_data(array_merge($currentpath, [get_string('privacy:gradepath', 'mod_assign')]), $gradedata);
@@ -395,7 +395,7 @@ class provider implements metadataprovider, pluginprovider, preference_provider
             'timemodified' => transform::datetime($submission->timemodified),
             'status' => get_string('submissionstatus_' . $submission->status, 'mod_assign'),
             'groupid' => $submission->groupid,
-            'attemptnumber' => $submission->attemptnumber,
+            'attemptnumber' => ($submission->attemptnumber + 1),
             'latest' => transform::yesno($submission->latest)
         ];
         writer::with_context($context)
index a9800bc..ac2ee02 100644 (file)
@@ -214,8 +214,8 @@ class mod_assign_privacy_testcase extends provider_testcase {
         // Check Submissions.
         $this->assertEquals($submissiontext, $writer->get_data(['attempt 1', 'Submission Text'])->text);
         $this->assertEquals($submissiontext2, $writer->get_data(['attempt 2', 'Submission Text'])->text);
-        $this->assertEquals(0, $writer->get_data(['attempt 1', 'submission'])->attemptnumber);
-        $this->assertEquals(1, $writer->get_data(['attempt 2', 'submission'])->attemptnumber);
+        $this->assertEquals(1, $writer->get_data(['attempt 1', 'submission'])->attemptnumber);
+        $this->assertEquals(2, $writer->get_data(['attempt 2', 'submission'])->attemptnumber);
         // Check grades.
         $this->assertEquals($grade1, $writer->get_data(['attempt 1', 'grade'])->grade);
         $this->assertEquals($grade2, $writer->get_data(['attempt 2', 'grade'])->grade);
index 00ffe0d..bdd4d74 100644 (file)
@@ -3,6 +3,10 @@ information provided here is intended especially for developers.
 
 === 3.5 ===
 
+* There is a new privacy API that every subsystem and plugin has to implement so that the site can become GDPR
+  compliant. Activity modules use this API to report what information they store or process regarding users, and provide
+  ability to export and delete personal data. See https://docs.moodle.org/dev/Privacy_API for guidelines on how to
+  implement the privacy API in your activity module.
 * Backup directory now can be outside of temp directory. Use make_backup_temp_directory($name) instead of
   make_temp_directory('/backup/'.$name)
 * Modules that provide their own interactive content and call cm_info::set_content() from [MODULENAME]_cm_info_view()
index ea2f2c9..9c07a8d 100644 (file)
@@ -1104,6 +1104,18 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
     background-color: $gray;
 }
 
+// Prevent adding backdrops to popups in popups.
+.pagelayout-popup {
+    .moodle-dialogue-base {
+        .moodle-dialogue-lightbox {
+            background-color: transparent;
+        }
+        .moodle-dialogue {
+            box-shadow: $popover-box-shadow;
+        }
+    }
+}
+
 .moodle-dialogue-base .hidden,
 .moodle-dialogue-base .moodle-dialogue-hidden {
     display: none;
index eeb6b2f..98d7c19 100644 (file)
@@ -1332,6 +1332,14 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
 .moodle-dialogue-base .moodle-dialogue-lightbox {
     background-color: #aaa;
 }
+// Prevent adding backdrops to popups in popups.
+.pagelayout-popup {
+    .moodle-dialogue-base {
+        .moodle-dialogue-lightbox {
+            background-color: transparent;
+        }
+    }
+}
 .moodle-dialogue-base .hidden,
 .moodle-dialogue-base .moodle-dialogue-hidden {
     display: none;
index 00150f8..6b31450 100644 (file)
@@ -3659,6 +3659,9 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
 .moodle-dialogue-base .moodle-dialogue-lightbox {
   background-color: #aaa;
 }
+.pagelayout-popup .moodle-dialogue-base .moodle-dialogue-lightbox {
+  background-color: transparent;
+}
 .moodle-dialogue-base .hidden,
 .moodle-dialogue-base .moodle-dialogue-hidden {
   display: none;
index 60bcd64..e5e796f 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2018050900.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2018050900.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.