Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Provides the {@link mod_workshop_privacy_provider_testcase} class.
  19   *
  20   * @package     mod_workshop
  21   * @category    test
  22   * @copyright   2018 David Mudrák <david@moodle.com>
  23   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  global $CFG;
  29  
  30  use core_privacy\local\request\writer;
  31  use core_privacy\tests\provider_testcase;
  32  
  33  /**
  34   * Unit tests for the privacy API implementation.
  35   *
  36   * @copyright 2018 David Mudrák <david@moodle.com>
  37   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class mod_workshop_privacy_provider_testcase extends provider_testcase {
  40  
  41      /** @var testing_data_generator */
  42      protected $generator;
  43  
  44      /** @var mod_workshop_generator */
  45      protected $workshopgenerator;
  46  
  47      /** @var stdClass */
  48      protected $course1;
  49  
  50      /** @var stdClass */
  51      protected $course2;
  52  
  53      /** @var stdClass */
  54      protected $student1;
  55  
  56      /** @var stdClass */
  57      protected $student2;
  58  
  59      /** @var stdClass */
  60      protected $student3;
  61  
  62      /** @var stdClass */
  63      protected $teacher4;
  64  
  65      /** @var stdClass first workshop in course1 */
  66      protected $workshop11;
  67  
  68      /** @var stdClass second workshop in course1 */
  69      protected $workshop12;
  70  
  71      /** @var stdClass first workshop in course2 */
  72      protected $workshop21;
  73  
  74      /** @var int ID of the submission in workshop11 by student1 */
  75      protected $submission111;
  76  
  77      /** @var int ID of the submission in workshop12 by student1 */
  78      protected $submission121;
  79  
  80      /** @var int ID of the submission in workshop12 by student2 */
  81      protected $submission122;
  82  
  83      /** @var int ID of the submission in workshop21 by student2 */
  84      protected $submission212;
  85  
  86      /** @var int ID of the assessment of submission111 by student1 */
  87      protected $assessment1111;
  88  
  89      /** @var int ID of the assessment of submission111 by student2 */
  90      protected $assessment1112;
  91  
  92      /** @var int ID of the assessment of submission111 by student3 */
  93      protected $assessment1113;
  94  
  95      /** @var int ID of the assessment of submission121 by student2 */
  96      protected $assessment1212;
  97  
  98      /** @var int ID of the assessment of submission212 by student1 */
  99      protected $assessment2121;
 100  
 101      /**
 102       * Set up the test environment.
 103       *
 104       * course1
 105       *  |
 106       *  +--workshop11 (first digit matches the course, second is incremental)
 107       *  |   |
 108       *  |   +--submission111 (first two digits match the workshop, last one matches the author)
 109       *  |       |
 110       *  |       +--assessment1111 (first three digits match the submission, last one matches the reviewer)
 111       *  |       +--assessment1112
 112       *  |       +--assessment1113
 113       *  |
 114       *  +--workshop12
 115       *      |
 116       *      +--submission121
 117       *      |   |
 118       *      |   +--assessment1212
 119       *      |
 120       *      +--submission122
 121       *
 122       *  etc.
 123       */
 124      protected function setUp(): void {
 125          global $DB;
 126          $this->resetAfterTest();
 127          $this->setAdminUser();
 128  
 129          $this->generator = $this->getDataGenerator();
 130          $this->workshopgenerator = $this->generator->get_plugin_generator('mod_workshop');
 131  
 132          $this->course1 = $this->generator->create_course();
 133          $this->course2 = $this->generator->create_course();
 134  
 135          $this->workshop11 = $this->generator->create_module('workshop', [
 136              'course' => $this->course1,
 137              'name' => 'Workshop11',
 138          ]);
 139          $DB->set_field('workshop', 'phase', 50, ['id' => $this->workshop11->id]);
 140  
 141          $this->workshop12 = $this->generator->create_module('workshop', ['course' => $this->course1]);
 142          $this->workshop21 = $this->generator->create_module('workshop', ['course' => $this->course2]);
 143  
 144          $this->student1 = $this->generator->create_user();
 145          $this->student2 = $this->generator->create_user();
 146          $this->student3 = $this->generator->create_user();
 147          $this->teacher4 = $this->generator->create_user();
 148  
 149          $this->submission111 = $this->workshopgenerator->create_submission($this->workshop11->id, $this->student1->id);
 150          $this->submission121 = $this->workshopgenerator->create_submission($this->workshop12->id, $this->student1->id,
 151              ['gradeoverby' => $this->teacher4->id]);
 152          $this->submission122 = $this->workshopgenerator->create_submission($this->workshop12->id, $this->student2->id);
 153          $this->submission212 = $this->workshopgenerator->create_submission($this->workshop21->id, $this->student2->id);
 154  
 155          $this->assessment1111 = $this->workshopgenerator->create_assessment($this->submission111, $this->student1->id, [
 156              'grade' => null,
 157          ]);
 158          $this->assessment1112 = $this->workshopgenerator->create_assessment($this->submission111, $this->student2->id, [
 159              'grade' => 92,
 160          ]);
 161          $this->assessment1113 = $this->workshopgenerator->create_assessment($this->submission111, $this->student3->id);
 162  
 163          $this->assessment1212 = $this->workshopgenerator->create_assessment($this->submission121, $this->student2->id, [
 164              'feedbackauthor' => 'This is what student 2 thinks about submission 121',
 165              'feedbackreviewer' => 'This is what the teacher thinks about this assessment',
 166          ]);
 167  
 168          $this->assessment2121 = $this->workshopgenerator->create_assessment($this->submission212, $this->student1->id, [
 169              'grade' => 68,
 170              'gradinggradeover' => 80,
 171              'gradinggradeoverby' => $this->teacher4->id,
 172              'feedbackauthor' => 'This is what student 1 thinks about submission 212',
 173              'feedbackreviewer' => 'This is what the teacher thinks about this assessment',
 174          ]);
 175      }
 176  
 177      /**
 178       * Test {@link \mod_workshop\privacy\provider::get_contexts_for_userid()} implementation.
 179       */
 180      public function test_get_contexts_for_userid() {
 181  
 182          $cm11 = get_coursemodule_from_instance('workshop', $this->workshop11->id);
 183          $cm12 = get_coursemodule_from_instance('workshop', $this->workshop12->id);
 184          $cm21 = get_coursemodule_from_instance('workshop', $this->workshop21->id);
 185  
 186          $context11 = context_module::instance($cm11->id);
 187          $context12 = context_module::instance($cm12->id);
 188          $context21 = context_module::instance($cm21->id);
 189  
 190          // Student1 has data in workshop11 (author + self reviewer), workshop12 (author) and workshop21 (reviewer).
 191          $contextlist = \mod_workshop\privacy\provider::get_contexts_for_userid($this->student1->id);
 192          $this->assertInstanceOf(\core_privacy\local\request\contextlist::class, $contextlist);
 193          $this->assertEqualsCanonicalizing([$context11->id, $context12->id, $context21->id], $contextlist->get_contextids());
 194  
 195          // Student2 has data in workshop11 (reviewer), workshop12 (reviewer) and workshop21 (author).
 196          $contextlist = \mod_workshop\privacy\provider::get_contexts_for_userid($this->student2->id);
 197          $this->assertEqualsCanonicalizing([$context11->id, $context12->id, $context21->id], $contextlist->get_contextids());
 198  
 199          // Student3 has data in workshop11 (reviewer).
 200          $contextlist = \mod_workshop\privacy\provider::get_contexts_for_userid($this->student3->id);
 201          $this->assertEqualsCanonicalizing([$context11->id], $contextlist->get_contextids());
 202  
 203          // Teacher4 has data in workshop12 (gradeoverby) and workshop21 (gradinggradeoverby).
 204          $contextlist = \mod_workshop\privacy\provider::get_contexts_for_userid($this->teacher4->id);
 205          $this->assertEqualsCanonicalizing([$context21->id, $context12->id], $contextlist->get_contextids());
 206      }
 207  
 208      /**
 209       * Test {@link \mod_workshop\privacy\provider::get_users_in_context()} implementation.
 210       */
 211      public function test_get_users_in_context() {
 212  
 213          $cm11 = get_coursemodule_from_instance('workshop', $this->workshop11->id);
 214          $cm12 = get_coursemodule_from_instance('workshop', $this->workshop12->id);
 215          $cm21 = get_coursemodule_from_instance('workshop', $this->workshop21->id);
 216  
 217          $context11 = context_module::instance($cm11->id);
 218          $context12 = context_module::instance($cm12->id);
 219          $context21 = context_module::instance($cm21->id);
 220  
 221          // Users in the workshop11.
 222          $userlist11 = new \core_privacy\local\request\userlist($context11, 'mod_workshop');
 223          \mod_workshop\privacy\provider::get_users_in_context($userlist11);
 224          $expected11 = [
 225              $this->student1->id, // Student1 has data in workshop11 (author + self reviewer).
 226              $this->student2->id, // Student2 has data in workshop11 (reviewer).
 227              $this->student3->id, // Student3 has data in workshop11 (reviewer).
 228          ];
 229          $actual11 = $userlist11->get_userids();
 230          $this->assertEqualsCanonicalizing($expected11, $actual11);
 231  
 232          // Users in the workshop12.
 233          $userlist12 = new \core_privacy\local\request\userlist($context12, 'mod_workshop');
 234          \mod_workshop\privacy\provider::get_users_in_context($userlist12);
 235          $expected12 = [
 236              $this->student1->id, // Student1 has data in workshop12 (author).
 237              $this->student2->id, // Student2 has data in workshop12 (reviewer).
 238              $this->teacher4->id, // Teacher4 has data in workshop12 (gradeoverby).
 239          ];
 240          $actual12 = $userlist12->get_userids();
 241          $this->assertEqualsCanonicalizing($expected12, $actual12);
 242  
 243          // Users in the workshop21.
 244          $userlist21 = new \core_privacy\local\request\userlist($context21, 'mod_workshop');
 245          \mod_workshop\privacy\provider::get_users_in_context($userlist21);
 246          $expected21 = [
 247              $this->student1->id, // Student1 has data in workshop21 (reviewer).
 248              $this->student2->id, // Student2 has data in workshop21 (author).
 249              $this->teacher4->id, // Teacher4 has data in workshop21 (gradinggradeoverby).
 250          ];
 251          $actual21 = $userlist21->get_userids();
 252          $this->assertEqualsCanonicalizing($expected21, $actual21);
 253      }
 254  
 255      /**
 256       * Test {@link \mod_workshop\privacy\provider::export_user_data()} implementation.
 257       */
 258      public function test_export_user_data_1() {
 259  
 260          $contextlist = new \core_privacy\local\request\approved_contextlist($this->student1, 'mod_workshop', [
 261              \context_module::instance($this->workshop11->cmid)->id,
 262              \context_module::instance($this->workshop12->cmid)->id,
 263          ]);
 264  
 265          \mod_workshop\privacy\provider::export_user_data($contextlist);
 266  
 267          $writer = writer::with_context(\context_module::instance($this->workshop11->cmid));
 268  
 269          $workshop = $writer->get_data([]);
 270          $this->assertEquals('Workshop11', $workshop->name);
 271          $this->assertObjectHasAttribute('phase', $workshop);
 272  
 273          $mysubmission = $writer->get_data([
 274              get_string('mysubmission', 'mod_workshop'),
 275          ]);
 276  
 277          $mysubmissionselfassessmentwithoutgrade = $writer->get_data([
 278              get_string('mysubmission', 'mod_workshop'),
 279              get_string('assessments', 'mod_workshop'),
 280              $this->assessment1111,
 281          ]);
 282          $this->assertNull($mysubmissionselfassessmentwithoutgrade->grade);
 283          $this->assertEquals(get_string('yes'), $mysubmissionselfassessmentwithoutgrade->selfassessment);
 284  
 285          $mysubmissionassessmentwithgrade = $writer->get_data([
 286              get_string('mysubmission', 'mod_workshop'),
 287              get_string('assessments', 'mod_workshop'),
 288              $this->assessment1112,
 289          ]);
 290          $this->assertEquals(92, $mysubmissionassessmentwithgrade->grade);
 291          $this->assertEquals(get_string('no'), $mysubmissionassessmentwithgrade->selfassessment);
 292  
 293          $mysubmissionassessmentwithoutgrade = $writer->get_data([
 294              get_string('mysubmission', 'mod_workshop'),
 295              get_string('assessments', 'mod_workshop'),
 296              $this->assessment1113,
 297          ]);
 298          $this->assertEquals(null, $mysubmissionassessmentwithoutgrade->grade);
 299          $this->assertEquals(get_string('no'), $mysubmissionassessmentwithoutgrade->selfassessment);
 300  
 301          $myassessments = $writer->get_data([
 302              get_string('myassessments', 'mod_workshop'),
 303          ]);
 304          $this->assertEmpty($myassessments);
 305      }
 306  
 307      /**
 308       * Test {@link \mod_workshop\privacy\provider::export_user_data()} implementation.
 309       */
 310      public function test_export_user_data_2() {
 311  
 312          $contextlist = new \core_privacy\local\request\approved_contextlist($this->student2, 'mod_workshop', [
 313              \context_module::instance($this->workshop11->cmid)->id,
 314          ]);
 315  
 316          \mod_workshop\privacy\provider::export_user_data($contextlist);
 317  
 318          $writer = writer::with_context(\context_module::instance($this->workshop11->cmid));
 319  
 320          $assessedsubmission = $writer->get_related_data([
 321              get_string('myassessments', 'mod_workshop'),
 322              $this->assessment1112,
 323          ], 'submission');
 324          $this->assertEquals(get_string('no'), $assessedsubmission->myownsubmission);
 325      }
 326  
 327      /**
 328       * Test {@link \mod_workshop\privacy\provider::delete_data_for_all_users_in_context()} implementation.
 329       */
 330      public function test_delete_data_for_all_users_in_context() {
 331          global $DB;
 332  
 333          $this->assertTrue($DB->record_exists('workshop_submissions', ['workshopid' => $this->workshop11->id]));
 334  
 335          // Passing a non-module context does nothing.
 336          \mod_workshop\privacy\provider::delete_data_for_all_users_in_context(\context_course::instance($this->course1->id));
 337          $this->assertTrue($DB->record_exists('workshop_submissions', ['workshopid' => $this->workshop11->id]));
 338  
 339          // Passing a workshop context removes all data.
 340          \mod_workshop\privacy\provider::delete_data_for_all_users_in_context(\context_module::instance($this->workshop11->cmid));
 341          $this->assertFalse($DB->record_exists('workshop_submissions', ['workshopid' => $this->workshop11->id]));
 342      }
 343  
 344      /**
 345       * Test {@link \mod_workshop\privacy\provider::delete_data_for_user()} implementation.
 346       */
 347      public function test_delete_data_for_user() {
 348          global $DB;
 349  
 350          $student1submissions = $DB->get_records('workshop_submissions', [
 351              'workshopid' => $this->workshop12->id,
 352              'authorid' => $this->student1->id,
 353          ]);
 354  
 355          $student2submissions = $DB->get_records('workshop_submissions', [
 356              'workshopid' => $this->workshop12->id,
 357              'authorid' => $this->student2->id,
 358          ]);
 359  
 360          $this->assertNotEmpty($student1submissions);
 361          $this->assertNotEmpty($student2submissions);
 362  
 363          foreach ($student1submissions as $submission) {
 364              $this->assertNotEquals(get_string('privacy:request:delete:title', 'mod_workshop'), $submission->title);
 365          }
 366  
 367          foreach ($student2submissions as $submission) {
 368              $this->assertNotEquals(get_string('privacy:request:delete:title', 'mod_workshop'), $submission->title);
 369          }
 370  
 371          $contextlist = new \core_privacy\local\request\approved_contextlist($this->student1, 'mod_workshop', [
 372              \context_module::instance($this->workshop12->cmid)->id,
 373              \context_module::instance($this->workshop21->cmid)->id,
 374          ]);
 375  
 376          \mod_workshop\privacy\provider::delete_data_for_user($contextlist);
 377  
 378          $student1submissions = $DB->get_records('workshop_submissions', [
 379              'workshopid' => $this->workshop12->id,
 380              'authorid' => $this->student1->id,
 381          ]);
 382  
 383          $student2submissions = $DB->get_records('workshop_submissions', [
 384              'workshopid' => $this->workshop12->id,
 385              'authorid' => $this->student2->id,
 386          ]);
 387  
 388          $this->assertNotEmpty($student1submissions);
 389          $this->assertNotEmpty($student2submissions);
 390  
 391          foreach ($student1submissions as $submission) {
 392              $this->assertEquals(get_string('privacy:request:delete:title', 'mod_workshop'), $submission->title);
 393          }
 394  
 395          foreach ($student2submissions as $submission) {
 396              $this->assertNotEquals(get_string('privacy:request:delete:title', 'mod_workshop'), $submission->title);
 397          }
 398  
 399          $student1assessments = $DB->get_records('workshop_assessments', [
 400              'submissionid' => $this->submission212,
 401              'reviewerid' => $this->student1->id,
 402          ]);
 403          $this->assertNotEmpty($student1assessments);
 404  
 405          foreach ($student1assessments as $assessment) {
 406              // In Moodle, feedback is seen to belong to the recipient user.
 407              $this->assertNotEquals(get_string('privacy:request:delete:content', 'mod_workshop'), $assessment->feedbackauthor);
 408              $this->assertEquals(get_string('privacy:request:delete:content', 'mod_workshop'), $assessment->feedbackreviewer);
 409              // We delete what we can without affecting others' grades.
 410              $this->assertEquals(68, $assessment->grade);
 411          }
 412  
 413          $assessments = $DB->get_records_list('workshop_assessments', 'submissionid', array_keys($student1submissions));
 414          $this->assertNotEmpty($assessments);
 415  
 416          foreach ($assessments as $assessment) {
 417              if ($assessment->reviewerid == $this->student1->id) {
 418                  $this->assertNotEquals(get_string('privacy:request:delete:content', 'mod_workshop'), $assessment->feedbackauthor);
 419                  $this->assertNotEquals(get_string('privacy:request:delete:content', 'mod_workshop'), $assessment->feedbackreviewer);
 420  
 421              } else {
 422                  $this->assertEquals(get_string('privacy:request:delete:content', 'mod_workshop'), $assessment->feedbackauthor);
 423                  $this->assertNotEquals(get_string('privacy:request:delete:content', 'mod_workshop'), $assessment->feedbackreviewer);
 424              }
 425          }
 426      }
 427  
 428      /**
 429       * Test {@link \mod_workshop\privacy\provider::delete_data_for_users()} implementation.
 430       */
 431      public function test_delete_data_for_users() {
 432          global $DB;
 433  
 434          // Student1 has submissions in two workshops.
 435          $this->assertFalse($this->is_submission_erased($this->submission111));
 436          $this->assertFalse($this->is_submission_erased($this->submission121));
 437  
 438          // Student1 has self-assessed one their submission.
 439          $this->assertFalse($this->is_given_assessment_erased($this->assessment1111));
 440          $this->assertFalse($this->is_received_assessment_erased($this->assessment1111));
 441  
 442          // Student2 and student3 peer-assessed student1's submission.
 443          $this->assertFalse($this->is_given_assessment_erased($this->assessment1112));
 444          $this->assertFalse($this->is_given_assessment_erased($this->assessment1113));
 445  
 446          // Delete data owned by student1 and student3 in the workshop11.
 447  
 448          $context11 = \context_module::instance($this->workshop11->cmid);
 449  
 450          $approveduserlist = new \core_privacy\local\request\approved_userlist($context11, 'mod_workshop', [
 451              $this->student1->id,
 452              $this->student3->id,
 453          ]);
 454          \mod_workshop\privacy\provider::delete_data_for_users($approveduserlist);
 455  
 456          // Student1's submission is erased in workshop11 but not in the other workshop12.
 457          $this->assertTrue($this->is_submission_erased($this->submission111));
 458          $this->assertFalse($this->is_submission_erased($this->submission121));
 459  
 460          // Student1's self-assessment is erased.
 461          $this->assertTrue($this->is_given_assessment_erased($this->assessment1111));
 462          $this->assertTrue($this->is_received_assessment_erased($this->assessment1111));
 463  
 464          // Student1's received peer-assessments are also erased because they are "owned" by the recipient of the assessment.
 465          $this->assertTrue($this->is_received_assessment_erased($this->assessment1112));
 466          $this->assertTrue($this->is_received_assessment_erased($this->assessment1113));
 467  
 468          // Student2's owned data in the given assessment are not erased.
 469          $this->assertFalse($this->is_given_assessment_erased($this->assessment1112));
 470  
 471          // Student3's owned data in the given assessment were erased because she/he was in the userlist.
 472          $this->assertTrue($this->is_given_assessment_erased($this->assessment1113));
 473  
 474          // Personal data in other contexts are not affected.
 475          $this->assertFalse($this->is_submission_erased($this->submission121));
 476          $this->assertFalse($this->is_given_assessment_erased($this->assessment2121));
 477          $this->assertFalse($this->is_received_assessment_erased($this->assessment2121));
 478      }
 479  
 480      /**
 481       * Check if the given submission has the author's personal data erased.
 482       *
 483       * @param int $submissionid Identifier of the submission.
 484       * @return boolean
 485       */
 486      protected function is_submission_erased(int $submissionid) {
 487          global $DB;
 488  
 489          $submission = $DB->get_record('workshop_submissions', ['id' => $submissionid], 'id, title, content', MUST_EXIST);
 490  
 491          $titledeleted = $submission->title === get_string('privacy:request:delete:title', 'mod_workshop');
 492          $contentdeleted = $submission->content === get_string('privacy:request:delete:content', 'mod_workshop');
 493  
 494          if ($titledeleted && $contentdeleted) {
 495              return true;
 496  
 497          } else {
 498              return false;
 499          }
 500      }
 501  
 502      /**
 503       * Check is the received assessment has recipient's (author's) personal data erased.
 504       *
 505       * @param int $assessmentid Identifier of the assessment.
 506       * @return boolean
 507       */
 508      protected function is_received_assessment_erased(int $assessmentid) {
 509          global $DB;
 510  
 511          $assessment = $DB->get_record('workshop_assessments', ['id' => $assessmentid], 'id, feedbackauthor', MUST_EXIST);
 512  
 513          if ($assessment->feedbackauthor === get_string('privacy:request:delete:content', 'mod_workshop')) {
 514              return true;
 515  
 516          } else {
 517              return false;
 518          }
 519      }
 520  
 521      /**
 522       * Check is the given assessment has reviewer's personal data erased.
 523       *
 524       * @param int $assessmentid Identifier of the assessment.
 525       * @return boolean
 526       */
 527      protected function is_given_assessment_erased(int $assessmentid) {
 528          global $DB;
 529  
 530          $assessment = $DB->get_record('workshop_assessments', ['id' => $assessmentid], 'id, feedbackreviewer', MUST_EXIST);
 531  
 532          if ($assessment->feedbackreviewer === get_string('privacy:request:delete:content', 'mod_workshop')) {
 533              return true;
 534  
 535          } else {
 536              return false;
 537          }
 538      }
 539  }