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