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

Development: Fix issues in the user data export #7084

Merged
merged 40 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
186e9ed
try to improve download
tobias-lippert Aug 17, 2023
cc85e6f
fix test
tobias-lippert Aug 17, 2023
75a4a6b
try different approach
tobias-lippert Aug 17, 2023
72026c9
fix client test
tobias-lippert Aug 18, 2023
d879ef7
use separate subdirectories for exercises and exams, do not include p…
tobias-lippert Aug 18, 2023
228d924
adjust tests to reflect new structure
tobias-lippert Aug 18, 2023
28553bd
extend test
tobias-lippert Aug 18, 2023
f96d0d8
code review comments
tobias-lippert Aug 20, 2023
bbf960f
add more javadoc
tobias-lippert Aug 21, 2023
d289740
improve javadoc
tobias-lippert Aug 21, 2023
889551f
improve javadoc again
tobias-lippert Aug 22, 2023
2a9893d
change cron expression
tobias-lippert Aug 25, 2023
9ef1091
use executor service to create exports manually
tobias-lippert Aug 25, 2023
8281d18
Apply suggestions from code review
tobias-lippert Aug 25, 2023
625cda8
use executor service to create exports in parallel
tobias-lippert Aug 25, 2023
370bbf4
Apply suggestions from code review
tobias-lippert Aug 25, 2023
9c37fcb
do not include all general exam information by default and fix button…
tobias-lippert Aug 25, 2023
282bf13
Merge remote-tracking branch 'origin/enhancement/improve-data-export-…
tobias-lippert Aug 25, 2023
c7ac292
move method to service layer
tobias-lippert Aug 25, 2023
afa77bb
await termination and shutdown executor service
tobias-lippert Aug 25, 2023
4e0003d
increase termination timeout and explain it
tobias-lippert Aug 26, 2023
da93949
fix authentication object null
tobias-lippert Aug 27, 2023
d8a9f16
consider due date for quiz exercises instead of assessment due date a…
tobias-lippert Aug 27, 2023
6bb5157
fix correct including of programming exercise results and do not show…
tobias-lippert Aug 28, 2023
e30143e
implement lucas' suggestion
tobias-lippert Aug 28, 2023
d2af5c2
add missing param tag
tobias-lippert Aug 28, 2023
e885a23
avoid failure if feedback is null
tobias-lippert Aug 28, 2023
19bd6b4
include information if is at least tutor
tobias-lippert Aug 28, 2023
b247057
Merge branch 'develop' into enhancement/improve-data-export-download
tobias-lippert Aug 29, 2023
45fe4fd
remove isTutorCheck again because it's inconsistent with the web inte…
tobias-lippert Aug 29, 2023
90691d5
download latest export if multiple downloadable exports exist
tobias-lippert Aug 30, 2023
418d4de
include all results if the user is at least an instructor in the cour…
tobias-lippert Sep 2, 2023
6154ed0
fix failing archunit tests
tobias-lippert Sep 2, 2023
2347248
Merge branch 'develop' into enhancement/improve-data-export-download
tobias-lippert Sep 2, 2023
9576b1f
Merge branch 'develop' into enhancement/improve-data-export-download
tobias-lippert Sep 2, 2023
650ec24
review comments
tobias-lippert Sep 3, 2023
e0607e7
make usage of apollon conversion service more failure tolerant
tobias-lippert Sep 7, 2023
77624ec
fix another typo
tobias-lippert Sep 8, 2023
8e85e8f
Merge branch 'develop' into enhancement/improve-data-export-download
tobias-lippert Sep 14, 2023
a6a8263
change cron expression to production value again
tobias-lippert Sep 14, 2023
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 @@ -8,10 +8,28 @@ public enum DataExportState {

REQUESTED, IN_CREATION, EMAIL_SENT, DOWNLOADED, DOWNLOADED_DELETED, DELETED, FAILED;

/**
* Checks if the data export can be downloaded.
tobias-lippert marked this conversation as resolved.
Show resolved Hide resolved
* <p>
* The data export can be downloaded if its state is either EMAIL_SENT or DOWNLOADED.
* The state is EMAIL_SENT if the data export has been created and the user has been notified via email.
* The state is DOWNLOADED if the user has downloaded the data export at least once.
*
* @return true if the data export can be downloaded, false otherwise
*/
public boolean isDownloadable() {
return this == DOWNLOADED || this == EMAIL_SENT;
}

/**
* Checks if the data export has been downloaded.
tobias-lippert marked this conversation as resolved.
Show resolved Hide resolved
* <p>
* The data export has been downloaded if its state is either DOWNLOADED or DOWNLOADED_DELETED.
* The state is DOWNLOADED if the user has downloaded the data export at least once, but it has not been deleted yet.
* The state is DOWNLOADED_DELETED if the user has downloaded the data export at least once, and it has been deleted.
*
* @return true if the data export has been downloaded, false otherwise
*/
public boolean hasBeenDownloaded() {
return this == DOWNLOADED || this == DOWNLOADED_DELETED;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.tum.in.www1.artemis.repository;

import java.util.List;
import java.util.Set;

import org.springframework.data.jpa.repository.JpaRepository;
Expand Down Expand Up @@ -55,12 +56,23 @@ default DataExport findByIdElseThrow(long dataExportId) {
""")
Set<DataExport> findAllToBeDeleted();

/**
* Find all data exports for the given user ordered by their request date descending.
* We use this sorting because this allows us to always get the latest data export without a doing any other calculations.
* <p>
* This is relevant if more than one data export exists that can be downloaded.
* This can happen if the user had requested a data export that was created and the admin requested another data export for the same user that has been created.
*
* @param userId the id of the user to find the data exports for
* @return a list of data exports for the given user ordered by their request date descending
*/
@Query("""
SELECT dataExport
FROM DataExport dataExport
WHERE dataExport.user.id = :userId
ORDER BY dataExport.createdDate DESC
""")
Set<DataExport> findAllDataExportsByUserId(long userId);
List<DataExport> findAllDataExportsByUserIdOrderByRequestDateDesc(long userId);

@Query("""
SELECT dataExport
Expand Down
46 changes: 29 additions & 17 deletions src/main/java/de/tum/in/www1/artemis/service/ResultService.java
Original file line number Diff line number Diff line change
Expand Up @@ -217,37 +217,49 @@ public List<Feedback> filterFeedbackForClient(Result result) {
* @param result a result of this participation
*/
public void filterSensitiveInformationIfNecessary(final Participation participation, final Result result) {
this.filterSensitiveInformationIfNecessary(participation, List.of(result));
this.filterSensitiveInformationIfNecessary(participation, List.of(result), Optional.empty());
}

/**
* Removes sensitive information that students should not see (yet) from the given results.
*
* @param participation the results belong to.
* @param results collection of results of this participation
* @param user the user for which the information should be filtered if it is an empty optional, the currently logged-in user is used
*/
public void filterSensitiveInformationIfNecessary(final Participation participation, final Collection<Result> results) {
public void filterSensitiveInformationIfNecessary(final Participation participation, final Collection<Result> results, Optional<User> user) {
results.forEach(Result::filterSensitiveInformation);
if (!authCheckService.isAtLeastTeachingAssistantForExercise(participation.getExercise())) {
// The test cases marked as after_due_date should only be shown after all
// students can no longer submit so that no unfair advantage is possible.
//
// For course exercises, this applies only to automatic results. For manual ones the instructors
// are responsible to set an appropriate assessment due date.
//
// For exams, we filter sensitive results until the results are published.
// For test exam exercises, this is the case when the student submitted the test exam.

Exercise exercise = participation.getExercise();
if (exercise.isExamExercise()) {
filterSensitiveFeedbacksInExamExercise(participation, results, exercise);
if (user.isPresent()) {
if (!authCheckService.isAtLeastTeachingAssistantForExercise(participation.getExercise(), user.get())) {
filterInformation(participation, results);
}
else {
filterSensitiveFeedbackInCourseExercise(participation, results, exercise);
}
else {
if (!authCheckService.isAtLeastTeachingAssistantForExercise(participation.getExercise())) {
filterInformation(participation, results);
}
}
}

private void filterInformation(Participation participation, Collection<Result> results) {
// The test cases marked as after_due_date should only be shown after all
// students can no longer submit so that no unfair advantage is possible.
//
// For course exercises, this applies only to automatic results. For manual ones the instructors
// are responsible to set an appropriate assessment due date.
//
// For exams, we filter sensitive results until the results are published.
// For test exam exercises, this is the case when the student submitted the test exam.

Exercise exercise = participation.getExercise();
if (exercise.isExamExercise()) {
filterSensitiveFeedbacksInExamExercise(participation, results, exercise);
}
else {
filterSensitiveFeedbackInCourseExercise(participation, results, exercise);
}
}

private void filterSensitiveFeedbackInCourseExercise(Participation participation, Collection<Result> results, Exercise exercise) {
boolean beforeLatestDueDate = exerciseDateService.isBeforeLatestDueDate(exercise);
boolean participationBeforeDueDate = exerciseDateService.isBeforeDueDate(participation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

/**
* A service to create the communication data export for users
* This includes messages (posts), thread replies (answer posts) and reactions to posts and answer posts
* All communication data is exported per course and stored in a CSV file.
*/
@Service
public class DataExportCommunicationDataService {
Expand Down Expand Up @@ -67,6 +69,12 @@ public void createCommunicationDataExport(long userId, Path workingDirectory) th
createCommunicationDataExportIfReactionsToAnswerPostsExist(workingDirectory, reactionsToAnswerPostsPerCourse);
}

/**
* Creates the communication data export for a course if only reactions to answer posts exist
*
* @param workingDirectory the directory where the export is stored
* @param reactionsToAnswerPostsPerCourse the reactions to answer posts grouped by course
*/
private void createCommunicationDataExportIfReactionsToAnswerPostsExist(Path workingDirectory, Map<Course, List<Reaction>> reactionsToAnswerPostsPerCourse) throws IOException {
// it can happen that only answer post reactions exist in a course but neither posts, nor answer posts nor reactions to posts
for (var entry : reactionsToAnswerPostsPerCourse.entrySet()) {
Expand All @@ -78,6 +86,13 @@ private void createCommunicationDataExportIfReactionsToAnswerPostsExist(Path wor
}
}

/**
* Creates the communication data export for a course if only reactions to posts (and potentially to answer posts) exist
*
* @param workingDirectory the directory where the export is stored
* @param reactionsToPostsPerCourse the reactions to posts grouped by course
* @param reactionsToAnswerPostsPerCourse the reactions to answer posts grouped by course
*/
private void createCommunicationDataExportIfReactionsToPostsExist(Path workingDirectory, Map<Course, List<Reaction>> reactionsToPostsPerCourse,
Map<Course, List<Reaction>> reactionsToAnswerPostsPerCourse) throws IOException {
// it can happen that only reactions exist in a course but no post or answer post
Expand All @@ -91,6 +106,14 @@ private void createCommunicationDataExportIfReactionsToPostsExist(Path workingDi
}
}

/**
* Creates the communication data export for a course if only answer posts (and potentially reactions to post and answer posts) exist
*
* @param workingDirectory the directory where the export is stored
* @param answerPostsPerCourse the answer posts grouped by course
* @param reactionsToPostsPerCourse the reactions to posts grouped by course
* @param reactionsToAnswerPostsPerCourse the reactions to answer posts grouped by course
*/
private void createCommunicationDataExportIfAnswerPostsExist(Path workingDirectory, Map<Course, List<AnswerPost>> answerPostsPerCourse,
Map<Course, List<Reaction>> reactionsToPostsPerCourse, Map<Course, List<Reaction>> reactionsToAnswerPostsPerCourse) throws IOException {
// it can happen that an answer post and reactions exist in a course but no post
Expand All @@ -105,6 +128,15 @@ private void createCommunicationDataExportIfAnswerPostsExist(Path workingDirecto
}
}

/**
* Creates the communication data export for a course if posts exist
*
* @param workingDirectory the directory where the export is stored
* @param postsPerCourse the posts grouped by course
* @param answerPostsPerCourse the answer posts grouped by course
* @param reactionsToPostsPerCourse the reactions to posts grouped by course
* @param reactionsToAnswerPostsPerCourse the reactions to answer posts grouped by course
*/
private void createCommunicationDataExportIfPostsExist(Path workingDirectory, Map<Course, List<Post>> postsPerCourse, Map<Course, List<AnswerPost>> answerPostsPerCourse,
Map<Course, List<Reaction>> reactionsToPostsPerCourse, Map<Course, List<Reaction>> reactionsToAnswerPostsPerCourse) throws IOException {
// this covers all cases where at least one post in a course exists
Expand All @@ -121,6 +153,15 @@ private void createCommunicationDataExportIfPostsExist(Path workingDirectory, Ma
}
}

/**
* Creates the actual CSV file containing the communication data for a course
*
* @param courseDir the directory where the CSV file is stored
* @param postsInCourse the posts in the course
* @param answerPostsInCourse the answer posts in the course
* @param postReactionsInCourse the reactions to posts in the course
* @param answerPostReactionsInCourse the reactions to answer posts in the course
*/
private void createCommunicationDataCsvFile(Path courseDir, List<Post> postsInCourse, List<AnswerPost> answerPostsInCourse, List<Reaction> postReactionsInCourse,
List<Reaction> answerPostReactionsInCourse) throws IOException {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@

/**
* A service to create data exports for users
* This service is responsible for creating the data export, delegating most tasks to the {@link DataExportExerciseCreationService} and {@link DataExportExamCreationService}
* and notifying the user about the creation.
*/
@Service
public class DataExportCreationService {
Expand Down Expand Up @@ -88,7 +90,7 @@ private DataExport createDataExportWithContent(DataExport dataExport) throws IOE
var userId = dataExport.getUser().getId();
var user = dataExport.getUser();
var workingDirectory = prepareDataExport(dataExport);
dataExportExerciseCreationService.createExercisesExport(workingDirectory, userId);
dataExportExerciseCreationService.createExercisesExport(workingDirectory, user);
dataExportExamCreationService.createExportForExams(userId, workingDirectory);
dataExportCommunicationDataService.createCommunicationDataExport(userId, workingDirectory);
addGeneralUserInformation(user, workingDirectory);
Expand All @@ -97,6 +99,17 @@ private DataExport createDataExportWithContent(DataExport dataExport) throws IOE
return finishDataExportCreation(dataExport, dataExportPath);
}

/**
* Adds a markdown file with the title README.md to the data export.
tobias-lippert marked this conversation as resolved.
Show resolved Hide resolved
* <p>
* This file contains information Art. 15 GDPR requires us to provide to the user.
* The file is retrieved from the resources folder.
* The file is added to the root of the data export.
*
* @param workingDirectory the directory in which the data export is created
* @throws IOException if the file could not be copied
* @throws URISyntaxException if the resource file path is invalid
*/
private void addReadmeFile(Path workingDirectory) throws IOException, URISyntaxException {
var readmeInDataExportPath = workingDirectory.resolve("README.md");
var readmeTemplatePath = Path.of("templates", "dataexport", "README.md");
Expand All @@ -105,6 +118,8 @@ private void addReadmeFile(Path workingDirectory) throws IOException, URISyntaxE

/**
* Creates the data export for the given user.
tobias-lippert marked this conversation as resolved.
Show resolved Hide resolved
* <p>
* This includes creation of the export and notifying the user about the creation.
*
* @param dataExport the data export to be created
* @return true if the export was successful, false otherwise
Expand All @@ -129,6 +144,15 @@ public boolean createDataExport(DataExport dataExport) {
return true;
}

/**
* Handles the case of a failed data export creation.
tobias-lippert marked this conversation as resolved.
Show resolved Hide resolved
* <p>
* This includes setting the state of the data export to failed, notifying the user about the failure and sending an email to the admin with the exception why the export
* failed.
*
* @param dataExport the data export that failed to be created
* @param exception the exception that occurred during the creation
*/
private void handleCreationFailure(DataExport dataExport, Exception exception) {
dataExport.setDataExportState(DataExportState.FAILED);
dataExport = dataExportRepository.save(dataExport);
Expand All @@ -141,6 +165,13 @@ private void handleCreationFailure(DataExport dataExport, Exception exception) {
mailService.sendDataExportFailedEmailToAdmin(admin.get(), dataExport, exception);
}

/**
* Finishes the creation of the data export by setting the file path to the zip file, the state to EMAIL_SENT and the creation finished date.
*
* @param dataExport the data export whose creation is finished
* @param dataExportPath the path to the zip file containing the data export
* @return the updated data export from the database
*/
private DataExport finishDataExportCreation(DataExport dataExport, Path dataExportPath) {
dataExport.setFilePath(dataExportPath.toString());
dataExport.setCreationFinishedDate(ZonedDateTime.now());
Expand All @@ -149,6 +180,15 @@ private DataExport finishDataExportCreation(DataExport dataExport, Path dataExpo
return dataExportRepository.save(dataExport);
}

/**
* Prepares the data export by creating the working directory, scheduling it for deletion and setting the state to IN_CREATION.
tobias-lippert marked this conversation as resolved.
Show resolved Hide resolved
* <p>
* If the path where the data exports are stored does not exist yet, it will be created.
*
* @param dataExport the data export to be prepared
* @return the path to the working directory
* @throws IOException if the working directory could not be created
*/
private Path prepareDataExport(DataExport dataExport) throws IOException {
if (!Files.exists(dataExportsPath)) {
Files.createDirectories(dataExportsPath);
Expand All @@ -161,6 +201,14 @@ private Path prepareDataExport(DataExport dataExport) throws IOException {
return workingDirectory;
}

/**
* Adds the general user information to the data export.
tobias-lippert marked this conversation as resolved.
Show resolved Hide resolved
* <p>
* This includes the login, name, email, and registration number (matriculation number).
*
* @param user the user for which the information should be added
* @param workingDirectory the directory in which the information should be stored
*/
private void addGeneralUserInformation(User user, Path workingDirectory) throws IOException {
String[] headers = { "login", "name", "email", "registration number" };
CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setHeader(headers).build();
Expand All @@ -171,11 +219,18 @@ private void addGeneralUserInformation(User user, Path workingDirectory) throws
}
}

/**
* Creates the zip file containing the data export.
*
* @param userLogin the login of the user for which the data export was created
* @param workingDirectory the directory containing the data export
* @return the path to the zip file
* @throws IOException if the zip file could not be created
*/
private Path createDataExportZipFile(String userLogin, Path workingDirectory) throws IOException {
// There should actually never exist more than one data export for a user at a time (once the feature is fully implemented), but to be sure the name is unique, we add the
// current timestamp
return zipFileService.createZipFileWithFolderContent(dataExportsPath.resolve("data-export_" + userLogin + ZonedDateTime.now().toEpochSecond() + ZIP_FILE_EXTENSION),
workingDirectory, null);

}
}
Loading