Gitlab CSE Unil

Commit 84642d6f authored by M. Chardon's avatar M. Chardon
Browse files

editpdfplus : adaptations moodle 3.7

parent 11e3d7f4
......@@ -74,6 +74,7 @@ if ($action === 'pollconversions') {
'filecount' => 0,
'pagecount' => 0,
'pageready' => 0,
'partial' => false,
'pages' => [],
];
......@@ -81,10 +82,13 @@ if ($action === 'pollconversions') {
$response->status = $combineddocument->get_status();
$response->filecount = $combineddocument->get_document_count();
if ($response->status === combined_document::STATUS_READY) {
$readystatuslist = [combined_document::STATUS_READY, combined_document::STATUS_READY_PARTIAL];
$completestatuslist = [combined_document::STATUS_COMPLETE, combined_document::STATUS_FAILED];
if (in_array($response->status, $readystatuslist)) {
$combineddocument = document_services::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
$response->pagecount = $combineddocument->get_page_count();
} else if ($response->status === combined_document::STATUS_COMPLETE || $response->status === combined_document::STATUS_FAILED) {
} else if (in_array($response->status, $completestatuslist)) {
$pages = document_services::get_page_images_for_attempt($assignment, $userid, $attemptnumber, $readonly);
$response->pagecount = $combineddocument->get_page_count();
......@@ -96,6 +100,7 @@ if ($action === 'pollconversions') {
if ($readonly) {
$filearea = document_services::PAGE_IMAGE_READONLY_FILEAREA;
}
$response->partial = $combineddocument->is_partial_conversion();
foreach ($pages as $id => $pagefile) {
$index = count($response->pages);
......@@ -174,11 +179,11 @@ if ($action === 'pollconversions') {
$teachers = get_users_by_capability($context, 'assignfeedback/editpdfplus:notify');
$course = $assignment->get_course();
$coursemodule = $assignment->get_course_module();
$a = (object)[
'coursename' => format_string($course->shortname, true),
'modulename' => get_string('modulename', 'assign'),
'assignmentname' => format_string($assignment->get_instance()->name, true),
'url' => $response->url
$a = (object) [
'coursename' => format_string($course->shortname, true),
'modulename' => get_string('modulename', 'assign'),
'assignmentname' => format_string($assignment->get_instance()->name, true),
'url' => $response->url
];
foreach ($teachers as $teacher) {
$res = email_to_user($teacher, $USER, get_string('assignmentgradedsubject', 'assignfeedback_editpdfplus'), get_string('assignmentgradedbody', 'assignfeedback_editpdfplus', $a));
......@@ -205,6 +210,27 @@ if ($action === 'pollconversions') {
$result = $result && page_editor::unrelease_drafts($grade->id);
echo json_encode($result);
die();
} else if ($action == 'rotatepage') {
require_capability(PERMISSION_ASSIGN_GRADE, $context);
$response = new stdClass();
$index = required_param('index', PARAM_INT);
$grade = $assignment->get_user_grade($userid, true, $attemptnumber);
$rotateleft = required_param('rotateleft', PARAM_BOOL);
$filearea = document_services::PAGE_IMAGE_FILEAREA;
$pagefile = document_services::rotate_page($assignment, $userid, $attemptnumber, $index, $rotateleft);
$page = new stdClass();
$page->url = moodle_url::make_pluginfile_url($context->id, document_services::COMPONENT, $filearea,
$grade->id, '/', $pagefile->get_filename())->out();
if ($imageinfo = $pagefile->get_imageinfo()) {
$page->width = $imageinfo['width'];
$page->height = $imageinfo['height'];
} else {
$page->width = 0;
$page->height = 0;
}
$response = (object) ['page' => $page];
echo json_encode($response);
die();
} else if ($action == 'updatestudentview') {
require_capability(PERMISSION_ASSIGN_SUBMIT, $context);
......
......@@ -47,16 +47,22 @@ class backup_assignfeedback_editpdfplus_subplugin extends backup_subplugin {
$subpluginelementannotation = new backup_nested_element(
'feedback_editpdfplus_annotation', null, array(self::GRADEID, 'pageno', 'x', 'y', 'endx', 'endy', 'cartridgex', 'cartridgey', 'path', 'toolid', 'textannot', 'colour', 'draft', 'answerrequested', 'studentanswer', 'studentstatus', 'displaylock', 'displayrotation', 'borderstyle', 'parent_annot')
);
$subpluginelementrotation = new backup_nested_element('feedback_editpdfplus_rotation');
$subpluginelementpagerotation = new backup_nested_element('pagerotation', null,
array('gradeid', 'pageno', 'pathnamehash', 'isrotated', 'degree'));
// Connect XML elements into the tree.
$subplugin->add_child($subpluginwrapper);
$subpluginelementannotations->add_child($subpluginelementannotation);
$subpluginelementrotation->add_child($subpluginelementpagerotation);
$subpluginwrapper->add_child($subpluginelementfiles);
$subpluginwrapper->add_child($subpluginelementannotations);
$subpluginwrapper->add_child($subpluginelementrotation);
// Set source to populate the data.
$subpluginelementfiles->set_source_sql('SELECT id AS gradeid from {assign_grades} where id = :' . self::GRADEID, array(self::GRADEID => backup::VAR_PARENTID));
$subpluginelementannotation->set_source_table('assignfeedback_editpp_annot', array(self::GRADEID => backup::VAR_PARENTID));
$subpluginelementpagerotation->set_source_table('assignfeedback_editpp_rot', array('gradeid' => backup::VAR_PARENTID));
// We only need to backup the files in the final pdf area, and the readonly page images - the others can be regenerated.
$subpluginelementfiles->annotate_files('assignfeedback_editpdfplus', \assignfeedback_editpdfplus\document_services::FINAL_PDF_FILEAREA, self::GRADEID);
$subpluginelementfiles->annotate_files('assignfeedback_editpdfplus', \assignfeedback_editpdfplus\document_services::PAGE_IMAGE_READONLY_FILEAREA, self::GRADEID);
......
......@@ -52,6 +52,12 @@ class restore_assignfeedback_editpdfplus_subplugin extends restore_subplugin {
$elename = $this->get_namefor('feedback_editpdfplus_annotation');
$elepath = $this->get_pathfor('/feedback_editpdfplus_annotations/feedback_editpdfplus_annotation');
$paths[] = new restore_path_element($elename, $elepath);
// Rotation details.
$elename = $this->get_namefor('pagerotation');
$elepath = $this->get_pathfor('/feedback_editpdfplus_rotation/pagerotation');
$paths[] = new restore_path_element($elename, $elepath);
return $paths;
}
......@@ -84,4 +90,15 @@ class restore_assignfeedback_editpdfplus_subplugin extends restore_subplugin {
$DB->insert_record('assignfeedback_editpp_annot', $data);
}
/**
* Processes one /feedback_editpdfplus_rotation/pagerotation element
* @param mixed $data
*/
public function process_assignfeedback_editpdfplus_pagerotation($data) {
global $DB;
$data = (object)$data;
$oldgradeid = $data->gradeid;
$data->gradeid = $this->get_mappingid('grade', $oldgradeid);
$DB->insert_record('assignfeedback_editpp_rot', $data);
}
}
......@@ -40,7 +40,9 @@ class tool_generic extends tool {
"rectangle" => "fa fa-square-o",
"drag" => "fa fa-hand-paper-o",
"select" => "fa fa-mouse-pointer",
"annotationcolour" => "fa fa-tint"
"annotationcolour" => "fa fa-tint",
"rotateleft" => "fa fa-undo",
"rotateright" => "fa fa-undo fa-flip-horizontal"
);
/**
......
......@@ -47,6 +47,11 @@ class combined_document {
*/
const STATUS_COMPLETE = 2;
/**
* Status value representing all documents are ready to be combined as are supported.
*/
const STATUS_READY_PARTIAL = 3;
/**
* Status value representing a permanent error.
*/
......@@ -74,6 +79,9 @@ class combined_document {
/**
* Check the current status of the document combination.
* Note that the combined document may not contain all the source files if some of the
* source files were not able to be converted. An example is an audio file with a pdf cover sheet. Only
* the cover sheet will be included in the combined document.
*
* @return int
*/
......@@ -95,6 +103,7 @@ class combined_document {
}
$pending = false;
$partial = false;
foreach ($this->sourcefiles as $file) {
// The combined file has not yet been generated.
// Check the status of each source file.
......@@ -107,7 +116,9 @@ class combined_document {
break;
case \core_files\conversion::STATUS_FAILED:
return self::STATUS_FAILED;
$partial = true;
break;
default :
break;
}
......@@ -116,10 +127,27 @@ class combined_document {
if ($pending) {
return self::STATUS_PENDING_INPUT;
} else {
if ($partial) {
return self::STATUS_READY_PARTIAL;
}
return self::STATUS_READY;
}
}
/**
* Return true of the combined file contained only some of the submission files.
*
* @return boolean
*/
public function is_partial_conversion() {
$combinedfile = $this->get_combined_file();
if (empty($combinedfile)) {
return false;
}
$filearea = $combinedfile->get_filearea();
return $filearea == document_services::PARTIAL_PDF_FILEAREA;
}
/**
* Set the completed combined file.
*
......@@ -188,7 +216,7 @@ class combined_document {
$status = $file->get('status');
switch ($status) {
case \core_files\conversion::STATUS_COMPLETE:
continue;
continue 2;
default:
$converter->poll_conversion($conversion);
}
......@@ -210,11 +238,12 @@ class combined_document {
global $CFG;
$currentstatus = $this->get_status();
$readystatuslist = [self::STATUS_READY, self::STATUS_READY_PARTIAL];
if ($currentstatus === self::STATUS_FAILED) {
$this->store_empty_document($contextid, $itemid);
return $this;
} else if ($currentstatus !== self::STATUS_READY) {
} else if (!in_array($currentstatus, $readystatuslist)) {
// The document is either:
// * already combined; or
// * pending input being fully converted; or
......@@ -235,7 +264,10 @@ class combined_document {
// Note: We drop non-compatible files.
$compatiblepdf = false;
if (is_a($file, \core_files\conversion::class)) {
$compatiblepdf = pdf::ensure_pdf_compatible($file->get_destfile());
$status = $file->get('status');
if ($status == \core_files\conversion::STATUS_COMPLETE) {
$compatiblepdf = pdf::ensure_pdf_compatible($file->get_destfile());
}
} else {
$compatiblepdf = pdf::ensure_pdf_compatible($file);
}
......@@ -270,7 +302,7 @@ class combined_document {
}
// Store the newly created file as a stored_file.
$this->store_combined_file($tmpfile, $contextid, $itemid);
$this->store_combined_file($tmpfile, $contextid, $itemid, ($currentstatus == self::STATUS_READY_PARTIAL));
// Note the verified page count.
$this->pagecount = $verifypagecount;
......@@ -295,11 +327,12 @@ class combined_document {
* @param string $tmpfile The path to the file on disk to be stored.
* @param int $contextid The contextid for the file to be stored under
* @param int $itemid The itemid for the file to be stored under
* @param boolean $partial The combined pdf contains only some of the source files.
* @return $this
*/
protected function store_combined_file($tmpfile, $contextid, $itemid) {
protected function store_combined_file($tmpfile, $contextid, $itemid, $partial = false) {
// Store the file.
$record = $this->get_stored_file_record($contextid, $itemid);
$record = $this->get_stored_file_record($contextid, $itemid, $partial);
$fs = get_file_storage();
// Delete existing files first.
......@@ -349,12 +382,14 @@ class combined_document {
return $this->pagecount;
}
if ($this->get_status() === self::STATUS_FAILED) {
$status = $this->get_status();
if ($status === self::STATUS_FAILED) {
// The empty document will be returned.
return 1;
}
if ($this->get_status() !== self::STATUS_COMPLETE) {
if ($status !== self::STATUS_COMPLETE) {
// No pages yet.
return 0;
}
......@@ -391,13 +426,18 @@ class combined_document {
*
* @param int $contextid The contextid for the file to be stored under
* @param int $itemid The itemid for the file to be stored under
* @param boolean $partial The combined file contains only some of the source files.
* @return stdClass
*/
protected function get_stored_file_record($contextid, $itemid) {
protected function get_stored_file_record($contextid, $itemid, $partial = false) {
$filearea = document_services::COMBINED_PDF_FILEAREA;
if ($partial) {
$filearea = document_services::PARTIAL_PDF_FILEAREA;
}
return (object) [
'contextid' => $contextid,
'component' => 'assignfeedback_editpdfplus',
'filearea' => document_services::COMBINED_PDF_FILEAREA,
'filearea' => $filearea,
'itemid' => $itemid,
'filepath' => '/',
'filename' => document_services::COMBINED_PDF_FILENAME,
......
......@@ -37,12 +37,18 @@ use DOMDocument;
class document_services {
/** Compoment name */
const COMPONENT = "assignfeedback_editpdfplus";
/** File area for generated pdf */
const FINAL_PDF_FILEAREA = 'download';
/** File area for combined pdf */
const COMBINED_PDF_FILEAREA = 'combined';
/** File area for partial combined pdf */
const PARTIAL_PDF_FILEAREA = 'partial';
/** File area for importing html */
const IMPORT_HTML_FILEAREA = 'importhtml';
......@@ -85,7 +91,6 @@ RTg3MkIyMjU4NTI2RTg+IF0KL0RvY0NoZWNrc3VtIC9BNTYwMEZCMDAzRURCRTg0MTNBNTk3RTZF
MURDQzJBRgo+PgpzdGFydHhyZWYKNzM2CiUlRU9GCg==
EOD;
const NOPERMISSIONMESSAGE = "nopermission";
const PLUGIN_NAME = "assignfeedback_editpdfplus";
/**
* This function will take an int or an assignment instance and
......@@ -128,23 +133,24 @@ EOD;
* @return string New html with no image tags.
*/
protected static function strip_images($html) {
// Load HTML and suppress any parsing errors (DOMDocument->loadHTML() does not current support HTML5 tags).
$dom = new DOMDocument();
$dom->loadHTML("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" . $html);
$images = $dom->getElementsByTagName('img');
$i = 0;
for ($i = ($images->length - 1); $i >= 0; $i--) {
$node = $images->item($i);
if ($node->hasAttribute('alt')) {
$replacement = ' [ ' . $node->getAttribute('alt') . ' ] ';
} else {
$replacement = ' ';
libxml_use_internal_errors(true);
$dom->loadHTML('<?xml version="1.0" encoding="UTF-8" ?>' . $html);
libxml_clear_errors();
// Find all img tags.
if ($imgnodes = $dom->getElementsByTagName('img')) {
// Replace img nodes with the img alt text without overriding DOM elements.
for ($i = ($imgnodes->length - 1); $i >= 0; $i--) {
$imgnode = $imgnodes->item($i);
$alt = ($imgnode->hasAttribute('alt')) ? ' [ ' . $imgnode->getAttribute('alt') . ' ] ' : ' ';
$textnode = $dom->createTextNode($alt);
$imgnode->parentNode->replaceChild($textnode, $imgnode);
}
$text = $dom->createTextNode($replacement);
$node->parentNode->replaceChild($text, $node);
}
$count = 1;
return str_replace("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>", "", $dom->saveHTML(), $count);
}
......@@ -200,7 +206,7 @@ EOD;
} else if ($converter->can_convert_format_to('html', 'pdf')) {
$record = new \stdClass();
$record->contextid = $assignment->get_context()->id;
$record->component = self::PLUGIN_NAME;
$record->component = self::COMPONENT;
$record->filearea = self::IMPORT_HTML_FILEAREA;
$record->itemid = $submission->id;
$record->filepath = '/';
......@@ -277,7 +283,7 @@ EOD;
}
$contextid = $assignment->get_context()->id;
$component = self::PLUGIN_NAME;
$component = self::COMPONENT;
$filearea = self::COMBINED_PDF_FILEAREA;
$itemid = $grade->id;
$filepath = '/';
......@@ -352,7 +358,7 @@ EOD;
if ($readonly) {
$grade = $assignment->get_user_grade($userid, true, $attemptnumber);
$fs = get_file_storage();
$files = $fs->get_directory_files($assignment->get_context()->id, self::PLUGIN_NAME, self::PAGE_IMAGE_READONLY_FILEAREA, $grade->id, '/');
$files = $fs->get_directory_files($assignment->get_context()->id, self::COMPONENT, self::PAGE_IMAGE_READONLY_FILEAREA, $grade->id, '/');
$pagecount = count($files);
if ($pagecount > 0) {
return $pagecount;
......@@ -369,9 +375,10 @@ EOD;
* @param int|\assign $assignment
* @param int $userid
* @param int $attemptnumber (-1 means latest attempt)
* @param bool $resetrotation check if need to reset page rotation information
* @return array(stored_file)
*/
protected static function generate_page_images_for_attempt($assignment, $userid, $attemptnumber) {
protected static function generate_page_images_for_attempt($assignment, $userid, $attemptnumber, $resetrotation = true) {
global $CFG;
require_once($CFG->libdir . '/pdflib.php');
......@@ -406,7 +413,7 @@ EOD;
$record = new \stdClass();
$record->contextid = $assignment->get_context()->id;
$record->component = self::PLUGIN_NAME;
$record->component = self::COMPONENT;
$record->filearea = self::PAGE_IMAGE_FILEAREA;
$record->itemid = $grade->id;
$record->filepath = '/';
......@@ -419,6 +426,16 @@ EOD;
for ($i = 0; $i < $pagecount; $i++) {
try {
$image = $pdf->get_image($i);
if (!$resetrotation) {
$pagerotation = page_editor::get_page_rotation($grade->id, $i);
$degree = !empty($pagerotation) ? $pagerotation->degree : 0;
if ($degree != 0) {
$filepath = $tmpdir . '/' . $image;
$imageresource = imagecreatefrompng($filepath);
$content = imagerotate($imageresource, $degree, 0);
imagepng($content, $filepath);
}
}
} catch (\moodle_exception $e) {
// We catch only moodle_exception here as other exceptions indicate issue with setup not the pdf.
$image = pdf::get_error_image($tmpdir, $i);
......@@ -426,6 +443,12 @@ EOD;
$record->filename = basename($image);
$files[$i] = $fs->create_file_from_pathname($record, $tmpdir . '/' . $image);
@unlink($tmpdir . '/' . $image);
// Set page rotation default value.
if (!empty($files[$i])) {
if ($resetrotation) {
page_editor::set_page_rotation($grade->id, $i, false, $files[$i]->get_pathnamehash());
}
}
}
$pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
......@@ -471,7 +494,7 @@ EOD;
$grade = $assignment->get_user_grade($userid, true, $attemptnumber);
$contextid = $assignment->get_context()->id;
$component = self::PLUGIN_NAME;
$component = self::COMPONENT;
$itemid = $grade->id;
$filepath = '/';
$filearea = self::PAGE_IMAGE_FILEAREA;
......@@ -492,6 +515,7 @@ EOD;
$files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath);
$pages = array();
$resetrotation = false;
if (!empty($files)) {
$first = reset($files);
$pagemodified = $first->get_timemodified();
......@@ -512,6 +536,7 @@ EOD;
$fs->delete_area_files($contextid, $component, $filearea, $itemid);
page_editor::delete_draft_content($itemid);
$files = array();
$resetrotation = true;
} else {
// Need to reorder the files following their name.
......@@ -521,7 +546,7 @@ EOD;
preg_match('/page([\d]+)\./', $file->get_filename(), $matches);
if (empty($matches) or ! is_numeric($matches[1])) {
throw new \coding_exception("'" . $file->get_filename()
. "' file hasn't the expected format filename: image_pageXXXX.png.");
. "' file hasn't the expected format filename: image_pageXXXX.png.");
}
$pagenumber = (int) $matches[1];
......@@ -538,7 +563,7 @@ EOD;
// 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);
$pages = self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber, $resetrotation);
}
return $pages;
......@@ -621,7 +646,7 @@ EOD;
$combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
$file->copy_content_to($combined); // Copy the file.
$pdf = new pdf();
$pdf = new dompdf();
$pagecount = $pdf->set_pdf($combined);
$grade = $assignment->get_user_grade($userid, true, $attemptnumber);
......@@ -640,7 +665,7 @@ EOD;
$record = new \stdClass();
$record->contextid = $assignment->get_context()->id;
$record->component = self::PLUGIN_NAME;
$record->component = self::COMPONENT;
$record->filearea = self::FINAL_PDF_FILEAREA;
$record->itemid = $grade->id;
$record->filepath = '/';
......@@ -672,9 +697,23 @@ EOD;
protected static function addAnnotationstoPDF($pdf, $pagecount, $gradeid) {
$annotation_index = [];
$compteur = 1;
$fs = get_file_storage();
for ($i = 0; $i < $pagecount; $i++) {
$pdf->copy_page();
$pagerotation = page_editor::get_page_rotation($gradeid, $i);
$pagemargin = $pdf->getBreakMargin();
$autopagebreak = $pdf->getAutoPageBreak();
if (empty($pagerotation) || !$pagerotation->isrotated) {
$pdf->copy_page();
} else {
$rotatedimagefile = $fs->get_file_by_hash($pagerotation->pathnamehash);
if (empty($rotatedimagefile)) {
$pdf->copy_page();
} else {
$pdf->add_image_page($rotatedimagefile);
}
}
$annotations = page_editor::get_annotations($gradeid, $i, false);
foreach ($annotations as $annotation) {
......@@ -685,6 +724,8 @@ EOD;
$annotation_index[$annotation->id] = $compteur;
$compteur++;
}
$pdf->SetAutoPageBreak($autopagebreak, $pagemargin);
$pdf->setPageMark();
}
//add feedback by annotation
......@@ -757,7 +798,7 @@ EOD;
$fs = get_file_storage();
$assignment = self::get_assignment_from_param($assignment);
$contextid = $assignment->get_context()->id;
$component = self::PLUGIN_NAME;
$component = self::COMPONENT;
$itemid = $grade->id;
// Get all the pages.
......@@ -794,7 +835,7 @@ EOD;
$grade = $assignment->get_user_grade($userid, true, $attemptnumber);
$contextid = $assignment->get_context()->id;
$component = self::PLUGIN_NAME;
$component = self::COMPONENT;
$filearea = self::FINAL_PDF_FILEAREA;
$itemid = $grade->id;
$filepath = '/';
......@@ -828,7 +869,7 @@ EOD;
$grade = $assignment->get_user_grade($userid, true, $attemptnumber);
$contextid = $assignment->get_context()->id;
$component = self::PLUGIN_NAME;
$component = self::COMPONENT;
$filearea = self::FINAL_PDF_FILEAREA;
$itemid = $grade->id;
......@@ -836,4 +877,130 @@ EOD;
return $fs->delete_area_files($contextid, $component, $filearea, $itemid);
}
/**
* Get All files in a File area
* @param int|\assign $assignment Assignment
* @param int $userid User ID
* @param int $attemptnumber Attempt Number
* @param string $filearea File Area
* @param string $filepath File Path
* @return array
*/
private static function get_files($assignment, $userid, $attemptnumber, $filearea, $filepath = '/') {
$grade = $assignment->get_user_grade($userid, true, $attemptnumber);
$itemid = $grade->id;
$contextid = $assignment->get_context()->id;
$component = self::COMPONENT;
$fs = get_file_storage();
$files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath);
return $files;
}
/**
* Save file.
* @param int|\assign $assignment Assignment
* @param int $userid User ID
* @param int $attemptnumber Attempt Number
* @param string $filearea File Area
* @param string $newfilepath File Path
* @param string $storedfilepath stored file path
* @return \stored_file
* @throws \file_exception
* @throws \stored_file_creation_exception
*/
private static function save_file($assignment, $userid, $attemptnumber, $filearea, $newfilepath, $storedfilepath = '/') {
$grade = $assignment->get_user_grade($userid, true, $attemptnumber);
$itemid = $grade->id;
$contextid = $assignment->get_context()->id;
$record = new \stdClass();
$record->contextid = $contextid;
$record->component = self::COMPONENT;
$record->filearea = $filearea;
$record->itemid = $itemid;
$record->filepath = $storedfilepath;
$record->filename = basename($newfilepath);
$fs = get_file_storage();
$oldfile = $fs->get_file($record->contextid, $record->component, $record->filearea,