Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]

   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   * This file contains the definition for the library class for PDF feedback plugin
  19   *
  20   *
  21   * @package   assignfeedback_editpdf
  22   * @copyright 2012 Davo Smith
  23   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  use \assignfeedback_editpdf\document_services;
  29  use \assignfeedback_editpdf\page_editor;
  30  
  31  /**
  32   * library class for editpdf feedback plugin extending feedback plugin base class
  33   *
  34   * @package   assignfeedback_editpdf
  35   * @copyright 2012 Davo Smith
  36   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class assign_feedback_editpdf extends assign_feedback_plugin {
  39  
  40      /** @var boolean|null $enabledcache Cached lookup of the is_enabled function */
  41      private $enabledcache = null;
  42  
  43      /**
  44       * Get the name of the file feedback plugin
  45       * @return string
  46       */
  47      public function get_name() {
  48          return get_string('pluginname', 'assignfeedback_editpdf');
  49      }
  50  
  51      /**
  52       * Create a widget for rendering the editor.
  53       *
  54       * @param int $userid
  55       * @param stdClass $grade
  56       * @param bool $readonly
  57       * @return assignfeedback_editpdf_widget
  58       */
  59      public function get_widget($userid, $grade, $readonly) {
  60          $attempt = -1;
  61          if ($grade && isset($grade->attemptnumber)) {
  62              $attempt = $grade->attemptnumber;
  63          } else {
  64              $grade = $this->assignment->get_user_grade($userid, true);
  65          }
  66  
  67          $feedbackfile = document_services::get_feedback_document(
  68              $this->assignment->get_instance()->id,
  69              $userid,
  70              $attempt
  71          );
  72  
  73          $stampfiles = array();
  74          $fs = get_file_storage();
  75          $syscontext = context_system::instance();
  76          $asscontext = $this->assignment->get_context();
  77  
  78          // Three file areas are used for stamps.
  79          // Current stamps are those configured as a site administration setting to be available for new uses.
  80          // When a stamp is removed from this filearea it is no longer available for new grade items.
  81          $currentstamps = $fs->get_area_files($syscontext->id, 'assignfeedback_editpdf', 'stamps', 0, 'filename', false);
  82  
  83          // Grade stamps are those which have been assigned for a specific grade item.
  84          // The stamps associated with a grade item are always used for that grade item, even if the stamp is removed
  85          // from the list of current stamps.
  86          $gradestamps = $fs->get_area_files($asscontext->id, 'assignfeedback_editpdf', 'stamps', $grade->id, 'filename', false);
  87  
  88          // The system stamps are perpetual and always exist.
  89          // They allow Moodle to serve a common URL for all users for any possible combination of stamps.
  90          // Files in the perpetual stamp filearea are within the system context, in itemid 0, and use the original stamps
  91          // contenthash as a folder name. This ensures that the combination of stamp filename, and stamp file content is
  92          // unique.
  93          $systemstamps = $fs->get_area_files($syscontext->id, 'assignfeedback_editpdf', 'systemstamps', 0, 'filename', false);
  94  
  95          // First check that all current stamps are listed in the grade stamps.
  96          foreach ($currentstamps as $stamp) {
  97              // Ensure that the current stamp is in the list of perpetual stamps.
  98              $systempathnamehash = $this->get_system_stamp_path($stamp);
  99              if (!array_key_exists($systempathnamehash, $systemstamps)) {
 100                  $filerecord = (object) [
 101                      'filearea' => 'systemstamps',
 102                      'filepath' => '/' . $stamp->get_contenthash() . '/',
 103                  ];
 104                  $newstamp = $fs->create_file_from_storedfile($filerecord, $stamp);
 105                  $systemstamps[$newstamp->get_pathnamehash()] = $newstamp;
 106              }
 107  
 108              // Ensure that the current stamp is in the list of stamps for the current grade item.
 109              $gradeitempathhash = $this->get_assignment_stamp_path($stamp, $grade->id);
 110              if (!array_key_exists($gradeitempathhash, $gradestamps)) {
 111                  $filerecord = (object) [
 112                      'contextid' => $asscontext->id,
 113                      'filearea' => 'stamps',
 114                      'itemid' => $grade->id,
 115                  ];
 116                  $newstamp = $fs->create_file_from_storedfile($filerecord, $stamp);
 117                  $gradestamps[$newstamp->get_pathnamehash()] = $newstamp;
 118              }
 119          }
 120  
 121          foreach ($gradestamps as $stamp) {
 122              // All gradestamps should be available in the systemstamps filearea, but some legacy stamps may not be.
 123              // These need to be copied over.
 124              // Note: This should ideally be performed as an upgrade step, but there may be other cases that these do not
 125              // exist, for example restored backups.
 126              // In any case this is a cheap operation as it is solely performing an array lookup.
 127              $systempathnamehash = $this->get_system_stamp_path($stamp);
 128              if (!array_key_exists($systempathnamehash, $systemstamps)) {
 129                  $filerecord = (object) [
 130                      'contextid' => $syscontext->id,
 131                      'itemid' => 0,
 132                      'filearea' => 'systemstamps',
 133                      'filepath' => '/' . $stamp->get_contenthash() . '/',
 134                  ];
 135                  $systemstamp = $fs->create_file_from_storedfile($filerecord, $stamp);
 136                  $systemstamps[$systemstamp->get_pathnamehash()] = $systemstamp;
 137              }
 138  
 139              // Always serve the perpetual system stamp.
 140              // This ensures that the stamp is highly cached and reduces the hit on the application server.
 141              $gradestamp = $systemstamps[$systempathnamehash];
 142              $url = moodle_url::make_pluginfile_url(
 143                  $gradestamp->get_contextid(),
 144                  $gradestamp->get_component(),
 145                  $gradestamp->get_filearea(),
 146                  null,
 147                  $gradestamp->get_filepath(),
 148                  $gradestamp->get_filename(),
 149                  false
 150              );
 151              array_push($stampfiles, $url->out());
 152          }
 153  
 154          $url = false;
 155          $filename = '';
 156          if ($feedbackfile) {
 157              $url = moodle_url::make_pluginfile_url(
 158                  $this->assignment->get_context()->id,
 159                  'assignfeedback_editpdf',
 160                  document_services::FINAL_PDF_FILEAREA,
 161                  $grade->id,
 162                  '/',
 163                  $feedbackfile->get_filename(),
 164                  false
 165              );
 166             $filename = $feedbackfile->get_filename();
 167          }
 168  
 169          $widget = new assignfeedback_editpdf_widget(
 170              $this->assignment->get_instance()->id,
 171              $userid,
 172              $attempt,
 173              $url,
 174              $filename,
 175              $stampfiles,
 176              $readonly
 177          );
 178          return $widget;
 179      }
 180  
 181      /**
 182       * Get the pathnamehash for the specified stamp if in the system stamps.
 183       *
 184       * @param   stored_file $file
 185       * @return  string
 186       */
 187      protected function get_system_stamp_path(stored_file $stamp): string {
 188          $systemcontext = context_system::instance();
 189  
 190          return file_storage::get_pathname_hash(
 191              $systemcontext->id,
 192              'assignfeedback_editpdf',
 193              'systemstamps',
 194              0,
 195              '/' . $stamp->get_contenthash() . '/',
 196              $stamp->get_filename()
 197          );
 198      }
 199  
 200      /**
 201       * Get the pathnamehash for the specified stamp if in the current assignment stamps.
 202       *
 203       * @param   stored_file $file
 204       * @param   int $gradeid
 205       * @return  string
 206       */
 207      protected function get_assignment_stamp_path(stored_file $stamp, int $gradeid): string {
 208          return file_storage::get_pathname_hash(
 209              $this->assignment->get_context()->id,
 210              'assignfeedback_editpdf',
 211              'stamps',
 212              $gradeid,
 213              $stamp->get_filepath(),
 214              $stamp->get_filename()
 215          );
 216      }
 217  
 218      /**
 219       * Get form elements for grading form
 220       *
 221       * @param stdClass $grade
 222       * @param MoodleQuickForm $mform
 223       * @param stdClass $data
 224       * @param int $userid
 225       * @return bool true if elements were added to the form
 226       */
 227      public function get_form_elements_for_user($grade, MoodleQuickForm $mform, stdClass $data, $userid) {
 228          global $PAGE;
 229  
 230          $attempt = -1;
 231          if ($grade) {
 232              $attempt = $grade->attemptnumber;
 233          }
 234  
 235          $renderer = $PAGE->get_renderer('assignfeedback_editpdf');
 236  
 237          // Links to download the generated pdf...
 238          if ($attempt > -1 && page_editor::has_annotations_or_comments($grade->id, false)) {
 239              $html = $this->assignment->render_area_files('assignfeedback_editpdf',
 240                                                           document_services::FINAL_PDF_FILEAREA,
 241                                                           $grade->id);
 242              $mform->addElement('static', 'editpdf_files', get_string('downloadfeedback', 'assignfeedback_editpdf'), $html);
 243          }
 244  
 245          $widget = $this->get_widget($userid, $grade, false);
 246  
 247          $html = $renderer->render($widget);
 248          $mform->addElement('static', 'editpdf', get_string('editpdf', 'assignfeedback_editpdf'), $html);
 249          $mform->addHelpButton('editpdf', 'editpdf', 'assignfeedback_editpdf');
 250          $mform->addElement('hidden', 'editpdf_source_userid', $userid);
 251          $mform->setType('editpdf_source_userid', PARAM_INT);
 252          $mform->setConstant('editpdf_source_userid', $userid);
 253      }
 254  
 255      /**
 256       * Check to see if the grade feedback for the pdf has been modified.
 257       *
 258       * @param stdClass $grade Grade object.
 259       * @param stdClass $data Data from the form submission (not used).
 260       * @return boolean True if the pdf has been modified, else false.
 261       */
 262      public function is_feedback_modified(stdClass $grade, stdClass $data) {
 263          // We only need to know if the source user's PDF has changed. If so then all
 264          // following users will have the same status. If it's only an individual annotation
 265          // then only one user will come through this method.
 266          // Source user id is only added to the form if there was a pdf.
 267          if (!empty($data->editpdf_source_userid)) {
 268              $sourceuserid = $data->editpdf_source_userid;
 269              // Retrieve the grade information for the source user.
 270              $sourcegrade = $this->assignment->get_user_grade($sourceuserid, true, $grade->attemptnumber);
 271              $pagenumbercount = document_services::page_number_for_attempt($this->assignment, $sourceuserid, $sourcegrade->attemptnumber);
 272              for ($i = 0; $i < $pagenumbercount; $i++) {
 273                  // Select all annotations.
 274                  $draftannotations = page_editor::get_annotations($sourcegrade->id, $i, true);
 275                  $nondraftannotations = page_editor::get_annotations($grade->id, $i, false);
 276                  // Check to see if the count is the same.
 277                  if (count($draftannotations) != count($nondraftannotations)) {
 278                      // The count is different so we have a modification.
 279                      return true;
 280                  } else {
 281                      $matches = 0;
 282                      // Have a closer look and see if the draft files match all the non draft files.
 283                      foreach ($nondraftannotations as $ndannotation) {
 284                          foreach ($draftannotations as $dannotation) {
 285                              foreach ($ndannotation as $key => $value) {
 286                                  // As the $draft was included in the class annotation,
 287                                  // it is necessary to omit it in the condition below as well,
 288                                  // otherwise, an error would be raised.
 289                                  if ($key != 'id' && $key != 'draft' && $value != $dannotation->{$key}) {
 290                                      continue 2;
 291                                  }
 292                              }
 293                              $matches++;
 294                          }
 295                      }
 296                      if ($matches !== count($nondraftannotations)) {
 297                          return true;
 298                      }
 299                  }
 300                  // Select all comments.
 301                  $draftcomments = page_editor::get_comments($sourcegrade->id, $i, true);
 302                  $nondraftcomments = page_editor::get_comments($grade->id, $i, false);
 303                  if (count($draftcomments) != count($nondraftcomments)) {
 304                      return true;
 305                  } else {
 306                      // Go for a closer inspection.
 307                      $matches = 0;
 308                      foreach ($nondraftcomments as $ndcomment) {
 309                          foreach ($draftcomments as $dcomment) {
 310                              foreach ($ndcomment as $key => $value) {
 311                                  // As the $draft was included in the class comment,
 312                                  // it is necessary to omit it in the condition below as well,
 313                                  // otherwise, an error would be raised.
 314                                  if ($key != 'id' && $key != 'draft' && $value != $dcomment->{$key}) {
 315                                      continue 2;
 316                                  }
 317                              }
 318                              $matches++;
 319                          }
 320                      }
 321                      if ($matches !== count($nondraftcomments)) {
 322                          return true;
 323                      }
 324                  }
 325              }
 326          }
 327          return false;
 328      }
 329  
 330      /**
 331       * Generate the pdf.
 332       *
 333       * @param stdClass $grade
 334       * @param stdClass $data
 335       * @return bool
 336       */
 337      public function save(stdClass $grade, stdClass $data) {
 338          // Source user id is only added to the form if there was a pdf.
 339          if (!empty($data->editpdf_source_userid)) {
 340              $sourceuserid = $data->editpdf_source_userid;
 341              // Copy drafts annotations and comments if current user is different to sourceuserid.
 342              if ($sourceuserid != $grade->userid) {
 343                  page_editor::copy_drafts_from_to($this->assignment, $grade, $sourceuserid);
 344              }
 345          }
 346          if (page_editor::has_annotations_or_comments($grade->id, true)) {
 347              document_services::generate_feedback_document($this->assignment, $grade->userid, $grade->attemptnumber);
 348          }
 349  
 350          return true;
 351      }
 352  
 353      /**
 354       * Display the list of files in the feedback status table.
 355       *
 356       * @param stdClass $grade
 357       * @param bool $showviewlink (Always set to false).
 358       * @return string
 359       */
 360      public function view_summary(stdClass $grade, & $showviewlink) {
 361          $showviewlink = false;
 362          return $this->view($grade);
 363      }
 364  
 365      /**
 366       * Display the list of files in the feedback status table.
 367       *
 368       * @param stdClass $grade
 369       * @return string
 370       */
 371      public function view(stdClass $grade) {
 372          global $PAGE;
 373          $html = '';
 374          // Show a link to download the pdf.
 375          if (page_editor::has_annotations_or_comments($grade->id, false)) {
 376              $html = $this->assignment->render_area_files('assignfeedback_editpdf',
 377                                                           document_services::FINAL_PDF_FILEAREA,
 378                                                           $grade->id);
 379  
 380              // Also show the link to the read-only interface.
 381              $renderer = $PAGE->get_renderer('assignfeedback_editpdf');
 382              $widget = $this->get_widget($grade->userid, $grade, true);
 383  
 384              $html .= $renderer->render($widget);
 385          }
 386          return $html;
 387      }
 388  
 389      /**
 390       * Return true if there are no released comments/annotations.
 391       *
 392       * @param stdClass $grade
 393       */
 394      public function is_empty(stdClass $grade) {
 395          global $DB;
 396  
 397          $comments = $DB->count_records('assignfeedback_editpdf_cmnt', array('gradeid'=>$grade->id, 'draft'=>0));
 398          $annotations = $DB->count_records('assignfeedback_editpdf_annot', array('gradeid'=>$grade->id, 'draft'=>0));
 399          return $comments == 0 && $annotations == 0;
 400      }
 401  
 402      /**
 403       * The assignment has been deleted - remove the plugin specific data
 404       *
 405       * @return bool
 406       */
 407      public function delete_instance() {
 408          global $DB;
 409          $grades = $DB->get_records('assign_grades', array('assignment'=>$this->assignment->get_instance()->id), '', 'id');
 410          if ($grades) {
 411              list($gradeids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED);
 412              $DB->delete_records_select('assignfeedback_editpdf_annot', 'gradeid ' . $gradeids, $params);
 413              $DB->delete_records_select('assignfeedback_editpdf_cmnt', 'gradeid ' . $gradeids, $params);
 414              $DB->delete_records_select('assignfeedback_editpdf_rot', 'gradeid ' . $gradeids, $params);
 415          }
 416          return true;
 417      }
 418  
 419      /**
 420       * Determine if ghostscript is available and working.
 421       *
 422       * @return bool
 423       */
 424      public function is_available() {
 425          if ($this->enabledcache === null) {
 426              $testpath = assignfeedback_editpdf\pdf::test_gs_path(false);
 427              $this->enabledcache = ($testpath->status == assignfeedback_editpdf\pdf::GSPATH_OK);
 428          }
 429          return $this->enabledcache;
 430      }
 431      /**
 432       * Prevent enabling this plugin if ghostscript is not available.
 433       *
 434       * @return bool false
 435       */
 436      public function is_configurable() {
 437          return $this->is_available();
 438      }
 439  
 440      /**
 441       * Get file areas returns a list of areas this plugin stores files.
 442       *
 443       * @return array - An array of fileareas (keys) and descriptions (values)
 444       */
 445      public function get_file_areas() {
 446          return [
 447              document_services::FINAL_PDF_FILEAREA => $this->get_name(),
 448              document_services::COMBINED_PDF_FILEAREA => $this->get_name(),
 449              document_services::PARTIAL_PDF_FILEAREA => $this->get_name(),
 450              document_services::IMPORT_HTML_FILEAREA => $this->get_name(),
 451              document_services::PAGE_IMAGE_FILEAREA => $this->get_name(),
 452              document_services::PAGE_IMAGE_READONLY_FILEAREA => $this->get_name(),
 453              document_services::STAMPS_FILEAREA => $this->get_name(),
 454              document_services::TMP_JPG_TO_PDF_FILEAREA => $this->get_name(),
 455              document_services::TMP_ROTATED_JPG_FILEAREA => $this->get_name()
 456          ];
 457      }
 458  
 459      /**
 460       * Get all file areas for user data related to this plugin.
 461       *
 462       * @return array - An array of user data fileareas (keys) and descriptions (values)
 463       */
 464      public function get_user_data_file_areas(): array {
 465          return [
 466              document_services::FINAL_PDF_FILEAREA => $this->get_name(),
 467          ];
 468      }
 469  
 470      /**
 471       * This plugin will inject content into the review panel with javascript.
 472       * @return bool true
 473       */
 474      public function supports_review_panel() {
 475          return true;
 476      }
 477  
 478      /**
 479       * Return the plugin configs for external functions.
 480       *
 481       * @return array the list of settings
 482       * @since Moodle 3.2
 483       */
 484      public function get_config_for_external() {
 485          return (array) $this->get_config();
 486      }
 487  }