Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into feat/https
Browse files Browse the repository at this point in the history
  • Loading branch information
jjgancfer committed Apr 9, 2024
2 parents 1737a53 + 326b014 commit 8cce1ec
Show file tree
Hide file tree
Showing 44 changed files with 762 additions and 604 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
node-version: 20
- run: npm --prefix webapp install
- run: npm --prefix webapp run build
#- run: npm --prefix webapp run test:e2e TODO: re-enable
# - run: npm --prefix webapp run test:e2e
docker-push-api:
runs-on: ubuntu-latest
needs: [ e2e-tests ]
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# 🧠🤔 KiWiq 🥝❓📚

Visit our page [here!!!](http://kiwiq.run.place/).

WIQ is a quiz game project inspired by the engaging and thought-provoking show "Saber y Ganar."
We aim to create a platform that not only challenges your knowledge but also sparks curiosity and the thrill of discovery.
Expand Down Expand Up @@ -48,4 +48,4 @@ Webapp | `webapp/` | Our own frontend to the backend. It is implemented in React

***

Both the backend/API and the question generator use PostgreSQL.
Both the backend/API and the question generator use PostgreSQL.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
@Log4j2
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CustomControllerAdvice extends ResponseEntityExceptionHandler {
@ExceptionHandler(InternalApiErrorException.class)
public ResponseEntity<String> handleInternalApiErrorException(InternalApiErrorException exception){
log.error(exception.getMessage(),exception);
return new ResponseEntity<>(exception.getMessage(),HttpStatus.SERVICE_UNAVAILABLE);
}
@ExceptionHandler(InvalidAuthenticationException.class)
public ResponseEntity<String> handleInvalidAuthenticationException(InvalidAuthenticationException exception){
log.error(exception.getMessage(),exception);
Expand All @@ -28,7 +33,11 @@ public ResponseEntity<String> handleNoSuchElementException(NoSuchElementExceptio
log.error(exception.getMessage(),exception);
return new ResponseEntity<>(exception.getMessage(),HttpStatus.NOT_FOUND);
}

@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<String> handleIllegalStateException(IllegalStateException exception){
log.error(exception.getMessage(),exception);
return new ResponseEntity<>(exception.getMessage(),HttpStatus.CONFLICT);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException exception){
log.error(exception.getMessage(),exception);
Expand Down Expand Up @@ -60,7 +69,7 @@ public ResponseEntity<String> handleInternalAuthenticationServiceException(Inter
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception exception){
log.error(exception.getMessage(),exception);
return new ResponseEntity<>(exception.getMessage(),HttpStatus.INTERNAL_SERVER_ERROR);
return new ResponseEntity<>("Internal Server Error",HttpStatus.INTERNAL_SERVER_ERROR);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package lab.en2b.quizapi.commons.exceptions;

public class InternalApiErrorException extends RuntimeException{
public InternalApiErrorException(String message) {
super(message);
}
}
34 changes: 20 additions & 14 deletions api/src/main/java/lab/en2b/quizapi/game/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
import lab.en2b.quizapi.commons.user.User;
import lab.en2b.quizapi.questions.answer.Answer;
import lab.en2b.quizapi.questions.question.Question;
import lab.en2b.quizapi.questions.question.QuestionRepository;
import lombok.*;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.List;

@Entity
Expand All @@ -25,12 +26,12 @@ public class Game {
@Setter(AccessLevel.NONE)
private Long id;

private int rounds = 9;
private int actualRound = 0;
private Long rounds = 9L;
private Long actualRound = 0L;

private int correctlyAnsweredQuestions = 0;
private Long correctlyAnsweredQuestions = 0L;
private String language;
private LocalDateTime roundStartTime;
private Long roundStartTime = 0L;
@NonNull
private Integer roundDuration;
private boolean currentQuestionAnswered;
Expand All @@ -56,29 +57,29 @@ public void newRound(Question question){
if(getActualRound() != 0){
if (isGameOver())
throw new IllegalStateException("You can't start a round for a finished game!");
//if(!currentQuestionAnswered)
// throw new IllegalStateException("You can't start a new round when the current round is not over yet!");
if(!currentRoundIsOver())
throw new IllegalStateException("You can't start a new round when the current round is not over yet!");
}

setCurrentQuestionAnswered(false);
getQuestions().add(question);
increaseRound();
setRoundStartTime(LocalDateTime.now());
setRoundStartTime(Instant.now().toEpochMilli());
}

private void increaseRound(){
setActualRound(getActualRound() + 1);
}

public boolean isGameOver(){
return getActualRound() > getRounds();
return isGameOver && getActualRound() >= getRounds();
}

public boolean isLastRound(){
return getActualRound() >= getRounds();
}

public Question getCurrentQuestion() {
if(getRoundStartTime() == null){
throw new IllegalStateException("The round is not active!");
}
if(currentRoundIsOver())
throw new IllegalStateException("The current round is over!");
if(isGameOver())
Expand All @@ -91,10 +92,10 @@ private boolean currentRoundIsOver(){
}

private boolean roundTimeHasExpired(){
return LocalDateTime.now().isAfter(getRoundStartTime().plusSeconds(getRoundDuration()));
return getRoundStartTime()!= null && Instant.now().isAfter(Instant.ofEpochMilli(getRoundStartTime()).plusSeconds(getRoundDuration()));
}

public void answerQuestion(Long answerId, QuestionRepository questionRepository){
public boolean answerQuestion(Long answerId){
if(currentRoundIsOver())
throw new IllegalStateException("You can't answer a question when the current round is over!");
if (isGameOver())
Expand All @@ -106,6 +107,7 @@ public void answerQuestion(Long answerId, QuestionRepository questionRepository)
setCorrectlyAnsweredQuestions(getCorrectlyAnsweredQuestions() + 1);
}
setCurrentQuestionAnswered(true);
return q.isCorrectAnswer(answerId);
}
public void setLanguage(String language){
if(!isLanguageSupported(language))
Expand All @@ -116,4 +118,8 @@ public void setLanguage(String language){
private boolean isLanguageSupported(String language) {
return language.equals("en") || language.equals("es");
}

public boolean shouldBeGameOver() {
return getActualRound() >= getRounds() && !isGameOver;
}
}
3 changes: 2 additions & 1 deletion api/src/main/java/lab/en2b/quizapi/game/GameController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import lab.en2b.quizapi.game.dtos.AnswerGameResponseDto;
import lab.en2b.quizapi.game.dtos.GameAnswerDto;
import lab.en2b.quizapi.game.dtos.GameResponseDto;
import lab.en2b.quizapi.questions.question.QuestionCategory;
Expand Down Expand Up @@ -57,7 +58,7 @@ public ResponseEntity<QuestionResponseDto> getCurrentQuestion(@PathVariable Long
@ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content),
})
@PostMapping("/{id}/answer")
public ResponseEntity<GameResponseDto> answerQuestion(@PathVariable Long id, @RequestBody GameAnswerDto dto, Authentication authentication){
public ResponseEntity<AnswerGameResponseDto> answerQuestion(@PathVariable Long id, @RequestBody GameAnswerDto dto, Authentication authentication){
return ResponseEntity.ok(gameService.answerQuestion(id, dto, authentication));
}

Expand Down
91 changes: 58 additions & 33 deletions api/src/main/java/lab/en2b/quizapi/game/GameService.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lab.en2b.quizapi.game;

import lab.en2b.quizapi.commons.user.UserService;
import lab.en2b.quizapi.game.dtos.AnswerGameResponseDto;
import lab.en2b.quizapi.game.dtos.GameAnswerDto;
import lab.en2b.quizapi.game.dtos.GameResponseDto;
import lab.en2b.quizapi.game.mappers.GameResponseDtoMapper;
Expand All @@ -14,10 +15,12 @@
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

@Service
@RequiredArgsConstructor
Expand All @@ -29,23 +32,38 @@ public class GameService {
private final QuestionRepository questionRepository;
private final QuestionResponseDtoMapper questionResponseDtoMapper;
private final StatisticsRepository statisticsRepository;

@Transactional
public GameResponseDto newGame(Authentication authentication) {
if (gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()).isPresent()){
return gameResponseDtoMapper.apply(gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()).get());
Optional<Game> game = gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId());

if (game.isPresent()){
if (game.get().shouldBeGameOver()){
game.get().setGameOver(true);
gameRepository.save(game.get());
saveStatistics(game.get());
}else{
return gameResponseDtoMapper.apply(game.get());
}
}
Game g = gameRepository.save(Game.builder()
return gameResponseDtoMapper.apply(gameRepository.save(Game.builder()
.user(userService.getUserByAuthentication(authentication))
.questions(new ArrayList<>())
.rounds(9)
.correctlyAnsweredQuestions(0)
.rounds(9L)
.actualRound(0L)
.correctlyAnsweredQuestions(0L)
.roundDuration(30)
.language("en")
.build());
return gameResponseDtoMapper.apply(g);
.build()));
}

public GameResponseDto startRound(Long id, Authentication authentication) {
Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow();
if (game.shouldBeGameOver()){
game.setGameOver(true);
gameRepository.save(game);
saveStatistics(game);
}
game.newRound(questionService.findRandomQuestion(game.getLanguage()));

return gameResponseDtoMapper.apply(gameRepository.save(game));
Expand All @@ -56,46 +74,53 @@ public QuestionResponseDto getCurrentQuestion(Long id, Authentication authentica
return questionResponseDtoMapper.apply(game.getCurrentQuestion());
}

public GameResponseDto answerQuestion(Long id, GameAnswerDto dto, Authentication authentication){
@Transactional
public AnswerGameResponseDto answerQuestion(Long id, GameAnswerDto dto, Authentication authentication){
Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow();
game.answerQuestion(dto.getAnswerId(), questionRepository);

System.out.println("Current round: " + game.getActualRound());
System.out.println("Total round: " + game.getRounds());
boolean wasCorrect = game.answerQuestion(dto.getAnswerId());

if (game.isLastRound()){
if (game.shouldBeGameOver()){
game.setGameOver(true);
gameRepository.save(game);
}
if (game.isGameOver()){
if (statisticsRepository.findByUserId(game.getUser().getId()).isPresent()){
Statistics statistics = statisticsRepository.findByUserId(game.getUser().getId()).get();
statistics.updateStatistics(Long.valueOf(game.getCorrectlyAnsweredQuestions()),
Long.valueOf(game.getQuestions().size()-game.getCorrectlyAnsweredQuestions()),
Long.valueOf(game.getRounds()));
statisticsRepository.save(statistics);
} else {
Statistics statistics = Statistics.builder()
.user(game.getUser())
.correct(Long.valueOf(game.getCorrectlyAnsweredQuestions()))
.wrong(Long.valueOf(game.getQuestions().size()-game.getCorrectlyAnsweredQuestions()))
.total(Long.valueOf(game.getRounds()))
.build();
statisticsRepository.save(statistics);
}
saveStatistics(game);
}

return gameResponseDtoMapper.apply(game);
return new AnswerGameResponseDto(wasCorrect);
}
private void saveStatistics(Game game){
if (statisticsRepository.findByUserId(game.getUser().getId()).isPresent()){
Statistics statistics = statisticsRepository.findByUserId(game.getUser().getId()).get();
statistics.updateStatistics(game.getCorrectlyAnsweredQuestions(),
game.getQuestions().size()-game.getCorrectlyAnsweredQuestions(),
game.getRounds());
statisticsRepository.save(statistics);
} else {
Statistics statistics = Statistics.builder()
.user(game.getUser())
.correct(game.getCorrectlyAnsweredQuestions())
.wrong(game.getQuestions().size()-game.getCorrectlyAnsweredQuestions())
.total(game.getRounds())
.build();
statisticsRepository.save(statistics);
}
}

public GameResponseDto changeLanguage(Long id, String language, Authentication authentication) {
Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow();
if(game.isGameOver()){
throw new IllegalStateException("Cannot change language after the game is over!");
}
game.setLanguage(language);
return gameResponseDtoMapper.apply(gameRepository.save(game));
}

public GameResponseDto getGameDetails(Long id, Authentication authentication) {
return gameResponseDtoMapper.apply(gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow());
Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow();
if (game.shouldBeGameOver()){
game.setGameOver(true);
gameRepository.save(game);
saveStatistics(game);
}
return gameResponseDtoMapper.apply(game);
}

public List<QuestionCategory> getQuestionCategories() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package lab.en2b.quizapi.game.dtos;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@Data
@NoArgsConstructor
public class AnswerGameResponseDto {
@JsonProperty("was_correct")
private boolean wasCorrect;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import lombok.EqualsAndHashCode;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;

@AllArgsConstructor
@Data
Expand All @@ -22,22 +23,23 @@ public class GameResponseDto {
private UserResponseDto user;

@Schema(description = "Total rounds for the game", example = "9")
private int rounds;
private Long rounds;

@Schema(description = "Actual round for the game", example = "3")
private int actualRound;
@JsonProperty("actual_round")
private Long actualRound;

@Schema(description = "Number of correct answered questions", example = "2")
@JsonProperty("correctly_answered_questions")
private int correctlyAnsweredQuestions;
private Long correctlyAnsweredQuestions;

@Schema(description = "Moment when the timer has started", example = "LocalDateTime.now()")
@JsonProperty("round_start_time")
private LocalDateTime roundStartTime;
private String roundStartTime;

@Schema(description = "Number of seconds for the player to answer the question", example = "20")
@JsonProperty("round_duration")
private int roundDuration;
private Integer roundDuration;

@Schema(description = "Whether the game has finished or not", example = "true")
private boolean isGameOver;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.function.Function;

@Service
Expand All @@ -21,7 +22,7 @@ public GameResponseDto apply(Game game) {
.correctlyAnsweredQuestions(game.getCorrectlyAnsweredQuestions())
.actualRound(game.getActualRound())
.roundDuration(game.getRoundDuration())
.roundStartTime(game.getRoundStartTime())
.roundStartTime(game.getRoundStartTime() != null? Instant.ofEpochMilli(game.getRoundStartTime()).toString(): null)
.isGameOver(game.isGameOver())
.build();
}
Expand Down
Loading

0 comments on commit 8cce1ec

Please sign in to comment.