From 1449f6d4b7a39308aba5b7a98f9d796b6446e94e Mon Sep 17 00:00:00 2001 From: El Hassan Jamaly <el.jamaly@epfl.ch> Date: Thu, 26 Dec 2024 12:14:27 +0000 Subject: [PATCH] Download All Submissions --- .../api/v1/AssignmentController.java | 22 +++ .../autograde/service/MoodleWebService.java | 24 +++ .../autograde_download_all_submissions.php | 163 ++++++++++++++++++ .../db/services.php | 8 +- 4 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 moodle-assignsubmission-autograde/classes/external/autograde_download_all_submissions.php diff --git a/autograde-service/src/main/java/ch/epfl/autograde/controller/api/v1/AssignmentController.java b/autograde-service/src/main/java/ch/epfl/autograde/controller/api/v1/AssignmentController.java index 0e7a9f14..21e67475 100644 --- a/autograde-service/src/main/java/ch/epfl/autograde/controller/api/v1/AssignmentController.java +++ b/autograde-service/src/main/java/ch/epfl/autograde/controller/api/v1/AssignmentController.java @@ -7,6 +7,8 @@ import ch.epfl.autograde.service.MoodleWebService; import ch.epfl.autograde.service.AutogradeService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -115,4 +117,24 @@ public final class AssignmentController { return ResponseEntity.status(HttpStatus.OK).body(submissions); } + /** + * REST Endpoint to download all submissions linked to the assignment in a zipped file + * + * @param id The unique id of the requested assignment + * @since 1.1.0 + * @return + * <ul> + * <li>{@link HttpStatus.OK} if the request is successful</li> + * <li>{@link HttpStatus.NOT_FOUND} if the assignment is missing (TODO)</li> + * <li>{@link HttpStatus.INTERNAL_SERVER_ERROR} in case of a server failure</li> + * </ul> + */ + @GetMapping(value = "/{id}/submissions", produces = "application/zip") + public Resource download(final @PathVariable int id) { + log.info("Received request to download the submissions for assignment {}", id); + var files = moodle.download_all_submissions(id); + log.info("Successfully download the submissions for assignment {}", id); + return new InputStreamResource(files); + } + } diff --git a/autograde-service/src/main/java/ch/epfl/autograde/service/MoodleWebService.java b/autograde-service/src/main/java/ch/epfl/autograde/service/MoodleWebService.java index 6a4af996..c9dad823 100644 --- a/autograde-service/src/main/java/ch/epfl/autograde/service/MoodleWebService.java +++ b/autograde-service/src/main/java/ch/epfl/autograde/service/MoodleWebService.java @@ -155,6 +155,30 @@ public final class MoodleWebService { throw new RuntimeException(e); } } + + public InputStream download_all_submissions(int assignmentid) { + log.info("Downloading all submissions of assignment with id {} from Moodle ({})", assignmentid, properties.baseUrl()); + + final var FUNCTION_NAME = "mod_assignsubmission_autograde_download_all_submissions"; + + final var params = Map.of( + "assignmentid", assignmentid + ); + + try { + var response = call(FUNCTION_NAME, params, ofInputStream()); + + // HR : Check the http status + if(response.statusCode() != HttpStatus.OK.value()){ + log.error("call to the moodle-autograde api failed - download_all_submissions - id {}", assignmentid); + throw new IllegalStateException(); + } + // HR : Deserialize the request + return response.body(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } public List<Submission> list_submissions(int id) { log.info("Listing the submissions for assignment with id '{}' from Moodle", id); diff --git a/moodle-assignsubmission-autograde/classes/external/autograde_download_all_submissions.php b/moodle-assignsubmission-autograde/classes/external/autograde_download_all_submissions.php new file mode 100644 index 00000000..c7178293 --- /dev/null +++ b/moodle-assignsubmission-autograde/classes/external/autograde_download_all_submissions.php @@ -0,0 +1,163 @@ +<?php +// This file is part of Moodle - https://moodle.org/ +// +// 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. +// +// 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 Moodle. If not, see <https://www.gnu.org/licenses/>. +/** + * This file defines the function to download all submissions + * + * @see https://moodledev.io/docs/apis/subsystems/external/functions + * @author ??? + * @package ??? + * @copyright 2023 AUTOGRADE-EPFL <autograde-support@groupes.epfl.ch> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace assignsubmission_autograde\external; + +use context_course; +use external_api; +use external_function_parameters; +use external_value; +use moodle_exception; +use stdClass; +use function assignsubmission_autograde\utils\assignsubmission_autograde_get_assignment; + +require_once(__DIR__.'/../../lib.php'); +require_once(__DIR__.'/../../utils.php'); +require_once($CFG->libdir . '/externallib.php'); +require_once($CFG->dirroot . '/mod/assign/locallib.php'); + +class autograde_download_all_submissions extends external_api{ + + /** + * ??? + * @return external_function_parameters ??? + */ + public static function download_all_submissions_parameters(): external_function_parameters + { + return new external_function_parameters([ + 'assignmentid' => new external_value(PARAM_INT, 'The ID of the assignment'), + ]); + } + + /** + * @throws \dml_exception + * @throws \required_capability_exception + * @throws \moodle_exception + */ + public static function download_all_submissions($assignmentid){ + global $DB, $USER; + + $course_id = $DB->get_field('assign', 'course', array('id' => $assignmentid)); + + // HR : Check if the user has the necessary capabilities + require_capability('assignsubmission/autograde:exportsubmissions', + context_course::instance($course_id), $USER->id, false); + + // HR : Fetch the correct assignment instance + $assignment = assignsubmission_autograde_get_assignment($assignmentid); + + // Check if group submissions are enabled + $is_group_submission = $DB->get_field('assign', 'teamsubmission', ['id' => $assignmentid]); + + //Fetch the submissions from the DB + $submissions = $is_group_submission ? + $DB->get_records_select( + 'assign_submission', + 'assignment = :assignmentid AND groupid != 0', + ['assignmentid' => $assignmentid] + ): + $DB->get_records('assign_submission', ['assignment' => $assignmentid]) ; + + + if (empty($submissions)) { + throw new moodle_exception('nosubmissions', 'mod_assign'); + } + + + $files_to_zip = []; + foreach ($submissions as $submission){ + $group_name = ''; + if ($is_group_submission) { + // Handle group submissions + // Fetch group members + $group_members = $DB->get_records('groups_members', ['groupid' => $submission->groupid], '', 'userid'); + if (empty($group_members)) { + throw new moodle_exception('invalidgroup', 'mod_assign', '', 'Group has no members.'); + } + + // Concatenate idnumbers of group members to create the group name + $group_idnumbers = []; + foreach ($group_members as $member) { + $user = $DB->get_record('user', ['id' => $member->userid], 'idnumber', MUST_EXIST); + $group_idnumbers[] = $user->idnumber; + } + $group_name = implode('_', $group_idnumbers); // Example: "idnumber1_idnumber2" + } else { + // Handle individual submissions + if (!empty($submission->userid)) { + $user = $DB->get_record('user', ['id' => $submission->userid], 'idnumber', MUST_EXIST); + $group_name = $user->idnumber; + } else { + throw new moodle_exception('invalidsubmission', 'mod_assign', '', 'Submission has no valid user or group ID.'); + } + } + + // HR : Retrieve the files associated with the specified area and item + $submission_files = array(); + foreach ($assignment->get_submission_plugins() as $plugin){ + if($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) + $submission_files += $plugin->get_files($submission, new stdClass()); + } + + + foreach ($submission_files as $file) { + $filepath = $file->get_filepath(); + $filename = $file->get_filename(); + // We observed by looking in the $DB that the $filepath always starts and ends with + // '/'. If the filepath is empty, then it's the single character '/' + $files_to_zip[$group_name . '/' . $submission->attemptnumber . $filepath . $filename] = $file; + } + + } + + if (empty($files_to_zip)) { + throw new moodle_exception('nofiles', 'mod_assign'); + } + + //Prepare a temporary file for zipping + $tempdir = make_temp_directory('submission_files') ; + $zipfilename = "assignment_{$assignmentid}_submissions.zip" ; + $tempzipfile = "$tempdir/$zipfilename" ; + + //Use Moodle's packer to create a zip file + $packer = get_file_packer('application/zip') ; + + //Pack the files into the zip archive + if (!$packer->archive_to_pathname($files_to_zip, $tempzipfile)) { + throw new moodle_exception('couldnotcreatezip'); + } + + //Send the zip file as a response using Moodle's API + send_temp_file($tempzipfile, $zipfilename); + + + + } + + /** + * @return void + */ + public static function download_all_submissions_returns() { + } +} \ No newline at end of file diff --git a/moodle-assignsubmission-autograde/db/services.php b/moodle-assignsubmission-autograde/db/services.php index 4fa7b9b1..103aa3cf 100644 --- a/moodle-assignsubmission-autograde/db/services.php +++ b/moodle-assignsubmission-autograde/db/services.php @@ -51,7 +51,12 @@ $functions = [ 'methodname' => 'info_submissions', 'description' => 'List all of the information for a given submission', 'capabilities' => 'assignsubmission/autograde:viewsubmission' - ] + ], + 'mod_assignsubmission_autograde_download_all_submissions' => [ + 'classname' => 'assignsubmission_autograde\external\autograde_download_all_submissions' , + 'methodname' => 'download_all_submissions', + 'description' => 'Download all submissions of a given assignment', +] ]; $services = [ @@ -61,6 +66,7 @@ $services = [ 'mod_assignsubmission_autograde_download_submission', 'mod_assignsubmission_autograde_list_submissions', 'mod_assignsubmission_autograde_submission_info', + 'mod_assignsubmission_autograde_download_all_submissions', ], 'restrictedusers' => 1, 'enabled' => 1, -- GitLab