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