[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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 25 namespace assignfeedback_editpdf; 26 27 use DOMDocument; 28 29 /** 30 * Functions for generating the annotated pdf. 31 * 32 * This class controls the ingest of student submission files to a normalised 33 * PDF 1.4 document with all submission files concatinated together. It also 34 * provides the functions to generate a downloadable pdf with all comments and 35 * annotations embedded. 36 * @copyright 2012 Davo Smith 37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 */ 39 class document_services { 40 41 /** File area for generated pdf */ 42 const FINAL_PDF_FILEAREA = 'download'; 43 /** File area for combined pdf */ 44 const COMBINED_PDF_FILEAREA = 'combined'; 45 /** File area for importing html */ 46 const IMPORT_HTML_FILEAREA = 'importhtml'; 47 /** File area for page images */ 48 const PAGE_IMAGE_FILEAREA = 'pages'; 49 /** File area for readonly page images */ 50 const PAGE_IMAGE_READONLY_FILEAREA = 'readonlypages'; 51 /** File area for the stamps */ 52 const STAMPS_FILEAREA = 'stamps'; 53 /** Filename for combined pdf */ 54 const COMBINED_PDF_FILENAME = 'combined.pdf'; 55 56 /** Base64 encoded blank pdf. This is the most reliable/fastest way to generate a blank pdf. */ 57 const BLANK_PDF_BASE64 = <<<EOD 58 JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl 59 Y29kZT4+CnN0cmVhbQp4nDPQM1Qo5ypUMFAwALJMLU31jBQsTAz1LBSKUrnCtRTyuAIVAIcdB3IK 60 ZW5kc3RyZWFtCmVuZG9iagoKMyAwIG9iago0MgplbmRvYmoKCjUgMCBvYmoKPDwKPj4KZW5kb2Jq 61 Cgo2IDAgb2JqCjw8L0ZvbnQgNSAwIFIKL1Byb2NTZXRbL1BERi9UZXh0XQo+PgplbmRvYmoKCjEg 62 MCBvYmoKPDwvVHlwZS9QYWdlL1BhcmVudCA0IDAgUi9SZXNvdXJjZXMgNiAwIFIvTWVkaWFCb3hb 63 MCAwIDU5NSA4NDJdL0dyb3VwPDwvUy9UcmFuc3BhcmVuY3kvQ1MvRGV2aWNlUkdCL0kgdHJ1ZT4+ 64 L0NvbnRlbnRzIDIgMCBSPj4KZW5kb2JqCgo0IDAgb2JqCjw8L1R5cGUvUGFnZXMKL1Jlc291cmNl 65 cyA2IDAgUgovTWVkaWFCb3hbIDAgMCA1OTUgODQyIF0KL0tpZHNbIDEgMCBSIF0KL0NvdW50IDE+ 66 PgplbmRvYmoKCjcgMCBvYmoKPDwvVHlwZS9DYXRhbG9nL1BhZ2VzIDQgMCBSCi9PcGVuQWN0aW9u 67 WzEgMCBSIC9YWVogbnVsbCBudWxsIDBdCi9MYW5nKGVuLUFVKQo+PgplbmRvYmoKCjggMCBvYmoK 68 PDwvQ3JlYXRvcjxGRUZGMDA1NzAwNzIwMDY5MDA3NDAwNjUwMDcyPgovUHJvZHVjZXI8RkVGRjAw 69 NEMwMDY5MDA2MjAwNzIwMDY1MDA0RjAwNjYwMDY2MDA2OTAwNjMwMDY1MDAyMDAwMzQwMDJFMDAz 70 ND4KL0NyZWF0aW9uRGF0ZShEOjIwMTYwMjI2MTMyMzE0KzA4JzAwJyk+PgplbmRvYmoKCnhyZWYK 71 MCA5CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMDIyNiAwMDAwMCBuIAowMDAwMDAwMDE5IDAw 72 MDAwIG4gCjAwMDAwMDAxMzIgMDAwMDAgbiAKMDAwMDAwMDM2OCAwMDAwMCBuIAowMDAwMDAwMTUx 73 IDAwMDAwIG4gCjAwMDAwMDAxNzMgMDAwMDAgbiAKMDAwMDAwMDQ2NiAwMDAwMCBuIAowMDAwMDAw 74 NTYyIDAwMDAwIG4gCnRyYWlsZXIKPDwvU2l6ZSA5L1Jvb3QgNyAwIFIKL0luZm8gOCAwIFIKL0lE 75 IFsgPEJDN0REQUQwRDQyOTQ1OTQ2OUU4NzJCMjI1ODUyNkU4Pgo8QkM3RERBRDBENDI5NDU5NDY5 76 RTg3MkIyMjU4NTI2RTg+IF0KL0RvY0NoZWNrc3VtIC9BNTYwMEZCMDAzRURCRTg0MTNBNTk3RTZF 77 MURDQzJBRgo+PgpzdGFydHhyZWYKNzM2CiUlRU9GCg== 78 EOD; 79 80 /** 81 * This function will take an int or an assignment instance and 82 * return an assignment instance. It is just for convenience. 83 * @param int|\assign $assignment 84 * @return assign 85 */ 86 private static function get_assignment_from_param($assignment) { 87 global $CFG; 88 89 require_once($CFG->dirroot . '/mod/assign/locallib.php'); 90 91 if (!is_object($assignment)) { 92 $cm = \get_coursemodule_from_instance('assign', $assignment, 0, false, MUST_EXIST); 93 $context = \context_module::instance($cm->id); 94 95 $assignment = new \assign($context, null, null); 96 } 97 return $assignment; 98 } 99 100 /** 101 * Get a hash that will be unique and can be used in a path name. 102 * @param int|\assign $assignment 103 * @param int $userid 104 * @param int $attemptnumber (-1 means latest attempt) 105 */ 106 private static function hash($assignment, $userid, $attemptnumber) { 107 if (is_object($assignment)) { 108 $assignmentid = $assignment->get_instance()->id; 109 } else { 110 $assignmentid = $assignment; 111 } 112 return sha1($assignmentid . '_' . $userid . '_' . $attemptnumber); 113 } 114 115 /** 116 * Use a DOM parser to accurately replace images with their alt text. 117 * @param string $html 118 * @return string New html with no image tags. 119 */ 120 protected static function strip_images($html) { 121 $dom = new DOMDocument(); 122 $dom->loadHTML("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" . $html); 123 $images = $dom->getElementsByTagName('img'); 124 $i = 0; 125 126 for ($i = ($images->length - 1); $i >= 0; $i--) { 127 $node = $images->item($i); 128 129 if ($node->hasAttribute('alt')) { 130 $replacement = ' [ ' . $node->getAttribute('alt') . ' ] '; 131 } else { 132 $replacement = ' '; 133 } 134 135 $text = $dom->createTextNode($replacement); 136 $node->parentNode->replaceChild($text, $node); 137 } 138 $count = 1; 139 return str_replace("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>", "", $dom->saveHTML(), $count); 140 } 141 142 /** 143 * This function will search for all files that can be converted 144 * and concatinated into a PDF (1.4) - for any submission plugin 145 * for this students attempt. 146 * @param int|\assign $assignment 147 * @param int $userid 148 * @param int $attemptnumber (-1 means latest attempt) 149 * @return array(stored_file) 150 */ 151 public static function list_compatible_submission_files_for_attempt($assignment, $userid, $attemptnumber) { 152 global $USER, $DB; 153 154 $assignment = self::get_assignment_from_param($assignment); 155 156 // Capability checks. 157 if (!$assignment->can_view_submission($userid)) { 158 \print_error('nopermission'); 159 } 160 161 $files = array(); 162 163 if ($assignment->get_instance()->teamsubmission) { 164 $submission = $assignment->get_group_submission($userid, 0, false); 165 } else { 166 $submission = $assignment->get_user_submission($userid, false); 167 } 168 $user = $DB->get_record('user', array('id' => $userid)); 169 170 // User has not submitted anything yet. 171 if (!$submission) { 172 return $files; 173 } 174 175 $fs = get_file_storage(); 176 // Ask each plugin for it's list of files. 177 foreach ($assignment->get_submission_plugins() as $plugin) { 178 if ($plugin->is_enabled() && $plugin->is_visible()) { 179 $pluginfiles = $plugin->get_files($submission, $user); 180 foreach ($pluginfiles as $filename => $file) { 181 if ($file instanceof \stored_file) { 182 if ($file->get_mimetype() === 'application/pdf') { 183 $files[$filename] = $file; 184 } else if ($convertedfile = $fs->get_converted_document($file, 'pdf')) { 185 $files[$filename] = $convertedfile; 186 } 187 } else { 188 // Create a tmp stored_file from this html string. 189 $file = reset($file); 190 // Strip image tags, because they will not be resolvable. 191 $file = self::strip_images($file); 192 $record = new \stdClass(); 193 $record->contextid = $assignment->get_context()->id; 194 $record->component = 'assignfeedback_editpdf'; 195 $record->filearea = self::IMPORT_HTML_FILEAREA; 196 $record->itemid = $submission->id; 197 $record->filepath = '/'; 198 $record->filename = $plugin->get_type() . '-' . $filename; 199 200 $htmlfile = $fs->create_file_from_string($record, $file); 201 $convertedfile = $fs->get_converted_document($htmlfile, 'pdf'); 202 $htmlfile->delete(); 203 if ($convertedfile) { 204 $files[$filename] = $convertedfile; 205 } 206 } 207 } 208 } 209 } 210 return $files; 211 } 212 213 /** 214 * This function return the combined pdf for all valid submission files. 215 * @param int|\assign $assignment 216 * @param int $userid 217 * @param int $attemptnumber (-1 means latest attempt) 218 * @return stored_file 219 */ 220 public static function get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber) { 221 222 global $USER, $DB; 223 224 $assignment = self::get_assignment_from_param($assignment); 225 226 // Capability checks. 227 if (!$assignment->can_view_submission($userid)) { 228 \print_error('nopermission'); 229 } 230 231 $grade = $assignment->get_user_grade($userid, true, $attemptnumber); 232 if ($assignment->get_instance()->teamsubmission) { 233 $submission = $assignment->get_group_submission($userid, 0, false); 234 } else { 235 $submission = $assignment->get_user_submission($userid, false); 236 } 237 238 $contextid = $assignment->get_context()->id; 239 $component = 'assignfeedback_editpdf'; 240 $filearea = self::COMBINED_PDF_FILEAREA; 241 $itemid = $grade->id; 242 $filepath = '/'; 243 $filename = self::COMBINED_PDF_FILENAME; 244 $fs = \get_file_storage(); 245 246 $combinedpdf = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename); 247 if (!$combinedpdf || 248 ($submission && ($combinedpdf->get_timemodified() < $submission->timemodified))) { 249 return self::generate_combined_pdf_for_attempt($assignment, $userid, $attemptnumber); 250 } 251 return $combinedpdf; 252 } 253 254 /** 255 * This function will take all of the compatible files for a submission 256 * and combine them into one PDF. 257 * @param int|\assign $assignment 258 * @param int $userid 259 * @param int $attemptnumber (-1 means latest attempt) 260 * @return stored_file 261 */ 262 public static function generate_combined_pdf_for_attempt($assignment, $userid, $attemptnumber) { 263 global $CFG; 264 265 require_once($CFG->libdir . '/pdflib.php'); 266 267 $assignment = self::get_assignment_from_param($assignment); 268 269 if (!$assignment->can_view_submission($userid)) { 270 \print_error('nopermission'); 271 } 272 273 $files = self::list_compatible_submission_files_for_attempt($assignment, $userid, $attemptnumber); 274 275 $pdf = new pdf(); 276 if ($files) { 277 // Create a mega joined PDF. 278 $compatiblepdfs = array(); 279 foreach ($files as $file) { 280 $compatiblepdf = pdf::ensure_pdf_compatible($file); 281 if ($compatiblepdf) { 282 array_push($compatiblepdfs, $compatiblepdf); 283 } 284 } 285 286 $tmpdir = \make_temp_directory('assignfeedback_editpdf/combined/' . self::hash($assignment, $userid, $attemptnumber)); 287 $tmpfile = $tmpdir . '/' . self::COMBINED_PDF_FILENAME; 288 289 @unlink($tmpfile); 290 try { 291 $pagecount = $pdf->combine_pdfs($compatiblepdfs, $tmpfile); 292 } catch (\Exception $e) { 293 debugging('TCPDF could not process the pdf files:' . $e->getMessage(), DEBUG_DEVELOPER); 294 // TCPDF does not recover from errors so we need to re-initialise the class. 295 $pagecount = 0; 296 } 297 if ($pagecount == 0) { 298 // We at least want a single blank page. 299 debugging('TCPDF did not produce a valid pdf:' . $tmpfile . '. Replacing with a blank pdf.', DEBUG_DEVELOPER); 300 @unlink($tmpfile); 301 $files = false; 302 } 303 } 304 $pdf->Close(); // No real need to close this pdf, because it has been saved by combine_pdfs(), but for clarity. 305 306 $grade = $assignment->get_user_grade($userid, true, $attemptnumber); 307 $record = new \stdClass(); 308 309 $record->contextid = $assignment->get_context()->id; 310 $record->component = 'assignfeedback_editpdf'; 311 $record->filearea = self::COMBINED_PDF_FILEAREA; 312 $record->itemid = $grade->id; 313 $record->filepath = '/'; 314 $record->filename = self::COMBINED_PDF_FILENAME; 315 $fs = \get_file_storage(); 316 317 $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid); 318 319 // Detect corrupt generated pdfs and replace with a blank one. 320 if ($files) { 321 $verifypdf = new pdf(); 322 $pagecount = $verifypdf->load_pdf($tmpfile); 323 if ($pagecount <= 0) { 324 $files = false; 325 } 326 $verifypdf->Close(); // PDF loaded and never saved/outputted needs to be closed. 327 } 328 329 if (!$files) { 330 $file = $fs->create_file_from_string($record, base64_decode(self::BLANK_PDF_BASE64)); 331 } else { 332 // This was a combined pdf. 333 $file = $fs->create_file_from_pathname($record, $tmpfile); 334 @unlink($tmpfile); 335 336 // Test the generated file for correctness. 337 $compatiblepdf = pdf::ensure_pdf_compatible($file); 338 } 339 340 return $file; 341 } 342 343 /** 344 * This function will return the number of pages of a pdf. 345 * @param int|\assign $assignment 346 * @param int $userid 347 * @param int $attemptnumber (-1 means latest attempt) 348 * @param bool $readonly When true we get the number of pages for the readonly version. 349 * @return int number of pages 350 */ 351 public static function page_number_for_attempt($assignment, $userid, $attemptnumber, $readonly = false) { 352 global $CFG; 353 354 require_once($CFG->libdir . '/pdflib.php'); 355 356 $assignment = self::get_assignment_from_param($assignment); 357 358 if (!$assignment->can_view_submission($userid)) { 359 \print_error('nopermission'); 360 } 361 362 // When in readonly we can return the number of images in the DB because they should already exist, 363 // if for some reason they do not, then we proceed as for the normal version. 364 if ($readonly) { 365 $grade = $assignment->get_user_grade($userid, true, $attemptnumber); 366 $fs = get_file_storage(); 367 $files = $fs->get_directory_files($assignment->get_context()->id, 'assignfeedback_editpdf', 368 self::PAGE_IMAGE_READONLY_FILEAREA, $grade->id, '/'); 369 $pagecount = count($files); 370 if ($pagecount > 0) { 371 return $pagecount; 372 } 373 } 374 375 // Get a combined pdf file from all submitted pdf files. 376 $file = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber); 377 if (!$file) { 378 \print_error('Could not generate combined pdf.'); 379 } 380 381 // Store the combined pdf file somewhere to be opened by tcpdf. 382 $tmpdir = \make_temp_directory('assignfeedback_editpdf/pagetotal/' 383 . self::hash($assignment, $userid, $attemptnumber)); 384 $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME; 385 $file->copy_content_to($combined); // Copy the file. 386 387 // Get the total number of pages. 388 $pdf = new pdf(); 389 $pagecount = $pdf->set_pdf($combined); 390 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed. 391 392 // Delete temporary folders and files. 393 @unlink($combined); 394 @rmdir($tmpdir); 395 396 return $pagecount; 397 } 398 399 /** 400 * This function will generate and return a list of the page images from a pdf. 401 * @param int|\assign $assignment 402 * @param int $userid 403 * @param int $attemptnumber (-1 means latest attempt) 404 * @return array(stored_file) 405 */ 406 public static function generate_page_images_for_attempt($assignment, $userid, $attemptnumber) { 407 global $CFG; 408 409 require_once($CFG->libdir . '/pdflib.php'); 410 411 $assignment = self::get_assignment_from_param($assignment); 412 413 if (!$assignment->can_view_submission($userid)) { 414 \print_error('nopermission'); 415 } 416 417 // Need to generate the page images - first get a combined pdf. 418 $file = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber); 419 if (!$file) { 420 throw new \moodle_exception('Could not generate combined pdf.'); 421 } 422 423 $tmpdir = \make_temp_directory('assignfeedback_editpdf/pageimages/' . self::hash($assignment, $userid, $attemptnumber)); 424 $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME; 425 $file->copy_content_to($combined); // Copy the file. 426 427 $pdf = new pdf(); 428 429 $pdf->set_image_folder($tmpdir); 430 $pagecount = $pdf->set_pdf($combined); 431 432 $grade = $assignment->get_user_grade($userid, true, $attemptnumber); 433 434 $record = new \stdClass(); 435 $record->contextid = $assignment->get_context()->id; 436 $record->component = 'assignfeedback_editpdf'; 437 $record->filearea = self::PAGE_IMAGE_FILEAREA; 438 $record->itemid = $grade->id; 439 $record->filepath = '/'; 440 $fs = \get_file_storage(); 441 442 // Remove the existing content of the filearea. 443 $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid); 444 445 $files = array(); 446 for ($i = 0; $i < $pagecount; $i++) { 447 $image = $pdf->get_image($i); 448 $record->filename = basename($image); 449 $files[$i] = $fs->create_file_from_pathname($record, $tmpdir . '/' . $image); 450 @unlink($tmpdir . '/' . $image); 451 } 452 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed. 453 454 @unlink($combined); 455 @rmdir($tmpdir); 456 457 return $files; 458 } 459 460 /** 461 * This function returns a list of the page images from a pdf. 462 * 463 * The readonly version is different than the normal one. The readonly version contains a copy 464 * of the pages in the state they were when the PDF was annotated, by doing so we prevent the 465 * the pages that are displayed to change as soon as the submission changes. 466 * 467 * Though there is an edge case, if the PDF was annotated before MDL-45580, then it is possible 468 * that we do not find any readonly version of the pages. In that case, we will get the normal 469 * pages and copy them to the readonly area. This ensures that the pages will remain in that 470 * state until the submission is updated. When the normal files do not exist, we throw an exception 471 * because the readonly pages should only ever be displayed after a teacher has annotated the PDF, 472 * they would not exist until they do. 473 * 474 * @param int|\assign $assignment 475 * @param int $userid 476 * @param int $attemptnumber (-1 means latest attempt) 477 * @param bool $readonly If true, then we are requesting the readonly version. 478 * @return array(stored_file) 479 */ 480 public static function get_page_images_for_attempt($assignment, $userid, $attemptnumber, $readonly = false) { 481 482 $assignment = self::get_assignment_from_param($assignment); 483 484 if (!$assignment->can_view_submission($userid)) { 485 \print_error('nopermission'); 486 } 487 488 if ($assignment->get_instance()->teamsubmission) { 489 $submission = $assignment->get_group_submission($userid, 0, false); 490 } else { 491 $submission = $assignment->get_user_submission($userid, false); 492 } 493 $grade = $assignment->get_user_grade($userid, true, $attemptnumber); 494 495 $contextid = $assignment->get_context()->id; 496 $component = 'assignfeedback_editpdf'; 497 $itemid = $grade->id; 498 $filepath = '/'; 499 $filearea = self::PAGE_IMAGE_FILEAREA; 500 501 $fs = \get_file_storage(); 502 503 // If we are after the readonly pages... 504 if ($readonly) { 505 $filearea = self::PAGE_IMAGE_READONLY_FILEAREA; 506 if ($fs->is_area_empty($contextid, $component, $filearea, $itemid)) { 507 // We have a problem here, we were supposed to find the files. 508 // Attempt to re-generate the pages from the combined images. 509 self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber); 510 self::copy_pages_to_readonly_area($assignment, $grade); 511 } 512 } 513 514 $files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath); 515 516 $pages = array(); 517 if (!empty($files)) { 518 $first = reset($files); 519 if (!$readonly && $first->get_timemodified() < $submission->timemodified) { 520 // Image files are stale, we need to regenerate them, except in readonly mode. 521 // We also need to remove the draft annotations and comments associated with this attempt. 522 $fs->delete_area_files($contextid, $component, $filearea, $itemid); 523 page_editor::delete_draft_content($itemid); 524 $files = array(); 525 } else { 526 527 // Need to reorder the files following their name. 528 // because get_directory_files() return a different order than generate_page_images_for_attempt(). 529 foreach($files as $file) { 530 // Extract the page number from the file name image_pageXXXX.png. 531 preg_match('/page([\d]+)\./', $file->get_filename(), $matches); 532 if (empty($matches) or !is_numeric($matches[1])) { 533 throw new \coding_exception("'" . $file->get_filename() 534 . "' file hasn't the expected format filename: image_pageXXXX.png."); 535 } 536 $pagenumber = (int)$matches[1]; 537 538 // Save the page in the ordered array. 539 $pages[$pagenumber] = $file; 540 } 541 ksort($pages); 542 } 543 } 544 545 if (empty($pages)) { 546 if ($readonly) { 547 // This should never happen, there should be a version of the pages available 548 // whenever we are requesting the readonly version. 549 throw new \moodle_exception('Could not find readonly pages for grade ' . $grade->id); 550 } 551 $pages = self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber); 552 } 553 554 return $pages; 555 } 556 557 /** 558 * This function returns sensible filename for a feedback file. 559 * @param int|\assign $assignment 560 * @param int $userid 561 * @param int $attemptnumber (-1 means latest attempt) 562 * @return string 563 */ 564 protected static function get_downloadable_feedback_filename($assignment, $userid, $attemptnumber) { 565 global $DB; 566 567 $assignment = self::get_assignment_from_param($assignment); 568 569 $groupmode = groups_get_activity_groupmode($assignment->get_course_module()); 570 $groupname = ''; 571 if ($groupmode) { 572 $groupid = groups_get_activity_group($assignment->get_course_module(), true); 573 $groupname = groups_get_group_name($groupid).'-'; 574 } 575 if ($groupname == '-') { 576 $groupname = ''; 577 } 578 $grade = $assignment->get_user_grade($userid, true, $attemptnumber); 579 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); 580 581 if ($assignment->is_blind_marking()) { 582 $prefix = $groupname . get_string('participant', 'assign'); 583 $prefix = str_replace('_', ' ', $prefix); 584 $prefix = clean_filename($prefix . '_' . $assignment->get_uniqueid_for_user($userid) . '_'); 585 } else { 586 $prefix = $groupname . fullname($user); 587 $prefix = str_replace('_', ' ', $prefix); 588 $prefix = clean_filename($prefix . '_' . $assignment->get_uniqueid_for_user($userid) . '_'); 589 } 590 $prefix .= $grade->attemptnumber; 591 592 return $prefix . '.pdf'; 593 } 594 595 /** 596 * This function takes the combined pdf and embeds all the comments and annotations. 597 * 598 * This also moves the annotations and comments from drafts to not drafts. And it will 599 * copy all the images stored to the readonly area, so that they can be viewed online, and 600 * not be overwritten when a new submission is sent. 601 * 602 * @param int|\assign $assignment 603 * @param int $userid 604 * @param int $attemptnumber (-1 means latest attempt) 605 * @return stored_file 606 */ 607 public static function generate_feedback_document($assignment, $userid, $attemptnumber) { 608 609 $assignment = self::get_assignment_from_param($assignment); 610 611 if (!$assignment->can_view_submission($userid)) { 612 \print_error('nopermission'); 613 } 614 if (!$assignment->can_grade()) { 615 \print_error('nopermission'); 616 } 617 618 // Need to generate the page images - first get a combined pdf. 619 $file = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber); 620 if (!$file) { 621 throw new \moodle_exception('Could not generate combined pdf.'); 622 } 623 624 $tmpdir = \make_temp_directory('assignfeedback_editpdf/final/' . self::hash($assignment, $userid, $attemptnumber)); 625 $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME; 626 $file->copy_content_to($combined); // Copy the file. 627 628 $pdf = new pdf(); 629 630 $fs = \get_file_storage(); 631 $stamptmpdir = \make_temp_directory('assignfeedback_editpdf/stamps/' . self::hash($assignment, $userid, $attemptnumber)); 632 $grade = $assignment->get_user_grade($userid, true, $attemptnumber); 633 // Copy any new stamps to this instance. 634 if ($files = $fs->get_area_files($assignment->get_context()->id, 635 'assignfeedback_editpdf', 636 'stamps', 637 $grade->id, 638 "filename", 639 false)) { 640 foreach ($files as $file) { 641 $filename = $stamptmpdir . '/' . $file->get_filename(); 642 $file->copy_content_to($filename); // Copy the file. 643 } 644 } 645 646 $pagecount = $pdf->set_pdf($combined); 647 $grade = $assignment->get_user_grade($userid, true, $attemptnumber); 648 page_editor::release_drafts($grade->id); 649 650 for ($i = 0; $i < $pagecount; $i++) { 651 $pdf->copy_page(); 652 $comments = page_editor::get_comments($grade->id, $i, false); 653 $annotations = page_editor::get_annotations($grade->id, $i, false); 654 655 foreach ($comments as $comment) { 656 $pdf->add_comment($comment->rawtext, 657 $comment->x, 658 $comment->y, 659 $comment->width, 660 $comment->colour); 661 } 662 663 foreach ($annotations as $annotation) { 664 $pdf->add_annotation($annotation->x, 665 $annotation->y, 666 $annotation->endx, 667 $annotation->endy, 668 $annotation->colour, 669 $annotation->type, 670 $annotation->path, 671 $stamptmpdir); 672 } 673 } 674 675 fulldelete($stamptmpdir); 676 677 $filename = self::get_downloadable_feedback_filename($assignment, $userid, $attemptnumber); 678 $filename = clean_param($filename, PARAM_FILE); 679 680 $generatedpdf = $tmpdir . '/' . $filename; 681 $pdf->save_pdf($generatedpdf); 682 683 684 $record = new \stdClass(); 685 686 $record->contextid = $assignment->get_context()->id; 687 $record->component = 'assignfeedback_editpdf'; 688 $record->filearea = self::FINAL_PDF_FILEAREA; 689 $record->itemid = $grade->id; 690 $record->filepath = '/'; 691 $record->filename = $filename; 692 693 694 // Only keep one current version of the generated pdf. 695 $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid); 696 697 $file = $fs->create_file_from_pathname($record, $generatedpdf); 698 699 // Cleanup. 700 @unlink($generatedpdf); 701 @unlink($combined); 702 @rmdir($tmpdir); 703 704 self::copy_pages_to_readonly_area($assignment, $grade); 705 706 return $file; 707 } 708 709 /** 710 * Copy the pages image to the readonly area. 711 * 712 * @param int|\assign $assignment The assignment. 713 * @param \stdClass $grade The grade record. 714 * @return void 715 */ 716 public static function copy_pages_to_readonly_area($assignment, $grade) { 717 $fs = get_file_storage(); 718 $assignment = self::get_assignment_from_param($assignment); 719 $contextid = $assignment->get_context()->id; 720 $component = 'assignfeedback_editpdf'; 721 $itemid = $grade->id; 722 723 // Get all the pages. 724 $originalfiles = $fs->get_area_files($contextid, $component, self::PAGE_IMAGE_FILEAREA, $itemid); 725 if (empty($originalfiles)) { 726 // Nothing to do here... 727 return; 728 } 729 730 // Delete the old readonly files. 731 $fs->delete_area_files($contextid, $component, self::PAGE_IMAGE_READONLY_FILEAREA, $itemid); 732 733 // Do the copying. 734 foreach ($originalfiles as $originalfile) { 735 $fs->create_file_from_storedfile(array('filearea' => self::PAGE_IMAGE_READONLY_FILEAREA), $originalfile); 736 } 737 } 738 739 /** 740 * This function returns the generated pdf (if it exists). 741 * @param int|\assign $assignment 742 * @param int $userid 743 * @param int $attemptnumber (-1 means latest attempt) 744 * @return stored_file 745 */ 746 public static function get_feedback_document($assignment, $userid, $attemptnumber) { 747 748 $assignment = self::get_assignment_from_param($assignment); 749 750 if (!$assignment->can_view_submission($userid)) { 751 \print_error('nopermission'); 752 } 753 754 $grade = $assignment->get_user_grade($userid, true, $attemptnumber); 755 756 $contextid = $assignment->get_context()->id; 757 $component = 'assignfeedback_editpdf'; 758 $filearea = self::FINAL_PDF_FILEAREA; 759 $itemid = $grade->id; 760 $filepath = '/'; 761 762 $fs = \get_file_storage(); 763 $files = $fs->get_area_files($contextid, 764 $component, 765 $filearea, 766 $itemid, 767 "itemid, filepath, filename", 768 false); 769 if ($files) { 770 return reset($files); 771 } 772 return false; 773 } 774 775 /** 776 * This function deletes the generated pdf for a student. 777 * @param int|\assign $assignment 778 * @param int $userid 779 * @param int $attemptnumber (-1 means latest attempt) 780 * @return bool 781 */ 782 public static function delete_feedback_document($assignment, $userid, $attemptnumber) { 783 784 $assignment = self::get_assignment_from_param($assignment); 785 786 if (!$assignment->can_view_submission($userid)) { 787 \print_error('nopermission'); 788 } 789 if (!$assignment->can_grade()) { 790 \print_error('nopermission'); 791 } 792 793 $grade = $assignment->get_user_grade($userid, true, $attemptnumber); 794 795 $contextid = $assignment->get_context()->id; 796 $component = 'assignfeedback_editpdf'; 797 $filearea = self::FINAL_PDF_FILEAREA; 798 $itemid = $grade->id; 799 800 $fs = \get_file_storage(); 801 return $fs->delete_area_files($contextid, $component, $filearea, $itemid); 802 } 803 804 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |