From 07b08c404ca76f3ce6bc0d073be6a04b0a1e9ebc Mon Sep 17 00:00:00 2001 From: VIHANGAGIT Date: Sun, 1 Dec 2024 22:23:34 +0530 Subject: [PATCH 1/6] Session API Update mode on same question --- .../Testify_Backend/model/MCQAnswer.java | 2 +- .../CandidateExamAnswerRepository.java | 4 ++ .../service/ExamManagementServiceImpl.java | 68 ++++++++++++------- .../service/GradingServiceImpl.java | 3 +- 4 files changed, 49 insertions(+), 28 deletions(-) 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 b7e98da..46eb6b8 100644 --- a/src/main/java/com/testify/Testify_Backend/repository/CandidateExamAnswerRepository.java +++ b/src/main/java/com/testify/Testify_Backend/repository/CandidateExamAnswerRepository.java @@ -1,6 +1,8 @@ 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.Question; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -14,4 +16,6 @@ public interface CandidateExamAnswerRepository extends JpaRepository findByCandidateExamSessionIdAndQuestionId (Long sessionId, Long questionId); @Query("SELECT e.answerText FROM EssayAnswer e WHERE e.id = :id") String findEssayAnswerTextById(@Param("id") Long id); + + CandidateExamAnswer findByCandidateExamSessionIdAndQuestionId(Long candidateExamSession_id, long question_id); } 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 75da816..562d4e9 100644 --- a/src/main/java/com/testify/Testify_Backend/service/ExamManagementServiceImpl.java +++ b/src/main/java/com/testify/Testify_Backend/service/ExamManagementServiceImpl.java @@ -927,35 +927,53 @@ 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"); + } } } + 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/GradingServiceImpl.java b/src/main/java/com/testify/Testify_Backend/service/GradingServiceImpl.java index 510a8ee..2e3a6aa 100644 --- a/src/main/java/com/testify/Testify_Backend/service/GradingServiceImpl.java +++ b/src/main/java/com/testify/Testify_Backend/service/GradingServiceImpl.java @@ -41,8 +41,7 @@ public List getEssayDetails(Long examId, Long userId) { return essayQuestions.stream().map(essayQuestion -> { // Fetch the candidate's answer for the essay question CandidateExamAnswer candidateAnswer = candidateExamAnswerRepository - .findByCandidateExamSessionIdAndQuestionId(candidateExamSession.getId(), essayQuestion.getId()) - .orElse(null); + .findByCandidateExamSessionIdAndQuestionId(candidateExamSession.getId(), essayQuestion.getId()); String answerText = ""; if(candidateAnswer != null) { From 204e8029722aade86c4c49aea2204b06766d5510 Mon Sep 17 00:00:00 2001 From: VIHANGAGIT Date: Mon, 2 Dec 2024 01:04:02 +0530 Subject: [PATCH 2/6] Session and Status API updates --- .../controller/ExamManagementController.java | 6 ++++ .../Testify_Backend/enums/ExamStatus.java | 3 +- .../service/CandidateServiceImpl.java | 33 +++++++++++-------- .../service/ExamManagementService.java | 1 + .../service/ExamManagementServiceImpl.java | 10 ++++++ 5 files changed, 39 insertions(+), 14 deletions(-) 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 c288566..195d34c 100644 --- a/src/main/java/com/testify/Testify_Backend/controller/ExamManagementController.java +++ b/src/main/java/com/testify/Testify_Backend/controller/ExamManagementController.java @@ -153,6 +153,12 @@ public ResponseEntity saveAnswer(@RequestBody SaveAnswerRequest request) 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") public ResponseEntity addOrUpdateProctors( 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/service/CandidateServiceImpl.java b/src/main/java/com/testify/Testify_Backend/service/CandidateServiceImpl.java index efff0c8..fa133d7 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,6 +53,9 @@ public class CandidateServiceImpl implements CandidateService { @Autowired private GenericResponse genericResponse; + @Autowired + private final ExamSessionRepository examSessionRepository; + @Override public List getCandidateExams(String status) { String currentUserEmail = SecurityContextHolder.getContext().getAuthentication().getName(); @@ -65,10 +66,10 @@ public List getCandidateExams(String status) { 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 @@ -83,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; } @@ -99,7 +101,6 @@ public List getCandidateExams(String status) { @Override public CandidateExam getCandidateExamDetails(Integer examId) { - // Get the username (email) from SecurityContextHolder String currentUserEmail = SecurityContextHolder.getContext().getAuthentication().getName(); if (currentUserEmail == null) { @@ -125,19 +126,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 4206fa2..59cd464 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,7 @@ 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); 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 562d4e9..250116c 100644 --- a/src/main/java/com/testify/Testify_Backend/service/ExamManagementServiceImpl.java +++ b/src/main/java/com/testify/Testify_Backend/service/ExamManagementServiceImpl.java @@ -974,6 +974,16 @@ public void saveAnswer(Long sessionId, Long questionId, Long optionId, String an } } + @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(); From 2bd244a1583735b3760d0c6a8d907473b5952f79 Mon Sep 17 00:00:00 2001 From: VIHANGAGIT Date: Tue, 3 Dec 2024 14:46:48 +0530 Subject: [PATCH 3/6] Grading mcq and session management API updates --- .../controller/ExamManagementController.java | 8 ++ .../controller/GradingController.java | 14 +++ .../CandidateExamAnswerRepository.java | 9 ++ .../responses/McqDetailsResponse.java | 27 ++++ .../service/ExamManagementService.java | 1 + .../service/ExamManagementServiceImpl.java | 115 ++++++++++++++++++ .../service/GradingService.java | 3 + .../service/GradingServiceImpl.java | 32 +++++ 8 files changed, 209 insertions(+) create mode 100644 src/main/java/com/testify/Testify_Backend/responses/McqDetailsResponse.java 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 195d34c..c129757 100644 --- a/src/main/java/com/testify/Testify_Backend/controller/ExamManagementController.java +++ b/src/main/java/com/testify/Testify_Backend/controller/ExamManagementController.java @@ -82,6 +82,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); diff --git a/src/main/java/com/testify/Testify_Backend/controller/GradingController.java b/src/main/java/com/testify/Testify_Backend/controller/GradingController.java index 8edaf6c..33bcdff 100644 --- a/src/main/java/com/testify/Testify_Backend/controller/GradingController.java +++ b/src/main/java/com/testify/Testify_Backend/controller/GradingController.java @@ -1,9 +1,12 @@ 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; @@ -11,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.Map; @RestController @RequestMapping("/api/v1/grade") @@ -33,4 +37,14 @@ public ResponseEntity> getGradingScheme(@PathVariable Long 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/repository/CandidateExamAnswerRepository.java b/src/main/java/com/testify/Testify_Backend/repository/CandidateExamAnswerRepository.java index 46eb6b8..261ab27 100644 --- a/src/main/java/com/testify/Testify_Backend/repository/CandidateExamAnswerRepository.java +++ b/src/main/java/com/testify/Testify_Backend/repository/CandidateExamAnswerRepository.java @@ -2,12 +2,14 @@ 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 @@ -17,5 +19,12 @@ public interface CandidateExamAnswerRepository extends JpaRepository findByCandidateExamSessionId(Long sessionId); + + + CandidateExamAnswer findByCandidateExamSessionIdAndQuestionId(Long candidateExamSession_id, long question_id); } 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/ExamManagementService.java b/src/main/java/com/testify/Testify_Backend/service/ExamManagementService.java index 59cd464..9ab7944 100644 --- a/src/main/java/com/testify/Testify_Backend/service/ExamManagementService.java +++ b/src/main/java/com/testify/Testify_Backend/service/ExamManagementService.java @@ -17,6 +17,7 @@ public interface ExamManagementService { 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 250116c..89c297c 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.*; @@ -514,6 +515,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 + } + } + diff --git a/src/main/java/com/testify/Testify_Backend/service/GradingService.java b/src/main/java/com/testify/Testify_Backend/service/GradingService.java index 5a984ed..654fae6 100644 --- a/src/main/java/com/testify/Testify_Backend/service/GradingService.java +++ b/src/main/java/com/testify/Testify_Backend/service/GradingService.java @@ -1,11 +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 index 2e3a6aa..01ca17a 100644 --- a/src/main/java/com/testify/Testify_Backend/service/GradingServiceImpl.java +++ b/src/main/java/com/testify/Testify_Backend/service/GradingServiceImpl.java @@ -11,7 +11,9 @@ 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 @@ -77,4 +79,34 @@ public List getGradingSchemeForExam(Long 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()); + } + + } From 831c3f3a9c213a9de042d046d42143ae52ef9275 Mon Sep 17 00:00:00 2001 From: VIHANGAGIT Date: Tue, 3 Dec 2024 19:57:31 +0530 Subject: [PATCH 4/6] Added grading API for MCQs --- .../com/testify/Testify_Backend/model/ExamCandidateGrade.java | 4 ++++ .../repository/ExamCandidateGradeRepository.java | 4 ++++ .../requests/exam_management/ExamCandidateGradeRequest.java | 4 ++++ 3 files changed, 12 insertions(+) create mode 100644 src/main/java/com/testify/Testify_Backend/model/ExamCandidateGrade.java create mode 100644 src/main/java/com/testify/Testify_Backend/repository/ExamCandidateGradeRepository.java create mode 100644 src/main/java/com/testify/Testify_Backend/requests/exam_management/ExamCandidateGradeRequest.java diff --git a/src/main/java/com/testify/Testify_Backend/model/ExamCandidateGrade.java b/src/main/java/com/testify/Testify_Backend/model/ExamCandidateGrade.java new file mode 100644 index 0000000..61130b9 --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/model/ExamCandidateGrade.java @@ -0,0 +1,4 @@ +package com.testify.Testify_Backend.model; + +public class ExamCandidateGrade { +} diff --git a/src/main/java/com/testify/Testify_Backend/repository/ExamCandidateGradeRepository.java b/src/main/java/com/testify/Testify_Backend/repository/ExamCandidateGradeRepository.java new file mode 100644 index 0000000..babf521 --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/repository/ExamCandidateGradeRepository.java @@ -0,0 +1,4 @@ +package com.testify.Testify_Backend.repository; + +public interface ExamCandidateGradeRepository { +} diff --git a/src/main/java/com/testify/Testify_Backend/requests/exam_management/ExamCandidateGradeRequest.java b/src/main/java/com/testify/Testify_Backend/requests/exam_management/ExamCandidateGradeRequest.java new file mode 100644 index 0000000..3bab8bc --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/requests/exam_management/ExamCandidateGradeRequest.java @@ -0,0 +1,4 @@ +package com.testify.Testify_Backend.requests.exam_management; + +public class ExamCandidateGradeRequest { +} From 59dee4ad1cfef3f15ed2c4cb176643313456efef Mon Sep 17 00:00:00 2001 From: VIHANGAGIT Date: Tue, 3 Dec 2024 19:59:27 +0530 Subject: [PATCH 5/6] Added Grading API to MCQ --- .../controller/GradingController.java | 14 +++++--- .../model/ExamCandidateGrade.java | 34 ++++++++++++++++++- .../ExamCandidateGradeRepository.java | 9 +++-- .../ExamCandidateGradeRequest.java | 17 ++++++++-- .../service/GradingService.java | 2 ++ .../service/GradingServiceImpl.java | 20 ++++++++--- 6 files changed, 83 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/testify/Testify_Backend/controller/GradingController.java b/src/main/java/com/testify/Testify_Backend/controller/GradingController.java index 33bcdff..164ad67 100644 --- a/src/main/java/com/testify/Testify_Backend/controller/GradingController.java +++ b/src/main/java/com/testify/Testify_Backend/controller/GradingController.java @@ -2,16 +2,14 @@ import com.testify.Testify_Backend.model.CandidateExamSession; import com.testify.Testify_Backend.model.Grade; +import com.testify.Testify_Backend.requests.exam_management.ExamCandidateGradeRequest; 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 org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; @@ -46,5 +44,13 @@ public ResponseEntity>> getResultsBySessionId( return ResponseEntity.ok(results); } + @PostMapping("/setExamCandidateGrade") + public ResponseEntity setExamCandidateGrade(@RequestBody ExamCandidateGradeRequest examCandidateGradeRequest) { + String response = gradingService.setExamCandidateGrade(examCandidateGradeRequest); + return ResponseEntity.ok(response); + } + + + } diff --git a/src/main/java/com/testify/Testify_Backend/model/ExamCandidateGrade.java b/src/main/java/com/testify/Testify_Backend/model/ExamCandidateGrade.java index 61130b9..d6bd3ed 100644 --- a/src/main/java/com/testify/Testify_Backend/model/ExamCandidateGrade.java +++ b/src/main/java/com/testify/Testify_Backend/model/ExamCandidateGrade.java @@ -1,4 +1,36 @@ package com.testify.Testify_Backend.model; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import lombok.*; +import org.springframework.data.annotation.Id; + +@Entity +@Data +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor public class ExamCandidateGrade { -} + + @Setter + @jakarta.persistence.Id + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) // Auto-generate the ID + @Column + private Long id; + + @Column(nullable = false) + private String examID; + + @Column(nullable = false) + private String candidateID; + + private String status; + + private String grade; + + private String score; +} \ No newline at end of file diff --git a/src/main/java/com/testify/Testify_Backend/repository/ExamCandidateGradeRepository.java b/src/main/java/com/testify/Testify_Backend/repository/ExamCandidateGradeRepository.java index babf521..4190c7c 100644 --- a/src/main/java/com/testify/Testify_Backend/repository/ExamCandidateGradeRepository.java +++ b/src/main/java/com/testify/Testify_Backend/repository/ExamCandidateGradeRepository.java @@ -1,4 +1,9 @@ package com.testify.Testify_Backend.repository; -public interface ExamCandidateGradeRepository { -} +import com.testify.Testify_Backend.model.ExamCandidateGrade; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ExamCandidateGradeRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/testify/Testify_Backend/requests/exam_management/ExamCandidateGradeRequest.java b/src/main/java/com/testify/Testify_Backend/requests/exam_management/ExamCandidateGradeRequest.java index 3bab8bc..8a85ad7 100644 --- a/src/main/java/com/testify/Testify_Backend/requests/exam_management/ExamCandidateGradeRequest.java +++ b/src/main/java/com/testify/Testify_Backend/requests/exam_management/ExamCandidateGradeRequest.java @@ -1,4 +1,17 @@ package com.testify.Testify_Backend.requests.exam_management; -public class ExamCandidateGradeRequest { -} +import lombok.*; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class ExamCandidateGradeRequest +{ + private String examID; + private String candidateID; + private String status; + private String grade; + private String score; +} \ No newline at end of file diff --git a/src/main/java/com/testify/Testify_Backend/service/GradingService.java b/src/main/java/com/testify/Testify_Backend/service/GradingService.java index 654fae6..444bf86 100644 --- a/src/main/java/com/testify/Testify_Backend/service/GradingService.java +++ b/src/main/java/com/testify/Testify_Backend/service/GradingService.java @@ -2,6 +2,7 @@ import com.testify.Testify_Backend.model.CandidateExamSession; import com.testify.Testify_Backend.model.Grade; +import com.testify.Testify_Backend.requests.exam_management.ExamCandidateGradeRequest; import com.testify.Testify_Backend.responses.EssayDetailsResponse; import java.util.List; @@ -11,4 +12,5 @@ public interface GradingService { List getEssayDetails(Long examId, Long userId); List getGradingSchemeForExam(Long examId); List> getQuestionAndOptionBySessionId(Long sessionId); + String setExamCandidateGrade(ExamCandidateGradeRequest examCandidateGradeRequest); } diff --git a/src/main/java/com/testify/Testify_Backend/service/GradingServiceImpl.java b/src/main/java/com/testify/Testify_Backend/service/GradingServiceImpl.java index 01ca17a..b9ea707 100644 --- a/src/main/java/com/testify/Testify_Backend/service/GradingServiceImpl.java +++ b/src/main/java/com/testify/Testify_Backend/service/GradingServiceImpl.java @@ -1,10 +1,8 @@ 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.repository.*; +import com.testify.Testify_Backend.requests.exam_management.ExamCandidateGradeRequest; import com.testify.Testify_Backend.responses.EssayDetailsResponse; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -25,6 +23,7 @@ public class GradingServiceImpl implements GradingService { private final CandidateExamAnswerRepository candidateExamAnswerRepository; private final GradeRepository gradeRepository; private final ExamSessionRepository examSessionRepository; + private final ExamCandidateGradeRepository examCandidateGradeRepository; @Override @Transactional @@ -108,5 +107,18 @@ public List> getQuestionAndOptionBySessionId(Long sessionId) .collect(Collectors.toList()); } + @Override + @Transactional + public String setExamCandidateGrade(ExamCandidateGradeRequest examCandidateGradeRequest){ + ExamCandidateGrade examCandidateGrade = new ExamCandidateGrade(); + examCandidateGrade.setExamID(examCandidateGradeRequest.getExamID()); + examCandidateGrade.setCandidateID(examCandidateGradeRequest.getCandidateID()); + examCandidateGrade.setStatus(examCandidateGradeRequest.getStatus()); + examCandidateGrade.setGrade(examCandidateGradeRequest.getGrade()); + examCandidateGrade.setScore(examCandidateGradeRequest.getScore()); + examCandidateGradeRepository.save(examCandidateGrade); + return "Grade set successfully"; + } + } From d27e7b9cf4fa4bc1476d197f24a3ef5ce3219193 Mon Sep 17 00:00:00 2001 From: VIHANGAGIT Date: Tue, 3 Dec 2024 23:05:14 +0530 Subject: [PATCH 6/6] Session check API --- .../controller/CandidateController.java | 10 ++++++ .../repository/ExamSessionRepository.java | 4 +++ .../CandidateExamAnswerRequest.java | 8 +++++ .../CandidateExamAnswerResponse.java | 21 ++++++++++++ .../service/ExamManagementService.java | 1 + .../service/ExamManagementServiceImpl.java | 33 +++++++++++++++++++ 6 files changed, 77 insertions(+) create mode 100644 src/main/java/com/testify/Testify_Backend/requests/exam_management/CandidateExamAnswerRequest.java create mode 100644 src/main/java/com/testify/Testify_Backend/responses/exam_management/CandidateExamAnswerResponse.java diff --git a/src/main/java/com/testify/Testify_Backend/controller/CandidateController.java b/src/main/java/com/testify/Testify_Backend/controller/CandidateController.java index 7fb2c87..43fdde0 100644 --- a/src/main/java/com/testify/Testify_Backend/controller/CandidateController.java +++ b/src/main/java/com/testify/Testify_Backend/controller/CandidateController.java @@ -3,12 +3,16 @@ import com.testify.Testify_Backend.model.Candidate; import com.testify.Testify_Backend.model.Organization; import com.testify.Testify_Backend.repository.CandidateRepository; +import com.testify.Testify_Backend.requests.exam_management.CandidateExamAnswerRequest; 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; import com.testify.Testify_Backend.responses.candidate_management.CandidateProfile; import com.testify.Testify_Backend.responses.candidate_management.OrganizationCandidateView; +import com.testify.Testify_Backend.responses.exam_management.CandidateExamAnswerResponse; import com.testify.Testify_Backend.service.CandidateService; +import com.testify.Testify_Backend.service.ExamManagementService; +import com.testify.Testify_Backend.service.ExamManagementServiceImpl; import com.testify.Testify_Backend.utils.VarList; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -23,6 +27,7 @@ @RequiredArgsConstructor public class CandidateController { private final CandidateService candidateService; + private final ExamManagementService examManagementService; @GetMapping("/exams") public ResponseEntity> getCandidateExams(@RequestParam(value = "status", required = false) String status) { @@ -94,4 +99,9 @@ public ResponseEntity deleteCandidateProfile(long id){ } + @PostMapping("/check-active-session") + public CandidateExamAnswerResponse getCandidateAnswers(@RequestBody CandidateExamAnswerRequest request) { + return examManagementService.getCandidateAnswers(request.getCandidateId()); + } + } 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 649aec4..5062881 100644 --- a/src/main/java/com/testify/Testify_Backend/repository/ExamSessionRepository.java +++ b/src/main/java/com/testify/Testify_Backend/repository/ExamSessionRepository.java @@ -2,6 +2,7 @@ import com.testify.Testify_Backend.model.CandidateExamSession; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.Optional; @@ -10,5 +11,8 @@ public interface ExamSessionRepository extends JpaRepository { Optional findByCandidateIdAndExamIdAndInProgress(Long candidateId, Long examId, Boolean inProgress); Optional findByExamIdAndCandidateId(Long examId, Long candidateId); + + @Query("SELECT s FROM CandidateExamSession s WHERE s.candidate.id = :candidateId AND s.inProgress = true AND s.endTime >= CURRENT_TIMESTAMP") + CandidateExamSession findInProgressSession(Long candidateId); } diff --git a/src/main/java/com/testify/Testify_Backend/requests/exam_management/CandidateExamAnswerRequest.java b/src/main/java/com/testify/Testify_Backend/requests/exam_management/CandidateExamAnswerRequest.java new file mode 100644 index 0000000..23fb5f6 --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/requests/exam_management/CandidateExamAnswerRequest.java @@ -0,0 +1,8 @@ +package com.testify.Testify_Backend.requests.exam_management; + +import lombok.Data; + +@Data +public class CandidateExamAnswerRequest { + private Long candidateId; +} diff --git a/src/main/java/com/testify/Testify_Backend/responses/exam_management/CandidateExamAnswerResponse.java b/src/main/java/com/testify/Testify_Backend/responses/exam_management/CandidateExamAnswerResponse.java new file mode 100644 index 0000000..4969151 --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/responses/exam_management/CandidateExamAnswerResponse.java @@ -0,0 +1,21 @@ +package com.testify.Testify_Backend.responses.exam_management; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +public class CandidateExamAnswerResponse { + private Long examId; + private List answers; + + @Data + @AllArgsConstructor + public static class AnswerDetail { + private Long questionId; + private String questionType; + private Object answer; + } +} 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 9ab7944..4c4517f 100644 --- a/src/main/java/com/testify/Testify_Backend/service/ExamManagementService.java +++ b/src/main/java/com/testify/Testify_Backend/service/ExamManagementService.java @@ -18,6 +18,7 @@ public interface ExamManagementService { void saveAnswer(Long sessionId, Long questionId, Long optionId, String answerText); void markSessionAsComplete(Long sessionId); ResponseEntity getAllQuestionsAnswersByExamId(long examId, long sessionId); + CandidateExamAnswerResponse getCandidateAnswers(Long candidateId); 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 89c297c..b738ab6 100644 --- a/src/main/java/com/testify/Testify_Backend/service/ExamManagementServiceImpl.java +++ b/src/main/java/com/testify/Testify_Backend/service/ExamManagementServiceImpl.java @@ -1099,6 +1099,39 @@ public void markSessionAsComplete(Long sessionId) { examSessionRepository.save(session); } + @Override + @Transactional + public CandidateExamAnswerResponse getCandidateAnswers(Long candidateId) { + CandidateExamSession session = examSessionRepository.findInProgressSession(candidateId); + + if (session == null) { + throw new RuntimeException("No exam session in progress for the candidate."); + } + + List answerDetails = new ArrayList<>(); + for (CandidateExamAnswer answer : session.getAnswers()) { + if (answer instanceof MCQAnswer) { + MCQAnswer mcqAnswer = (MCQAnswer) answer; + answerDetails.add(new CandidateExamAnswerResponse.AnswerDetail( + answer.getQuestion().getId(), + "MCQ", + mcqAnswer.getOption() != null ? mcqAnswer.getOption().getId() : null + )); + } else { + answerDetails.add(new CandidateExamAnswerResponse.AnswerDetail( + answer.getQuestion().getId(), + "Essay", + ((EssayAnswer) answer).getAnswerText() + )); + } + } + + return new CandidateExamAnswerResponse( + session.getExam().getId(), + answerDetails + ); + } + public ResponseEntity addProctorsToExam(long examId, List proctorEmails) { Optional optionalExam = examRepository.findById(examId); GenericAddOrUpdateResponse response = new GenericAddOrUpdateResponse();