reportEntries) {
+ return exportExerciseWithSubmissions(exercise, optionsDTO, exportDir, exportErrors, reportEntries);
+ }
+}
diff --git a/src/main/java/de/tum/in/www1/artemis/service/ModelingSubmissionExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/ModelingSubmissionExportService.java
similarity index 89%
rename from src/main/java/de/tum/in/www1/artemis/service/ModelingSubmissionExportService.java
rename to src/main/java/de/tum/in/www1/artemis/service/export/ModelingSubmissionExportService.java
index f8732e884b21..7554240eba93 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/ModelingSubmissionExportService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/export/ModelingSubmissionExportService.java
@@ -1,4 +1,4 @@
-package de.tum.in.www1.artemis.service;
+package de.tum.in.www1.artemis.service.export;
import java.io.*;
import java.nio.charset.StandardCharsets;
@@ -9,6 +9,8 @@
import de.tum.in.www1.artemis.domain.Submission;
import de.tum.in.www1.artemis.domain.modeling.ModelingSubmission;
import de.tum.in.www1.artemis.repository.ExerciseRepository;
+import de.tum.in.www1.artemis.service.FileService;
+import de.tum.in.www1.artemis.service.ZipFileService;
@Service
public class ModelingSubmissionExportService extends SubmissionExportService {
diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/ProgrammingExerciseExportService.java
similarity index 76%
rename from src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseExportService.java
rename to src/main/java/de/tum/in/www1/artemis/service/export/ProgrammingExerciseExportService.java
index 6cdadd39c01d..29bafc2e0296 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseExportService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/export/ProgrammingExerciseExportService.java
@@ -1,4 +1,4 @@
-package de.tum.in.www1.artemis.service.programming;
+package de.tum.in.www1.artemis.service.export;
import static de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationService.RepositoryCheckoutPath;
import static de.tum.in.www1.artemis.service.util.XmlFileUtils.getDocumentBuilderFactory;
@@ -10,12 +10,15 @@
import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.function.Predicate;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
@@ -45,8 +48,6 @@
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
import de.tum.in.www1.artemis.domain.*;
import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage;
import de.tum.in.www1.artemis.domain.enumeration.RepositoryType;
@@ -57,15 +58,17 @@
import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository;
import de.tum.in.www1.artemis.repository.StudentParticipationRepository;
import de.tum.in.www1.artemis.service.ExerciseDateService;
-import de.tum.in.www1.artemis.service.FilePathService;
import de.tum.in.www1.artemis.service.FileService;
import de.tum.in.www1.artemis.service.ZipFileService;
import de.tum.in.www1.artemis.service.archival.ArchivalReportEntry;
import de.tum.in.www1.artemis.service.connectors.GitService;
import de.tum.in.www1.artemis.web.rest.dto.RepositoryExportOptionsDTO;
+/**
+ * Service for exporting programming exercises.
+ */
@Service
-public class ProgrammingExerciseExportService {
+public class ProgrammingExerciseExportService extends ExerciseWithSubmissionsExportService {
private final Logger log = LoggerFactory.getLogger(ProgrammingExerciseExportService.class);
@@ -79,8 +82,6 @@ public class ProgrammingExerciseExportService {
private final AuxiliaryRepositoryRepository auxiliaryRepositoryRepository;
- private final ObjectMapper objectMapper;
-
private final FileService fileService;
private final GitService gitService;
@@ -91,18 +92,13 @@ public class ProgrammingExerciseExportService {
public static final String EXPORTED_EXERCISE_PROBLEM_STATEMENT_FILE_PREFIX = "Problem-Statement";
- private static final String EMBEDDED_FILE_MARKDOWN_SYNTAX_REGEX = "\\[.*] *\\(/api/files/markdown/.*\\)";
-
- private static final String EMBEDDED_FILE_HTML_SYNTAX_REGEX = "";
-
- private static final String API_MARKDOWN_FILE_PATH = "/api/files/markdown/";
-
public ProgrammingExerciseExportService(ProgrammingExerciseRepository programmingExerciseRepository, StudentParticipationRepository studentParticipationRepository,
FileService fileService, GitService gitService, ZipFileService zipFileService, MappingJackson2HttpMessageConverter springMvcJacksonConverter,
AuxiliaryRepositoryRepository auxiliaryRepositoryRepository) {
+ // Programming exercises do not have a submission export service
+ super(fileService, springMvcJacksonConverter, null);
this.programmingExerciseRepository = programmingExerciseRepository;
this.studentParticipationRepository = studentParticipationRepository;
- this.objectMapper = springMvcJacksonConverter.getObjectMapper();
this.fileService = fileService;
this.gitService = gitService;
this.zipFileService = zipFileService;
@@ -111,180 +107,90 @@ public ProgrammingExerciseExportService(ProgrammingExerciseRepository programmin
/**
* Export programming exercise material for instructors including instructor repositories, problem statement (.md) and exercise detail (.json).
+ *
+ * Optionally, student repositories can be included as well.
*
- * @param exercise the programming exercise
- * @param exportErrors List of failures that occurred during the export
+ * @param exercise the programming exercise
+ * @param exportErrors List of failures that occurred during the export
+ * @param includeStudentRepos flag that indicates whether the student repos should also be exported
+ * @param shouldZipZipFiles flag that indicates whether the zip files should be zipped again (this is necessary for the import to work)
+ * @param exportDir the directory used to store the zip file
+ * @param archivalReportEntries List of all exercises and their statistics
* @return the path to the zip file
*/
- public Path exportProgrammingExerciseInstructorMaterial(ProgrammingExercise exercise, List exportErrors) throws IOException {
- // Create export directory for programming exercises
- if (!Files.exists(repoDownloadClonePath)) {
- Files.createDirectories(repoDownloadClonePath);
+ private Path exportProgrammingExerciseMaterialWithStudentReposOptional(ProgrammingExercise exercise, List exportErrors, boolean includeStudentRepos,
+ boolean shouldZipZipFiles, Optional exportDir, List archivalReportEntries, List pathsToBeZipped) throws IOException {
+ if (exportDir.isEmpty()) {
+ // Create export directory for programming exercises
+ if (!Files.exists(repoDownloadClonePath)) {
+ Files.createDirectories(repoDownloadClonePath);
+ }
+ exportDir = Optional.of(fileService.getTemporaryUniquePathWithoutPathCreation(repoDownloadClonePath, 5));
}
- Path exportDir = fileService.getTemporaryUniquePath(repoDownloadClonePath, 5);
-
- // List to add paths of files that should be contained in the zip folder of exported programming exercise:
- // i.e., problem statement, exercise details, instructor repositories
- var pathsToBeZipped = new ArrayList();
// Add the exported zip folder containing template, solution, and tests repositories
- // Ignore report data
- pathsToBeZipped.add(exportProgrammingExerciseRepositories(exercise, false, exportDir, exportErrors, new ArrayList<>()));
-
- // Add problem statement as .md file if it is not null
- if (exercise.getProblemStatement() != null) {
- exportProblemStatementAndEmbeddedFiles(exercise, exportErrors, exportDir, pathsToBeZipped);
+ // wrap this in a try catch block to prevent the problem statement and exercise details not being exported if the repositories fail to export
+ try {
+ var repoExportsPaths = exportProgrammingExerciseRepositories(exercise, includeStudentRepos, shouldZipZipFiles, repoDownloadClonePath, exportDir.orElseThrow(),
+ exportErrors, archivalReportEntries);
+ repoExportsPaths.forEach(path -> {
+ if (path != null) {
+ pathsToBeZipped.add(path);
+ }
+ });
}
+ catch (Exception e) {
+ exportErrors.add("Failed to export programming exercise repositories: " + e.getMessage());
- // Add programming exercise details (object) as .json file
- var exerciseDetailsFileExtension = ".json";
- String exerciseDetailsFileName = EXPORTED_EXERCISE_DETAILS_FILE_PREFIX + "-" + exercise.getTitle() + exerciseDetailsFileExtension;
- String cleanExerciseDetailsFileName = FileService.sanitizeFilename(exerciseDetailsFileName);
- var exerciseDetailsExportPath = exportDir.resolve(cleanExerciseDetailsFileName);
- pathsToBeZipped.add(fileService.writeObjectToJsonFile(exercise, this.objectMapper, exerciseDetailsExportPath));
-
- // Setup path to store the zip file for the exported programming exercise
- var timestamp = ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-Hmss"));
- String exportedExerciseZipFileName = "Material-" + exercise.getCourseViaExerciseGroupOrCourseMember().getShortName() + "-" + exercise.getTitle() + "-" + exercise.getId()
- + "-" + timestamp + ".zip";
- String cleanFilename = FileService.sanitizeFilename(exportedExerciseZipFileName);
- Path pathToZippedExercise = exportDir.resolve(cleanFilename);
-
- // Create the zip folder of the exported programming exercise and return the path to the created folder
- zipFileService.createTemporaryZipFile(pathToZippedExercise, pathsToBeZipped, 5);
- return pathToZippedExercise;
- }
-
- /**
- * Export problem statement and embedded files for a given programming exercise.
- *
- * @param exercise the programming exercise that is exported
- * @param exportErrors List of failures that occurred during the export
- * @param exportDir the directory where the content of the export is stored
- * @param pathsToBeZipped the paths that should be included in the zip file
- */
- private void exportProblemStatementAndEmbeddedFiles(ProgrammingExercise exercise, List exportErrors, Path exportDir, List pathsToBeZipped) {
- var problemStatementFileExtension = ".md";
- String problemStatementFileName = EXPORTED_EXERCISE_PROBLEM_STATEMENT_FILE_PREFIX + "-" + exercise.getTitle() + problemStatementFileExtension;
- String cleanProblemStatementFileName = FileService.sanitizeFilename(problemStatementFileName);
- var problemStatementExportPath = Path.of(exportDir.toString(), cleanProblemStatementFileName);
- pathsToBeZipped.add(fileService.writeStringToFile(exercise.getProblemStatement(), problemStatementExportPath));
- copyEmbeddedFiles(exercise, exportDir, pathsToBeZipped, exportErrors);
- }
-
- /**
- * In case the problem statement contains embedded files, they need to be part of the zip, so they can be imported again.
- *
- * @param exercise the programming exercise that is exported
- * @param outputDir the directory where the content of the export is stored
- * @param pathsToBeZipped the paths that should be included in the zip file
- */
- private void copyEmbeddedFiles(ProgrammingExercise exercise, Path outputDir, List pathsToBeZipped, List exportErrors) {
- Set embeddedFilesWithMarkdownSyntax = new HashSet<>();
- Set embeddedFilesWithHtmlSyntax = new HashSet<>();
-
- Matcher matcherForMarkdownSyntax = Pattern.compile(EMBEDDED_FILE_MARKDOWN_SYNTAX_REGEX).matcher(exercise.getProblemStatement());
- Matcher matcherForHtmlSyntax = Pattern.compile(EMBEDDED_FILE_HTML_SYNTAX_REGEX).matcher(exercise.getProblemStatement());
- checkForMatchesInProblemStatementAndCreateDirectoryForFiles(outputDir, pathsToBeZipped, exportErrors, embeddedFilesWithMarkdownSyntax, matcherForMarkdownSyntax);
- Path embeddedFilesDir = checkForMatchesInProblemStatementAndCreateDirectoryForFiles(outputDir, pathsToBeZipped, exportErrors, embeddedFilesWithHtmlSyntax,
- matcherForHtmlSyntax);
- // if the returned path is null the directory could not be created
- if (embeddedFilesDir == null) {
- return;
- }
- copyFilesEmbeddedWithMarkdownSyntax(exercise, exportErrors, embeddedFilesWithMarkdownSyntax, embeddedFilesDir);
- copyFilesEmbeddedWithHtmlSyntax(exercise, exportErrors, embeddedFilesWithHtmlSyntax, embeddedFilesDir);
-
- }
-
- /**
- * Copies the files that are embedded with Markdown syntax to the embedded files' directory.
- *
- * @param exercise the programming exercise that is exported
- * @param exportErrors List of failures that occurred during the export
- * @param embeddedFilesWithMarkdownSyntax the files that are embedded with Markdown syntax
- * @param embeddedFilesDir the directory where the embedded files are stored
- */
- private void copyFilesEmbeddedWithMarkdownSyntax(ProgrammingExercise exercise, List exportErrors, Set embeddedFilesWithMarkdownSyntax, Path embeddedFilesDir) {
- for (String embeddedFile : embeddedFilesWithMarkdownSyntax) {
- // avoid matching other closing ] or () in the squared brackets by getting the index of the last ]
- String lastPartOfMatchedString = embeddedFile.substring(embeddedFile.lastIndexOf("]") + 1);
- String filePath = lastPartOfMatchedString.substring(lastPartOfMatchedString.indexOf("(") + 1, lastPartOfMatchedString.indexOf(")"));
- constructFilenameAndCopyFile(exercise, exportErrors, embeddedFilesDir, filePath);
}
- }
+ // Add problem statement as .md file
+ super.exportProblemStatementAndEmbeddedFilesAndExerciseDetails(exercise, exportErrors, exportDir.orElseThrow(), pathsToBeZipped);
- /**
- * Copies the files that are embedded with html syntax to the embedded files' directory.
- *
- * @param exercise the programming exercise that is exported
- * @param exportErrors List of failures that occurred during the export
- * @param embeddedFilesWithHtmlSyntax the files that are embedded with html syntax
- * @param embeddedFilesDir the directory where the embedded files are stored
- */
- private void copyFilesEmbeddedWithHtmlSyntax(ProgrammingExercise exercise, List exportErrors, Set embeddedFilesWithHtmlSyntax, Path embeddedFilesDir) {
- for (String embeddedFile : embeddedFilesWithHtmlSyntax) {
- int indexOfFirstQuotationMark = embeddedFile.indexOf('"');
- String filePath = embeddedFile.substring(embeddedFile.indexOf("src=") + 5, embeddedFile.indexOf('"', indexOfFirstQuotationMark + 1));
- constructFilenameAndCopyFile(exercise, exportErrors, embeddedFilesDir, filePath);
- }
+ return exportDir.orElseThrow();
}
/**
- * Extracts the filename from the matched string and copies the file to the embedded files' directory.
+ * Exports a programming exercise for archival purposes. This includes the instructor repositories, the student repositories, the problem statement, and the exercise details.
*
- * @param exercise the programming exercise that is exported
- * @param exportErrors List of failures that occurred during the export
- * @param embeddedFilesDir the directory where the embedded files are stored
- * @param filePath the path of the file that should be copied
+ * @param exercise the programming exercise
+ * @param exportErrors List of failures that occurred during the export
+ * @param exportDir the directory used to store the exported exercise
+ * @param archivalReportEntries List of all exercises and their statistics
+ * @return the path to the exported exercise
*/
- private void constructFilenameAndCopyFile(ProgrammingExercise exercise, List exportErrors, Path embeddedFilesDir, String filePath) {
- String fileName = filePath.replace(API_MARKDOWN_FILE_PATH, "");
- Path imageFilePath = Path.of(FilePathService.getMarkdownFilePath(), fileName);
- Path imageExportPath = embeddedFilesDir.resolve(fileName);
- // we need this check as it might be that the matched string is different and not filtered out above but the file is already copied
- if (!Files.exists(imageExportPath)) {
- try {
- Files.copy(imageFilePath, imageExportPath);
- }
- catch (IOException e) {
- exportErrors.add("Failed to copy embedded files: " + e.getMessage());
- log.warn("Could not copy embedded file {} for exercise with id {}", fileName, exercise.getId());
- }
+ public Optional exportProgrammingExerciseForArchival(ProgrammingExercise exercise, List exportErrors, Optional exportDir,
+ List archivalReportEntries) {
+ try {
+ return Optional.of(exportProgrammingExerciseMaterialWithStudentReposOptional(exercise, exportErrors, true, false, exportDir, archivalReportEntries, new ArrayList<>()));
+ }
+ catch (IOException e) {
+ // this should actually never happen because all operations that throw an IOException are not executed when calling the method with an exportDir
+ log.error("Failed to export programming exercise for archival: {}", e.getMessage());
+ exportErrors.add("Failed to export programming exercise for archival: " + e.getMessage());
}
+ return Optional.empty();
}
/**
- * Checks for matches in the problem statement and creates a directory for the embedded files.
+ * Exports a programming exercise for download purposes. This includes the instructor repositories, the problem statement, and the exercise details.
*
- * @param outputDir the directory where the content of the export is stored
- * @param pathsToBeZipped the paths that should be included in the zip file
- * @param exportErrors List of failures that occurred during the export
- * @param embeddedFiles the files that are embedded in the problem statement
- * @param matcher the matcher that is used to find the embedded files
- * @return the path to the embedded files directory or null if the directory could not be created
+ * @param exercise the programming exercise to export
+ * @param exportErrors List of failures that occurred during the export
+ * @return the path to the exported exercise
+ * @throws IOException if an error occurs while accessing the file system
*/
- private Path checkForMatchesInProblemStatementAndCreateDirectoryForFiles(Path outputDir, List pathsToBeZipped, List exportErrors, Set embeddedFiles,
- Matcher matcher) {
- while (matcher.find()) {
- embeddedFiles.add(matcher.group());
- }
- log.debug("Found embedded files: {} ", embeddedFiles);
- Path embeddedFilesDir = outputDir.resolve("files");
- if (!embeddedFiles.isEmpty()) {
- if (!Files.exists(embeddedFilesDir)) {
- try {
- Files.createDirectory(embeddedFilesDir);
- }
- catch (IOException e) {
- exportErrors.add("Could not create directory for embedded files: " + e.getMessage());
- log.warn("Could not create directory for embedded files. Won't include embedded files: " + e.getMessage());
- return null;
- }
- }
- pathsToBeZipped.add(embeddedFilesDir);
- }
- return embeddedFilesDir;
+ public Path exportProgrammingExerciseForDownload(ProgrammingExercise exercise, List exportErrors) throws IOException {
+ List pathsToBeZipped = new ArrayList<>();
+ var exportDir = exportProgrammingExerciseMaterialWithStudentReposOptional(exercise, exportErrors, false, true, Optional.empty(), new ArrayList<>(), pathsToBeZipped);
+ // Setup path to store the zip file for the exported programming exercise
+ var timestamp = ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-Hmss"));
+ String exportedExerciseZipFileName = "Material-" + exercise.getCourseViaExerciseGroupOrCourseMember().getShortName() + "-" + exercise.getTitle() + "-" + exercise.getId()
+ + "-" + timestamp + ".zip";
+ String cleanFilename = FileService.sanitizeFilename(exportedExerciseZipFileName);
+ Path pathToZippedExercise = exportDir.resolve(cleanFilename);
+ // Create the zip folder of the exported programming exercise and return the path to the created folder
+ zipFileService.createTemporaryZipFile(pathToZippedExercise, pathsToBeZipped, 5);
+ return pathToZippedExercise;
}
/**
@@ -295,13 +201,15 @@ private Path checkForMatchesInProblemStatementAndCreateDirectoryForFiles(Path ou
*
* @param exercise the programming exercise
* @param includingStudentRepos flag for including the students repos as well
+ * @param shouldZipZipFiles flag for zipping the zip files again
+ * @param workingDir the directory used to clone the repository
* @param outputDir the path to a directory that will be used to store the zipped programming exercise.
* @param exportErrors List of failures that occurred during the export
* @param reportData List of all exercises and their statistics
- * @return the path to the zip file
+ * @return a list of paths to one or more zip files
*/
- public Path exportProgrammingExerciseRepositories(ProgrammingExercise exercise, Boolean includingStudentRepos, Path outputDir, List exportErrors,
- List reportData) {
+ public List exportProgrammingExerciseRepositories(ProgrammingExercise exercise, boolean includingStudentRepos, boolean shouldZipZipFiles, Path workingDir, Path outputDir,
+ List exportErrors, List reportData) {
log.info("Exporting programming exercise {} with title {}", exercise.getId(), exercise.getTitle());
// List to add paths of files that should be contained in the zip folder of exported programming exercise repositories:
// i.e., student repositories (if `includingStudentRepos` is true), instructor repositories template, solution and tests
@@ -316,21 +224,22 @@ public Path exportProgrammingExerciseRepositories(ProgrammingExercise exercise,
exportOptions.setExportAllParticipants(true);
// Export student repositories and add them to list
- var exportedStudentRepositoryFiles = exportStudentRepositories(exercise, studentParticipations, exportOptions, outputDir, outputDir, exportErrors).stream()
+ var exportedStudentRepositoryFiles = exportStudentRepositories(exercise, studentParticipations, exportOptions, workingDir, outputDir, exportErrors).stream()
.filter(Objects::nonNull).toList();
pathsToBeZipped.addAll(exportedStudentRepositoryFiles);
}
// Export the template, solution, and tests repositories and add them to list
- pathsToBeZipped.add(exportInstructorRepositoryForExercise(exercise.getId(), RepositoryType.TEMPLATE, outputDir, exportErrors).map(File::toPath).orElse(null));
- pathsToBeZipped.add(exportInstructorRepositoryForExercise(exercise.getId(), RepositoryType.SOLUTION, outputDir, exportErrors).map(File::toPath).orElse(null));
- pathsToBeZipped.add(exportInstructorRepositoryForExercise(exercise.getId(), RepositoryType.TESTS, outputDir, exportErrors).map(File::toPath).orElse(null));
+ pathsToBeZipped.add(exportInstructorRepositoryForExercise(exercise.getId(), RepositoryType.TEMPLATE, workingDir, outputDir, exportErrors).map(File::toPath).orElse(null));
+ pathsToBeZipped.add(exportInstructorRepositoryForExercise(exercise.getId(), RepositoryType.SOLUTION, workingDir, outputDir, exportErrors).map(File::toPath).orElse(null));
+ pathsToBeZipped.add(exportInstructorRepositoryForExercise(exercise.getId(), RepositoryType.TESTS, workingDir, outputDir, exportErrors).map(File::toPath).orElse(null));
List auxiliaryRepositories = auxiliaryRepositoryRepository.findByExerciseId(exercise.getId());
// Export the auxiliary repositories and add them to list
auxiliaryRepositories.forEach(auxiliaryRepository -> {
- pathsToBeZipped.add(exportInstructorAuxiliaryRepositoryForExercise(exercise.getId(), auxiliaryRepository, outputDir, exportErrors).map(File::toPath).orElse(null));
+ pathsToBeZipped
+ .add(exportInstructorAuxiliaryRepositoryForExercise(exercise.getId(), auxiliaryRepository, workingDir, outputDir, exportErrors).map(File::toPath).orElse(null));
});
// Setup path to store the zip file for the exported repositories
@@ -356,8 +265,14 @@ public Path exportProgrammingExerciseRepositories(ProgrammingExercise exercise,
}
// Create the zip folder of the exported programming exercise and return the path to the created folder
- zipFileService.createZipFile(pathToZippedExercise, includedFilePathsNotNull);
- return pathToZippedExercise;
+ if (shouldZipZipFiles) {
+ zipFileService.createZipFile(pathToZippedExercise, includedFilePathsNotNull);
+ return List.of(pathToZippedExercise);
+ }
+ else {
+ return includedFilePathsNotNull;
+ }
+
}
catch (Exception e) {
var error = "Failed to export programming exercise because the zip file " + pathToZippedExercise + " could not be created: " + e.getMessage();
@@ -379,8 +294,8 @@ public Path exportProgrammingExerciseRepositories(ProgrammingExercise exercise,
* @return a zipped file
*/
public Optional exportInstructorRepositoryForExercise(long exerciseId, RepositoryType repositoryType, List exportErrors) {
- Path outputDir = fileService.getTemporaryUniquePath(repoDownloadClonePath, 5);
- return exportInstructorRepositoryForExercise(exerciseId, repositoryType, outputDir, exportErrors);
+ Path outputDir = fileService.getTemporaryUniquePathWithoutPathCreation(repoDownloadClonePath, 5);
+ return exportInstructorRepositoryForExercise(exerciseId, repositoryType, outputDir, outputDir, exportErrors);
}
/**
@@ -394,7 +309,7 @@ public Optional exportInstructorRepositoryForExercise(long exerciseId, Rep
* @return a zipped file
*/
public Optional exportStudentRequestedRepository(long exerciseId, boolean includeTests, List exportErrors) {
- Path uniquePath = fileService.getTemporaryUniquePath(repoDownloadClonePath, 5);
+ Path uniquePath = fileService.getTemporaryUniquePathWithoutPathCreation(repoDownloadClonePath, 5);
return exportStudentRequestedRepository(exerciseId, includeTests, uniquePath, exportErrors);
}
@@ -409,8 +324,8 @@ public Optional exportStudentRequestedRepository(long exerciseId, boolean
* @return a zipped file
*/
public Optional exportInstructorAuxiliaryRepositoryForExercise(long exerciseId, AuxiliaryRepository auxiliaryRepository, List exportErrors) {
- Path outputDir = fileService.getTemporaryUniquePath(repoDownloadClonePath, 5);
- return exportInstructorAuxiliaryRepositoryForExercise(exerciseId, auxiliaryRepository, outputDir, exportErrors);
+ Path outputDir = fileService.getTemporaryUniquePathWithoutPathCreation(repoDownloadClonePath, 5);
+ return exportInstructorAuxiliaryRepositoryForExercise(exerciseId, auxiliaryRepository, outputDir, outputDir, exportErrors);
}
/**
@@ -419,11 +334,12 @@ public Optional exportInstructorAuxiliaryRepositoryForExercise(long exerci
*
* @param exerciseId The id of the programming exercise that has the repository
* @param repositoryType The type of repository to export
+ * @param workingDir The directory used to clone the repository
* @param outputDir The directory used for store the zip file
* @param exportErrors List of failures that occurred during the export
* @return a zipped file
*/
- public Optional exportInstructorRepositoryForExercise(long exerciseId, RepositoryType repositoryType, Path outputDir, List exportErrors) {
+ public Optional exportInstructorRepositoryForExercise(long exerciseId, RepositoryType repositoryType, Path workingDir, Path outputDir, List exportErrors) {
var exerciseOrEmpty = loadExerciseForRepoExport(exerciseId, exportErrors);
if (exerciseOrEmpty.isEmpty()) {
return Optional.empty();
@@ -431,7 +347,7 @@ public Optional exportInstructorRepositoryForExercise(long exerciseId, Rep
var exercise = exerciseOrEmpty.get();
String zippedRepoName = getZippedRepoName(exercise, repositoryType.getName());
var repositoryUrl = exercise.getRepositoryURL(repositoryType);
- return exportRepository(repositoryUrl, repositoryType.getName(), zippedRepoName, exercise, outputDir, null, exportErrors);
+ return exportRepository(repositoryUrl, repositoryType.getName(), zippedRepoName, exercise, workingDir, outputDir, null, exportErrors);
}
/**
@@ -439,11 +355,13 @@ public Optional exportInstructorRepositoryForExercise(long exerciseId, Rep
*
* @param exerciseId The id of the programming exercise that has the repository
* @param auxiliaryRepository the auxiliary repository to export
+ * @param workingDir The directory used to clone the repository
* @param outputDir The directory used for storing the zip file
* @param exportErrors List of failures that occurred during the export
* @return the zipped file containing the auxiliary repository
*/
- public Optional exportInstructorAuxiliaryRepositoryForExercise(long exerciseId, AuxiliaryRepository auxiliaryRepository, Path outputDir, List exportErrors) {
+ public Optional exportInstructorAuxiliaryRepositoryForExercise(long exerciseId, AuxiliaryRepository auxiliaryRepository, Path workingDir, Path outputDir,
+ List exportErrors) {
var exerciseOrEmpty = loadExerciseForRepoExport(exerciseId, exportErrors);
if (exerciseOrEmpty.isEmpty()) {
return Optional.empty();
@@ -451,7 +369,7 @@ public Optional exportInstructorAuxiliaryRepositoryForExercise(long exerci
var exercise = exerciseOrEmpty.get();
String zippedRepoName = getZippedRepoName(exercise, auxiliaryRepository.getRepositoryName());
var repositoryUrl = auxiliaryRepository.getVcsRepositoryUrl();
- return exportRepository(repositoryUrl, auxiliaryRepository.getName(), zippedRepoName, exercise, outputDir, null, exportErrors);
+ return exportRepository(repositoryUrl, auxiliaryRepository.getName(), zippedRepoName, exercise, workingDir, outputDir, null, exportErrors);
}
/**
@@ -479,7 +397,7 @@ public Optional exportStudentRequestedRepository(long exerciseId, boolean
}
else {
var repositoryUrl = exercise.getRepositoryURL(repositoryType);
- return exportRepository(repositoryUrl, repositoryType.getName(), zippedRepoName, exercise, uniquePath, gitDirFilter, exportErrors);
+ return exportRepository(repositoryUrl, repositoryType.getName(), zippedRepoName, exercise, uniquePath, uniquePath, gitDirFilter, exportErrors);
}
}
@@ -503,8 +421,18 @@ private String getZippedRepoName(ProgrammingExercise exercise, String repository
return FileService.sanitizeFilename(courseShortName + "-" + exercise.getTitle() + "-" + repositoryName);
}
- private Optional exportRepository(VcsRepositoryUrl repositoryUrl, String repositoryName, String zippedRepoName, ProgrammingExercise exercise, Path outputDir,
- @Nullable Predicate contentFilter, List exportErrors) {
+ /**
+ * Exports a given repository and stores it in a zip file.
+ *
+ * @param repositoryUrl the url of the repository
+ * @param zippedRepoName the name of the zip file
+ * @param workingDir the directory used to clone the repository
+ * @param outputDir the directory used for store the zip file
+ * @param contentFilter a filter for the content of the zip file
+ * @return an optional containing the path to the zip file if the export was successful
+ */
+ private Optional exportRepository(VcsRepositoryUrl repositoryUrl, String repositoryName, String zippedRepoName, ProgrammingExercise exercise, Path workingDir,
+ Path outputDir, @Nullable Predicate contentFilter, List exportErrors) {
try {
// It's not guaranteed that the repository url is defined (old courses).
if (repositoryUrl == null) {
@@ -514,7 +442,7 @@ private Optional exportRepository(VcsRepositoryUrl repositoryUrl, String r
return Optional.empty();
}
- Path zippedRepo = createZipForRepository(repositoryUrl, zippedRepoName, outputDir, contentFilter);
+ Path zippedRepo = createZipForRepository(repositoryUrl, zippedRepoName, workingDir, outputDir, contentFilter);
if (zippedRepo != null) {
return Optional.of(zippedRepo.toFile());
}
@@ -578,7 +506,7 @@ public File exportStudentRepositoriesToZipFile(long programmingExerciseId, @NotN
ProgrammingExercise programmingExercise = programmingExerciseRepository.findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesById(programmingExerciseId)
.orElseThrow();
- Path outputDir = fileService.getTemporaryUniquePath(repoDownloadClonePath, 10);
+ Path outputDir = fileService.getTemporaryUniquePathWithoutPathCreation(repoDownloadClonePath, 10);
var zippedRepos = exportStudentRepositories(programmingExercise, participations, repositoryExportOptions, outputDir, outputDir, new ArrayList<>());
try {
@@ -650,13 +578,13 @@ public List exportStudentRepositories(ProgrammingExercise programmingExerc
* @throws IOException if the zip file couldn't be created
* @throws GitAPIException if the repo couldn't get checked out
*/
- private Path createZipForRepository(VcsRepositoryUrl repositoryUrl, String zipFilename, Path outputDir, @Nullable Predicate contentFilter)
+ private Path createZipForRepository(VcsRepositoryUrl repositoryUrl, String zipFilename, Path workingDir, Path outputDir, @Nullable Predicate contentFilter)
throws IOException, GitAPIException, GitException, UncheckedIOException {
- var repositoryDir = fileService.getTemporaryUniquePath(outputDir, 5);
+ var repositoryDir = fileService.getTemporaryUniquePathWithoutPathCreation(workingDir, 5);
Path localRepoPath;
// Checkout the repository
- try (Repository repository = gitService.getOrCheckoutRepository(repositoryUrl, repositoryDir, true)) {
+ try (Repository repository = gitService.getOrCheckoutRepository(repositoryUrl, repositoryDir, false)) {
gitService.resetToOriginHead(repository);
localRepoPath = repository.getLocalPath();
}
@@ -713,8 +641,9 @@ private Path createZipForRepositoryWithParticipation(final ProgrammingExercise p
}
try {
+ var repositoryDir = fileService.getTemporaryUniquePathWithoutPathCreation(workingDir, 5);
// Checkout the repository
- Repository repository = gitService.getOrCheckoutRepository(participation, workingDir.toString());
+ Repository repository = gitService.getOrCheckoutRepository(participation, repositoryDir.toString());
if (repository == null) {
log.warn("Cannot checkout repository for participation id: {}", participation.getId());
return null;
diff --git a/src/main/java/de/tum/in/www1/artemis/service/SubmissionExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/SubmissionExportService.java
similarity index 78%
rename from src/main/java/de/tum/in/www1/artemis/service/SubmissionExportService.java
rename to src/main/java/de/tum/in/www1/artemis/service/export/SubmissionExportService.java
index 6d30ed6d3da7..9bf450a76ccf 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/SubmissionExportService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/export/SubmissionExportService.java
@@ -1,11 +1,15 @@
-package de.tum.in.www1.artemis.service;
+package de.tum.in.www1.artemis.service.export;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
@@ -22,6 +26,9 @@
import de.tum.in.www1.artemis.domain.Submission;
import de.tum.in.www1.artemis.domain.participation.StudentParticipation;
import de.tum.in.www1.artemis.repository.ExerciseRepository;
+import de.tum.in.www1.artemis.service.ExerciseDateService;
+import de.tum.in.www1.artemis.service.FileService;
+import de.tum.in.www1.artemis.service.ZipFileService;
import de.tum.in.www1.artemis.service.archival.ArchivalReportEntry;
import de.tum.in.www1.artemis.web.rest.dto.SubmissionExportOptionsDTO;
import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException;
@@ -58,8 +65,12 @@ public SubmissionExportService(ExerciseRepository exerciseRepository, ZipFileSer
* @return the zipped file with the exported submissions
*/
public File exportStudentSubmissionsElseThrow(Long exerciseId, SubmissionExportOptionsDTO submissionExportOptions) {
- return exportStudentSubmissions(exerciseId, submissionExportOptions)
- .orElseThrow(() -> new BadRequestAlertException("Failed to export student submissions.", "SubmissionExport", "noSubmissions"));
+ var zippedSubmissionsPaths = exportStudentSubmissions(exerciseId, submissionExportOptions);
+ if (zippedSubmissionsPaths.isEmpty()) {
+ throw new BadRequestAlertException("Failed to export student submissions.", "SubmissionExport", "noSubmissions");
+ }
+ return zippedSubmissionsPaths.get(0).toFile();
+
}
/**
@@ -70,38 +81,32 @@ public File exportStudentSubmissionsElseThrow(Long exerciseId, SubmissionExportO
* @param submissionExportOptions the options for the export
* @return the zipped file with the exported submissions
*/
- public Optional exportStudentSubmissions(Long exerciseId, SubmissionExportOptionsDTO submissionExportOptions) {
+ public List exportStudentSubmissions(Long exerciseId, SubmissionExportOptionsDTO submissionExportOptions) {
Path outputDir = fileService.getTemporaryUniquePath(submissionExportPath, EXPORTED_SUBMISSIONS_DELETION_DELAY_IN_MINUTES);
- try {
- return exportStudentSubmissions(exerciseId, submissionExportOptions, outputDir, new ArrayList<>(), new ArrayList<>());
- }
- catch (IOException e) {
- log.error("Failed to export student submissions for exercise {} to {}: {}", exerciseId, outputDir, e);
- return Optional.empty();
- }
+ return exportStudentSubmissions(exerciseId, submissionExportOptions, true, outputDir, new ArrayList<>(), new ArrayList<>());
}
/**
* Exports student submissions to a zip file for an exercise.
- *
+ *
* The outputDir is used to store the zip file and temporary files used for zipping so make
* sure to delete it if it's no longer used.
*
* @param exerciseId the id of the exercise to be exported
* @param submissionExportOptions the options for the export
+ * @param zipSubmissions true, if the submissions should be zipped
* @param outputDir directory to store the temporary files in
* @param exportErrors a list of errors for submissions that couldn't be exported and are not included in the file
* @param reportData a list of all exercises and their statistics
- * @return a reference to the zipped file
- * @throws IOException if an error occurred while zipping
+ * @return paths of the exported submissions
*/
- public Optional exportStudentSubmissions(Long exerciseId, SubmissionExportOptionsDTO submissionExportOptions, Path outputDir, List exportErrors,
- List reportData) throws IOException {
+ public List exportStudentSubmissions(Long exerciseId, SubmissionExportOptionsDTO submissionExportOptions, boolean zipSubmissions, Path outputDir,
+ List exportErrors, List reportData) {
Optional exerciseOpt = exerciseRepository.findWithEagerStudentParticipationsStudentAndSubmissionsById(exerciseId);
if (exerciseOpt.isEmpty()) {
- return Optional.empty();
+ return List.of();
}
Exercise exercise = exerciseOpt.get();
@@ -133,12 +138,13 @@ public Optional exportStudentSubmissions(Long exerciseId, SubmissionExport
// Sort the student participations by id
exportedStudentParticipations.sort(Comparator.comparing(DomainObject::getId));
- return createZipFileFromParticipations(exercise, exportedStudentParticipations, enableFilterAfterDueDate, filterLateSubmissionsDate, outputDir, exportErrors, reportData);
+ return exportSubmissionsFromParticipationsOptionallyZipped(exercise, exportedStudentParticipations, enableFilterAfterDueDate, filterLateSubmissionsDate, zipSubmissions,
+ outputDir, exportErrors, reportData);
}
/**
* Creates a zip file from a list of participations for an exercise.
- *
+ *
* The outputDir is used to store the zip file and temporary files used for zipping so make
* sure to delete it if it's no longer used.
*
@@ -149,11 +155,10 @@ public Optional exportStudentSubmissions(Long exerciseId, SubmissionExport
* @param outputDir directory to store the temporary files in
* @param exportErrors a list of errors for submissions that couldn't be exported and are not included in the file
* @param reportData a list of all exercises and their statistics
- * @return the zipped file
- * @throws IOException if an error occurred while zipping
+ * @return paths of the exported submissions
*/
- private Optional createZipFileFromParticipations(Exercise exercise, List participations, boolean enableFilterAfterDueDate,
- @Nullable ZonedDateTime lateSubmissionFilter, Path outputDir, List exportErrors, List reportData) throws IOException {
+ private List exportSubmissionsFromParticipationsOptionallyZipped(Exercise exercise, List participations, boolean enableFilterAfterDueDate,
+ @Nullable ZonedDateTime lateSubmissionFilter, boolean zipSubmissions, Path outputDir, List exportErrors, List reportData) {
Course course = exercise.getCourseViaExerciseGroupOrCourseMember();
@@ -163,15 +168,7 @@ private Optional createZipFileFromParticipations(Exercise exercise, List createZipFileFromParticipations(Exercise exercise, List createZipFileFromParticipations(Exercise exercise, List exportErrors,
+ List reportEntries) {
+ return exportExerciseWithSubmissions(exercise, optionsDTO, exportDir, exportErrors, reportEntries);
+ }
+}
diff --git a/src/main/java/de/tum/in/www1/artemis/service/TextSubmissionExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/TextSubmissionExportService.java
similarity index 93%
rename from src/main/java/de/tum/in/www1/artemis/service/TextSubmissionExportService.java
rename to src/main/java/de/tum/in/www1/artemis/service/export/TextSubmissionExportService.java
index aa24de287191..3a7da9a013a9 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/TextSubmissionExportService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/export/TextSubmissionExportService.java
@@ -1,4 +1,4 @@
-package de.tum.in.www1.artemis.service;
+package de.tum.in.www1.artemis.service.export;
import java.io.*;
import java.nio.charset.StandardCharsets;
@@ -9,6 +9,8 @@
import de.tum.in.www1.artemis.domain.Submission;
import de.tum.in.www1.artemis.domain.TextSubmission;
import de.tum.in.www1.artemis.repository.ExerciseRepository;
+import de.tum.in.www1.artemis.service.FileService;
+import de.tum.in.www1.artemis.service.ZipFileService;
@Service
public class TextSubmissionExportService extends SubmissionExportService {
diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java
index 42303cc9b6c6..ca47d0ef6f4e 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java
@@ -39,8 +39,8 @@
import de.tum.in.www1.artemis.service.FileService;
import de.tum.in.www1.artemis.service.UrlService;
import de.tum.in.www1.artemis.service.connectors.GitService;
+import de.tum.in.www1.artemis.service.export.ProgrammingExerciseExportService;
import de.tum.in.www1.artemis.service.plagiarism.cache.PlagiarismCacheService;
-import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseExportService;
import de.tum.in.www1.artemis.service.util.TimeLogUtil;
import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException;
diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/TextPlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/TextPlagiarismDetectionService.java
index 8b8e56241f68..c0e141e604dd 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/TextPlagiarismDetectionService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/TextPlagiarismDetectionService.java
@@ -25,7 +25,7 @@
import de.tum.in.www1.artemis.domain.participation.Participation;
import de.tum.in.www1.artemis.domain.participation.StudentParticipation;
import de.tum.in.www1.artemis.domain.plagiarism.text.TextPlagiarismResult;
-import de.tum.in.www1.artemis.service.TextSubmissionExportService;
+import de.tum.in.www1.artemis.service.export.TextSubmissionExportService;
import de.tum.in.www1.artemis.service.plagiarism.cache.PlagiarismCacheService;
import de.tum.in.www1.artemis.service.util.TimeLogUtil;
import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException;
diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/DataExportScheduleService.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/DataExportScheduleService.java
index 8278387c1efe..cb7346fda827 100644
--- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/DataExportScheduleService.java
+++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/DataExportScheduleService.java
@@ -15,8 +15,8 @@
import de.tum.in.www1.artemis.repository.DataExportRepository;
import de.tum.in.www1.artemis.security.SecurityUtils;
import de.tum.in.www1.artemis.service.ProfileService;
-import de.tum.in.www1.artemis.service.dataexport.DataExportCreationService;
-import de.tum.in.www1.artemis.service.dataexport.DataExportService;
+import de.tum.in.www1.artemis.service.export.DataExportCreationService;
+import de.tum.in.www1.artemis.service.export.DataExportService;
import de.tum.in.www1.artemis.service.notifications.MailService;
import de.tum.in.www1.artemis.service.user.UserService;
diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/DataExportResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/DataExportResource.java
index 366a915199ef..73f9d1ae0e58 100644
--- a/src/main/java/de/tum/in/www1/artemis/web/rest/DataExportResource.java
+++ b/src/main/java/de/tum/in/www1/artemis/web/rest/DataExportResource.java
@@ -18,7 +18,7 @@
import de.tum.in.www1.artemis.repository.DataExportRepository;
import de.tum.in.www1.artemis.repository.UserRepository;
import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent;
-import de.tum.in.www1.artemis.service.dataexport.DataExportService;
+import de.tum.in.www1.artemis.service.export.DataExportService;
import de.tum.in.www1.artemis.web.rest.dto.DataExportDTO;
import de.tum.in.www1.artemis.web.rest.dto.RequestDataExportDTO;
import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException;
diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/FileUploadExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/FileUploadExerciseResource.java
index 88f19fc188a2..c24560c7488a 100644
--- a/src/main/java/de/tum/in/www1/artemis/web/rest/FileUploadExerciseResource.java
+++ b/src/main/java/de/tum/in/www1/artemis/web/rest/FileUploadExerciseResource.java
@@ -24,6 +24,7 @@
import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor;
import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor;
import de.tum.in.www1.artemis.service.*;
+import de.tum.in.www1.artemis.service.export.FileUploadSubmissionExportService;
import de.tum.in.www1.artemis.service.feature.Feature;
import de.tum.in.www1.artemis.service.feature.FeatureToggle;
import de.tum.in.www1.artemis.service.metis.conversation.ChannelService;
diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java
index 5f262e3fa6e8..dfcf760984a9 100644
--- a/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java
+++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java
@@ -29,6 +29,7 @@
import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor;
import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor;
import de.tum.in.www1.artemis.service.*;
+import de.tum.in.www1.artemis.service.export.SubmissionExportService;
import de.tum.in.www1.artemis.service.feature.Feature;
import de.tum.in.www1.artemis.service.feature.FeatureToggle;
import de.tum.in.www1.artemis.service.metis.conversation.ChannelService;
diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseExportImportResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseExportImportResource.java
index 689bb01ba706..ed0df229a1f5 100644
--- a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseExportImportResource.java
+++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseExportImportResource.java
@@ -43,6 +43,7 @@
import de.tum.in.www1.artemis.service.CourseService;
import de.tum.in.www1.artemis.service.SubmissionPolicyService;
import de.tum.in.www1.artemis.service.exam.ExamAccessService;
+import de.tum.in.www1.artemis.service.export.ProgrammingExerciseExportService;
import de.tum.in.www1.artemis.service.feature.Feature;
import de.tum.in.www1.artemis.service.feature.FeatureToggle;
import de.tum.in.www1.artemis.service.programming.*;
@@ -267,7 +268,7 @@ public ResponseEntity exportInstructorExercise(@PathVariable long exer
long start = System.nanoTime();
Path path;
try {
- path = programmingExerciseExportService.exportProgrammingExerciseInstructorMaterial(programmingExercise, Collections.synchronizedList(new ArrayList<>()));
+ path = programmingExerciseExportService.exportProgrammingExerciseForDownload(programmingExercise, Collections.synchronizedList(new ArrayList<>()));
}
catch (Exception e) {
log.error("Error while exporting programming exercise with id " + exerciseId + " for instructor", e);
diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java
index 6361d050658d..2d4f41229288 100644
--- a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java
+++ b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java
@@ -23,6 +23,7 @@
import de.tum.in.www1.artemis.security.Role;
import de.tum.in.www1.artemis.security.annotations.*;
import de.tum.in.www1.artemis.service.*;
+import de.tum.in.www1.artemis.service.export.TextSubmissionExportService;
import de.tum.in.www1.artemis.service.feature.Feature;
import de.tum.in.www1.artemis.service.feature.FeatureToggle;
import de.tum.in.www1.artemis.service.messaging.InstanceMessageSendService;
diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminDataExportResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminDataExportResource.java
index 50aced0c8a8e..4644dadb20d2 100644
--- a/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminDataExportResource.java
+++ b/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminDataExportResource.java
@@ -3,7 +3,7 @@
import org.springframework.web.bind.annotation.*;
import de.tum.in.www1.artemis.security.annotations.EnforceAdmin;
-import de.tum.in.www1.artemis.service.dataexport.DataExportService;
+import de.tum.in.www1.artemis.service.export.DataExportService;
import de.tum.in.www1.artemis.web.rest.dto.RequestDataExportDTO;
/**
diff --git a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java
index ed857f5a52ee..8e547f2b3c99 100644
--- a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java
+++ b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java
@@ -20,7 +20,6 @@
import javax.validation.constraints.NotNull;
import org.assertj.core.data.Offset;
-import org.mockito.ArgumentMatchers;
import org.mockito.MockedStatic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -72,6 +71,7 @@
import de.tum.in.www1.artemis.service.dto.StudentDTO;
import de.tum.in.www1.artemis.service.dto.UserDTO;
import de.tum.in.www1.artemis.service.dto.UserPublicInfoDTO;
+import de.tum.in.www1.artemis.service.export.CourseExamExportService;
import de.tum.in.www1.artemis.service.notifications.GroupNotificationService;
import de.tum.in.www1.artemis.team.TeamUtilService;
import de.tum.in.www1.artemis.user.UserFactory;
@@ -87,7 +87,7 @@
public class CourseTestService {
@Value("${artemis.course-archives-path}")
- private String courseArchivesDirPath;
+ private Path courseArchivesDirPath;
@Autowired
private CourseRepository courseRepo;
@@ -128,9 +128,6 @@ public class CourseTestService {
@Autowired
private CourseExamExportService courseExamExportService;
- @Autowired
- private ZipFileService zipFileService;
-
@Autowired
private FileService fileService;
@@ -2078,27 +2075,12 @@ public void testArchiveCourseWithTestModelingAndFileUploadExercisesFailToExportF
}
private void archiveCourseAndAssertExerciseDoesntExist(Course course, Exercise exercise) throws Exception {
- Files.createDirectories(Path.of(courseArchivesDirPath));
-
- String zipGroupName = course.getShortName() + "-" + exercise.getTitle() + "-" + exercise.getId();
- String cleanZipGroupName = FileService.sanitizeFilename(zipGroupName);
- doThrow(new IOException("IOException")).when(zipFileService).createZipFile(ArgumentMatchers.argThat(argument -> argument.toString().contains(cleanZipGroupName)), anyList(),
- any(Path.class));
-
+ Files.createDirectories(courseArchivesDirPath);
List files = archiveCourseAndExtractFiles(course);
- assertThat(files).hasSize(4);
-
- String exerciseType = "";
- if (exercise instanceof FileUploadExercise) {
- exerciseType = "FileUpload";
- }
- else if (exercise instanceof ModelingExercise) {
- exerciseType = "Modeling";
- }
- else if (exercise instanceof TextExercise) {
- exerciseType = "Text";
- }
- assertThat(files).doesNotContain(Path.of(exerciseType + "-" + userPrefix + "student1"));
+ // report.csv, errors.text, one submission per exercise, one problem statement per exercise and no exercise details.json per exercise
+ // because of the thrown exception. --> 2+3+3=8
+ assertThat(files).hasSize(8);
+ assertThat(files).doesNotContain(Path.of("Exercise-Details-" + exercise.getTitle() + ".json"));
}
private List archiveCourseAndExtractFiles(Course course) throws IOException {
diff --git a/src/test/java/de/tum/in/www1/artemis/dataexport/DataExportResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/dataexport/DataExportResourceIntegrationTest.java
index 23be5b9f4bba..f9a7599e5eff 100644
--- a/src/test/java/de/tum/in/www1/artemis/dataexport/DataExportResourceIntegrationTest.java
+++ b/src/test/java/de/tum/in/www1/artemis/dataexport/DataExportResourceIntegrationTest.java
@@ -27,7 +27,7 @@
import de.tum.in.www1.artemis.domain.DataExport;
import de.tum.in.www1.artemis.domain.enumeration.DataExportState;
import de.tum.in.www1.artemis.repository.DataExportRepository;
-import de.tum.in.www1.artemis.service.dataexport.DataExportService;
+import de.tum.in.www1.artemis.service.export.DataExportService;
import de.tum.in.www1.artemis.user.UserUtilService;
import de.tum.in.www1.artemis.web.rest.dto.DataExportDTO;
import de.tum.in.www1.artemis.web.rest.dto.RequestDataExportDTO;
diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java
index d183ef4bef66..ef5cf3a90efd 100644
--- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java
+++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java
@@ -1,9 +1,12 @@
package de.tum.in.www1.artemis.exercise.programmingexercise;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.*;
+import java.io.IOException;
+import java.nio.file.Path;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
@@ -16,6 +19,8 @@
import org.springframework.http.HttpStatus;
import org.springframework.security.test.context.support.WithMockUser;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest;
import de.tum.in.www1.artemis.course.CourseFactory;
import de.tum.in.www1.artemis.course.CourseTestService;
@@ -671,19 +676,26 @@ void testArchiveCourseWithTestModelingAndFileUploadExercises() throws Exception
@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void testArchiveCourseWithTestModelingAndFileUploadExercisesFailToExportModelingExercise() throws Exception {
+ doThrow(new IOException("Error")).when(fileService).writeObjectToJsonFile(any(), any(ObjectMapper.class), any(Path.class));
courseTestService.testArchiveCourseWithTestModelingAndFileUploadExercisesFailToExportModelingExercise();
+ verify(fileService).scheduleForDirectoryDeletion(any(Path.class), anyLong());
}
@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void testArchiveCourseWithTestModelingAndFileUploadExercisesFailToExportFileUploadExercise() throws Exception {
+ doThrow(new IOException("Error")).when(fileService).writeObjectToJsonFile(any(), any(ObjectMapper.class), any(Path.class));
courseTestService.testArchiveCourseWithTestModelingAndFileUploadExercisesFailToExportFileUploadExercise();
+ // the temp directory should be deleted
+ verify(fileService).scheduleForDirectoryDeletion(any(Path.class), anyLong());
}
@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void testArchiveCourseWithTestModelingAndFileUploadExercisesFailToExportTextExercise() throws Exception {
+ doThrow(new IOException("Error")).when(fileService).writeObjectToJsonFile(any(), any(ObjectMapper.class), any(Path.class));
courseTestService.testArchiveCourseWithTestModelingAndFileUploadExercisesFailToExportTextExercise();
+ verify(fileService).scheduleForDirectoryDeletion(any(Path.class), anyLong());
}
@Test
diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseBitbucketBambooIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseBitbucketBambooIntegrationTest.java
index 999217984e5b..711baa4c219a 100644
--- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseBitbucketBambooIntegrationTest.java
+++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseBitbucketBambooIntegrationTest.java
@@ -449,7 +449,7 @@ void testExportProgrammingExerciseInstructorMaterial_embeddedFilesDontExist() th
@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void testExportProgrammingExerciseInstructorMaterial_failToExportRepository() throws Exception {
- doThrow(GitException.class).when(fileService).getTemporaryUniquePath(any(Path.class), anyLong());
+ doThrow(GitException.class).when(fileService).getTemporaryUniquePathWithoutPathCreation(any(Path.class), anyLong());
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial(HttpStatus.INTERNAL_SERVER_ERROR, false, true, true);
}
diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestService.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestService.java
index 64a93b2726b6..3217a31da02f 100644
--- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestService.java
+++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestService.java
@@ -3,8 +3,8 @@
import static de.tum.in.www1.artemis.domain.enumeration.ExerciseMode.INDIVIDUAL;
import static de.tum.in.www1.artemis.domain.enumeration.ExerciseMode.TEAM;
import static de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage.*;
-import static de.tum.in.www1.artemis.service.programming.ProgrammingExerciseExportService.EXPORTED_EXERCISE_DETAILS_FILE_PREFIX;
-import static de.tum.in.www1.artemis.service.programming.ProgrammingExerciseExportService.EXPORTED_EXERCISE_PROBLEM_STATEMENT_FILE_PREFIX;
+import static de.tum.in.www1.artemis.service.export.ProgrammingExerciseExportService.EXPORTED_EXERCISE_DETAILS_FILE_PREFIX;
+import static de.tum.in.www1.artemis.service.export.ProgrammingExerciseExportService.EXPORTED_EXERCISE_PROBLEM_STATEMENT_FILE_PREFIX;
import static de.tum.in.www1.artemis.web.rest.ProgrammingExerciseResourceEndpoints.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -72,15 +72,13 @@
import de.tum.in.www1.artemis.repository.*;
import de.tum.in.www1.artemis.repository.hestia.ProgrammingExerciseTaskRepository;
import de.tum.in.www1.artemis.security.Role;
-import de.tum.in.www1.artemis.service.CourseExamExportService;
-import de.tum.in.www1.artemis.service.FilePathService;
-import de.tum.in.www1.artemis.service.ParticipationService;
-import de.tum.in.www1.artemis.service.UrlService;
+import de.tum.in.www1.artemis.service.*;
import de.tum.in.www1.artemis.service.connectors.GitService;
import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationService;
import de.tum.in.www1.artemis.service.connectors.gitlab.GitLabException;
import de.tum.in.www1.artemis.service.connectors.vcs.VersionControlRepositoryPermission;
import de.tum.in.www1.artemis.service.connectors.vcs.VersionControlService;
+import de.tum.in.www1.artemis.service.export.CourseExamExportService;
import de.tum.in.www1.artemis.service.programming.JavaTemplateUpgradeService;
import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeature;
import de.tum.in.www1.artemis.service.scheduled.AutomaticProgrammingExerciseCleanupService;
@@ -154,7 +152,7 @@ public class ProgrammingExerciseTestService {
private AutomaticProgrammingExerciseCleanupService automaticProgrammingExerciseCleanupService;
@Value("${artemis.course-archives-path}")
- private String courseArchivesDirPath;
+ private Path courseArchivesDirPath;
@Autowired
private CourseExamExportService courseExamExportService;
@@ -1531,6 +1529,7 @@ void testArchiveCourseWithProgrammingExercise() throws Exception {
exercise = programmingExerciseRepository.save(exercise);
exercise = programmingExerciseUtilService.addTemplateParticipationForProgrammingExercise(exercise);
exercise = programmingExerciseUtilService.addSolutionParticipationForProgrammingExercise(exercise);
+ exercise.setProblemStatement("Lorem Ipsum");
programmingExerciseUtilService.addTestCasesToProgrammingExercise(exercise);
// Add student participation
@@ -1566,7 +1565,17 @@ void testArchiveCourseWithProgrammingExercise() throws Exception {
var updatedCourse = courseRepository.findByIdElseThrow(course.getId());
assertThat(updatedCourse.getCourseArchivePath()).isNotEmpty();
-
+ // extract archive content and check that all expected files exist.
+ Path courseArchivePath = courseArchivesDirPath.resolve(updatedCourse.getCourseArchivePath());
+ zipFileTestUtilService.extractZipFileRecursively(courseArchivePath.toString());
+ String extractedArchiveDir = updatedCourse.getCourseArchivePath().substring(0, updatedCourse.getCourseArchivePath().length() - 4);
+ try (var files = Files.walk(courseArchivesDirPath.resolve(extractedArchiveDir))) {
+ assertThat(files).map(Path::getFileName).anyMatch((filename) -> filename.toString().matches(".*-exercise.zip"))
+ .anyMatch((filename) -> filename.toString().matches(".*-solution.zip")).anyMatch((filename) -> filename.toString().matches(".*-tests.zip"))
+ .anyMatch((filename) -> filename.toString().matches(EXPORTED_EXERCISE_PROBLEM_STATEMENT_FILE_PREFIX + ".*.md"))
+ .anyMatch((filename) -> filename.toString().matches(EXPORTED_EXERCISE_DETAILS_FILE_PREFIX + ".*.json"))
+ .anyMatch((filename) -> filename.toString().matches(".*student1.zip"));
+ }
}
// Test
diff --git a/src/test/java/de/tum/in/www1/artemis/participation/SubmissionExportIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/participation/SubmissionExportIntegrationTest.java
index d0aa44b0efe9..c765b30921fb 100644
--- a/src/test/java/de/tum/in/www1/artemis/participation/SubmissionExportIntegrationTest.java
+++ b/src/test/java/de/tum/in/www1/artemis/participation/SubmissionExportIntegrationTest.java
@@ -7,7 +7,6 @@
import java.io.IOException;
import java.nio.file.Path;
import java.time.ZonedDateTime;
-import java.util.regex.Pattern;
import java.util.zip.ZipFile;
import org.junit.jupiter.api.AfterEach;
@@ -140,9 +139,7 @@ else if (exercise instanceof FileUploadExercise) {
private void saveEmptySubmissionFile(Exercise exercise, FileUploadSubmission submission) throws IOException {
- String[] parts = submission.getFilePath().split(Pattern.quote(File.separator));
- String fileName = parts[parts.length - 1];
- File file = Path.of(FileUploadSubmission.buildFilePath(exercise.getId(), submission.getId()), fileName).toFile();
+ File file = Path.of(FileUploadSubmission.buildFilePath(exercise.getId(), submission.getId()), submission.getFilePath()).toFile();
File parent = file.getParentFile();
if (!parent.exists() && !parent.mkdirs()) {
@@ -242,14 +239,14 @@ void testExportAll() throws Exception {
@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void testExportAll_IOException() throws Exception {
- doThrow(IOException.class).when(zipFileService).createZipFile(any(), any(), any());
+ doThrow(IOException.class).when(zipFileService).createZipFile(any(), any());
request.postWithResponseBodyFile("/api/file-upload-exercises/" + fileUploadExercise.getId() + "/export-submissions", baseExportOptions, HttpStatus.BAD_REQUEST);
}
@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void testExportTextExerciseSubmission_IOException() throws Exception {
- doThrow(IOException.class).when(zipFileService).createZipFile(any(), any(), any());
+ doThrow(IOException.class).when(zipFileService).createZipFile(any(), any());
request.postWithResponseBodyFile("/api/text-exercises/" + textExercise.getId() + "/export-submissions", baseExportOptions, HttpStatus.BAD_REQUEST);
}
diff --git a/src/test/java/de/tum/in/www1/artemis/service/DataExportCreationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/DataExportCreationServiceTest.java
index b911774ec828..d5ddd2c30e30 100644
--- a/src/test/java/de/tum/in/www1/artemis/service/DataExportCreationServiceTest.java
+++ b/src/test/java/de/tum/in/www1/artemis/service/DataExportCreationServiceTest.java
@@ -47,7 +47,7 @@
import de.tum.in.www1.artemis.repository.metis.AnswerPostRepository;
import de.tum.in.www1.artemis.repository.metis.PostRepository;
import de.tum.in.www1.artemis.service.connectors.apollon.ApollonConversionService;
-import de.tum.in.www1.artemis.service.dataexport.DataExportCreationService;
+import de.tum.in.www1.artemis.service.export.DataExportCreationService;
import de.tum.in.www1.artemis.user.UserUtilService;
import de.tum.in.www1.artemis.util.FileUtils;
import de.tum.in.www1.artemis.util.ZipFileTestUtilService;
From 8d51aec9e807681cca93602a7739944da1e21971 Mon Sep 17 00:00:00 2001
From: Patrick Bassner
Date: Sun, 17 Sep 2023 15:40:07 +0200
Subject: [PATCH 15/16] Development: Allow short variable names in DTOs
---
ruleset.xml | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/ruleset.xml b/ruleset.xml
index 502e1450195c..caa96b49676c 100644
--- a/ruleset.xml
+++ b/ruleset.xml
@@ -131,7 +131,10 @@ http://pmd.sourceforge.net/ruleset/2.0.0 ">
-
+
+ // It's okay to use short variable names in DTOs, e.g. "id" or "name"
+ .*/de/tum/in/www1/artemis/web/rest/dto/.*
+
From d2913ae314589b65934a08e0183bc23ef69d81db Mon Sep 17 00:00:00 2001
From: Tobias Lippert <84102468+tobias-lippert@users.noreply.github.com>
Date: Sun, 17 Sep 2023 23:39:16 +0200
Subject: [PATCH 16/16] General: Improve archive downloading (#7215)
---
.../www1/artemis/web/rest/CourseResource.java | 11 +++++-----
.../www1/artemis/web/rest/ExamResource.java | 11 +++++-----
.../manage/course-management.service.ts | 8 +++----
.../exam/manage/exam-management.service.ts | 8 +++----
.../course-exam-archive-button.component.ts | 18 +++-------------
.../artemis/course/CourseTestService.java | 4 +++-
.../artemis/exam/ExamIntegrationTest.java | 5 ++++-
.../www1/artemis/util/RequestUtilService.java | 21 ++++++++++++-------
.../course/course-management.service.spec.ts | 14 +++++--------
.../manage/exam-management.service.spec.ts | 16 +++-----------
...urse-exam-archive-button.component.spec.ts | 10 ++++-----
11 files changed, 53 insertions(+), 73 deletions(-)
diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java
index 193ec41ed027..53c7e4229abf 100644
--- a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java
+++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java
@@ -21,10 +21,7 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
+import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
@@ -761,7 +758,11 @@ public ResponseEntity downloadCourseArchive(@PathVariable Long courseI
File zipFile = archive.toFile();
InputStreamResource resource = new InputStreamResource(new FileInputStream(zipFile));
- return ResponseEntity.ok().contentLength(zipFile.length()).contentType(MediaType.APPLICATION_OCTET_STREAM).header("filename", zipFile.getName()).body(resource);
+ ContentDisposition contentDisposition = ContentDisposition.builder("attachment").filename(zipFile.getName()).build();
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentDisposition(contentDisposition);
+ return ResponseEntity.ok().headers(headers).contentLength(zipFile.length()).contentType(MediaType.APPLICATION_OCTET_STREAM).header("filename", zipFile.getName())
+ .body(resource);
}
/**
diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java
index 9f42c7385907..d3cd5fedc05b 100644
--- a/src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java
+++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java
@@ -24,10 +24,7 @@
import org.springframework.core.io.Resource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
+import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
@@ -1150,8 +1147,12 @@ public ResponseEntity downloadExamArchive(@PathVariable Long courseId,
Path archive = Path.of(examArchivesDirPath, exam.getExamArchivePath());
File zipFile = archive.toFile();
+ ContentDisposition contentDisposition = ContentDisposition.builder("attachment").filename(zipFile.getName()).build();
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentDisposition(contentDisposition);
InputStreamResource resource = new InputStreamResource(new FileInputStream(zipFile));
- return ResponseEntity.ok().contentLength(zipFile.length()).contentType(MediaType.APPLICATION_OCTET_STREAM).header("filename", zipFile.getName()).body(resource);
+ return ResponseEntity.ok().headers(headers).contentLength(zipFile.length()).contentType(MediaType.APPLICATION_OCTET_STREAM).header("filename", zipFile.getName())
+ .body(resource);
}
/**
diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts
index 0639d98c2711..a95f1a978c77 100644
--- a/src/main/webapp/app/course/manage/course-management.service.ts
+++ b/src/main/webapp/app/course/manage/course-management.service.ts
@@ -417,11 +417,9 @@ export class CourseManagementService {
* if the archive does not exist.
* @param courseId The id of the course
*/
- downloadCourseArchive(courseId: number): Observable> {
- return this.http.get(`${this.resourceUrl}/${courseId}/download-archive`, {
- observe: 'response',
- responseType: 'blob',
- });
+ downloadCourseArchive(courseId: number): void {
+ const url = `${this.resourceUrl}/${courseId}/download-archive`;
+ window.open(url, '_blank');
}
/**
diff --git a/src/main/webapp/app/exam/manage/exam-management.service.ts b/src/main/webapp/app/exam/manage/exam-management.service.ts
index fd05d95cb618..a145093dc20f 100644
--- a/src/main/webapp/app/exam/manage/exam-management.service.ts
+++ b/src/main/webapp/app/exam/manage/exam-management.service.ts
@@ -468,11 +468,9 @@ export class ExamManagementService {
* @param courseId
* @param examId The id of the exam
*/
- downloadExamArchive(courseId: number, examId: number): Observable> {
- return this.http.get(`${this.resourceUrl}/${courseId}/exams/${examId}/download-archive`, {
- observe: 'response',
- responseType: 'blob',
- });
+ downloadExamArchive(courseId: number, examId: number): void {
+ const url = `${this.resourceUrl}/${courseId}/exams/${examId}/download-archive`;
+ window.open(url, '_blank');
}
/**
diff --git a/src/main/webapp/app/shared/components/course-exam-archive-button/course-exam-archive-button.component.ts b/src/main/webapp/app/shared/components/course-exam-archive-button/course-exam-archive-button.component.ts
index f24bc13c9a73..02aecadc1187 100644
--- a/src/main/webapp/app/shared/components/course-exam-archive-button/course-exam-archive-button.component.ts
+++ b/src/main/webapp/app/shared/components/course-exam-archive-button/course-exam-archive-button.component.ts
@@ -1,4 +1,4 @@
-import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { Component, Input, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { AlertService } from 'app/core/util/alert.service';
import { CourseManagementService } from 'app/course/manage/course-management.service';
import { JhiWebsocketService } from 'app/core/websocket/websocket.service';
@@ -9,7 +9,6 @@ import { ExamManagementService } from 'app/exam/manage/exam-management.service';
import { Course } from 'app/entities/course.model';
import { Exam } from 'app/entities/exam.model';
import dayjs from 'dayjs/esm';
-import { downloadZipFileFromResponse } from 'app/shared/util/download.util';
import { ButtonSize } from '../button.component';
import { ActionType } from 'app/shared/delete-dialog/delete-dialog.model';
import { Subject } from 'rxjs';
@@ -70,7 +69,6 @@ export class CourseExamArchiveButtonComponent implements OnInit, OnDestroy {
private translateService: TranslateService,
private modalService: NgbModal,
private accountService: AccountService,
- private changeDetectionRef: ChangeDetectorRef,
) {}
ngOnInit() {
@@ -127,13 +125,11 @@ export class CourseExamArchiveButtonComponent implements OnInit, OnDestroy {
if (this.archiveMode === 'Exam' && this.exam) {
this.examService.find(this.course.id!, this.exam.id!).subscribe((res) => {
this.exam = res.body!;
- this.changeDetectionRef.detectChanges();
this.displayDownloadArchiveButton = this.canDownloadArchive();
});
} else {
this.courseService.find(this.course.id!).subscribe((res) => {
this.course = res.body!;
- this.changeDetectionRef.detectChanges();
this.displayDownloadArchiveButton = this.canDownloadArchive();
});
}
@@ -181,8 +177,6 @@ export class CourseExamArchiveButtonComponent implements OnInit, OnDestroy {
}
if (result === 'archive' || !this.canDownloadArchive()) {
this.archive();
- } else {
- this.reloadCourseOrExam();
}
},
() => {},
@@ -211,15 +205,9 @@ export class CourseExamArchiveButtonComponent implements OnInit, OnDestroy {
downloadArchive() {
if (this.archiveMode === 'Exam' && this.exam) {
- this.examService.downloadExamArchive(this.course.id!, this.exam.id!).subscribe({
- next: (response) => downloadZipFileFromResponse(response),
- error: () => this.alertService.error('artemisApp.courseExamArchive.archiveDownloadError'),
- });
+ this.examService.downloadExamArchive(this.course.id!, this.exam.id!);
} else {
- this.courseService.downloadCourseArchive(this.course.id!).subscribe({
- next: (response) => downloadZipFileFromResponse(response),
- error: () => this.alertService.error('artemisApp.courseExamArchive.archiveDownloadError'),
- });
+ this.courseService.downloadCourseArchive(this.course.id!);
}
}
diff --git a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java
index 8e547f2b3c99..6135065201d5 100644
--- a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java
+++ b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java
@@ -2176,9 +2176,11 @@ public void testDownloadCourseArchiveAsInstructor_not_found() throws Exception {
public void testDownloadCourseArchiveAsInstructor() throws Exception {
// Archive the course and wait until it's complete
Course updatedCourse = testArchiveCourseWithTestModelingAndFileUploadExercises();
+ Map expectedHeaders = new HashMap<>();
+ expectedHeaders.put("Content-Disposition", "attachment; filename=\"" + updatedCourse.getCourseArchivePath() + "\"");
// Download the archive
- var archive = request.getFile("/api/courses/" + updatedCourse.getId() + "/download-archive", HttpStatus.OK, new LinkedMultiValueMap<>());
+ var archive = request.getFile("/api/courses/" + updatedCourse.getId() + "/download-archive", HttpStatus.OK, new LinkedMultiValueMap<>(), expectedHeaders);
assertThat(archive).isNotNull();
assertThat(archive).exists();
assertThat(archive.getPath().length()).isGreaterThanOrEqualTo(4);
diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java
index f1016817f6aa..ae08e0c9a3c2 100644
--- a/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java
+++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java
@@ -1069,7 +1069,10 @@ void testDownloadExamArchiveAsInstructor() throws Exception {
// Download the archive
var exam = examRepository.findByCourseId(course.getId()).stream().findFirst().orElseThrow();
- var archive = request.getFile("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/download-archive", HttpStatus.OK, new LinkedMultiValueMap<>());
+ Map expectedHeaders = new HashMap<>();
+ expectedHeaders.put("Content-Disposition", "attachment; filename=\"" + exam.getExamArchivePath() + "\"");
+ var archive = request.getFile("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/download-archive", HttpStatus.OK, new LinkedMultiValueMap<>(),
+ expectedHeaders);
assertThat(archive).isNotNull();
// Extract the archive
diff --git a/src/test/java/de/tum/in/www1/artemis/util/RequestUtilService.java b/src/test/java/de/tum/in/www1/artemis/util/RequestUtilService.java
index 551f2422811b..1f3fe7254ca9 100644
--- a/src/test/java/de/tum/in/www1/artemis/util/RequestUtilService.java
+++ b/src/test/java/de/tum/in/www1/artemis/util/RequestUtilService.java
@@ -69,9 +69,9 @@ public ObjectMapper getObjectMapper() {
* @param file the optional file to be sent
* @param responseType the expected response type as class
* @param expectedStatus the expected status
+ * @param the type of the main object to send
+ * @param the type of the response object
* @return the response as object of the given type or null if the status is not 2xx
- * @param the type of the main object to send
- * @param the type of the response object
* @throws Exception if the request fails
*/
public R postWithMultipartFile(String path, T paramValue, String paramName, MockMultipartFile file, Class responseType, HttpStatus expectedStatus) throws Exception {
@@ -87,9 +87,9 @@ public R postWithMultipartFile(String path, T paramValue, String paramNam
* @param files the optional files to be sent
* @param responseType the expected response type as class
* @param expectedStatus the expected status
+ * @param the type of the main object to send
+ * @param the type of the response object
* @return the response as object of the given type or null if the status is not 2xx
- * @param the type of the main object to send
- * @param the type of the response object
* @throws Exception if the request fails
*/
public R postWithMultipartFiles(String path, T paramValue, String paramName, List files, Class responseType, HttpStatus expectedStatus)
@@ -380,9 +380,9 @@ public R postWithResponseBody(String path, T body, Class responseType)
* @param responseType the expected response type as class
* @param expectedStatus the expected status
* @param params the optional parameters for the request
+ * @param the type of the main object to send
+ * @param the type of the response object
* @return the response as object of the given type or null if the status is not 2xx
- * @param the type of the main object to send
- * @param the type of the response object
* @throws Exception if the request fails
*/
public R putWithMultipartFile(String path, T paramValue, String paramName, MockMultipartFile file, Class responseType, HttpStatus expectedStatus,
@@ -400,9 +400,9 @@ public R putWithMultipartFile(String path, T paramValue, String paramName
* @param responseType the expected response type as class
* @param expectedStatus the expected status
* @param params the optional parameters for the request
+ * @param the type of the main object to send
+ * @param the type of the response object
* @return the response as object of the given type or null if the status is not 2xx
- * @param the type of the main object to send
- * @param the type of the response object
* @throws Exception if the request fails
*/
public R putWithMultipartFiles(String path, T paramValue, String paramName, List files, Class responseType, HttpStatus expectedStatus,
@@ -558,12 +558,17 @@ public T get(String path, HttpStatus expectedStatus, Class responseType,
}
public File getFile(String path, HttpStatus expectedStatus, MultiValueMap params) throws Exception {
+ return getFile(path, expectedStatus, params, null);
+ }
+
+ public File getFile(String path, HttpStatus expectedStatus, MultiValueMap params, @Nullable Map expectedResponseHeaders) throws Exception {
MvcResult res = mvc.perform(MockMvcRequestBuilders.get(new URI(path)).params(params).headers(new HttpHeaders())).andExpect(status().is(expectedStatus.value())).andReturn();
restoreSecurityContext();
if (!expectedStatus.is2xxSuccessful()) {
assertThat(res.getResponse().containsHeader("location")).as("no location header on failed request").isFalse();
return null;
}
+ verifyExpectedResponseHeaders(expectedResponseHeaders, res);
String tmpDirectory = System.getProperty("java.io.tmpdir");
var filename = res.getResponse().getHeader("filename");
diff --git a/src/test/javascript/spec/component/course/course-management.service.spec.ts b/src/test/javascript/spec/component/course/course-management.service.spec.ts
index 50bb53271755..61cacac53048 100644
--- a/src/test/javascript/spec/component/course/course-management.service.spec.ts
+++ b/src/test/javascript/spec/component/course/course-management.service.spec.ts
@@ -426,15 +426,11 @@ describe('Course Management Service', () => {
tick();
}));
- it('should download course archive', fakeAsync(() => {
- const expectedBlob = new Blob(['abc', 'cfe']);
- courseManagementService.downloadCourseArchive(course.id!).subscribe((resp) => {
- expect(resp.body).toEqual(expectedBlob);
- });
- const req = httpMock.expectOne({ method: 'GET', url: `${resourceUrl}/${course.id}/download-archive` });
- req.flush(expectedBlob);
- tick();
- }));
+ it('should download course archive', () => {
+ const windowSpy = jest.spyOn(window, 'open').mockImplementation();
+ courseManagementService.downloadCourseArchive(1);
+ expect(windowSpy).toHaveBeenCalledWith('api/courses/1/download-archive', '_blank');
+ });
it('should archive the course', fakeAsync(() => {
courseManagementService.archiveCourse(course.id!).subscribe((res) => expect(res.body).toEqual(course));
diff --git a/src/test/javascript/spec/component/exam/manage/exam-management.service.spec.ts b/src/test/javascript/spec/component/exam/manage/exam-management.service.spec.ts
index 8370f024633e..9f1b6c9ecc84 100644
--- a/src/test/javascript/spec/component/exam/manage/exam-management.service.spec.ts
+++ b/src/test/javascript/spec/component/exam/manage/exam-management.service.spec.ts
@@ -641,21 +641,11 @@ describe('Exam Management Service Tests', () => {
}));
it('should download the exam from archive', fakeAsync(() => {
- // GIVEN
const mockExam: Exam = { id: 1 };
- const mockResponse = new Blob();
- const expected = new Blob();
-
- // WHEN
- service.downloadExamArchive(course.id!, mockExam.id!).subscribe((res) => expect(res.body).toEqual(expected));
- // THEN
- const req = httpMock.expectOne({
- method: 'GET',
- url: `${service.resourceUrl}/${course.id}/exams/${mockExam.id}/download-archive`,
- });
- req.flush(mockResponse);
- tick();
+ const windowSpy = jest.spyOn(window, 'open').mockImplementation();
+ service.downloadExamArchive(course.id!, mockExam.id!);
+ expect(windowSpy).toHaveBeenCalledWith('api/courses/456/exams/1/download-archive', '_blank');
}));
it('should archive the exam', fakeAsync(() => {
diff --git a/src/test/javascript/spec/component/shared/course-exam-archive-button.component.spec.ts b/src/test/javascript/spec/component/shared/course-exam-archive-button.component.spec.ts
index 287f39430d54..2dfb1b17f5f6 100644
--- a/src/test/javascript/spec/component/shared/course-exam-archive-button.component.spec.ts
+++ b/src/test/javascript/spec/component/shared/course-exam-archive-button.component.spec.ts
@@ -170,8 +170,7 @@ describe('Course Exam Archive Button Component', () => {
}));
it('should download archive for course', fakeAsync(() => {
- const response: HttpResponse = new HttpResponse({ status: 200 });
- const downloadStub = jest.spyOn(courseManagementService, 'downloadCourseArchive').mockReturnValue(of(response));
+ const downloadStub = jest.spyOn(courseManagementService, 'downloadCourseArchive').mockImplementation(() => {});
comp.downloadArchive();
@@ -253,14 +252,13 @@ describe('Course Exam Archive Button Component', () => {
expect(comp.canCleanupCourse()).toBeFalse();
}));
- it('should download archive for exam', fakeAsync(() => {
- const response: HttpResponse = new HttpResponse({ status: 200 });
- const downloadStub = jest.spyOn(examManagementService, 'downloadExamArchive').mockReturnValue(of(response));
+ it('should download archive for exam', () => {
+ const downloadStub = jest.spyOn(examManagementService, 'downloadExamArchive').mockImplementation(() => {});
comp.downloadArchive();
expect(downloadStub).toHaveBeenCalledOnce();
- }));
+ });
it('should archive course', fakeAsync(() => {
const response: HttpResponse = new HttpResponse({ status: 200 });