Skip to content

Commit

Permalink
Merge pull request #313 from Arquisoft/feat/statistics-top
Browse files Browse the repository at this point in the history
statistics improvement
  • Loading branch information
GOLASOOO authored Apr 28, 2024
2 parents 77fc5b3 + 7132d41 commit 755d5f4
Show file tree
Hide file tree
Showing 11 changed files with 201 additions and 157 deletions.
3 changes: 3 additions & 0 deletions api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
Expand All @@ -19,10 +20,12 @@ public class RegisterDto {
private String email;
@NonNull
@NotBlank
@Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters")
@Schema(description = "Username used for registering", example = "example user" )
private String username;
@NonNull
@NotBlank
@Size(min = 6, max = 20, message = "Password must be between 6 and 20 characters")
@Schema(description = "Password used for registering", example = "password" )
private String password;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,21 @@
import org.springframework.data.jpa.repository.Query;

import java.util.Optional;
import java.util.List;

public interface StatisticsRepository extends JpaRepository<Statistics, Long> {

@Query(value = "SELECT * FROM Statistics WHERE user_id = ?1", nativeQuery = true)
Optional<Statistics> findByUserId(Long userId);

//Query that gets the top ten ordered by statistics -> statistics.getCorrectRate() * statistics.getTotal() / 9L
@Query(value = "SELECT *, \n" +
" CASE \n" +
" WHEN total = 0 THEN 0 \n" +
" ELSE (correct * 100.0 / NULLIF(total, 0)) * total \n" +
" END AS points \n" +
"FROM Statistics \n" +
"ORDER BY points DESC LIMIT 10 ", nativeQuery = true)
List<Statistics> findTopTen();

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,7 @@ public StatisticsResponseDto getStatisticsForUser(Authentication authentication)
}

public List<StatisticsResponseDto> getTopTenStatistics(){
List<Statistics> all = new ArrayList<>(statisticsRepository.findAll());
all.sort(Comparator.comparing(Statistics::getCorrectRate).reversed());
List<Statistics> topTen = all.stream().limit(10).toList();
return topTen.stream().map(statisticsResponseDtoMapper).collect(Collectors.toList());
return statisticsRepository.findTopTen().stream().map(statisticsResponseDtoMapper).collect(Collectors.toList());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ public class StatisticsResponseDto {
private Long wrong;
private Long total;
private UserResponseDto user;
@JsonProperty("correct_rate")
private Long correctRate;
@JsonProperty("percentage")
private Long percentage;
@JsonProperty("points")
private Long points;
@JsonProperty("finished_games")
private Long finishedGames;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ public StatisticsResponseDto apply(Statistics statistics) {
.right(statistics.getCorrect())
.wrong(statistics.getWrong())
.total(statistics.getTotal())
.percentage(statistics.getCorrectRate())
.user(userResponseDtoMapper.apply(statistics.getUser()))
.correctRate(statistics.getCorrectRate())
.points(statistics.getCorrectRate() * statistics.getTotal() )
.finishedGames(statistics.getFinishedGames())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ public void setUp(){
.right(5L)
.wrong(5L)
.total(10L)
.correctRate(50L)
.percentage(50L)
.points(500L)
.user(defaultUserResponseDto)
.finishedGames(1L)
.build();
Expand All @@ -105,7 +106,8 @@ public void setUp(){
.right(7L)
.wrong(3L)
.total(10L)
.correctRate(70L)
.points(700L)
.percentage(70L)
.user(defaultUserResponseDto)
.finishedGames(1L)
.build();
Expand All @@ -131,7 +133,8 @@ public void getStatisticsForUserTestEmpty(){
.right(0L)
.wrong(0L)
.total(0L)
.correctRate(0L)
.points(0L)
.percentage(0L)
.finishedGames(0L)
.user(defaultUserResponseDto).build()
, result);
Expand All @@ -152,7 +155,7 @@ public void getCorrectRateTotalZero(){

@Test
public void getTopTenStatisticsTestWhenThereAreNotTen(){
when(statisticsRepository.findAll()).thenReturn(List.of(defaultStatistics2, defaultStatistics1));
when(statisticsRepository.findTopTen()).thenReturn(List.of(defaultStatistics2, defaultStatistics1));
List<StatisticsResponseDto> result = statisticsService.getTopTenStatistics();
Assertions.assertEquals(List.of(defaultStatisticsResponseDto2,defaultStatisticsResponseDto1), result);
}
Expand All @@ -172,11 +175,12 @@ public void getTopTenStatisticsTestWhenThereAreNotTenAndAreEqual(){
.right(5L)
.wrong(5L)
.total(10L)
.correctRate(50L)
.points(500L)
.percentage(50L)
.user(defaultUserResponseDto)
.finishedGames(1L)
.build();
when(statisticsRepository.findAll()).thenReturn(List.of(defaultStatistics1, defaultStatistics3));
when(statisticsRepository.findTopTen()).thenReturn(List.of(defaultStatistics1, defaultStatistics3));
List<StatisticsResponseDto> result = statisticsService.getTopTenStatistics();
Assertions.assertEquals(List.of(defaultStatisticsResponseDto1,defaultStatisticsResponseDto3), result);
}
Expand Down
1 change: 1 addition & 0 deletions webapp/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"wrongAnswers": "Wrong",
"totalAnswers": "Total",
"percentage": "Rate",
"points": "Points",
"empty": "Currently, there are no statistics saved",
"texts": {
"personalRight": "{{right, number}} correct answers",
Expand Down
5 changes: 3 additions & 2 deletions webapp/public/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@
"statistics": {
"position": "Posición",
"username": "Nombre",
"rightAnswers": "Respuestas correctas",
"wrongAnswers": "Respuestas falladas",
"rightAnswers": "Correctas",
"wrongAnswers": "Falladas",
"totalAnswers": "En total",
"percentage": "Acierto",
"points": "Puntos",
"empty": "Actualmente, no hay estadísticas guardadas",
"texts": {
"personalRight": "{{right, number}} respuestas correctas",
Expand Down
218 changes: 109 additions & 109 deletions webapp/src/components/statistics/UserStatistics.jsx
Original file line number Diff line number Diff line change
@@ -1,109 +1,109 @@
import { Box, Flex, Heading, Stack, Text, CircularProgress } from "@chakra-ui/react";
import { HttpStatusCode } from "axios";
import ErrorMessageAlert from "components/ErrorMessageAlert";
import AuthManager from "components/auth/AuthManager";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Cell, Pie, PieChart } from "recharts";

export default function UserStatistics() {
const { t } = useTranslation();
const [userData, setUserData] = useState(null);
const [retrievedData, setRetrievedData] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);

const getData = useCallback(async () => {
try {
const request = await new AuthManager().getAxiosInstance().get(process.env.REACT_APP_API_ENDPOINT + "/statistics/personal");
if (request.status === HttpStatusCode.Ok) {
setUserData({
raw: [
{
name: t("statistics.texts.personalRight"),
value: request.data.right,
},
{
name: t("statistics.texts.personalWrong"),
value: request.data.wrong,
},
],
rate: request.data.correct_rate
});
setRetrievedData(true);
} else {
throw request;
}
} catch (error) {
let errorType;
switch (error.response ? error.response.status : null) {
case 400:
errorType = { type: t("error.validation.type"), message: t("error.validation.message") };
break;
case 404:
errorType = { type: t("error.notFound.type"), message: t("error.notFound.message") };
break;
default:
errorType = { type: t("error.unknown.type"), message: t("error.unknown.message") };
break;
}
setErrorMessage(errorType);
}
}, [t, setErrorMessage, setRetrievedData, setUserData]);

useEffect(() => {
if (!retrievedData) {
getData();
}
}, [retrievedData, getData]);

return (
<Flex w={"100%"} minH={"10%"} data-testid={"user-statistics"} flexDirection={"column"}>
{retrievedData ? (
<Stack align={"center"} width="100%">
<ErrorMessageAlert errorMessage={errorMessage} t={t} errorWhere={"error.statistics.personal"} />
<Heading as="h2" fontSize={"1.75em"}>
{t("common.statistics.personal")}
</Heading>
<Stack width="100%" direction={"row"} justifyContent={"space-between"} alignItems={"flex-start"}>
<Stack paddingLeft={"6rem"}>
<Box>
<Heading mt={2} mb={2} display={"flex"} justifyContent={"center"} alignItems={"center"} as="h3" fontSize={"1.25em"}>
{t("statistics.rightAnswers")}
</Heading>
<Text display={"flex"} justifyContent={"center"} alignItems={"center"}>
{t("statistics.texts.personalRight", { right: userData.raw[0].value })}
</Text>
</Box>
<Box>
<Heading mt={2} mb={2} display={"flex"} justifyContent={"center"} alignItems={"center"} as="h3" fontSize={"1.25em"}>
{t("statistics.wrongAnswers")}
</Heading>
<Text display={"flex"} justifyContent={"center"} alignItems={"center"}>
{t("statistics.texts.personalWrong", { wrong: userData.raw[1].value })}
</Text>
</Box>
<Box>
<Heading mt={2} mb={2} display={"flex"} justifyContent={"center"} alignItems={"center"} as="h3" fontSize={"1.25em"}>
{t("statistics.percentage")}
</Heading>
<Text display={"flex"} justifyContent={"center"} alignItems={"center"}>
{t("statistics.texts.personalRate", { rate: userData.rate })}
</Text>
</Box>
</Stack>
<Box width={"50%"} height={"50%"} paddingLeft={"2rem"}>
<PieChart width={200} height={200} data-testid={"chart"}>
<Pie data={userData.raw} dataKey="value" innerRadius={48} outerRadius={65} fill="#82ca9d" paddingAngle={5}>
<Cell key={"cell-right"} fill={"green"} />
<Cell key={"cell-right"} fill={"red"} />
</Pie>
</PieChart>
</Box>
</Stack>
</Stack>
) : (
<CircularProgress isIndeterminate color="green" data-testid={"user-statistics-spinner"} />
)}
</Flex>
);
}
import { Box, Flex, Heading, Stack, Text, CircularProgress } from "@chakra-ui/react";
import { HttpStatusCode } from "axios";
import ErrorMessageAlert from "components/ErrorMessageAlert";
import AuthManager from "components/auth/AuthManager";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Cell, Pie, PieChart } from "recharts";

export default function UserStatistics() {
const { t } = useTranslation();
const [userData, setUserData] = useState(null);
const [retrievedData, setRetrievedData] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);

const getData = useCallback(async () => {
try {
const request = await new AuthManager().getAxiosInstance().get(process.env.REACT_APP_API_ENDPOINT + "/statistics/personal");
if (request.status === HttpStatusCode.Ok) {
setUserData({
raw: [
{
name: t("statistics.texts.personalRight"),
value: request.data.right,
},
{
name: t("statistics.texts.personalWrong"),
value: request.data.wrong,
},
],
rate: request.data.percentage
});
setRetrievedData(true);
} else {
throw request;
}
} catch (error) {
let errorType;
switch (error.response ? error.response.status : null) {
case 400:
errorType = { type: t("error.validation.type"), message: t("error.validation.message") };
break;
case 404:
errorType = { type: t("error.notFound.type"), message: t("error.notFound.message") };
break;
default:
errorType = { type: t("error.unknown.type"), message: t("error.unknown.message") };
break;
}
setErrorMessage(errorType);
}
}, [t, setErrorMessage, setRetrievedData, setUserData]);

useEffect(() => {
if (!retrievedData) {
getData();
}
}, [retrievedData, getData]);

return (
<Flex w={"100%"} minH={"10%"} data-testid={"user-statistics"} flexDirection={"column"}>
{retrievedData ? (
<Stack align={"center"} width="100%">
<ErrorMessageAlert errorMessage={errorMessage} t={t} errorWhere={"error.statistics.personal"} />
<Heading as="h2" fontSize={"1.75em"}>
{t("common.statistics.personal")}
</Heading>
<Stack width="100%" direction={"row"} justifyContent={"space-between"} alignItems={"flex-start"}>
<Stack paddingLeft={"6rem"}>
<Box>
<Heading mt={2} mb={2} display={"flex"} justifyContent={"center"} alignItems={"center"} as="h3" fontSize={"1.25em"}>
{t("statistics.rightAnswers")}
</Heading>
<Text display={"flex"} justifyContent={"center"} alignItems={"center"}>
{t("statistics.texts.personalRight", { right: userData.raw[0].value })}
</Text>
</Box>
<Box>
<Heading mt={2} mb={2} display={"flex"} justifyContent={"center"} alignItems={"center"} as="h3" fontSize={"1.25em"}>
{t("statistics.wrongAnswers")}
</Heading>
<Text display={"flex"} justifyContent={"center"} alignItems={"center"}>
{t("statistics.texts.personalWrong", { wrong: userData.raw[1].value })}
</Text>
</Box>
<Box>
<Heading mt={2} mb={2} display={"flex"} justifyContent={"center"} alignItems={"center"} as="h3" fontSize={"1.25em"}>
{t("statistics.percentage")}
</Heading>
<Text display={"flex"} justifyContent={"center"} alignItems={"center"}>
{t("statistics.texts.personalRate", { rate: userData.rate })}
</Text>
</Box>
</Stack>
<Box width={"50%"} height={"50%"} paddingLeft={"2rem"}>
<PieChart width={200} height={200} data-testid={"chart"}>
<Pie data={userData.raw} dataKey="value" innerRadius={48} outerRadius={65} fill="#82ca9d" paddingAngle={5}>
<Cell key={"cell-right"} fill={"green"} />
<Cell key={"cell-right"} fill={"red"} />
</Pie>
</PieChart>
</Box>
</Stack>
</Stack>
) : (
<CircularProgress isIndeterminate color="green" data-testid={"user-statistics-spinner"} />
)}
</Flex>
);
}
Loading

0 comments on commit 755d5f4

Please sign in to comment.