diff --git a/src/main/java/com/testify/Testify_Backend/controller/ExamManagementController.java b/src/main/java/com/testify/Testify_Backend/controller/ExamManagementController.java index 5d8f02c..c93aa85 100644 --- a/src/main/java/com/testify/Testify_Backend/controller/ExamManagementController.java +++ b/src/main/java/com/testify/Testify_Backend/controller/ExamManagementController.java @@ -83,6 +83,14 @@ public ResponseEntity getAllQuestionsByExamId(@PathVariabl return examManagementService.getAllQuestionsByExamId(examId); } + @GetMapping("/{examId}/questions/{sessionId}/answers") + public ResponseEntity getAllQuestionsAnswersByExamId( + @PathVariable long examId, + @PathVariable long sessionId) { + + return examManagementService.getAllQuestionsAnswersByExamId(examId, sessionId); + } + @PutMapping("/question/{questionId}") public ResponseEntity> deleteQuestion(@PathVariable long questionId){ return examManagementService.deleteQuestion(questionId); @@ -153,6 +161,13 @@ public ResponseEntity saveAnswer(@RequestBody SaveAnswerRequest request) } catch (Exception e) { return new ResponseEntity<>("Error saving answer: " + e.getMessage(), HttpStatus.BAD_REQUEST); } + } + + @PutMapping("/{sessionId}/submit") + public ResponseEntity submitExam(@PathVariable Long sessionId) { + examManagementService.markSessionAsComplete(sessionId); + return ResponseEntity.ok("Exam submitted successfully."); + } } @PostMapping("/{examId}/proctors") diff --git a/src/main/java/com/testify/Testify_Backend/controller/GradingController.java b/src/main/java/com/testify/Testify_Backend/controller/GradingController.java new file mode 100644 index 0000000..33bcdff --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/controller/GradingController.java @@ -0,0 +1,50 @@ +package com.testify.Testify_Backend.controller; + +import com.testify.Testify_Backend.model.CandidateExamSession; +import com.testify.Testify_Backend.model.Grade; +import com.testify.Testify_Backend.responses.EssayDetailsResponse; +import com.testify.Testify_Backend.responses.McqDetailsResponse; +import com.testify.Testify_Backend.service.GradingService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/grade") +@RequiredArgsConstructor +public class GradingController { + + private final GradingService gradingService; + + @GetMapping("/{examId}/users/{userId}/essay-details") + public ResponseEntity> getEssayDetails( + @PathVariable Long examId, + @PathVariable Long userId) { + List response = gradingService.getEssayDetails(examId, userId); + return ResponseEntity.ok(response); + } + + @GetMapping("/{examId}/grading-scheme") + public ResponseEntity> getGradingScheme(@PathVariable Long examId) { + List grades = gradingService.getGradingSchemeForExam(examId); + return ResponseEntity.ok(grades); + } + + @GetMapping("/{sessionId}/mcq-details") + public ResponseEntity>> getResultsBySessionId( + @PathVariable Long sessionId) { + + List> results = gradingService.getQuestionAndOptionBySessionId(sessionId); + + return ResponseEntity.ok(results); + } + + +} diff --git a/src/main/java/com/testify/Testify_Backend/enums/ExamStatus.java b/src/main/java/com/testify/Testify_Backend/enums/ExamStatus.java index 0d2ee5c..7382c79 100644 --- a/src/main/java/com/testify/Testify_Backend/enums/ExamStatus.java +++ b/src/main/java/com/testify/Testify_Backend/enums/ExamStatus.java @@ -3,5 +3,6 @@ public enum ExamStatus { UPCOMING, ONGOING, - EXPIRED + AVAILABLE, + COMPLETED, EXPIRED } diff --git a/src/main/java/com/testify/Testify_Backend/model/EssayAnswer.java b/src/main/java/com/testify/Testify_Backend/model/EssayAnswer.java index 98a0afd..d38b180 100644 --- a/src/main/java/com/testify/Testify_Backend/model/EssayAnswer.java +++ b/src/main/java/com/testify/Testify_Backend/model/EssayAnswer.java @@ -13,5 +13,5 @@ public class EssayAnswer extends CandidateExamAnswer { @Column(name = "answer_text") - private String answerText; // The text answer for the essay question + private String answerText; } diff --git a/src/main/java/com/testify/Testify_Backend/model/Exam.java b/src/main/java/com/testify/Testify_Backend/model/Exam.java index 8289136..63472bd 100644 --- a/src/main/java/com/testify/Testify_Backend/model/Exam.java +++ b/src/main/java/com/testify/Testify_Backend/model/Exam.java @@ -1,4 +1,5 @@ package com.testify.Testify_Backend.model; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.testify.Testify_Backend.converter.QuestionSequenceConverter; import com.testify.Testify_Backend.enums.ExamType; import com.testify.Testify_Backend.enums.OrderType; @@ -101,6 +102,7 @@ public class Exam { private List questionSequence; @OneToMany(mappedBy = "exam", cascade = CascadeType.ALL) + @JsonIgnore private List gradings; @Column(nullable = false) diff --git a/src/main/java/com/testify/Testify_Backend/model/Grade.java b/src/main/java/com/testify/Testify_Backend/model/Grade.java index 494a885..5d27d8a 100644 --- a/src/main/java/com/testify/Testify_Backend/model/Grade.java +++ b/src/main/java/com/testify/Testify_Backend/model/Grade.java @@ -1,5 +1,6 @@ package com.testify.Testify_Backend.model; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; @@ -24,6 +25,7 @@ public class Grade { @ManyToOne @JoinColumn(name = "exam_id") + @JsonIgnore private Exam exam; } diff --git a/src/main/java/com/testify/Testify_Backend/model/MCQAnswer.java b/src/main/java/com/testify/Testify_Backend/model/MCQAnswer.java index 410a541..5a6e3a5 100644 --- a/src/main/java/com/testify/Testify_Backend/model/MCQAnswer.java +++ b/src/main/java/com/testify/Testify_Backend/model/MCQAnswer.java @@ -13,6 +13,6 @@ public class MCQAnswer extends CandidateExamAnswer { @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "option_id") + @JoinColumn(name = "option_id", nullable = true) private MCQOption option; // Reference to the selected Option } diff --git a/src/main/java/com/testify/Testify_Backend/repository/CandidateExamAnswerRepository.java b/src/main/java/com/testify/Testify_Backend/repository/CandidateExamAnswerRepository.java index fefdde6..261ab27 100644 --- a/src/main/java/com/testify/Testify_Backend/repository/CandidateExamAnswerRepository.java +++ b/src/main/java/com/testify/Testify_Backend/repository/CandidateExamAnswerRepository.java @@ -1,10 +1,30 @@ package com.testify.Testify_Backend.repository; import com.testify.Testify_Backend.model.CandidateExamAnswer; +import com.testify.Testify_Backend.model.CandidateExamSession; +import com.testify.Testify_Backend.model.MCQOption; +import com.testify.Testify_Backend.model.Question; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + @Repository public interface CandidateExamAnswerRepository extends JpaRepository { + Optional findByCandidateExamSessionIdAndQuestionId (Long sessionId, Long questionId); + @Query("SELECT e.answerText FROM EssayAnswer e WHERE e.id = :id") + String findEssayAnswerTextById(@Param("id") Long id); + + @Query("SELECT e.option FROM MCQAnswer e WHERE e.id = :id") + MCQOption findMcqAnswerTextById(@Param("id") Long id); + + List findByCandidateExamSessionId(Long sessionId); + + + + CandidateExamAnswer findByCandidateExamSessionIdAndQuestionId(Long candidateExamSession_id, long question_id); } diff --git a/src/main/java/com/testify/Testify_Backend/repository/ExamSessionRepository.java b/src/main/java/com/testify/Testify_Backend/repository/ExamSessionRepository.java index 1f97437..649aec4 100644 --- a/src/main/java/com/testify/Testify_Backend/repository/ExamSessionRepository.java +++ b/src/main/java/com/testify/Testify_Backend/repository/ExamSessionRepository.java @@ -9,5 +9,6 @@ @Repository public interface ExamSessionRepository extends JpaRepository { Optional findByCandidateIdAndExamIdAndInProgress(Long candidateId, Long examId, Boolean inProgress); + Optional findByExamIdAndCandidateId(Long examId, Long candidateId); } diff --git a/src/main/java/com/testify/Testify_Backend/repository/GradeRepository.java b/src/main/java/com/testify/Testify_Backend/repository/GradeRepository.java index 5717338..0b785f6 100644 --- a/src/main/java/com/testify/Testify_Backend/repository/GradeRepository.java +++ b/src/main/java/com/testify/Testify_Backend/repository/GradeRepository.java @@ -5,9 +5,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import java.util.List; +@Repository public interface GradeRepository extends JpaRepository { @Query("SELECT g FROM Grade g WHERE g.exam.id = :examId") List findByExamId(@Param("examId") Long examId); diff --git a/src/main/java/com/testify/Testify_Backend/repository/QuestionRepository.java b/src/main/java/com/testify/Testify_Backend/repository/QuestionRepository.java index e531141..1145c77 100644 --- a/src/main/java/com/testify/Testify_Backend/repository/QuestionRepository.java +++ b/src/main/java/com/testify/Testify_Backend/repository/QuestionRepository.java @@ -1,14 +1,22 @@ package com.testify.Testify_Backend.repository; +import com.testify.Testify_Backend.model.Essay; import com.testify.Testify_Backend.model.Question; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; +@Repository public interface QuestionRepository extends JpaRepository{ @Query("SELECT q FROM Question q WHERE q.exam.id = :examId AND q.isDeleted = false") List findAllActiveQuestionsByExamId(@Param("examId") long examId); List findByExamId(Long examId); + + @Query("SELECT e FROM Essay e WHERE e.exam.id = :examId") + List findAllEssaysByExamId(@Param("examId") Long examId); + } diff --git a/src/main/java/com/testify/Testify_Backend/responses/EssayDetailsResponse.java b/src/main/java/com/testify/Testify_Backend/responses/EssayDetailsResponse.java new file mode 100644 index 0000000..9b22797 --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/responses/EssayDetailsResponse.java @@ -0,0 +1,22 @@ +package com.testify.Testify_Backend.responses; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class EssayDetailsResponse { + private String questionText; + private List coverPoints; + private String userAnswer; + + @Data + @Builder + public static class CoverPointDto { + private String coverPointText; + private double marks; + } +} + diff --git a/src/main/java/com/testify/Testify_Backend/responses/McqDetailsResponse.java b/src/main/java/com/testify/Testify_Backend/responses/McqDetailsResponse.java new file mode 100644 index 0000000..2c4e0e2 --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/responses/McqDetailsResponse.java @@ -0,0 +1,27 @@ +package com.testify.Testify_Backend.responses; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class McqDetailsResponse { + private Long questionId; + private String questionText; + private String questionType; + private String difficultyLevel; // Could be nullable if not required + private List options; + private Long userAnswer; + + @Data + @Builder + public static class OptionResponse { + + private Long optionId; + private String optionText; + private boolean correct; + private Double marks; + } +} diff --git a/src/main/java/com/testify/Testify_Backend/service/CandidateService.java b/src/main/java/com/testify/Testify_Backend/service/CandidateService.java index b67b7f7..840874e 100644 --- a/src/main/java/com/testify/Testify_Backend/service/CandidateService.java +++ b/src/main/java/com/testify/Testify_Backend/service/CandidateService.java @@ -10,9 +10,7 @@ import java.util.List; public interface CandidateService { - List getAllCandidatesForSearch(); - // temp comment public List getCandidateExams(String status); public CandidateExam getCandidateExamDetails(Integer examId); diff --git a/src/main/java/com/testify/Testify_Backend/service/CandidateServiceImpl.java b/src/main/java/com/testify/Testify_Backend/service/CandidateServiceImpl.java index 879c005..0d4af1c 100644 --- a/src/main/java/com/testify/Testify_Backend/service/CandidateServiceImpl.java +++ b/src/main/java/com/testify/Testify_Backend/service/CandidateServiceImpl.java @@ -2,12 +2,10 @@ import com.testify.Testify_Backend.enums.ExamStatus; import com.testify.Testify_Backend.model.Candidate; +import com.testify.Testify_Backend.model.CandidateExamSession; import com.testify.Testify_Backend.model.Exam; import com.testify.Testify_Backend.model.Organization; -import com.testify.Testify_Backend.repository.CandidateRepository; -import com.testify.Testify_Backend.repository.ExamRepository; -import com.testify.Testify_Backend.repository.OrganizationRepository; -import com.testify.Testify_Backend.repository.UserRepository; +import com.testify.Testify_Backend.repository.*; import com.testify.Testify_Backend.responses.GenericResponse; import com.testify.Testify_Backend.responses.candidate_management.CandidateExam; import com.testify.Testify_Backend.responses.candidate_management.CandidateResponse; @@ -55,18 +53,23 @@ public class CandidateServiceImpl implements CandidateService { @Autowired private GenericResponse genericResponse; + @Autowired + private final ExamSessionRepository examSessionRepository; + @Override public List getCandidateExams(String status) { - // Get the current user's email (you can adapt this to your actual method of getting the logged-in user's email) - String currentUserEmail = UserUtil.getCurrentUserName(); - log.info("Current user email: {}", currentUserEmail); + String currentUserEmail = SecurityContextHolder.getContext().getAuthentication().getName(); + + if (currentUserEmail == null) { + throw new IllegalStateException("No authenticated user found"); + } Candidate candidate = candidateRepository.findByEmail(currentUserEmail) .orElseThrow(() -> new EntityNotFoundException("Candidate not found")); - // Get exams directly associated with the candidate + List candidateExams = examRepository.findExamsByCandidateId(candidate.getId()); - // Get all public exams + List publicExams = examRepository.findAllPublicExams(); // Combine both lists, ensuring no duplicates @@ -81,10 +84,11 @@ public List getCandidateExams(String status) { // Filter exams based on the provided status return allExams.stream() .map(exam -> { - return getCandidateExam(exam, now); + CandidateExamSession candidateExamSession = examSessionRepository.findByExamIdAndCandidateId(exam.getId(), candidate.getId()) + .orElse(null); + return getCandidateExam(exam, now, candidateExamSession); }) .filter(candidateExam -> { - // Apply the status filter if (status == null) { return true; } @@ -97,7 +101,12 @@ public List getCandidateExams(String status) { @Override public CandidateExam getCandidateExamDetails(Integer examId) { - String currentUserEmail = UserUtil.getCurrentUserName(); + String currentUserEmail = SecurityContextHolder.getContext().getAuthentication().getName(); + + if (currentUserEmail == null) { + throw new IllegalStateException("No authenticated user found"); + } + Candidate candidate = candidateRepository.findByEmail(currentUserEmail) .orElseThrow(() -> new EntityNotFoundException("Candidate not found")); @@ -118,19 +127,25 @@ public CandidateExam getCandidateExamDetails(Integer examId) { // Determine the status of the exam LocalDateTime now = LocalDateTime.now(); - return getCandidateExam(exam, now); + CandidateExamSession candidateExamSession = examSessionRepository.findByExamIdAndCandidateId(exam.getId(), candidate.getId()) + .orElse(null); + return getCandidateExam(exam, now, candidateExamSession); } @NotNull - private CandidateExam getCandidateExam(Exam exam, LocalDateTime now) { + private CandidateExam getCandidateExam(Exam exam, LocalDateTime now, CandidateExamSession session) { CandidateExam candidateExam = modelMapper.map(exam, CandidateExam.class); if (now.isBefore(exam.getStartDatetime())) { candidateExam.setStatus(ExamStatus.UPCOMING); - } else if (now.isAfter(exam.getEndDatetime())) { + } else if (session == null && now.isAfter(exam.getEndDatetime())) { candidateExam.setStatus(ExamStatus.EXPIRED); - } else { + } else if (session == null) { + candidateExam.setStatus(ExamStatus.AVAILABLE); + } else if (session.getInProgress()) { candidateExam.setStatus(ExamStatus.ONGOING); + } else { + candidateExam.setStatus(ExamStatus.COMPLETED); } return candidateExam; diff --git a/src/main/java/com/testify/Testify_Backend/service/ExamManagementService.java b/src/main/java/com/testify/Testify_Backend/service/ExamManagementService.java index 3d055a3..88ba83c 100644 --- a/src/main/java/com/testify/Testify_Backend/service/ExamManagementService.java +++ b/src/main/java/com/testify/Testify_Backend/service/ExamManagementService.java @@ -16,6 +16,8 @@ public interface ExamManagementService { ExamResponse getExamById(long examId); ExamSessionResponse startExam(StartExamRequest request); void saveAnswer(Long sessionId, Long questionId, Long optionId, String answerText); + void markSessionAsComplete(Long sessionId); + ResponseEntity getAllQuestionsAnswersByExamId(long examId, long sessionId); GenericAddOrUpdateResponse updateMCQQuestion(MCQUpdateRequest mcqUpdateRequest); GenericAddOrUpdateResponse saveMCQ(MCQRequest mcqRequest); diff --git a/src/main/java/com/testify/Testify_Backend/service/ExamManagementServiceImpl.java b/src/main/java/com/testify/Testify_Backend/service/ExamManagementServiceImpl.java index 1938389..290e655 100644 --- a/src/main/java/com/testify/Testify_Backend/service/ExamManagementServiceImpl.java +++ b/src/main/java/com/testify/Testify_Backend/service/ExamManagementServiceImpl.java @@ -21,6 +21,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.ErrorResponse; import java.time.LocalDateTime; import java.util.*; @@ -45,6 +46,7 @@ public class ExamManagementServiceImpl implements ExamManagementService { private final CandidateExamAnswerRepository candidateExamAnswerRepository; private final MCQOptionRepository mcqOptionRepository; private final CandidateGroupRepository candidateGroupRepository; + private final EmailSender emailSender; private final ModelMapper modelMapper; @@ -518,6 +520,120 @@ else if (question instanceof Essay) { } } + @Override + @Transactional(readOnly = true) + public ResponseEntity getAllQuestionsAnswersByExamId(long examId, long sessionId) { + QuestionListResponse response = new QuestionListResponse(); + List questionResponses = new ArrayList<>(); + + try { + // Extract username from JWT + String currentUserEmail = SecurityContextHolder.getContext().getAuthentication().getName(); + + if (currentUserEmail == null) { + throw new IllegalStateException("No authenticated user found"); + } + + // Retrieve the session based on sessionId + Optional sessionOpt = examSessionRepository.findById(sessionId); + if (sessionOpt.isEmpty()) { + throw new IllegalStateException("Session not found"); + } + + CandidateExamSession session = sessionOpt.get(); + + // Check if the session is in progress (only proceed if it's false) + if (session.getInProgress()) { + return null; + } + + // Fetch the role of the user + String role = userRepository.findByEmail(currentUserEmail) + .map(user -> user.getRole().name()) // Convert UserRole enum to String + .orElseThrow(() -> new IllegalArgumentException("User not found")); + + // Fetch questions associated with the exam ID that are not deleted + List questions = questionRepository.findAllActiveQuestionsByExamId(examId); + Optional examOptional = examRepository.findById(examId); + + if (examOptional.isEmpty()) { + throw new IllegalArgumentException("Exam not found for ID: " + examId); + } + + System.out.println(role); + + Exam exam = examOptional.get(); + ExamType examType = exam.getExamType(); + + // Check if questions are found + if (questions.isEmpty()) { + // Return null instead of an error message + return ResponseEntity.ok(null); + } + + for (Question question : questions) { + String questionType = question instanceof MCQ ? "MCQ" : "Essay"; + + List options = null; + List coverPoints = null; + + // Populate options for MCQs + if (question instanceof MCQ) { + options = ((MCQ) question).getOptions().stream() + .map(option -> MCQOptionResponse.builder() + .optionId(option.getId()) + .optionText(option.getOptionText()) + // Exclude 'correct' flag if the user is a candidate + .correct(option.isCorrect()) + .marks(option.getMarks()) + .build()) + .collect(Collectors.toList()); + } + + // Populate cover points for essays + else if (question instanceof Essay) { + // Exclude cover points if the user is a candidate + if (!role.equals("CANDIDATE")) { + coverPoints = ((Essay) question).getCoverPoints().stream() + .map(point -> EssayCoverPointResponse.builder() + .coverPointId(point.getId()) + .coverPointText(point.getCoverPointText()) + .marks(point.getMarks()) + .build()) + .collect(Collectors.toList()); + } + } + + // Build the QuestionResponse object + QuestionResponse questionResponse = QuestionResponse.builder() + .questionId(question.getId()) + .questionText(question.getQuestionText()) + .questionType(questionType) + .options(options) + .coverPoints(coverPoints) + .build(); + + questionResponses.add(questionResponse); + } + + // Set the response with the questions + response.setExamId(examId); + response.setExamType(examType); + response.setQuestions(questionResponses); + return ResponseEntity.ok(response); // Return 200 OK with questions + + } catch (IllegalArgumentException e) { + // Handle specific exceptions + response.setErrorMessage("Invalid request: " + e.getMessage()); + return ResponseEntity.badRequest().body(response); // Return 400 Bad Request + } catch (Exception e) { + // Log unexpected errors and return a server error response + log.error("Unexpected error retrieving questions for exam ID {}: {}", examId, e.getMessage(), e); + response.setErrorMessage("Unexpected error: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); // Return 500 Internal Server Error + } + } + @@ -931,35 +1047,63 @@ public void saveAnswer(Long sessionId, Long questionId, Long optionId, String an answerText = null; } - // Handle answer saving based on question type - if (question.getType().equals(QuestionType.MCQ)) { - // Find the selected option for MCQ - MCQOption option = mcqOptionRepository.findById(optionId) - .orElseThrow(() -> new IllegalArgumentException("Invalid option ID")); - - // Create and save the MCQAnswer - MCQAnswer mcqAnswer = new MCQAnswer(); - mcqAnswer.setCandidateExamSession(session); - mcqAnswer.setQuestion(question); - mcqAnswer.setOption(option); // Option is set for MCQ - - candidateExamAnswerRepository.save(mcqAnswer); - } else if (question.getType().equals(QuestionType.ESSAY)) { - // Create and save the EssayAnswer - EssayAnswer essayAnswer = new EssayAnswer(); - essayAnswer.setCandidateExamSession(session); - essayAnswer.setQuestion(question); - - // Set essay answer text, if provided - if (answerText != null) { - essayAnswer.setAnswerText(answerText); // Only set if not null + // Check if an existing answer already exists for the given session and question + CandidateExamAnswer existingAnswer = candidateExamAnswerRepository + .findByCandidateExamSessionIdAndQuestionId(session.getId(), question.getId()); + + if (existingAnswer != null) { + // Existing answer found, update it + if (question.getType().equals(QuestionType.MCQ)) { + // Find the selected option for MCQ + MCQOption option = mcqOptionRepository.findById(optionId) + .orElseThrow(() -> new IllegalArgumentException("Invalid option ID")); + + MCQAnswer mcqAnswer = (MCQAnswer) existingAnswer; // Cast to MCQAnswer if existing answer is MCQ + mcqAnswer.setOption(option != null ? option : null); // Update the option + candidateExamAnswerRepository.save(mcqAnswer); // Save updated MCQAnswer + } else if (question.getType().equals(QuestionType.ESSAY)) { + EssayAnswer essayAnswer = (EssayAnswer) existingAnswer; // Cast to EssayAnswer if existing answer is Essay + essayAnswer.setAnswerText(answerText); // Update the answerText + candidateExamAnswerRepository.save(essayAnswer); // Save updated EssayAnswer + } else { + throw new IllegalArgumentException("Unsupported question type"); } - - candidateExamAnswerRepository.save(essayAnswer); } else { - throw new IllegalArgumentException("Unsupported question type"); + // No existing answer found, create a new one + if (question.getType().equals(QuestionType.MCQ)) { + // Find the selected option for MCQ + MCQOption option = mcqOptionRepository.findById(optionId) + .orElseThrow(() -> new IllegalArgumentException("Invalid option ID")); + + // Create and save the new MCQAnswer + MCQAnswer mcqAnswer = new MCQAnswer(); + mcqAnswer.setCandidateExamSession(session); + mcqAnswer.setQuestion(question); + mcqAnswer.setOption(option != null ? option : null); // Only set if not null + candidateExamAnswerRepository.save(mcqAnswer); + } else if (question.getType().equals(QuestionType.ESSAY)) { + // Create and save the new EssayAnswer + EssayAnswer essayAnswer = new EssayAnswer(); + essayAnswer.setCandidateExamSession(session); + essayAnswer.setQuestion(question); + essayAnswer.setAnswerText(answerText != null ? answerText : null); // Only set if not null + candidateExamAnswerRepository.save(essayAnswer); + } else { + throw new IllegalArgumentException("Unsupported question type"); + } } } + + @Override + @Transactional + public void markSessionAsComplete(Long sessionId) { + CandidateExamSession session = examSessionRepository.findById(sessionId) + .orElseThrow(() -> new IllegalArgumentException("Invalid session ID")); + + session.setInProgress(false); + examSessionRepository.save(session); + } + public ResponseEntity addProctorsToExam(long examId, List proctorEmails) { Optional optionalExam = examRepository.findById(examId); GenericAddOrUpdateResponse response = new GenericAddOrUpdateResponse(); diff --git a/src/main/java/com/testify/Testify_Backend/service/GradingService.java b/src/main/java/com/testify/Testify_Backend/service/GradingService.java new file mode 100644 index 0000000..654fae6 --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/service/GradingService.java @@ -0,0 +1,14 @@ +package com.testify.Testify_Backend.service; + +import com.testify.Testify_Backend.model.CandidateExamSession; +import com.testify.Testify_Backend.model.Grade; +import com.testify.Testify_Backend.responses.EssayDetailsResponse; + +import java.util.List; +import java.util.Map; + +public interface GradingService { + List getEssayDetails(Long examId, Long userId); + List getGradingSchemeForExam(Long examId); + List> getQuestionAndOptionBySessionId(Long sessionId); +} diff --git a/src/main/java/com/testify/Testify_Backend/service/GradingServiceImpl.java b/src/main/java/com/testify/Testify_Backend/service/GradingServiceImpl.java new file mode 100644 index 0000000..01ca17a --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/service/GradingServiceImpl.java @@ -0,0 +1,112 @@ +package com.testify.Testify_Backend.service; + +import com.testify.Testify_Backend.model.*; +import com.testify.Testify_Backend.repository.CandidateExamAnswerRepository; +import com.testify.Testify_Backend.repository.ExamSessionRepository; +import com.testify.Testify_Backend.repository.GradeRepository; +import com.testify.Testify_Backend.repository.QuestionRepository; +import com.testify.Testify_Backend.responses.EssayDetailsResponse; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class GradingServiceImpl implements GradingService { + + private final QuestionRepository questionRepository; + private final CandidateExamAnswerRepository candidateExamAnswerRepository; + private final GradeRepository gradeRepository; + private final ExamSessionRepository examSessionRepository; + + @Override + @Transactional + public List getEssayDetails(Long examId, Long userId) { + // Fetch all essay questions for the exam + List essayQuestions = questionRepository.findAllEssaysByExamId(examId); + if (essayQuestions.isEmpty()) { + throw new IllegalArgumentException("No essay questions found for the given exam ID"); + } + + // Fetch the candidate's session for the exam + CandidateExamSession candidateExamSession = examSessionRepository.findByExamIdAndCandidateId(examId, userId) + .orElseThrow(() -> new IllegalArgumentException("No session found")); + + // Process each essay question + return essayQuestions.stream().map(essayQuestion -> { + // Fetch the candidate's answer for the essay question + CandidateExamAnswer candidateAnswer = candidateExamAnswerRepository + .findByCandidateExamSessionIdAndQuestionId(candidateExamSession.getId(), essayQuestion.getId()); + + String answerText = ""; + if(candidateAnswer != null) { + answerText = candidateExamAnswerRepository.findEssayAnswerTextById(candidateAnswer.getId()); + } + + // Map the cover points + List coverPointDtos = essayQuestion.getCoverPoints().stream() + .map(cp -> EssayDetailsResponse.CoverPointDto.builder() + .coverPointText(cp.getCoverPointText()) + .marks(cp.getMarks()) + .build()) + .collect(Collectors.toList()); + + // Build the response for this essay question + return EssayDetailsResponse.builder() + .questionText(essayQuestion.getQuestionText()) + .coverPoints(coverPointDtos) + .userAnswer(answerText) + .build(); + }).collect(Collectors.toList()); + } + + + @Override + @Transactional + public List getGradingSchemeForExam(Long examId) { + List grades = gradeRepository.findByExamId(examId); + + if (grades == null || grades.isEmpty()) { + throw new IllegalArgumentException("Grading scheme not found for exam ID: " + examId); + } + + return grades; + } + + @Override + @Transactional + public List> getQuestionAndOptionBySessionId(Long sessionId) { + List answers = candidateExamAnswerRepository.findByCandidateExamSessionId(sessionId); + + // Transform the list of answers into a list of maps containing questionId and optionId + return answers.stream() + .map(answer -> { + Map result = new HashMap<>(); + result.put("questionId", String.valueOf(answer.getQuestion().getId())); + CandidateExamAnswer candidateAnswer = candidateExamAnswerRepository + .findByCandidateExamSessionIdAndQuestionId(sessionId, answer.getQuestion().getId()); + + MCQOption optionId = null; + if(candidateAnswer != null) { + optionId = candidateExamAnswerRepository.findMcqAnswerTextById(candidateAnswer.getId()); + } + if(optionId != null) { + result.put("optionId", String.valueOf(optionId.getId())); + }else{ + result.put("optionId", null); + } + + return result; + }) + .collect(Collectors.toList()); + } + + +}