diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0bf5d7eb..79b3d46d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,20 +31,9 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - e2e-tests: - needs: [ unit-tests ] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - run: npm --prefix webapp install - - run: npm --prefix webapp run build - # - run: npm --prefix webapp run test:e2e docker-push-api: runs-on: ubuntu-latest - needs: [ e2e-tests ] + needs: [ unit-tests ] steps: - uses: actions/checkout@v4 - name: Publish to Registry @@ -67,7 +56,7 @@ jobs: SSL_PASSWORD docker-push-prometheus: runs-on: ubuntu-latest - needs: [ e2e-tests ] + needs: [ unit-tests ] steps: - uses: actions/checkout@v4 - name: Publish to Registry @@ -80,7 +69,7 @@ jobs: workdir: api/monitoring/prometheus docker-push-grafana: runs-on: ubuntu-latest - needs: [ e2e-tests ] + needs: [ unit-tests ] steps: - uses: actions/checkout@v4 - name: Publish to Registry @@ -93,7 +82,7 @@ jobs: workdir: api/monitoring/grafana docker-push-reverse-proxy: runs-on: ubuntu-latest - needs: [ e2e-tests ] + needs: [ unit-tests ] steps: - uses: actions/checkout@v4 - name: Publish to Registry @@ -109,11 +98,9 @@ jobs: permissions: contents: read packages: write - needs: [ e2e-tests ] + needs: [ unit-tests ] steps: - - uses: actions/checkout@v4 - - name: Create .env file run: echo "REACT_APP_API_ENDPOINT=https://${{secrets.APP_DOMAIN}}:8443" > webapp/.env @@ -132,7 +119,7 @@ jobs: REACT_APP_API_ENDPOINT docker-push-question-generator: runs-on: ubuntu-latest - needs: [ e2e-tests ] + needs: [ unit-tests ] steps: - uses: actions/checkout@v4 - name: Publish to Registry @@ -175,4 +162,4 @@ jobs: echo "SSL_PASSWORD=${{ secrets.SSL_PASSWORD }}" >> .env docker compose --profile prod down docker compose --profile prod up -d --pull always - docker image prune -f + docker image prune -f \ No newline at end of file diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java index 05abfd5f..43947764 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java @@ -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; @@ -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; } diff --git a/api/src/main/java/lab/en2b/quizapi/questions/answer/AnswerCategory.java b/api/src/main/java/lab/en2b/quizapi/questions/answer/AnswerCategory.java index f74bd1b0..3e0f25b4 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/answer/AnswerCategory.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/answer/AnswerCategory.java @@ -1,6 +1,6 @@ package lab.en2b.quizapi.questions.answer; public enum AnswerCategory { - CAPITAL_CITY, COUNTRY, SONG, STADIUM, BALLON_DOR, GAMES_PUBLISHER, PAINTING, WTPOKEMON, GAMES_COUNTRY, GAMES_GENRE, BASKETBALL_VENUE, COUNTRY_FLAG + CAPITAL_CITY, COUNTRY, SONG, STADIUM, BALLON_DOR, GAMES_PUBLISHER, PAINTING, WTPOKEMON, GAMES_COUNTRY, GAMES_GENRE, BASKETBALL_VENUE, COUNTRY_FLAG, GAMES_RELEASE } diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionHelper.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionHelper.java index f5c20c61..9a42bf82 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionHelper.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionHelper.java @@ -14,9 +14,7 @@ public class QuestionHelper { private QuestionHelper(){} // To hide the implicit public constructor as this is static only - private static final int MAX_DISTRACTORS = 3; - public static List getDistractors(AnswerRepository answerRepository, Question question){ - return answerRepository.findDistractors(question.getAnswerCategory().toString(), question.getLanguage(), question.getCorrectAnswer().getText(), MAX_DISTRACTORS); + return answerRepository.findDistractors(question.getAnswerCategory().toString(), question.getLanguage(), question.getCorrectAnswer().getText(), 3); } } diff --git a/api/src/main/java/lab/en2b/quizapi/statistics/StatisticsRepository.java b/api/src/main/java/lab/en2b/quizapi/statistics/StatisticsRepository.java index c3ed50ee..67a9fe29 100644 --- a/api/src/main/java/lab/en2b/quizapi/statistics/StatisticsRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/statistics/StatisticsRepository.java @@ -4,10 +4,21 @@ import org.springframework.data.jpa.repository.Query; import java.util.Optional; +import java.util.List; public interface StatisticsRepository extends JpaRepository { @Query(value = "SELECT * FROM Statistics WHERE user_id = ?1", nativeQuery = true) Optional 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 findTopTen(); + } diff --git a/api/src/main/java/lab/en2b/quizapi/statistics/StatisticsService.java b/api/src/main/java/lab/en2b/quizapi/statistics/StatisticsService.java index a4839924..70fe8c16 100644 --- a/api/src/main/java/lab/en2b/quizapi/statistics/StatisticsService.java +++ b/api/src/main/java/lab/en2b/quizapi/statistics/StatisticsService.java @@ -47,10 +47,7 @@ public StatisticsResponseDto getStatisticsForUser(Authentication authentication) } public List getTopTenStatistics(){ - List all = new ArrayList<>(statisticsRepository.findAll()); - all.sort(Comparator.comparing(Statistics::getCorrectRate).reversed()); - List topTen = all.stream().limit(10).toList(); - return topTen.stream().map(statisticsResponseDtoMapper).collect(Collectors.toList()); + return statisticsRepository.findTopTen().stream().map(statisticsResponseDtoMapper).collect(Collectors.toList()); } } diff --git a/api/src/main/java/lab/en2b/quizapi/statistics/dtos/StatisticsResponseDto.java b/api/src/main/java/lab/en2b/quizapi/statistics/dtos/StatisticsResponseDto.java index f0503def..a6a4401e 100644 --- a/api/src/main/java/lab/en2b/quizapi/statistics/dtos/StatisticsResponseDto.java +++ b/api/src/main/java/lab/en2b/quizapi/statistics/dtos/StatisticsResponseDto.java @@ -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; diff --git a/api/src/main/java/lab/en2b/quizapi/statistics/mappers/StatisticsResponseDtoMapper.java b/api/src/main/java/lab/en2b/quizapi/statistics/mappers/StatisticsResponseDtoMapper.java index 0d6652eb..f509b7b2 100644 --- a/api/src/main/java/lab/en2b/quizapi/statistics/mappers/StatisticsResponseDtoMapper.java +++ b/api/src/main/java/lab/en2b/quizapi/statistics/mappers/StatisticsResponseDtoMapper.java @@ -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(); } diff --git a/api/src/test/java/lab/en2b/quizapi/statistics/StatisticsServiceTest.java b/api/src/test/java/lab/en2b/quizapi/statistics/StatisticsServiceTest.java index cdbfdd7d..7b5fd7d1 100644 --- a/api/src/test/java/lab/en2b/quizapi/statistics/StatisticsServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/statistics/StatisticsServiceTest.java @@ -86,7 +86,8 @@ public void setUp(){ .right(5L) .wrong(5L) .total(10L) - .correctRate(50L) + .percentage(50L) + .points(500L) .user(defaultUserResponseDto) .finishedGames(1L) .build(); @@ -105,7 +106,8 @@ public void setUp(){ .right(7L) .wrong(3L) .total(10L) - .correctRate(70L) + .points(700L) + .percentage(70L) .user(defaultUserResponseDto) .finishedGames(1L) .build(); @@ -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); @@ -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 result = statisticsService.getTopTenStatistics(); Assertions.assertEquals(List.of(defaultStatisticsResponseDto2,defaultStatisticsResponseDto1), result); } @@ -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 result = statisticsService.getTopTenStatistics(); Assertions.assertEquals(List.of(defaultStatisticsResponseDto1,defaultStatisticsResponseDto3), result); } diff --git a/docs/images/05_building_blocks-EN.png b/docs/images/05_building_blocks-EN.png deleted file mode 100644 index 0862b64e..00000000 Binary files a/docs/images/05_building_blocks-EN.png and /dev/null differ diff --git a/docs/images/08-Crosscutting-Concepts-Structure-EN.png b/docs/images/08-Crosscutting-Concepts-Structure-EN.png deleted file mode 100644 index 5598a0bb..00000000 Binary files a/docs/images/08-Crosscutting-Concepts-Structure-EN.png and /dev/null differ diff --git a/docs/images/ContainerDiagram.png b/docs/images/ContainerDiagram.png deleted file mode 100644 index 8c1c1488..00000000 Binary files a/docs/images/ContainerDiagram.png and /dev/null differ diff --git a/docs/images/Gatling_10000_users.png b/docs/images/Gatling_10000_users.png new file mode 100644 index 00000000..c1f1d22b Binary files /dev/null and b/docs/images/Gatling_10000_users.png differ diff --git a/docs/images/Gatling_1000_users.png b/docs/images/Gatling_1000_users.png new file mode 100644 index 00000000..69fa7f88 Binary files /dev/null and b/docs/images/Gatling_1000_users.png differ diff --git a/docs/images/Gatling_100_users.png b/docs/images/Gatling_100_users.png new file mode 100644 index 00000000..0bffaceb Binary files /dev/null and b/docs/images/Gatling_100_users.png differ diff --git a/docs/images/Gatling_1_user.png b/docs/images/Gatling_1_user.png new file mode 100644 index 00000000..6b340975 Binary files /dev/null and b/docs/images/Gatling_1_user.png differ diff --git a/docs/images/TechnicalContextDiagram.png b/docs/images/TechnicalContextDiagram.png deleted file mode 100644 index 6b635ca7..00000000 Binary files a/docs/images/TechnicalContextDiagram.png and /dev/null differ diff --git a/docs/images/actuator.png b/docs/images/actuator.png new file mode 100644 index 00000000..094be1ec Binary files /dev/null and b/docs/images/actuator.png differ diff --git a/docs/images/arc42-logo.png b/docs/images/arc42-logo.png deleted file mode 100644 index 88c76d06..00000000 Binary files a/docs/images/arc42-logo.png and /dev/null differ diff --git a/docs/images/codescene-general.png b/docs/images/codescene-general.png new file mode 100644 index 00000000..e2947572 Binary files /dev/null and b/docs/images/codescene-general.png differ diff --git a/docs/images/grafana.png b/docs/images/grafana.png new file mode 100644 index 00000000..a5f51cd8 Binary files /dev/null and b/docs/images/grafana.png differ diff --git a/docs/images/prometheus.png b/docs/images/prometheus.png new file mode 100644 index 00000000..47a53366 Binary files /dev/null and b/docs/images/prometheus.png differ diff --git a/docs/src/01_introduction_and_goals.adoc b/docs/src/01_introduction_and_goals.adoc index aa8975c0..27f30999 100644 --- a/docs/src/01_introduction_and_goals.adoc +++ b/docs/src/01_introduction_and_goals.adoc @@ -28,8 +28,8 @@ See the complete functional requirements in the xref:#section-annex[Annex] of th | Reliability | The system should be reliable in generating questions from Wikidata, ensuring that questions are accurate and diverse. The system shall handle user registrations, logins, and game data storage without errors. | Availability | The system shall be available 99% of the time a user tries to access it. | Maintainability | The system shall be designed and implemented in a way that facilitates easy maintenance and updates. -| Performance efficiency | The system shall deliver optimal performance, ensuring responsive interactions for users. The automatic generation of questions from Wikidata and the real-time gameplay shall be efficient. The system shall handle N concurrent users. -| Security | The system shall prioritize user data security. It shall implement robust authentication mechanisms for user registration and login. The API access points for user information and generated questions shall be secured with proper authorization. +| Performance efficiency | The system shall deliver optimal performance, ensuring responsive interactions for users. The automatic generation of questions from Wikidata and the real-time gameplay shall be efficient. The system shall handle 1000 concurrent users. +| Security | The system shall prioritize user data security. It shall implement robust authentication mechanisms for user registration and login. The API access points for user information and generated questions shall be secured with proper authorization. | Usability | The system shall provide a user-friendly interface, making it easy for users to register, log in, and play the game. The system learning time for a user should be less than 4 hours. | Compatibility | The system shall be compatible with various web browsers and devices, ensuring a seamless experience for users regardless of their choice of platform. It has to be well-optimized for different screen sizes and functionalities. |=== diff --git a/docs/src/09_architecture_decisions.adoc b/docs/src/09_architecture_decisions.adoc index 421aff80..3daf3d02 100644 --- a/docs/src/09_architecture_decisions.adoc +++ b/docs/src/09_architecture_decisions.adoc @@ -26,7 +26,13 @@ During the application development process, decisions had to be made as issues e |Architecture of the Question Generator |It has been decided to implement the QG in an independent module so it is only run once and it is not generating questions at real time, this way, we are not dependent on wikidata for having our application available. + +|Template method pattern +|We chose to apply our software design knowledge and generate the questions following a template method in Java. This allows us to create new questions easily and without duplicating too much code. + +|Use of JPA in the Question Generator +|We used JPA in favor of JDBC as when tables are related in a DB, JPA makes it easier to write into the DB. |=== -If needed, a more descriptive record can be seen link:https://github.com/Arquisoft/wiq_en2b/wiki[here]. +If needed, more details are given in the link:https://github.com/Arquisoft/wiq_en2b/wiki[wiki]. diff --git a/docs/src/10_quality_requirements.adoc b/docs/src/10_quality_requirements.adoc index f5f10a41..b9cb0680 100644 --- a/docs/src/10_quality_requirements.adoc +++ b/docs/src/10_quality_requirements.adoc @@ -5,33 +5,10 @@ ifndef::imagesdir[:imagesdir: ../images] === Quality Tree This quality tree is a high-level overview of the quality goals and requirements. The Quality tree uses "quality" as a root while the rest of the quality categories will be displayed as branches. -[plantuml,"Quality Tree",png] +[plantuml,"Quality Tree",png,align="center"] ---- -@startuml -title Quality attributes -agent Quality -agent Security -agent Reliability -agent Transferability -agent Usability -agent "Performance Efficiency" -agent Maintainability -agent Availability -agent Compatibility -agent "Functional Suitability" - -Quality --- Security -Quality --- Reliability -Quality --- Transferability -Quality --- Usability -Quality --- "Performance Efficiency" -Quality --- Maintainability -Quality --- Availability -Quality --- Compatibility -Quality --- "Functional Suitability" -@enduml +include::../diagrams/10_Quality_Tree.puml[] ---- - === Quality Scenarios To obtain a measurable system response to stimulus corresponding to the various quality branches outlined in the mindmap, we will use quality scenarios. Scenarios make quality requirements concrete and allow to more easily measure or decide whether they are fulfilled. @@ -42,17 +19,74 @@ To obtain a measurable system response to stimulus corresponding to the various | Functional suitability | Users shall be able to register, log in, play the quiz, and access historical data without encountering errors or glitches. | High, Medium | Reliability | The system shall reliably generate accurate and diverse questions from Wikidata. User registrations, logins, and game data storage shall be handled without errors. | High, Medium | Availability | The system shall be available 99% of the time when a user attempts to access it. | High, High -| Performance efficiency | The system shall deliver optimal performance, ensuring responsive interactions for users. It shall efficiently generate questions from Wikidata and handle real-time gameplay with up to N concurrent users. | High, High +| Performance efficiency | The system shall deliver optimal performance, ensuring responsive interactions for users. It shall efficiently generate questions from Wikidata and handle real-time gameplay with up to 1000 concurrent users. | High, High | Usability | The system shall provide a user-friendly interface, allowing users to register, log in, and play the game with a learning time of less than 4 hours. | High, Medium | Security | User data shall be securely handled. Robust authentication mechanisms shall be in place for user registration and login. API access points for user information and generated questions shall be secured with proper authorization. | Medium, High | Compatibility | The system shall be compatible with various web browsers and devices, providing a seamless experience for users regardless of their choice of platform. It shall be well-optimized for different screen sizes and functionalities. | High, Medium -| Transferability | The system shall allow for easy transfer of user data and game-related information through its APIs. | Medium, High | Testability | The unit tests shall have at least 80% coverage. | High, Medium +| Monitoring | The system shall have monitoring in place to track the performance and availability of the system. | High, Medium |=== ==== Change Scenarios [options="header",cols="1,3,1"] |=== |Quality attribute|Scenario|Priority -| Maintainability | The system shall be designed and implemented in a way that allows for easy maintenance and updates. | High, Medium +| Modifiability | The system shall be designed and implemented in a way that allows for easy maintenance and updates. | High, Medium | Maintainability | The code of the system shall be well-documented, and modular, allowing for efficient troubleshooting and modifications. | High, Medium -|=== \ No newline at end of file +|=== + +==== Implementation + +===== Performance efficiency +The tests were done with a 2 core and 4 GB of memory system. +This system's efficiency has been measured with Gatling. For the script that we used, a user already created, logged in and played a full game. After that, the user clicked to look the statistics. +The scripts were run a total of 4 times. One with 1 user, other with 100 users, another one with 1000 users and finally one with 10000 users. +The results of this scripts show that response times are reasonable up until 1000 users. Having 10000 users playing a game at the same time make a lot of failures. +Here are the results. + +**1 user:** + +image::Gatling_1_user.png[align="center", title="Gatling results with 1 user"] + +**100 users:** + +image::Gatling_100_users.png[align="center", title="Gatling results with 100 user"] + +**1000 users:** + +image::Gatling_1000_users.png[align="center", title="Gatling results with 1000 user"] + +**10000 users:** + +image::Gatling_10000_users.png[align="center", title="Gatling results with 10000 user"] + +===== Security +The system is secured using Spring Security. The user data is stored in a database and the passwords are hashed using BCrypt. The API access points are secured with proper authorization. HTTPS is used to encrypt the data in transit. + +The system is also protected against SQL injection via using JPA repositories and prepared statements. + +The system is also designed in such a way that prevents cheating, by limiting the options available for the user and doing all validation in the backend, such as checking if the answer is correct, preventing request forgery. + +===== Testability + +===== Monitoring +The system is monitored using Spring Boot Actuator and Prometheus. The monitoring data is visualized using Grafana. + +The actuator is deployed in https://kiwiq.run.place:8443/actuator/prometheus. + +image::actuator.png[align="center", title="Spring Boot Actuator"] + +The Prometheus server is deployed in http://20.199.84.5:9090. + +image::prometheus.png[align="center", title="Prometheus"] + +The Grafana is deployed in http://20.199.84.5:9091. The Grafana dashboard is available at the following link with user asw@uniovi.es and password aswgrafana. + +The dashboard used is: https://grafana.com/grafana/dashboards/6756-spring-boot-statistics/ +Make sure to put kiwiq.run.place:8443 as the Instance and WIQ API as the application. + +image::grafana.png[align="center", title="Graphana Spring Boot dashboard"] + +===== Maintainability +In our CodeScene analysis we find that our knowledge distribution is well-balanced as well as a nice code health, excepting one hotspot on a test that is not relevant. + +image::codescene-general.png[align="center", title="CodeScene general view"] diff --git a/docs/src/11_technical_risks.adoc b/docs/src/11_technical_risks.adoc index 4b7987dd..c48e770a 100644 --- a/docs/src/11_technical_risks.adoc +++ b/docs/src/11_technical_risks.adoc @@ -16,6 +16,52 @@ ifndef::imagesdir[:imagesdir: ../images] |Coordination and responsability problems |It is probably the first time involvement in developing a project from scratch, including decisions on architecture, design, and implementation, introduces various challenges. Misunderstandings regarding tasks and version control management errors can result in individuals inadvertently disrupting the work of others. Additionally, the necessity to make numerous decisions and reach agreements increases the likelihood of errors, potentially consuming significant time and effort. |To ensure effective collaboration and organization, adhere to the teachers' instructions concerning GitHub, including utilizing features such as issues and pull requests, and maintain a disciplined approach to your work. Furthermore, leverage the collective knowledge and suggestions of every team member, integrating them with your existing expertise. + +|Hardcoded url in prometheus.yml +|We do not have the expertise in prometheus to know how to properly set up the URL, so we decided to harcode it. +|Investigate the way to use a variable in the file and use it. + +|Hardcoded ip in graphana dashboard.yml +|As in the previous case, we do not have the expertise in graphana to know how to properly set up the IP, so we decided to harcode it. +|Investigate the way to use a variable in the file and use it. + +|Non optimal code (loadAnswers) +|When designing the application we did not take into account that two users may have the same question given to them at the same time. To solve a problem that would arise with that, we decided to just load the distractors of a question once and use the same distractors for all following games, this means that a question always has the same distractors which can get boring. +|Rewrite the code (which means altering the workflow) so the distractors are load per game and user, so everytime a questions is asked, the distractors are different. + +|Use of "supress warnings" +|We use Lombok to annotate our classes and make the code lighter, this means that getters and setters are generated by the annotations and not explicitly written in our code, due to this, SonarCloud thinks that our private attributes are not used (the getters and setters are public and used by outside classes, but SonarCloud does not detect that). +|Sadly, at this moment we do not think this can be avoided, so the mitigation is just to wait for a SonarCloud update that does detect this. + +|Questions tied to ID +|Questions are tied to the game entities by means of relational tables via id, which makes it difficult to update if they change. +|Creating a unique identifier for each question that can stay throughout versions, so we do not need the BD autogenerated ID to access them. + +|JWT handling could be improved +|Handling the session has been difficult and we came up with a solution that works, but can be improved. +|We should investigate how to improve session handling, as an example using React context + +|Usage of React +|React has proven to be a pain to work with, so we could change it for better options in case we shall continue the developing of the application. +|Changing to Vue.js should be considered. + +|Queries are hardcoded +|To write the questions faster and prevent losing time with possible bugs we hardcoded the queries to Wikidata, which makes the code less readable and maintainable. +|The queries can be written in an external file and then read in the code, possibly using a properties file. + +|QG code can be refactored for lighter classes +|As with all the projects in Software developing, as you code you find yourself repeating lines, this has happened in the Question templates, which makes it a bit tedious to change those files. +|More abstract classes should be included between the QuestionTemplate.java class and each implementation of it. + +|Use of relational DB for questions +|We started using a relational DB as we were more comfortable with it, but it caused its own troubles, as repeating many lines in the DB. +|Using a NoSQL DB as Neo4j for storing the questions could be better as it would be lighter and we would keep the relations that exist in Wikidata between questions. + +|e2e ran in local +|We had many troubles trying to set up the e2e in the actions, as they include setting up the db and filling it before this tests are started. +|We should investigate about this and learn how to set up the tests. + + |=== -In terms of technical debt, it's likely to be significant due to our lack of familiarity with the majority of the technologies involved for most of the team. Both Wikidata and React present considerable challenges, and we anticipate accumulating substantial technical debt in both areas. At present, our only strategy for mitigation is to search for potential solutions online. \ No newline at end of file +In terms of technical debt, it's likely to be significant due to our lack of familiarity with the majority of the technologies involved for most of the team. Both Wikidata and React present considerable challenges, and we anticipate accumulating substantial technical debt in both areas. At present, our only strategy for mitigation is to search for potential solutions online. diff --git a/docs/src/12_glossary.adoc b/docs/src/12_glossary.adoc index f9503e9f..f7e783a9 100644 --- a/docs/src/12_glossary.adoc +++ b/docs/src/12_glossary.adoc @@ -9,5 +9,5 @@ ifndef::imagesdir[:imagesdir: ../images] |Term |Definition |Distractor | Incorrect answer shown to the user alongside the correct one in each question |KiWiq | The name of our project -|Question Generator |A module of the application responsible for querying Wikidata, creating the questions and storing them in our DB +|Question Generator (QG) |A module of the application responsible for querying Wikidata, creating the questions and storing them in our DB |=== diff --git a/questiongenerator/src/main/java/Main.java b/questiongenerator/src/main/java/Main.java index d89a60bb..6ef99cb1 100644 --- a/questiongenerator/src/main/java/Main.java +++ b/questiongenerator/src/main/java/Main.java @@ -43,6 +43,11 @@ public static void main(String[] args) { new MusicAuthorQuestion("es"); } + if (GeneralRepositoryStorer.doesntExist(AnswerCategory.GAMES_RELEASE)) { + new VideogamesReleaseDate("en"); + new VideogamesReleaseDate("es"); + } + // IMAGES if(GeneralRepositoryStorer.doesntExist(AnswerCategory.STADIUM)) { @@ -65,6 +70,8 @@ public static void main(String[] args) { new CountryFlagQuestion("es"); } + + /* // VIDEOS not yet supported if(GeneralRepositoryStorer.doesntExist(AnswerCategory.SONG.toString())) { diff --git a/questiongenerator/src/main/java/model/AnswerCategory.java b/questiongenerator/src/main/java/model/AnswerCategory.java index c4b0f5a1..dae58ced 100644 --- a/questiongenerator/src/main/java/model/AnswerCategory.java +++ b/questiongenerator/src/main/java/model/AnswerCategory.java @@ -1,6 +1,6 @@ package model; public enum AnswerCategory { - CAPITAL_CITY, COUNTRY, SONG, STADIUM, BALLON_DOR, GAMES_PUBLISHER, PAINTING, WTPOKEMON, GAMES_COUNTRY, GAMES_GENRE, BASKETBALL_VENUE, COUNTRY_FLAG + CAPITAL_CITY, COUNTRY, SONG, STADIUM, BALLON_DOR, GAMES_PUBLISHER, PAINTING, WTPOKEMON, GAMES_COUNTRY, GAMES_GENRE, BASKETBALL_VENUE, COUNTRY_FLAG, GAMES_RELEASE } diff --git a/questiongenerator/src/main/java/templates/VideogamesReleaseDate.java b/questiongenerator/src/main/java/templates/VideogamesReleaseDate.java new file mode 100644 index 00000000..76741c90 --- /dev/null +++ b/questiongenerator/src/main/java/templates/VideogamesReleaseDate.java @@ -0,0 +1,94 @@ +package templates; + +import model.*; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +public class VideogamesReleaseDate extends QuestionTemplate { + List videoGameLabels; + + private static final String[] spanishStringsIni = {"¿Cuándo se publicó ", "¿En qué fecha fue lanzado ", "¿Cuándo fue lanzado ", "¿En qué fecha fue publicado "}; + private static final String[] englishStringsIni= {"When was ", "On what date was ", "When was ", "On what date was "}; + + private static final String[] spanishStringsFin = {"?", "?", "?", "?"}; + private static final String[] englishStringsFin = {" released?", " released?", " launched?", " launched?"}; + + public VideogamesReleaseDate(String langCode) { + super(langCode); + } + + @Override + public void setQuery() { + this.sparqlQuery = "SELECT ?gameLabel (MAX(?unitsSoldValue) as ?maxUnitsSold) (MIN(?publicationDate) as ?oldestPublicationDate)\n" + + "WHERE { " + + " ?game wdt:P31 wd:Q7889; " + + " wdt:P2664 ?unitsSoldValue. " + + " ?game wdt:P577 ?publicationDate. " + + " SERVICE wikibase:label { bd:serviceParam wikibase:language \"" + langCode + "\". } " + + "} " + + "GROUP BY ?game ?gameLabel " + + "ORDER BY DESC(?maxUnitsSold) " + + "LIMIT 150 "; + } + + @Override + public void processResults() { + videoGameLabels = new ArrayList<>(); + List questions = new ArrayList<>(); + List answers = new ArrayList<>(); + + for (int i = 0; i < results.length()-10; i++) { + + JSONObject result = results.getJSONObject(i); + + String videoGameLabel = ""; + String publishDateLabel = ""; + + try { + JSONObject videoGameLabelObject = result.getJSONObject("gameLabel"); + videoGameLabel = videoGameLabelObject.getString("value"); + + JSONObject publishDateLabelObject = result.getJSONObject("oldestPublicationDate"); + publishDateLabel = publishDateLabelObject.getString("value"); + + publishDateLabel = publishDateLabel.substring(0, 4); + + } catch (Exception e) { + continue; + } + + if (needToSkip(videoGameLabel, publishDateLabel)) + continue; + + Answer a = new Answer(publishDateLabel, AnswerCategory.GAMES_RELEASE, langCode); + answers.add(a); + + String questionString = ""; + + if (langCode.equals("es")) + questionString = spanishStringsIni[i%4] + videoGameLabel + spanishStringsFin[i%4]; + else + questionString = englishStringsIni[i%4] + videoGameLabel + englishStringsFin[i%4]; + + questions.add(new Question(a, questionString, QuestionCategory.VIDEOGAMES, QuestionType.TEXT)); + } + + repository.saveAll(new ArrayList<>(answers)); + repository.saveAll(new ArrayList<>(questions)); + } + + private boolean needToSkip(String videoGameLabel, String countryLabel) { + if (videoGameLabels.contains(videoGameLabel)) { + return true; + } + videoGameLabels.add(videoGameLabel); + + if (QGHelper.isEntityName(videoGameLabel) || QGHelper.isEntityName(countryLabel)) + return true; + + return false; + } +} + diff --git a/webapp/public/locales/en/translation.json b/webapp/public/locales/en/translation.json index 9196de2f..3bd09ec6 100644 --- a/webapp/public/locales/en/translation.json +++ b/webapp/public/locales/en/translation.json @@ -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", diff --git a/webapp/public/locales/es/translation.json b/webapp/public/locales/es/translation.json index c0fa406e..0b17fb8b 100644 --- a/webapp/public/locales/es/translation.json +++ b/webapp/public/locales/es/translation.json @@ -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", diff --git a/webapp/src/components/statistics/UserStatistics.jsx b/webapp/src/components/statistics/UserStatistics.jsx index a87457b4..b4922379 100644 --- a/webapp/src/components/statistics/UserStatistics.jsx +++ b/webapp/src/components/statistics/UserStatistics.jsx @@ -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 ( - - {retrievedData ? ( - - - - {t("common.statistics.personal")} - - - - - - {t("statistics.rightAnswers")} - - - {t("statistics.texts.personalRight", { right: userData.raw[0].value })} - - - - - {t("statistics.wrongAnswers")} - - - {t("statistics.texts.personalWrong", { wrong: userData.raw[1].value })} - - - - - {t("statistics.percentage")} - - - {t("statistics.texts.personalRate", { rate: userData.rate })} - - - - - - - - - - - - - - ) : ( - - )} - - ); -} +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 ( + + {retrievedData ? ( + + + + {t("common.statistics.personal")} + + + + + + {t("statistics.rightAnswers")} + + + {t("statistics.texts.personalRight", { right: userData.raw[0].value })} + + + + + {t("statistics.wrongAnswers")} + + + {t("statistics.texts.personalWrong", { wrong: userData.raw[1].value })} + + + + + {t("statistics.percentage")} + + + {t("statistics.texts.personalRate", { rate: userData.rate })} + + + + + + + + + + + + + + ) : ( + + )} + + ); +} diff --git a/webapp/src/pages/About.jsx b/webapp/src/pages/About.jsx index 261fe654..9f2a8a74 100644 --- a/webapp/src/pages/About.jsx +++ b/webapp/src/pages/About.jsx @@ -32,7 +32,7 @@ export default function About() { - Gonzalo Alonso Fernández + Gonzalo Alonso Fernández diff --git a/webapp/src/pages/Statistics.jsx b/webapp/src/pages/Statistics.jsx index 92ace61f..74bacf1d 100644 --- a/webapp/src/pages/Statistics.jsx +++ b/webapp/src/pages/Statistics.jsx @@ -1,15 +1,29 @@ -import { Box, Center, Heading, Stack, Table, Tbody, Text, - Td, Th, Thead, Tr, CircularProgress} from "@chakra-ui/react"; +import { + Box, + Center, + Heading, + Stack, + Table, + Tbody, + Text, + CircularProgress, + AccordionItem, + Accordion, + AccordionButton, + AccordionIcon, + AccordionPanel, + Flex, ListItem, ListIcon, UnorderedList +} from "@chakra-ui/react"; import React, {useEffect, useState} from "react"; import { useTranslation } from "react-i18next"; import GoBack from "components/GoBack"; import AuthManager from "components/auth/AuthManager"; import { HttpStatusCode } from "axios"; import ErrorMessageAlert from "components/ErrorMessageAlert"; -import UserStatistics from "components/statistics/UserStatistics"; import { FaChartBar } from 'react-icons/fa'; import MenuButton from '../components/menu/MenuButton'; import LateralMenu from '../components/menu/LateralMenu'; +import {MdCheckCircle, MdClear, MdPercent} from "react-icons/md"; export default function Statistics() { const { t, i18n } = useTranslation(); @@ -46,14 +60,37 @@ export default function Statistics() { const formatTopTen = () => { return topTen.map((element, counter) => { - return - {counter + 1} - {element.user.username} - {element.right} - {element.wrong} - {element.total} - {element.correct_rate}% - + return + + + + {counter + 1} + {element.user.username} + {element.points} {element.user.username !== 'Dario G. Mori'? '🥝': '🍌' } + + + + + + + + + {t("statistics.texts.personalRight", {right: element.right})} + + + + {t("statistics.texts.personalWrong", {wrong: element.wrong})} + + + + {t("statistics.texts.personalRate", {rate: element.percentage})} + + + + + + + }); } useEffect(() => { @@ -71,16 +108,17 @@ export default function Statistics() { return (
+ justifyContent={"center"} alignItems={"center"} bgImage={'/background.svg'} > setIsMenuOpen(true)}/> setIsMenuOpen(false)} changeLanguage={changeLanguage} isDashboard={false}/> - + + t={t} errorWhere={"error.statistics.top"} /> {t("common.statistics.title")} - + {retrievedData ? @@ -89,26 +127,17 @@ export default function Statistics() { { topTen.length === 0 ? {t("statistics.empty")} : - - - - - - - - - - - +
{t("statistics.position")}{t("statistics.username")}{t("statistics.rightAnswers")}{t("statistics.wrongAnswers")}{t("statistics.totalAnswers")}{t("statistics.percentage")}
- {formatTopTen()} + + {formatTopTen()} +
}
: } -
diff --git a/webapp/src/tests/Statistics.test.js b/webapp/src/tests/Statistics.test.js index 5f0e5134..2c3c6d76 100644 --- a/webapp/src/tests/Statistics.test.js +++ b/webapp/src/tests/Statistics.test.js @@ -34,11 +34,6 @@ describe("Statistics", () => { expect(screen.getByTestId("leaderboard-spinner")).toBeEnabled(); }); - test("the user statistics component is rendered", () => { - render(); - expect(screen.getByTestId("user-statistics")).toBeEnabled(); - }) - describe("a petition is made requesting the top ten", () => { const authManager = new AuthManager(); let mockAxios;