diff --git a/amd/build/submissionform.min.js b/amd/build/submissionform.min.js new file mode 100644 index 00000000..5e7ae076 --- /dev/null +++ b/amd/build/submissionform.min.js @@ -0,0 +1,8 @@ +/** + * Submission form utility. + * @copyright 2024 Astor Bizard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define("mod_vpl/submissionform",["jquery"],(function($){return{setup:function(){var updateHeaders=function(submitType){var archive="archive"==submitType;$("#id_headersubmitarchive").toggle(archive).toggleClass("collapsed",!archive),$("#id_headersubmitfiles").toggle(!archive).toggleClass("collapsed",archive)},$select=$('select[name="submitmethod"]');$select.change((function(){updateHeaders($select.val())})),updateHeaders($select.val())}}})); + +//# sourceMappingURL=submissionform.min.js.map \ No newline at end of file diff --git a/amd/build/submissionform.min.js.map b/amd/build/submissionform.min.js.map new file mode 100644 index 00000000..a6197915 --- /dev/null +++ b/amd/build/submissionform.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"submissionform.min.js","sources":["../src/submissionform.js"],"sourcesContent":["// This file is part of VPL for Moodle - http://vpl.dis.ulpgc.es/\n//\n// VPL for Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// VPL for Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with VPL for Moodle. If not, see .\n\n/**\n * Submission form utility.\n * @copyright 2024 Astor Bizard\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n return {\n setup: function() {\n var updateHeaders = function(submitType) {\n var archive = submitType == 'archive';\n $('#id_headersubmitarchive').toggle(archive).toggleClass('collapsed', !archive);\n $('#id_headersubmitfiles').toggle(!archive).toggleClass('collapsed', archive);\n };\n var $select = $('select[name=\"submitmethod\"]');\n $select.change(function() {\n updateHeaders($select.val());\n });\n updateHeaders($select.val());\n }\n };\n});\n"],"names":["define","$","setup","updateHeaders","submitType","archive","toggle","toggleClass","$select","change","val"],"mappings":";;;;;AAqBAA,gCAAO,CAAC,WAAW,SAASC,GACxB,MAAO,CACHC,MAAO,WACH,IAAIC,cAAgB,SAASC,YACzB,IAAIC,QAAwB,WAAdD,WACdH,EAAE,2BAA2BK,OAAOD,SAASE,YAAY,aAAcF,SACvEJ,EAAE,yBAAyBK,QAAQD,SAASE,YAAY,YAAaF,UAErEG,QAAUP,EAAE,+BAChBO,QAAQC,QAAO,WACXN,cAAcK,QAAQE,MAC1B,IACAP,cAAcK,QAAQE,MAC1B,EAER"} \ No newline at end of file diff --git a/amd/src/submissionform.js b/amd/src/submissionform.js new file mode 100644 index 00000000..b5cfe180 --- /dev/null +++ b/amd/src/submissionform.js @@ -0,0 +1,37 @@ +// This file is part of VPL for Moodle - http://vpl.dis.ulpgc.es/ +// +// VPL for Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// VPL for Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with VPL for Moodle. If not, see . + +/** + * Submission form utility. + * @copyright 2024 Astor Bizard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery'], function($) { + return { + setup: function() { + var updateHeaders = function(submitType) { + var archive = submitType == 'archive'; + $('#id_headersubmitarchive').toggle(archive).toggleClass('collapsed', !archive); + $('#id_headersubmitfiles').toggle(!archive).toggleClass('collapsed', archive); + }; + var $select = $('select[name="submitmethod"]'); + $select.change(function() { + updateHeaders($select.val()); + }); + updateHeaders($select.val()); + } + }; +}); diff --git a/forms/submission.php b/forms/submission.php index bf27374f..88acc9e2 100644 --- a/forms/submission.php +++ b/forms/submission.php @@ -62,7 +62,7 @@ $instance = $vpl->get_instance(); $vpl->print_header( get_string( 'submission', VPL ) ); $vpl->print_view_tabs( basename( __FILE__ ) ); -$mform = new mod_vpl_submission_form( 'submission.php', $vpl ); +$mform = new mod_vpl_submission_form( 'submission.php', $vpl, $userid ); if ($mform->is_cancelled()) { vpl_inmediate_redirect( vpl_mod_href( 'view.php', 'id', $id ) ); die(); @@ -76,20 +76,94 @@ die(); } \mod_vpl\util\phpconfig::increase_memory_limit(); + $rfn = $vpl->get_required_fgm(); + $reqfiles = $rfn->getFileList(); $files = []; - for ($i = 0; $i < $instance->maxfiles; $i ++) { - $attribute = 'file' . $i; - $name = $mform->get_new_filename( $attribute ); - $data = $mform->get_file_content( $attribute ); - if ($data !== false && $name !== false) { + $prevsub = $vpl->last_user_submission( $userid ); + $firstsub = ($prevsub === false); + if (!$firstsub) { + $prevsubfiles = (new mod_vpl_submission( $vpl, $prevsub ))->get_submitted_fgm()->getAllFiles(); + } + if ($fromform->submitmethod == 'archive') { + if (!$firstsub && $fromform->archiveaction == 'replace') { + // Use files of previous submission. + foreach ($prevsubfiles as $subfilename => $subfilecontent) { + $files[$subfilename] = $subfilecontent; + } + } + // Open archive. + $zipname = $mform->save_temp_file( 'archive' ); + $zip = new ZipArchive(); + $zip->open($zipname); + $subreqfiles = []; + $subotherfiles = []; + // Read archive and split between required / additional files. + for ($i = 0; $i < $zip->numFiles; $i++) { + $filename = $zip->statIndex($i)['name']; + if (substr($filename, -1) == '/') { // Directory. + continue; + } + $filecontent = file_get_contents('zip://' . $zipname . '#' . $filename); // Autodetect text file encode if not binary. - if (! vpl_is_binary( $name )) { - $encode = mb_detect_encoding( $data, 'UNICODE, UTF-16, UTF-8, ISO-8859-1', true ); - if ($encode > '') { // If code detected. - $data = iconv( $encode, 'UTF-8', $data ); + if (! vpl_is_binary( $filename )) { + $encoding = mb_detect_encoding( $filecontent, 'UNICODE, UTF-16, UTF-8, ISO-8859-1', true ); + if ($encoding > '') { // If code detected. + $filecontent = iconv( $encoding, 'UTF-8', $filecontent ); + } + } else { + if (in_array($filename . '.b64', $reqfiles)) { + $filename = $filename . '.b64'; + $filecontent = base64_encode($filecontent); + } + } + if (in_array($filename, $reqfiles)) { + $subreqfiles[$filename] = $filecontent; + } else { + $subotherfiles[$filename] = $filecontent; + } + } + foreach ($reqfiles as $reqfile) { + if (isset($subreqfiles[$reqfile])) { + $files[$reqfile] = $subreqfiles[$reqfile]; + } + } + foreach ($subotherfiles as $filename => $filecontent) { + $files[$filename] = $filecontent; + } + // Close archive. + $zip->close(); + unlink($zipname); + } else { + for ($i = 0; $i < $instance->maxfiles; $i ++) { + $field = 'file' . $i; + if (!$firstsub && isset($fromform->{$field . 'action'}) && $fromform->{$field . 'action'} == 'keep') { + $filename = $fromform->{$field . 'name'}; + $files[$filename] = $prevsubfiles[$filename]; + } else { + if (isset($fromform->{$field . 'action'}) && $fromform->{$field . 'action'} == 'replace' + || !empty($fromform->{$field . 'rename'}) && !empty($fromform->{$field . 'name'})) { + $name = $fromform->{$field . 'name'}; + } else { + $name = $mform->get_new_filename( $field ); + } + $data = $mform->get_file_content( $field ); + if ($data !== false && $name !== false) { + // Autodetect text file encode if not binary. + if (! vpl_is_binary( $name )) { + $encode = mb_detect_encoding( $data, 'UNICODE, UTF-16, UTF-8, ISO-8859-1', true ); + if ($encode > '') { // If code detected. + $data = iconv( $encode, 'UTF-8', $data ); + } + $files[$name] = $data; + } else { + if (in_array($name . '.b64', $reqfiles)) { + $files[$name . '.b64'] = base64_encode($data); + } else { + $files[$name] = $data; + } + } } } - $files[$name] = $data; } } $errormessage = ''; @@ -102,15 +176,14 @@ // If evaluate on submission. if ($instance->evaluate && $instance->evaluateonsubmission) { - vpl_redirect( vpl_mod_href( 'forms/evaluation.php', 'id', $id, 'userid', $userid ), - get_string( 'saved', VPL )); + $redirecturl = vpl_mod_href( 'forms/evaluation.php', 'id', $id, 'userid', $userid ); + } else { + $redirecturl = vpl_mod_href( 'forms/submissionview.php', 'id', $id, 'userid', $userid ); } - vpl_redirect( vpl_mod_href( 'forms/submissionview.php', 'id', $id, 'userid', $userid ), - get_string( 'saved', VPL )); + vpl_redirect( $redirecturl, get_string( 'saved', VPL )); } else { - vpl_notice( get_string( 'notsaved', VPL ) ); vpl_redirect( vpl_mod_href( 'forms/submission.php', 'id', $id, 'userid', $userid ), - $errormessage); + get_string( 'notsaved', VPL ) . '
' . $errormessage, 'error'); } } diff --git a/forms/submission_form.php b/forms/submission_form.php index aee50cfe..4d14bfb2 100644 --- a/forms/submission_form.php +++ b/forms/submission_form.php @@ -31,17 +31,18 @@ class mod_vpl_submission_form extends moodleform { protected $vpl; + protected $userid; protected function getinternalform() { return $this->_form; } - public function __construct($page, $vpl) { + public function __construct($page, $vpl, $userid) { $this->vpl = $vpl; + $this->userid = $userid; parent::__construct( $page ); } protected function definition() { - global $CFG; + global $CFG, $OUTPUT, $PAGE; $mform = & $this->_form; - $mform->addElement( 'header', 'headersubmission', get_string( 'submission', VPL ) ); // Identification info. $mform->addElement( 'hidden', 'id' ); $mform->setType( 'id', PARAM_INT ); @@ -54,18 +55,97 @@ protected function definition() { ] ); $mform->setType( 'comments', PARAM_TEXT ); - // Files upload. + $submission = $this->vpl->last_user_submission( $this->userid ); + $firstsub = ($submission === false); $instance = $this->vpl->get_instance(); - $files = $this->vpl->get_required_files(); - $nfiles = count( $files ); - for ($i = 0; $i < $instance->maxfiles; $i ++) { + $reqfiles = $this->vpl->get_required_files(); + + $mform->addElement( 'select', 'submitmethod', get_string( 'submitmethod', VPL ), + [ 'archive' => get_string( 'archive', VPL ), 'files' => get_string( 'files' ) ] ); + $mform->setDefault( 'submitmethod', count($reqfiles) == 1 ? 'files' : 'archive' ); + + $mform->addElement( 'header', 'headersubmitarchive', get_string( 'submitarchive', VPL ) ); + + $filepickertitle = get_string( 'submitarchive', VPL ); + if (!$firstsub) { + $mform->addElement( 'radio', 'archiveaction', $filepickertitle, + get_string( 'archivereplacedelete', VPL ), 'replacedelete'); + $mform->addElement( 'radio', 'archiveaction', '', + get_string( 'archivereplace', VPL ), 'replace' ); + $mform->disabledIf( 'archiveaction', 'submitmethod', 'neq', 'archive' ); + $filepickertitle = null; + } + $mform->addElement( 'filepicker', 'archive', $filepickertitle, null, [ 'accepted_types' => '.zip' ] ); + $mform->disabledIf( 'archive', 'submitmethod', 'neq', 'archive' ); + + $mform->addElement( 'header', 'headersubmitfiles', get_string( 'submitfiles', VPL ) ); + + // Files upload. + $i = 0; + $requiredicon = $OUTPUT->pix_icon('requestedfiles', get_string('required'), 'mod_vpl', [ 'class' => 'text-info' ]); + foreach ($reqfiles as $reqfile) { $field = 'file' . $i; - if ($i < $nfiles) { - $mform->addElement( 'filepicker', $field, $files[$i] ); - } else { - $mform->addElement( 'filepicker', $field, get_string( 'anyfile', VPL ) ); + $filepickertitle = $requiredicon . $reqfile; + if (!$firstsub) { + $mform->addElement( 'radio', $field . 'action', $filepickertitle, + get_string( 'keepcurrentfile', VPL ), 'keep'); + $mform->addElement( 'radio', $field . 'action', '', + get_string( 'replacefile', VPL ), 'replace' ); + $mform->disabledIf( $field . 'action', 'submitmethod', 'neq', 'files' ); + $mform->addElement( 'hidden', $field . 'name', $reqfile ); + $mform->setType( $field . 'name', PARAM_RAW ); + $filepickertitle = null; + } + $mform->addElement( 'filepicker', $field, $filepickertitle ); + $mform->disabledIf( $field, 'submitmethod', 'neq', 'files' ); + if (!$firstsub) { + $mform->disabledIf( $field, $field . 'action', 'neq', 'replace' ); } + $i++; + } + if (!$firstsub) { + $subfiles = (new mod_vpl_submission( $this->vpl, $submission ))->get_submitted_fgm()->getFileList(); + foreach ($subfiles as $subfile) { + if (!in_array($subfile, $reqfiles)) { + $field = 'file' . $i; + $mform->addElement( 'radio', $field . 'action', $subfile, get_string( 'keepcurrentfile', VPL ), 'keep'); + $mform->addElement( 'radio', $field . 'action', '', get_string( 'deletefile', VPL ), 'delete' ); + $mform->addElement( 'radio', $field . 'action', '', get_string( 'replacefile', VPL ), 'replace' ); + $mform->disabledIf( $field . 'action', 'submitmethod', 'neq', 'files' ); + $mform->addElement( 'hidden', $field . 'name', $subfile ); + $mform->setType( $field . 'name', PARAM_PATH ); + $mform->addElement( 'filepicker', $field ); + $mform->disabledIf( $field, 'submitmethod', 'neq', 'files' ); + $mform->disabledIf( $field, $field . 'action', 'neq', 'replace' ); + $i++; + } + } + } + + while ($i < $instance->maxfiles) { + $field = 'file' . $i; + $mform->addElement( 'filepicker', $field, get_string( 'anyfile', VPL ) ); + $mform->disabledIf( $field, 'submitmethod', 'neq', 'files' ); + $mform->addGroup([ + $mform->createElement('advcheckbox', $field . 'rename', get_string('renameuploadedfile', VPL)), + $mform->createElement('text', $field . 'name', get_string('new_file_name', VPL), + [ 'size' => 32, 'placeholder' => get_string('new_file_name', VPL) ]), + ]); + $mform->setType( $field . 'name', PARAM_PATH ); + $mform->setDefault( $field . 'rename', 0 ); + $mform->disabledIf( $field . 'name', $field . 'rename' ); + $mform->disabledIf( $field . 'name', $field . 'name', 'neq', 'files' ); + $i++; } $this->add_action_buttons( true, get_string( 'submit' ) ); + + $PAGE->requires->js_call_amd('mod_vpl/submissionform', 'setup'); + } + public function set_data($data) { + for ($i = 0; $i < $this->vpl->get_instance()->maxfiles; $i++) { + $data->{'file'.$i.'action'} = 'keep'; + $data->{'archiveaction'} = 'replacedelete'; + } + parent::set_data($data); } } diff --git a/lang/en/vpl.php b/lang/en/vpl.php index a1034e91..2c6829a1 100644 --- a/lang/en/vpl.php +++ b/lang/en/vpl.php @@ -30,6 +30,9 @@ $string['allfiles'] = 'All files'; $string['allsubmissions'] = 'All submissions'; $string['anyfile'] = 'Any file'; +$string['archive'] = 'Archive'; +$string['archivereplace'] = 'Replace only files present in archive'; +$string['archivereplacedelete'] = 'Replace all files and delete files not present in archive'; $string['attemptnumber'] = 'Attempt number {$a}'; $string['autodetect'] = 'Autodetect'; $string['automaticevaluation'] = 'Automatic evaluation'; @@ -89,6 +92,7 @@ $string['delete_file_fq'] = "delete '{\$a}' file?"; $string['delete_file_q'] = 'Delete file?'; $string['deleteallsubmissions'] = 'Delete all submissions'; +$string['deletefile'] = 'Delete file'; $string['description'] = 'Description'; $string['diff'] = 'diff'; $string['directory_not_renamed'] = 'Directory \'{$a}\' has not been renamed'; @@ -175,6 +179,7 @@ $string['jail_servers_config'] = 'Execution servers config'; $string['jail_servers_description'] = 'Write a line for each server'; $string['joinedfiles'] = 'Joined selected files'; +$string['keepcurrentfile'] = 'Keep current file'; $string['keepfiles'] = 'Files to keep when running'; $string['keyboard'] = 'Keyboard'; $string['lasterror'] = 'Last error info'; @@ -294,7 +299,9 @@ $string['rename'] = 'Rename'; $string['rename_file'] = 'Rename file'; $string['rename_directory'] = 'Rename directory'; +$string['renameuploadedfile'] = 'Rename uploaded file'; $string['replace_find'] = 'Replace/Find'; +$string['replacefile'] = 'Replace contents'; $string['replacenewer'] = "A newer version was already saved.\nDo you want to replace the newer version with this one?"; $string['requestedfiles'] = 'Requested files'; $string['requirednet'] = 'Require network address'; @@ -347,6 +354,9 @@ $string['submissionselection'] = 'Submission selection'; $string['submissionslist'] = 'Submissions list'; $string['submissionview'] = 'Submission view'; +$string['submitarchive'] = 'Submit archive'; +$string['submitfiles'] = 'Submit files'; +$string['submitmethod'] = 'Submit method'; $string['submittedby'] = 'Submitted by {$a}'; $string['submittedon'] = 'Submitted on'; $string['submittedonp'] = 'Submitted on {$a}'; diff --git a/locallib.php b/locallib.php index c88ae1f9..be43df88 100644 --- a/locallib.php +++ b/locallib.php @@ -1065,7 +1065,7 @@ function vpl_get_overrides($vplid) { * @param array $parms Parameters to pass to the function * @return mixed Value returned by the function or throw exception */ -function vpl_call_with_lock(string $locktype, string $resource, string $function, array $parms) { +function vpl_call_with_lock(string $locktype, string $resource, string $function, array & $parms) { $lockfactory = \core\lock\lock_config::get_lock_factory($locktype); if ($lock = $lockfactory->get_lock($resource, VPL_LOCK_TIMEOUT)) { try { diff --git a/vpl.class.php b/vpl.class.php index a250742c..30b32afa 100644 --- a/vpl.class.php +++ b/vpl.class.php @@ -809,7 +809,6 @@ public function pass_submission_restriction(& $alldata, & $error) { public static function internal_add_submission($vpl, $userid, & $files, $comments, & $error) { global $USER, $DB; if (! $vpl->pass_submission_restriction( $files, $error )) { - $error = get_string('notavailable'); return false; } $group = false;