Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1a4fd05
implement pagination to quiz search
Wassim-Rached Jun 1, 2024
79c3f9d
update test cases for pagingation with quiz search
Wassim-Rached Jun 1, 2024
0375ae5
add exception handling for MethodArgumentTypeMismatchException
Wassim-Rached Jun 1, 2024
a4a34c1
fix a problem with api docs not finding the test generations
Wassim-Rached Jun 2, 2024
83360b3
implement reverse constructor for QuizCreationDTO to be created from …
Wassim-Rached Jun 2, 2024
36cca5c
implement logic for reverse creation
Wassim-Rached Jun 2, 2024
cccaeba
create the endpoint for retrieving the data for quiz creation
Wassim-Rached Jun 2, 2024
7b7d075
changed getQuizCreationInfo endpoint from the quiz controller to the …
Wassim-Rached Jun 2, 2024
af92f58
create the first runner which will load pre-difined data to the datab…
Wassim-Rached Jun 2, 2024
1b0e2e4
update seed name
Wassim-Rached Jun 2, 2024
50ddfc5
created new utils service to be responsible for loading the seed data
Wassim-Rached Jun 2, 2024
743e7e3
implement the new service ISeedDataLoader in StartupRunner
Wassim-Rached Jun 2, 2024
64a4537
fix dev controller endpoint
Wassim-Rached Jun 2, 2024
0e91500
implement reverse constructor for QuizCreationDTO to be created from …
Wassim-Rached Jun 2, 2024
2edb497
implement logic for reverse creation
Wassim-Rached Jun 2, 2024
3211177
create the endpoint for retrieving the data for quiz creation
Wassim-Rached Jun 2, 2024
99efd93
changed getQuizCreationInfo endpoint from the quiz controller to the …
Wassim-Rached Jun 2, 2024
8fba898
create the first runner which will load pre-difined data to the datab…
Wassim-Rached Jun 2, 2024
45334f1
update seed name
Wassim-Rached Jun 2, 2024
d5f7c0d
created new utils service to be responsible for loading the seed data
Wassim-Rached Jun 2, 2024
d549f79
implement the new service ISeedDataLoader in StartupRunner
Wassim-Rached Jun 2, 2024
6b6d3da
fix dev controller endpoint
Wassim-Rached Jun 2, 2024
24681df
fill some seed data
Wassim-Rached Jun 4, 2024
4f6f7d9
add comparing logic to 'Match' and 'Option' enitity
Wassim-Rached Jun 4, 2024
793a0e2
remove unnecessary empty arrays and objects initialization
Wassim-Rached Jun 4, 2024
b4956a0
add new method to generate perfect quiz attempt in FakeDataLogicalGen…
Wassim-Rached Jun 4, 2024
f3cd24e
change fetch type to Eager for needed attributes in question processing
Wassim-Rached Jun 4, 2024
ff511d1
null is better for empty data
Wassim-Rached Jun 4, 2024
707e6bf
swap to null for empty data in QuestionAttemptResultDTO
Wassim-Rached Jun 4, 2024
4cb463a
fix error by flushing data before fetching again
Wassim-Rached Jun 4, 2024
02aed53
improve docs generation tests to work with more human readable data
Wassim-Rached Jun 4, 2024
ce6e3f9
turn off deep source errors for missing docs
Wassim-Rached Jun 4, 2024
02aa37c
swap to Eager fetch in question for all attributes
Wassim-Rached Jun 5, 2024
6ea0acd
improve e2e tests
Wassim-Rached Jun 5, 2024
24a9844
override .equals for question to fix warning
Wassim-Rached Jun 5, 2024
de87d20
Merge branch 'staging' into dev
Wassim-Rached Jun 5, 2024
6c5a36e
Merge pull request #31 from Wassim-Rached/dev
Wassim-Rached Jun 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .deepsource.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@ enabled = true

[[analyzers]]
name = "shell"
enabled = true
enabled = true

[[rules]]
analyzer = "java"
enabled = true
name = "doc-missing"
7 changes: 4 additions & 3 deletions .github/workflows/continuous_delivery_staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ jobs:
distribution: 'corretto'
java-version: 21

- name: Clean the application
run: mvn clean

- name: Test the application
run: mvn -B test --file pom.xml

- name: Build the application
run: |
mvn clean
mvn -B package --file pom.xml
run: mvn -B package --file pom.xml

