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 65f7548..835c6db 100644 --- a/src/main/java/com/testify/Testify_Backend/controller/ExamManagementController.java +++ b/src/main/java/com/testify/Testify_Backend/controller/ExamManagementController.java @@ -13,6 +13,7 @@ import com.testify.Testify_Backend.service.ExamManagementService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -194,4 +195,87 @@ public ResponseEntity> getCandidateConflicti return ResponseEntity.ok(response); } + @PutMapping("/{examId}/real-time-monitoring") + public ResponseEntity updateRealTimeMonitoring( + @PathVariable Long examId, + @RequestBody RealTimeMonitoringRequest dto) { + try { + // Delegate to the service and return the response directly + GenericResponse response = examManagementService.updateRealTimeMonitoring(examId, dto); + return ResponseEntity.ok(response); + } catch (Exception ex) { + return ResponseEntity.status(500).body(new GenericResponse("false", "Error: " + ex.getMessage())); + } + } + + @GetMapping("/{examId}/real-time-monitoring") + public ResponseEntity getRealTimeMonitoringStatus(@PathVariable Long examId) { + try { + RealTimeMonitoringResponse response = examManagementService.getRealTimeMonitoringStatus(examId); + return ResponseEntity.ok(response); + } catch (Exception ex) { + return ResponseEntity.status(500).body(null); + } + } + + @PutMapping("/{examId}/browser-lockdown") + public ResponseEntity updateBrowserLockdown( + @PathVariable Long examId, + @RequestParam boolean browserLockdown) { + try { + GenericResponse response = examManagementService.updateBrowserLockdown(examId, browserLockdown); + return ResponseEntity.ok(response); + } catch (Exception ex) { + return ResponseEntity.status(500).body(new GenericResponse("false", "Error: " + ex.getMessage())); + } + } + + @GetMapping("/{examId}/browser-lockdown") + public ResponseEntity getBrowserLockdown(@PathVariable Long examId) { + try { + boolean browserLockdown = examManagementService.getBrowserLockdownStatus(examId); + return ResponseEntity.ok(new BrowserLockdownResponse(browserLockdown)); + } catch (Exception ex) { + return ResponseEntity.status(500).body(null); // Handle errors gracefully + } + } + + @PutMapping("/{examId}/hosted") + public ResponseEntity updateHosted(@PathVariable Long examId, @RequestParam boolean hosted) { + try { + GenericResponse response = examManagementService.updateHostedStatus(examId, hosted); + return ResponseEntity.ok(response); + } catch (Exception ex) { + return ResponseEntity.status(500).body(new GenericResponse("false", "Error: " + ex.getMessage())); + } + } + + @GetMapping("/{examId}/hosted") + public ResponseEntity getHosted(@PathVariable Long examId) { + try { + boolean hosted = examManagementService.getHostedStatus(examId); + log.info(String.valueOf(hosted)); + return ResponseEntity.ok(new HostedResponse(hosted)); + } catch (Exception ex) { + return ResponseEntity.status(500).body(null); + } + } + + @PostMapping("/{examId}/set-moderator") + public ResponseEntity setModerator(@PathVariable Long examId, @RequestBody ModeratorRequest moderatorRequest) { + log.info("Setting moderator for examId: " + examId); + try { + examManagementService.assignModerator(examId, moderatorRequest.getModeratorEmail()); + return ResponseEntity.ok("Moderator assigned successfully."); + } catch (RuntimeException ex) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage()); + } + } + + @GetMapping("/{examId}/moderator") + public ResponseEntity getModerator(@PathVariable Long examId) { + ModeratorResponse moderatorResponse = examManagementService.getModeratorDetails(examId); + return ResponseEntity.ok(moderatorResponse); // Returns null in the body if no moderator exists + } + } 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 b231687..6b1d95e 100644 --- a/src/main/java/com/testify/Testify_Backend/model/Exam.java +++ b/src/main/java/com/testify/Testify_Backend/model/Exam.java @@ -102,4 +102,16 @@ public class Exam { @OneToMany(mappedBy = "exam", cascade = CascadeType.ALL) private List gradings; + @Column(nullable = false) + private boolean realTimeMonitoring = false; + + @Column + private String zoomLink; + + @Column(nullable = false) + private boolean browserLockdown = false; + + @Column(nullable = false) + private boolean hosted = false; + } diff --git a/src/main/java/com/testify/Testify_Backend/model/ExamSetter.java b/src/main/java/com/testify/Testify_Backend/model/ExamSetter.java index a24b8dc..37f625b 100644 --- a/src/main/java/com/testify/Testify_Backend/model/ExamSetter.java +++ b/src/main/java/com/testify/Testify_Backend/model/ExamSetter.java @@ -43,4 +43,6 @@ public class ExamSetter extends User{ @OneToMany(mappedBy = "moderator") private Set moderatedExams; + + } diff --git a/src/main/java/com/testify/Testify_Backend/repository/CandidateRepository.java b/src/main/java/com/testify/Testify_Backend/repository/CandidateRepository.java index a2108b5..2c59e61 100644 --- a/src/main/java/com/testify/Testify_Backend/repository/CandidateRepository.java +++ b/src/main/java/com/testify/Testify_Backend/repository/CandidateRepository.java @@ -22,6 +22,5 @@ public interface CandidateRepository extends JpaRepository { List findCandidatesAssignedToExamWithConflictingExams(Long examId, LocalDateTime startDatetime, LocalDateTime endDatetime); boolean existsByEmail(String currentUserEmail); - Set findAllByEmailIn(List emails); } diff --git a/src/main/java/com/testify/Testify_Backend/requests/exam_management/ModeratorRequest.java b/src/main/java/com/testify/Testify_Backend/requests/exam_management/ModeratorRequest.java new file mode 100644 index 0000000..5d7ddcd --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/requests/exam_management/ModeratorRequest.java @@ -0,0 +1,14 @@ +package com.testify.Testify_Backend.requests.exam_management; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class ModeratorRequest { + private String moderatorEmail; +} diff --git a/src/main/java/com/testify/Testify_Backend/requests/exam_management/RealTimeMonitoringRequest.java b/src/main/java/com/testify/Testify_Backend/requests/exam_management/RealTimeMonitoringRequest.java new file mode 100644 index 0000000..934625a --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/requests/exam_management/RealTimeMonitoringRequest.java @@ -0,0 +1,15 @@ +package com.testify.Testify_Backend.requests.exam_management; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class RealTimeMonitoringRequest { + private boolean realTimeMonitoring; + private String zoomLink; +} diff --git a/src/main/java/com/testify/Testify_Backend/responses/GenericResponse.java b/src/main/java/com/testify/Testify_Backend/responses/GenericResponse.java index ffe12aa..755ad7d 100644 --- a/src/main/java/com/testify/Testify_Backend/responses/GenericResponse.java +++ b/src/main/java/com/testify/Testify_Backend/responses/GenericResponse.java @@ -9,7 +9,6 @@ @AllArgsConstructor @Data public class GenericResponse { - private String code; + private String success; private String message; - private Object content; } diff --git a/src/main/java/com/testify/Testify_Backend/responses/exam_management/BrowserLockdownResponse.java b/src/main/java/com/testify/Testify_Backend/responses/exam_management/BrowserLockdownResponse.java new file mode 100644 index 0000000..73bc8db --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/responses/exam_management/BrowserLockdownResponse.java @@ -0,0 +1,14 @@ +package com.testify.Testify_Backend.responses.exam_management; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class BrowserLockdownResponse { + private boolean browserLockdown; +} diff --git a/src/main/java/com/testify/Testify_Backend/responses/exam_management/ExamResponse.java b/src/main/java/com/testify/Testify_Backend/responses/exam_management/ExamResponse.java index 1ea95f9..2f949a3 100644 --- a/src/main/java/com/testify/Testify_Backend/responses/exam_management/ExamResponse.java +++ b/src/main/java/com/testify/Testify_Backend/responses/exam_management/ExamResponse.java @@ -1,6 +1,7 @@ package com.testify.Testify_Backend.responses.exam_management; import com.testify.Testify_Backend.enums.OrderType; +import jakarta.persistence.Column; import lombok.*; import java.time.LocalDateTime; @@ -31,4 +32,8 @@ public class ExamResponse { private OrderType orderType; private FixedOrderResponse fixedOrder; private RandomOrderResponse randomOrder; + private boolean realTimeMonitoring; + private String zoomLink; + private boolean browserLockdown; + private boolean hosted; } diff --git a/src/main/java/com/testify/Testify_Backend/responses/exam_management/HostedResponse.java b/src/main/java/com/testify/Testify_Backend/responses/exam_management/HostedResponse.java new file mode 100644 index 0000000..c971218 --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/responses/exam_management/HostedResponse.java @@ -0,0 +1,14 @@ +package com.testify.Testify_Backend.responses.exam_management; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class HostedResponse { + private boolean hosted; +} diff --git a/src/main/java/com/testify/Testify_Backend/responses/exam_management/ModeratorResponse.java b/src/main/java/com/testify/Testify_Backend/responses/exam_management/ModeratorResponse.java new file mode 100644 index 0000000..482c95a --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/responses/exam_management/ModeratorResponse.java @@ -0,0 +1,14 @@ +package com.testify.Testify_Backend.responses.exam_management; + +import lombok.*; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ModeratorResponse { + private String email; + private String firstName; + private String lastName; +} diff --git a/src/main/java/com/testify/Testify_Backend/responses/exam_management/RealTimeMonitoringResponse.java b/src/main/java/com/testify/Testify_Backend/responses/exam_management/RealTimeMonitoringResponse.java new file mode 100644 index 0000000..1db9ae8 --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/responses/exam_management/RealTimeMonitoringResponse.java @@ -0,0 +1,13 @@ +package com.testify.Testify_Backend.responses.exam_management; + +import lombok.*; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class RealTimeMonitoringResponse { + private boolean realTimeMonitoring; + private String zoomLink; +} diff --git a/src/main/java/com/testify/Testify_Backend/service/AuthenticationService.java b/src/main/java/com/testify/Testify_Backend/service/AuthenticationService.java index 7e3951e..c194271 100644 --- a/src/main/java/com/testify/Testify_Backend/service/AuthenticationService.java +++ b/src/main/java/com/testify/Testify_Backend/service/AuthenticationService.java @@ -217,10 +217,10 @@ public RegisterResponse register(@ModelAttribute RegistrationRequest request, bo // Log the confirmation link log.info("Confirmation link: {}", link); -// emailSender.send( -// request.getEmail(), -// buildEmail(request.getEmail(), link) -// ); + emailSender.send( + request.getEmail(), + buildEmail(request.getEmail(), link) + ); //TODO: save jwt token var jwtToken = jwtService.generateToken(savedUser); 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 05381bd..b67b7f7 100644 --- a/src/main/java/com/testify/Testify_Backend/service/CandidateService.java +++ b/src/main/java/com/testify/Testify_Backend/service/CandidateService.java @@ -10,7 +10,6 @@ import java.util.List; public interface CandidateService { - List getCandidateExams(); List getAllCandidatesForSearch(); diff --git a/src/main/java/com/testify/Testify_Backend/service/EmailSenderImpl.java b/src/main/java/com/testify/Testify_Backend/service/EmailSenderImpl.java index d4bb81d..086ec60 100644 --- a/src/main/java/com/testify/Testify_Backend/service/EmailSenderImpl.java +++ b/src/main/java/com/testify/Testify_Backend/service/EmailSenderImpl.java @@ -19,24 +19,24 @@ public class EmailSenderImpl implements EmailSender { private final static Logger LOGGER = LoggerFactory .getLogger(EmailSenderImpl.class); -// private final JavaMailSender mailSender; + private final JavaMailSender mailSender; @Override @Async public void send(String to, String email) { -// try { -// MimeMessage mimeMessage = mailSender.createMimeMessage(); -// MimeMessageHelper helper = -// new MimeMessageHelper(mimeMessage, "utf-8"); -// helper.setText(email, true); -// helper.setTo(to); -// helper.setSubject("Confirm your email"); -// helper.setFrom("hello@amigoscode.com"); -// mailSender.send(mimeMessage); -// } catch (MessagingException e) { -// LOGGER.error("failed to send email", e); -// throw new IllegalStateException("failed to send email"); -// } + try { + MimeMessage mimeMessage = mailSender.createMimeMessage(); + MimeMessageHelper helper = + new MimeMessageHelper(mimeMessage, "utf-8"); + helper.setText(email, true); + helper.setTo(to); + helper.setSubject("Confirm your email"); + helper.setFrom("hello@amigoscode.com"); + mailSender.send(mimeMessage); + } catch (MessagingException e) { + LOGGER.error("failed to send email", e); + throw new IllegalStateException("failed to send email"); + } System.out.println("Skipping email sending during development."); } } 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..3d055a3 100644 --- a/src/main/java/com/testify/Testify_Backend/service/ExamManagementService.java +++ b/src/main/java/com/testify/Testify_Backend/service/ExamManagementService.java @@ -49,4 +49,16 @@ public interface ExamManagementService { List getExamsScheduledBetween(Long examId); List getCandidateConflictingExams(Long examId); + + GenericResponse updateRealTimeMonitoring(Long examId, RealTimeMonitoringRequest dto); + RealTimeMonitoringResponse getRealTimeMonitoringStatus(Long examId); + + GenericResponse updateBrowserLockdown(Long examId, boolean browserLockdown); + boolean getBrowserLockdownStatus(Long examId); + + GenericResponse updateHostedStatus(Long examId, boolean hosted); + boolean getHostedStatus(Long examId); + + void assignModerator(Long examId, String moderatorEmail); + ModeratorResponse getModeratorDetails(Long examId); } 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 779a752..d626c9c 100644 --- a/src/main/java/com/testify/Testify_Backend/service/ExamManagementServiceImpl.java +++ b/src/main/java/com/testify/Testify_Backend/service/ExamManagementServiceImpl.java @@ -46,6 +46,7 @@ public class ExamManagementServiceImpl implements ExamManagementService { private final MCQOptionRepository mcqOptionRepository; private final CandidateGroupRepository candidateGroupRepository; private final CandidateGroupRepository candidateGroupRepository; + private final EmailSender emailSender; private final ModelMapper modelMapper; @@ -79,6 +80,9 @@ public GenericAddOrUpdateResponse createExam(ExamRequest examReques .endDatetime(examRequest.getEndDatetime()) .isPrivate(true) .orderType(examRequest.getOrderType() != null ? examRequest.getOrderType() : OrderType.FIXED) + .browserLockdown(false) + .realTimeMonitoring(false) + .hosted(false) .build(); exam = examRepository.save(exam); @@ -675,6 +679,11 @@ public ExamResponse getExamById(long examId) { // Handle question sequence (assuming it's never null) .questionSequence(exam.getQuestionSequence()) + .browserLockdown(exam.isBrowserLockdown()) + .realTimeMonitoring(exam.isRealTimeMonitoring()) + .zoomLink(exam.getZoomLink()) + .hosted(exam.isHosted()) + .build(); return examResponse; @@ -810,29 +819,24 @@ public ResponseEntity saveGrades(Long examId, List> getGradesByExamId(Long examId) { - List gradeResponses; - try { List grades = gradeRepository.findByExamId(examId); log.info("Grades: {}", grades); - if (grades.isEmpty()) { - throw new RuntimeException("No grades found for the given exam ID"); - } - - gradeResponses = grades.stream() + // Convert grades to responses, even if the list is empty + List gradeResponses = grades.stream() .map(grade -> new GradeResponse(grade.getId(), grade.getGrade(), grade.getMinMarks(), grade.getMaxMarks())) .collect(Collectors.toList()); return ResponseEntity.ok(gradeResponses); - } catch (RuntimeException ex) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); } catch (Exception ex) { + log.error("An error occurred while fetching grades for examId {}: {}", examId, ex.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); } } + @Transactional public ResponseEntity updateGrades(Long examId, List gradeRequestList) { GenericAddOrUpdateResponse response = new GenericAddOrUpdateResponse(); @@ -1193,7 +1197,154 @@ public List getCandidateConflictingExams(Long exa return responseList; // This will return a list of conflicting exams or empty if none found } + @Transactional + public GenericResponse updateRealTimeMonitoring(Long examId, RealTimeMonitoringRequest dto) { + // Fetch the exam by ID + Exam exam = examRepository.findById(examId) + .orElseThrow(() -> new RuntimeException("Exam not found with ID: " + examId)); + + // Update the fields + exam.setRealTimeMonitoring(dto.isRealTimeMonitoring()); + if (dto.isRealTimeMonitoring()) { + // If activating, set the Zoom link + exam.setZoomLink(dto.getZoomLink()); + } else { + // If deactivating, clear the Zoom link + exam.setZoomLink(null); + } + + // Save the updated exam + examRepository.save(exam); + + // Create and return a success response + String message = dto.isRealTimeMonitoring() ? + "Real-time monitoring activated successfully." : + "Real-time monitoring deactivated successfully."; + return new GenericResponse("true", message); + } + + public RealTimeMonitoringResponse getRealTimeMonitoringStatus(Long examId) { + // Find the exam by ID + Exam exam = examRepository.findById(examId).orElseThrow(() -> new RuntimeException("Exam not found")); + + // Return the real-time monitoring status and Zoom link + return new RealTimeMonitoringResponse(exam.isRealTimeMonitoring(), exam.getZoomLink()); + } + + @Transactional + public GenericResponse updateBrowserLockdown(Long examId, boolean browserLockdown) { + // Fetch the exam by ID + Exam exam = examRepository.findById(examId) + .orElseThrow(() -> new RuntimeException("Exam not found with ID: " + examId)); + + // Update the browserLockdown status + exam.setBrowserLockdown(browserLockdown); + examRepository.save(exam); + + // Construct and return the response + String message = browserLockdown ? + "Browser lockdown activated successfully." : + "Browser lockdown deactivated successfully."; + return new GenericResponse("true", message); + } + + @Transactional(readOnly = true) + public boolean getBrowserLockdownStatus(Long examId) { + Exam exam = examRepository.findById(examId) + .orElseThrow(() -> new RuntimeException("Exam not found with ID: " + examId)); + return exam.isBrowserLockdown(); + } + + @Transactional + public GenericResponse updateHostedStatus(Long examId, boolean hosted) { + Exam exam = examRepository.findById(examId) + .orElseThrow(() -> new RuntimeException("Exam not found with ID: " + examId)); + + exam.setHosted(hosted); + examRepository.save(exam); + + if (hosted) { + notifyCandidates(exam); + } + + String message = hosted ? "Exam hosting activated successfully." : "Exam hosting deactivated successfully."; + return new GenericResponse("true", message); + } + + private void notifyCandidates(Exam exam) { + Set candidates = exam.getCandidates(); + if (candidates == null || candidates.isEmpty()) { + return; // No candidates to notify + } + + String emailSubject = "Exam Hosted Notification"; + String emailContent = String.format( + """ + Dear Candidate, + + The exam "%s" has been hosted. Please log in to your account for further details. + + Exam Details: + Title: %s + Start Date and Time: %s + End Date and Time: %s + + Best regards, + Testify Team + """, + exam.getTitle(), + exam.getTitle(), + exam.getStartDatetime(), + exam.getEndDatetime() + ); + + for (Candidate candidate : candidates) { + String email = candidate.getEmail(); // Assuming `Candidate` inherits `email` from `User` + try { + emailSender.send(email, emailContent); + } catch (Exception e) { + log.error("Failed to send email to candidate: {}", email, e); + } + } + } + + @Transactional(readOnly = true) + public boolean getHostedStatus(Long examId) { + Exam exam = examRepository.findById(examId) + .orElseThrow(() -> new RuntimeException("Exam not found with ID: " + examId)); + return exam.isHosted(); + } + @Transactional + public void assignModerator(Long examId, String moderatorEmail) { + Exam exam = examRepository.findById(examId) + .orElseThrow(() -> new RuntimeException("Exam not found with ID: " + examId)); + + log.info("Exam found with ID: {}", examId); + log.info("Searching for moderator with email: {}", moderatorEmail); + + ExamSetter moderator = examSetterRepository.findByEmail(moderatorEmail) + .orElseThrow(() -> new RuntimeException("Exam setter not found with email: " + moderatorEmail)); + + log.info("Moderator found with email: {}", moderatorEmail); + + + exam.setModerator(moderator); + examRepository.save(exam); + } + + @Transactional + public ModeratorResponse getModeratorDetails(Long examId) { + Exam exam = examRepository.findById(examId) + .orElseThrow(() -> new RuntimeException("Exam not found with ID: " + examId)); + + ExamSetter moderator = exam.getModerator(); + if (moderator == null) { + return null; // Return null if no moderator is assigned + } + + return new ModeratorResponse(moderator.getEmail(), moderator.getFirstName(), moderator.getLastName()); + } }