Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Programming exercises: Export files embedded with HTML syntax #7187

Merged
merged 6 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
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;
import de.tum.in.www1.artemis.domain.participation.*;
import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation;
import de.tum.in.www1.artemis.domain.participation.StudentParticipation;
import de.tum.in.www1.artemis.exception.GitException;
import de.tum.in.www1.artemis.repository.AuxiliaryRepositoryRepository;
import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository;
Expand Down Expand Up @@ -90,7 +91,9 @@ public class ProgrammingExerciseExportService {

public static final String EXPORTED_EXERCISE_PROBLEM_STATEMENT_FILE_PREFIX = "Problem-Statement";

private static final String EMBEDDED_FILE_REGEX = "\\[.*] *\\(/api/files/markdown/.*\\)";
private static final String EMBEDDED_FILE_MARKDOWN_SYNTAX_REGEX = "\\[.*] *\\(/api/files/markdown/.*\\)";

private static final String EMBEDDED_FILE_HTML_SYNTAX_REGEX = "<img src=\"/api/files/markdown/.*\" .*>";

private static final String API_MARKDOWN_FILE_PATH = "/api/files/markdown/";

Expand Down Expand Up @@ -152,6 +155,14 @@ public Path exportProgrammingExerciseInstructorMaterial(ProgrammingExercise exer
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<String> exportErrors, Path exportDir, List<Path> pathsToBeZipped) {
var problemStatementFileExtension = ".md";
String problemStatementFileName = EXPORTED_EXERCISE_PROBLEM_STATEMENT_FILE_PREFIX + "-" + exercise.getTitle() + problemStatementFileExtension;
Expand All @@ -168,15 +179,100 @@ private void exportProblemStatementAndEmbeddedFiles(ProgrammingExercise exercise
* @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<Path> pathsToBeZipped, List<String> exportErrors) {
Set<String> embeddedFiles = new HashSet<>();
Set<String> embeddedFilesWithMarkdownSyntax = new HashSet<>();
Set<String> 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<String> exportErrors, Set<String> 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);
}
}

/**
* Copies the files that are embedded with html syntax to the embedded files' directory.
* <p>
tobias-lippert marked this conversation as resolved.
Show resolved Hide resolved
*
* @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<String> exportErrors, Set<String> 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);
}
}

Matcher matcher = Pattern.compile(EMBEDDED_FILE_REGEX).matcher(exercise.getProblemStatement());
/**
* Extracts the filename from the matched string and copies the file to the embedded files' directory.
* <p>
tobias-lippert marked this conversation as resolved.
Show resolved Hide resolved
*
* @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
*/
private void constructFilenameAndCopyFile(ProgrammingExercise exercise, List<String> 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());
}
}
}

/**
* Checks for matches in the problem statement and creates a directory for the embedded files.
* <p>
tobias-lippert marked this conversation as resolved.
Show resolved Hide resolved
*
* @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
*/
private Path checkForMatchesInProblemStatementAndCreateDirectoryForFiles(Path outputDir, List<Path> pathsToBeZipped, List<String> exportErrors, Set<String> embeddedFiles,
Matcher matcher) {
while (matcher.find()) {
embeddedFiles.add(matcher.group());
}
log.debug("Found embedded files:{} ", embeddedFiles);
log.debug("Found embedded files: {} ", embeddedFiles);
Path embeddedFilesDir = outputDir.resolve("files");
if (!embeddedFiles.isEmpty()) {
if (!Files.exists(embeddedFilesDir)) {
Expand All @@ -186,30 +282,12 @@ private void copyEmbeddedFiles(ProgrammingExercise exercise, Path outputDir, Lis
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;
return null;
}
}
pathsToBeZipped.add(embeddedFilesDir);
}
for (String embeddedFile : embeddedFiles) {
// 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(")"));
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());
}
}
}

return embeddedFilesDir;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.tum.in.www1.artemis.exercise.programmingexercise;

import static de.tum.in.www1.artemis.domain.enumeration.ExerciseMode.*;
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;
Expand All @@ -17,6 +18,7 @@
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -48,6 +50,8 @@
import de.tum.in.www1.artemis.config.StaticCodeAnalysisConfigurer;
import de.tum.in.www1.artemis.course.CourseUtilService;
import de.tum.in.www1.artemis.domain.*;
import de.tum.in.www1.artemis.domain.Authority;
import de.tum.in.www1.artemis.domain.AuxiliaryRepository;
import de.tum.in.www1.artemis.domain.enumeration.*;
import de.tum.in.www1.artemis.domain.exam.Exam;
import de.tum.in.www1.artemis.domain.exam.ExamUser;
Expand Down Expand Up @@ -1492,7 +1496,7 @@ private void generateProgrammingExerciseForExport(boolean saveEmbeddedFiles) thr
exercise.setProblemStatement(String.format("""
Problem statement
![mountain.jpg](/api/files/markdown/%s)
![matterhorn.jpg](/api/files/markdown/%s)
<img src="/api/files/markdown/%s" width="400">
""", embeddedFileName1, embeddedFileName2));
if (saveEmbeddedFiles) {
Files.write(Path.of(FilePathService.getMarkdownFilePath(), embeddedFileName1),
Expand Down