MDL-68665 assignfeedback_editpdf: Add filearea to persist stamps
[moodle.git] / mod / assign / feedback / editpdf / locallib.php
CommitLineData
5c386472
DW
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
26defined('MOODLE_INTERNAL') || die();
27
28use \assignfeedback_editpdf\document_services;
29use \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 */
38class assign_feedback_editpdf extends assign_feedback_plugin {
39
c70de965
DW
40 /** @var boolean|null $enabledcache Cached lookup of the is_enabled function */
41 private $enabledcache = null;
42
5c386472
DW
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;
fc9bc658 61 if ($grade && isset($grade->attemptnumber)) {
5c386472
DW
62 $attempt = $grade->attemptnumber;
63 } else {
64 $grade = $this->assignment->get_user_grade($userid, true);
65 }
66
67 $feedbackfile = document_services::get_feedback_document($this->assignment->get_instance()->id,
68 $userid,
69 $attempt);
70
71 $stampfiles = array();
1343d84c 72 $systemfiles = array();
5c386472
DW
73 $fs = get_file_storage();
74 $syscontext = context_system::instance();
74b4d636
JS
75 $asscontext = $this->assignment->get_context();
76
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);
81
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);
86
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);
93
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 }
5c386472 106
74b4d636
JS
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;
5c386472
DW
117 }
118 }
119
74b4d636
JS
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;
5c386472 136 }
74b4d636
JS
137
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());
5c386472
DW
151 }
152
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 }
165
5c386472
DW
166 $widget = new assignfeedback_editpdf_widget($this->assignment->get_instance()->id,
167 $userid,
168 $attempt,
169 $url,
170 $filename,
171 $stampfiles,
f7a9f1dd
AN
172 $readonly
173 );
5c386472
DW
174 return $widget;
175 }
176
74b4d636
JS
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();
185
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 }
195
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 }
213
5c386472
DW
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;
225
226 $attempt = -1;
227 if ($grade) {
228 $attempt = $grade->attemptnumber;
229 }
230
bb690849 231 $renderer = $PAGE->get_renderer('assignfeedback_editpdf');
5c386472 232
574b2f1f
DW
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 }
240
bb690849 241 $widget = $this->get_widget($userid, $grade, false);
5c386472 242
bb690849
DW
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);
5c386472
DW
249 }
250
238c05c2
AG
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) {
181c20bc
AG
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 }
4edd8e77 285 }
181c20bc 286 $matches++;
238c05c2 287 }
181c20bc
AG
288 }
289 if ($matches !== count($nondraftannotations)) {
290 return true;
238c05c2
AG
291 }
292 }
181c20bc
AG
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)) {
4edd8e77 297 return true;
181c20bc
AG
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 }
4edd8e77 307 }
181c20bc 308 $matches++;
238c05c2
AG
309 }
310 }
181c20bc
AG
311 if ($matches !== count($nondraftcomments)) {
312 return true;
313 }
4edd8e77 314 }
238c05c2
AG
315 }
316 }
317 return false;
318 }
319
5c386472
DW
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) {
177cc379
DW
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 }
028c9d20 335 }
5c386472
DW
336 if (page_editor::has_annotations_or_comments($grade->id, true)) {
337 document_services::generate_feedback_document($this->assignment, $grade->userid, $grade->attemptnumber);
338 }
339
340 return true;
341 }
342
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 }
354
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);
369
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);
373
374 $html .= $renderer->render($widget);
375 }
376 return $html;
377 }
378
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;
386
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 }
391
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;
9f5193a4
DW
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;
5c386472
DW
406 }
407
f159ad73 408 /**
9a73d11c 409 * Determine if ghostscript is available and working.
f159ad73 410 *
411 * @return bool
412 */
9a73d11c 413 public function is_available() {
c70de965
DW
414 if ($this->enabledcache === null) {
415 $testpath = assignfeedback_editpdf\pdf::test_gs_path(false);
416 $this->enabledcache = ($testpath->status == assignfeedback_editpdf\pdf::GSPATH_OK);
f159ad73 417 }
c70de965 418 return $this->enabledcache;
f159ad73 419 }
420 /**
9a73d11c 421 * Prevent enabling this plugin if ghostscript is not available.
f159ad73 422 *
423 * @return bool false
424 */
425 public function is_configurable() {
9a73d11c 426 return $this->is_available();
f159ad73 427 }
2fa6c632
JL
428
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 }
bb690849
DW
437
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 }
30cdddb0
JL
445
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 }
5c386472 455}