Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 311 and 402] [Versions 400 and 402] [Versions 401 and 402]

   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  namespace assignfeedback_editpdf;
  18  
  19  use mod_assign_test_generator;
  20  
  21  defined('MOODLE_INTERNAL') || die();
  22  
  23  global $CFG;
  24  require_once($CFG->dirroot . '/mod/assign/tests/generator.php');
  25  
  26  /**
  27   * Unit tests for assignfeedback_editpdf\comments_quick_list
  28   *
  29   * @package    assignfeedback_editpdf
  30   * @category   test
  31   * @copyright  2013 Damyon Wiese
  32   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  class feedback_test extends \advanced_testcase {
  35  
  36      // Use the generator helper.
  37      use mod_assign_test_generator;
  38  
  39      /**
  40       * Ensure that GS is available.
  41       */
  42      protected function require_ghostscript() {
  43          // Skip this test if ghostscript is not supported.
  44          $result = pdf::test_gs_path(false);
  45          if ($result->status !== pdf::GSPATH_OK) {
  46              $this->markTestSkipped('Ghostscript not setup');
  47          }
  48      }
  49  
  50      /**
  51       * Helper method to add a file to a submission.
  52       *
  53       * @param \stdClass $student Student submitting.
  54       * @param \assign   $assign Assignment being submitted.
  55       * @param bool     $textfile Use textfile fixture instead of pdf.
  56       */
  57      protected function add_file_submission($student, $assign, $textfile = false) {
  58          global $CFG;
  59  
  60          $this->setUser($student);
  61  
  62          // Create a file submission with the test pdf.
  63          $submission = $assign->get_user_submission($student->id, true);
  64  
  65          $fs = get_file_storage();
  66          $filerecord = (object) array(
  67              'contextid' => $assign->get_context()->id,
  68              'component' => 'assignsubmission_file',
  69              'filearea' => ASSIGNSUBMISSION_FILE_FILEAREA,
  70              'itemid' => $submission->id,
  71              'filepath' => '/',
  72              'filename' => $textfile ? 'submission.txt' : 'submission.pdf'
  73          );
  74          $sourcefile = $CFG->dirroot . '/mod/assign/feedback/editpdf/tests/fixtures/submission.' . ($textfile ? 'txt' : 'pdf');
  75          $fs->create_file_from_pathname($filerecord, $sourcefile);
  76  
  77          $data = new \stdClass();
  78          $plugin = $assign->get_submission_plugin_by_type('file');
  79          $plugin->save($submission, $data);
  80      }
  81  
  82      public function test_comments_quick_list() {
  83          $this->resetAfterTest();
  84          $course = $this->getDataGenerator()->create_course();
  85          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
  86  
  87          $this->setUser($teacher);
  88  
  89          $this->assertEmpty(comments_quick_list::get_comments());
  90  
  91          $comment = comments_quick_list::add_comment('test', 45, 'red');
  92          $comments = comments_quick_list::get_comments();
  93          $this->assertEquals(count($comments), 1);
  94          $first = reset($comments);
  95          $this->assertEquals($comment, $first);
  96  
  97          $commentbyid = comments_quick_list::get_comment($comment->id);
  98          $this->assertEquals($comment, $commentbyid);
  99  
 100          $this->assertTrue(comments_quick_list::remove_comment($comment->id));
 101  
 102          $comments = comments_quick_list::get_comments();
 103          $this->assertEmpty($comments);
 104      }
 105  
 106      public function test_page_editor() {
 107          $this->resetAfterTest();
 108          $course = $this->getDataGenerator()->create_course();
 109          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 110          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 111          $assign = $this->create_instance($course, [
 112                  'assignsubmission_onlinetext_enabled' => 1,
 113                  'assignsubmission_file_enabled' => 1,
 114                  'assignsubmission_file_maxfiles' => 1,
 115                  'assignfeedback_editpdf_enabled' => 1,
 116                  'assignsubmission_file_maxsizebytes' => 1000000,
 117              ]);
 118  
 119          // Add the standard submission.
 120          $this->add_file_submission($student, $assign);
 121  
 122          $this->setUser($teacher);
 123  
 124          $grade = $assign->get_user_grade($student->id, true);
 125  
 126          $notempty = page_editor::has_annotations_or_comments($grade->id, false);
 127          $this->assertFalse($notempty);
 128  
 129          $comment = new comment();
 130          $comment->rawtext = 'Comment text';
 131          $comment->width = 100;
 132          $comment->x = 100;
 133          $comment->y = 100;
 134          $comment->colour = 'red';
 135  
 136          $comment2 = new comment();
 137          $comment2->rawtext = 'Comment text 2';
 138          $comment2->width = 100;
 139          $comment2->x = 200;
 140          $comment2->y = 100;
 141          $comment2->colour = 'clear';
 142  
 143          page_editor::set_comments($grade->id, 0, array($comment, $comment2));
 144  
 145          $annotation = new annotation();
 146          $annotation->path = '';
 147          $annotation->x = 100;
 148          $annotation->y = 100;
 149          $annotation->endx = 200;
 150          $annotation->endy = 200;
 151          $annotation->type = 'line';
 152          $annotation->colour = 'red';
 153  
 154          $annotation2 = new annotation();
 155          $annotation2->path = '';
 156          $annotation2->x = 100;
 157          $annotation2->y = 100;
 158          $annotation2->endx = 200;
 159          $annotation2->endy = 200;
 160          $annotation2->type = 'rectangle';
 161          $annotation2->colour = 'yellow';
 162  
 163          page_editor::set_annotations($grade->id, 0, array($annotation, $annotation2));
 164  
 165          // Still empty because all edits are still drafts.
 166          $this->assertFalse(page_editor::has_annotations_or_comments($grade->id, false));
 167  
 168          $comments = page_editor::get_comments($grade->id, 0, false);
 169          $this->assertEmpty($comments);
 170  
 171          $comments = page_editor::get_comments($grade->id, 0, true);
 172          $this->assertEquals(count($comments), 2);
 173  
 174          $annotations = page_editor::get_annotations($grade->id, 0, false);
 175          $this->assertEmpty($annotations);
 176  
 177          $annotations = page_editor::get_annotations($grade->id, 0, true);
 178          $this->assertEquals(count($annotations), 2);
 179  
 180          $comment = reset($comments);
 181          $annotation = reset($annotations);
 182  
 183          page_editor::remove_comment($comment->id);
 184          page_editor::remove_annotation($annotation->id);
 185  
 186          $comments = page_editor::get_comments($grade->id, 0, true);
 187          $this->assertEquals(count($comments), 1);
 188  
 189          $annotations = page_editor::get_annotations($grade->id, 0, true);
 190          $this->assertEquals(count($annotations), 1);
 191  
 192          // Release the drafts.
 193          page_editor::release_drafts($grade->id);
 194  
 195          $notempty = page_editor::has_annotations_or_comments($grade->id, false);
 196          $this->assertTrue($notempty);
 197  
 198          // Unrelease the drafts.
 199          page_editor::unrelease_drafts($grade->id);
 200  
 201          $notempty = page_editor::has_annotations_or_comments($grade->id, false);
 202          $this->assertFalse($notempty);
 203      }
 204  
 205      public function test_document_services() {
 206          $this->require_ghostscript();
 207          $this->resetAfterTest();
 208          $course = $this->getDataGenerator()->create_course();
 209          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 210          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 211          $assign = $this->create_instance($course, [
 212                  'assignsubmission_onlinetext_enabled' => 1,
 213                  'assignsubmission_file_enabled' => 1,
 214                  'assignsubmission_file_maxfiles' => 1,
 215                  'assignfeedback_editpdf_enabled' => 1,
 216                  'assignsubmission_file_maxsizebytes' => 1000000,
 217              ]);
 218  
 219          // Add the standard submission.
 220          $this->add_file_submission($student, $assign);
 221  
 222          $this->setUser($teacher);
 223  
 224          $grade = $assign->get_user_grade($student->id, true);
 225  
 226          $contextid = $assign->get_context()->id;
 227          $component = 'assignfeedback_editpdf';
 228          $filearea = document_services::COMBINED_PDF_FILEAREA;
 229          $itemid = $grade->id;
 230          $filepath = '/';
 231          $filename = document_services::COMBINED_PDF_FILENAME;
 232          $fs = \get_file_storage();
 233  
 234          // Generate a blank combined pdf.
 235          $record = new \stdClass();
 236          $record->contextid = $contextid;
 237          $record->component = $component;
 238          $record->filearea = $filearea;
 239          $record->itemid = $itemid;
 240          $record->filepath = $filepath;
 241          $record->filename = $filename;
 242          $fs->create_file_from_string($record, base64_decode(document_services::BLANK_PDF_BASE64));
 243  
 244          // Verify that the blank combined pdf has the expected hash.
 245          $combinedpdf = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename);
 246          $this->assertEquals($combinedpdf->get_contenthash(), document_services::BLANK_PDF_HASH);
 247  
 248          // Generate page images and verify that the combined pdf has been replaced.
 249          document_services::get_page_images_for_attempt($assign, $student->id, -1);
 250          $combinedpdf = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename);
 251          $this->assertNotEquals($combinedpdf->get_contenthash(), document_services::BLANK_PDF_HASH);
 252  
 253          $notempty = page_editor::has_annotations_or_comments($grade->id, false);
 254          $this->assertFalse($notempty);
 255  
 256          $comment = new comment();
 257  
 258          // Use some different charset in the comment text.
 259          $comment->rawtext = 'Testing example: בקלות ואמנות';
 260          $comment->width = 100;
 261          $comment->x = 100;
 262          $comment->y = 100;
 263          $comment->colour = 'red';
 264  
 265          page_editor::set_comments($grade->id, 0, array($comment));
 266  
 267          $annotations = array();
 268  
 269          $annotation = new annotation();
 270          $annotation->path = '';
 271          $annotation->x = 100;
 272          $annotation->y = 100;
 273          $annotation->endx = 200;
 274          $annotation->endy = 200;
 275          $annotation->type = 'line';
 276          $annotation->colour = 'red';
 277          array_push($annotations, $annotation);
 278  
 279          $annotation = new annotation();
 280          $annotation->path = '';
 281          $annotation->x = 100;
 282          $annotation->y = 100;
 283          $annotation->endx = 200;
 284          $annotation->endy = 200;
 285          $annotation->type = 'rectangle';
 286          $annotation->colour = 'yellow';
 287          array_push($annotations, $annotation);
 288  
 289          $annotation = new annotation();
 290          $annotation->path = '';
 291          $annotation->x = 100;
 292          $annotation->y = 100;
 293          $annotation->endx = 200;
 294          $annotation->endy = 200;
 295          $annotation->type = 'oval';
 296          $annotation->colour = 'green';
 297          array_push($annotations, $annotation);
 298  
 299          $annotation = new annotation();
 300          $annotation->path = '';
 301          $annotation->x = 100;
 302          $annotation->y = 100;
 303          $annotation->endx = 200;
 304          $annotation->endy = 116;
 305          $annotation->type = 'highlight';
 306          $annotation->colour = 'blue';
 307          array_push($annotations, $annotation);
 308  
 309          $annotation = new annotation();
 310          $annotation->path = '100,100:105,105:110,100';
 311          $annotation->x = 100;
 312          $annotation->y = 100;
 313          $annotation->endx = 110;
 314          $annotation->endy = 105;
 315          $annotation->type = 'pen';
 316          $annotation->colour = 'black';
 317          array_push($annotations, $annotation);
 318          page_editor::set_annotations($grade->id, 0, $annotations);
 319  
 320          page_editor::release_drafts($grade->id);
 321  
 322          $notempty = page_editor::has_annotations_or_comments($grade->id, false);
 323  
 324          $this->assertTrue($notempty);
 325  
 326          $file = document_services::generate_feedback_document($assign->get_instance()->id, $grade->userid, $grade->attemptnumber);
 327          $this->assertNotEmpty($file);
 328  
 329          $file2 = document_services::get_feedback_document($assign->get_instance()->id, $grade->userid, $grade->attemptnumber);
 330  
 331          $this->assertEquals($file, $file2);
 332  
 333          document_services::delete_feedback_document($assign->get_instance()->id, $grade->userid, $grade->attemptnumber);
 334          $file3 = document_services::get_feedback_document($assign->get_instance()->id, $grade->userid, $grade->attemptnumber);
 335  
 336          $this->assertEmpty($file3);
 337      }
 338  
 339      /**
 340       * Test Convert submission ad-hoc task.
 341       *
 342       * @covers \assignfeedback_editpdf\task\convert_submission
 343       */
 344      public function test_conversion_task() {
 345          $this->require_ghostscript();
 346          $this->resetAfterTest();
 347          \core\cron::setup_user();
 348  
 349          $course = $this->getDataGenerator()->create_course();
 350          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 351          $assignopts = [
 352              'assignsubmission_file_enabled' => 1,
 353              'assignsubmission_file_maxfiles' => 1,
 354              'assignfeedback_editpdf_enabled' => 1,
 355              'assignsubmission_file_maxsizebytes' => 1000000,
 356          ];
 357          $assign = $this->create_instance($course, $assignopts);
 358  
 359          // Add the standard submission.
 360          $this->add_file_submission($student, $assign);
 361  
 362          // Run the conversion task.
 363          $task = \core\task\manager::get_next_adhoc_task(time());
 364          ob_start();
 365          $task->execute();
 366          \core\task\manager::adhoc_task_complete($task);
 367          $output = ob_get_clean();
 368  
 369          // Confirm, that submission has been converted and the task queue is now empty.
 370          $this->assertStringContainsString('Converting submission for user id ' . $student->id, $output);
 371          $this->assertStringContainsString('The document has been successfully converted', $output);
 372          $this->assertNull(\core\task\manager::get_next_adhoc_task(time()));
 373  
 374          // Trigger a re-queue by 'updating' a submission.
 375          $submission = $assign->get_user_submission($student->id, true);
 376          $plugin = $assign->get_submission_plugin_by_type('file');
 377          $plugin->save($submission, (new \stdClass));
 378  
 379          $task = \core\task\manager::get_next_adhoc_task(time());
 380          // Verify that queued a conversion task.
 381          $this->assertNotNull($task);
 382  
 383          ob_start();
 384          $task->execute();
 385          \core\task\manager::adhoc_task_complete($task);
 386          $output = ob_get_clean();
 387  
 388          // Confirm, that submission has been converted and the task queue is now empty.
 389          $this->assertStringContainsString('Converting submission for user id ' . $student->id, $output);
 390          $this->assertStringContainsString('The document has been successfully converted', $output);
 391          $this->assertNull(\core\task\manager::get_next_adhoc_task(time()));
 392      }
 393  
 394      /**
 395       * Test that modifying the annotated pdf form return true when modified
 396       * and false when not modified.
 397       */
 398      public function test_is_feedback_modified() {
 399          $this->resetAfterTest();
 400          $course = $this->getDataGenerator()->create_course();
 401          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 402          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 403          $assign = $this->create_instance($course, [
 404                  'assignsubmission_onlinetext_enabled' => 1,
 405                  'assignsubmission_file_enabled' => 1,
 406                  'assignsubmission_file_maxfiles' => 1,
 407                  'assignfeedback_editpdf_enabled' => 1,
 408                  'assignsubmission_file_maxsizebytes' => 1000000,
 409              ]);
 410  
 411          // Add the standard submission.
 412          $this->add_file_submission($student, $assign);
 413  
 414          $this->setUser($teacher);
 415          $grade = $assign->get_user_grade($student->id, true);
 416  
 417          $notempty = page_editor::has_annotations_or_comments($grade->id, false);
 418          $this->assertFalse($notempty);
 419  
 420          $comment = new comment();
 421  
 422          $comment->rawtext = 'Comment text';
 423          $comment->width = 100;
 424          $comment->x = 100;
 425          $comment->y = 100;
 426          $comment->colour = 'red';
 427  
 428          page_editor::set_comments($grade->id, 0, array($comment));
 429  
 430          $annotations = array();
 431  
 432          $annotation = new annotation();
 433          $annotation->path = '';
 434          $annotation->x = 100;
 435          $annotation->y = 100;
 436          $annotation->endx = 200;
 437          $annotation->endy = 200;
 438          $annotation->type = 'line';
 439          $annotation->colour = 'red';
 440          array_push($annotations, $annotation);
 441  
 442          page_editor::set_annotations($grade->id, 0, $annotations);
 443  
 444          $plugin = $assign->get_feedback_plugin_by_type('editpdf');
 445          $data = new \stdClass();
 446          $data->editpdf_source_userid = $student->id;
 447          $this->assertTrue($plugin->is_feedback_modified($grade, $data));
 448          $plugin->save($grade, $data);
 449  
 450          $annotation = new annotation();
 451          $annotation->gradeid = $grade->id;
 452          $annotation->pageno = 0;
 453          $annotation->path = '';
 454          $annotation->x = 100;
 455          $annotation->y = 100;
 456          $annotation->endx = 200;
 457          $annotation->endy = 200;
 458          $annotation->type = 'rectangle';
 459          $annotation->colour = 'yellow';
 460  
 461          $yellowannotationid = page_editor::add_annotation($annotation);
 462  
 463          // Add a comment as well.
 464          $comment = new comment();
 465          $comment->gradeid = $grade->id;
 466          $comment->pageno = 0;
 467          $comment->rawtext = 'Second Comment text';
 468          $comment->width = 100;
 469          $comment->x = 100;
 470          $comment->y = 100;
 471          $comment->colour = 'red';
 472          page_editor::add_comment($comment);
 473  
 474          $this->assertTrue($plugin->is_feedback_modified($grade, $data));
 475          $plugin->save($grade, $data);
 476  
 477          // We should have two annotations.
 478          $this->assertCount(2, page_editor::get_annotations($grade->id, 0, false));
 479          // And two comments.
 480          $this->assertCount(2, page_editor::get_comments($grade->id, 0, false));
 481  
 482          // Add one annotation and delete another.
 483          $annotation = new annotation();
 484          $annotation->gradeid = $grade->id;
 485          $annotation->pageno = 0;
 486          $annotation->path = '100,100:105,105:110,100';
 487          $annotation->x = 100;
 488          $annotation->y = 100;
 489          $annotation->endx = 110;
 490          $annotation->endy = 105;
 491          $annotation->type = 'pen';
 492          $annotation->colour = 'black';
 493          page_editor::add_annotation($annotation);
 494  
 495          $annotations = page_editor::get_annotations($grade->id, 0, true);
 496          page_editor::remove_annotation($yellowannotationid);
 497          $this->assertTrue($plugin->is_feedback_modified($grade, $data));
 498          $plugin->save($grade, $data);
 499  
 500          // We should have two annotations.
 501          $this->assertCount(2, page_editor::get_annotations($grade->id, 0, false));
 502          // And two comments.
 503          $this->assertCount(2, page_editor::get_comments($grade->id, 0, false));
 504  
 505          // Add a comment and then remove it. Should not be considered as modified.
 506          $comment = new comment();
 507          $comment->gradeid = $grade->id;
 508          $comment->pageno = 0;
 509          $comment->rawtext = 'Third Comment text';
 510          $comment->width = 400;
 511          $comment->x = 57;
 512          $comment->y = 205;
 513          $comment->colour = 'black';
 514          $comment->id = page_editor::add_comment($comment);
 515  
 516          // We should now have three comments.
 517          $this->assertCount(3, page_editor::get_comments($grade->id, 0, true));
 518          // Now delete the newest record.
 519          page_editor::remove_comment($comment->id);
 520          // Back to two comments.
 521          $this->assertCount(2, page_editor::get_comments($grade->id, 0, true));
 522          // No modification.
 523          $this->assertFalse($plugin->is_feedback_modified($grade, $data));
 524      }
 525  
 526      /**
 527       * Test that overwriting a submission file deletes any associated conversions.
 528       *
 529       * @covers \core_files\conversion::get_conversions_for_file
 530       */
 531      public function test_submission_file_overridden() {
 532          $this->resetAfterTest();
 533          $course = $this->getDataGenerator()->create_course();
 534          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 535          $assign = $this->create_instance($course, [
 536              'assignsubmission_onlinetext_enabled' => 1,
 537              'assignsubmission_file_enabled' => 1,
 538              'assignsubmission_file_maxfiles' => 1,
 539              'assignfeedback_editpdf_enabled' => 1,
 540              'assignsubmission_file_maxsizebytes' => 1000000,
 541          ]);
 542  
 543          $this->add_file_submission($student, $assign, true);
 544          $submission = $assign->get_user_submission($student->id, true);
 545  
 546          $fs = get_file_storage();
 547          $sourcefile = $fs->get_file(
 548              $assign->get_context()->id,
 549              'assignsubmission_file',
 550              ASSIGNSUBMISSION_FILE_FILEAREA,
 551              $submission->id,
 552              '/',
 553              'submission.txt'
 554          );
 555  
 556          $conversion = new \core_files\conversion(0, (object)[
 557              'sourcefileid' => $sourcefile->get_id(),
 558              'targetformat' => 'pdf'
 559          ]);
 560          $conversion->create();
 561  
 562          $conversions = \core_files\conversion::get_conversions_for_file($sourcefile, 'pdf');
 563          $this->assertCount(1, $conversions);
 564  
 565          $filerecord = (object)[
 566              'contextid' => $assign->get_context()->id,
 567              'component' => 'core',
 568              'filearea'  => 'unittest',
 569              'itemid'    => $submission->id,
 570              'filepath'  => '/',
 571              'filename'  => 'submission.txt'
 572          ];
 573  
 574          $fs = get_file_storage();
 575          $newfile = $fs->create_file_from_string($filerecord, 'something totally different');
 576          $sourcefile->replace_file_with($newfile);
 577  
 578          $conversions = \core_files\conversion::get_conversions_for_file($sourcefile, 'pdf');
 579          $this->assertCount(0, $conversions);
 580      }
 581  
 582      /**
 583       * Tests that when the plugin is not enabled for an assignment it does not create conversion tasks.
 584       *
 585       * @covers \assignfeedback_editpdf\event\observer
 586       */
 587      public function test_submission_not_enabled() {
 588          $this->require_ghostscript();
 589          $this->resetAfterTest();
 590          \core\cron::setup_user();
 591  
 592          $course = $this->getDataGenerator()->create_course();
 593          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 594          $assignopts = [
 595              'assignsubmission_file_enabled' => 1,
 596              'assignsubmission_file_maxfiles' => 1,
 597              'assignfeedback_editpdf_enabled' => 0,
 598              'assignsubmission_file_maxsizebytes' => 1000000,
 599          ];
 600          $assign = $this->create_instance($course, $assignopts);
 601  
 602          // Add the standard submission.
 603          $this->add_file_submission($student, $assign);
 604  
 605          $task = \core\task\manager::get_next_adhoc_task(time());
 606  
 607          // No task was created.
 608          $this->assertNull($task);
 609      }
 610  }