Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

statistics improvement #313

Merged
merged 11 commits into from
Apr 28, 2024
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