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); 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 15218a66..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 @@ -271,8 +271,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) { @@ -683,5 +681,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 copy 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 f254eff4..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 @@ -2,7 +2,15 @@ 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; +import java.util.HashMap; +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; @@ -30,6 +38,12 @@ public class CommonDatabaseActions { private TestRepository testRepository; @Autowired private FileUtil fileUtil; + @Autowired + private FileRepository fileRepository; + @Autowired + private CourseRepository courseRepository; + @Autowired + private CourseUserRepository courseUserRepository; /** @@ -124,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()); @@ -161,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)) { @@ -192,4 +207,189 @@ 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, long userId) { + // Copy the course + CourseEntity newCourse = new CourseEntity(course.getName(), course.getDescription(), course.getCourseYear()); + // 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); + } + } + + // Add user to course + CourseUserEntity courseUserEntity = new CourseUserEntity(newCourse.getId(), userId, CourseRelation.creator); + courseUserRepository.save(courseUserEntity); + + 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.setCreatedAt(OffsetDateTime.now()); + + 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