diff --git a/os-server/src/main/java/app/openschool/common/security/SecurityConfiguration.java b/os-server/src/main/java/app/openschool/common/security/SecurityConfiguration.java index 6435ac03..2a8077e2 100644 --- a/os-server/src/main/java/app/openschool/common/security/SecurityConfiguration.java +++ b/os-server/src/main/java/app/openschool/common/security/SecurityConfiguration.java @@ -27,6 +27,9 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { "/api/v1/categories/**", "/api/v1/mentors/**", "/api/v1/courses/**", + "/api/v1/module/*/quizzes/**", + "/api/v1/quiz/*/questions/**", + "/api/v1/courses/**", "/api/v1/courses/peers-questions/**", "/api/v1/courses/mentor-questions/**", "/api/v1/courses/peers-questions/answer/**", diff --git a/os-server/src/main/java/app/openschool/course/api/mapper/CourseMapper.java b/os-server/src/main/java/app/openschool/course/api/mapper/CourseMapper.java index 8014d2da..89a4d29a 100644 --- a/os-server/src/main/java/app/openschool/course/api/mapper/CourseMapper.java +++ b/os-server/src/main/java/app/openschool/course/api/mapper/CourseMapper.java @@ -11,6 +11,7 @@ import app.openschool.course.module.EnrolledModule; import app.openschool.course.module.Module; import app.openschool.course.module.item.ModuleItem; +import app.openschool.course.module.quiz.EnrolledQuiz; import app.openschool.course.status.CourseStatus; import app.openschool.user.User; import app.openschool.user.api.mapper.MentorMapper; diff --git a/os-server/src/main/java/app/openschool/course/api/mapper/EnrolledModuleMapper.java b/os-server/src/main/java/app/openschool/course/api/mapper/EnrolledModuleMapper.java index 4f0d77c3..69a3e0e0 100644 --- a/os-server/src/main/java/app/openschool/course/api/mapper/EnrolledModuleMapper.java +++ b/os-server/src/main/java/app/openschool/course/api/mapper/EnrolledModuleMapper.java @@ -5,6 +5,8 @@ import app.openschool.course.module.EnrolledModule; import app.openschool.course.module.item.EnrolledModuleItem; import app.openschool.course.module.item.status.ModuleItemStatus; +import app.openschool.course.module.quiz.EnrolledQuiz; +import app.openschool.course.module.quiz.status.QuizStatus; import app.openschool.course.module.status.ModuleStatus; import java.util.Set; import java.util.stream.Collectors; @@ -19,10 +21,10 @@ public static Set toEnrolledModules( .collect(Collectors.toSet()); return enrolledModules.stream() - .map( + .peek( enrolledModule -> { enrolledModule.setEnrolledModuleItems(getEnrolledModuleItems(enrolledModule)); - return enrolledModule; + enrolledModule.setEnrolledQuizzes(getEnrolledQuiz(enrolledModule)); }) .collect(Collectors.toSet()); } @@ -34,4 +36,10 @@ private static Set getEnrolledModuleItems(EnrolledModule enr new EnrolledModuleItem(moduleItem, enrolledModule, ModuleItemStatus.inProgress())) .collect(Collectors.toSet()); } + + private static Set getEnrolledQuiz(EnrolledModule enrolledModule) { + return enrolledModule.getModule().getQuizzes().stream() + .map(quiz -> new EnrolledQuiz(QuizStatus.inProgress(), quiz, enrolledModule)) + .collect(Collectors.toSet()); + } } diff --git a/os-server/src/main/java/app/openschool/course/module/EnrolledModule.java b/os-server/src/main/java/app/openschool/course/module/EnrolledModule.java index 022a1cb6..632d0054 100644 --- a/os-server/src/main/java/app/openschool/course/module/EnrolledModule.java +++ b/os-server/src/main/java/app/openschool/course/module/EnrolledModule.java @@ -2,6 +2,7 @@ import app.openschool.course.EnrolledCourse; import app.openschool.course.module.item.EnrolledModuleItem; +import app.openschool.course.module.quiz.EnrolledQuiz; import app.openschool.course.module.status.ModuleStatus; import java.util.Set; import javax.persistence.CascadeType; @@ -39,6 +40,9 @@ public class EnrolledModule { @OneToMany(cascade = CascadeType.MERGE, mappedBy = "enrolledModule") private Set enrolledModuleItems; + @OneToMany(cascade = CascadeType.MERGE, mappedBy = "enrolledModule") + private Set enrolledQuizzes; + public EnrolledModule() {} public EnrolledModule(Module module, ModuleStatus moduleStatus, EnrolledCourse enrolledCourse) { @@ -52,12 +56,14 @@ public EnrolledModule( Module module, EnrolledCourse enrolledCourse, ModuleStatus moduleStatus, - Set enrolledModuleItems) { + Set enrolledModuleItems, + Set enrolledQuizzes) { this.id = id; this.module = module; this.enrolledCourse = enrolledCourse; this.moduleStatus = moduleStatus; this.enrolledModuleItems = enrolledModuleItems; + this.enrolledQuizzes = enrolledQuizzes; } public Long getId() { @@ -88,6 +94,14 @@ public ModuleStatus getModuleStatus() { return moduleStatus; } + public Set getEnrolledQuizzes() { + return enrolledQuizzes; + } + + public void setEnrolledQuizzes(Set enrolledQuizzes) { + this.enrolledQuizzes = enrolledQuizzes; + } + public void setModuleStatus(ModuleStatus moduleStatus) { this.moduleStatus = moduleStatus; } diff --git a/os-server/src/main/java/app/openschool/course/module/Module.java b/os-server/src/main/java/app/openschool/course/module/Module.java index f77ba900..0d39b69c 100644 --- a/os-server/src/main/java/app/openschool/course/module/Module.java +++ b/os-server/src/main/java/app/openschool/course/module/Module.java @@ -2,6 +2,7 @@ import app.openschool.course.Course; import app.openschool.course.module.item.ModuleItem; +import app.openschool.course.module.quiz.Quiz; import java.util.HashSet; import java.util.Set; import javax.persistence.CascadeType; @@ -37,16 +38,20 @@ public class Module { @OneToMany(cascade = CascadeType.ALL, mappedBy = "module") private Set moduleItems = new HashSet<>(); + @OneToMany(mappedBy = "module", cascade = CascadeType.ALL) + private Set quizzes; + public Module() {} public Module(Course course) { this.course = course; } - public Module(Long id, Course course, Set moduleItems) { + public Module(Long id, Course course, Set moduleItems, Set quizzes) { this.id = id; this.course = course; this.moduleItems = moduleItems; + this.quizzes = quizzes; } public Long getId() { @@ -88,4 +93,12 @@ public String getDescription() { public void setDescription(String description) { this.description = description; } + + public Set getQuizzes() { + return quizzes; + } + + public void setQuizzes(Set quizzes) { + this.quizzes = quizzes; + } } diff --git a/os-server/src/main/java/app/openschool/course/module/ModuleRepository.java b/os-server/src/main/java/app/openschool/course/module/ModuleRepository.java index 1cf16073..a56eff88 100644 --- a/os-server/src/main/java/app/openschool/course/module/ModuleRepository.java +++ b/os-server/src/main/java/app/openschool/course/module/ModuleRepository.java @@ -1,6 +1,5 @@ package app.openschool.course.module; -import app.openschool.course.module.Module; import org.springframework.data.jpa.repository.JpaRepository; public interface ModuleRepository extends JpaRepository {} \ No newline at end of file diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/EnrolledQuiz.java b/os-server/src/main/java/app/openschool/course/module/quiz/EnrolledQuiz.java new file mode 100644 index 00000000..c7a913f2 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/EnrolledQuiz.java @@ -0,0 +1,85 @@ +package app.openschool.course.module.quiz; + +import app.openschool.course.module.EnrolledModule; +import app.openschool.course.module.quiz.status.QuizStatus; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name = "enrolled_quiz") +public class EnrolledQuiz { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "student_grade") + private int studentGrade = 0; + + @ManyToOne + @JoinColumn(name = "quiz_status_id") + private QuizStatus quizStatus; + + @ManyToOne + @JoinColumn(name = "quiz_id") + private Quiz quiz; + + @ManyToOne + @JoinColumn(name = "enrolled_module_id") + private EnrolledModule enrolledModule; + + public EnrolledQuiz() {} + + public EnrolledQuiz(QuizStatus quizStatus, Quiz quiz, EnrolledModule enrolledModule) { + this.quizStatus = quizStatus; + this.quiz = quiz; + this.enrolledModule = enrolledModule; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public QuizStatus getQuizStatus() { + return quizStatus; + } + + public void setQuizStatus(QuizStatus quizStatus) { + this.quizStatus = quizStatus; + } + + public Quiz getQuiz() { + return quiz; + } + + public void setQuiz(Quiz quiz) { + this.quiz = quiz; + } + + public EnrolledModule getEnrolledModule() { + return enrolledModule; + } + + public void setEnrolledModule(EnrolledModule enrolledModule) { + this.enrolledModule = enrolledModule; + } + + public int getStudentGrade() { + return studentGrade; + } + + public void setStudentGrade(int studentGrade) { + this.studentGrade = studentGrade; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/EnrolledQuizRepository.java b/os-server/src/main/java/app/openschool/course/module/quiz/EnrolledQuizRepository.java new file mode 100644 index 00000000..5ad4b100 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/EnrolledQuizRepository.java @@ -0,0 +1,5 @@ +package app.openschool.course.module.quiz; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface EnrolledQuizRepository extends JpaRepository {} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/Quiz.java b/os-server/src/main/java/app/openschool/course/module/quiz/Quiz.java new file mode 100644 index 00000000..368a20da --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/Quiz.java @@ -0,0 +1,104 @@ +package app.openschool.course.module.quiz; + +import app.openschool.course.module.Module; +import app.openschool.course.module.quiz.question.Question; +import java.util.Set; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name = "quiz") +public class Quiz { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "title", nullable = false) + private String title; + + @Column(name = "description", nullable = false) + private String description; + + @Column(name = "max_grade", nullable = false) + private int maxGrade; + + @Column(name = "passing_score", nullable = false) + private int passingScore; + + @OneToMany(mappedBy = "quiz", cascade = CascadeType.ALL) + private Set questions; + + @ManyToOne + @JoinColumn(name = "module_id") + private Module module; + + public static Quiz getInstance() { + return new Quiz(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public int getMaxGrade() { + return maxGrade; + } + + public void setMaxGrade(int maxGrade) { + this.maxGrade = maxGrade; + } + + public int getPassingScore() { + return passingScore; + } + + public void setPassingScore(int passingScore) { + this.passingScore = passingScore; + } + + public Set getQuestions() { + return questions; + } + + public void setQuestions(Set questions) { + this.questions = questions; + } + + public Module getModule() { + return module; + } + + public void setModule(Module module) { + this.module = module; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java b/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java new file mode 100644 index 00000000..017e5cac --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java @@ -0,0 +1,258 @@ +package app.openschool.course.module.quiz; + +import app.openschool.common.exceptionhandler.exception.PermissionDeniedException; +import app.openschool.common.response.ResponseMessage; +import app.openschool.course.module.Module; +import app.openschool.course.module.ModuleRepository; +import app.openschool.course.module.quiz.api.dto.CreateQuizDto; +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentRequestDto; +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentResponseDto; +import app.openschool.course.module.quiz.api.dto.ModifyQuizDataRequest; +import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.api.mapper.QuizMapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import java.security.Principal; +import java.util.Locale; +import javax.validation.Valid; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/module/{moduleId}/quizzes") +public class QuizController { + + private final QuizService quizService; + private final ModuleRepository moduleRepository; + + public QuizController(QuizService quizService, ModuleRepository moduleRepository) { + this.quizService = quizService; + this.moduleRepository = moduleRepository; + } + + @Operation( + summary = "create quiz for the module", + security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses( + value = { + @ApiResponse( + responseCode = "201", + description = "Creates a quiz for the module and returns it"), + @ApiResponse( + responseCode = "404", + description = "Invalid module id supplied", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "403", + description = + "If the user have not mentor role, request body is not built correctly, etc.", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "400", + description = "if the module doesn't belong to the current user", + content = @Content(schema = @Schema())) + }) + @PreAuthorize("hasAuthority('MENTOR')") + @PostMapping() + public ResponseEntity createQuiz( + @Parameter(description = "Id of the module for which the quiz will be created") @PathVariable + Long moduleId, + Principal principal, + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "Request object to create a quiz for a specific module") + @RequestBody + CreateQuizDto createQuizDto) { + checkPermission(moduleId, principal); + return quizService + .createQuiz(moduleId, createQuizDto) + .map(quiz -> ResponseEntity.status(HttpStatus.CREATED).body(QuizMapper.quizToQuizDto(quiz))) + .orElse(ResponseEntity.notFound().build()); + } + + @Operation( + summary = "modify data of quizzes", + security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Modifies provided field or fields of quiz and returns updated quiz"), + @ApiResponse( + responseCode = "400", + description = + "Invalid quiz id, invalid new parent quiz id or" + + " invalid or existing quiz title supplied", + content = @Content(schema = @Schema(implementation = ResponseMessage.class))), + @ApiResponse( + responseCode = "403", + description = "Only user with admin role has access to this method", + content = @Content(schema = @Schema())) + }) + @PatchMapping(value = "/{id}") + @PreAuthorize("hasAuthority('MENTOR')") + public ResponseEntity updateQuiz( + @Parameter(description = "Id of quiz which fields will be modified") + @PathVariable(value = "id") + Long quizId, + @Parameter(description = "Id of the module to which this quiz belongs") @PathVariable() + Long moduleId, + Principal principal, + @Parameter( + description = + "Request object for modifying quiz, " + + "which includes new title,new description that must be unique ") + @Valid + @RequestBody + ModifyQuizDataRequest request) { + checkPermission(moduleId, principal); + return ResponseEntity.ok() + .body(QuizMapper.quizToQuizDto(quizService.updateQuiz(quizId, request))); + } + + @Operation(summary = "delete quiz", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses( + value = { + @ApiResponse(responseCode = "204", description = "quiz successfully deleted"), + @ApiResponse( + responseCode = "404", + description = "Invalid quiz id supplied", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "403", + description = "If the user have not mentor role", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "400", + description = "if the quiz doesn't belong to the current user", + content = @Content(schema = @Schema())) + }) + @PreAuthorize("hasAuthority('MENTOR')") + @DeleteMapping("/{quizId}") + public ResponseEntity deleteQuiz( + @Parameter(description = "Id of the quiz which will be deleted") @PathVariable Long quizId, + @Parameter(description = "Id of the module to which this quiz belongs") @PathVariable + Long moduleId, + Principal principal) { + System.out.println(principal.getName()); + checkPermission(moduleId, principal); + if (quizService.deleteQuiz(quizId)) { + return ResponseEntity.noContent().build(); + } else { + return ResponseEntity.notFound().build(); + } + } + + @Operation(summary = "find quiz by quiz id", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Get details and information about the quiz"), + @ApiResponse( + responseCode = "400", + description = "Invalid quiz id ", + content = @Content(schema = @Schema(implementation = ResponseMessage.class))), + @ApiResponse( + responseCode = "401", + description = " Lacks valid authentication credentials for the requested resource", + content = @Content(schema = @Schema())) + }) + @GetMapping("/{quizId}") + public ResponseEntity findById( + @Parameter(description = "Id of the quiz which will be find") @PathVariable Long quizId, + @Parameter(description = "Id of the module to which this quiz belongs") @PathVariable() + Long moduleId) { + return ResponseEntity.ok(quizService.findById(quizId)); + } + + @Operation( + summary = "find quiz by module id", + security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Get details and information about the quiz"), + @ApiResponse( + responseCode = "400", + description = "Invalid module id ", + content = @Content(schema = @Schema(implementation = ResponseMessage.class))), + @ApiResponse( + responseCode = "401", + description = " Lacks valid authentication credentials for the requested resource", + content = @Content(schema = @Schema())) + }) + @GetMapping() + public ResponseEntity> findAllByModuleId( + @Parameter(description = "Module ID that belongs to the quizzes being searched") @PathVariable + Long moduleId, + @Parameter( + description = + "Includes parameters page, size, and sort which is not required. " + + "Page results page you want to retrieve (0..N). " + + "Size is count of records per page(1..N). " + + "Sorting criteria in the format: property(,asc|desc). " + + "Default sort order is ascending. " + + "Multiple sort criteria are supported.") + Pageable pageable) { + + return ResponseEntity.ok(quizService.findAllByModuleId(moduleId, pageable)); + } + + @Operation( + summary = "complete enrolled quiz", + security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "Complete enrolled quiz"), + @ApiResponse( + responseCode = "400", + description = "Invalid enrolled quiz Id ", + content = @Content(schema = @Schema(implementation = ResponseMessage.class))), + @ApiResponse( + responseCode = "401", + description = " Lacks valid authentication credentials for the requested resource", + content = @Content(schema = @Schema())) + }) + @PostMapping("/enrolledQuizzes/{enrolledQuizId}") + public ResponseEntity completeEnrolledQuiz( + @Parameter(description = "Id of the enrolled quiz which will be completed") @PathVariable + Long enrolledQuizId, + @Parameter(description = "Id of the module to which this quiz belongs") @PathVariable() + Long moduleId, + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "Request object which contains questions and chosen answer options") + @RequestBody + EnrolledQuizAssessmentRequestDto enrolledQuizAssessmentRequestDto, + Locale locale) { + return quizService + .completeEnrolledQuiz(enrolledQuizId, enrolledQuizAssessmentRequestDto, locale) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + private void checkPermission(Long moduleId, Principal principal) { + System.out.println("started"); + Module module = moduleRepository.findById(moduleId).orElseThrow(IllegalArgumentException::new); + if (!module.getCourse().getMentor().getEmail().equals(principal.getName())) { + System.out.println(principal.getName()); + throw new PermissionDeniedException("permission.denied"); + } + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/QuizRepository.java b/os-server/src/main/java/app/openschool/course/module/quiz/QuizRepository.java new file mode 100644 index 00000000..dfaf13d2 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/QuizRepository.java @@ -0,0 +1,10 @@ +package app.openschool.course.module.quiz; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface QuizRepository extends JpaRepository { + Page findAllByModuleId(Long id, Pageable pageable); +} + diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/QuizService.java b/os-server/src/main/java/app/openschool/course/module/quiz/QuizService.java new file mode 100644 index 00000000..26b73398 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/QuizService.java @@ -0,0 +1,29 @@ +package app.openschool.course.module.quiz; + +import app.openschool.course.module.quiz.api.dto.CreateQuizDto; +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentRequestDto; +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentResponseDto; +import app.openschool.course.module.quiz.api.dto.ModifyQuizDataRequest; +import app.openschool.course.module.quiz.api.dto.QuizDto; +import java.util.Locale; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface QuizService { + + Optional createQuiz(Long moduleId, CreateQuizDto createQuizDto); + + Boolean deleteQuiz(Long quizId); + + QuizDto findById(Long id); + + Page findAllByModuleId(Long id, Pageable pageable); + + Optional completeEnrolledQuiz( + Long enrolledQuizId, + EnrolledQuizAssessmentRequestDto enrolledQuizAssessmentRequestDto, + Locale locale); + + Quiz updateQuiz(Long id, ModifyQuizDataRequest request); +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/QuizServiceImpl.java b/os-server/src/main/java/app/openschool/course/module/quiz/QuizServiceImpl.java new file mode 100644 index 00000000..6481ece4 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/QuizServiceImpl.java @@ -0,0 +1,185 @@ +package app.openschool.course.module.quiz; + +import app.openschool.common.exceptionhandler.exception.PermissionDeniedException; +import app.openschool.course.module.Module; +import app.openschool.course.module.ModuleRepository; +import app.openschool.course.module.quiz.api.dto.CreateQuizDto; +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentRequestDto; +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentResponseDto; +import app.openschool.course.module.quiz.api.dto.ModifyQuizDataRequest; +import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.api.mapper.EnrolledQuizAssessmentResponseMapper; +import app.openschool.course.module.quiz.api.mapper.QuizMapper; +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.status.QuizStatus; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import javax.transaction.Transactional; +import org.springframework.context.MessageSource; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +public class QuizServiceImpl implements QuizService { + + private final ModuleRepository moduleRepository; + private final QuizRepository quizRepository; + private final EnrolledQuizRepository enrolledQuizRepository; + private final MessageSource messageSource; + + public QuizServiceImpl( + ModuleRepository moduleRepository, + QuizRepository quizRepository, + EnrolledQuizRepository enrolledQuizRepository, + MessageSource messageSource) { + this.moduleRepository = moduleRepository; + this.quizRepository = quizRepository; + this.enrolledQuizRepository = enrolledQuizRepository; + this.messageSource = messageSource; + } + + @Override + public Optional createQuiz(Long moduleId, CreateQuizDto createQuizDto) { + return moduleRepository + .findById(moduleId) + .map( + module -> { + Quiz quiz = QuizMapper.createQuizDtoToQuiz(createQuizDto, module); + return quizRepository.save(quiz); + }); + } + + @Override + public Boolean deleteQuiz(Long quizId) { + return quizRepository + .findById(quizId) + .map( + quiz -> { + quizRepository.delete(quiz); + return true; + }) + .orElse(false); + } + + @Override + public QuizDto findById(Long id) { + return QuizMapper.quizToQuizDto( + quizRepository.findById(id).orElseThrow(IllegalArgumentException::new)); + } + + @Override + public Page findAllByModuleId(Long id, Pageable pageable) { + return QuizMapper.toQuizDtoPage(quizRepository.findAllByModuleId(id, pageable)); + } + + @Override + @Transactional + public Optional completeEnrolledQuiz( + Long enrolledQuizId, + EnrolledQuizAssessmentRequestDto enrolledQuizAssessmentRequestDto, + Locale locale) { + return enrolledQuizRepository + .findById(enrolledQuizId) + .map(enrolledQuiz -> completeQuiz(enrolledQuiz, enrolledQuizAssessmentRequestDto, locale)); + } + + @Override + public Quiz updateQuiz(Long id, ModifyQuizDataRequest request) { + Quiz quiz = quizRepository.findById(id).orElseThrow(IllegalArgumentException::new); + if (Objects.nonNull(request.getTitle())) { + quiz.setTitle(request.getTitle()); + } + if (Objects.nonNull(request.getDescription())) { + quiz.setDescription(request.getDescription()); + } + if (request.getPassingScore() != -1) { + quiz.setPassingScore(request.getPassingScore()); + } + if (request.getMaxGrade() != -1) { + quiz.setMaxGrade(request.getMaxGrade()); + } + + return quizRepository.save(quiz); + } + + private EnrolledQuizAssessmentResponseDto completeQuiz( + EnrolledQuiz enrolledQuiz, + EnrolledQuizAssessmentRequestDto enrolledQuizAssessmentRequestDto, + Locale locale) { + AtomicInteger rightAnswers = + checkQuestionsAndReturnRightAnswersCount( + enrolledQuiz.getQuiz(), enrolledQuizAssessmentRequestDto); + String assessmentResult = + getAssessmentResult(rightAnswers.get(), enrolledQuiz.getQuiz(), locale); + updateEnrolledQuiz(rightAnswers.get(), enrolledQuiz); + return EnrolledQuizAssessmentResponseMapper.toEnrolledQuizAssessmentResponseDto( + rightAnswers.get(), + assessmentResult, + enrolledQuiz.getQuiz().getPassingScore(), + enrolledQuiz.getQuiz().getMaxGrade()); + } + + private void updateEnrolledQuiz(int studentGrade, EnrolledQuiz enrolledQuiz) { + enrolledQuiz.setStudentGrade(studentGrade); + if (studentGrade >= enrolledQuiz.getQuiz().getPassingScore()) { + enrolledQuiz.setQuizStatus(QuizStatus.isCompleted()); + } else { + enrolledQuiz.setQuizStatus(QuizStatus.isFailed()); + } + enrolledQuizRepository.save(enrolledQuiz); + } + + private String getAssessmentResult(int rightAnswers, Quiz quiz, Locale locale) { + return quiz.getPassingScore() <= rightAnswers + ? messageSource.getMessage("quiz.completed", null, locale) + : messageSource.getMessage("quiz.failed", null, locale); + } + + private AtomicInteger checkQuestionsAndReturnRightAnswersCount( + Quiz quiz, EnrolledQuizAssessmentRequestDto enrolledQuizAssessmentRequestDto) { + AtomicInteger rightAnswers = new AtomicInteger(0); + quiz.getQuestions() + .forEach( + question -> { + if (checkQuestion(question, enrolledQuizAssessmentRequestDto)) { + rightAnswers.getAndIncrement(); + } + }); + return rightAnswers; + } + + private boolean checkQuestion( + Question question, EnrolledQuizAssessmentRequestDto enrolledQuizAssessmentRequestDto) { + AtomicBoolean isRightAnswer = new AtomicBoolean(false); + enrolledQuizAssessmentRequestDto + .getQuestionWithChosenAnswerDtoSet() + .forEach( + questionWithChosenAnswerDto -> { + if (Objects.equals(questionWithChosenAnswerDto.getQuestionId(), question.getId())) { + isRightAnswer.set( + checkQuestionAnswers( + question, questionWithChosenAnswerDto.getChosenAnswersIds())); + } + }); + return isRightAnswer.get(); + } + + private boolean checkQuestionAnswers(Question question, List chosenAnswerIds) { + AtomicInteger rightAnswers = new AtomicInteger(); + question + .getAnswerOptions() + .forEach( + answerOption -> { + if (chosenAnswerIds.contains(answerOption.getId()) && answerOption.isRightAnswer()) { + rightAnswers.getAndIncrement(); + } + }); + return rightAnswers.get() == question.getRightAnswersCount(); + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/CreateQuizDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/CreateQuizDto.java new file mode 100644 index 00000000..d9391e2b --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/CreateQuizDto.java @@ -0,0 +1,70 @@ +package app.openschool.course.module.quiz.api.dto; + +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Set; + +public class CreateQuizDto { + + @Schema(description = "Maximum possible grade", example = "10") + private int maxGrade; + + @Schema(description = "The minimum score to pass quiz", example = "7") + private int passingScore; + + @Schema(description = "Quiz title", example = "Quiz about ArrayList ") + private String title; + + @Schema( + description = "Quiz description", + example = "With the ArrayList quiz you must answer at least 70% of the questions") + private String description; + + @ArraySchema(schema = @Schema(implementation = CreateQuestionDto.class)) + private Set questions; + + public static CreateQuizDto getInstance() { + return new CreateQuizDto(); + } + + public int getMaxGrade() { + return maxGrade; + } + + public void setMaxGrade(int maxGrade) { + this.maxGrade = maxGrade; + } + + public int getPassingScore() { + return passingScore; + } + + public void setPassingScore(int passingScore) { + this.passingScore = passingScore; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Set getQuestions() { + return questions; + } + + public void setQuestions(Set questions) { + this.questions = questions; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizAssessmentRequestDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizAssessmentRequestDto.java new file mode 100644 index 00000000..56327785 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizAssessmentRequestDto.java @@ -0,0 +1,22 @@ +package app.openschool.course.module.quiz.api.dto; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Set; + +public class EnrolledQuizAssessmentRequestDto { + + @ArraySchema(schema = @Schema(implementation = QuestionWithChosenAnswerDto.class)) + private Set questionWithChosenAnswerDtoSet; + + + + public Set getQuestionWithChosenAnswerDtoSet() { + return questionWithChosenAnswerDtoSet; + } + + public void setQuestionWithChosenAnswerDtoSet( + Set questionWithChosenAnswerDtoSet) { + this.questionWithChosenAnswerDtoSet = questionWithChosenAnswerDtoSet; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizAssessmentResponseDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizAssessmentResponseDto.java new file mode 100644 index 00000000..c014565e --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizAssessmentResponseDto.java @@ -0,0 +1,80 @@ +package app.openschool.course.module.quiz.api.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Objects; + +public class EnrolledQuizAssessmentResponseDto { + + @Schema(description = "Right answers count", example = "6") + private int rightAnswersCount; + + @Schema(description = "Assessment result", example = "FAILED") + private String assessmentResult; + + @Schema(description = "The minimum score to pass quiz", example = "7") + private int passingScore; + + @Schema(description = "Maximum possible grade", example = "10") + private int maxGrade; + + public EnrolledQuizAssessmentResponseDto() {} + + public EnrolledQuizAssessmentResponseDto( + int rightAnswersCount, String assessmentResult, int passingScore, int maxGrade) { + this.rightAnswersCount = rightAnswersCount; + this.assessmentResult = assessmentResult; + this.passingScore = passingScore; + this.maxGrade = maxGrade; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EnrolledQuizAssessmentResponseDto that = (EnrolledQuizAssessmentResponseDto) o; + return rightAnswersCount == that.rightAnswersCount + && passingScore == that.passingScore + && maxGrade == that.maxGrade + && Objects.equals(assessmentResult, that.assessmentResult); + } + + public static EnrolledQuizAssessmentResponseDto getInstant() { + return new EnrolledQuizAssessmentResponseDto(); + } + + public int getRightAnswersCount() { + return rightAnswersCount; + } + + public void setRightAnswersCount(int rightAnswersCount) { + this.rightAnswersCount = rightAnswersCount; + } + + public String getAssessmentResult() { + return assessmentResult; + } + + public void setAssessmentResult(String assessmentResult) { + this.assessmentResult = assessmentResult; + } + + public int getPassingScore() { + return passingScore; + } + + public void setPassingScore(int passingScore) { + this.passingScore = passingScore; + } + + public int getMaxGrade() { + return maxGrade; + } + + public void setMaxGrade(int maxGrade) { + this.maxGrade = maxGrade; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizDto.java new file mode 100644 index 00000000..f72375c2 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizDto.java @@ -0,0 +1,92 @@ +package app.openschool.course.module.quiz.api.dto; + +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Set; + +public class EnrolledQuizDto { + + @Schema(description = "Enrolled quiz id", example = "1") + private Long id; + + @Schema( + description = "Id of the enrolled module to which the enrolled quiz belongs", + example = "1") + private Long enrolledModuleId; + + @Schema(description = "Maximum possible grade", example = "10") + private int maxGrade; + + @Schema(description = "The student score", example = "9") + private int studentGrade; + + @Schema(description = "The quiz status", example = "FAILED") + private String quizStatus; + + @Schema(description = "The minimum score to pass quiz", example = "7") + private int passingScore; + + @ArraySchema(schema = @Schema(implementation = QuestionDto.class)) + private Set questions; + + public static EnrolledQuizDto getInstance() { + return new EnrolledQuizDto(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getEnrolledModuleId() { + return enrolledModuleId; + } + + public void setEnrolledModuleId(Long enrolledModuleId) { + this.enrolledModuleId = enrolledModuleId; + } + + public int getMaxGrade() { + return maxGrade; + } + + public void setMaxGrade(int maxGrade) { + this.maxGrade = maxGrade; + } + + public int getStudentGrade() { + return studentGrade; + } + + public void setStudentGrade(int studentGrade) { + this.studentGrade = studentGrade; + } + + public int getPassingScore() { + return passingScore; + } + + public void setPassingScore(int passingScore) { + this.passingScore = passingScore; + } + + public String getQuizStatus() { + return quizStatus; + } + + public void setQuizStatus(String quizStatus) { + this.quizStatus = quizStatus; + } + + public Set getQuestions() { + return questions; + } + + public void setQuestions(Set questions) { + this.questions = questions; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/ModifyQuizDataRequest.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/ModifyQuizDataRequest.java new file mode 100644 index 00000000..16177efb --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/ModifyQuizDataRequest.java @@ -0,0 +1,51 @@ +package app.openschool.course.module.quiz.api.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +public class ModifyQuizDataRequest { + @Schema(description = "Quiz title", example = "Quiz about ArrayList ") + private String title; + + @Schema( + description = "Quiz description", + example = "With the ArrayList quiz you must answer at least 70% of the questions") + private String description; + + @Schema(description = "Quiz maximum grade", example = "7") + private int maxGrade = -1; + + @Schema(description = "Quiz passing score", example = "7") + private int passingScore = -1; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public int getMaxGrade() { + return maxGrade; + } + + public void setMaxGrade(int maxGrade) { + this.maxGrade = maxGrade; + } + + public int getPassingScore() { + return passingScore; + } + + public void setPassingScore(int passingScore) { + this.passingScore = passingScore; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/QuestionWithChosenAnswerDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/QuestionWithChosenAnswerDto.java new file mode 100644 index 00000000..706dc86f --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/QuestionWithChosenAnswerDto.java @@ -0,0 +1,34 @@ +package app.openschool.course.module.quiz.api.dto; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public class QuestionWithChosenAnswerDto { + + @Schema(description = "Regular quiz question or multiple choice question id", example = "1") + private Long questionId; + + @ArraySchema(schema = @Schema(description = "One or multiple chosen answer's ids", example = "2")) + private List chosenAnswersIds; + + public static QuestionWithChosenAnswerDto getInstance() { + return new QuestionWithChosenAnswerDto(); + } + + public Long getQuestionId() { + return questionId; + } + + public void setQuestionId(Long questionId) { + this.questionId = questionId; + } + + public List getChosenAnswersIds() { + return chosenAnswersIds; + } + + public void setChosenAnswersIds(List chosenAnswersIds) { + this.chosenAnswersIds = chosenAnswersIds; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/QuizDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/QuizDto.java new file mode 100644 index 00000000..aa07e53e --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/QuizDto.java @@ -0,0 +1,90 @@ +package app.openschool.course.module.quiz.api.dto; + +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Set; + +public class QuizDto { + + @Schema(description = "Quiz id", example = "1") + private Long id; + + @Schema(description = "Id of the module to which the quiz belongs", example = "1") + private Long moduleId; + + @Schema(description = "The title of the quiz", example = "title") + private String title; + + @Schema(description = "The description of the quiz", example = "description") + private String description; + + @Schema(description = "Maximum possible grade", example = "10") + private int maxGrade; + + @Schema(description = "The minimum score to pass quiz", example = "7") + private int passingScore; + + @ArraySchema(schema = @Schema(implementation = QuestionDto.class)) + private Set questions; + + public static QuizDto getInstance() { + return new QuizDto(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getModuleId() { + return moduleId; + } + + public void setModuleId(Long moduleId) { + this.moduleId = moduleId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public int getMaxGrade() { + return maxGrade; + } + + public void setMaxGrade(int maxGrade) { + this.maxGrade = maxGrade; + } + + public int getPassingScore() { + return passingScore; + } + + public void setPassingScore(int passingScore) { + this.passingScore = passingScore; + } + + public Set getQuestions() { + return questions; + } + + public void setQuestions(Set questions) { + this.questions = questions; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/mapper/EnrolledQuizAssessmentResponseMapper.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/mapper/EnrolledQuizAssessmentResponseMapper.java new file mode 100644 index 00000000..f0a2ed09 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/mapper/EnrolledQuizAssessmentResponseMapper.java @@ -0,0 +1,19 @@ +package app.openschool.course.module.quiz.api.mapper; + +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentResponseDto; + +public final class EnrolledQuizAssessmentResponseMapper { + + private EnrolledQuizAssessmentResponseMapper() {} + + public static EnrolledQuizAssessmentResponseDto toEnrolledQuizAssessmentResponseDto( + int rightAnswers, String assessmentResult, int passingScore, int maxGrade) { + EnrolledQuizAssessmentResponseDto enrolledQuizAssessmentResponseDto = + EnrolledQuizAssessmentResponseDto.getInstant(); + enrolledQuizAssessmentResponseDto.setRightAnswersCount(rightAnswers); + enrolledQuizAssessmentResponseDto.setAssessmentResult(assessmentResult); + enrolledQuizAssessmentResponseDto.setMaxGrade(maxGrade); + enrolledQuizAssessmentResponseDto.setPassingScore(passingScore); + return enrolledQuizAssessmentResponseDto; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/mapper/QuizMapper.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/mapper/QuizMapper.java new file mode 100644 index 00000000..7d843560 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/mapper/QuizMapper.java @@ -0,0 +1,111 @@ +package app.openschool.course.module.quiz.api.mapper; + +import app.openschool.course.module.Module; +import app.openschool.course.module.quiz.EnrolledQuiz; +import app.openschool.course.module.quiz.Quiz; +import app.openschool.course.module.quiz.api.dto.CreateQuizDto; +import app.openschool.course.module.quiz.api.dto.EnrolledQuizDto; +import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.question.answer.AnswerOption; +import app.openschool.course.module.quiz.question.answer.api.dto.AnswerOptionDto; +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import app.openschool.course.module.quiz.question.api.mapper.QuestionMapper; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +public final class QuizMapper { + + private static final String MULTIPLE_CHOICE = "MULTIPLE_CHOICE"; + + private QuizMapper() {} + + public static Quiz createQuizDtoToQuiz(CreateQuizDto createQuizDto, Module module) { + Quiz quiz = Quiz.getInstance(); + quiz.setTitle(createQuizDto.getTitle()); + quiz.setDescription(createQuizDto.getDescription()); + quiz.setMaxGrade(createQuizDto.getMaxGrade()); + quiz.setPassingScore(createQuizDto.getPassingScore()); + quiz.setQuestions(createQuestionDtoToQuestions(createQuizDto.getQuestions(), quiz)); + quiz.setModule(module); + return quiz; + } + + public static QuizDto quizToQuizDto(Quiz quiz) { + QuizDto quizDto = QuizDto.getInstance(); + quizDto.setId(quiz.getId()); + quizDto.setTitle(quiz.getTitle()); + quizDto.setDescription(quiz.getDescription()); + quizDto.setModuleId(quiz.getModule().getId()); + quizDto.setMaxGrade(quiz.getMaxGrade()); + quizDto.setPassingScore(quiz.getPassingScore()); + quizDto.setQuestions(questionsToQuestionDto(quiz.getQuestions())); + return quizDto; + } + + public static Set enrolledQuizzesToEnrolledQuizDtoSet( + Set enrolledQuiz) { + return enrolledQuiz.stream().map(QuizMapper::toEnrolledQuizDtoSet).collect(Collectors.toSet()); + } + + public static Page toQuizDtoPage(Page quizzesPage) { + + List collect = + quizzesPage.toList().stream().map(QuizMapper::quizToQuizDto).collect(Collectors.toList()); + return new PageImpl<>(collect, quizzesPage.getPageable(), quizzesPage.getTotalElements()); + } + + private static Set createQuestionDtoToQuestions( + Set questions, Quiz quiz) { + return questions.stream() + .map( + createQuestionDto -> + QuestionMapper.createQuestionDtoToQuestion(createQuestionDto, quiz)) + .collect(Collectors.toSet()); + } + + private static Set questionsToQuestionDto(Set questions) { + return questions.stream() + .map( + question -> { + QuestionDto questionDto = QuestionDto.getInstance(); + questionDto.setId(question.getId()); + questionDto.setQuestion(question.getQuestion()); + questionDto.setRightAnswersCount(question.getRightAnswersCount()); + questionDto.setQuestionType(question.getQuestionType().getType()); + questionDto.setAnswerOptions( + answerOptionToAnswerOptionDto(question.getAnswerOptions())); + return questionDto; + }) + .collect(Collectors.toSet()); + } + + private static Set answerOptionToAnswerOptionDto( + Set answerOptions) { + return answerOptions.stream() + .map( + answerOption -> { + AnswerOptionDto answerOptionDto = AnswerOptionDto.getInstance(); + answerOptionDto.setId(answerOption.getId()); + answerOptionDto.setAnswerOption(answerOption.getAnswerOption()); + return answerOptionDto; + }) + .collect(Collectors.toSet()); + } + + private static EnrolledQuizDto toEnrolledQuizDtoSet(EnrolledQuiz enrolledQuiz) { + EnrolledQuizDto enrolledQuizDto = EnrolledQuizDto.getInstance(); + enrolledQuizDto.setId(enrolledQuiz.getId()); + enrolledQuizDto.setEnrolledModuleId(enrolledQuiz.getEnrolledModule().getId()); + enrolledQuizDto.setMaxGrade(enrolledQuiz.getQuiz().getMaxGrade()); + enrolledQuizDto.setStudentGrade(enrolledQuiz.getStudentGrade()); + enrolledQuizDto.setPassingScore(enrolledQuiz.getQuiz().getPassingScore()); + enrolledQuizDto.setQuizStatus(enrolledQuiz.getQuizStatus().getType()); + enrolledQuizDto.setQuestions(questionsToQuestionDto(enrolledQuiz.getQuiz().getQuestions())); + return enrolledQuizDto; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/Question.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/Question.java new file mode 100644 index 00000000..d11092be --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/Question.java @@ -0,0 +1,114 @@ +package app.openschool.course.module.quiz.question; + +import app.openschool.course.module.quiz.Quiz; +import app.openschool.course.module.quiz.question.answer.AnswerOption; +import app.openschool.course.module.quiz.question.type.QuestionType; +import java.util.Objects; +import java.util.Set; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name = "questions") +public class Question { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "question", nullable = false) + private String question; + + @Column(name = "right_answer_count", nullable = false) + private int rightAnswersCount = 1; + + @ManyToOne + @JoinColumn(name = "question_type_id", nullable = false) + private QuestionType questionType; + + @OneToMany(mappedBy = "question", orphanRemoval = true, cascade = CascadeType.ALL) + private Set answerOptions; + + @ManyToOne + @JoinColumn(name = "quiz_id") + private Quiz quiz; + + public static Question getInstance() { + return new Question(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Question question1 = (Question) o; + return question.equals(question1.question) + && questionType.getType().equals(question1.questionType.getType()); + } + + @Override + public int hashCode() { + return Objects.hash(question); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getQuestion() { + return question; + } + + public void setQuestion(String question) { + this.question = question; + } + + public Quiz getQuiz() { + return quiz; + } + + public void setQuiz(Quiz quiz) { + this.quiz = quiz; + } + + public QuestionType getQuestionType() { + return questionType; + } + + public void setQuestionType(QuestionType questionType) { + this.questionType = questionType; + } + + public Set getAnswerOptions() { + return answerOptions; + } + + public void setAnswerOptions(Set answerOptions) { + this.answerOptions = answerOptions; + } + + public int getRightAnswersCount() { + return rightAnswersCount; + } + + public void setRightAnswersCount(int rightAnswersCount) { + this.rightAnswersCount = rightAnswersCount; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionController.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionController.java new file mode 100644 index 00000000..d08db786 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionController.java @@ -0,0 +1,133 @@ +package app.openschool.course.module.quiz.question; + +import app.openschool.common.exceptionhandler.exception.PermissionDeniedException; +import app.openschool.course.module.quiz.Quiz; +import app.openschool.course.module.quiz.QuizRepository; +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import java.security.Principal; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/quiz/{quizId}/questions") +public class QuestionController { + + private final QuestionService questionService; + private final QuizRepository quizRepository; + + public QuestionController(QuestionService questionService, QuizRepository quizRepository) { + this.questionService = questionService; + this.quizRepository = quizRepository; + } + + @Operation(summary = "delete question", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses( + value = { + @ApiResponse(responseCode = "204", description = "question successfully deleted"), + @ApiResponse( + responseCode = "404", + description = "Invalid question id supplied", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "403", + description = "If the user have not mentor role", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "400", + description = "if the question doesn't belong to the current user", + content = @Content(schema = @Schema())) + }) + @PreAuthorize("hasAuthority('MENTOR')") + @DeleteMapping("/{questionId}") + public ResponseEntity deleteQuestion( + @Parameter(description = "Id of the quiz to which this question belongs") @PathVariable() + Long quizId, + @Parameter(description = "Id of the question which will be deleted") @PathVariable + Long questionId, + Principal principal) { + checkQuestionAuthor(quizId, principal); + if (questionService.deleteQuestion(questionId)) { + return ResponseEntity.noContent().build(); + } else { + return ResponseEntity.notFound().build(); + } + } + + @Operation(summary = "update question", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses( + value = { + @ApiResponse(responseCode = "204", description = "question successfully updated"), + @ApiResponse( + responseCode = "404", + description = "Invalid question id supplied", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "403", + description = "If the user have not mentor role", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "400", + description = "if the question doesn't belong to the current user", + content = @Content(schema = @Schema())) + }) + @PreAuthorize("hasAuthority('MENTOR')") + @PutMapping("/{questionId}") + public ResponseEntity updateQuestion( + @Parameter(description = "Id of the quiz to which this question belongs") @PathVariable() + Long quizId, + @Parameter(description = "Id of the question which will be updated") @PathVariable + Long questionId, + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "Request object to update question") + @RequestBody + CreateQuestionDto createQuestionDto, + Principal principal) { + checkQuestionAuthor(quizId, principal); + if (questionService.updateQuestion(questionId, createQuestionDto)) { + return ResponseEntity.noContent().build(); + } else { + return ResponseEntity.notFound().build(); + } + } + + @GetMapping() + public ResponseEntity> findAllByQuizId( + @Parameter(description = "Quiz ID that belongs to the questions being searched") @PathVariable + Long quizId, + @Parameter( + description = + "Includes parameters page, size, and sort which is not required. " + + "Page results page you want to retrieve (0..N). " + + "Size is count of records per page(1..N). " + + "Sorting criteria in the format: property(,asc|desc). " + + "Default sort order is ascending. " + + "Multiple sort criteria are supported.") + Pageable pageable) { + + return ResponseEntity.ok(questionService.findAllByQuizId(quizId, pageable)); + } + + private void checkQuestionAuthor(Long quizId, Principal principal) { + Quiz quiz = quizRepository.findById(quizId).orElseThrow(IllegalArgumentException::new); + if (!quiz.getModule().getCourse().getMentor().getEmail().equals(principal.getName())) { + throw new PermissionDeniedException("permission.denied"); + } + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionRepository.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionRepository.java new file mode 100644 index 00000000..411af516 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionRepository.java @@ -0,0 +1,12 @@ +package app.openschool.course.module.quiz.question; + +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + + + +public interface QuestionRepository extends JpaRepository { + Optional> findAllQuestionsByQuizId(Long id, Pageable pageable); +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionService.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionService.java new file mode 100644 index 00000000..d149b4dc --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionService.java @@ -0,0 +1,16 @@ +package app.openschool.course.module.quiz.question; + +import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface QuestionService { + + boolean deleteQuestion(Long questionId); + + boolean updateQuestion(Long questionId, CreateQuestionDto createQuestionDto); + + Page findAllByQuizId(Long id, Pageable pageable); +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionServiceImpl.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionServiceImpl.java new file mode 100644 index 00000000..bff6556e --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionServiceImpl.java @@ -0,0 +1,53 @@ +package app.openschool.course.module.quiz.question; + +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import app.openschool.course.module.quiz.question.api.mapper.QuestionMapper; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +public class QuestionServiceImpl implements QuestionService { + + private final QuestionRepository questionRepository; + + public QuestionServiceImpl(QuestionRepository questionRepository) { + this.questionRepository = questionRepository; + } + + @Override + public boolean deleteQuestion(Long questionId) { + return questionRepository + .findById(questionId) + .map( + question -> { + questionRepository.delete(question); + return true; + }) + .orElse(false); + } + + @Override + public boolean updateQuestion(Long questionId, CreateQuestionDto createQuestionDto) { + return questionRepository + .findById(questionId) + .map( + question -> { + Question updatedQuestion = + QuestionMapper.createQuestionDtoToQuestion(createQuestionDto, question.getQuiz()); + updatedQuestion.setId(question.getId()); + questionRepository.save(updatedQuestion); + return true; + }) + .orElse(false); + } + + @Override + public Page findAllByQuizId(Long id, Pageable pageable) { + return QuestionMapper.toQuestionDtoPage( + questionRepository + .findAllQuestionsByQuizId(id, pageable) + .orElseThrow(IllegalArgumentException::new)); + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/AnswerOption.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/AnswerOption.java new file mode 100644 index 00000000..84fdbdeb --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/AnswerOption.java @@ -0,0 +1,85 @@ +package app.openschool.course.module.quiz.question.answer; + +import app.openschool.course.module.quiz.question.Question; +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name = "answer_options") +public class AnswerOption { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "answer_option", nullable = false) + private String answerOption; + + @Column(name = "is_right_answer", nullable = false) + private Boolean isRightAnswer; + + @ManyToOne + @JoinColumn(name = "question_id") + private Question question; + + public static AnswerOption getInstance() { + return new AnswerOption(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AnswerOption that = (AnswerOption) o; + return answerOption.equals(that.answerOption); + } + + @Override + public int hashCode() { + return Objects.hash(answerOption); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getAnswerOption() { + return answerOption; + } + + public void setAnswerOption(String answerOption) { + this.answerOption = answerOption; + } + + public Boolean isRightAnswer() { + return isRightAnswer; + } + + public void setRightAnswer(Boolean rightAnswer) { + isRightAnswer = rightAnswer; + } + + public Question getQuestion() { + return question; + } + + public void setQuestion(Question question) { + this.question = question; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/dto/AnswerOptionDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/dto/AnswerOptionDto.java new file mode 100644 index 00000000..00cde080 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/dto/AnswerOptionDto.java @@ -0,0 +1,32 @@ +package app.openschool.course.module.quiz.question.answer.api.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +public class AnswerOptionDto { + + @Schema(description = "Answer option id", example = "1") + private Long id; + + @Schema(description = "One of the answers to the quiz question", example = "static") + private String answerOption; + + public static AnswerOptionDto getInstance() { + return new AnswerOptionDto(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getAnswerOption() { + return answerOption; + } + + public void setAnswerOption(String answerOption) { + this.answerOption = answerOption; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/dto/CreateAnswerOptionDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/dto/CreateAnswerOptionDto.java new file mode 100644 index 00000000..852f55de --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/dto/CreateAnswerOptionDto.java @@ -0,0 +1,34 @@ +package app.openschool.course.module.quiz.question.answer.api.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +public class CreateAnswerOptionDto { + + @Schema( + description = "One of the answers to the quiz question", + example = "Java bytecode is the instruction set for the Java Virtual Machine") + private String answerOption; + + @Schema(description = "Indicates if the answer is correct or not", example = "true") + private Boolean isRightAnswer; + + public static CreateAnswerOptionDto getInstance() { + return new CreateAnswerOptionDto(); + } + + public String getAnswerOption() { + return answerOption; + } + + public void setAnswerOption(String answerOption) { + this.answerOption = answerOption; + } + + public Boolean getRightAnswer() { + return isRightAnswer; + } + + public void setRightAnswer(Boolean rightAnswer) { + isRightAnswer = rightAnswer; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/mapper/AnswerMapper.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/mapper/AnswerMapper.java new file mode 100644 index 00000000..a8c20b70 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/mapper/AnswerMapper.java @@ -0,0 +1,40 @@ +package app.openschool.course.module.quiz.question.answer.api.mapper; + +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.question.answer.AnswerOption; +import app.openschool.course.module.quiz.question.answer.api.dto.AnswerOptionDto; +import app.openschool.course.module.quiz.question.answer.api.dto.CreateAnswerOptionDto; +import java.util.Set; +import java.util.stream.Collectors; + +public class AnswerMapper { + + public static AnswerOptionDto toAnswerDto(AnswerOption answerOption) { + AnswerOptionDto answerOptionDto = new AnswerOptionDto(); + answerOptionDto.setId(answerOptionDto.getId()); + answerOptionDto.setAnswerOption(answerOptionDto.getAnswerOption()); + + return answerOptionDto; + } + + public static AnswerOption toAnswerOption( + CreateAnswerOptionDto answerOptionDto, Question question) { + AnswerOption answerOption = new AnswerOption(); + answerOption.setQuestion(question); + answerOption.setRightAnswer(answerOptionDto.getRightAnswer()); + answerOption.setAnswerOption(answerOptionDto.getAnswerOption()); + + return answerOption; + } + + public static Set toAnswerDtoSet(Set answerOptions) { + return answerOptions.stream().map(AnswerMapper::toAnswerDto).collect(Collectors.toSet()); + } + + public static Set toAnswerOptionSet( + Set createAnswerOptionDtoSet, Question question) { + return createAnswerOptionDtoSet.stream() + .map(createAnswerOptionDto -> AnswerMapper.toAnswerOption(createAnswerOptionDto, question)) + .collect(Collectors.toSet()); + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/CreateQuestionDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/CreateQuestionDto.java new file mode 100644 index 00000000..61506fa0 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/CreateQuestionDto.java @@ -0,0 +1,61 @@ +package app.openschool.course.module.quiz.question.api.dto; + +import app.openschool.course.module.quiz.question.answer.api.dto.CreateAnswerOptionDto; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Set; + +public class CreateQuestionDto { + + @Schema( + description = "Regular quiz question or multiple choice question", + example = "What is Java bytecode") + private String question; + + @Schema(description = "The right answers count", example = "3") + private int rightAnswersCount = -1; + + @ArraySchema(schema = @Schema(implementation = CreateAnswerOptionDto.class)) + private Set answerOptions; + + @Schema( + description = "One of quiz question type (MATCHING or MULTIPLE_CHOICE)", + example = "MULTIPLE_CHOICE") + private String questionType; + + public static CreateQuestionDto getInstance() { + return new CreateQuestionDto(); + } + + public String getQuestion() { + return question; + } + + public void setQuestion(String question) { + this.question = question; + } + + public Set getAnswerOptions() { + return answerOptions; + } + + public void setAnswerOptions(Set answerOptions) { + this.answerOptions = answerOptions; + } + + public String getQuestionType() { + return questionType; + } + + public void setQuestionType(String questionType) { + this.questionType = questionType; + } + + public int getRightAnswersCount() { + return rightAnswersCount; + } + + public void setRightAnswersCount(int rightAnswersCount) { + this.rightAnswersCount = rightAnswersCount; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/QuestionDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/QuestionDto.java new file mode 100644 index 00000000..33563944 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/QuestionDto.java @@ -0,0 +1,72 @@ +package app.openschool.course.module.quiz.question.api.dto; + +import app.openschool.course.module.quiz.question.answer.api.dto.AnswerOptionDto; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Set; + +public class QuestionDto { + + @Schema(description = "Question id", example = "1") + private Long id; + + @Schema( + description = "Regular quiz question or multiple choice question", + example = "Which of the following is a Java keyword") + private String question; + + @Schema(description = "The right answers count", example = "3") + private int rightAnswersCount; + + @Schema( + description = "One of quiz question type (MATCHING or MULTIPLE_CHOICE)", + example = "MULTIPLE_CHOICE") + private String questionType; + + @ArraySchema(schema = @Schema(implementation = AnswerOptionDto.class)) + private Set answerOptions; + + public static QuestionDto getInstance() { + return new QuestionDto(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getQuestion() { + return question; + } + + public void setQuestion(String question) { + this.question = question; + } + + public String getQuestionType() { + return questionType; + } + + public void setQuestionType(String questionType) { + this.questionType = questionType; + } + + public Set getAnswerOptions() { + return answerOptions; + } + + public void setAnswerOptions(Set answerOptions) { + this.answerOptions = answerOptions; + } + + public int getRightAnswersCount() { + return rightAnswersCount; + } + + public void setRightAnswersCount(int rightAnswersCount) { + this.rightAnswersCount = rightAnswersCount; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapper.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapper.java new file mode 100644 index 00000000..23e50aed --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapper.java @@ -0,0 +1,59 @@ +package app.openschool.course.module.quiz.question.api.mapper; + +import app.openschool.course.module.quiz.Quiz; +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.question.answer.AnswerOption; +import app.openschool.course.module.quiz.question.answer.api.dto.CreateAnswerOptionDto; +import app.openschool.course.module.quiz.question.answer.api.mapper.AnswerMapper; +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import app.openschool.course.module.quiz.question.type.QuestionType; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +public final class QuestionMapper { + + private static final String MULTIPLE_CHOICE = "MULTIPLE_CHOICE"; + + private QuestionMapper() {} + + public static Question createQuestionDtoToQuestion( + CreateQuestionDto createQuestionDto, Quiz quiz) { + Question question = Question.getInstance(); + question.setQuestion(createQuestionDto.getQuestion()); + question.setRightAnswersCount(createQuestionDto.getRightAnswersCount()); + question.setAnswerOptions( + AnswerMapper.toAnswerOptionSet(createQuestionDto.getAnswerOptions(), question)); + question.setQuestionType(createQuestionType(createQuestionDto.getQuestionType())); + question.setQuiz(quiz); + return question; + } + + public static Page toQuestionDtoPage(Page questionPage) { + + List collect = + questionPage.toList().stream() + .map(QuestionMapper::toQuestionDto) + .collect(Collectors.toList()); + return new PageImpl<>(collect, questionPage.getPageable(), questionPage.getTotalElements()); + } + + public static QuestionDto toQuestionDto(Question question) { + QuestionDto questionDto = new QuestionDto(); + questionDto.setId(question.getId()); + questionDto.setQuestion(question.getQuestion()); + questionDto.setQuestionType(question.getQuestionType().getType()); + questionDto.setAnswerOptions(AnswerMapper.toAnswerDtoSet(question.getAnswerOptions())); + questionDto.setRightAnswersCount(question.getRightAnswersCount()); + return questionDto; + } + + private static QuestionType createQuestionType(String questionType) { + return questionType.equals(MULTIPLE_CHOICE) + ? QuestionType.isMultipleChoice() + : QuestionType.isMatching(); + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/type/QuestionType.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/type/QuestionType.java new file mode 100644 index 00000000..39bc9afe --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/type/QuestionType.java @@ -0,0 +1,55 @@ +package app.openschool.course.module.quiz.question.type; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "question_type") +public class QuestionType { + + private static final String MATCHING = "MATCHING"; + private static final String MULTIPLE_CHOICE = "MULTIPLE_CHOICE"; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "type", nullable = false) + private String type; + + public QuestionType() {} + + public QuestionType(Long id, String type) { + this.id = id; + this.type = type; + } + + public static QuestionType isMatching() { + return new QuestionType(1L, MATCHING); + } + + public static QuestionType isMultipleChoice() { + return new QuestionType(2L, MULTIPLE_CHOICE); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/status/QuizStatus.java b/os-server/src/main/java/app/openschool/course/module/quiz/status/QuizStatus.java new file mode 100644 index 00000000..6d4acbe1 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/status/QuizStatus.java @@ -0,0 +1,64 @@ +package app.openschool.course.module.quiz.status; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "quiz_status") +public class QuizStatus { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "status_type") + private String type; + + public QuizStatus() {} + + public QuizStatus(Long id) { + this.id = id; + } + + public QuizStatus(Long id, String type) { + this.id = id; + this.type = type; + } + + public static QuizStatus inProgress() { + return new QuizStatus(1L); + } + + public static QuizStatus isInProgress() { + return new QuizStatus(1L, "IN_PROGRESS"); + } + + public static QuizStatus isFailed() { + return new QuizStatus(3L, "FAILED"); + } + + public static QuizStatus isCompleted() { + return new QuizStatus(2L, "COMPLETED"); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/os-server/src/main/resources/db/migration/h2/V1_0_19__create_quiz_question_answer_option_tablies.sql b/os-server/src/main/resources/db/migration/h2/V1_0_19__create_quiz_question_answer_option_tablies.sql new file mode 100644 index 00000000..d7e878be --- /dev/null +++ b/os-server/src/main/resources/db/migration/h2/V1_0_19__create_quiz_question_answer_option_tablies.sql @@ -0,0 +1,116 @@ +-- ----------------------------------------------------- +-- Table quiz_status +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS quiz_status ( + id BIGINT NOT NULL, + status_type VARCHAR(45) NOT NULL, +PRIMARY KEY (id) +); + +-- ----------------------------------------------------- +-- Table question_type +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS question_type ( + id BIGINT NOT NULL, + type VARCHAR(45) NOT NULL, +PRIMARY KEY (id) +); + +-- ----------------------------------------------------- +-- Table quiz +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS quiz ( + id BIGINT NOT NULL AUTO_INCREMENT, + title varchar(255) NOT NULL , + description TEXT NOT NULL , + max_grade INT NOT NULL, + passing_score INT NOT NULL, + module_id BIGINT NOT NULL, + PRIMARY KEY (id), +CONSTRAINT fk_quiz_module +FOREIGN KEY (module_id) +REFERENCES module (id) +ON DELETE CASCADE +ON UPDATE CASCADE +); + +CREATE INDEX fk_quiz_module_idx ON quiz (module_id ASC); + +-- ----------------------------------------------------- +-- Table enrolled_quiz +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS enrolled_quiz ( + id BIGINT NOT NULL AUTO_INCREMENT, + student_grade INT NOT NULL, + quiz_status_id BIGINT NOT NULL, + quiz_id BIGINT NOT NULL, + enrolled_module_id BIGINT NOT NULL, + PRIMARY KEY (id), +CONSTRAINT fk_enrolled_quiz_quiz_status +FOREIGN KEY (quiz_status_id) +REFERENCES quiz_status (id) +ON DELETE CASCADE +ON UPDATE CASCADE, +CONSTRAINT fk_enrolled_quiz_quiz +FOREIGN KEY (quiz_id) +REFERENCES quiz (id) +ON DELETE CASCADE +ON UPDATE CASCADE, +CONSTRAINT fk_enrolled_quiz_enrolled_module +FOREIGN KEY (enrolled_module_id) +REFERENCES enrolled_module (id) +ON DELETE CASCADE +ON UPDATE CASCADE +); + +CREATE INDEX fk_enrolled_quiz_quiz_status_idx ON enrolled_quiz (quiz_status_id ASC); +CREATE INDEX fk_enrolled_quiz_quiz_idx ON enrolled_quiz (quiz_id ASC); +CREATE INDEX fk_enrolled_quiz_enrolled_module_idx ON enrolled_quiz (enrolled_module_id ASC); + +-- ----------------------------------------------------- +-- Table questions +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS questions ( + id BIGINT NOT NULL AUTO_INCREMENT, + question TEXT NOT NULL, + right_answer_count INT NOT NULL, + quiz_id BIGINT NOT NULL, + question_type_id BIGINT NOT NULL, + PRIMARY KEY (id), +CONSTRAINT fk_questions_quiz +FOREIGN KEY (quiz_id) +REFERENCES quiz (id) +ON DELETE CASCADE +ON UPDATE CASCADE, +CONSTRAINT fk_questions_question_type +FOREIGN KEY (question_type_id) +REFERENCES question_type (id) +); + +CREATE INDEX fk_questions_quiz_idx ON questions (quiz_id ASC); +CREATE INDEX fk_questions_question_type_idx ON questions (question_type_id ASC); + +-- ----------------------------------------------------- +-- Table answer_options +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS answer_options ( + id BIGINT NOT NULL AUTO_INCREMENT, + answer_option TEXT NOT NULL, + is_right_answer BOOLEAN NOT NULL, + question_id BIGINT NOT NULL, + PRIMARY KEY (id), +CONSTRAINT fk_answer_options_question +FOREIGN KEY (question_id) +REFERENCES questions (id) +ON DELETE CASCADE +ON UPDATE CASCADE +); + +CREATE INDEX fk_answer_options_question_idx ON answer_options(question_id ASC); + +INSERT INTO quiz_status(id, status_type) VALUES (1,'IN_PROGRESS'); +INSERT INTO quiz_status(id, status_type) VALUES (2,'COMPLETED'); +INSERT INTO quiz_status(id, status_type) VALUES (3,'FAILED'); + +INSERT INTO question_type(id, type) VALUES (1,'MATCHING'); +INSERT INTO question_type(id, type) VALUES (2,'MULTIPLE_CHOICE'); \ No newline at end of file diff --git a/os-server/src/main/resources/db/migration/mysql/V1_0_19__create_quiz_question_answer_option_tablies.sql b/os-server/src/main/resources/db/migration/mysql/V1_0_19__create_quiz_question_answer_option_tablies.sql new file mode 100644 index 00000000..7cf174cb --- /dev/null +++ b/os-server/src/main/resources/db/migration/mysql/V1_0_19__create_quiz_question_answer_option_tablies.sql @@ -0,0 +1,116 @@ +-- ----------------------------------------------------- +-- Table `open_school_db`.`quiz_status` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `open_school_db`.`quiz_status` ( + `id` BIGINT NOT NULL, + `status_type` VARCHAR(45) NOT NULL, +PRIMARY KEY (`id`)) +ENGINE = InnoDB; + +-- ----------------------------------------------------- +-- Table `open_school_db`.`question_type` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `open_school_db`.`question_type` ( + `id` BIGINT NOT NULL, + `type` VARCHAR(45) NOT NULL, +PRIMARY KEY (`id`)) +ENGINE = InnoDB; + +-- ----------------------------------------------------- +-- Table `open_school_db`.`quiz` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `open_school_db`.`quiz` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `title` VARCHAR(255) NOT NULL, + `description` TEXT NOT NULL, + `max_grade` INT NOT NULL, + + `passing_score` INT NOT NULL, + `module_id` BIGINT NOT NULL, + PRIMARY KEY (`id`), +INDEX `fk_quiz_module_idx` (`module_id` ASC) VISIBLE, +CONSTRAINT `fk_quiz_module` +FOREIGN KEY (`module_id`) +REFERENCES `open_school_db`.`module` (`id`) +ON DELETE CASCADE +ON UPDATE CASCADE +) +ENGINE = InnoDB; + +-- ----------------------------------------------------- +-- Table `open_school_db`.`enrolled_quiz` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `open_school_db`.`enrolled_quiz` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `student_grade` INT NULL, + `quiz_id` BIGINT NOT NULL, + `enrolled_module_id` BIGINT NOT NULL, + `quiz_status_id` BIGINT NOT NULL, + PRIMARY KEY (`id`), + INDEX `fk_enrolled_quiz_quiz_idx` (`quiz_id` ASC) VISIBLE, + INDEX `fk_enrolled_quiz_enrolled_module_idx` (`enrolled_module_id` ASC) VISIBLE, + INDEX `fk_enrolled_quiz_quiz_status_idx` (`quiz_status_id` ASC) VISIBLE, + CONSTRAINT `fk_enrolled_quiz_quiz` + FOREIGN KEY (`quiz_id`) + REFERENCES `open_school_db`.`quiz` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT `fk_enrolled_quiz_enrolled_module` + FOREIGN KEY (`enrolled_module_id`) + REFERENCES `open_school_db`.`enrolled_module` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT `fk_enrolled_quiz_quiz_status` + FOREIGN KEY (`quiz_status_id`) + REFERENCES `open_school_db`.`quiz_status` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE +) +ENGINE = InnoDB; + +-- ----------------------------------------------------- +-- Table `open_school_db`.`questions` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `open_school_db`.`questions` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `question` TEXT NOT NULL, + `right_answer_count` INT NOT NULL, + `quiz_id` BIGINT NOT NULL, + `question_type_id` BIGINT NOT NULL, + PRIMARY KEY (`id`), +INDEX `fk_questions_quiz_idx` (`quiz_id` ASC) VISIBLE, +INDEX `fk_questions_question_type_idx` (`question_type_id` ASC) VISIBLE, +CONSTRAINT `fk_questions_quiz` +FOREIGN KEY (`quiz_id`) +REFERENCES `open_school_db`.`quiz` (`id`) +ON DELETE CASCADE +ON UPDATE CASCADE, +CONSTRAINT `fk_questions_question_type` +FOREIGN KEY (`question_type_id`) +REFERENCES `open_school_db`.`question_type` (`id`) +) +ENGINE = InnoDB; + +-- ----------------------------------------------------- +-- Table `open_school_db`.`answer_options` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `open_school_db`.`answer_options` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `answer_option` TEXT NOT NULL, + `is_right_answer` BOOLEAN NOT NULL, + `question_id` BIGINT NOT NULL, + PRIMARY KEY (`id`), +INDEX `fk_answer_options_question_idx` (`question_id` ASC) VISIBLE, +CONSTRAINT `fk_answer_options_question` +FOREIGN KEY (`question_id`) +REFERENCES `open_school_db`.`questions` (`id`) +ON DELETE CASCADE +ON UPDATE CASCADE) +ENGINE = InnoDB; + +INSERT INTO quiz_status(id, status_type) VALUES (1,'IN_PROGRESS'); +INSERT INTO quiz_status(id, status_type) VALUES (2,'COMPLETED'); +INSERT INTO quiz_status(id, status_type) VALUES (3,'FAILED'); + +INSERT INTO question_type(id, type) VALUES (1,'MATCHING'); +INSERT INTO question_type(id, type) VALUES (2,'MULTIPLE_CHOICE'); \ No newline at end of file diff --git a/os-server/src/main/resources/messages.properties b/os-server/src/main/resources/messages.properties index 88f2879f..ad24428d 100644 --- a/os-server/src/main/resources/messages.properties +++ b/os-server/src/main/resources/messages.properties @@ -39,4 +39,6 @@ exception.file.not.found=File does not exist exception.file.not.deleted=File was not deleted exception.persist=Unable to create new data, please check the input data. faq.length.max=Maximum 500 characters are allowed +quiz.failed=FAILED +quiz.completed=COMPLETED exception.user.verification=User already verified. diff --git a/os-server/src/test/java/app/openschool/course/api/CourseGenerator.java b/os-server/src/test/java/app/openschool/course/api/CourseGenerator.java index 31b68b40..0ba288af 100644 --- a/os-server/src/test/java/app/openschool/course/api/CourseGenerator.java +++ b/os-server/src/test/java/app/openschool/course/api/CourseGenerator.java @@ -1,5 +1,6 @@ package app.openschool.course.api; +import app.openschool.category.Category; import app.openschool.course.Course; import app.openschool.course.EnrolledCourse; import app.openschool.course.difficulty.Difficulty; @@ -19,7 +20,7 @@ public static Course generateCourseWithEnrolledCourses() { Course course = new Course(); Set modules = new HashSet<>(); Set moduleItems = new HashSet<>(); - Module module = new Module(1L, course, moduleItems); + Module module = new Module(1L, course, moduleItems, new HashSet<>()); ModuleItemType moduleItemType = new ModuleItemType(); moduleItemType.setType("TheType"); ModuleItem moduleItem = new ModuleItem(1L, "String", moduleItemType, "kkk", 100L, module); @@ -46,7 +47,7 @@ public static Course generateCourse() { Course course = new Course(); Set modules = new HashSet<>(); Set moduleItems = new HashSet<>(); - Module module = new Module(1L, course, moduleItems); + Module module = new Module(1L, course, moduleItems, new HashSet<>()); ModuleItemType moduleItemType = new ModuleItemType(); moduleItemType.setType("TheType"); ModuleItem moduleItem = new ModuleItem(1L, "String", moduleItemType, "kkk", 100L, module); @@ -59,8 +60,16 @@ public static Course generateCourse() { course.setModules(modules); course.setMentor(new User(1L)); course.setRating(5.0); - course.setDifficulty(new Difficulty("Basic")); - course.setLanguage(new Language("English")); + Category category = new Category(); + category.setId(1L); + category.setTitle("category"); + course.setCategory(category); + Difficulty basic = new Difficulty("Basic"); + basic.setId(1); + course.setDifficulty(basic); + Language english = new Language("English"); + english.setId(1); + course.setLanguage(english); return course; } diff --git a/os-server/src/test/java/app/openschool/course/module/api/ModuleGenerator.java b/os-server/src/test/java/app/openschool/course/module/api/ModuleGenerator.java new file mode 100644 index 00000000..b1a6cb88 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/api/ModuleGenerator.java @@ -0,0 +1,23 @@ +package app.openschool.course.module.api; + +import app.openschool.course.Course; +import app.openschool.course.module.Module; +import app.openschool.user.User; +import app.openschool.user.api.UserGenerator; +import app.openschool.user.role.Role; + +public class ModuleGenerator { + public static Module generateModule() { + Module module = new Module(); + Course course = new Course(); + course.setMentor(UserGenerator.generateMentor()); + module.setCourse(course); + return module; + } + + public static Module generateModuleWithAnotherUser() { + Module module = ModuleGenerator.generateModule(); + module.getCourse().getMentor().setEmail("another@gmail"); + return module; + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java new file mode 100644 index 00000000..59569490 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java @@ -0,0 +1,288 @@ +package app.openschool.course.module.quiz; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import app.openschool.common.security.JwtTokenProvider; +import app.openschool.common.security.UserPrincipal; +import app.openschool.course.module.Module; +import app.openschool.course.module.ModuleRepository; +import app.openschool.course.module.api.ModuleGenerator; +import app.openschool.course.module.quiz.api.mapper.QuizMapper; +import app.openschool.course.module.quiz.util.EnrolledQuizAssessmentResponseDtoGenerator; +import app.openschool.course.module.quiz.util.QuizGenerator; +import app.openschool.user.User; +import app.openschool.user.api.UserGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Optional; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + +@ExtendWith(SpringExtension.class) +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc +@Disabled +public class QuizControllerTest { + + private static final String REQUEST_BODY = + "{\n" + + " \"maxGrade\": \"10\",\n" + + " \"passingScore\": \"7\",\n" + + " \"questions\": [\n" + + " {\n" + + " \"question\": \"What is Java bytecode\",\n" + + " \"answerOptions\": [\n" + + " {\n" + + " \"answerOption\": \"Java bytecode is the instruction set for the JVM\",\n" + + " \"rightAnswer\": \"true\"\n" + + " }\n" + + " ],\n" + + " \"questionType\": \"MULTIPLE_CHOICE\"\n" + + " }\n" + + " ]\n" + + "}"; + + @Autowired private MockMvc mockMvc; + + @Autowired private JwtTokenProvider jwtTokenProvider; + + @MockBean private QuizServiceImpl quizService; + @MockBean private ModuleRepository moduleRepository; + + private static final String AUTHORIZATION = "Authorization"; + + @Test + void createQuiz_withCorrectCredentials_isCreated() throws Exception { + Quiz quiz = QuizGenerator.generateQuiz(); + when(quizService.createQuiz(anyLong(), any())).thenReturn(Optional.of(quiz)); + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModule())); + mockMvc + .perform( + post("/api/v1/module/1/quizzes") + .contentType(MediaType.APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isCreated()) + .andExpect( + content().json(new ObjectMapper().writeValueAsString(QuizMapper.quizToQuizDto(quiz)))); + } + + @Test + void createQuiz_withIncorrectModuleId_returnNotFound() throws Exception { + when(quizService.createQuiz(anyLong(), any())).thenReturn(Optional.empty()); + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModule())); + mockMvc + .perform( + post("/api/v1/module/1/quizzes") + .contentType(MediaType.APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isNotFound()); + } + + @Test + void createQuiz_withIncorrectMentor_isForbidden() throws Exception { + when(quizService.createQuiz(anyLong(), any())).thenThrow(IllegalArgumentException.class); + Module module = ModuleGenerator.generateModule(); + module.getCourse().getMentor().setEmail("anotherEmail"); + when(moduleRepository.findById(anyLong())).thenReturn(Optional.of(module)); + mockMvc + .perform( + post("/api/v1/module/1/quizzes") + .contentType(MediaType.APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isForbidden()); + } + + @Test + void createQuiz_withStudent_isForbidden() throws Exception { + mockMvc + .perform( + post("/api/v1/module/1/quizzes/1") + .contentType(MediaType.APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateStudent()))) + .andExpect(status().isForbidden()); + } + + @Test + void createQuiz_withoutJwtToken_isUnauthorized() throws Exception { + + mockMvc + .perform( + post("/api/v1/module/1/quizzes") + .contentType(MediaType.APPLICATION_JSON) + .content(REQUEST_BODY)) + .andExpect(status().isUnauthorized()); + } + + @Test + void deleteQuiz_withCorrectCredentials_isNoContent() throws Exception { + when(quizService.deleteQuiz(anyLong())).thenReturn(true); + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModule())); + mockMvc + .perform( + delete("/api/v1/module/1/quizzes/1") + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isNoContent()); + } + + @Test + void deleteQuiz_withIncorrectQuizId_returnNotFound() throws Exception { + when(quizService.deleteQuiz(anyLong())).thenReturn(false); + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModule())); + + mockMvc + .perform( + delete("/api/v1/module/1/quizzes/1") + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isNotFound()); + } + + @Test + void deleteQuiz_withIncorrectMentor_isForbidden() throws Exception { + Module module = ModuleGenerator.generateModule(); + module.getCourse().getMentor().setEmail("anotherEmail@gmail.com"); + when(moduleRepository.findById(anyLong())).thenReturn(Optional.of(module)); + + mockMvc + .perform( + delete("/api/v1/module/1/quizzes/1") + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isForbidden()); + } + + @Test + void deleteQuiz_withStudent_isForbidden() throws Exception { + mockMvc + .perform( + delete("/api/v1/module/1/quizzes/1") + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateStudent()))) + .andExpect(status().isForbidden()); + } + + @Test + void deleteQuiz_withoutJwtToken_isUnauthorized() throws Exception { + mockMvc.perform(delete("/api/v1/module/1/quizzes/1")).andExpect(status().isUnauthorized()); + } + + @Test + void updateQuiz_withCorrectArg_returnOk() throws Exception { + when(quizService.updateQuiz(anyLong(), any())).thenReturn(QuizGenerator.generateQuiz()); + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModule())); + mockMvc + .perform( + patch("/api/v1/module/1/quizzes/1") + .contentType(MediaType.APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isOk()); + } + + @Test + void updateQuiz_withIncorrectMentor_isForbidden() throws Exception { + when(quizService.updateQuiz(anyLong(), any())).thenReturn(QuizGenerator.generateQuiz()); + + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModuleWithAnotherUser())); + mockMvc + .perform( + patch("/api/v1/module/1/quizzes/1") + .contentType(MediaType.APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isForbidden()); + } + + @Test + void updateQuiz_withoutJwtToken_isUnauthorized() throws Exception { + mockMvc.perform(patch("/api/v1/module/1/quizzes/1")).andExpect(status().isUnauthorized()); + } + + @Test + void findById_withCorrectArg_returnOk() throws Exception { + when(quizService.findById(anyLong())) + .thenReturn(QuizMapper.quizToQuizDto(QuizGenerator.generateQuiz())); + + mockMvc + .perform( + get("/api/v1/module/1/quizzes/1") + .contentType(MediaType.APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isOk()); + } + + @Test + void findById_withoutJwtToken_isUnauthorized() throws Exception { + mockMvc.perform(get("/api/v1/module/1/quizzes/1")).andExpect(status().isUnauthorized()); + } + + @Test + void findAllByModuleId_withCorrectArg_returnOk() throws Exception { + when(quizService.findAllByModuleId(anyLong(), any())) + .thenReturn(QuizGenerator.generateQuizDtoPage()); + mockMvc + .perform( + get("/api/v1/module/1/quizzes") + .contentType(MediaType.APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isOk()); + } + + @Test + void findByModuleId_withoutJwtToken_isUnauthorized() throws Exception { + mockMvc.perform(get("/api/v1/module/1/quizzes")).andExpect(status().isUnauthorized()); + } + + @Test + void completeEnrolledQuiz_withCorrectArg_returnOk() throws Exception { + when(quizService.completeEnrolledQuiz(anyLong(), any(), any())) + .thenReturn( + Optional.of( + EnrolledQuizAssessmentResponseDtoGenerator + .generateEnrolledQuizAssessmentResponseDto("FAILED"))); + mockMvc + .perform( + get("/api/v1/module/1/quizzes/enrolledQuizzes/1") + .contentType(MediaType.APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isOk()); + } + + @Test + void completeEnrolledQuiz_withoutJwtToken_isUnauthorized() throws Exception { + mockMvc + .perform(get("/api/v1/module/1/quizzes/enrolledQuizzes/1")) + .andExpect(status().isUnauthorized()); + } + + private String generateJwtToken(User user) { + return "Bearer " + jwtTokenProvider.generateJwtToken(new UserPrincipal(user)); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/QuizRepositoryTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/QuizRepositoryTest.java new file mode 100644 index 00000000..ee41eda2 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/QuizRepositoryTest.java @@ -0,0 +1,158 @@ +package app.openschool.course.module.quiz; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.annotation.DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD; + +import app.openschool.category.Category; +import app.openschool.category.CategoryRepository; +import app.openschool.course.Course; +import app.openschool.course.CourseRepository; +import app.openschool.course.difficulty.Difficulty; +import app.openschool.course.difficulty.DifficultyRepository; +import app.openschool.course.language.Language; +import app.openschool.course.language.LanguageRepository; +import app.openschool.course.module.Module; +import app.openschool.course.module.ModuleRepository; +import app.openschool.course.module.api.ModuleGenerator; +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.question.QuestionRepository; +import app.openschool.course.module.quiz.question.answer.AnswerOption; +import app.openschool.course.module.quiz.question.type.QuestionType; +import app.openschool.user.User; +import app.openschool.user.UserRepository; +import app.openschool.user.api.UserGenerator; +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.transaction.annotation.Transactional; + +@DataJpaTest +@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) +public class QuizRepositoryTest { + @Autowired QuizRepository quizRepository; + @Autowired ModuleRepository moduleRepository; + @Autowired UserRepository userRepository; + @Autowired CourseRepository courseRepository; + @Autowired CategoryRepository categoryRepository; + @Autowired DifficultyRepository difficultyRepository; + @Autowired LanguageRepository languageRepository; + + @Autowired QuestionRepository questionRepository; + + @BeforeEach + public void setup() { + User user = UserGenerator.generateMentor(); + userRepository.save(user); + Category category = new Category(); + category.setTitle("category title"); + categoryRepository.save(category); + Difficulty difficulty = new Difficulty(); + difficulty.setTitle("difficulty title"); + difficultyRepository.save(difficulty); + Language language = new Language(); + language.setTitle("English"); + languageRepository.save(language); + Course course = new Course(); + course.setTitle("Stream"); + course.setRating(5.5); + course.setCategory(categoryRepository.getById(1L)); + course.setDifficulty(difficultyRepository.getById(1)); + course.setLanguage(languageRepository.getById(1)); + course.setMentor(user); + courseRepository.save(course); + Module module = ModuleGenerator.generateModule(); + module.setCourse(course); + module.setDescription("module description"); + moduleRepository.save(module); + } + + @Test + void saveQuiz() { + Quiz quiz = createQuiz(); + Quiz savedQuiz = saveQuizInDb(); + assertEquals(quiz.getTitle(), savedQuiz.getTitle()); + assertEquals(quiz.getDescription(), savedQuiz.getDescription()); + assertEquals(quiz.getMaxGrade(), savedQuiz.getMaxGrade()); + assertEquals(quiz.getPassingScore(), savedQuiz.getPassingScore()); + assertEquals(quiz.getQuestions(), savedQuiz.getQuestions()); + assertEquals( + Objects.requireNonNull(quiz.getQuestions().stream().findFirst().orElse(null)) + .getAnswerOptions() + .stream() + .findFirst(), + Objects.requireNonNull(savedQuiz.getQuestions().stream().findFirst().orElse(null)) + .getAnswerOptions() + .stream() + .findFirst()); + assertEquals(quiz.getModule(), savedQuiz.getModule()); + } + + @Test + @Transactional + void deleteQuiz() { + Quiz quiz = saveQuizInDb(); + quizRepository.delete(quiz); + Optional byId = quizRepository.findById(quiz.getId()); + assertTrue(byId.isEmpty()); + } + + @Test + @Transactional + void findById() { + Quiz quiz = saveQuizInDb(); + Quiz findQuiz = quizRepository.findById(quiz.getId()).orElseThrow(); + assertEquals(findQuiz.getId(), quiz.getId()); + } + + @Test + @Transactional + void findAllByModuleId() { + Quiz quiz = saveQuizInDb(); + Pageable of = PageRequest.of(0, 1); + Page allByModuleId = quizRepository.findAllByModuleId(1L, of); + assertEquals(allByModuleId.getTotalElements(), 1); + assertEquals(allByModuleId.getTotalPages(), 1); + assertEquals( + allByModuleId.getContent().stream().findAny().orElse(new Quiz()).getId(), quiz.getId()); + } + + private Quiz saveQuizInDb() { + Quiz quiz = createQuiz(); + return quizRepository.save(quiz); + } + + private Quiz createQuiz() { + Quiz quiz = new Quiz(); + quiz.setTitle("quiz title"); + quiz.setDescription("quiz description"); + Question question = new Question(); + question.setQuestion("question"); + AnswerOption answerOption = new AnswerOption(); + answerOption.setAnswerOption("Answer1"); + answerOption.setRightAnswer(true); + answerOption.setQuestion(question); + Set answerOptions = new HashSet<>(); + answerOptions.add(answerOption); + question.setAnswerOptions(answerOptions); + question.setQuiz(quiz); + QuestionType questionType = new QuestionType(1L, "Question type"); + question.setQuestionType(questionType); + Set questions = new HashSet<>(); + questions.add(question); + quiz.setQuestions(questions); + quiz.setMaxGrade(7); + quiz.setPassingScore(5); + quiz.setModule(moduleRepository.getById(1L)); + return quiz; + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/QuizServiceImplTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/QuizServiceImplTest.java new file mode 100644 index 00000000..6cb3d08a --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/QuizServiceImplTest.java @@ -0,0 +1,203 @@ +package app.openschool.course.module.quiz; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import app.openschool.course.module.ModuleRepository; +import app.openschool.course.module.api.ModuleGenerator; +import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.util.EnrolledQuizAssessmentRequestDtoGenerator; +import app.openschool.course.module.quiz.util.EnrolledQuizAssessmentResponseDtoGenerator; +import app.openschool.course.module.quiz.util.EnrolledQuizGenerator; +import app.openschool.course.module.quiz.util.QuizDtoGenerator; +import app.openschool.course.module.quiz.util.QuizGenerator; +import app.openschool.user.User; +import app.openschool.user.api.UserGenerator; +import java.util.Locale; +import java.util.Optional; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.MessageSource; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; + +@ExtendWith(MockitoExtension.class) +class QuizServiceImplTest { + @Mock ModuleRepository moduleRepository; + + @Mock QuizRepository quizRepository; + @Mock EnrolledQuizRepository enrolledQuizRepository; + + @Mock MessageSource messageSource; + private QuizServiceImpl quizService; + + @BeforeEach + void setUp() { + quizService = + new QuizServiceImpl( + moduleRepository, quizRepository, enrolledQuizRepository, messageSource); + } + + @AfterEach + void cleanUp() { + SecurityContextHolder.clearContext(); + } + + @Test + void createQuiz_withCorrectArg_returnQuiz() { + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModule())); + when(quizRepository.save(any())).thenReturn(QuizGenerator.generateQuiz()); + Quiz actualQuiz = quizService.createQuiz(1L, QuizDtoGenerator.generateCreateQuizDto()).get(); + Quiz expectedQuiz = QuizGenerator.generateQuiz(); + assertEquals(actualQuiz.getDescription(), expectedQuiz.getDescription()); + assertEquals(actualQuiz.getTitle(), expectedQuiz.getTitle()); + assertEquals(actualQuiz.getMaxGrade(), expectedQuiz.getMaxGrade()); + assertEquals(actualQuiz.getPassingScore(), expectedQuiz.getPassingScore()); + } + + @Test + void createQuiz_withIncorrectModuleId_returnEmptyOptional() { + Long moduleId = 1L; + when(moduleRepository.findById(anyLong())).thenReturn(Optional.empty()); + + assertEquals(quizService.createQuiz(moduleId, any()), Optional.empty()); + } + + @Test + void createQuiz_withCorrectModuleId_returnOptionalOfQuiz() { + Quiz quiz = Quiz.getInstance(); + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModule())); + when(quizRepository.save(any())).thenReturn(quiz); + setAuthentication(UserGenerator.generateMentor()); + + Long moduleId = 1L; + assertEquals( + quizService.createQuiz(moduleId, QuizDtoGenerator.generateCreateQuizDto()), + Optional.of(quiz)); + verify(moduleRepository, times(1)).findById(anyLong()); + verify(quizRepository, times(1)).save(any()); + } + + @Test + void deleteQuiz_withInCorrectQuizId_returnFalse() { + when(quizRepository.findById(anyLong())).thenReturn(Optional.empty()); + + Long quizId = 1L; + assertEquals(quizService.deleteQuiz(quizId), false); + } + + @Test + void deleteQuiz_withCorrectQuizId_returnTrue() { + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(QuizGenerator.generateQuiz())); + + setAuthentication(UserGenerator.generateMentor()); + + Long quizId = 1L; + assertEquals(quizService.deleteQuiz(quizId), true); + } + + @Test + void updateQuiz_withCorrectArg_returnQuiz() { + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(QuizGenerator.generateQuiz())); + when(quizRepository.save(any())).thenReturn(QuizGenerator.generateQuiz()); + + Quiz actualQuiz = quizService.updateQuiz(1L, QuizDtoGenerator.generateModifyQuizDataRequest()); + Quiz expectedQuiz = QuizGenerator.generateQuiz(); + assertEquals(actualQuiz.getTitle(), expectedQuiz.getTitle()); + assertEquals(actualQuiz.getDescription(), expectedQuiz.getDescription()); + assertEquals(actualQuiz.getMaxGrade(), expectedQuiz.getMaxGrade()); + assertEquals(actualQuiz.getPassingScore(), expectedQuiz.getPassingScore()); + } + + @Test + void updateQuiz_withInCorrectQuizId_throwIllegalArgumentException() { + when(quizRepository.findById(anyLong())).thenReturn(Optional.empty()); + assertThatThrownBy(() -> quizService.updateQuiz(1L, any())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void findById_withCorrectArg_returnQuiz() { + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(QuizGenerator.generateQuiz())); + QuizDto actualQuizDto = quizService.findById(1L); + QuizDto quizDto = QuizDtoGenerator.generateQuizDto(); + assertEquals(actualQuizDto.getTitle(), quizDto.getTitle()); + assertEquals(actualQuizDto.getDescription(), quizDto.getDescription()); + assertEquals(actualQuizDto.getMaxGrade(), quizDto.getMaxGrade()); + assertEquals(actualQuizDto.getPassingScore(), quizDto.getPassingScore()); + } + + @Test + void findById_withInCorrectId_throwIllegalArgumentException() { + when(quizRepository.findById(anyLong())).thenReturn(Optional.empty()); + assertThatThrownBy(() -> quizService.findById(1L)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void findAllByModuleId_withCorrectArg_quizDtoPage() { + doReturn(QuizGenerator.generateQuizPage()) + .when(quizRepository) + .findAllByModuleId(anyLong(), any()); + PageRequest pageRequest = PageRequest.of(0, 1); + Page actualPageQuizDto = quizService.findAllByModuleId(1L, pageRequest); + Page quizDtoPage = QuizGenerator.generateQuizDtoPage(); + assertEquals( + actualPageQuizDto.stream().findFirst().orElseThrow().getTitle(), + quizDtoPage.stream().findFirst().orElseThrow().getTitle()); + assertEquals( + actualPageQuizDto.stream().findFirst().orElseThrow().getDescription(), + quizDtoPage.stream().findFirst().orElseThrow().getDescription()); + assertEquals( + actualPageQuizDto.stream().findFirst().orElseThrow().getMaxGrade(), + quizDtoPage.stream().findFirst().orElseThrow().getMaxGrade()); + assertEquals( + actualPageQuizDto.stream().findFirst().orElseThrow().getPassingScore(), + quizDtoPage.stream().findFirst().orElseThrow().getPassingScore()); + } + + @Test + void completeEnrolledQuiz_WithCorrectQuizId_returnOptionalOfEnrolledQuizAssessmentResponseDto() { + String quizStatus = "FAILED"; + when(enrolledQuizRepository.findById(anyLong())) + .thenReturn(Optional.of(EnrolledQuizGenerator.generateEnrolledQuiz())); + when(messageSource.getMessage(anyString(), any(), any())).thenReturn(quizStatus); + + assertEquals( + quizService.completeEnrolledQuiz( + 1L, + EnrolledQuizAssessmentRequestDtoGenerator.generateEnrolledQuizAssessmentRequestDto(), + Locale.ROOT), + Optional.of( + EnrolledQuizAssessmentResponseDtoGenerator.generateEnrolledQuizAssessmentResponseDto( + quizStatus))); + } + + @Test + void completeEnrolledQuiz_WithIncorrectQuizId_returnEmptyOptional() { + when(enrolledQuizRepository.findById(anyLong())).thenReturn(Optional.empty()); + + assertEquals(quizService.completeEnrolledQuiz(1L, null, null), Optional.empty()); + } + + private void setAuthentication(User user) { + SecurityContextHolder.setContext( + new SecurityContextImpl( + new UsernamePasswordAuthenticationToken(user.getEmail(), null, null))); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/api/mapper/EnrolledQuizAssessmentResponseMapperTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/api/mapper/EnrolledQuizAssessmentResponseMapperTest.java new file mode 100644 index 00000000..36ba5351 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/api/mapper/EnrolledQuizAssessmentResponseMapperTest.java @@ -0,0 +1,18 @@ +package app.openschool.course.module.quiz.api.mapper; + +import static org.assertj.core.api.Assertions.assertThat; + +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentResponseDto; +import org.junit.jupiter.api.Test; + +public class EnrolledQuizAssessmentResponseMapperTest { + @Test + void toEnrolledQuizAssessmentResponseDto() { + EnrolledQuizAssessmentResponseDto enrolledQuizAssessmentResponseDto = + EnrolledQuizAssessmentResponseMapper.toEnrolledQuizAssessmentResponseDto( + 2, "COMPLETED", 2, 3); + + assertThat(enrolledQuizAssessmentResponseDto) + .hasOnlyFields("rightAnswersCount", "assessmentResult", "passingScore", "maxGrade"); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/api/mapper/QuizMapperTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/api/mapper/QuizMapperTest.java new file mode 100644 index 00000000..3fddf76a --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/api/mapper/QuizMapperTest.java @@ -0,0 +1,55 @@ +package app.openschool.course.module.quiz.api.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.openschool.course.module.Module; +import app.openschool.course.module.quiz.EnrolledQuiz; +import app.openschool.course.module.quiz.Quiz; +import app.openschool.course.module.quiz.api.dto.EnrolledQuizDto; +import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.util.EnrolledQuizGenerator; +import app.openschool.course.module.quiz.util.QuizDtoGenerator; +import app.openschool.course.module.quiz.util.QuizGenerator; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Page; + +public class QuizMapperTest { + + @Test + void createQuizDtoToQuiz() { + Quiz quiz = + QuizMapper.createQuizDtoToQuiz(QuizDtoGenerator.generateCreateQuizDto(), new Module()); + + assertThat(quiz) + .hasOnlyFields( + "id", "description", "title", "maxGrade", "passingScore", "questions", "module"); + } + + @Test + void quizToQuizDto() { + QuizDto quizDto = QuizMapper.quizToQuizDto(QuizGenerator.generateQuiz()); + + assertThat(quizDto) + .hasOnlyFields( + "id", "description", "title", "moduleId", "maxGrade", "passingScore", "questions"); + } + + @Test + void toEnrolledQuizDtoSet() { + Set enrolledQuizSet = new HashSet<>(); + enrolledQuizSet.add(EnrolledQuizGenerator.generateEnrolledQuiz()); + Set enrolledQuizDtoSet = + QuizMapper.enrolledQuizzesToEnrolledQuizDtoSet(enrolledQuizSet); + + assertEquals(1, enrolledQuizDtoSet.size()); + } + + @Test + void toQuizDtoPage() { + Page quizDtoPage = QuizMapper.toQuizDtoPage(QuizGenerator.generateQuizPage()); + assertThat(quizDtoPage).hasOnlyElementsOfTypes(QuizDto.class); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionControllerTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionControllerTest.java new file mode 100644 index 00000000..87a28362 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionControllerTest.java @@ -0,0 +1,192 @@ +package app.openschool.course.module.quiz.question; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import app.openschool.common.security.JwtTokenProvider; +import app.openschool.common.security.UserPrincipal; +import app.openschool.course.module.quiz.Quiz; +import app.openschool.course.module.quiz.QuizRepository; +import app.openschool.course.module.quiz.question.util.QuestionGenerator; +import app.openschool.course.module.quiz.util.QuizGenerator; +import app.openschool.user.User; +import app.openschool.user.api.UserGenerator; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + + +@ExtendWith(SpringExtension.class) +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc +public class QuestionControllerTest { + private static final String UPDATE_QUESTION_REQUEST = + "{\n" + + " \"question\": \"What is Java bytecode\",\n" + + " \"rightAnswersCount\": 3,\n" + + " \"answerOptions\": [\n" + + " {\n" + + " \"answerOption\": \"Java bytecode is the instruction\",\n" + + " \"rightAnswer\": true\n" + + " }\n" + + " ],\n" + + " \"questionType\": \"MULTIPLE_CHOICE\"\n" + + "}"; + + @Autowired private MockMvc mockMvc; + + @Autowired private JwtTokenProvider jwtTokenProvider; + + @MockBean private QuestionServiceImpl questionService; + @MockBean private QuizRepository quizRepository; + private static final String AUTHORIZATION = "Authorization"; + + @Test + void deleteQuestion_withCorrectCredentials_isNoContent() throws Exception { + when(questionService.deleteQuestion(anyLong())).thenReturn(true); + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(QuizGenerator.generateQuiz())); + mockMvc + .perform( + delete("/api/v1/quiz/1/questions/1") + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isNoContent()); + } + + @Test + void deleteQuestion_withIncorrectQuestionId_returnNotFound() throws Exception { + when(questionService.deleteQuestion(anyLong())).thenReturn(false); + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(QuizGenerator.generateQuiz())); + + mockMvc + .perform( + delete("/api/v1/quiz/1/questions/1") + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isNotFound()); + } + + @Test + void deleteQuestion_withIncorrectMentor_isForbidden() throws Exception { + Quiz quiz = QuizGenerator.generateQuiz(); + quiz.getModule().getCourse().getMentor().setEmail("anotherEmail"); + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(quiz)); + + mockMvc + .perform( + delete("/api/v1/quiz/1/questions/1") + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isForbidden()); + } + + @Test + void deleteQuestion_withStudent_isForbidden() throws Exception { + mockMvc + .perform( + delete("/api/v1/quiz/1/questions/1") + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateStudent()))) + .andExpect(status().isForbidden()); + } + + @Test + void deleteQuestion_withoutJwtToken_isUnauthorized() throws Exception { + mockMvc.perform(delete("/api/v1/quiz/1/questions/1")).andExpect(status().isUnauthorized()); + } + + @Test + void updateQuestion_withCorrectCredentials_isNoContent() throws Exception { + when(questionService.updateQuestion(anyLong(), any())).thenReturn(true); + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(QuizGenerator.generateQuiz())); + + mockMvc + .perform( + put("/api/v1/quiz/1/questions/1") + .contentType(MediaType.APPLICATION_JSON) + .content(UPDATE_QUESTION_REQUEST) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isNoContent()); + } + + @Test + void updateQuestion_withIncorrectQuestionId_returnNotFound() throws Exception { + when(questionService.updateQuestion(anyLong(), any())).thenReturn(false); + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(QuizGenerator.generateQuiz())); + + mockMvc + .perform( + put("/api/v1/quiz/1/questions/1") + .contentType(MediaType.APPLICATION_JSON) + .content(UPDATE_QUESTION_REQUEST) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isNotFound()); + } + + @Test + void updateQuestion_withIncorrectMentor_isForbidden() throws Exception { + Quiz quiz = QuizGenerator.generateQuiz(); + quiz.getModule().getCourse().getMentor().setEmail("anotherEmail"); + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(quiz)); + mockMvc + .perform( + put("/api/v1/quiz/1/questions/1") + .contentType(MediaType.APPLICATION_JSON) + .content(UPDATE_QUESTION_REQUEST) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isForbidden()); + } + + @Test + void updateQuestion_withStudent_isForbidden() throws Exception { + mockMvc + .perform( + put("/api/v1/quiz/1/questions/1") + .contentType(MediaType.APPLICATION_JSON) + .content(UPDATE_QUESTION_REQUEST) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateStudent()))) + .andExpect(status().isForbidden()); + } + + @Test + void updateQuestion_withoutJwtToken_isUnauthorized() throws Exception { + mockMvc + .perform( + put("/api/v1/quiz/1/questions/1") + .contentType(MediaType.APPLICATION_JSON) + .content(UPDATE_QUESTION_REQUEST)) + .andExpect(status().isUnauthorized()); + } + + @Test + void findAllByQuizId_withCorrectArg_returnOk() throws Exception { + when(questionService.findAllByQuizId(anyLong(), any())) + .thenReturn(QuestionGenerator.generateQuestionDtoPage()); + mockMvc + .perform( + get("/api/v1/quiz/1/questions") + .contentType(MediaType.APPLICATION_JSON) + .content(UPDATE_QUESTION_REQUEST) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isOk()); + } + + @Test + void findAllByQuizId_withoutJwtToken_isUnauthorized() throws Exception { + mockMvc.perform(get("/api/v1/quiz/1/questions")).andExpect(status().isUnauthorized()); + } + + private String generateJwtToken(User user) { + return "Bearer " + jwtTokenProvider.generateJwtToken(new UserPrincipal(user)); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionServiceImplTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionServiceImplTest.java new file mode 100644 index 00000000..2acc7cda --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionServiceImplTest.java @@ -0,0 +1,151 @@ +package app.openschool.course.module.quiz.question; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import app.openschool.course.module.quiz.question.answer.api.dto.AnswerOptionDto; +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import app.openschool.course.module.quiz.question.util.CreateQuestionDtoGenerator; +import app.openschool.course.module.quiz.question.util.QuestionGenerator; +import app.openschool.user.User; +import app.openschool.user.api.UserGenerator; +import java.util.Optional; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; + +@ExtendWith(MockitoExtension.class) +public class QuestionServiceImplTest { + @Mock QuestionRepository questionRepository; + + private QuestionServiceImpl questionService; + + @BeforeEach + void setUp() { + questionService = new QuestionServiceImpl(questionRepository); + } + + @AfterEach + void cleanUp() { + SecurityContextHolder.clearContext(); + } + + @Test + void deleteQuestion_withCorrectQuestionId_returnTrue() { + when(questionRepository.findById(anyLong())) + .thenReturn(Optional.of(QuestionGenerator.generateQuestion())); + + setAuthentication(UserGenerator.generateMentor()); + + Long questionId = 1L; + assertTrue(questionService.deleteQuestion(questionId)); + } + + @Test + void deleteQuestion_withInCorrectQuestionId_returnFalse() { + when(questionRepository.findById(anyLong())).thenReturn(Optional.empty()); + + Long questionId = 1L; + assertFalse(questionService.deleteQuestion(questionId)); + } + + @Test + void deleteQuestion_withIncorrectMentor_throwIllegalArgumentException() { + when(questionRepository.findById(anyLong())).thenReturn(Optional.empty()); + + setAuthentication(UserGenerator.generateStudent()); + + Long questionId = 1L; + assertFalse(questionService.deleteQuestion(1L)); + } + + @Test + void updateQuestion_withCorrectQuestionId_returnTrue() { + when(questionRepository.findById(anyLong())) + .thenReturn(Optional.of(QuestionGenerator.generateQuestion())); + + setAuthentication(UserGenerator.generateMentor()); + + Long questionId = 1L; + assertTrue( + questionService.updateQuestion( + questionId, CreateQuestionDtoGenerator.generateCreateQuestionDto())); + } + + @Test + void updateQuestion_withInCorrectQuestionId_returnFalse() { + when(questionRepository.findById(anyLong())).thenReturn(Optional.empty()); + + Long questionId = 1L; + assertFalse(questionService.updateQuestion(questionId, CreateQuestionDto.getInstance())); + } + + @Test + void updateQuestion_withIncorrectMentor_throwIllegalArgumentException() { + when(questionRepository.findById(anyLong())).thenReturn(Optional.empty()); + + setAuthentication(UserGenerator.generateStudent()); + + Long questionId = 1L; + assertFalse(questionService.updateQuestion(questionId, CreateQuestionDto.getInstance())); + } + + @Test + void findAllByModuleId_withCorrectArg_quizDtoPage() { + doReturn(Optional.of(QuestionGenerator.generateQuestionPage())) + .when(questionRepository) + .findAllQuestionsByQuizId(anyLong(), any()); + PageRequest pageRequest = PageRequest.of(0, 1); + Page actualPageQuestionDto = questionService.findAllByQuizId(1L, pageRequest); + + Page questionDtoPage = QuestionGenerator.generateQuestionDtoPage(); + assertEquals( + actualPageQuestionDto.stream().findFirst().orElseThrow().getQuestion(), + questionDtoPage.stream().findFirst().orElseThrow().getQuestion()); + assertEquals( + actualPageQuestionDto.stream().findFirst().orElseThrow().getQuestionType(), + questionDtoPage.stream().findFirst().orElseThrow().getQuestionType()); + assertEquals( + actualPageQuestionDto.stream().findFirst().orElseThrow().getRightAnswersCount(), + questionDtoPage.stream().findFirst().orElseThrow().getRightAnswersCount()); + assertEquals( + actualPageQuestionDto.stream().findFirst().orElseThrow().getAnswerOptions().stream() + .findFirst() + .orElse(new AnswerOptionDto()) + .getAnswerOption(), + questionDtoPage.stream().findFirst().orElseThrow().getAnswerOptions().stream() + .findFirst() + .orElse(new AnswerOptionDto()) + .getAnswerOption()); + } + + @Test + void findAllByModuleId_withInCorrectModuleId_throwIllegalArgumentException() { + when(questionRepository.findAllQuestionsByQuizId(anyLong(), any())) + .thenReturn(Optional.empty()); + PageRequest pageRequest = PageRequest.of(0, 1); + assertThatThrownBy(() -> questionService.findAllByQuizId(1L, pageRequest)) + .isInstanceOf(IllegalArgumentException.class); + } + + private void setAuthentication(User user) { + SecurityContextHolder.setContext( + new SecurityContextImpl( + new UsernamePasswordAuthenticationToken(user.getEmail(), null, null))); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapperTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapperTest.java new file mode 100644 index 00000000..5b1a85fa --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapperTest.java @@ -0,0 +1,40 @@ +package app.openschool.course.module.quiz.question.api.mapper; + +import static org.assertj.core.api.Assertions.assertThat; + +import app.openschool.course.module.quiz.Quiz; +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import app.openschool.course.module.quiz.question.util.CreateQuestionDtoGenerator; +import app.openschool.course.module.quiz.question.util.QuestionGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Page; + +public class QuestionMapperTest { + @Test + void createQuestionDtoToQuestion() { + Question question = + QuestionMapper.createQuestionDtoToQuestion( + CreateQuestionDtoGenerator.generateCreateQuestionDto(), Quiz.getInstance()); + + assertThat(question) + .hasOnlyFields( + "id", "question", "rightAnswersCount", "questionType", "answerOptions", "quiz"); + } + + @Test + void toQuestionDtoPage() { + + Page questionDtoPage = + QuestionMapper.toQuestionDtoPage(QuestionGenerator.generateQuestionPage()); + + assertThat(questionDtoPage).hasOnlyElementsOfTypes(QuestionDto.class); + } + + @Test + void toQuestionDto() { + QuestionDto questionDto = QuestionMapper.toQuestionDto(QuestionGenerator.generateQuestion()); + assertThat(questionDto) + .hasOnlyFields("id", "question", "rightAnswersCount", "questionType", "answerOptions"); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/question/util/CreateQuestionDtoGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/question/util/CreateQuestionDtoGenerator.java new file mode 100644 index 00000000..75483525 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/question/util/CreateQuestionDtoGenerator.java @@ -0,0 +1,22 @@ +package app.openschool.course.module.quiz.question.util; + +import app.openschool.course.module.quiz.question.answer.api.dto.CreateAnswerOptionDto; +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import java.util.Set; + +public class CreateQuestionDtoGenerator { + private static final String QUESTION = "question"; + private static final int RIGHT_ANSWER_COUNT = 1; + private static final String MATCHING = "MATCHING"; + + private CreateQuestionDtoGenerator() {} + + public static CreateQuestionDto generateCreateQuestionDto() { + CreateQuestionDto createQuestionDto = CreateQuestionDto.getInstance(); + createQuestionDto.setQuestion(QUESTION); + createQuestionDto.setRightAnswersCount(RIGHT_ANSWER_COUNT); + createQuestionDto.setQuestionType(MATCHING); + createQuestionDto.setAnswerOptions(Set.of(CreateAnswerOptionDto.getInstance())); + return createQuestionDto; + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/question/util/QuestionGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/question/util/QuestionGenerator.java new file mode 100644 index 00000000..14634365 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/question/util/QuestionGenerator.java @@ -0,0 +1,51 @@ +package app.openschool.course.module.quiz.question.util; + +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.question.answer.AnswerOption; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import app.openschool.course.module.quiz.question.api.mapper.QuestionMapper; +import app.openschool.course.module.quiz.question.type.QuestionType; +import app.openschool.course.module.quiz.util.QuizGenerator; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +public class QuestionGenerator { + private static final Long ID = 1L; + private static final String QUESTION = "question"; + + private QuestionGenerator() {} + + public static Question generateQuestion() { + Question question = Question.getInstance(); + question.setId(ID); + question.setQuestion(QUESTION); + question.setQuestionType(QuestionType.isMultipleChoice()); + question.setQuiz(QuizGenerator.generateQuiz()); + question.setAnswerOptions(generateAnswerOption()); + return question; + } + + public static Page generateQuestionDtoPage() { + List questionDtoList = new ArrayList<>(); + QuestionDto questionDto = QuestionMapper.toQuestionDto(generateQuestion()); + questionDtoList.add(questionDto); + return new PageImpl<>(questionDtoList); + } + + public static Page generateQuestionPage() { + List questionList = new ArrayList<>(); + questionList.add(generateQuestion()); + return new PageImpl<>(questionList); + } + + private static Set generateAnswerOption() { + AnswerOption answerOption = new AnswerOption(); + answerOption.setAnswerOption("answer 1"); + answerOption.setRightAnswer(true); + + return Set.of(answerOption); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentRequestDtoGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentRequestDtoGenerator.java new file mode 100644 index 00000000..ab5ad229 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentRequestDtoGenerator.java @@ -0,0 +1,31 @@ +package app.openschool.course.module.quiz.util; + +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentRequestDto; +import app.openschool.course.module.quiz.api.dto.QuestionWithChosenAnswerDto; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class EnrolledQuizAssessmentRequestDtoGenerator { + private static final long ID = 1L; + + private EnrolledQuizAssessmentRequestDtoGenerator() {} + + public static EnrolledQuizAssessmentRequestDto generateEnrolledQuizAssessmentRequestDto() { + EnrolledQuizAssessmentRequestDto enrolledQuizAssessmentRequestDto = + new EnrolledQuizAssessmentRequestDto(); + enrolledQuizAssessmentRequestDto.setQuestionWithChosenAnswerDtoSet( + generateQuestionWithChosenAnswerDtoSet()); + return enrolledQuizAssessmentRequestDto; + } + + private static Set generateQuestionWithChosenAnswerDtoSet() { + Set questionWithChosenAnswerDtoSet = new HashSet<>(); + QuestionWithChosenAnswerDto questionWithChosenAnswerDto = + QuestionWithChosenAnswerDto.getInstance(); + questionWithChosenAnswerDto.setQuestionId(ID); + questionWithChosenAnswerDto.setChosenAnswersIds(List.of(ID)); + questionWithChosenAnswerDtoSet.add(questionWithChosenAnswerDto); + return questionWithChosenAnswerDtoSet; + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentResponseDtoGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentResponseDtoGenerator.java new file mode 100644 index 00000000..337e320e --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentResponseDtoGenerator.java @@ -0,0 +1,17 @@ +package app.openschool.course.module.quiz.util; + +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentResponseDto; + +public class EnrolledQuizAssessmentResponseDtoGenerator { + private static final int MAX_GRADE = 10; + private static final int PASSING_SCORE = 7; + private static final int RIGHT_ANSWER_COUNT = 1; + + private EnrolledQuizAssessmentResponseDtoGenerator() {} + + public static EnrolledQuizAssessmentResponseDto generateEnrolledQuizAssessmentResponseDto( + String assessmentResult) { + return new EnrolledQuizAssessmentResponseDto( + RIGHT_ANSWER_COUNT, assessmentResult, PASSING_SCORE, MAX_GRADE); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizGenerator.java new file mode 100644 index 00000000..40eff57c --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizGenerator.java @@ -0,0 +1,14 @@ +package app.openschool.course.module.quiz.util; + +import app.openschool.course.module.EnrolledModule; +import app.openschool.course.module.quiz.EnrolledQuiz; +import app.openschool.course.module.quiz.status.QuizStatus; + +public class EnrolledQuizGenerator { + private EnrolledQuizGenerator() {} + + public static EnrolledQuiz generateEnrolledQuiz() { + return new EnrolledQuiz( + QuizStatus.isInProgress(), QuizGenerator.generateQuiz(), new EnrolledModule()); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizDtoGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizDtoGenerator.java new file mode 100644 index 00000000..03ff217a --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizDtoGenerator.java @@ -0,0 +1,87 @@ +package app.openschool.course.module.quiz.util; + +import app.openschool.course.module.quiz.api.dto.CreateQuizDto; +import app.openschool.course.module.quiz.api.dto.ModifyQuizDataRequest; +import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.question.answer.api.dto.CreateAnswerOptionDto; +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + + + +public class QuizDtoGenerator { + private QuizDtoGenerator() {} + + private static final int MAX_GRADE = 10; + private static final int PASSING_SCORE = 7; + private static final String QUESTION = "question"; + + private static final String DESCRIPTION = "description"; + + private static final String TITLE = "title"; + private static final String MULTIPLE_CHOICE = "MULTIPLE_CHOICE"; + private static final String ANSWER_OPTION = "answerOption"; + private static final Boolean IS_RIGHT_ANSWER = true; + + public static CreateQuizDto generateCreateQuizDto() { + CreateQuizDto createQuizDto = CreateQuizDto.getInstance(); + createQuizDto.setTitle(TITLE); + createQuizDto.setDescription(DESCRIPTION); + createQuizDto.setMaxGrade(MAX_GRADE); + createQuizDto.setPassingScore(PASSING_SCORE); + createQuizDto.setQuestions(generateCreateQuestionDtoSet()); + return createQuizDto; + } + + public static Page generateQuizDtoPage() { + List quizDtoList = new ArrayList<>(); + quizDtoList.add(generateQuizDto()); + return new PageImpl<>(quizDtoList); + } + + public static QuizDto generateQuizDto() { + QuizDto quizDto = new QuizDto(); + quizDto.setTitle(TITLE); + quizDto.setDescription(DESCRIPTION); + quizDto.setMaxGrade(MAX_GRADE); + quizDto.setPassingScore(PASSING_SCORE); + quizDto.setQuestions(generateQuestionDtoSet()); + return quizDto; + } + + public static ModifyQuizDataRequest generateModifyQuizDataRequest() { + ModifyQuizDataRequest request = new ModifyQuizDataRequest(); + request.setTitle(TITLE); + request.setDescription(DESCRIPTION); + request.setMaxGrade(MAX_GRADE); + request.setPassingScore(PASSING_SCORE); + return request; + } + + private static Set generateCreateQuestionDtoSet() { + CreateQuestionDto createQuestionDto = CreateQuestionDto.getInstance(); + createQuestionDto.setQuestion(QUESTION); + createQuestionDto.setQuestionType(MULTIPLE_CHOICE); + createQuestionDto.setAnswerOptions(generateCreateAnswerOptionDtoSet()); + return Set.of(createQuestionDto); + } + + private static Set generateQuestionDtoSet() { + QuestionDto questionDto = QuestionDto.getInstance(); + questionDto.setQuestion(QUESTION); + questionDto.setQuestionType(MULTIPLE_CHOICE); + return Set.of(questionDto); + } + + private static Set generateCreateAnswerOptionDtoSet() { + CreateAnswerOptionDto createAnswerOptionDto = CreateAnswerOptionDto.getInstance(); + createAnswerOptionDto.setAnswerOption(ANSWER_OPTION); + createAnswerOptionDto.setRightAnswer(IS_RIGHT_ANSWER); + return Set.of(createAnswerOptionDto); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizGenerator.java new file mode 100644 index 00000000..e67f27a6 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizGenerator.java @@ -0,0 +1,73 @@ +package app.openschool.course.module.quiz.util; + +import app.openschool.course.module.api.ModuleGenerator; +import app.openschool.course.module.quiz.Quiz; +import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.api.mapper.QuizMapper; +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.question.answer.AnswerOption; +import app.openschool.course.module.quiz.question.type.QuestionType; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +public class QuizGenerator { + private static final Long ID = 1L; + private static final int MAX_GRADE = 10; + + private static final String DESCRIPTION = "description"; + + private static final String TITLE = "title"; + private static final int PASSING_SCORE = 7; + private static final String QUESTION = "question"; + private static final String ANSWER_OPTION = "answerOption"; + private static final Boolean IS_RIGHT_ANSWER = true; + private static final int RIGHT_ANSWER_COUNT = 1; + + private QuizGenerator() {} + + public static Quiz generateQuiz() { + Quiz quiz = Quiz.getInstance(); + quiz.setId(ID); + quiz.setTitle(TITLE); + quiz.setDescription(DESCRIPTION); + quiz.setModule(ModuleGenerator.generateModule()); + quiz.setMaxGrade(MAX_GRADE); + quiz.setPassingScore(PASSING_SCORE); + quiz.setQuestions(generateQuestionSet()); + return quiz; + } + + public static Page generateQuizPage() { + List quizList = new ArrayList<>(); + quizList.add(generateQuiz()); + return new PageImpl<>(quizList); + } + + public static Page generateQuizDtoPage() { + List quizDtoList = new ArrayList<>(); + QuizDto quizDto = QuizMapper.quizToQuizDto(generateQuiz()); + quizDtoList.add(quizDto); + return new PageImpl<>(quizDtoList); + } + + private static Set generateQuestionSet() { + Question question = Question.getInstance(); + question.setId(ID); + question.setQuestion(QUESTION); + question.setRightAnswersCount(RIGHT_ANSWER_COUNT); + question.setQuestionType(QuestionType.isMultipleChoice()); + question.setAnswerOptions(generateAnswerOptionSet()); + return Set.of(question); + } + + private static Set generateAnswerOptionSet() { + AnswerOption answerOption = AnswerOption.getInstance(); + answerOption.setId(ID); + answerOption.setAnswerOption(ANSWER_OPTION); + answerOption.setRightAnswer(IS_RIGHT_ANSWER); + return Set.of(answerOption); + } +} diff --git a/os-server/src/test/java/app/openschool/user/api/UserGenerator.java b/os-server/src/test/java/app/openschool/user/api/UserGenerator.java index 19b8eae0..5cb6da78 100644 --- a/os-server/src/test/java/app/openschool/user/api/UserGenerator.java +++ b/os-server/src/test/java/app/openschool/user/api/UserGenerator.java @@ -34,4 +34,29 @@ public static User generateUserWithSavedMentors() { user.getMentors().add(generateUser()); return user; } + + public static User generateMentor() { + + String email = "mentor"; + String password = "pass"; + User mentor = new User(email, password); + mentor.setName("Jon"); + mentor.setSurname("Smith"); + Role role = new Role(); + role.setId(3); + role.setType("MENTOR"); + mentor.setRole(role); + return mentor; + } + + public static User generateStudent() { + String email = "student"; + String password = "pass"; + User mentor = new User(email, password); + Role role = new Role(); + role.setId(1); + role.setType("STUDENT"); + mentor.setRole(role); + return mentor; + } }