From 2df40c4f90b178e986a67849bb606832ba18e4e7 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Wed, 11 Jun 2014 18:11:01 +0800 Subject: [PATCH 1/3] MDL-45580 assignfeedback_editpdf: Store a readonly version of the PDF --- mod/assign/feedback/editpdf/ajax.php | 18 ++- mod/assign/feedback/editpdf/ajax_progress.php | 1 + .../editpdf/classes/document_services.php | 111 ++++++++++++++++-- mod/assign/feedback/editpdf/locallib.php | 3 +- ...dle-assignfeedback_editpdf-editor-debug.js | 3 +- ...oodle-assignfeedback_editpdf-editor-min.js | 6 +- .../moodle-assignfeedback_editpdf-editor.js | 3 +- .../editpdf/yui/src/editor/js/editor.js | 3 +- 8 files changed, 128 insertions(+), 20 deletions(-) diff --git a/mod/assign/feedback/editpdf/ajax.php b/mod/assign/feedback/editpdf/ajax.php index 1451d5a7b44..8baf3ffb7f8 100644 --- a/mod/assign/feedback/editpdf/ajax.php +++ b/mod/assign/feedback/editpdf/ajax.php @@ -37,6 +37,7 @@ $action = optional_param('action', '', PARAM_ALPHANUM); $assignmentid = required_param('assignmentid', PARAM_INT); $userid = required_param('userid', PARAM_INT); $attemptnumber = required_param('attemptnumber', PARAM_INT); +$readonly = optional_param('readonly', false, PARAM_BOOL); $cm = \get_coursemodule_from_instance('assign', $assignmentid, 0, false, MUST_EXIST); $context = \context_module::instance($cm->id); @@ -53,12 +54,19 @@ if ($action == 'loadallpages') { $draft = true; if (!has_capability('mod/assign:grade', $context)) { $draft = false; + $readonly = true; // A student always sees the readonly version. require_capability('mod/assign:submit', $context); } + // Whoever is viewing the readonly version should not use the drafts, but the actual annotations. + if ($readonly) { + $draft = false; + } + $pages = document_services::get_page_images_for_attempt($assignment, $userid, - $attemptnumber); + $attemptnumber, + $readonly); $response = new stdClass(); $response->pagecount = count($pages); @@ -66,13 +74,19 @@ if ($action == 'loadallpages') { $grade = $assignment->get_user_grade($userid, true); + // The readonly files are stored in a different file area. + $filearea = document_services::PAGE_IMAGE_FILEAREA; + if ($readonly) { + $filearea = document_services::PAGE_IMAGE_READONLY_FILEAREA; + } + foreach ($pages as $id => $pagefile) { $index = count($response->pages); $page = new stdClass(); $comments = page_editor::get_comments($grade->id, $index, $draft); $page->url = moodle_url::make_pluginfile_url($context->id, 'assignfeedback_editpdf', - document_services::PAGE_IMAGE_FILEAREA, + $filearea, $grade->id, '/', $pagefile->get_filename())->out(); diff --git a/mod/assign/feedback/editpdf/ajax_progress.php b/mod/assign/feedback/editpdf/ajax_progress.php index 184a957550c..815484818cd 100644 --- a/mod/assign/feedback/editpdf/ajax_progress.php +++ b/mod/assign/feedback/editpdf/ajax_progress.php @@ -49,6 +49,7 @@ try { throw new coding_exception('grade not found'); } + // No need to handle the readonly files here, the should be already generated. $component = 'assignfeedback_editpdf'; $filearea = document_services::PAGE_IMAGE_FILEAREA; $filepath = '/'; diff --git a/mod/assign/feedback/editpdf/classes/document_services.php b/mod/assign/feedback/editpdf/classes/document_services.php index 468b1032921..7ae0b30ef02 100644 --- a/mod/assign/feedback/editpdf/classes/document_services.php +++ b/mod/assign/feedback/editpdf/classes/document_services.php @@ -42,6 +42,8 @@ class document_services { const COMBINED_PDF_FILEAREA = 'combined'; /** File area for page images */ const PAGE_IMAGE_FILEAREA = 'pages'; + /** File area for readonly page images */ + const PAGE_IMAGE_READONLY_FILEAREA = 'readonlypages'; /** Filename for combined pdf */ const COMBINED_PDF_FILENAME = 'combined.pdf'; @@ -268,9 +270,10 @@ class document_services { * @param int|\assign $assignment * @param int $userid * @param int $attemptnumber (-1 means latest attempt) + * @param bool $readonly When true we get the number of pages for the readonly version. * @return int number of pages */ - public static function page_number_for_attempt($assignment, $userid, $attemptnumber) { + public static function page_number_for_attempt($assignment, $userid, $attemptnumber, $readonly = false) { global $CFG; require_once($CFG->libdir . '/pdflib.php'); @@ -281,6 +284,19 @@ class document_services { \print_error('nopermission'); } + // When in readonly we can return the number of images in the DB because they should already exist, + // if for some reason they do not, then we proceed as for the normal version. + if ($readonly) { + $grade = $assignment->get_user_grade($userid, true, $attemptnumber); + $fs = get_file_storage(); + $files = $fs->get_directory_files($assignment->get_context()->id, 'assignfeedback_editpdf', + self::PAGE_IMAGE_READONLY_FILEAREA, $grade->id, '/'); + $pagecount = count($files); + if ($pagecount > 0) { + return $pagecount; + } + } + // Get a combined pdf file from all submitted pdf files. $file = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber); if (!$file) { @@ -363,12 +379,25 @@ class document_services { /** * This function returns a list of the page images from a pdf. + * + * The readonly version is different than the normal one. The readonly version contains a copy + * of the pages in the state they were when the PDF was annotated, by doing so we prevent the + * the pages that are displayed to change as soon as the submission changes. + * + * Though there is an edge case, if the PDF was annotated before MDL-45580, then it is possible + * that we do not find any readonly version of the pages. In that case, we will get the normal + * pages and copy them to the readonly area. This ensures that the pages will remain in that + * state until the submission is updated. When the normal files do not exist, we throw an exception + * because the readonly pages should only ever be displayed after a teacher has annotated the PDF, + * they would not exist until they do. + * * @param int|\assign $assignment * @param int $userid * @param int $attemptnumber (-1 means latest attempt) + * @param bool $readonly If true, then we are requesting the readonly version. * @return array(stored_file) */ - public static function get_page_images_for_attempt($assignment, $userid, $attemptnumber) { + public static function get_page_images_for_attempt($assignment, $userid, $attemptnumber, $readonly = false) { $assignment = self::get_assignment_from_param($assignment); @@ -385,26 +414,37 @@ class document_services { $contextid = $assignment->get_context()->id; $component = 'assignfeedback_editpdf'; - $filearea = self::PAGE_IMAGE_FILEAREA; $itemid = $grade->id; $filepath = '/'; + $filearea = self::PAGE_IMAGE_FILEAREA; $fs = \get_file_storage(); + // If we are after the readonly pages... + $copytoreadonly = false; + if ($readonly) { + $filearea = self::PAGE_IMAGE_READONLY_FILEAREA; + if ($fs->is_area_empty($contextid, $component, $filearea, $itemid)) { + // We have a problem here, we were supposed to find the files... + // let's fallback on the other area, and copy the files to the readonly area. + $copytoreadonly = true; + $filearea = self::PAGE_IMAGE_FILEAREA; + } + } + $files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath); + $pages = array(); if (!empty($files)) { $first = reset($files); - if ($first->get_timemodified() < $submission->timemodified) { - + if (!$readonly && $first->get_timemodified() < $submission->timemodified) { + // Image files are stale - regenerate them, except in readonly mode. $fs->delete_area_files($contextid, $component, $filearea, $itemid); - // Image files are stale - regenerate them. $files = array(); } else { // Need to reorder the files following their name. // because get_directory_files() return a different order than generate_page_images_for_attempt(). - $orderedfiles = array(); foreach($files as $file) { // Extract the page number from the file name image_pageXXXX.png. preg_match('/page([\d]+)\./', $file->get_filename(), $matches); @@ -415,14 +455,26 @@ class document_services { $pagenumber = (int)$matches[1]; // Save the page in the ordered array. - $orderedfiles[$pagenumber] = $file; + $pages[$pagenumber] = $file; } - ksort($orderedfiles); + ksort($pages); - return $orderedfiles; + if ($copytoreadonly) { + self::copy_pages_to_readonly_area($assignment, $grade); + } } } - return self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber); + + if (empty($pages)) { + if ($readonly) { + // This should never happen, there should be a version of the pages available + // whenever we are requesting the readonly version. + throw new \moodle_exception('Could not find readonly pages for grade ' . $grade->id); + } + $pages = self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber); + } + + return $pages; } /** @@ -465,6 +517,11 @@ class document_services { /** * This function takes the combined pdf and embeds all the comments and annotations. + * + * This also moves the annotations and comments from drafts to not drafts. And it will + * copy all the images stored to the readonly area, so that they can be viewed online, and + * not be overwritten when a new submission is sent. + * * @param int|\assign $assignment * @param int $userid * @param int $attemptnumber (-1 means latest attempt) @@ -567,9 +624,41 @@ class document_services { @unlink($combined); @rmdir($tmpdir); + self::copy_pages_to_readonly_area($assignment, $grade); + return $file; } + /** + * Copy the pages image to the readonly area. + * + * @param int|\assign $assignment The assignment. + * @param \stdClass $grade The grade record. + * @return void + */ + public static function copy_pages_to_readonly_area($assignment, $grade) { + $fs = get_file_storage(); + $assignment = self::get_assignment_from_param($assignment); + $contextid = $assignment->get_context()->id; + $component = 'assignfeedback_editpdf'; + $itemid = $grade->id; + + // Get all the pages. + $originalfiles = $fs->get_area_files($contextid, $component, self::PAGE_IMAGE_FILEAREA, $itemid); + if (empty($originalfiles)) { + // Nothing to do here... + return; + } + + // Delete the old readonly files. + $fs->delete_area_files($contextid, $component, self::PAGE_IMAGE_READONLY_FILEAREA, $itemid); + + // Do the copying. + foreach ($originalfiles as $originalfile) { + $fs->create_file_from_storedfile(array('filearea' => self::PAGE_IMAGE_READONLY_FILEAREA), $originalfile); + } + } + /** * This function returns the generated pdf (if it exists). * @param int|\assign $assignment diff --git a/mod/assign/feedback/editpdf/locallib.php b/mod/assign/feedback/editpdf/locallib.php index 90fcac7ad41..10275246ed0 100644 --- a/mod/assign/feedback/editpdf/locallib.php +++ b/mod/assign/feedback/editpdf/locallib.php @@ -137,7 +137,8 @@ class assign_feedback_editpdf extends assign_feedback_plugin { // Retrieve total number of pages. $pagetotal = document_services::page_number_for_attempt($this->assignment->get_instance()->id, $userid, - $attempt); + $attempt, + $readonly); $widget = new assignfeedback_editpdf_widget($this->assignment->get_instance()->id, $userid, diff --git a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js index df2407af783..7f5b1ab36cc 100644 --- a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js +++ b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js @@ -3245,7 +3245,8 @@ EDITOR.prototype = { action : 'loadallpages', userid : this.get('userid'), attemptnumber : this.get('attemptnumber'), - assignmentid : this.get('assignmentid') + assignmentid : this.get('assignmentid'), + readonly : this.get('readonly') ? 1 : 0 }, on: { success: function(tid, response) { diff --git a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js index 3d4b3b9b795..5bc5e3fb335 100644 --- a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js +++ b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js @@ -3,6 +3,6 @@ YUI.add("moodle-assignfeedback_editpdf-editor",function(e,t){var n=M.cfg.wwwroot ,this.endy)]),i=c[this.colour],i=i.replace("rgb","rgba"),i=i.replace(")",",0.5)"),n=this.editor.graphic.addShape({type:e.Rect,width:r.width,height:r.height,stroke:!1,fill:{color:i},x:r.x,y:r.y}),t.shapes.push(n),this.drawable=t,ANNOTATIONHIGHLIGHT.superclass.draw.apply(this)},draw_current_edit:function(t){var n=new M.assignfeedback_editpdf.drawable(this.editor),r,i,s;return i=new M.assignfeedback_editpdf.rect,i.bound([new M.assignfeedback_editpdf.point(t.start.x,t.start.y),new M.assignfeedback_editpdf.point(t.end.x,t.end.y)]),i.has_min_width()||i.set_min_width(),s=c[t.annotationcolour],s=s.replace("rgb","rgba"),s=s.replace(")",",0.5)"),r=this.editor.graphic.addShape({type:e.Rect,width:i.width,height:16,stroke:!1,fill:{color:s},x:i.x,y:t.start.y}),n.shapes.push(r),n},init_from_edit:function(e){var t=new M.assignfeedback_editpdf.rect;return t.bound([e.start,e.end]),this.gradeid=this.editor.get("gradeid"),this.pageno=this.editor.currentpage,this.x=t.x,this.y=e.start.y,this.endx=t.x+t.width,this.endy=e.start.y+16,this.colour=e.annotationcolour,this.page="",t.has_min_width()}}),M.assignfeedback_editpdf=M.assignfeedback_editpdf||{},M.assignfeedback_editpdf.annotationhighlight=ANNOTATIONHIGHLIGHT,ANNOTATIONSTAMP=function(e){ANNOTATIONSTAMP.superclass.constructor.apply(this,[e])},ANNOTATIONSTAMP.NAME="annotationstamp",ANNOTATIONSTAMP.ATTRS={},e.extend(ANNOTATIONSTAMP,M.assignfeedback_editpdf.annotation,{draw:function(){var t=new M.assignfeedback_editpdf.drawable(this.editor),n=e.one(o.DRAWINGREGION),r,i;return i=this.editor.get_window_coordinates(new M.assignfeedback_editpdf.point(this.x,this.y)),r=e.Node.create("
"),r.setStyles({position:"absolute",display:"inline-block",backgroundImage:"url("+this.editor.get_stamp_image_url(this.path)+")",width:this.endx-this.x,height:this.endy-this.y,backgroundSize:"100% 100%",zIndex:50}),n.append(r),r.setX(i.x),r.setY(i.y),r.on("gesturemovestart",this.editor.edit_start,null,this.editor),r.on("gesturemove",this.editor.edit_move,null,this.editor),r.on("gesturemoveend",this.editor.edit_end,null,this.editor),t.nodes.push(r),this.drawable=t,ANNOTATIONSTAMP.superclass.draw.apply(this)},draw_current_edit:function(t){var n=new M.assignfeedback_editpdf.rect,r=new M.assignfeedback_editpdf.drawable(this.editor),i=e.one(o.DRAWINGREGION),s,u;return n.bound([t.start,t.end]),u=this.editor.get_window_coordinates(new M.assignfeedback_editpdf.point(n.x,n.y)),s=e.Node.create("
"),s.setStyles({position:"absolute",display:"inline-block",backgroundImage:"url("+this.editor.get_stamp_image_url(t.stamp)+")",width:n.width,height:n.height,backgroundSize:"100% 100%",zIndex:50}),i.append(s),s.setX(u.x),s.setY(u.y),r.nodes.push(s),r},init_from_edit:function(e){var t=new M.assignfeedback_editpdf.rect;return t.bound([e.start,e.end]),t.width<40&&(t.width=40),t.height<40&&(t.height=40),this.gradeid=this.editor.get("gradeid"),this.pageno=this.editor.currentpage,this.x=t.x,this.y=t.y,this.endx=t.x+t.width,this.endy=t.y+t.height,this.colour=e.annotationcolour,this.path=e.stamp,!0},move:function(e,t){var n=e-this.x,r=t-this.y;this.x+=n,this.y+=r,this.endx+=n,this.endy+=r,this.drawable&&this.drawable.erase(),this.editor.drawables.push(this.draw())}}),M.assignfeedback_editpdf=M.assignfeedback_editpdf||{},M.assignfeedback_editpdf.annotationstamp=ANNOTATIONSTAMP;var v="Dropdown menu",m;m=function(e){e.draggable=!1,e.centered=!1,e.width="auto",e.lightbox=!1,e.visible=!1,e.footerContent="",m.superclass.constructor.apply(this,[e])},e.extend(m,M.core.dialogue,{initializer:function(t){var n,r,i,s;m.superclass.initializer.call(this,t),s=this.get("boundingBox"),s.addClass("assignfeedback_editpdf_dropdown"),n=this.get("buttonNode"),r=this.bodyNode,i=e.Node.create("

"),i.addClass("accesshide"),i.setHTML(this.get("headerText")),r.prepend(i),r.on("clickoutside",function(e){this.get("visible")&&e.target.get("id")!==n.get("id")&&e.target.ancestor().get("id")!==n.get("id")&&(e.preventDefault(),this.hide())},this),n.on("click",function(e){e.preventDefault(),this.show()},this),n.on("key",this.show,"enter,space",this)},show:function(){var t=this.get("buttonNode");result=m.superclass.show.call(this),this.align(t,[e.WidgetPositionAlign.TL,e.WidgetPositionAlign.BL])}},{NAME:v,ATTRS:{headerText:{value:""},buttonNode:{value:null}}}),M.assignfeedback_editpdf=M.assignfeedback_editpdf||{},M.assignfeedback_editpdf.dropdown=m;var g="Colourpicker",b;b=function(e){b.superclass.constructor.apply(this,[e])},e.extend(b,M.assignfeedback_editpdf.dropdown,{initializer:function(t){var n=e.Node.create('