diff --git a/pom.xml b/pom.xml index d65ac4c..ad6c1a1 100644 --- a/pom.xml +++ b/pom.xml @@ -14,9 +14,9 @@ Testify-Backend Spring Boot Backend for Testify - 21 - 21 - 21 + 22 + 22 + 22 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 c129757..1403c64 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.*; @@ -169,9 +170,7 @@ public ResponseEntity submitExam(@PathVariable Long sessionId) { } @PostMapping("/{examId}/proctors") - public ResponseEntity addOrUpdateProctors( - @PathVariable Long examId, - @RequestBody List emails) { + public ResponseEntity addOrUpdateProctors(@PathVariable Long examId, @RequestBody List emails) { log.info("Adding proctors to examId: " + examId); log.info("Emails: " + emails); return examManagementService.addProctorsToExam(examId, emails); @@ -209,4 +208,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/controller/ExamSetterController.java b/src/main/java/com/testify/Testify_Backend/controller/ExamSetterController.java index 36c0e89..0f4f59a 100644 --- a/src/main/java/com/testify/Testify_Backend/controller/ExamSetterController.java +++ b/src/main/java/com/testify/Testify_Backend/controller/ExamSetterController.java @@ -36,4 +36,10 @@ public ResponseEntity addSetterToOrganization(@PathV GenericAddOrUpdateResponse response = examSetterService.addSetterToOrganization(token); return ResponseEntity.ok(response); } + + @PutMapping("/{setterId}/{organizationId}/deleteSetter") + public ResponseEntity deleteSetter(@PathVariable("setterId") String setterId, @PathVariable("organizationId") String organizationId) { + GenericAddOrUpdateResponse response = examSetterService.deleteSetter(setterId, organizationId); + return ResponseEntity.ok(response); + } } 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..3128c15 100644 --- a/src/main/java/com/testify/Testify_Backend/controller/GradingController.java +++ b/src/main/java/com/testify/Testify_Backend/controller/GradingController.java @@ -1,17 +1,17 @@ package com.testify.Testify_Backend.controller; import com.testify.Testify_Backend.model.CandidateExamSession; +import com.testify.Testify_Backend.model.ExamCandidateGrade; 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.responses.exam_management.ExamCandidateGradeResponse; 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 +46,16 @@ public ResponseEntity>> getResultsBySessionId( return ResponseEntity.ok(results); } + @PostMapping("/setExamCandidateGrade") + public ResponseEntity setExamCandidateGrade(@RequestBody ExamCandidateGradeRequest examCandidateGradeRequest) { + String response = gradingService.setExamCandidateGrade(examCandidateGradeRequest); + return ResponseEntity.ok(response); + } + + @GetMapping("/getExamCandidateGrade") + public ResponseEntity> getExamCandidateGrade() { + List response = gradingService.getExamCandidateGrade(); + return ResponseEntity.ok(response); + } } 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 90cc009..0cf2e03 100644 --- a/src/main/java/com/testify/Testify_Backend/model/Exam.java +++ b/src/main/java/com/testify/Testify_Backend/model/Exam.java @@ -92,6 +92,7 @@ public class Exam { joinColumns = @JoinColumn(name = "exam_id"), inverseJoinColumns = @JoinColumn(name = "candidate_id") ) + private Set candidates; @OneToMany(mappedBy = "exam", cascade = CascadeType.ALL) @@ -104,4 +105,17 @@ public class Exam { @JsonIgnore 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/ExamCandidateGrade.java b/src/main/java/com/testify/Testify_Backend/model/ExamCandidateGrade.java new file mode 100644 index 0000000..3d52a44 --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/model/ExamCandidateGrade.java @@ -0,0 +1,40 @@ +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; + + public Long getId() { + return id; + } +} 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/model/ExamSetterOrganization.java b/src/main/java/com/testify/Testify_Backend/model/ExamSetterOrganization.java new file mode 100644 index 0000000..ade0bdb --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/model/ExamSetterOrganization.java @@ -0,0 +1,37 @@ +package com.testify.Testify_Backend.model; + +import jakarta.annotation.Nullable; +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 ExamSetterOrganization { + @Setter + @jakarta.persistence.Id + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) // Auto-generate the ID + @Column + private Long id; + + @Column(nullable = false) + private String organizationID; + + @Column(nullable = false) + private String examSetterID; + + private boolean isDeleted = false; + + public Long getId() { + return id; + } + +} 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..7889aaa --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/repository/ExamCandidateGradeRepository.java @@ -0,0 +1,16 @@ +package com.testify.Testify_Backend.repository; + +import com.testify.Testify_Backend.model.ExamCandidateGrade; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ExamCandidateGradeRepository extends JpaRepository { + +// write a query to get the exam candidate grade details along with exam name and candidate name + @Query("SELECT ecg FROM ExamCandidateGrade ecg") + List getExamCandidateGradeDetails(); +} diff --git a/src/main/java/com/testify/Testify_Backend/repository/ExamSetterOrganizationRepository.java b/src/main/java/com/testify/Testify_Backend/repository/ExamSetterOrganizationRepository.java new file mode 100644 index 0000000..f6149e0 --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/repository/ExamSetterOrganizationRepository.java @@ -0,0 +1,22 @@ +package com.testify.Testify_Backend.repository; + +import com.testify.Testify_Backend.model.ExamSetterOrganization; +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 ExamSetterOrganizationRepository extends JpaRepository { +// write a method to update is deleted to true by organizationID and examSetterID +// write a method to find by organizationID and examSetterID + ExamSetterOrganization findByOrganizationIDAndExamSetterID(String organizationID, String examSetterID); + List findByOrganizationID(String organizationID); + ExamSetterOrganization findByExamSetterID(String examSetterID); + + @Query("SELECT e.examSetterID FROM ExamSetterOrganization e WHERE e.organizationID = :organizationId AND e.isDeleted = false") + List findActiveExamSetterIDs(@Param("organizationId") String organizationId); + +} 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..177db92 --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/requests/exam_management/ExamCandidateGradeRequest.java @@ -0,0 +1,17 @@ +package com.testify.Testify_Backend.requests.exam_management; + +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; +} 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/ExamCandidateGradeResponse.java b/src/main/java/com/testify/Testify_Backend/responses/exam_management/ExamCandidateGradeResponse.java new file mode 100644 index 0000000..12a5319 --- /dev/null +++ b/src/main/java/com/testify/Testify_Backend/responses/exam_management/ExamCandidateGradeResponse.java @@ -0,0 +1,23 @@ +package com.testify.Testify_Backend.responses.exam_management; + + +import com.testify.Testify_Backend.model.Candidate; +import com.testify.Testify_Backend.model.Exam; +import lombok.*; + +@Data +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ExamCandidateGradeResponse { + + private String examID; + private String examTitle; + private String candidateName; + private String candidateID; + private String status; + private String grade; + private String score; +} 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..5167401 100644 --- a/src/main/java/com/testify/Testify_Backend/service/AuthenticationService.java +++ b/src/main/java/com/testify/Testify_Backend/service/AuthenticationService.java @@ -54,6 +54,8 @@ public class AuthenticationService { private final VerificationRequestRepository verificationRequestRepository; + private final ExamSetterOrganizationRepository examSetterOrganizationRepository; + private User user; @@ -114,6 +116,14 @@ public RegisterResponse register(@ModelAttribute RegistrationRequest request, bo invitation.setAccepted(true); examSetterInvitationRepository.save(invitation); }); + +// add to examSetterOrganization + ExamSetterOrganization examSetterOrganization = new ExamSetterOrganization(); + examSetterOrganization.setOrganizationID(String.valueOf(organization.getId())); + examSetterOrganization.setExamSetterID(String.valueOf(examSetter.getId())); + examSetterOrganizationRepository.save(examSetterOrganization); + + } else if (request.getRole().equals(UserRole.ORGANIZATION)) { System.out.println("Organization"); Organization organization = Organization.builder() @@ -217,10 +227,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 840874e..adc3303 100644 --- a/src/main/java/com/testify/Testify_Backend/service/CandidateService.java +++ b/src/main/java/com/testify/Testify_Backend/service/CandidateService.java @@ -10,6 +10,7 @@ import java.util.List; public interface CandidateService { + List getAllCandidatesForSearch(); // temp comment public List getCandidateExams(String status); 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 9ab7944..88ba83c 100644 --- a/src/main/java/com/testify/Testify_Backend/service/ExamManagementService.java +++ b/src/main/java/com/testify/Testify_Backend/service/ExamManagementService.java @@ -51,4 +51,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 89c297c..9786df4 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 CandidateExamAnswerRepository candidateExamAnswerRepository; private final MCQOptionRepository mcqOptionRepository; 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); @@ -789,6 +793,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; @@ -924,29 +933,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(); @@ -1335,7 +1339,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()); + } } diff --git a/src/main/java/com/testify/Testify_Backend/service/ExamSetterService.java b/src/main/java/com/testify/Testify_Backend/service/ExamSetterService.java index 1e06c7f..9389938 100644 --- a/src/main/java/com/testify/Testify_Backend/service/ExamSetterService.java +++ b/src/main/java/com/testify/Testify_Backend/service/ExamSetterService.java @@ -11,4 +11,6 @@ public interface ExamSetterService { long checkSetterRegistration(String token); GenericAddOrUpdateResponse addSetterToOrganization(String token); + + GenericAddOrUpdateResponse deleteSetter(String setterId, String organizationId); } diff --git a/src/main/java/com/testify/Testify_Backend/service/ExamSetterServiceImpl.java b/src/main/java/com/testify/Testify_Backend/service/ExamSetterServiceImpl.java index 7239611..e54b033 100644 --- a/src/main/java/com/testify/Testify_Backend/service/ExamSetterServiceImpl.java +++ b/src/main/java/com/testify/Testify_Backend/service/ExamSetterServiceImpl.java @@ -2,8 +2,10 @@ import com.testify.Testify_Backend.model.ExamSetter; import com.testify.Testify_Backend.model.ExamSetterInvitation; +import com.testify.Testify_Backend.model.ExamSetterOrganization; import com.testify.Testify_Backend.model.Organization; import com.testify.Testify_Backend.repository.ExamSetterInvitationRepository; +import com.testify.Testify_Backend.repository.ExamSetterOrganizationRepository; import com.testify.Testify_Backend.repository.ExamSetterRepository; import com.testify.Testify_Backend.repository.OrganizationRepository; import com.testify.Testify_Backend.responses.GenericAddOrUpdateResponse; @@ -27,10 +29,13 @@ public class ExamSetterServiceImpl implements ExamSetterService { @Autowired private ModelMapper modelMapper; + @Autowired + private ExamSetterOrganizationRepository examSetterOrganizationRepository; @Override public Set getOrganizations(long setterId) { Optional examSetter = examSetterRepository.findById(setterId); + Set organizationResponses = new HashSet<>(); if (examSetter.isPresent()) { Set organizations = examSetter.get().getOrganizations(); @@ -63,6 +68,12 @@ public GenericAddOrUpdateResponse addSetterToOrganization(String token) { examSetter.getOrganizations().add(organization); organization.getExamSetters().add(examSetter); +// add to examSetterOrganization + ExamSetterOrganization examSetterOrganization = new ExamSetterOrganization(); + examSetterOrganization.setOrganizationID(String.valueOf(organization.getId())); + examSetterOrganization.setExamSetterID(String.valueOf(examSetter.getId())); + examSetterOrganizationRepository.save(examSetterOrganization); + examSetterRepository.save(examSetter); organizationRepository.save(organization); @@ -70,4 +81,21 @@ public GenericAddOrUpdateResponse addSetterToOrganization(String token) { response.setMessage("Successfully added an organization"); return response; } + + @Override + public GenericAddOrUpdateResponse deleteSetter(String setterId, String organizationId) { + GenericAddOrUpdateResponse response = new GenericAddOrUpdateResponse(); + ExamSetterOrganization examSetter = examSetterOrganizationRepository.findByOrganizationIDAndExamSetterID(organizationId, setterId); + try{ + examSetter.setDeleted(true); + examSetterOrganizationRepository.save(examSetter); + response.setSuccess(true); + response.setMessage("Successfully deleted the setter"); + } + catch (Exception e){ + response.setSuccess(false); + response.setMessage("Failed to delete the setter"); + } + return response; + } } 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..3200ad8 100644 --- a/src/main/java/com/testify/Testify_Backend/service/GradingService.java +++ b/src/main/java/com/testify/Testify_Backend/service/GradingService.java @@ -1,8 +1,11 @@ package com.testify.Testify_Backend.service; import com.testify.Testify_Backend.model.CandidateExamSession; +import com.testify.Testify_Backend.model.ExamCandidateGrade; 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.exam_management.ExamCandidateGradeResponse; import java.util.List; import java.util.Map; @@ -11,4 +14,8 @@ public interface GradingService { List getEssayDetails(Long examId, Long userId); List getGradingSchemeForExam(Long examId); List> getQuestionAndOptionBySessionId(Long sessionId); + + String setExamCandidateGrade(ExamCandidateGradeRequest examCandidateGradeRequest); + + List getExamCandidateGrade(); } 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..6ffec7c 100644 --- a/src/main/java/com/testify/Testify_Backend/service/GradingServiceImpl.java +++ b/src/main/java/com/testify/Testify_Backend/service/GradingServiceImpl.java @@ -1,16 +1,16 @@ 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 com.testify.Testify_Backend.responses.exam_management.ExamCandidateGradeResponse; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -25,6 +25,9 @@ public class GradingServiceImpl implements GradingService { private final CandidateExamAnswerRepository candidateExamAnswerRepository; private final GradeRepository gradeRepository; private final ExamSessionRepository examSessionRepository; + private final ExamCandidateGradeRepository examCandidateGradeRepository; + private final ExamRepository examRepository; + private final CandidateRepository candidateRepository; @Override @Transactional @@ -108,5 +111,44 @@ 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"; + } + + @Override + @Transactional + public List getExamCandidateGrade() { +// initialize the response list + List examCandidateGradeResponses = new ArrayList<>(); + List examCandidateGrades = examCandidateGradeRepository.findAll(); + if (examCandidateGrades == null || examCandidateGrades.isEmpty()) { + throw new IllegalArgumentException("No exam candidate grades found"); + } + examCandidateGrades.forEach(examCandidateGrade -> { + ExamCandidateGradeResponse response = new ExamCandidateGradeResponse(); + Exam exam = examRepository.findById(Long.parseLong(examCandidateGrade.getExamID().toString())).orElseThrow(() -> new IllegalArgumentException("Exam not found")); + Candidate candidate = candidateRepository.findById(Long.parseLong(examCandidateGrade.getCandidateID().toString())).orElseThrow(() -> new IllegalArgumentException("Candidate not found")); + response.setExamID(String.valueOf(exam.getId())); + response.setCandidateID(String.valueOf(candidate.getId())); + response.setExamTitle(exam.getTitle()); + response.setCandidateName(candidate.getFirstName() + " " + candidate.getLastName()); + response.setStatus(examCandidateGrade.getStatus()); + response.setGrade(examCandidateGrade.getGrade()); + response.setScore(examCandidateGrade.getScore()); + + examCandidateGradeResponses.add(response); + }); + + return examCandidateGradeResponses; + } } diff --git a/src/main/java/com/testify/Testify_Backend/service/OrganizationServiceImpl.java b/src/main/java/com/testify/Testify_Backend/service/OrganizationServiceImpl.java index bc51055..e97e41d 100644 --- a/src/main/java/com/testify/Testify_Backend/service/OrganizationServiceImpl.java +++ b/src/main/java/com/testify/Testify_Backend/service/OrganizationServiceImpl.java @@ -49,6 +49,8 @@ public class OrganizationServiceImpl implements OrganizationService{ @Autowired private ModelMapper modelMapper; + @Autowired + private ExamSetterOrganizationRepository examSetterOrganizationRepository; @Override public ResponseEntity addSetterToOrganization(long organizationId, AddExamSetterRequest request) { @@ -173,6 +175,7 @@ public Set getCourseModulesByOrganization(Long organizatio .collect(Collectors.toSet()); } @Override + @Transactional public GenericAddOrUpdateResponse createCandidateGroup(long organizationId, CandidateGroupRequest candidateGroupRequest){ GenericAddOrUpdateResponse response = new GenericAddOrUpdateResponse<>(); @@ -196,6 +199,7 @@ public GenericAddOrUpdateResponse createCandidateGroup(lo } + @Transactional public Set getCandidateGroupsByOrganization(Long organizationId) { Set candidateGroupsResponse = new HashSet<>(); Set candidateGroups = new HashSet<>(); @@ -207,6 +211,7 @@ public Set getCandidateGroupsByOrganization(Long organiz } @Override + @Transactional public GenericAddOrUpdateResponse addCandidateToGroup(long groupId, String name, String email) { GenericAddOrUpdateResponse response = new GenericAddOrUpdateResponse<>(); CandidateGroup candidateGroup = candidateGroupRepository.findById(groupId).orElseThrow(() -> new IllegalArgumentException("Group not found")); @@ -220,6 +225,7 @@ public GenericAddOrUpdateResponse addCandidateToGroup(long groupId, String name, } @Override + @Transactional public GenericDeleteResponse deleteGroup(long groupId) { CandidateGroup candidateGroup = candidateGroupRepository.findById(groupId).orElseThrow(() -> new IllegalArgumentException("Group not found")); candidateGroupRepository.delete(candidateGroup); @@ -230,6 +236,7 @@ public GenericDeleteResponse deleteGroup(long groupId) { } @Override + @Transactional public GenericDeleteResponse deleteCandidate(long groupId, long candidateId) { CandidateGroup candidateGroup = candidateGroupRepository.findById(groupId).orElseThrow(() -> new IllegalArgumentException("Group not found")); candidateGroup.getCandidates().removeIf(candidate -> candidate.getId() == candidateId); @@ -241,6 +248,7 @@ public GenericDeleteResponse deleteCandidate(long groupId, long candidateId) { } @Override + @Transactional public GenericAddOrUpdateResponse updateCandidateGroup(long groupId, String groupName) { CandidateGroup candidateGroup = candidateGroupRepository.findById(groupId).orElseThrow(() -> new IllegalArgumentException("Group not found")); candidateGroup.setName(groupName); @@ -253,8 +261,18 @@ public GenericAddOrUpdateResponse updateCandidateGroup(long groupId, String grou @Override public Set getExamSetters(long organizationId) { - Organization organization = organizationRepository.findById(organizationId).orElseThrow(() -> new IllegalArgumentException("Organization not found")); - return organization.getExamSetters(); + // Fetch the organization and its associated ExamSetters + Organization organization = organizationRepository.findById(organizationId) + .orElseThrow(() -> new IllegalArgumentException("Organization not found")); + + // Fetch active ExamSetter IDs from ExamSetterOrganization table + List activeExamSetterIDs = examSetterOrganizationRepository.findActiveExamSetterIDs(String.valueOf(organizationId)); + + // Filter ExamSetters linked to the organization based on active IDs + return organization.getExamSetters().stream() + .filter(examSetter -> activeExamSetterIDs.contains(String.valueOf(examSetter.getId()))) + .collect(Collectors.toSet()); + } @Override