- name: Build Docker Image
uses: docker/build-push-action@v2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.wa55death405.quizhub.dto.StandardApiResponse;
import org.wa55death405.quizhub.enums.StandardApiStatus;
import org.wa55death405.quizhub.exceptions.InputValidationException;
Expand All @@ -18,6 +19,12 @@ this class is the controller advice
it is used to handle the exceptions thrown by the controllers
*/

/*
TODO:
separate the exception handlers into different classes
* one for predefined spring or library exceptions
* one for custom exceptions
*/
@ControllerAdvice
public class ControllerExceptionHandler {

Expand All @@ -40,4 +47,9 @@ public ResponseEntity<StandardApiResponse<Void>> handleRateLimitReachedException
public ResponseEntity<StandardApiResponse<Void>> handleFileNotFoundException(FileNotFoundException e) {
return new ResponseEntity<>(new StandardApiResponse<>(StandardApiStatus.FAILURE, "File not found"), HttpStatus.NOT_FOUND);
}

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<StandardApiResponse<Void>> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
return new ResponseEntity<>(new StandardApiResponse<>(StandardApiStatus.FAILURE, "Invalid value for parameter "+ e.getPropertyName() +". Expected type: "+ e.getRequiredType()), HttpStatus.BAD_REQUEST);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.wa55death405.quizhub.dto.StandardApiResponse;
import org.wa55death405.quizhub.dto.questionAttempt.QuestionAttemptSubmissionDTO;
import org.wa55death405.quizhub.dto.quiz.QuizCreationDTO;
import org.wa55death405.quizhub.enums.StandardApiStatus;
import org.wa55death405.quizhub.interfaces.utils.IFakeDataLogicalGenerator;
import org.wa55death405.quizhub.repositories.QuizAttemptRepository;
import org.wa55death405.quizhub.services.QuizService;
Expand All @@ -23,6 +26,7 @@ this class is the controller of the dev
@RestController
@RequiredArgsConstructor
@Profile("dev")
@RequestMapping("/api/dev")
public class DevController {
private final QuizService quizService;
private final QuizAttemptRepository quizAttemptRepository;
Expand All @@ -44,4 +48,15 @@ public void submitPerfectResponse(@PathVariable UUID quizAttemptId) {
List<QuestionAttemptSubmissionDTO> attempts = fakeDataLogicalGenerator.getPerfectScoreQuestionAttemptSubmissionDTOsForQuiz(quiz);
quizService.submitQuestionAttempts(attempts,quizAttemptId);
}

/*
this api is used to get the needed information to
be able to create certain quiz that already exists
@Param quizId the id of the quiz
@return the needed information to create the quiz
*/
@GetMapping("/quiz/{quizId}/creation-info")
public ResponseEntity<StandardApiResponse<QuizCreationDTO>> getQuizCreationInfo(@PathVariable UUID quizId) {
return new ResponseEntity<>(new StandardApiResponse<>(StandardApiStatus.SUCCESS, "Quiz creation info fetched successfully", quizService.getQuizCreationInfo(quizId)), HttpStatus.OK);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.wa55death405.quizhub.dto.StandardPageList;
import org.wa55death405.quizhub.dto.questionAttempt.QuestionAttemptSubmissionDTO;
import org.wa55death405.quizhub.dto.quiz.QuizCreationDTO;
import org.wa55death405.quizhub.dto.StandardApiResponse;
Expand Down Expand Up @@ -32,11 +33,16 @@ public class QuizController {
/*
this api is used to get search for quizzes
it takes the title of the quiz as a query parameter
along with the page number and the size of the page
and returns a list of quizzes that match the title
*/
@GetMapping("/search")
public ResponseEntity<StandardApiResponse<List<QuizGeneralInfoDTO>>> searchQuizzes(@RequestParam(required = false,defaultValue = "") String title) {
return new ResponseEntity<>(new StandardApiResponse<>(StandardApiStatus.SUCCESS,"Quizzes fetched successfully",quizService.searchQuizzes(title)), HttpStatus.OK);
public ResponseEntity<StandardApiResponse<StandardPageList<QuizGeneralInfoDTO>>> searchQuizzes(
@RequestParam(defaultValue = "") String title,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
) {
return new ResponseEntity<>(new StandardApiResponse<>(StandardApiStatus.SUCCESS,"Quizzes fetched successfully",quizService.searchQuizzes(title, page, size)), HttpStatus.OK);
}

/*
Expand Down Expand Up @@ -110,4 +116,5 @@ public ResponseEntity<StandardApiResponse<QuizAttemptResultDTO>> getQuizAttemptR
return new ResponseEntity<>(new StandardApiResponse<>(StandardApiStatus.SUCCESS,"Quiz attempt result fetched successfully",quizService.getQuizAttemptResult(quizAttemptId)), HttpStatus.OK);
}


}
17 changes: 17 additions & 0 deletions src/main/java/org/wa55death405/quizhub/dto/StandardPageList.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.wa55death405.quizhub.dto;

import lombok.Data;

import java.util.List;

/*
* this class is used as standard response for the paginated list
* @param <T> the type of the items in the list
*/
@Data
public class StandardPageList<T>{
private Integer currentPage;
private Integer currentItemsSize;
private Long totalItems;
private List<T> items;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.wa55death405.quizhub.dto.question;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.wa55death405.quizhub.entities.*;
import org.wa55death405.quizhub.enums.QuestionType;
import org.wa55death405.quizhub.exceptions.InputValidationException;
Expand All @@ -11,15 +12,64 @@
import java.util.List;

@Data
@NoArgsConstructor
public class QuestionCreationRequestDTO implements EntityDTO<Question,Quiz> {
private String question;
private QuestionType questionType;
private Float coefficient;
private String answer;
private HashMap<String,Boolean> choices = new HashMap<>();
private HashMap<Integer,String> orderedOptions = new HashMap<>();
// HashMap<option, [match1, match2, ...]>
private HashMap<String,List<String>> optionMatches = new HashMap<>();

/*
transform the question back to
the dto that was used to create it
*/
public QuestionCreationRequestDTO(Question question) {
this.question = question.getQuestion();
this.questionType = question.getQuestionType();
this.coefficient = question.getCoefficient();
switch (questionType) {
case TRUE_FALSE,SHORT_ANSWER,NUMERIC,FILL_IN_THE_BLANK:
this.answer = question.getAnswer().getAnswer();
break;

case MULTIPLE_CHOICE:
case SINGLE_CHOICE:
question.getChoices().forEach(choice -> choices.put(choice.getChoice(), choice.getIsCorrect()));
break;

case OPTION_ORDERING:
question.getOrderedOptions().forEach(orderedOption -> orderedOptions.put(orderedOption.getCorrectPosition(), orderedOption.getOption()));
break;

case OPTION_MATCHING:
// TODO: this might not work as expected
/*
the double call on:
* the getMatch().getMatch() method
* and the getOption().getOption() method
is because the first one is for the entity instance (Match, Option)
and the second one is for the value of the entity (String)
*/
for (Match match : question.getMatches()){
optionMatches.put(match.getMatch(), new ArrayList<>());
for (CorrectOptionMatch correctOptionMatch : question.getCorrectOptionMatches()){
if (correctOptionMatch.getMatch().getMatch().equals(match.getMatch())){
optionMatches.get(match.getMatch()).add(correctOptionMatch.getOption().getOption());
}
}
}
break;
}
}

/*
transform the dto to the question entity
the quiz is used as a reference to the question
*/
@Override
public Question toEntity(Quiz quiz) {
if (coefficient <= 0) {
Expand Down Expand Up @@ -112,4 +162,6 @@ public Question toEntity(Quiz quiz) {
}
return question;
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.wa55death405.quizhub.dto.questionAttempt.QuestionAttemptResultDTO;
import org.wa55death405.quizhub.entities.*;
import org.wa55death405.quizhub.enums.QuestionType;
import org.wa55death405.quizhub.exceptions.InputValidationException;

import java.util.ArrayList;
import java.util.List;
Expand All @@ -30,15 +31,15 @@ public class QuestionResultDTO {
private AnswerResultDTO answer;

// for MULTIPLE_CHOICE, SINGLE_CHOICE
private List<ChoiceResultDTO> choices = new ArrayList<>();
private List<ChoiceResultDTO> choices;

// for OPTION_ORDERING
private List<OrderedOptionResultDTO> orderedOptions = new ArrayList<>();
private List<OrderedOptionResultDTO> orderedOptions;

// for MATCHING_OPTION
private List<MatchGeneralDTO> matches = new ArrayList<>();
private List<OptionGeneralDTO> options = new ArrayList<>();
private List<CorrectOptionMatchResultDTO> correctOptionMatches = new ArrayList<>();
private List<MatchGeneralDTO> matches;
private List<OptionGeneralDTO> options;
private List<CorrectOptionMatchResultDTO> correctOptionMatches;

// to show the play's attempt
private QuestionAttemptResultDTO questionAttempt;
Expand All @@ -48,37 +49,47 @@ public QuestionResultDTO(Question question,QuestionAttempt questionAttempt) {
this.question = question.getQuestion();
this.coefficient = question.getCoefficient();
this.questionType = question.getQuestionType();
// I know this is ugly, but I have to do this

if (questionAttempt != null){
this.questionAttempt = new QuestionAttemptResultDTO(questionAttempt);
}
if (question.getAnswer() != null){
this.answer = new AnswerResultDTO(question.getAnswer());
}
if (question.getChoices() != null) {
for (Choice choice : question.getChoices()) {
choices.add(new ChoiceResultDTO(choice));
}
}
if (question.getOrderedOptions() != null) {
for (OrderedOption orderedOption : question.getOrderedOptions()) {
orderedOptions.add(new OrderedOptionResultDTO(orderedOption));

switch (this.questionType){
case TRUE_FALSE,FILL_IN_THE_BLANK,NUMERIC,SHORT_ANSWER->{
if (question.getAnswer() == null)
throw new InputValidationException("Answer is required for question of type " + this.questionType);
this.answer = new AnswerResultDTO(question.getAnswer());
}
}
if (question.getMatches() != null) {
for (Match match : question.getMatches()) {
matches.add(new MatchGeneralDTO(match));
case MULTIPLE_CHOICE,SINGLE_CHOICE -> {
choices = new ArrayList<>();
for (Choice choice : question.getChoices()) {
choices.add(new ChoiceResultDTO(choice));
}
}
}
if (question.getOptions() != null) {
for (Option option : question.getOptions()) {
options.add(new OptionGeneralDTO(option));
case OPTION_MATCHING -> {
matches = new ArrayList<>();
options = new ArrayList<>();
correctOptionMatches = new ArrayList<>();

for (Match match : question.getMatches()) {
matches.add(new MatchGeneralDTO(match));
}

for (Option option : question.getOptions()) {
options.add(new OptionGeneralDTO(option));
}

for (CorrectOptionMatch correctOptionMatch : question.getCorrectOptionMatches()) {
correctOptionMatches.add(new CorrectOptionMatchResultDTO(correctOptionMatch));
}
}
}
if (question.getCorrectOptionMatches() != null) {
for (CorrectOptionMatch correctOptionMatch : question.getCorrectOptionMatches()) {
correctOptionMatches.add(new CorrectOptionMatchResultDTO(correctOptionMatch));
case OPTION_ORDERING -> {
orderedOptions = new ArrayList<>();
for (OrderedOption orderedOption : question.getOrderedOptions()) {
orderedOptions.add(new OrderedOptionResultDTO(orderedOption));
}
}
default -> {throw new IllegalArgumentException("Invalid question type");}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ public class QuestionTakingDTO {
private QuestionType questionType;

// For MULTIPLE_CHOICE and SINGLE_CHOICE
private List<ChoiceGeneralDTO> choices = new ArrayList<>();
private List<ChoiceGeneralDTO> choices;

// For OPTION_ORDERING
private List<OrderedOptionGeneralDTO> orderedOptions = new ArrayList<>();
private List<OrderedOptionGeneralDTO> orderedOptions;

// For OPTION_MATCHING
private List<MatchGeneralDTO> matches = new ArrayList<>();
private List<OptionGeneralDTO> options = new ArrayList<>();
private List<MatchGeneralDTO> matches;
private List<OptionGeneralDTO> options;

// For previous attempts
private QuestionAttemptTakingDTO questionAttempt;
Expand All @@ -46,16 +46,24 @@ public QuestionTakingDTO(Question question, QuestionAttempt questionAttempt){
if (questionAttempt != null){
this.questionAttempt = new QuestionAttemptTakingDTO(questionAttempt);
}
this.choices = question.getChoices().stream()

if (!question.getChoices().isEmpty())
this.choices = question.getChoices().stream()
.map(ChoiceGeneralDTO::new)
.toList();
this.orderedOptions = question.getOrderedOptions().stream()

if (!question.getOrderedOptions().isEmpty())
this.orderedOptions = question.getOrderedOptions().stream()
.map(OrderedOptionGeneralDTO::new)
.toList();
this.matches = question.getMatches().stream()

if (!question.getMatches().isEmpty())
this.matches = question.getMatches().stream()
.map(MatchGeneralDTO::new)
.toList();
this.options = question.getOptions().stream()

if (!question.getOptions().isEmpty())
this.options = question.getOptions().stream()
.map(OptionGeneralDTO::new)
.toList();
}
Expand Down
Loading