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 8 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 @@ -176,6 +176,5 @@ private Path createDataExportZipFile(String userLogin, Path workingDirectory) th
// current timestamp
return zipFileService.createZipFileWithFolderContent(dataExportsPath.resolve("data-export_" + userLogin + ZonedDateTime.now().toEpochSecond() + ZIP_FILE_EXTENSION),
workingDirectory, null);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand All @@ -18,8 +16,10 @@
import org.springframework.stereotype.Service;

import de.tum.in.www1.artemis.domain.Course;
import de.tum.in.www1.artemis.domain.GradingScale;
import de.tum.in.www1.artemis.domain.ProgrammingExercise;
import de.tum.in.www1.artemis.domain.exam.StudentExam;
import de.tum.in.www1.artemis.repository.GradingScaleRepository;
import de.tum.in.www1.artemis.repository.StudentExamRepository;
import de.tum.in.www1.artemis.service.exam.ExamService;
import de.tum.in.www1.artemis.web.rest.dto.ExamScoresDTO;
Expand All @@ -38,11 +38,14 @@ public class DataExportExamCreationService {

private final ExamService examService;

public DataExportExamCreationService(StudentExamRepository studentExamRepository, DataExportExerciseCreationService dataExportExerciseCreationService,
ExamService examService) {
private final GradingScaleRepository gradingScaleRepository;

public DataExportExamCreationService(StudentExamRepository studentExamRepository, DataExportExerciseCreationService dataExportExerciseCreationService, ExamService examService,
GradingScaleRepository gradingScaleRepository) {
this.studentExamRepository = studentExamRepository;
this.dataExportExerciseCreationService = dataExportExerciseCreationService;
this.examService = examService;
this.gradingScaleRepository = gradingScaleRepository;
}

/**
Expand All @@ -61,9 +64,10 @@ public void createExportForExams(long userId, Path workingDirectory) throws IOEx
var exam = studentExam.getExam();
var examTitle = exam.getSanitizedExamTitle();
var courseDirPath = retrieveCourseDirPath(workingDirectory, exam.getCourse());
createDirectoryIfNotExistent(courseDirPath);
var examsDirPath = courseDirPath.resolve("exams");
createDirectoryIfNotExistent(examsDirPath);
var examDirectoryName = EXAM_DIRECTORY_PREFIX + examTitle + "_" + studentExam.getId();
var examWorkingDir = Files.createDirectories(courseDirPath.resolve(examDirectoryName));
var examWorkingDir = Files.createDirectories(examsDirPath.resolve(examDirectoryName));
tobias-lippert marked this conversation as resolved.
Show resolved Hide resolved
createStudentExamExport(studentExam, examWorkingDir);
}
}
Expand Down Expand Up @@ -92,8 +96,9 @@ private void createStudentExamExport(StudentExam studentExam, Path examWorkingDi
private void addExamScores(StudentExam studentExam, Path examWorkingDir) throws IOException {
var studentExamGrade = examService.getStudentExamGradeForDataExport(studentExam);
var studentResult = studentExamGrade.studentResult();
var gradingScale = gradingScaleRepository.findByExamId(studentExam.getExam().getId());
List<String> headers = new ArrayList<>();
var examResults = getExamResultsStreamToPrint(studentResult, headers);
var examResults = getExamResultsStreamToPrint(studentResult, headers, gradingScale);
CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setHeader(headers.toArray(new String[0])).build();
try (final CSVPrinter printer = new CSVPrinter(
Files.newBufferedWriter(examWorkingDir.resolve(EXAM_DIRECTORY_PREFIX + studentExam.getId() + "_result" + CSV_FILE_EXTENSION)), csvFormat)) {
Expand All @@ -102,21 +107,21 @@ private void addExamScores(StudentExam studentExam, Path examWorkingDir) throws
}
}

private Stream<?> getExamResultsStreamToPrint(ExamScoresDTO.StudentResult studentResult, List<String> headers) {
private Stream<?> getExamResultsStreamToPrint(ExamScoresDTO.StudentResult studentResult, List<String> headers, Optional<GradingScale> gradingScaleOptional) {
var builder = Stream.builder();
if (studentResult.overallPointsAchieved() != null) {
builder.add(studentResult.overallPointsAchieved());
headers.add("overall points");
}
if (studentResult.hasPassed() != null) {
if (studentResult.hasPassed() != null && gradingScaleOptional.isPresent()) {
builder.add(studentResult.hasPassed());
headers.add("passed");
}
if (studentResult.overallGrade() != null) {
if (studentResult.overallGrade() != null && gradingScaleOptional.isPresent()) {
builder.add(studentResult.overallGrade());
headers.add("overall grade");
}
if (studentResult.gradeWithBonus() != null) {
if (studentResult.gradeWithBonus() != null && gradingScaleOptional.isPresent()) {
builder.add(studentResult.gradeWithBonus());
headers.add("grade with bonus");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.tum.in.www1.artemis.service.dataexport;

import static de.tum.in.www1.artemis.service.dataexport.DataExportQuizExerciseCreationService.TXT_FILE_EXTENSION;
import static de.tum.in.www1.artemis.service.dataexport.DataExportUtil.createDirectoryIfNotExistent;
import static de.tum.in.www1.artemis.service.dataexport.DataExportUtil.retrieveCourseDirPath;

import java.io.File;
Expand Down Expand Up @@ -45,6 +46,8 @@ public class DataExportExerciseCreationService {

private static final String PDF_FILE_EXTENSION = ".pdf";

private static final String EXERCISE_PREFIX = "exercise_";

static final String CSV_FILE_EXTENSION = ".csv";

private final Path repoClonePath;
Expand Down Expand Up @@ -96,15 +99,16 @@ public void createExercisesExport(Path workingDirectory, long userId) throws IOE
var course = entry.getKey();
Path courseDir = retrieveCourseDirPath(workingDirectory, course);
var exercises = entry.getValue();
Path exercisesDir = courseDir.resolve("exercises");
if (!exercises.isEmpty()) {
Files.createDirectory(courseDir);
createDirectoryIfNotExistent(exercisesDir);
}
for (var exercise : exercises) {
if (exercise instanceof ProgrammingExercise programmingExercise) {
createProgrammingExerciseExport(programmingExercise, courseDir, userId);
createProgrammingExerciseExport(programmingExercise, exercisesDir, userId);
}
else {
createNonProgrammingExerciseExport(exercise, courseDir, userId);
createNonProgrammingExerciseExport(exercise, exercisesDir, userId);
}
}
}
Expand All @@ -114,13 +118,12 @@ public void createExercisesExport(Path workingDirectory, long userId) throws IOE
* Creates an export for a given programming exercise. Includes submission information, the repository from the VCS and potential plagiarism cases.
tobias-lippert marked this conversation as resolved.
Show resolved Hide resolved
*
* @param programmingExercise the programming exercise for which the export should be created
* @param courseDir the directory that is used for the course the exercise belongs to
* @param exercisesDir the directory that is used for to store the exercises of the course the exercise belongs to
Strohgelaender marked this conversation as resolved.
Show resolved Hide resolved
* @param userId the id of the user that requested the export
* @throws IOException if an error occurs while accessing the file system
*/

public void createProgrammingExerciseExport(ProgrammingExercise programmingExercise, Path courseDir, long userId) throws IOException {
Path exerciseDir = courseDir.resolve(programmingExercise.getSanitizedExerciseTitle());
public void createProgrammingExerciseExport(ProgrammingExercise programmingExercise, Path exercisesDir, long userId) throws IOException {
Path exerciseDir = exercisesDir.resolve(EXERCISE_PREFIX + programmingExercise.getSanitizedExerciseTitle());
if (!Files.exists(exerciseDir)) {
Files.createDirectory(exerciseDir);
}
Expand Down Expand Up @@ -156,7 +159,7 @@ public void createProgrammingExerciseExport(ProgrammingExercise programmingExerc
* @throws IOException if an error occurs while accessing the file system
*/
public void createNonProgrammingExerciseExport(Exercise exercise, Path courseDir, long userId) throws IOException {
Path exercisePath = courseDir.resolve(exercise.getSanitizedExerciseTitle());
Path exercisePath = courseDir.resolve(EXERCISE_PREFIX + exercise.getSanitizedExerciseTitle());
if (!Files.exists(exercisePath)) {
Files.createDirectory(exercisePath);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ private DataExportUtil() {

static void createDirectoryIfNotExistent(Path directory) throws IOException {
if (!Files.exists(directory)) {
Files.createDirectory(directory);
Files.createDirectories(directory);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ public DataExportScheduleService(DataExportRepository dataExportRepository, Data
* Created will be all data exports that are in the state REQUESTED OR IN_CREATION
* Deleted will be all data exports that have a creation date older than seven days
*/
@Scheduled(cron = "${artemis.scheduling.data-export-creation-time: 0 0 4 * * *}")
// TODO change again after testing. As long as we do not use this cron expression one server test always fails
tobias-lippert marked this conversation as resolved.
Show resolved Hide resolved
// @Scheduled(cron = "${artemis.scheduling.data-export-creation-time: 0 0 4 * * *}")
@Scheduled(cron = "0 */5 * * * *")
public void createDataExportsAndDeleteOldOnes() {
if (profileService.isDev()) {
// do not execute this in a development environment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.*;
tobias-lippert marked this conversation as resolved.
Show resolved Hide resolved
import org.springframework.web.bind.annotation.*;

import de.tum.in.www1.artemis.domain.DataExport;
Expand Down Expand Up @@ -96,7 +95,11 @@ public ResponseEntity<Resource> downloadDataExport(@PathVariable long dataExport
checkDataExportCanBeDownloaded(dataExport);
Resource resource = dataExportService.downloadDataExport(dataExport);
File finalZipFile = Path.of(dataExport.getFilePath()).toFile();
return ResponseEntity.ok().contentLength(finalZipFile.length()).contentType(MediaType.APPLICATION_OCTET_STREAM).header("filename", finalZipFile.getName()).body(resource);
ContentDisposition contentDisposition = ContentDisposition.builder("attachment").filename(finalZipFile.getName()).build();
HttpHeaders headers = new HttpHeaders();
headers.setContentDisposition(contentDisposition);
return ResponseEntity.ok().contentLength(finalZipFile.length()).headers(headers).contentType(MediaType.APPLICATION_OCTET_STREAM).header("filename", finalZipFile.getName())
.body(resource);
}

private void checkDataExportCanBeDownloaded(DataExport dataExport) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import { Subject } from 'rxjs';
import { ButtonSize, ButtonType } from 'app/shared/components/button.component';
import { DataExportService } from 'app/core/legal/data-export/data-export.service';
import { AccountService } from 'app/core/auth/account.service';
import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
import { HttpErrorResponse } from '@angular/common/http';
import { AlertService } from 'app/core/util/alert.service';
import { downloadZipFileFromResponse } from 'app/shared/util/download.util';
import { DataExport, DataExportState } from 'app/entities/data-export.model';
import { ActivatedRoute } from '@angular/router';
import { convertDateFromServer } from 'app/utils/date.utils';
Expand Down Expand Up @@ -94,9 +93,6 @@ export class DataExportComponent implements OnInit {
}

downloadDataExport() {
this.dataExportService.downloadDataExport(this.dataExportId).subscribe((response: HttpResponse<Blob>) => {
downloadZipFileFromResponse(response);
this.alertService.success('artemisApp.dataExport.downloadSuccess');
tobias-lippert marked this conversation as resolved.
Show resolved Hide resolved
});
this.dataExportService.downloadDataExport(this.dataExportId);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HttpClient, HttpResponse } from '@angular/common/http';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { DataExport } from 'app/entities/data-export.model';
Expand All @@ -11,11 +11,9 @@ export class DataExportService {
return this.http.post<DataExport>(`api/data-exports`, {});
}

downloadDataExport(dataExportId: number): Observable<HttpResponse<Blob>> {
return this.http.get(`api/data-exports/${dataExportId}`, {
observe: 'response',
responseType: 'blob',
});
downloadDataExport(dataExportId: number) {
const url = `api/data-exports/${dataExportId}`;
window.open(url, '_blank');
}

canRequestDataExport(): Observable<boolean> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,25 @@

import javax.validation.constraints.NotNull;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ResourceUtils;

import com.opencsv.CSVReader;

import de.tum.in.www1.artemis.domain.*;
import de.tum.in.www1.artemis.domain.exam.Exam;
import de.tum.in.www1.artemis.repository.GradingScaleRepository;

/**
* Service responsible for initializing the database with specific testdata related to grading for use in integration tests.
*/
@Service
public class GradingScaleUtilService {

@Autowired
private GradingScaleRepository gradingScaleRepository;

@NotNull
public Set<GradeStep> generateGradeStepSet(GradingScale gradingScale, boolean valid) {
GradeStep gradeStep1 = new GradeStep();
Expand Down Expand Up @@ -64,6 +70,13 @@ public GradingScale generateGradingScale(int gradeStepCount, double[] intervals,
return gradingScale;
}

public GradingScale generateAndSaveGradingScale(int gradeStepCount, double[] intervals, boolean lowerBoundInclusivity, int firstPassingIndex, Optional<String[]> gradeNames,
Exam exam) {
GradingScale gradingScale = generateGradingScale(gradeStepCount, intervals, lowerBoundInclusivity, firstPassingIndex, gradeNames);
gradingScale.setExam(exam);
return gradingScaleRepository.save(gradingScale);
}

public GradingScale generateGradingScale(int gradeStepCount, double[] intervals, boolean lowerBoundInclusivity, int firstPassingIndex, Optional<String[]> gradeNames) {
if (gradeStepCount != intervals.length - 1 || firstPassingIndex >= gradeStepCount || firstPassingIndex < 0) {
fail("Invalid grading scale parameters");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.springframework.stereotype.Service;

import de.tum.in.www1.artemis.assessment.ComplaintUtilService;
import de.tum.in.www1.artemis.assessment.GradingScaleUtilService;
import de.tum.in.www1.artemis.competency.CompetencyUtilService;
import de.tum.in.www1.artemis.domain.*;
import de.tum.in.www1.artemis.domain.enumeration.*;
Expand Down Expand Up @@ -138,6 +139,9 @@ public class CourseUtilService {
@Autowired
private ComplaintUtilService complaintUtilService;

@Autowired
private GradingScaleUtilService gradingScaleUtilService;

public Course createCourse() {
return createCourse(null);
}
Expand Down Expand Up @@ -878,11 +882,12 @@ public void updateCourseGroups(String userPrefix, Course course, String suffix)
courseRepo.save(course);
}

public Course createCourseWithCustomStudentUserGroupWithExamAndExerciseGroupAndExercises(User user, String studentGroupName, String shortName, boolean withProgrammingExercise,
boolean withAllQuizQuestionTypes) {
public Course createCourseWithCustomStudentUserGroupWithExamAndExerciseGroupAndExercisesAndGradingScale(User user, String studentGroupName, String shortName,
boolean withProgrammingExercise, boolean withAllQuizQuestionTypes) {
Course course = createCourseWithCustomStudentGroupName(studentGroupName, shortName);
Exam exam = examUtilService.addExam(course, user, ZonedDateTime.now().minusMinutes(10), ZonedDateTime.now().minusMinutes(5), ZonedDateTime.now().minusMinutes(2),
ZonedDateTime.now().minusMinutes(1));
gradingScaleUtilService.generateAndSaveGradingScale(2, new double[] { 0, 50, 100 }, true, 1, Optional.empty(), exam);
tobias-lippert marked this conversation as resolved.
Show resolved Hide resolved
course.addExam(exam);
examUtilService.addExerciseGroupsAndExercisesToExam(exam, withProgrammingExercise, withAllQuizQuestionTypes);
return courseRepo.save(course);
Expand Down
Loading