From 53a143e6e4f2fa9508a6c76f52d1e72af5170274 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:49:53 +0200 Subject: [PATCH 1/5] Added copyCourse method to commonDatabaseActions --- .../pidgeon/util/CommonDatabaseActions.java | 190 ++++++++++++++++++ .../com/ugent/pidgeon/util/Filehandler.java | 28 +++ 2 files changed, 218 insertions(+) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java b/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java index f254eff4..6cd077ed 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java @@ -3,6 +3,12 @@ import com.ugent.pidgeon.postgre.models.*; import com.ugent.pidgeon.postgre.repository.*; +import java.nio.file.Path; +import java.time.OffsetDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -30,6 +36,10 @@ public class CommonDatabaseActions { private TestRepository testRepository; @Autowired private FileUtil fileUtil; + @Autowired + private FileRepository fileRepository; + @Autowired + private CourseRepository courseRepository; /** @@ -192,4 +202,184 @@ public CheckResult deleteClusterById(long clusterId) { return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while deleting cluster", null); } } + + + /** + * Copy a course and all its related data. Assumes that permissions are already checked + * @param course course to copy + * @return CheckResult with the status of the copy and the new course + */ + public CheckResult copyCourse(CourseEntity course) { + // Copy the course + CourseEntity newCourse = new CourseEntity(course.getName(), course.getDescription()); + // Change the createdAt, archivedAt and joinKey + newCourse.setCreatedAt(OffsetDateTime.now()); + newCourse.setArchivedAt(null); + newCourse.setJoinKey(UUID.randomUUID().toString()); + + newCourse = courseRepository.save(newCourse); + + Map groupClusterMap = new HashMap<>(); + // Copy the group(clusters) linked to the course + GroupClusterEntity groupCluster = groupClusterRepository.findIndividualClusterByCourseId( + course.getId()).orElse(null); + if (groupCluster != null) { + CheckResult checkResult = copyGroupCluster(groupCluster, newCourse.getId(), false); + if (!checkResult.getStatus().equals(HttpStatus.OK)) { + return new CheckResult<>(checkResult.getStatus(), checkResult.getMessage(), null); + } + groupClusterMap.put(groupCluster.getId(), checkResult.getData().getId()); + } else { + return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while copying course", null); + } + + List groupClusters = groupClusterRepository.findClustersWithoutInvidualByCourseId(course.getId()); + for (GroupClusterEntity cluster : groupClusters) { + CheckResult checkResult = copyGroupCluster(cluster, + newCourse.getId(), true); + if (!checkResult.getStatus().equals(HttpStatus.OK)) { + return new CheckResult<>(checkResult.getStatus(), checkResult.getMessage(), null); + } + groupClusterMap.put(cluster.getId(), checkResult.getData().getId()); + } + + // Copy the projects linked to the course + List projects = projectRepository.findByCourseId(course.getId()); + for (ProjectEntity project : projects) { + CheckResult checkResult = copyProject(project, newCourse.getId(), groupClusterMap.get(project.getGroupClusterId())); + if (!checkResult.getStatus().equals(HttpStatus.OK)) { + return new CheckResult<>(checkResult.getStatus(), checkResult.getMessage(), null); + } + } + + return new CheckResult<>(HttpStatus.OK, "", newCourse); + } + + /** + * Copy a group cluster and all its related data. Assumes that permissions are already checked + * @param groupCluster group cluster that needs to be copied + * @return CheckResult with the status of the copy and the new group cluster + */ + public CheckResult copyGroupCluster(GroupClusterEntity groupCluster, long courseId, boolean copyGroups) { + GroupClusterEntity newGroupCluster = new GroupClusterEntity( + courseId, + groupCluster.getMaxSize(), + groupCluster.getName(), + groupCluster.getGroupAmount() + ); + + newGroupCluster = groupClusterRepository.save(newGroupCluster); + if (copyGroups) { + List groups = groupRepository.findAllByClusterId(groupCluster.getId()); + for (GroupEntity group : groups) { + GroupEntity newGroup = new GroupEntity(group.getName(), newGroupCluster.getId()); + groupRepository.save(newGroup); + } + } + + return new CheckResult<>(HttpStatus.OK, "", newGroupCluster); + } + + + + /** + * Copy a project and all its related data. Assumes that permissions are already checked + * @param project project that needs to be copied + * @param courseId id of the course the project is linked to + * @param clusterId id of the cluster the project is linked to + * @return CheckResult with the status of the copy and the new project + */ + public CheckResult copyProject(ProjectEntity project, long courseId, long clusterId) { + // Copy the project + ProjectEntity newProject = new ProjectEntity( + courseId, + project.getName(), + project.getDescription(), + clusterId, + null, + project.isVisible(), + project.getMaxScore(), + project.getDeadline()); + + newProject = projectRepository.save(newProject); + + + // Copy the test linked to the project + if (project.getTestId() != null) { + TestEntity test = testRepository.findById(project.getTestId()).orElse(null); + if (test != null) { + CheckResult checkResult = copyTest(test, newProject.getId()); + if (!checkResult.getStatus().equals(HttpStatus.OK)) { + return new CheckResult<>(checkResult.getStatus(), checkResult.getMessage(), null); + } + newProject.setTestId(checkResult.getData().getId()); + newProject = projectRepository.save(newProject); + } else { + return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while copying project", null); + } + } + + + return new CheckResult<>(HttpStatus.OK, "", newProject); + } + + /** + * Copy a test and all its related data. Assumes that permissions are already checked + * @param test test that needs to be copied + * @param projectId id of the project the test is linked to + * @return CheckResult with the status of the copy and the new test + */ + public CheckResult copyTest(TestEntity test, long projectId) { + // Copy the test + TestEntity newTest = new TestEntity( + test.getDockerImage(), + test.getDockerTestId(), + test.getStructureTestId() + ); + + // Copy the files linked to the test + try { + FileEntity dockerFile = fileRepository.findById(test.getDockerTestId()).orElse(null); + FileEntity structureFile = fileRepository.findById(test.getStructureTestId()).orElse(null); + if (dockerFile == null || structureFile == null) { + return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while copying test", null); + } + + CheckResult copyDockRes = copyTestFile(dockerFile, projectId); + if (!copyDockRes.getStatus().equals(HttpStatus.OK)) { + return new CheckResult<>(copyDockRes.getStatus(), copyDockRes.getMessage(), null); + } + newTest.setDockerTestId(copyDockRes.getData().getId()); + + CheckResult copyStructRes = copyTestFile(structureFile, projectId); + if (!copyStructRes.getStatus().equals(HttpStatus.OK)) { + return new CheckResult<>(copyStructRes.getStatus(), copyStructRes.getMessage(), null); + } + newTest.setStructureTestId(copyStructRes.getData().getId()); + } catch (Exception e) { + return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while copying test", null); + } + + newTest = testRepository.save(newTest); + return new CheckResult<>(HttpStatus.OK, "", newTest); + } + + + /** + * Copy a file and all its related data. Assumes that permissions are already checked + * @param file file to copy + * @param projectId id of the project the file is linked to + * @return CheckResult with the status of the copy and the new file + */ + public CheckResult copyTestFile(FileEntity file, long projectId) { + // Copy the file + try { + Path newPath = Filehandler.copyTest(Path.of(file.getPath()), projectId); + FileEntity newFile = new FileEntity(newPath.getFileName().toString(), newPath.toString(), file.getUploadedBy()); + newFile = fileRepository.save(newFile); + return new CheckResult<>(HttpStatus.OK, "", newFile); + } catch (Exception e) { + return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while copying file", null); + } + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java b/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java index 415ab5e7..3051422a 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java @@ -197,6 +197,34 @@ public static Path saveTest(MultipartFile file, long projectId) throws IOExcepti return filePath; } + /** + * Copy a file to the server project directory. + * @param sourceFilePath the path of the file to copy + * @param projectId the ID of the project + * @return the path of the copied file + * @throws IOException if an error occurs while copying the file + */ + public static Path copyTest(Path sourceFilePath, long projectId) throws IOException { + // Check if the source file exists + if (!Files.exists(sourceFilePath)) { + throw new IOException("Source file does not exist"); + } + + // Create project directory if it doesn't exist + Path projectDirectory = getTestPath(projectId); + if (!Files.exists(projectDirectory)) { + Files.createDirectories(projectDirectory); + } + + // Resolve destination file path + Path destinationFilePath = projectDirectory.resolve(sourceFilePath.getFileName()); + + // Copy the file to the project directory + Files.copy(sourceFilePath, destinationFilePath); + + return destinationFilePath; + } + /** * Get the structure test file contents as string * @param path path of the structure test file From a709c22af6ea5545c5e8af4c8a294b3b87581e47 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Fri, 26 Apr 2024 16:54:11 +0200 Subject: [PATCH 2/5] Added some more specifiek error handling --- .../com/ugent/pidgeon/GlobalErrorHandler.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/GlobalErrorHandler.java b/backend/app/src/main/java/com/ugent/pidgeon/GlobalErrorHandler.java index c7bfbdb1..715598cc 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/GlobalErrorHandler.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/GlobalErrorHandler.java @@ -6,13 +6,17 @@ import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; +import org.apache.http.MethodNotSupportedException; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.resource.NoResourceFoundException; @@ -45,6 +49,24 @@ public ResponseEntity handleHttpMessageNotFoundException(HttpSe "Endpoint doesn't exist", path)); } + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity handleMethodNotSupportedException(HttpServletRequest request, Exception ex) { + logError(ex); + String path = request.getRequestURI(); + HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED; + return ResponseEntity.status(status).body(new ApiErrorReponse(OffsetDateTime.now(), status.value(), status.getReasonPhrase(), + "Method not supported", path)); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity handleMethodArgumentTypeMismatchException(HttpServletRequest request, Exception ex) { + logError(ex); + String path = request.getRequestURI(); + HttpStatus status = HttpStatus.BAD_REQUEST; + return ResponseEntity.status(status).body(new ApiErrorReponse(OffsetDateTime.now(), status.value(), status.getReasonPhrase(), + "Invalid url argument type", path)); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleException(HttpServletRequest request, Exception ex) { logError(ex); From cfd16d810e39dbd3bb4c04191f30b48dd79401ab Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Fri, 26 Apr 2024 18:07:45 +0200 Subject: [PATCH 3/5] Copying courses works --- .../pidgeon/controllers/CourseController.java | 28 +++++++++++++++++-- .../pidgeon/util/CommonDatabaseActions.java | 18 +++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java index 8a171c56..043ffd82 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java @@ -268,8 +268,6 @@ public ResponseEntity deleteCourse(@PathVariable long courseId, Auth auth) { } List clusters = groupClusterRepository.findByCourseId(courseId); - Optional individualCluster = groupClusterRepository.findIndividualClusterByCourseId(courseId); - individualCluster.ifPresent(clusters::add); // Delete all groupclusters linked to the course for (GroupClusterEntity groupCluster : clusters) { @@ -679,4 +677,30 @@ public ResponseEntity deleteCourseKey(Auth auth, @PathVariable Long cour return ResponseEntity.ok(""); } + @PostMapping(ApiRoutes.COURSE_BASE_PATH + "/{courseId}/copy") + @Roles({UserRole.teacher}) + @Transactional + public ResponseEntity copyCourse(@PathVariable long courseId, Auth auth) { + try { + CheckResult> checkResult = courseUtil.getCourseIfUserInCourse(courseId, auth.getUserEntity()); + if (checkResult.getStatus() != HttpStatus.OK) { + return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); + } + if (!checkResult.getData().getSecond().equals(CourseRelation.creator)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Only the creator of a course can delete it"); + } + + CourseEntity course = checkResult.getData().getFirst(); + + CheckResult copyCheckRes = commonDatabaseActions.copyCourse(course, auth.getUserEntity().getId()); + CourseEntity newCourse = copyCheckRes.getData(); + + return ResponseEntity.ok(entityToJsonConverter.courseEntityToCourseWithInfo(newCourse, courseUtil.getJoinLink(newCourse.getJoinKey(), "" + newCourse.getId()), false)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + + } \ No newline at end of file diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java b/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java index 6cd077ed..1d5f48af 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java @@ -2,6 +2,7 @@ import com.ugent.pidgeon.postgre.models.*; +import com.ugent.pidgeon.postgre.models.types.CourseRelation; import com.ugent.pidgeon.postgre.repository.*; import java.nio.file.Path; import java.time.OffsetDateTime; @@ -9,6 +10,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import org.hibernate.annotations.Check; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -40,6 +42,8 @@ public class CommonDatabaseActions { private FileRepository fileRepository; @Autowired private CourseRepository courseRepository; + @Autowired + private CourseUserRepository courseUserRepository; /** @@ -134,9 +138,12 @@ public CheckResult deleteProject(long projectId) { if (testEntity == null) { return new CheckResult<>(HttpStatus.NOT_FOUND, "Test not found", null); } - return deleteTestById(projectEntity, testEntity); + CheckResult delRes = deleteTestById(projectEntity, testEntity); + return delRes; } + + return new CheckResult<>(HttpStatus.OK, "", null); } catch (Exception e) { System.out.println(e.getMessage()); @@ -171,8 +178,6 @@ public CheckResult deleteSubmissionById(long submissionId) { */ public CheckResult deleteTestById(ProjectEntity projectEntity, TestEntity testEntity) { try { - projectEntity.setTestId(null); - projectRepository.save(projectEntity); testRepository.deleteById(testEntity.getId()) ; CheckResult checkAndDeleteRes = fileUtil.deleteFileById(testEntity.getStructureTestId()); if (!checkAndDeleteRes.getStatus().equals(HttpStatus.OK)) { @@ -209,7 +214,7 @@ public CheckResult deleteClusterById(long clusterId) { * @param course course to copy * @return CheckResult with the status of the copy and the new course */ - public CheckResult copyCourse(CourseEntity course) { + public CheckResult copyCourse(CourseEntity course, long userId) { // Copy the course CourseEntity newCourse = new CourseEntity(course.getName(), course.getDescription()); // Change the createdAt, archivedAt and joinKey @@ -252,6 +257,10 @@ public CheckResult copyCourse(CourseEntity course) { } } + // Add user to course + CourseUserEntity courseUserEntity = new CourseUserEntity(newCourse.getId(), userId, CourseRelation.creator); + courseUserRepository.save(courseUserEntity); + return new CheckResult<>(HttpStatus.OK, "", newCourse); } @@ -267,6 +276,7 @@ public CheckResult copyGroupCluster(GroupClusterEntity group groupCluster.getName(), groupCluster.getGroupAmount() ); + newGroupCluster.setCreatedAt(OffsetDateTime.now()); newGroupCluster = groupClusterRepository.save(newGroupCluster); if (copyGroups) { From 498edda4e14254ef5e04815f243fa37233d88846 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Sat, 27 Apr 2024 08:56:00 +0200 Subject: [PATCH 4/5] Copy course year --- .../main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java b/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java index 1d5f48af..d05bf63e 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java @@ -216,7 +216,7 @@ public CheckResult deleteClusterById(long clusterId) { */ public CheckResult copyCourse(CourseEntity course, long userId) { // Copy the course - CourseEntity newCourse = new CourseEntity(course.getName(), course.getDescription()); + CourseEntity newCourse = new CourseEntity(course.getName(), course.getDescription(), course.getCourseYear()); // Change the createdAt, archivedAt and joinKey newCourse.setCreatedAt(OffsetDateTime.now()); newCourse.setArchivedAt(null); From c17a2b8cd977b56a6b1a68c85553c7d83ee9fc21 Mon Sep 17 00:00:00 2001 From: Inti Danschutter <108478185+Aqua-sc@users.noreply.github.com> Date: Sun, 28 Apr 2024 12:27:11 +0200 Subject: [PATCH 5/5] ~delete~ -> copy in 403 response for /copy --- .../java/com/ugent/pidgeon/controllers/CourseController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java index b0148065..747ce82f 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java @@ -691,7 +691,7 @@ public ResponseEntity copyCourse(@PathVariable long courseId, Auth auth) { return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } if (!checkResult.getData().getSecond().equals(CourseRelation.creator)) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Only the creator of a course can delete it"); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Only the creator of a course can copy it"); } CourseEntity course = checkResult.getData().getFirst(); @@ -707,4 +707,4 @@ public ResponseEntity copyCourse(@PathVariable long courseId, Auth auth) { -} \ No newline at end of file +}