From afedfc9b36a18a2e084877e1af7a167b88bad605 Mon Sep 17 00:00:00 2001 From: Ryan Ez Zerqti Date: Tue, 5 May 2026 14:55:34 +0200 Subject: [PATCH] upgrade quiz --- .../controller/AdminController.java | 14 ++-- .../dtos/quiz/CreateQuizRequest.java | 30 ++++++++- .../dtos/quiz/UpdateQuizRequest.java | 9 +++ .../moneybuddy/fr/moneybuddy/model/Quiz.java | 17 ++++- .../fr/moneybuddy/model/enums/QuizType.java | 13 ++++ .../fr/moneybuddy/service/QuizService.java | 66 +++++++++++++++++-- 6 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 src/main/java/moneybuddy/fr/moneybuddy/model/enums/QuizType.java diff --git a/src/main/java/moneybuddy/fr/moneybuddy/controller/AdminController.java b/src/main/java/moneybuddy/fr/moneybuddy/controller/AdminController.java index 93cbad7..ac8f2b5 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/controller/AdminController.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/controller/AdminController.java @@ -206,14 +206,20 @@ public ResponseEntity
updateSection( } // Quizzes - @PostMapping("/courses/sections/quizzes") - public ResponseEntity createQuiz(@Valid @RequestBody CreateQuizRequest req) { + @PostMapping( + value = "/courses/sections/quizzes", + consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity createQuiz(@Valid @ModelAttribute CreateQuizRequest req) + throws FileUploadException, JsonMappingException, JsonProcessingException { return ResponseEntity.status(HttpStatus.CREATED).body(quizService.createQuiz(req)); } - @PutMapping("/courses/sections/quizzes/{quizId}") + @PutMapping( + value = "/courses/sections/quizzes/{quizId}", + consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity updateQuiz( - @Valid @RequestBody UpdateQuizRequest req, @PathVariable String quizId) { + @Valid @ModelAttribute UpdateQuizRequest req, @PathVariable String quizId) + throws FileUploadException, JsonMappingException, JsonProcessingException { return ResponseEntity.status(HttpStatus.ACCEPTED).body(quizService.updateQuiz(quizId, req)); } diff --git a/src/main/java/moneybuddy/fr/moneybuddy/dtos/quiz/CreateQuizRequest.java b/src/main/java/moneybuddy/fr/moneybuddy/dtos/quiz/CreateQuizRequest.java index ef811d4..6c4c5bd 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/dtos/quiz/CreateQuizRequest.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/dtos/quiz/CreateQuizRequest.java @@ -3,10 +3,14 @@ */ package moneybuddy.fr.moneybuddy.dtos.quiz; +import java.util.List; import java.util.Map; import jakarta.validation.constraints.*; import lombok.*; +import moneybuddy.fr.moneybuddy.model.Quiz.WrongResponse; +import moneybuddy.fr.moneybuddy.model.enums.QuizType; +import org.springframework.web.multipart.MultipartFile; @Data @Builder @@ -20,11 +24,33 @@ public class CreateQuizRequest { private String question; private String response; + private MultipartFile file; + private List moneyValues; + private List wrongAnswers; - @NotNull(message = "At least two options are required") private Map options; + private List optionsImages; @Min(0) @NotNull(message = "CorrectAnswerIndex has to have at least one correct answer") - private int correctAnswerIndex; + private Integer correctAnswerIndex; + + @NotNull(message = "Insert quiz type") + private QuizType quizType; + + @AssertTrue( + message = "QuizType CALCULATE has to have moneyValues length > 0 and options lenght = 1") + public boolean isQuizTypeCorrect() { + if (quizType == null) return true; + + if (QuizType.CALCULATE.equals(quizType)) + return moneyValues != null + && !moneyValues.isEmpty() + && options != null + && options.size() == 1; + + if (QuizType.IMAGES.equals(quizType)) return optionsImages != null && optionsImages.size() >= 2; + + return options != null && options.size() >= 2; + } } diff --git a/src/main/java/moneybuddy/fr/moneybuddy/dtos/quiz/UpdateQuizRequest.java b/src/main/java/moneybuddy/fr/moneybuddy/dtos/quiz/UpdateQuizRequest.java index 6f2baa8..7405d13 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/dtos/quiz/UpdateQuizRequest.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/dtos/quiz/UpdateQuizRequest.java @@ -7,13 +7,22 @@ import jakarta.validation.constraints.Min; import lombok.Data; +import moneybuddy.fr.moneybuddy.model.Quiz.WrongResponse; +import moneybuddy.fr.moneybuddy.model.enums.QuizType; +import org.springframework.web.multipart.MultipartFile; @Data public class UpdateQuizRequest { private String question; private String response; + private MultipartFile file; + private List moneyValues; + private List wrongAnswers; + private QuizType quizType; + private List options; + private List optionsImages; @Min(0) private int correctAnswerIndex; diff --git a/src/main/java/moneybuddy/fr/moneybuddy/model/Quiz.java b/src/main/java/moneybuddy/fr/moneybuddy/model/Quiz.java index 02a8fdb..0e8ebe9 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/model/Quiz.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/model/Quiz.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.*; +import moneybuddy.fr.moneybuddy.model.enums.QuizType; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; @@ -20,19 +21,31 @@ @NoArgsConstructor @AllArgsConstructor public class Quiz { - @Id String id; + @Id private String id; private String sectionId; private String courseId; private String question; + private String imageUrl; private String response; + private QuizType quizType; @JsonIgnore @Builder.Default private Map options = new HashMap<>(); + private int correctAnswerIndex; + private List wrongAnswers; + + private List moneyValues; @JsonProperty("options") public List getOptionsAsList() { return new ArrayList<>(options.values()); } - private int correctAnswerIndex; + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class WrongResponse { + private int answerIndex; + private String response; + } } diff --git a/src/main/java/moneybuddy/fr/moneybuddy/model/enums/QuizType.java b/src/main/java/moneybuddy/fr/moneybuddy/model/enums/QuizType.java new file mode 100644 index 0000000..ad000a5 --- /dev/null +++ b/src/main/java/moneybuddy/fr/moneybuddy/model/enums/QuizType.java @@ -0,0 +1,13 @@ +/* + * Copyright moneybuddy.fr moneybuddy + */ +package moneybuddy.fr.moneybuddy.model.enums; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(enumAsRef = true) +public enum QuizType { + IMAGES, + TEXT, + CALCULATE +} diff --git a/src/main/java/moneybuddy/fr/moneybuddy/service/QuizService.java b/src/main/java/moneybuddy/fr/moneybuddy/service/QuizService.java index ae0976a..9acc9a6 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/service/QuizService.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/service/QuizService.java @@ -3,10 +3,13 @@ */ package moneybuddy.fr.moneybuddy.service; -import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Optional; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; import lombok.RequiredArgsConstructor; import moneybuddy.fr.moneybuddy.dtos.quiz.CreateQuizRequest; import moneybuddy.fr.moneybuddy.dtos.quiz.UpdateQuizRequest; @@ -14,8 +17,10 @@ import moneybuddy.fr.moneybuddy.exception.SectionNotFoundException; import moneybuddy.fr.moneybuddy.model.Quiz; import moneybuddy.fr.moneybuddy.model.Section; +import moneybuddy.fr.moneybuddy.model.enums.QuizType; import moneybuddy.fr.moneybuddy.repository.QuizRepository; import moneybuddy.fr.moneybuddy.repository.SectionRepository; +import org.apache.tomcat.util.http.fileupload.FileUploadException; import org.springframework.stereotype.Service; @Service @@ -23,6 +28,7 @@ public class QuizService { private final QuizRepository quizRepository; private final SectionRepository sectionRepository; + private final CloudflareService cloudflareService; public Quiz getById(String id) { return quizRepository.findById(id).orElseThrow(() -> new QuizNotFoundException(id)); @@ -34,18 +40,40 @@ public Section getSection(String sectionId) { .orElseThrow(() -> new SectionNotFoundException(sectionId)); } - public Quiz createQuiz(CreateQuizRequest req) { + public Quiz createQuiz(CreateQuizRequest req) + throws FileUploadException, JsonMappingException, JsonProcessingException { Section section = getSection(req.getSectionId()); + Map resolvedOptions; + if (QuizType.IMAGES.equals(req.getQuizType())) { + resolvedOptions = new LinkedHashMap<>(); + List images = req.getOptionsImages(); + for (int i = 0; i < images.size(); i++) { + String key = cloudflareService.uploadImage(images.get(i)); + resolvedOptions.put("option" + (i + 1), key); + } + } else { + resolvedOptions = req.getOptions(); + } + Quiz quiz = Quiz.builder() .correctAnswerIndex(req.getCorrectAnswerIndex()) - .options(req.getOptions()) + .options(resolvedOptions) .question(req.getQuestion()) .response(req.getResponse()) .sectionId(req.getSectionId()) .courseId(section.getCourseId()) + .quizType(req.getQuizType()) + .wrongAnswers(req.getWrongAnswers()) + .moneyValues(req.getMoneyValues()) .build(); + + if (req.getFile() != null && req.getFile().getSize() > 0) { + String imageUrl = cloudflareService.uploadImage(req.getFile()); + quiz.setImageUrl(imageUrl); + } + quiz = quizRepository.save(quiz); section.getQuiz().put(quiz.getId(), quiz); @@ -54,14 +82,40 @@ public Quiz createQuiz(CreateQuizRequest req) { return quiz; } - public Quiz updateQuiz(String quizId, UpdateQuizRequest req) { + public Quiz updateQuiz(String quizId, UpdateQuizRequest req) + throws FileUploadException, JsonMappingException, JsonProcessingException { Quiz quiz = getById(quizId); if (req.getCorrectAnswerIndex() >= 0) quiz.setCorrectAnswerIndex(req.getCorrectAnswerIndex()); + quiz.setQuestion(Optional.ofNullable(req.getQuestion()).orElse(quiz.getQuestion())); quiz.setResponse(Optional.ofNullable(req.getResponse()).orElse(quiz.getResponse())); - if (req.getOptions() != null && !req.getOptions().isEmpty()) { - Map options = new HashMap<>(); + quiz.setQuizType(Optional.ofNullable(req.getQuizType()).orElse(quiz.getQuizType())); + quiz.setWrongAnswers(Optional.ofNullable(req.getWrongAnswers()).orElse(quiz.getWrongAnswers())); + quiz.setMoneyValues(Optional.ofNullable(req.getMoneyValues()).orElse(quiz.getMoneyValues())); + + if (req.getFile() != null && req.getFile().getSize() > 0) { + String image_url = cloudflareService.uploadImage(req.getFile()); + cloudflareService.remove(quiz.getImageUrl()); + quiz.setImageUrl(image_url); + } + + if (QuizType.IMAGES.equals(quiz.getQuizType()) + && req.getOptionsImages() != null + && !req.getOptionsImages().isEmpty()) { + // Delete old R2 images before uploading new ones + if (quiz.getOptions() != null) { + quiz.getOptions().values().forEach(cloudflareService::remove); + } + Map newOptions = new LinkedHashMap<>(); + List images = req.getOptionsImages(); + for (int i = 0; i < images.size(); i++) { + String key = cloudflareService.uploadImage(images.get(i)); + newOptions.put("option" + (i + 1), key); + } + quiz.setOptions(newOptions); + } else if (req.getOptions() != null && !req.getOptions().isEmpty()) { + Map options = new LinkedHashMap<>(); for (int i = 0; i < req.getOptions().size(); i++) { options.put("option" + (i + 1), req.getOptions().get(i)); }