Merge branch 'wip-mdl-42222' of git://github.com/rajeshtaneja/moodle
[moodle.git] / mod / assign / feedback / editpdf / classes / document_services.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 ingest manager for the assignfeedback_editpdf plugin
19 *
20 * @package assignfeedback_editpdf
21 * @copyright 2012 Davo Smith
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25namespace assignfeedback_editpdf;
26
27/**
28 * Functions for generating the annotated pdf.
29 *
30 * This class controls the ingest of student submission files to a normalised
31 * PDF 1.4 document with all submission files concatinated together. It also
32 * provides the functions to generate a downloadable pdf with all comments and
33 * annotations embedded.
34 * @copyright 2012 Davo Smith
35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36 */
37class document_services {
38
39 /** File area for generated pdf */
40 const FINAL_PDF_FILEAREA = 'download';
41 /** File area for combined pdf */
42 const COMBINED_PDF_FILEAREA = 'combined';
43 /** File area for page images */
44 const PAGE_IMAGE_FILEAREA = 'pages';
45 /** Filename for combined pdf */
46 const COMBINED_PDF_FILENAME = 'combined.pdf';
47
48 /**
49 * This function will take an int or an assignment instance and
50 * return an assignment instance. It is just for convenience.
51 * @param int|\assign $assignment
52 * @return assign
53 */
54 private static function get_assignment_from_param($assignment) {
55 global $CFG;
56
57 require_once($CFG->dirroot . '/mod/assign/locallib.php');
58
59 if (!is_object($assignment)) {
60 $cm = \get_coursemodule_from_instance('assign', $assignment, 0, false, MUST_EXIST);
61 $context = \context_module::instance($cm->id);
62
63 $assignment = new \assign($context, null, null);
64 }
65 return $assignment;
66 }
67
68 /**
69 * Get a hash that will be unique and can be used in a path name.
70 * @param int|\assign $assignment
71 * @param int $userid
72 * @param int $attemptnumber (-1 means latest attempt)
73 */
74 private static function hash($assignment, $userid, $attemptnumber) {
75 if (is_object($assignment)) {
76 $assignmentid = $assignment->get_instance()->id;
77 } else {
78 $assignmentid = $assignment;
79 }
80 return sha1($assignmentid . '_' . $userid . '_' . $attemptnumber);
81 }
82
83 /**
84 * This function will search for all files that can be converted
85 * and concatinated into a PDF (1.4) - for any submission plugin
86 * for this students attempt.
87 * @param int|\assign $assignment
88 * @param int $userid
89 * @param int $attemptnumber (-1 means latest attempt)
90 * @return array(stored_file)
91 */
92 public static function list_compatible_submission_files_for_attempt($assignment, $userid, $attemptnumber) {
93 global $USER, $DB;
94
95 $assignment = self::get_assignment_from_param($assignment);
96
97 // Capability checks.
98 if (!$assignment->can_view_submission($userid)) {
99 \print_error('nopermission');
100 }
101
102 $files = array();
103
104 if ($assignment->get_instance()->teamsubmission) {
105 $submission = $assignment->get_group_submission($userid, 0, false);
106 } else {
107 $submission = $assignment->get_user_submission($userid, false);
108 }
109 $user = $DB->get_record('user', array('id' => $userid));
110
111 // User has not submitted anything yet.
112 if (!$submission) {
113 return $files;
114 }
115 // Ask each plugin for it's list of files.
116 foreach ($assignment->get_submission_plugins() as $plugin) {
117 if ($plugin->is_enabled() && $plugin->is_visible()) {
118 $pluginfiles = $plugin->get_files($submission, $user);
119 foreach ($pluginfiles as $filename => $file) {
9dbe0313 120 if (($file instanceof \stored_file) && ($file->get_mimetype() === 'application/pdf')) {
5c386472
DW
121 $files[$filename] = $file;
122 }
123 }
124 }
125 }
126 return $files;
127 }
128
129 /**
130 * This function return the combined pdf for all valid submission files.
131 * @param int|\assign $assignment
132 * @param int $userid
133 * @param int $attemptnumber (-1 means latest attempt)
134 * @return stored_file
135 */
136 public static function get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber) {
137
138 global $USER, $DB;
139
140 $assignment = self::get_assignment_from_param($assignment);
141
142 // Capability checks.
143 if (!$assignment->can_view_submission($userid)) {
144 \print_error('nopermission');
145 }
146
147 $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
148 if ($assignment->get_instance()->teamsubmission) {
149 $submission = $assignment->get_group_submission($userid, 0, false);
150 } else {
151 $submission = $assignment->get_user_submission($userid, false);
152 }
153
154 $contextid = $assignment->get_context()->id;
155 $component = 'assignfeedback_editpdf';
156 $filearea = self::COMBINED_PDF_FILEAREA;
157 $itemid = $grade->id;
158 $filepath = '/';
159 $filename = self::COMBINED_PDF_FILENAME;
160 $fs = \get_file_storage();
161
162 $combinedpdf = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename);
163 if (!$combinedpdf ||
164 ($submission && ($combinedpdf->get_timemodified() < $submission->timemodified))) {
165 return self::generate_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
166 }
167 return $combinedpdf;
168 }
169
170 /**
171 * This function will take all of the compatible files for a submission
172 * and combine them into one PDF.
173 * @param int|\assign $assignment
174 * @param int $userid
175 * @param int $attemptnumber (-1 means latest attempt)
176 * @return stored_file
177 */
178 public static function generate_combined_pdf_for_attempt($assignment, $userid, $attemptnumber) {
179 global $CFG;
180
181 require_once($CFG->libdir . '/pdflib.php');
182
183 $assignment = self::get_assignment_from_param($assignment);
184
185 if (!$assignment->can_view_submission($userid)) {
186 \print_error('nopermission');
187 }
188
189 $files = self::list_compatible_submission_files_for_attempt($assignment, $userid, $attemptnumber);
190
191 $pdf = new pdf();
192 if (!$files) {
193 // No valid submission files - create an empty pdf.
194 $pdf->AddPage();
195 } else {
196
197 // Create a mega joined PDF.
198 $compatiblepdfs = array();
199 foreach ($files as $file) {
200 $compatiblepdf = pdf::ensure_pdf_compatible($file);
201 if ($compatiblepdf) {
202 array_push($compatiblepdfs, $compatiblepdf);
203 }
204 }
205
206 $tmpdir = \make_temp_directory('assignfeedback_editpdf/combined/' . self::hash($assignment, $userid, $attemptnumber));
207 $tmpfile = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
208
209 @unlink($tmpfile);
210 $pagecount = $pdf->combine_pdfs($compatiblepdfs, $tmpfile);
211 if ($pagecount == 0) {
212 // We at least want a single blank page.
213 $pdf->AddPage();
214 @unlink($tmpfile);
215 $files = false;
216 }
217 }
218
219 $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
220 $record = new \stdClass();
221
222 $record->contextid = $assignment->get_context()->id;
223 $record->component = 'assignfeedback_editpdf';
224 $record->filearea = self::COMBINED_PDF_FILEAREA;
225 $record->itemid = $grade->id;
226 $record->filepath = '/';
227 $record->filename = self::COMBINED_PDF_FILENAME;
228 $fs = \get_file_storage();
229
230 $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
231
232 if (!$files) {
233 // This was a blank pdf.
234 $content = $pdf->Output(self::COMBINED_PDF_FILENAME, 'S');
235 $file = $fs->create_file_from_string($record, $content);
236 } else {
237 // This was a combined pdf.
238 $file = $fs->create_file_from_pathname($record, $tmpfile);
239 @unlink($tmpfile);
240 }
241
242 return $file;
243 }
244
245 /**
246 * This function will generate and return a list of the page images from a pdf.
247 * @param int|\assign $assignment
248 * @param int $userid
249 * @param int $attemptnumber (-1 means latest attempt)
250 * @return array(stored_file)
251 */
252 public static function generate_page_images_for_attempt($assignment, $userid, $attemptnumber) {
253 global $CFG;
254
255 require_once($CFG->libdir . '/pdflib.php');
256
257 $assignment = self::get_assignment_from_param($assignment);
258
259 if (!$assignment->can_view_submission($userid)) {
260 \print_error('nopermission');
261 }
262
263 // Need to generate the page images - first get a combined pdf.
264 $file = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
265 if (!$file) {
266 throw \moodle_exception('Could not generate combined pdf.');
267 }
268
269 $tmpdir = \make_temp_directory('assignfeedback_editpdf/pageimages/' . self::hash($assignment, $userid, $attemptnumber));
270 $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
271 $file->copy_content_to($combined); // Copy the file.
272
273 $pdf = new pdf();
274
275 $pdf->set_image_folder($tmpdir);
276 $pagecount = $pdf->set_pdf($combined);
277
278 $i = 0;
279 $images = array();
280 for ($i = 0; $i < $pagecount; $i++) {
281 $images[$i] = $pdf->get_image($i);
282 }
283 $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
284
285 $files = array();
286 $record = new \stdClass();
287 $record->contextid = $assignment->get_context()->id;
288 $record->component = 'assignfeedback_editpdf';
289 $record->filearea = self::PAGE_IMAGE_FILEAREA;
290 $record->itemid = $grade->id;
291 $record->filepath = '/';
292 $fs = \get_file_storage();
293
294 foreach ($images as $index => $image) {
295 $record->filename = basename($image);
296 $files[$index] = $fs->create_file_from_pathname($record, $tmpdir . '/' . $image);
297 @unlink($tmpdir . '/' . $image);
298 }
299 @unlink($combined);
300 @rmdir($tmpdir);
301
302 return $files;
303 }
304
305 /**
306 * This function returns a list of the page images from a pdf.
307 * @param int|\assign $assignment
308 * @param int $userid
309 * @param int $attemptnumber (-1 means latest attempt)
310 * @return array(stored_file)
311 */
312 public static function get_page_images_for_attempt($assignment, $userid, $attemptnumber) {
313
314 $assignment = self::get_assignment_from_param($assignment);
315
316 if (!$assignment->can_view_submission($userid)) {
317 \print_error('nopermission');
318 }
319
320 if ($assignment->get_instance()->teamsubmission) {
321 $submission = $assignment->get_group_submission($userid, 0, false);
322 } else {
323 $submission = $assignment->get_user_submission($userid, false);
324 }
325 $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
326
327 $contextid = $assignment->get_context()->id;
328 $component = 'assignfeedback_editpdf';
329 $filearea = self::PAGE_IMAGE_FILEAREA;
330 $itemid = $grade->id;
331 $filepath = '/';
332
333 $fs = \get_file_storage();
334
335 $files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath);
336
337 if (!empty($files)) {
338 $first = reset($files);
339 if ($first->get_timemodified() < $submission->timemodified) {
340
341 $fs->delete_area_files($contextid, $component, $filearea, $itemid);
342 // Image files are stale - regenerate them.
343 $files = array();
344 } else {
345 return $files;
346 }
347 }
348 return self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber);
349 }
350
351 /**
352 * This function returns sensible filename for a feedback file.
353 * @param int|\assign $assignment
354 * @param int $userid
355 * @param int $attemptnumber (-1 means latest attempt)
356 * @return string
357 */
358 protected static function get_downloadable_feedback_filename($assignment, $userid, $attemptnumber) {
359 global $DB;
360
361 $assignment = self::get_assignment_from_param($assignment);
362
363 $groupmode = groups_get_activity_groupmode($assignment->get_course_module());
364 $groupname = '';
365 if ($groupmode) {
366 $groupid = groups_get_activity_group($assignment->get_course_module(), true);
367 $groupname = groups_get_group_name($groupid).'-';
368 }
369 if ($groupname == '-') {
370 $groupname = '';
371 }
372 $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
373 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
374
375 if ($assignment->is_blind_marking()) {
376 $prefix = $groupname . get_string('participant', 'assign');
377 $prefix = str_replace('_', ' ', $prefix);
378 $prefix = clean_filename($prefix . '_' . $assignment->get_uniqueid_for_user($userid) . '_');
379 } else {
380 $prefix = $groupname . fullname($user);
381 $prefix = str_replace('_', ' ', $prefix);
382 $prefix = clean_filename($prefix . '_' . $assignment->get_uniqueid_for_user($userid) . '_');
383 }
384 $prefix .= $grade->attemptnumber;
385
386 return $prefix . '.pdf';
387 }
388
389 /**
390 * This function takes the combined pdf and embeds all the comments and annotations.
391 * @param int|\assign $assignment
392 * @param int $userid
393 * @param int $attemptnumber (-1 means latest attempt)
394 * @return stored_file
395 */
396 public static function generate_feedback_document($assignment, $userid, $attemptnumber) {
397
398 $assignment = self::get_assignment_from_param($assignment);
399
400 if (!$assignment->can_view_submission($userid)) {
401 \print_error('nopermission');
402 }
403 if (!$assignment->can_grade()) {
404 \print_error('nopermission');
405 }
406
407 // Need to generate the page images - first get a combined pdf.
408 $file = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
409 if (!$file) {
410 throw \moodle_exception('Could not generate combined pdf.');
411 }
412
413 $tmpdir = \make_temp_directory('assignfeedback_editpdf/final/' . self::hash($assignment, $userid, $attemptnumber));
414 $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
415 $file->copy_content_to($combined); // Copy the file.
416
417 $pdf = new pdf();
418
419 $fs = \get_file_storage();
420 $stamptmpdir = \make_temp_directory('assignfeedback_editpdf/stamps/' . self::hash($assignment, $userid, $attemptnumber));
421 $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
422 // Copy any new stamps to this instance.
423 if ($files = $fs->get_area_files($assignment->get_context()->id,
424 'assignfeedback_editpdf',
425 'stamps',
426 $grade->id,
427 "filename",
428 false)) {
429 foreach ($files as $file) {
430 $filename = $stamptmpdir . '/' . $file->get_filename();
431 $file->copy_content_to($filename); // Copy the file.
432 }
433 }
434
435 $pagecount = $pdf->set_pdf($combined);
436 $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
437 page_editor::release_drafts($grade->id);
438
439 for ($i = 0; $i < $pagecount; $i++) {
440 $pdf->copy_page();
441 $comments = page_editor::get_comments($grade->id, $i, false);
442 $annotations = page_editor::get_annotations($grade->id, $i, false);
443
444 foreach ($comments as $comment) {
445 $pdf->add_comment($comment->rawtext,
446 $comment->x,
447 $comment->y,
448 $comment->width,
449 $comment->colour);
450 }
451
452 foreach ($annotations as $annotation) {
453 $pdf->add_annotation($annotation->x,
454 $annotation->y,
455 $annotation->endx,
456 $annotation->endy,
457 $annotation->colour,
458 $annotation->type,
459 $annotation->path,
460 $stamptmpdir);
461 }
462 }
463
464 fulldelete($stamptmpdir);
465
466 $filename = self::get_downloadable_feedback_filename($assignment, $userid, $attemptnumber);
467 $filename = clean_param($filename, PARAM_FILE);
468
469 $generatedpdf = $tmpdir . '/' . $filename;
470 $pdf->save_pdf($generatedpdf);
471
472
473 $record = new \stdClass();
474
475 $record->contextid = $assignment->get_context()->id;
476 $record->component = 'assignfeedback_editpdf';
477 $record->filearea = self::FINAL_PDF_FILEAREA;
478 $record->itemid = $grade->id;
479 $record->filepath = '/';
480 $record->filename = $filename;
481
482
483 // Only keep one current version of the generated pdf.
484 $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
485
486 $file = $fs->create_file_from_pathname($record, $generatedpdf);
487
488 // Cleanup.
489 @unlink($generatedpdf);
490 @unlink($combined);
491 @rmdir($tmpdir);
492
493 return $file;
494 }
495
496 /**
497 * This function returns the generated pdf (if it exists).
498 * @param int|\assign $assignment
499 * @param int $userid
500 * @param int $attemptnumber (-1 means latest attempt)
501 * @return stored_file
502 */
503 public static function get_feedback_document($assignment, $userid, $attemptnumber) {
504
505 $assignment = self::get_assignment_from_param($assignment);
506
507 if (!$assignment->can_view_submission($userid)) {
508 \print_error('nopermission');
509 }
510
511 $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
512
513 $contextid = $assignment->get_context()->id;
514 $component = 'assignfeedback_editpdf';
515 $filearea = self::FINAL_PDF_FILEAREA;
516 $itemid = $grade->id;
517 $filepath = '/';
518
519 $fs = \get_file_storage();
520 $files = $fs->get_area_files($contextid,
521 $component,
522 $filearea,
523 $itemid,
524 "itemid, filepath, filename",
525 false);
526 if ($files) {
527 return reset($files);
528 }
529 return false;
530 }
531
532 /**
533 * This function deletes the generated pdf for a student.
534 * @param int|\assign $assignment
535 * @param int $userid
536 * @param int $attemptnumber (-1 means latest attempt)
537 * @return bool
538 */
539 public static function delete_feedback_document($assignment, $userid, $attemptnumber) {
540
541 $assignment = self::get_assignment_from_param($assignment);
542
543 if (!$assignment->can_view_submission($userid)) {
544 \print_error('nopermission');
545 }
546 if (!$assignment->can_grade()) {
547 \print_error('nopermission');
548 }
549
550 $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
551
552 $contextid = $assignment->get_context()->id;
553 $component = 'assignfeedback_editpdf';
554 $filearea = self::FINAL_PDF_FILEAREA;
555 $itemid = $grade->id;
556
557 $fs = \get_file_storage();
558 return $fs->delete_area_files($contextid, $component, $filearea, $itemid);
559 }
560
561}