Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

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