├── .env ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docker-compose-build.yml ├── docker-compose-mongo.yml ├── docker-compose-quboo.yml ├── docker-compose-sonar.yml ├── package-lock.json ├── sonar-connector ├── Dockerfile ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── thepracticaldeveloper │ │ │ └── devgame │ │ │ ├── Application.java │ │ │ ├── app │ │ │ └── SecurityConfiguration.java │ │ │ ├── modules │ │ │ ├── badges │ │ │ │ ├── calculators │ │ │ │ │ ├── BadgeCalculator.java │ │ │ │ │ ├── EarlyBirdBadgeCalculator.java │ │ │ │ │ ├── UnitTesterBronzeBadgeCalculator.java │ │ │ │ │ ├── UnitTesterGoldBadgeCalculator.java │ │ │ │ │ ├── UnitTesterPaperBadgeCalculator.java │ │ │ │ │ └── UnitTesterSilverBadgeCalculator.java │ │ │ │ └── domain │ │ │ │ │ ├── BadgeDetails.java │ │ │ │ │ └── SonarBadge.java │ │ │ ├── code │ │ │ │ ├── CodeController.java │ │ │ │ ├── CodeDetails.java │ │ │ │ ├── CodeParser.java │ │ │ │ ├── CodeService.java │ │ │ │ └── InvalidCodeException.java │ │ │ ├── configuration │ │ │ │ ├── controller │ │ │ │ │ └── SonarServerConfigurationController.java │ │ │ │ ├── dao │ │ │ │ │ ├── SonarServerConfigurationDao.java │ │ │ │ │ └── SonarServerConfigurationDaoImpl.java │ │ │ │ ├── domain │ │ │ │ │ ├── SonarServerConfiguration.java │ │ │ │ │ └── sonar │ │ │ │ │ │ ├── SonarAuthenticationResponse.java │ │ │ │ │ │ └── SonarServerStatus.java │ │ │ │ └── service │ │ │ │ │ ├── SonarServerConfigurationService.java │ │ │ │ │ └── SonarServerConfigurationServiceImpl.java │ │ │ ├── retriever │ │ │ │ ├── controller │ │ │ │ │ └── RetrieverController.java │ │ │ │ └── service │ │ │ │ │ └── SonarDataRetriever.java │ │ │ ├── sonarapi │ │ │ │ └── resultbeans │ │ │ │ │ ├── Component.java │ │ │ │ │ ├── Issue.java │ │ │ │ │ ├── Issues.java │ │ │ │ │ ├── Paging.java │ │ │ │ │ ├── TextRange.java │ │ │ │ │ ├── User.java │ │ │ │ │ └── Users.java │ │ │ ├── stats │ │ │ │ ├── controller │ │ │ │ │ └── SonarStatsController.java │ │ │ │ ├── domain │ │ │ │ │ ├── BadgeCard.java │ │ │ │ │ ├── ScoreCard.java │ │ │ │ │ ├── SeverityType.java │ │ │ │ │ ├── SonarStats.java │ │ │ │ │ ├── SonarStatsRow.java │ │ │ │ │ └── SonarStatsRowBuilder.java │ │ │ │ ├── repository │ │ │ │ │ ├── BadgeCardMongoRepository.java │ │ │ │ │ └── ScoreCardMongoRepository.java │ │ │ │ └── service │ │ │ │ │ ├── BadgeService.java │ │ │ │ │ ├── BadgeServiceImpl.java │ │ │ │ │ ├── ScoreCardService.java │ │ │ │ │ ├── ScoreCardServiceImpl.java │ │ │ │ │ ├── SonarStatsService.java │ │ │ │ │ └── SonarStatsServiceImpl.java │ │ │ └── users │ │ │ │ ├── controller │ │ │ │ ├── TeamController.java │ │ │ │ └── UserController.java │ │ │ │ ├── dao │ │ │ │ ├── TeamMongoRepository.java │ │ │ │ └── UserMongoRepository.java │ │ │ │ ├── domain │ │ │ │ ├── Team.java │ │ │ │ └── User.java │ │ │ │ ├── dto │ │ │ │ ├── CreateTeamDTO.java │ │ │ │ ├── MessageResponseDTO.java │ │ │ │ ├── TeamsAndUsersDTO.java │ │ │ │ └── UserDTO.java │ │ │ │ └── service │ │ │ │ ├── SonarUsersRetriever.java │ │ │ │ ├── UserService.java │ │ │ │ ├── UserServiceImpl.java │ │ │ │ └── YamlUserCreatorService.java │ │ │ └── util │ │ │ ├── ApiHttpUtils.java │ │ │ ├── IssueDateFormatter.java │ │ │ └── Utils.java │ └── resources │ │ ├── config │ │ └── application.yml │ │ └── data │ │ └── users.yml │ └── test │ └── java │ └── com │ └── thepracticaldeveloper │ └── devgame │ ├── app │ └── ApplicationTest.java │ ├── modules │ └── configuration │ │ └── service │ │ └── SonarServerConfigurationServiceTest.java │ └── util │ ├── ApiHttpUtilsTest.java │ └── UtilsTest.java └── web-client ├── .dockerignore ├── .editorconfig ├── .gitignore ├── Dockerfile ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── nginx.conf ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.spec.ts │ ├── app-routing.module.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── common │ │ ├── Badge.ts │ │ ├── MessageResponse.ts │ │ └── StatsRow.ts │ ├── footer │ │ ├── Code.ts │ │ ├── footer.component.html │ │ ├── footer.component.ts │ │ └── footer.service.ts │ ├── members │ │ ├── User.ts │ │ ├── member.service.ts │ │ ├── members.component.html │ │ └── members.component.ts │ ├── retriever │ │ └── retriever.service.ts │ ├── settings │ │ ├── Settings.ts │ │ ├── organizer.component.html │ │ ├── organizer.component.ts │ │ ├── server-url.component.html │ │ ├── server-url.component.ts │ │ ├── server-url.service.ts │ │ ├── settings.component.html │ │ └── settings.component.ts │ └── teams │ │ ├── Team.ts │ │ ├── mock-teams.ts │ │ ├── teams.component.html │ │ ├── teams.component.ts │ │ └── teams.service.ts ├── assets │ ├── .gitkeep │ ├── css │ │ └── bootstrap.css │ ├── img │ │ ├── BMC-btn-logo.svg │ │ ├── become_a_patron_button.png │ │ ├── monkey_logo.gif │ │ ├── quboo_gold.png │ │ └── quboo_logo_orange_250.png │ └── js │ │ ├── bootstrap.min.js │ │ ├── bootstrapv3.min.js │ │ └── jquery.min.js ├── browserslist ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.json └── tslint.json /.env: -------------------------------------------------------------------------------- 1 | BACKEND_VERSION=1.2.0 2 | UI_VERSION=1.0.4 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mechero 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target/ 2 | 3 | # Eclipse configuration files 4 | **/.classpath 5 | **/.settings/ 6 | **/.project 7 | **/.springBeans 8 | 9 | # IntelliJ configuration files 10 | **/.idea/ 11 | **/*.iml 12 | 13 | # Angular files 14 | **/node_modules 15 | /frontend/app/**/*.js 16 | /frontend/app/**/*.js.map -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk11 4 | 5 | stages: 6 | - tests-sonar 7 | - docker-build 8 | - deploy 9 | 10 | jobs: 11 | include: 12 | - stage: tests-sonar 13 | addons: 14 | sonarcloud: 15 | organization: "thepracticaldeveloper" 16 | before_script: cd sonar-connector 17 | script: 18 | - mvn clean install sonar:sonar 19 | - stage: docker-build 20 | sudo: required 21 | services: docker 22 | script: docker-compose -f docker-compose-build.yml build 23 | - stage: deploy 24 | script: echo "Deploying... (noop)" 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quboo - Code Quality Game [![Build Status](https://travis-ci.org/mechero/code-quality-game.svg?branch=master)](https://travis-ci.org/mechero/code-quality-game) 2 | 3 | A simple web application to improve **code quality** using Gamification with SonarQube. 4 | 5 | Go to the [Quboo Cloud](https://quboo.io) website and create a free account if you prefer to skip installation and use Quboo as a service. 6 | 7 | ## Introduction 8 | 9 | This is a very simple web page that shows a ranking of developers by how much technical debt they are fixing on SonarQube. It encourages a 'friendly competition' and tries to solve one of the main problems of fixing legacy code: **it's boring**. 10 | 11 | You can find all the information about this game and the background of this project on [The Official Quboo website](https://docs.quboo.io) 12 | 13 | ## Installation 14 | 15 | Simple way: go to [Standalone Installation](https://docs.quboo.io/docs/standalone/) on the Quboo website and follow the instructions. 16 | 17 | ## Contribute 18 | 19 | In case you want to become a code contributor, you can submit your Pull Request. The best option for new functionalities is contacting me in advance to see if I can integrate it. If it gets accepted, I'll include you in the list of contributors and you'll get a code. 20 | 21 | ## Building the project yourself 22 | 23 | **From here onwards, this README file describes the game code. If you just want to install it and use it, you better continue reading the instructions on the [Quboo website](https://docs.quboo.io/docs/standalone/).** 24 | 25 | ### Requirements 26 | 27 | * Java 11+ 28 | * Docker (recommended) or Maven/JDK11/Node.js 29 | * A SonarQube (or SonarCloud) server to connect to. The application is tested up to version 7.1 30 | * *IMPORTANT*: If you use Java 10, you may need to update the SonarJava sensor so it's able to read the coverage reports. 31 | * You may need to create a user in SonarQube for this application to connect to (if your server doesn't provide anonymous access). See *Connection to the server* for more details. 32 | * This repository includes a `docker-compose-sonar.yml` file that you can use to deploy a SonarQube instance on Docker. 33 | * By default, this project connects to its own repository on SonarCloud. 34 | 35 | ### Building and Running the app 36 | 37 | The easiest way to build and run the app is via Docker Compose: 38 | 39 | ``` 40 | $ docker-compose -f docker-compose-quboo.yml up 41 | ``` 42 | 43 | **Note that you have to configure the game settings via environment variables, as described in the section Configuration on the [Quboo website](https://quboo.tpd.io).** 44 | 45 | The command above will build the application inside docker containers the first time you run it, and will generate the corresponding Docker images. 46 | 47 | After everything is built and the docker images are up and running, you can access the app by navigating with your browser to `http://localhost:1827` 48 | 49 | You can also build the components and run them locally. This application has two parts: 50 | 51 | - The backend side is a Spring Boot application. Normally, you need to create a distributable file (`.war` in this case) and run it using Java, but you can also run it directly from the `sonar-connector` folder by executing `mvn spring-boot:run`. Check the configuration section before running it, since you need to change the SonarQube connection settings. 52 | - The frontend side is built with Angular. You can get it up and running by executing first `npm install` (to install the required dependencies) and then `npm start` (to run it in development mode) from the `web-client` folder. 53 | 54 | If you don't have a SonarQube server available and want to test locally, there is a `docker-compose-sonar.yml` in the repository as well. If you execute `docker-compose -f docker-compose-sonar.yml up` within the root folder, you should get an instance running at `http://localhost:9000` (or the Docker VM IP if you're running Docker in a VirtualBox environment like boot2docker). 55 | 56 | ## Feedback 57 | 58 | You can help making this game better. Just contact me or open an issue. Any feedback is appreciated! 59 | -------------------------------------------------------------------------------- /docker-compose-build.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | 5 | quboo-backend: 6 | build: 7 | context: ./sonar-connector 8 | image: "mechero/quboo-backend:${BACKEND_VERSION}" 9 | depends_on: 10 | - mongo 11 | environment: 12 | SPRING_DATA_MONGODB_HOST: mongo 13 | GAME_DATES_LEGACY: "${LEGACY_DATE}" 14 | GAME_DATES_EARLYBIRD: "${EARLYBIRD_DATE}" 15 | GAME_DATES_CAMPAIGNSTART: "${CAMPAIGNSTART_DATE}" 16 | GAME_CODE: "${QUBOO_CODE}" 17 | SONAR_SERVER: "${SONAR_SERVER}" 18 | SONAR_ORGANIZATION: "${SONAR_ORGANIZATION}" 19 | SONAR_TOKEN: "${SONAR_TOKEN}" 20 | ports: 21 | - "8080:8080" 22 | networks: 23 | - network1 24 | 25 | mongo: 26 | image: mongo:3.4 27 | hostname: mongo 28 | ports: 29 | - "27017:27017" 30 | volumes: 31 | - mongodata:/data/db 32 | networks: 33 | - network1 34 | 35 | quboo-web-client: 36 | build: 37 | context: ./web-client 38 | image: "mechero/quboo-web-client:${UI_VERSION}" 39 | ports: 40 | - "1827:1827" 41 | networks: 42 | - network1 43 | 44 | networks: 45 | network1: 46 | volumes: 47 | mongodata: 48 | -------------------------------------------------------------------------------- /docker-compose-mongo.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | mongo: 5 | image: mongo:3.4 6 | hostname: mongo 7 | ports: 8 | - "27017:27017" 9 | volumes: 10 | - mongodata:/data/db 11 | 12 | volumes: 13 | mongodata: 14 | -------------------------------------------------------------------------------- /docker-compose-quboo.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | 5 | quboo-backend: 6 | image: "mechero/quboo-backend:1.2" 7 | depends_on: 8 | - mongo 9 | environment: 10 | SPRING_DATA_MONGODB_HOST: mongo 11 | GAME_DATES_LEGACY: "${LEGACY_DATE}" 12 | GAME_DATES_EARLYBIRD: "${EARLYBIRD_DATE}" 13 | GAME_DATES_CAMPAIGNSTART: "${CAMPAIGNSTART_DATE}" 14 | GAME_CODE: "${QUBOO_CODE}" 15 | SONAR_SERVER: "${SONAR_SERVER}" 16 | SONAR_ORGANIZATION: "${SONAR_ORGANIZATION}" 17 | SONAR_TOKEN: "${SONAR_TOKEN}" 18 | ports: 19 | - "8080:8080" 20 | networks: 21 | - network1 22 | 23 | mongo: 24 | image: mongo:3.4 25 | hostname: mongo 26 | ports: 27 | - "27017:27017" 28 | volumes: 29 | - mongodata:/data/db 30 | networks: 31 | - network1 32 | 33 | quboo-web-client: 34 | image: "mechero/quboo-web-client:1.0" 35 | ports: 36 | - "1827:1827" 37 | networks: 38 | - network1 39 | 40 | networks: 41 | network1: 42 | volumes: 43 | mongodata: 44 | -------------------------------------------------------------------------------- /docker-compose-sonar.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | sonar: 5 | image: sonarqube:7.1 6 | ports: 7 | - '9000:9000' 8 | - '9092:9092' 9 | networks: 10 | - network-sonar 11 | environment: 12 | - SONARQUBE_JDBC_URL=jdbc:postgresql://db:5432/sonar 13 | volumes: 14 | - sonarqube_conf:/opt/sonarqube/conf 15 | - sonarqube_data:/opt/sonarqube/data 16 | - sonarqube_extensions:/opt/sonarqube/extensions 17 | - sonarqube_bundled-plugins:/opt/sonarqube/lib/bundled-plugins 18 | db: 19 | image: postgres:10.5-alpine 20 | ports: 21 | - '5432:5432' 22 | networks: 23 | - network-sonar 24 | environment: 25 | - POSTGRES_USER=sonar 26 | - POSTGRES_PASSWORD=sonar 27 | volumes: 28 | - postgresql:/var/lib/postgresql 29 | - postgresql_data:/var/lib/postgresql/data 30 | 31 | networks: 32 | network-sonar: 33 | driver: bridge 34 | 35 | volumes: 36 | sonarqube_conf: 37 | sonarqube_data: 38 | sonarqube_extensions: 39 | sonarqube_bundled-plugins: 40 | postgresql: 41 | postgresql_data: 42 | -------------------------------------------------------------------------------- /sonar-connector/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.6-jdk-11-slim AS builder 2 | COPY . /usr/src/code-quality-game 3 | WORKDIR /usr/src/code-quality-game 4 | RUN mkdir -p /usr/src/code-quality-game/target && mvn clean install 5 | 6 | 7 | FROM openjdk:11-jre-slim 8 | COPY --from=builder /usr/src/code-quality-game/target/code-quality-game-1.0.0-SNAPSHOT.war /usr/src/code-quality-game/ 9 | WORKDIR /usr/src/code-quality-game 10 | EXPOSE 8080 11 | CMD ["java", "-jar", "code-quality-game-1.0.0-SNAPSHOT.war"] 12 | -------------------------------------------------------------------------------- /sonar-connector/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.thepracticaldeveloper.codequalitygame 5 | code-quality-game 6 | code-quality-game 7 | 1.0.0-SNAPSHOT 8 | Code Quality Game 9 | war 10 | 11 | 11 12 | 0.8.1 13 | UTF-8 14 | **/resultbeans/*.java 15 | git 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-parent 21 | 2.1.8.RELEASE 22 | 23 | 24 | 25 | 26 | 27 | org.apache.maven.plugins 28 | maven-compiler-plugin 29 | 30 | 11 31 | 11 32 | 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-maven-plugin 38 | 39 | 40 | 41 | org.jacoco 42 | jacoco-maven-plugin 43 | ${jacoco.version} 44 | 45 | 46 | default-prepare-agent 47 | 48 | prepare-agent 49 | 50 | 51 | 52 | default-report 53 | prepare-package 54 | 55 | report 56 | 57 | 58 | 59 | 60 | 61 | org.apache.maven.plugins 62 | maven-surefire-plugin 63 | 2.22.1 64 | 65 | 0 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.springframework.boot 74 | spring-boot-starter-data-mongodb 75 | 76 | 77 | org.springframework.boot 78 | spring-boot-starter-web 79 | 80 | 81 | org.springframework.boot 82 | spring-boot-starter-actuator 83 | 84 | 85 | org.springframework.boot 86 | spring-boot-starter-test 87 | test 88 | 89 | 90 | com.fasterxml.jackson.dataformat 91 | jackson-dataformat-yaml 92 | 93 | 94 | 95 | javax.servlet 96 | javax.servlet-api 97 | provided 98 | 99 | 100 | commons-codec 101 | commons-codec 102 | 1.11 103 | 104 | 105 | org.apache.commons 106 | commons-lang3 107 | 3.7 108 | 109 | 110 | 111 | javax.xml.bind 112 | jaxb-api 113 | 2.3.0 114 | 115 | 116 | org.glassfish.jaxb 117 | jaxb-runtime 118 | 2.3.0 119 | runtime 120 | 121 | 122 | javax.activation 123 | javax.activation-api 124 | 1.2.0 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/Application.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.web.client.RestTemplate; 7 | 8 | @SpringBootApplication 9 | public class Application { 10 | 11 | @Bean 12 | public RestTemplate restTemplate() { 13 | return new RestTemplate(); 14 | } 15 | 16 | public static void main(final String[] args) { 17 | SpringApplication.run(Application.class, args); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/app/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.app; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | 8 | @Configuration 9 | public class SecurityConfiguration { 10 | 11 | @Bean 12 | public WebMvcConfigurer corsConfigurer() { 13 | // Modifies CORS configuration to allow connecting from the Angular frontend. 14 | return new WebMvcConfigurer() { 15 | @Override 16 | public void addCorsMappings(CorsRegistry registry) { 17 | registry.addMapping("/**") 18 | .allowedMethods("OPTIONS", "POST", "PUT", "GET", "DELETE", "PATCH") 19 | .allowedOrigins("*"); 20 | } 21 | }; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/badges/calculators/BadgeCalculator.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.badges.calculators; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans.Issue; 4 | import com.thepracticaldeveloper.devgame.modules.stats.domain.BadgeCard; 5 | 6 | import java.util.Optional; 7 | import java.util.Set; 8 | 9 | public interface BadgeCalculator { 10 | 11 | String badgeKey(); 12 | 13 | Optional badgeFromIssueList(String userId, Set issues); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/badges/calculators/EarlyBirdBadgeCalculator.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.badges.calculators; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.badges.domain.BadgeDetails; 4 | import com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans.Issue; 5 | import com.thepracticaldeveloper.devgame.modules.stats.domain.BadgeCard; 6 | import com.thepracticaldeveloper.devgame.util.IssueDateFormatter; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.time.Instant; 13 | import java.time.LocalDate; 14 | import java.util.Optional; 15 | import java.util.Set; 16 | import java.util.UUID; 17 | 18 | @Component 19 | public class EarlyBirdBadgeCalculator implements BadgeCalculator { 20 | 21 | private static final Logger log = LoggerFactory.getLogger(EarlyBirdBadgeCalculator.class); 22 | 23 | public static final String KEY = BadgeDetails.EARLY_BIRD.name(); 24 | 25 | private static final int EXTRA_POINTS = 100; 26 | 27 | private final LocalDate earlyBirdDate; 28 | 29 | public EarlyBirdBadgeCalculator(@Value("${game.dates.earlyBird}") final String earlyBirdDate) { 30 | this.earlyBirdDate = LocalDate.parse(earlyBirdDate); 31 | log.info("Early Bird Badge is configured with max date {}", this.earlyBirdDate); 32 | } 33 | 34 | @Override 35 | public String badgeKey() { 36 | return KEY; 37 | } 38 | 39 | @Override 40 | public Optional badgeFromIssueList(final String userId, Set issues) { 41 | if (issues.stream().filter(i -> i.getCloseDate() != null) 42 | .anyMatch(i -> IssueDateFormatter.format(i.getCloseDate()).isBefore(earlyBirdDate))) { 43 | return Optional.of(new BadgeCard(UUID.randomUUID().toString(), userId, KEY, Instant.now(), EXTRA_POINTS)); 44 | } else { 45 | return Optional.empty(); 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/badges/calculators/UnitTesterBronzeBadgeCalculator.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.badges.calculators; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.badges.domain.BadgeDetails; 4 | import com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans.Issue; 5 | import com.thepracticaldeveloper.devgame.modules.stats.domain.BadgeCard; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.time.Instant; 9 | import java.util.Optional; 10 | import java.util.Set; 11 | import java.util.UUID; 12 | 13 | @Component 14 | public class UnitTesterBronzeBadgeCalculator implements BadgeCalculator { 15 | 16 | public static final String KEY = BadgeDetails.UNIT_TESTER_BRONZE.name(); 17 | 18 | private static final int EXTRA_POINTS = 250; 19 | 20 | private static final String RULE_ID_LINE = "InsufficientLineCoverage"; 21 | private static final String RULE_ID_BRANCH = "InsufficientBranchCoverage"; 22 | 23 | @Override 24 | public String badgeKey() { 25 | return KEY; 26 | } 27 | 28 | @Override 29 | public Optional badgeFromIssueList(final String userId, final Set issues) { 30 | long count = issues.stream() 31 | .filter(i -> i.getRule().contains(RULE_ID_LINE) || i.getRule().contains(RULE_ID_BRANCH)).count(); 32 | if (count >= 10) { 33 | return Optional.of(new BadgeCard(UUID.randomUUID().toString(), userId, KEY, Instant.now(), EXTRA_POINTS)); 34 | } else { 35 | return Optional.empty(); 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/badges/calculators/UnitTesterGoldBadgeCalculator.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.badges.calculators; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.badges.domain.BadgeDetails; 4 | import com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans.Issue; 5 | import com.thepracticaldeveloper.devgame.modules.stats.domain.BadgeCard; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.time.Instant; 9 | import java.util.Optional; 10 | import java.util.Set; 11 | import java.util.UUID; 12 | 13 | @Component 14 | public class UnitTesterGoldBadgeCalculator implements BadgeCalculator { 15 | 16 | public static final String KEY = BadgeDetails.UNIT_TESTER_GOLD.name(); 17 | 18 | private static final int EXTRA_POINTS = 1000; 19 | 20 | private static final String RULE_ID_LINE = "InsufficientLineCoverage"; 21 | private static final String RULE_ID_BRANCH = "InsufficientBranchCoverage"; 22 | 23 | @Override 24 | public String badgeKey() { 25 | return KEY; 26 | } 27 | 28 | @Override 29 | public Optional badgeFromIssueList(final String userId, final Set issues) { 30 | long count = issues.stream() 31 | .filter(i -> i.getRule().contains(RULE_ID_LINE) || i.getRule().contains(RULE_ID_BRANCH)).count(); 32 | if (count >= 100) { 33 | return Optional.of(new BadgeCard(UUID.randomUUID().toString(), userId, KEY, Instant.now(), EXTRA_POINTS)); 34 | } else { 35 | return Optional.empty(); 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/badges/calculators/UnitTesterPaperBadgeCalculator.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.badges.calculators; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.badges.domain.BadgeDetails; 4 | import com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans.Issue; 5 | import com.thepracticaldeveloper.devgame.modules.stats.domain.BadgeCard; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.time.Instant; 9 | import java.util.Optional; 10 | import java.util.Set; 11 | import java.util.UUID; 12 | 13 | @Component 14 | public class UnitTesterPaperBadgeCalculator implements BadgeCalculator { 15 | 16 | public static final String KEY = BadgeDetails.UNIT_TESTER_PAPER.name(); 17 | 18 | private static final int EXTRA_POINTS = 100; 19 | 20 | private static final String RULE_ID_LINE = "InsufficientLineCoverage"; 21 | private static final String RULE_ID_BRANCH = "InsufficientBranchCoverage"; 22 | 23 | @Override 24 | public String badgeKey() { 25 | return KEY; 26 | } 27 | 28 | @Override 29 | public Optional badgeFromIssueList(final String userId, final Set issues) { 30 | long count = issues.stream() 31 | .filter(i -> i.getRule().contains(RULE_ID_LINE) || i.getRule().contains(RULE_ID_BRANCH)).count(); 32 | if (count >= 1) { 33 | return Optional.of(new BadgeCard(UUID.randomUUID().toString(), userId, KEY, Instant.now(), EXTRA_POINTS)); 34 | } else { 35 | return Optional.empty(); 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/badges/calculators/UnitTesterSilverBadgeCalculator.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.badges.calculators; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.badges.domain.BadgeDetails; 4 | import com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans.Issue; 5 | import com.thepracticaldeveloper.devgame.modules.stats.domain.BadgeCard; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.time.Instant; 9 | import java.util.Optional; 10 | import java.util.Set; 11 | import java.util.UUID; 12 | 13 | @Component 14 | public class UnitTesterSilverBadgeCalculator implements BadgeCalculator { 15 | 16 | public static final String KEY = BadgeDetails.UNIT_TESTER_SILVER.name(); 17 | 18 | private static final int EXTRA_POINTS = 650; 19 | 20 | private static final String RULE_ID_LINE = "InsufficientLineCoverage"; 21 | private static final String RULE_ID_BRANCH = "InsufficientBranchCoverage"; 22 | 23 | @Override 24 | public String badgeKey() { 25 | return KEY; 26 | } 27 | 28 | @Override 29 | public Optional badgeFromIssueList(final String userId, final Set issues) { 30 | long count = issues.stream() 31 | .filter(i -> i.getRule().contains(RULE_ID_LINE) || i.getRule().contains(RULE_ID_BRANCH)).count(); 32 | if (count >= 25) { 33 | return Optional.of(new BadgeCard(UUID.randomUUID().toString(), userId, KEY, Instant.now(), EXTRA_POINTS)); 34 | } else { 35 | return Optional.empty(); 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/badges/domain/BadgeDetails.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.badges.domain; 2 | 3 | public enum BadgeDetails { 4 | 5 | EARLY_BIRD("Early Bird", "You're an early adopter of the game"), 6 | UNIT_TESTER_PAPER("Paper Unit Tester", "You covered legacy code with at least 1 Unit Test"), 7 | UNIT_TESTER_BRONZE("Bronze Unit Tester", "You covered legacy code with at least 10 Unit Tests"), 8 | UNIT_TESTER_SILVER("Silver Unit Tester", "You covered legacy code with at least 25 Unit Tests"), 9 | UNIT_TESTER_GOLD("Gold Unit Tester", "You covered legacy code with at least 100 Unit Tests"); 10 | 11 | private final String name; 12 | private final String description; 13 | 14 | BadgeDetails(final String name, final String description) { 15 | this.name = name; 16 | this.description = description; 17 | } 18 | 19 | public String getName() { 20 | return name; 21 | } 22 | 23 | public String getDescription() { 24 | return description; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/badges/domain/SonarBadge.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.badges.domain; 2 | 3 | public class SonarBadge { 4 | private String name; 5 | private String description; 6 | private int extraPoints; 7 | 8 | public SonarBadge(String name, String description, int extraPoints) { 9 | super(); 10 | this.name = name; 11 | this.description = description; 12 | this.extraPoints = extraPoints; 13 | } 14 | 15 | public String getName() { 16 | return name; 17 | } 18 | 19 | public String getDescription() { 20 | return description; 21 | } 22 | 23 | public int getExtraPoints() { 24 | return extraPoints; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return "SonarBadge [name=" + name + ", description=" + description + "]"; 30 | } 31 | 32 | @Override 33 | public int hashCode() { 34 | final int prime = 31; 35 | int result = 1; 36 | result = prime * result + ((name == null) ? 0 : name.hashCode()); 37 | return result; 38 | } 39 | 40 | @Override 41 | public boolean equals(Object obj) { 42 | if (this == obj) 43 | return true; 44 | if (obj == null) 45 | return false; 46 | if (getClass() != obj.getClass()) 47 | return false; 48 | SonarBadge other = (SonarBadge) obj; 49 | if (name == null) { 50 | if (other.name != null) 51 | return false; 52 | } else if (!name.equals(other.name)) 53 | return false; 54 | return true; 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/code/CodeController.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.code; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | @RestController 8 | @RequestMapping(value = {"/code"}) 9 | public class CodeController { 10 | 11 | private final CodeService codeService; 12 | 13 | public CodeController(final CodeService codeService) { 14 | this.codeService = codeService; 15 | } 16 | 17 | @GetMapping 18 | public CodeDetails getCode() { 19 | return codeService.getCodeDetails().orElse(new CodeDetails("-NOCODE-")); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/code/CodeDetails.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.code; 2 | 3 | import java.util.StringTokenizer; 4 | 5 | public class CodeDetails { 6 | private final String name; 7 | 8 | public CodeDetails(final String name) { 9 | this.name = name; 10 | } 11 | 12 | public String getName() { 13 | return name; 14 | } 15 | 16 | public static CodeDetails fromCodeChain(final String codeChain) { 17 | final StringTokenizer tokenizer = new StringTokenizer(codeChain, "^T^", false); 18 | String name = null; 19 | while (tokenizer.hasMoreElements()) { 20 | final String token = tokenizer.nextToken(); 21 | final String tokenKey = token.substring(0, token.indexOf('=')); 22 | final String tokenValue = token.substring(token.indexOf('=') + 1); 23 | switch (tokenKey) { 24 | case "name": { 25 | name = tokenValue; 26 | break; 27 | } 28 | } 29 | } 30 | return new CodeDetails(name); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/code/CodeParser.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.code; 2 | 3 | import javax.crypto.Cipher; 4 | import java.security.KeyFactory; 5 | import java.security.PublicKey; 6 | import java.security.spec.X509EncodedKeySpec; 7 | import java.util.Base64; 8 | 9 | public class CodeParser { 10 | 11 | private final String pub1 = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5jO1hXLNTaYc55OQ4T3"; 12 | private final String pub2 = "5i7YyJ7jCydqkNEhWZptdby30xh/6QqQFN8L/Ly0hr/bIggmOTrBzAB0rr26PA6IB5NAZndWAKUX7jH/snh3"; 13 | private final String pub8 = "jVs88FEV7O97Kd0FboRRNga8Rn2M8oZBSXVBTdd83r6dlh14uKj5bIMyeCwBcA4Efe05yB/AaHC9Ag7lg1D5j4K/v5QRQMP2V"; 14 | private final String pub9 = "zU2AAHHHWsPnlflM/2XdwHgYB90UzstE2yoxhPZNXVv2n60KcFGooKszvYddZoFsw9T/gByXufJO3kBtk1Hd3liWdO"; 15 | private final String pub12 = "XL6/IDTBH94ds4Nw30hmvrYFkd1pNX2AMsHoTjGICh4N/uzl1iiQIDAQAB"; 16 | 17 | private PublicKey getPublicKey() { 18 | try { 19 | final byte[] pubKeyBytes = Base64.getDecoder().decode(pub1 + pub2 + pub8 + pub9 + pub12); 20 | X509EncodedKeySpec keySpec = new X509EncodedKeySpec(pubKeyBytes); 21 | KeyFactory keyFactory = KeyFactory.getInstance("RSA"); 22 | PublicKey publicKey = keyFactory.generatePublic(keySpec); 23 | return publicKey; 24 | } catch (final Exception e) { 25 | throw new RuntimeException("Error loading public key", e); 26 | } 27 | } 28 | 29 | public CodeDetails getCodeDetails(final String encryptedCodeBase64) throws InvalidCodeException { 30 | try { 31 | Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); 32 | cipher.init(Cipher.DECRYPT_MODE, getPublicKey()); 33 | final String decryptedCode = new String(cipher.doFinal(Base64.getDecoder().decode(encryptedCodeBase64))); 34 | return CodeDetails.fromCodeChain(decryptedCode); 35 | } catch (final Exception e) { 36 | throw new InvalidCodeException("Error while trying to read the code", e); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/code/CodeService.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.code; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.InitializingBean; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.Optional; 11 | 12 | @Service 13 | public class CodeService implements InitializingBean { 14 | 15 | private static final Logger log = LoggerFactory.getLogger(CodeService.class); 16 | 17 | private String codeString; 18 | private Optional codeDetails; 19 | 20 | public CodeService(@Value("${game.code}") final String codeString) { 21 | this.codeString = codeString; 22 | this.codeDetails = Optional.empty(); 23 | } 24 | 25 | @Override 26 | public void afterPropertiesSet() { 27 | try { 28 | if (StringUtils.isNotEmpty(codeString)) { 29 | final CodeParser codeParser = new CodeParser(); 30 | codeDetails = Optional.of(codeParser.getCodeDetails(codeString)); 31 | log.info("Valid code found for {}", codeDetails.get().getName()); 32 | } 33 | } catch (Exception e) { 34 | log.error("Error validating the code: " + e.getMessage(), e); 35 | } 36 | } 37 | 38 | public Optional getCodeDetails() { 39 | return codeDetails; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/code/InvalidCodeException.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.code; 2 | 3 | public class InvalidCodeException extends Exception { 4 | public InvalidCodeException(String msg, Exception e) { 5 | super(msg, e); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/configuration/controller/SonarServerConfigurationController.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.configuration.controller; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.configuration.domain.SonarServerConfiguration; 4 | import com.thepracticaldeveloper.devgame.modules.configuration.domain.sonar.SonarServerStatus; 5 | import com.thepracticaldeveloper.devgame.modules.configuration.service.SonarServerConfigurationService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestMethod; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | @RestController 12 | @RequestMapping(value = {"/configuration"}) 13 | public class SonarServerConfigurationController { 14 | 15 | private final SonarServerConfigurationService configurationService; 16 | 17 | @Autowired 18 | public SonarServerConfigurationController(final SonarServerConfigurationService configurationService) { 19 | this.configurationService = configurationService; 20 | } 21 | 22 | @RequestMapping(value = "/server-status", method = RequestMethod.GET) 23 | public SonarServerStatus getServerStatus() { 24 | final SonarServerConfiguration configuration = configurationService.getConfiguration(); 25 | return configurationService.checkServerDetails(configuration); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/configuration/dao/SonarServerConfigurationDao.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.configuration.dao; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.configuration.domain.SonarServerConfiguration; 4 | 5 | public interface SonarServerConfigurationDao { 6 | 7 | SonarServerConfiguration getConfiguration(); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/configuration/dao/SonarServerConfigurationDaoImpl.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.configuration.dao; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.configuration.domain.SonarServerConfiguration; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class SonarServerConfigurationDaoImpl implements SonarServerConfigurationDao { 10 | 11 | private SonarServerConfiguration sonarServerConfiguration; 12 | 13 | private final String sonarUrl; 14 | private final String sonarToken; 15 | private final String sonarOrganization; 16 | 17 | @Autowired 18 | public SonarServerConfigurationDaoImpl(@Value("${sonar.token}") final String sonarToken, 19 | @Value("${sonar.server}") final String sonarUrl, 20 | @Value("${sonar.organization}") final String sonarOrganization) { 21 | this.sonarUrl = sonarUrl; 22 | this.sonarToken = sonarToken; 23 | this.sonarOrganization = sonarOrganization; 24 | this.sonarServerConfiguration = createSonarServerConfiguration(); 25 | } 26 | 27 | private SonarServerConfiguration createSonarServerConfiguration() { 28 | return new SonarServerConfiguration(sonarUrl, sonarToken, sonarOrganization); 29 | } 30 | 31 | @Override 32 | public SonarServerConfiguration getConfiguration() { 33 | return sonarServerConfiguration; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/configuration/domain/SonarServerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.configuration.domain; 2 | 3 | public class SonarServerConfiguration { 4 | private String url; 5 | private String token; 6 | private String organization; 7 | 8 | public SonarServerConfiguration() { 9 | } 10 | 11 | public SonarServerConfiguration(final String url, final String token, final String organization) { 12 | this.url = url; 13 | this.token = token; 14 | this.organization = organization; 15 | } 16 | 17 | public String getUrl() { 18 | return url; 19 | } 20 | 21 | public String getToken() { 22 | return token; 23 | } 24 | 25 | public String getOrganization() { 26 | return organization; 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return "SonarServerConfiguration{" + 32 | "url='" + url + '\'' + 33 | ", token='" + token + '\'' + 34 | ", organization='" + organization + '\'' + 35 | '}'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/configuration/domain/sonar/SonarAuthenticationResponse.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.configuration.domain.sonar; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | public final class SonarAuthenticationResponse { 7 | 8 | private boolean valid; 9 | 10 | @JsonCreator 11 | public SonarAuthenticationResponse(@JsonProperty(value = "valid") boolean valid) { 12 | this.valid = valid; 13 | } 14 | 15 | public boolean isValid() { 16 | return valid; 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return "SonarAuthenticationResponse{" + 22 | "valid=" + valid + 23 | '}'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/configuration/domain/sonar/SonarServerStatus.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.configuration.domain.sonar; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | public final class SonarServerStatus { 7 | 8 | public enum Key { 9 | UP, 10 | UNAUTHORIZED, 11 | CONNECTION_ERROR, 12 | UNKNOWN_ERROR 13 | } 14 | 15 | public static final String STATUS_UP = "UP"; 16 | 17 | private final String id; 18 | private final String version; 19 | private final String status; 20 | private final String message; 21 | 22 | @JsonCreator 23 | public SonarServerStatus(@JsonProperty("id") String id, @JsonProperty("version") String version, @JsonProperty("status") String status, @JsonProperty("message") final String message) { 24 | this.id = id; 25 | this.version = version; 26 | this.status = status; 27 | this.message = message; 28 | } 29 | 30 | public SonarServerStatus(final Key status, final String message) { 31 | this(null, null, status.name(), message); 32 | } 33 | 34 | public SonarServerStatus(final Key status) { 35 | this(null, null, status.name(), null); 36 | } 37 | 38 | public String getId() { 39 | return id; 40 | } 41 | 42 | public String getVersion() { 43 | return version; 44 | } 45 | 46 | public String getStatus() { 47 | return status; 48 | } 49 | 50 | public String getMessage() { 51 | return message; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/configuration/service/SonarServerConfigurationService.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.configuration.service; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.configuration.domain.SonarServerConfiguration; 4 | import com.thepracticaldeveloper.devgame.modules.configuration.domain.sonar.SonarServerStatus; 5 | 6 | public interface SonarServerConfigurationService { 7 | SonarServerStatus checkServerDetails(SonarServerConfiguration config); 8 | 9 | boolean checkServerAuthentication(SonarServerConfiguration config); 10 | 11 | SonarServerConfiguration getConfiguration(); 12 | } 13 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/configuration/service/SonarServerConfigurationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.configuration.service; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.configuration.dao.SonarServerConfigurationDao; 4 | import com.thepracticaldeveloper.devgame.modules.configuration.domain.SonarServerConfiguration; 5 | import com.thepracticaldeveloper.devgame.modules.configuration.domain.sonar.SonarAuthenticationResponse; 6 | import com.thepracticaldeveloper.devgame.modules.configuration.domain.sonar.SonarServerStatus; 7 | import com.thepracticaldeveloper.devgame.util.ApiHttpUtils; 8 | import org.apache.commons.logging.Log; 9 | import org.apache.commons.logging.LogFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.http.*; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.web.client.HttpClientErrorException; 14 | import org.springframework.web.client.ResourceAccessException; 15 | import org.springframework.web.client.RestTemplate; 16 | 17 | @Service 18 | final class SonarServerConfigurationServiceImpl implements SonarServerConfigurationService { 19 | 20 | private static final Log log = LogFactory.getLog(SonarServerConfigurationServiceImpl.class); 21 | private static final String API_SYSTEM_STATUS = "/api/system/status"; 22 | private static final String API_AUTHENTICATION_VALIDATE = "/api/authentication/validate"; 23 | 24 | private SonarServerConfigurationDao sonarServerConfigurationDao; 25 | private RestTemplate restTemplate; 26 | 27 | @Autowired 28 | public SonarServerConfigurationServiceImpl(final SonarServerConfigurationDao sonarServerConfigurationDao, final RestTemplate restTemplate) { 29 | this.sonarServerConfigurationDao = sonarServerConfigurationDao; 30 | this.restTemplate = restTemplate; 31 | } 32 | 33 | /** 34 | * Checks the current status of the server and the version running on it. 35 | * It also detects if the server is not reachable. 36 | * 37 | * @param config The configuration for Sonar Server 38 | * @return the response from server 39 | */ 40 | @Override 41 | public SonarServerStatus checkServerDetails(final SonarServerConfiguration config) { 42 | log.info("Trying to reach Sonar server at " + config.getUrl() + API_SYSTEM_STATUS); 43 | try { 44 | final HttpHeaders authHeaders = ApiHttpUtils.getHeaders(config.getToken()); 45 | HttpEntity request = new HttpEntity<>(authHeaders); 46 | final ResponseEntity response = restTemplate 47 | .exchange(config.getUrl() + API_SYSTEM_STATUS, 48 | HttpMethod.GET, request, SonarServerStatus.class); 49 | log.info("Response received from server: " + response.getBody()); 50 | return response.getBody(); 51 | } catch (final HttpClientErrorException clientErrorException) { 52 | log.error(clientErrorException); 53 | if (clientErrorException.getStatusCode() == HttpStatus.UNAUTHORIZED) { 54 | return new SonarServerStatus(SonarServerStatus.Key.UNAUTHORIZED); 55 | } else { 56 | return new SonarServerStatus(SonarServerStatus.Key.UNKNOWN_ERROR, clientErrorException.getMessage()); 57 | } 58 | } catch (final ResourceAccessException resourceAccessException) { 59 | log.error(resourceAccessException); 60 | return new SonarServerStatus(SonarServerStatus.Key.CONNECTION_ERROR, resourceAccessException.getMessage()); 61 | } 62 | } 63 | 64 | /** 65 | * Checks if the provided user and password are valid for this server. 66 | * It assumes that server is reachable, so it may throw Exceptions if it isn't. 67 | * 68 | * @param config The server configuration. 69 | * @return true if the authentication is valid, false otherwise. 70 | */ 71 | @Override 72 | public boolean checkServerAuthentication(final SonarServerConfiguration config) { 73 | log.info("Trying to authenticate with provided token..."); 74 | final HttpHeaders authHeaders = ApiHttpUtils.getHeaders(config.getToken()); 75 | HttpEntity request = new HttpEntity<>(authHeaders); 76 | final ResponseEntity response = restTemplate 77 | .exchange(config.getUrl() + API_AUTHENTICATION_VALIDATE, 78 | HttpMethod.GET, request, SonarAuthenticationResponse.class); 79 | log.info("Response to authentication attempt: " + response.getBody()); 80 | return response.getBody().isValid(); 81 | } 82 | 83 | /** 84 | * Retrieves the server configuration 85 | * 86 | * @return the Sonar server configuration object 87 | */ 88 | @Override 89 | public SonarServerConfiguration getConfiguration() { 90 | return sonarServerConfigurationDao.getConfiguration(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/retriever/controller/RetrieverController.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.retriever.controller; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.retriever.service.SonarDataRetriever; 4 | import com.thepracticaldeveloper.devgame.modules.users.dto.MessageResponseDTO; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | @RestController 11 | @RequestMapping("/retriever") 12 | public class RetrieverController { 13 | 14 | private final SonarDataRetriever sonarDataRetriever; 15 | 16 | public RetrieverController(final SonarDataRetriever retriever) { 17 | this.sonarDataRetriever = retriever; 18 | } 19 | 20 | @PostMapping("/now") 21 | public ResponseEntity retrieveNow() { 22 | sonarDataRetriever.retrieveData(); 23 | return ResponseEntity.ok().body(new MessageResponseDTO("Requested")); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/retriever/service/SonarDataRetriever.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.retriever.service; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.configuration.service.SonarServerConfigurationService; 4 | import com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans.Issue; 5 | import com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans.Issues; 6 | import com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans.Paging; 7 | import com.thepracticaldeveloper.devgame.modules.stats.service.BadgeService; 8 | import com.thepracticaldeveloper.devgame.modules.stats.service.ScoreCardService; 9 | import com.thepracticaldeveloper.devgame.modules.users.domain.User; 10 | import com.thepracticaldeveloper.devgame.modules.users.service.UserService; 11 | import com.thepracticaldeveloper.devgame.util.ApiHttpUtils; 12 | import org.apache.commons.logging.Log; 13 | import org.apache.commons.logging.LogFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.beans.factory.annotation.Value; 16 | import org.springframework.http.*; 17 | import org.springframework.scheduling.annotation.EnableAsync; 18 | import org.springframework.scheduling.annotation.EnableScheduling; 19 | import org.springframework.scheduling.annotation.Scheduled; 20 | import org.springframework.stereotype.Service; 21 | import org.springframework.web.client.HttpServerErrorException; 22 | import org.springframework.web.client.RestTemplate; 23 | import org.springframework.web.util.UriComponentsBuilder; 24 | 25 | import java.net.URI; 26 | import java.util.Collections; 27 | import java.util.HashSet; 28 | import java.util.Set; 29 | import java.util.concurrent.atomic.AtomicBoolean; 30 | import java.util.function.Consumer; 31 | 32 | @Service 33 | @EnableAsync 34 | @EnableScheduling 35 | public final class SonarDataRetriever { 36 | 37 | private static final Log log = LogFactory.getLog(SonarDataRetriever.class); 38 | private static final String GET_ISSUES_COMMAND = "/api/issues/search?organizations={organization}&assignees={assignees}&p={page}&ps={pageSize}"; 39 | 40 | private final UserService userService; 41 | private final ScoreCardService scoreCardService; 42 | private final BadgeService badgeCardService; 43 | private final SonarServerConfigurationService configurationService; 44 | 45 | private final String organization; 46 | private AtomicBoolean retrieving; 47 | 48 | @Autowired 49 | public SonarDataRetriever(final UserService userService, 50 | final ScoreCardService scoreCardService, 51 | final BadgeService badgeCardService, 52 | final SonarServerConfigurationService configurationService, 53 | final @Value("${sonar.organization}") String organization) { 54 | this.userService = userService; 55 | this.scoreCardService = scoreCardService; 56 | this.badgeCardService = badgeCardService; 57 | this.configurationService = configurationService; 58 | this.organization = organization; 59 | this.retrieving = new AtomicBoolean(false); 60 | } 61 | 62 | @Scheduled(fixedRate = 10 * 60000) 63 | public void retrieveData() { 64 | try { 65 | if (retrieving.compareAndSet(false, true)) { 66 | // It seems that sonar doesn't allow parallel queries with same user since it creates a register for internal 67 | // stats and that causes an error when inserting into the database. 68 | userService.getAllActiveUsers().forEach( 69 | new RequestLauncher(scoreCardService, badgeCardService, organization, configurationService.getConfiguration().getUrl(), 70 | configurationService.getConfiguration().getToken()) 71 | ); 72 | retrieving.set(false); 73 | } else { 74 | log.info("Ignoring retrieval request since a retrieval is already in progress..."); 75 | } 76 | } catch (final Throwable t) { 77 | log.error("Failed to retrieve data", t); 78 | retrieving.set(false); 79 | } 80 | } 81 | 82 | private static final class RequestLauncher implements Consumer { 83 | 84 | private final ScoreCardService scoreCardService; 85 | private final BadgeService badgeCardService; 86 | private final String sonarOrganization; 87 | private final String sonarUrl; 88 | private final String token; 89 | 90 | RequestLauncher(final ScoreCardService scoreCardService, final BadgeService badgeCardService, String sonarOrganization, final String sonarUrl, final String token) { 91 | this.scoreCardService = scoreCardService; 92 | this.badgeCardService = badgeCardService; 93 | this.sonarOrganization = sonarOrganization; 94 | this.sonarUrl = sonarUrl; 95 | this.token = token; 96 | } 97 | 98 | @Override 99 | public void accept(final User user) { 100 | try { 101 | int pageIndex = 1; 102 | int pageTotal = 1; 103 | int pageSize = 1; 104 | Set issues = new HashSet<>(); 105 | while (pageTotal == pageSize) { 106 | log.trace("Requesting page " + pageIndex); 107 | RestTemplate restTemplate = new RestTemplate(); 108 | HttpEntity request = new HttpEntity<>(getHeaders()); 109 | URI uri = getResolvedIssuesForAssignee(user.getLogin(), pageIndex); 110 | log.trace("URI: " + uri); 111 | ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, request, Issues.class); 112 | Paging paging = response.getBody().getPaging(); 113 | pageTotal = paging.getTotal(); 114 | pageSize = paging.getPageSize(); 115 | issues.addAll(response.getBody().getIssues()); 116 | pageIndex++; 117 | } 118 | scoreCardService.saveNewCardsFromIssueList(user.getId(), issues); 119 | badgeCardService.saveNewBadgesFromIssueList(user.getId(), issues); 120 | } catch (final HttpServerErrorException serverException) { 121 | log.error(serverException); 122 | throw serverException; 123 | } 124 | } 125 | 126 | HttpHeaders getHeaders() { 127 | HttpHeaders httpHeaders; 128 | if (token != null && !token.trim().isEmpty()) { 129 | httpHeaders = (ApiHttpUtils.getHeaders(token)); 130 | } else { 131 | httpHeaders = new HttpHeaders(); 132 | } 133 | httpHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8)); 134 | return httpHeaders; 135 | } 136 | 137 | URI getResolvedIssuesForAssignee(final String assignee, final int pageIndex) { 138 | return UriComponentsBuilder.fromHttpUrl(sonarUrl + GET_ISSUES_COMMAND) 139 | .buildAndExpand(sonarOrganization, assignee.toLowerCase() + "," + assignee.toUpperCase(), 140 | pageIndex, 500) 141 | .toUri(); 142 | } 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/sonarapi/resultbeans/Component.java: -------------------------------------------------------------------------------- 1 | 2 | package com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans; 3 | 4 | import com.fasterxml.jackson.annotation.*; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | @JsonInclude(JsonInclude.Include.NON_NULL) 11 | @JsonPropertyOrder({ 12 | "id", 13 | "key", 14 | "uuid", 15 | "enabled", 16 | "qualifier", 17 | "name", 18 | "longName", 19 | "path", 20 | "projectId", 21 | "subProjectId" 22 | }) 23 | public class Component { 24 | 25 | @JsonProperty("id") 26 | private Integer id; 27 | @JsonProperty("key") 28 | private String key; 29 | @JsonProperty("uuid") 30 | private String uuid; 31 | @JsonProperty("enabled") 32 | private Boolean enabled; 33 | @JsonProperty("qualifier") 34 | private String qualifier; 35 | @JsonProperty("name") 36 | private String name; 37 | @JsonProperty("longName") 38 | private String longName; 39 | @JsonProperty("path") 40 | private String path; 41 | @JsonProperty("projectId") 42 | private Integer projectId; 43 | @JsonProperty("subProjectId") 44 | private Integer subProjectId; 45 | @JsonIgnore 46 | private Map additionalProperties = new HashMap(); 47 | 48 | /** 49 | * 50 | * @return 51 | * The id 52 | */ 53 | @JsonProperty("id") 54 | public Integer getId() { 55 | return id; 56 | } 57 | 58 | /** 59 | * 60 | * @param id 61 | * The id 62 | */ 63 | @JsonProperty("id") 64 | public void setId(Integer id) { 65 | this.id = id; 66 | } 67 | 68 | public Component withId(Integer id) { 69 | this.id = id; 70 | return this; 71 | } 72 | 73 | /** 74 | * 75 | * @return 76 | * The key 77 | */ 78 | @JsonProperty("key") 79 | public String getKey() { 80 | return key; 81 | } 82 | 83 | /** 84 | * 85 | * @param key 86 | * The key 87 | */ 88 | @JsonProperty("key") 89 | public void setKey(String key) { 90 | this.key = key; 91 | } 92 | 93 | public Component withKey(String key) { 94 | this.key = key; 95 | return this; 96 | } 97 | 98 | /** 99 | * 100 | * @return 101 | * The uuid 102 | */ 103 | @JsonProperty("uuid") 104 | public String getUuid() { 105 | return uuid; 106 | } 107 | 108 | /** 109 | * 110 | * @param uuid 111 | * The uuid 112 | */ 113 | @JsonProperty("uuid") 114 | public void setUuid(String uuid) { 115 | this.uuid = uuid; 116 | } 117 | 118 | public Component withUuid(String uuid) { 119 | this.uuid = uuid; 120 | return this; 121 | } 122 | 123 | /** 124 | * 125 | * @return 126 | * The enabled 127 | */ 128 | @JsonProperty("enabled") 129 | public Boolean getEnabled() { 130 | return enabled; 131 | } 132 | 133 | /** 134 | * 135 | * @param enabled 136 | * The enabled 137 | */ 138 | @JsonProperty("enabled") 139 | public void setEnabled(Boolean enabled) { 140 | this.enabled = enabled; 141 | } 142 | 143 | public Component withEnabled(Boolean enabled) { 144 | this.enabled = enabled; 145 | return this; 146 | } 147 | 148 | /** 149 | * 150 | * @return 151 | * The qualifier 152 | */ 153 | @JsonProperty("qualifier") 154 | public String getQualifier() { 155 | return qualifier; 156 | } 157 | 158 | /** 159 | * 160 | * @param qualifier 161 | * The qualifier 162 | */ 163 | @JsonProperty("qualifier") 164 | public void setQualifier(String qualifier) { 165 | this.qualifier = qualifier; 166 | } 167 | 168 | public Component withQualifier(String qualifier) { 169 | this.qualifier = qualifier; 170 | return this; 171 | } 172 | 173 | /** 174 | * 175 | * @return 176 | * The name 177 | */ 178 | @JsonProperty("name") 179 | public String getName() { 180 | return name; 181 | } 182 | 183 | /** 184 | * 185 | * @param name 186 | * The name 187 | */ 188 | @JsonProperty("name") 189 | public void setName(String name) { 190 | this.name = name; 191 | } 192 | 193 | public Component withName(String name) { 194 | this.name = name; 195 | return this; 196 | } 197 | 198 | /** 199 | * 200 | * @return 201 | * The longName 202 | */ 203 | @JsonProperty("longName") 204 | public String getLongName() { 205 | return longName; 206 | } 207 | 208 | /** 209 | * 210 | * @param longName 211 | * The longName 212 | */ 213 | @JsonProperty("longName") 214 | public void setLongName(String longName) { 215 | this.longName = longName; 216 | } 217 | 218 | public Component withLongName(String longName) { 219 | this.longName = longName; 220 | return this; 221 | } 222 | 223 | /** 224 | * 225 | * @return 226 | * The path 227 | */ 228 | @JsonProperty("path") 229 | public String getPath() { 230 | return path; 231 | } 232 | 233 | /** 234 | * 235 | * @param path 236 | * The path 237 | */ 238 | @JsonProperty("path") 239 | public void setPath(String path) { 240 | this.path = path; 241 | } 242 | 243 | public Component withPath(String path) { 244 | this.path = path; 245 | return this; 246 | } 247 | 248 | /** 249 | * 250 | * @return 251 | * The projectId 252 | */ 253 | @JsonProperty("projectId") 254 | public Integer getProjectId() { 255 | return projectId; 256 | } 257 | 258 | /** 259 | * 260 | * @param projectId 261 | * The projectId 262 | */ 263 | @JsonProperty("projectId") 264 | public void setProjectId(Integer projectId) { 265 | this.projectId = projectId; 266 | } 267 | 268 | public Component withProjectId(Integer projectId) { 269 | this.projectId = projectId; 270 | return this; 271 | } 272 | 273 | /** 274 | * 275 | * @return 276 | * The subProjectId 277 | */ 278 | @JsonProperty("subProjectId") 279 | public Integer getSubProjectId() { 280 | return subProjectId; 281 | } 282 | 283 | /** 284 | * 285 | * @param subProjectId 286 | * The subProjectId 287 | */ 288 | @JsonProperty("subProjectId") 289 | public void setSubProjectId(Integer subProjectId) { 290 | this.subProjectId = subProjectId; 291 | } 292 | 293 | public Component withSubProjectId(Integer subProjectId) { 294 | this.subProjectId = subProjectId; 295 | return this; 296 | } 297 | 298 | @Override 299 | public String toString() { 300 | return ToStringBuilder.reflectionToString(this); 301 | } 302 | 303 | @JsonAnyGetter 304 | public Map getAdditionalProperties() { 305 | return this.additionalProperties; 306 | } 307 | 308 | @JsonAnySetter 309 | public void setAdditionalProperty(String name, Object value) { 310 | this.additionalProperties.put(name, value); 311 | } 312 | 313 | public Component withAdditionalProperty(String name, Object value) { 314 | this.additionalProperties.put(name, value); 315 | return this; 316 | } 317 | 318 | } 319 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/sonarapi/resultbeans/Issues.java: -------------------------------------------------------------------------------- 1 | 2 | package com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans; 3 | 4 | import com.fasterxml.jackson.annotation.*; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | @JsonInclude(JsonInclude.Include.NON_NULL) 13 | @JsonPropertyOrder({ 14 | "total", 15 | "p", 16 | "ps", 17 | "paging", 18 | "issues", 19 | "components" 20 | }) 21 | public class Issues { 22 | 23 | @JsonProperty("total") 24 | private Integer total; 25 | @JsonProperty("p") 26 | private Integer p; 27 | @JsonProperty("ps") 28 | private Integer ps; 29 | @JsonProperty("paging") 30 | private Paging paging; 31 | @JsonProperty("issues") 32 | private List issues = new ArrayList(); 33 | @JsonProperty("components") 34 | private List components = new ArrayList(); 35 | @JsonIgnore 36 | private Map additionalProperties = new HashMap(); 37 | 38 | /** 39 | * 40 | * @return 41 | * The total 42 | */ 43 | @JsonProperty("total") 44 | public Integer getTotal() { 45 | return total; 46 | } 47 | 48 | /** 49 | * 50 | * @param total 51 | * The total 52 | */ 53 | @JsonProperty("total") 54 | public void setTotal(Integer total) { 55 | this.total = total; 56 | } 57 | 58 | public Issues withTotal(Integer total) { 59 | this.total = total; 60 | return this; 61 | } 62 | 63 | /** 64 | * 65 | * @return 66 | * The p 67 | */ 68 | @JsonProperty("p") 69 | public Integer getP() { 70 | return p; 71 | } 72 | 73 | /** 74 | * 75 | * @param p 76 | * The p 77 | */ 78 | @JsonProperty("p") 79 | public void setP(Integer p) { 80 | this.p = p; 81 | } 82 | 83 | public Issues withP(Integer p) { 84 | this.p = p; 85 | return this; 86 | } 87 | 88 | /** 89 | * 90 | * @return 91 | * The ps 92 | */ 93 | @JsonProperty("ps") 94 | public Integer getPs() { 95 | return ps; 96 | } 97 | 98 | /** 99 | * 100 | * @param ps 101 | * The ps 102 | */ 103 | @JsonProperty("ps") 104 | public void setPs(Integer ps) { 105 | this.ps = ps; 106 | } 107 | 108 | public Issues withPs(Integer ps) { 109 | this.ps = ps; 110 | return this; 111 | } 112 | 113 | /** 114 | * 115 | * @return 116 | * The paging 117 | */ 118 | @JsonProperty("paging") 119 | public Paging getPaging() { 120 | return paging; 121 | } 122 | 123 | /** 124 | * 125 | * @param paging 126 | * The paging 127 | */ 128 | @JsonProperty("paging") 129 | public void setPaging(Paging paging) { 130 | this.paging = paging; 131 | } 132 | 133 | public Issues withPaging(Paging paging) { 134 | this.paging = paging; 135 | return this; 136 | } 137 | 138 | /** 139 | * 140 | * @return 141 | * The issues 142 | */ 143 | @JsonProperty("issues") 144 | public List getIssues() { 145 | return issues; 146 | } 147 | 148 | /** 149 | * 150 | * @param issues 151 | * The issues 152 | */ 153 | @JsonProperty("issues") 154 | public void setIssues(List issues) { 155 | this.issues = issues; 156 | } 157 | 158 | public Issues withIssues(List issues) { 159 | this.issues = issues; 160 | return this; 161 | } 162 | 163 | /** 164 | * 165 | * @return 166 | * The components 167 | */ 168 | @JsonProperty("components") 169 | public List getComponents() { 170 | return components; 171 | } 172 | 173 | /** 174 | * 175 | * @param components 176 | * The components 177 | */ 178 | @JsonProperty("components") 179 | public void setComponents(List components) { 180 | this.components = components; 181 | } 182 | 183 | public Issues withComponents(List components) { 184 | this.components = components; 185 | return this; 186 | } 187 | 188 | @Override 189 | public String toString() { 190 | return ToStringBuilder.reflectionToString(this); 191 | } 192 | 193 | @JsonAnyGetter 194 | public Map getAdditionalProperties() { 195 | return this.additionalProperties; 196 | } 197 | 198 | @JsonAnySetter 199 | public void setAdditionalProperty(String name, Object value) { 200 | this.additionalProperties.put(name, value); 201 | } 202 | 203 | public Issues withAdditionalProperty(String name, Object value) { 204 | this.additionalProperties.put(name, value); 205 | return this; 206 | } 207 | 208 | } 209 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/sonarapi/resultbeans/Paging.java: -------------------------------------------------------------------------------- 1 | 2 | package com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans; 3 | 4 | import com.fasterxml.jackson.annotation.*; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | @JsonInclude(JsonInclude.Include.NON_NULL) 11 | @JsonPropertyOrder({ 12 | "pageIndex", 13 | "pageSize", 14 | "total" 15 | }) 16 | public class Paging { 17 | 18 | @JsonProperty("pageIndex") 19 | private Integer pageIndex; 20 | @JsonProperty("pageSize") 21 | private Integer pageSize; 22 | @JsonProperty("total") 23 | private Integer total; 24 | @JsonIgnore 25 | private Map additionalProperties = new HashMap(); 26 | 27 | /** 28 | * 29 | * @return 30 | * The pageIndex 31 | */ 32 | @JsonProperty("pageIndex") 33 | public Integer getPageIndex() { 34 | return pageIndex; 35 | } 36 | 37 | /** 38 | * 39 | * @param pageIndex 40 | * The pageIndex 41 | */ 42 | @JsonProperty("pageIndex") 43 | public void setPageIndex(Integer pageIndex) { 44 | this.pageIndex = pageIndex; 45 | } 46 | 47 | public Paging withPageIndex(Integer pageIndex) { 48 | this.pageIndex = pageIndex; 49 | return this; 50 | } 51 | 52 | /** 53 | * 54 | * @return 55 | * The pageSize 56 | */ 57 | @JsonProperty("pageSize") 58 | public Integer getPageSize() { 59 | return pageSize; 60 | } 61 | 62 | /** 63 | * 64 | * @param pageSize 65 | * The pageSize 66 | */ 67 | @JsonProperty("pageSize") 68 | public void setPageSize(Integer pageSize) { 69 | this.pageSize = pageSize; 70 | } 71 | 72 | public Paging withPageSize(Integer pageSize) { 73 | this.pageSize = pageSize; 74 | return this; 75 | } 76 | 77 | /** 78 | * 79 | * @return 80 | * The total 81 | */ 82 | @JsonProperty("total") 83 | public Integer getTotal() { 84 | return total; 85 | } 86 | 87 | /** 88 | * 89 | * @param total 90 | * The total 91 | */ 92 | @JsonProperty("total") 93 | public void setTotal(Integer total) { 94 | this.total = total; 95 | } 96 | 97 | public Paging withTotal(Integer total) { 98 | this.total = total; 99 | return this; 100 | } 101 | 102 | @Override 103 | public String toString() { 104 | return ToStringBuilder.reflectionToString(this); 105 | } 106 | 107 | @JsonAnyGetter 108 | public Map getAdditionalProperties() { 109 | return this.additionalProperties; 110 | } 111 | 112 | @JsonAnySetter 113 | public void setAdditionalProperty(String name, Object value) { 114 | this.additionalProperties.put(name, value); 115 | } 116 | 117 | public Paging withAdditionalProperty(String name, Object value) { 118 | this.additionalProperties.put(name, value); 119 | return this; 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/sonarapi/resultbeans/TextRange.java: -------------------------------------------------------------------------------- 1 | 2 | package com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans; 3 | 4 | import com.fasterxml.jackson.annotation.*; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | @JsonInclude(JsonInclude.Include.NON_NULL) 11 | @JsonPropertyOrder({ 12 | "startLine", 13 | "endLine", 14 | "startOffset", 15 | "endOffset" 16 | }) 17 | public class TextRange { 18 | 19 | @JsonProperty("startLine") 20 | private Integer startLine; 21 | @JsonProperty("endLine") 22 | private Integer endLine; 23 | @JsonProperty("startOffset") 24 | private Integer startOffset; 25 | @JsonProperty("endOffset") 26 | private Integer endOffset; 27 | @JsonIgnore 28 | private Map additionalProperties = new HashMap(); 29 | 30 | /** 31 | * 32 | * @return 33 | * The startLine 34 | */ 35 | @JsonProperty("startLine") 36 | public Integer getStartLine() { 37 | return startLine; 38 | } 39 | 40 | /** 41 | * 42 | * @param startLine 43 | * The startLine 44 | */ 45 | @JsonProperty("startLine") 46 | public void setStartLine(Integer startLine) { 47 | this.startLine = startLine; 48 | } 49 | 50 | public TextRange withStartLine(Integer startLine) { 51 | this.startLine = startLine; 52 | return this; 53 | } 54 | 55 | /** 56 | * 57 | * @return 58 | * The endLine 59 | */ 60 | @JsonProperty("endLine") 61 | public Integer getEndLine() { 62 | return endLine; 63 | } 64 | 65 | /** 66 | * 67 | * @param endLine 68 | * The endLine 69 | */ 70 | @JsonProperty("endLine") 71 | public void setEndLine(Integer endLine) { 72 | this.endLine = endLine; 73 | } 74 | 75 | public TextRange withEndLine(Integer endLine) { 76 | this.endLine = endLine; 77 | return this; 78 | } 79 | 80 | /** 81 | * 82 | * @return 83 | * The startOffset 84 | */ 85 | @JsonProperty("startOffset") 86 | public Integer getStartOffset() { 87 | return startOffset; 88 | } 89 | 90 | /** 91 | * 92 | * @param startOffset 93 | * The startOffset 94 | */ 95 | @JsonProperty("startOffset") 96 | public void setStartOffset(Integer startOffset) { 97 | this.startOffset = startOffset; 98 | } 99 | 100 | public TextRange withStartOffset(Integer startOffset) { 101 | this.startOffset = startOffset; 102 | return this; 103 | } 104 | 105 | /** 106 | * 107 | * @return 108 | * The endOffset 109 | */ 110 | @JsonProperty("endOffset") 111 | public Integer getEndOffset() { 112 | return endOffset; 113 | } 114 | 115 | /** 116 | * 117 | * @param endOffset 118 | * The endOffset 119 | */ 120 | @JsonProperty("endOffset") 121 | public void setEndOffset(Integer endOffset) { 122 | this.endOffset = endOffset; 123 | } 124 | 125 | public TextRange withEndOffset(Integer endOffset) { 126 | this.endOffset = endOffset; 127 | return this; 128 | } 129 | 130 | @Override 131 | public String toString() { 132 | return ToStringBuilder.reflectionToString(this); 133 | } 134 | 135 | @JsonAnyGetter 136 | public Map getAdditionalProperties() { 137 | return this.additionalProperties; 138 | } 139 | 140 | @JsonAnySetter 141 | public void setAdditionalProperty(String name, Object value) { 142 | this.additionalProperties.put(name, value); 143 | } 144 | 145 | public TextRange withAdditionalProperty(String name, Object value) { 146 | this.additionalProperties.put(name, value); 147 | return this; 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/sonarapi/resultbeans/User.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans; 2 | 3 | import com.fasterxml.jackson.annotation.*; 4 | import org.apache.commons.lang3.builder.ToStringBuilder; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | @JsonInclude(JsonInclude.Include.NON_NULL) 10 | @JsonPropertyOrder({ 11 | "login", 12 | "name" 13 | }) 14 | public class User { 15 | 16 | @JsonProperty("login") 17 | private String login; 18 | @JsonProperty("name") 19 | private String name; 20 | @JsonIgnore 21 | private Map additionalProperties = new HashMap(); 22 | 23 | @JsonProperty("login") 24 | public String getLogin() { 25 | return login; 26 | } 27 | 28 | @JsonProperty("login") 29 | public void setLogin(String login) { 30 | this.login = login; 31 | } 32 | 33 | @JsonProperty("name") 34 | public String getName() { 35 | return name; 36 | } 37 | 38 | @JsonProperty("name") 39 | public void setName(String name) { 40 | this.name = name; 41 | } 42 | 43 | @JsonAnyGetter 44 | public Map getAdditionalProperties() { 45 | return this.additionalProperties; 46 | } 47 | 48 | @JsonAnySetter 49 | public void setAdditionalProperty(String name, Object value) { 50 | this.additionalProperties.put(name, value); 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return new ToStringBuilder(this).append("login", login).append("name", name).append("additionalProperties", additionalProperties).toString(); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/sonarapi/resultbeans/Users.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans; 2 | 3 | import com.fasterxml.jackson.annotation.*; 4 | import org.apache.commons.lang3.builder.ToStringBuilder; 5 | 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | @JsonInclude(JsonInclude.Include.NON_NULL) 11 | @JsonPropertyOrder({ 12 | "paging", 13 | "users" 14 | }) 15 | public class Users { 16 | 17 | @JsonProperty("paging") 18 | private Paging paging; 19 | @JsonProperty("users") 20 | private List users = null; 21 | @JsonIgnore 22 | private Map additionalProperties = new HashMap(); 23 | 24 | @JsonProperty("paging") 25 | public Paging getPaging() { 26 | return paging; 27 | } 28 | 29 | @JsonProperty("paging") 30 | public void setPaging(Paging paging) { 31 | this.paging = paging; 32 | } 33 | 34 | @JsonProperty("users") 35 | public List getUsers() { 36 | return users; 37 | } 38 | 39 | @JsonProperty("users") 40 | public void setUsers(List users) { 41 | this.users = users; 42 | } 43 | 44 | @JsonAnyGetter 45 | public Map getAdditionalProperties() { 46 | return this.additionalProperties; 47 | } 48 | 49 | @JsonAnySetter 50 | public void setAdditionalProperty(String name, Object value) { 51 | this.additionalProperties.put(name, value); 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return new ToStringBuilder(this).append("paging", paging).append("users", users).append("additionalProperties", additionalProperties).toString(); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/stats/controller/SonarStatsController.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.stats.controller; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.stats.domain.SonarStatsRow; 4 | import com.thepracticaldeveloper.devgame.modules.stats.service.SonarStatsService; 5 | import com.thepracticaldeveloper.devgame.modules.users.dto.MessageResponseDTO; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.web.bind.annotation.DeleteMapping; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RequestMethod; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import java.util.Collection; 14 | 15 | @RestController 16 | @RequestMapping(value = {"/stats"}) 17 | public class SonarStatsController { 18 | private SonarStatsService sonarStatsService; 19 | 20 | @Autowired 21 | public SonarStatsController(final SonarStatsService sonarStatsService) { 22 | this.sonarStatsService = sonarStatsService; 23 | } 24 | 25 | @RequestMapping(value = {"/users", ""}, method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) 26 | public Collection statsHome() { 27 | return sonarStatsService.getSortedStatsPerUser(); 28 | } 29 | 30 | @DeleteMapping("/users") 31 | public MessageResponseDTO deleteAllStats() { 32 | return sonarStatsService.deleteAllStats(); 33 | } 34 | 35 | @RequestMapping(value = "/teams", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) 36 | public Collection statsTeams() { 37 | return sonarStatsService.getSortedStatsPerTeam(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/stats/domain/BadgeCard.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.stats.domain; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.mongodb.core.index.Indexed; 5 | import org.springframework.data.mongodb.core.mapping.Document; 6 | 7 | import java.time.Instant; 8 | 9 | @Document(collection = "badge-cards") 10 | public class BadgeCard { 11 | 12 | @Id 13 | private String id; 14 | @Indexed 15 | private String userId; 16 | @Indexed 17 | private String badgeKey; 18 | private Instant wonAt; 19 | private int score; 20 | 21 | public BadgeCard() { 22 | } 23 | 24 | public BadgeCard(String id, String userId, String badgeKey, Instant wonAt, int score) { 25 | this.id = id; 26 | this.userId = userId; 27 | this.badgeKey = badgeKey; 28 | this.wonAt = wonAt; 29 | this.score = score; 30 | } 31 | 32 | public String getId() { 33 | return id; 34 | } 35 | 36 | public String getUserId() { 37 | return userId; 38 | } 39 | 40 | public String getBadgeKey() { 41 | return badgeKey; 42 | } 43 | 44 | public Instant getWonAt() { 45 | return wonAt; 46 | } 47 | 48 | public int getScore() { 49 | return score; 50 | } 51 | 52 | @Override 53 | public boolean equals(Object o) { 54 | if (this == o) return true; 55 | if (o == null || getClass() != o.getClass()) return false; 56 | 57 | BadgeCard badgeCard = (BadgeCard) o; 58 | 59 | if (score != badgeCard.score) return false; 60 | if (id != null ? !id.equals(badgeCard.id) : badgeCard.id != null) return false; 61 | if (userId != null ? !userId.equals(badgeCard.userId) : badgeCard.userId != null) return false; 62 | if (badgeKey != null ? !badgeKey.equals(badgeCard.badgeKey) : badgeCard.badgeKey != null) return false; 63 | return wonAt != null ? wonAt.equals(badgeCard.wonAt) : badgeCard.wonAt == null; 64 | } 65 | 66 | @Override 67 | public int hashCode() { 68 | int result = id != null ? id.hashCode() : 0; 69 | result = 31 * result + (userId != null ? userId.hashCode() : 0); 70 | result = 31 * result + (badgeKey != null ? badgeKey.hashCode() : 0); 71 | result = 31 * result + (wonAt != null ? wonAt.hashCode() : 0); 72 | result = 31 * result + score; 73 | return result; 74 | } 75 | 76 | @Override 77 | public String toString() { 78 | return "BadgeCard{" + 79 | "id='" + id + '\'' + 80 | ", userId='" + userId + '\'' + 81 | ", badgeKey='" + badgeKey + '\'' + 82 | ", wonAt=" + wonAt + 83 | ", score=" + score + 84 | '}'; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/stats/domain/ScoreCard.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.stats.domain; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.mongodb.core.index.Indexed; 5 | import org.springframework.data.mongodb.core.mapping.Document; 6 | 7 | import java.time.Instant; 8 | 9 | @Document(collection = "score-cards") 10 | public class ScoreCard { 11 | 12 | @Id 13 | private String id; 14 | @Indexed 15 | private String sonarId; 16 | @Indexed 17 | private String userId; 18 | 19 | private Instant wonAt; 20 | private int score; 21 | private int paidDebtMinutes; 22 | private SeverityType severityType; 23 | 24 | public ScoreCard() { 25 | } 26 | 27 | public ScoreCard(String id, String sonarId, String userId, Instant wonAt, int score, int paidDebtMinutes, SeverityType severityType) { 28 | this.id = id; 29 | this.sonarId = sonarId; 30 | this.userId = userId; 31 | this.wonAt = wonAt; 32 | this.score = score; 33 | this.paidDebtMinutes = paidDebtMinutes; 34 | this.severityType = severityType; 35 | } 36 | 37 | public String getId() { 38 | return id; 39 | } 40 | 41 | public void setId(String id) { 42 | this.id = id; 43 | } 44 | 45 | public String getSonarId() { 46 | return sonarId; 47 | } 48 | 49 | public void setSonarId(String sonarId) { 50 | this.sonarId = sonarId; 51 | } 52 | 53 | public String getUserId() { 54 | return userId; 55 | } 56 | 57 | public void setUserId(String userId) { 58 | this.userId = userId; 59 | } 60 | 61 | public Instant getWonAt() { 62 | return wonAt; 63 | } 64 | 65 | public void setWonAt(Instant wonAt) { 66 | this.wonAt = wonAt; 67 | } 68 | 69 | public int getScore() { 70 | return score; 71 | } 72 | 73 | public void setScore(int score) { 74 | this.score = score; 75 | } 76 | 77 | public int getPaidDebtMinutes() { 78 | return paidDebtMinutes; 79 | } 80 | 81 | public void setPaidDebtMinutes(int paidDebtMinutes) { 82 | this.paidDebtMinutes = paidDebtMinutes; 83 | } 84 | 85 | public SeverityType getSeverityType() { 86 | return severityType; 87 | } 88 | 89 | public void setSeverityType(SeverityType severityType) { 90 | this.severityType = severityType; 91 | } 92 | 93 | @Override 94 | public boolean equals(Object o) { 95 | if (this == o) return true; 96 | if (o == null || getClass() != o.getClass()) return false; 97 | 98 | ScoreCard scoreCard = (ScoreCard) o; 99 | 100 | if (score != scoreCard.score) return false; 101 | if (paidDebtMinutes != scoreCard.paidDebtMinutes) return false; 102 | if (id != null ? !id.equals(scoreCard.id) : scoreCard.id != null) return false; 103 | if (sonarId != null ? !sonarId.equals(scoreCard.sonarId) : scoreCard.sonarId != null) return false; 104 | if (userId != null ? !userId.equals(scoreCard.userId) : scoreCard.userId != null) return false; 105 | if (wonAt != null ? !wonAt.equals(scoreCard.wonAt) : scoreCard.wonAt != null) return false; 106 | return severityType == scoreCard.severityType; 107 | } 108 | 109 | @Override 110 | public int hashCode() { 111 | int result = id != null ? id.hashCode() : 0; 112 | result = 31 * result + (sonarId != null ? sonarId.hashCode() : 0); 113 | result = 31 * result + (userId != null ? userId.hashCode() : 0); 114 | result = 31 * result + (wonAt != null ? wonAt.hashCode() : 0); 115 | result = 31 * result + score; 116 | result = 31 * result + paidDebtMinutes; 117 | result = 31 * result + (severityType != null ? severityType.hashCode() : 0); 118 | return result; 119 | } 120 | 121 | @Override 122 | public String toString() { 123 | return "ScoreCard{" + 124 | "id='" + id + '\'' + 125 | ", sonarId='" + sonarId + '\'' + 126 | ", userId='" + userId + '\'' + 127 | ", wonAt=" + wonAt + 128 | ", score=" + score + 129 | ", paidDebtMinutes=" + paidDebtMinutes + 130 | ", severityType=" + severityType + 131 | '}'; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/stats/domain/SeverityType.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.stats.domain; 2 | 3 | public enum SeverityType { 4 | BLOCKER(20), CRITICAL(10), MAJOR(5), MINOR(2), INFO(1); 5 | 6 | private final int score; 7 | 8 | SeverityType(final int score) { 9 | this.score = score; 10 | } 11 | 12 | public int getScore() { 13 | return score; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/stats/domain/SonarStats.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.stats.domain; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.badges.domain.SonarBadge; 4 | 5 | import java.util.List; 6 | 7 | public class SonarStats { 8 | 9 | private int totalPaidDebt; 10 | private int blocker; 11 | private int critical; 12 | private int major; 13 | private int minor; 14 | private int info; 15 | private int totalPoints; 16 | 17 | private List badges; 18 | 19 | public SonarStats(final int totalPaidDebt, final int blocker, final int critical, final int major, final int minor, final int info, 20 | final int totalPoints, 21 | final List badges) { 22 | super(); 23 | this.totalPaidDebt = totalPaidDebt; 24 | this.blocker = blocker; 25 | this.critical = critical; 26 | this.major = major; 27 | this.minor = minor; 28 | this.info = info; 29 | this.badges = badges; 30 | this.totalPoints = totalPoints; 31 | } 32 | 33 | public int getTotalPaidDebt() { 34 | return totalPaidDebt; 35 | } 36 | 37 | public int getBlocker() { 38 | return blocker; 39 | } 40 | 41 | public int getCritical() { 42 | return critical; 43 | } 44 | 45 | public int getMajor() { 46 | return major; 47 | } 48 | 49 | public int getMinor() { 50 | return minor; 51 | } 52 | 53 | public int getInfo() { 54 | return info; 55 | } 56 | 57 | public int getTotalPoints() { 58 | return totalPoints; 59 | } 60 | 61 | public List getBadges() { 62 | return badges; 63 | } 64 | 65 | @Override 66 | public String toString() { 67 | return "SonarStats [totalPaidDebt=" + totalPaidDebt + ", blocker=" + blocker + ", critical=" + critical + ", major=" + major + ", minor=" + 68 | minor + ", info=" + info + ", totalPoints=" 69 | + totalPoints + ", badges=" + badges + "]"; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/stats/domain/SonarStatsRow.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.stats.domain; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.badges.domain.SonarBadge; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collection; 7 | 8 | public final class SonarStatsRow { 9 | 10 | private String userAlias; 11 | private String userTeam; 12 | private int totalPoints; 13 | private int totalPaidDebt; 14 | private int blocker; 15 | private int critical; 16 | private int major; 17 | private int minor; 18 | private int info; 19 | private Collection badges; 20 | 21 | public SonarStatsRow(String userAlias, String userTeam, int totalPoints, int totalPaidDebt, int blocker, int critical, int major, int minor, int info, Collection badges) { 22 | super(); 23 | this.userAlias = userAlias; 24 | this.userTeam = userTeam; 25 | this.totalPoints = totalPoints; 26 | this.totalPaidDebt = totalPaidDebt; 27 | this.blocker = blocker; 28 | this.critical = critical; 29 | this.major = major; 30 | this.minor = minor; 31 | this.info = info; 32 | this.badges = badges; 33 | } 34 | 35 | public SonarStatsRow(final String userAlias, final String userTeam) { 36 | this(userAlias, userTeam, 0, 0, 0, 0, 0, 0, 0, new ArrayList<>()); 37 | } 38 | 39 | public void addTotalPoints(int points) { 40 | this.totalPoints += points; 41 | } 42 | 43 | public void addPaidDebt(int debt) { 44 | this.totalPaidDebt += debt; 45 | } 46 | 47 | public void addBlocker(int blocker) { 48 | this.blocker += blocker; 49 | } 50 | 51 | public void addCritical(int critical) { 52 | this.critical = critical; 53 | } 54 | 55 | public void addMajor(int major) { 56 | this.major = major; 57 | } 58 | 59 | public void addMinor(int minor) { 60 | this.minor = minor; 61 | } 62 | 63 | public void addInfo(int info) { 64 | this.info = info; 65 | } 66 | 67 | public void addBadge(SonarBadge badge) { 68 | this.badges.add(badge); 69 | } 70 | 71 | public String getUserAlias() { 72 | return userAlias; 73 | } 74 | 75 | public String getUserTeam() { 76 | return userTeam; 77 | } 78 | 79 | public int getTotalPoints() { 80 | return totalPoints; 81 | } 82 | 83 | public int getTotalPaidDebt() { 84 | return totalPaidDebt; 85 | } 86 | 87 | public int getBlocker() { 88 | return blocker; 89 | } 90 | 91 | public int getCritical() { 92 | return critical; 93 | } 94 | 95 | public int getMajor() { 96 | return major; 97 | } 98 | 99 | public int getMinor() { 100 | return minor; 101 | } 102 | 103 | public int getInfo() { 104 | return info; 105 | } 106 | 107 | public Collection getBadges() { 108 | return badges; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/stats/domain/SonarStatsRowBuilder.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.stats.domain; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.badges.domain.SonarBadge; 4 | 5 | import java.util.Collection; 6 | import java.util.Collections; 7 | 8 | public final class SonarStatsRowBuilder { 9 | 10 | private String userAlias; 11 | private String userTeam; 12 | private int totalPoints = 0; 13 | private int totalPaidDebt = 0; 14 | private int blocker = 0; 15 | private int critical = 0; 16 | private int major = 0; 17 | private int minor = 0; 18 | private int info = 0; 19 | private Collection badges = Collections.emptyList(); 20 | 21 | public SonarStatsRowBuilder(String userAlias, String userTeam) { 22 | this.userAlias = userAlias; 23 | this.userTeam = userTeam; 24 | } 25 | 26 | public SonarStatsRowBuilder withTotalPoints(int totalPoints) { 27 | this.totalPoints = totalPoints; 28 | return this; 29 | } 30 | 31 | public SonarStatsRowBuilder withTotalPaidDebt(int totalPaidDebt) { 32 | this.totalPaidDebt = totalPaidDebt; 33 | return this; 34 | } 35 | 36 | public SonarStatsRowBuilder withBlocker(int blocker) { 37 | this.blocker = blocker; 38 | return this; 39 | } 40 | 41 | public SonarStatsRowBuilder withCritical(int critical) { 42 | this.critical = critical; 43 | return this; 44 | } 45 | 46 | public SonarStatsRowBuilder withMajor(int major) { 47 | this.major = major; 48 | return this; 49 | } 50 | 51 | public SonarStatsRowBuilder withMinor(int minor) { 52 | this.minor = minor; 53 | return this; 54 | } 55 | 56 | public SonarStatsRowBuilder withInfo(int info) { 57 | this.info = info; 58 | return this; 59 | } 60 | 61 | public SonarStatsRowBuilder withBadges(Collection badges) { 62 | this.badges = badges; 63 | return this; 64 | } 65 | 66 | public SonarStatsRow createSonarStatsRow() { 67 | return new SonarStatsRow(userAlias, userTeam, totalPoints, totalPaidDebt, blocker, critical, major, minor, info, badges); 68 | } 69 | } -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/stats/repository/BadgeCardMongoRepository.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.stats.repository; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.stats.domain.BadgeCard; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | import java.util.stream.Stream; 7 | 8 | public interface BadgeCardMongoRepository extends CrudRepository { 9 | 10 | Stream findAllByUserId(final String userId); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/stats/repository/ScoreCardMongoRepository.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.stats.repository; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.stats.domain.ScoreCard; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | import java.util.Optional; 7 | import java.util.stream.Stream; 8 | 9 | public interface ScoreCardMongoRepository extends CrudRepository { 10 | 11 | Optional findBySonarId(final String sonarId); 12 | 13 | Stream findAllByUserId(final String userId); 14 | 15 | boolean existsBySonarId(final String sonarId); 16 | } 17 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/stats/service/BadgeService.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.stats.service; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans.Issue; 4 | 5 | import java.util.Set; 6 | 7 | public interface BadgeService { 8 | void saveNewBadgesFromIssueList(String userId, Set issues); 9 | } 10 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/stats/service/BadgeServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.stats.service; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.badges.calculators.BadgeCalculator; 4 | import com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans.Issue; 5 | import com.thepracticaldeveloper.devgame.modules.stats.domain.BadgeCard; 6 | import com.thepracticaldeveloper.devgame.modules.stats.repository.BadgeCardMongoRepository; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.List; 10 | import java.util.Set; 11 | import java.util.stream.Collectors; 12 | 13 | @Service 14 | public class BadgeServiceImpl implements BadgeService { 15 | 16 | private final List badgeCalculators; 17 | private final BadgeCardMongoRepository badgeRepository; 18 | 19 | public BadgeServiceImpl(final List badgeCalculators, 20 | final BadgeCardMongoRepository badgeRepository) { 21 | this.badgeCalculators = badgeCalculators; 22 | this.badgeRepository = badgeRepository; 23 | } 24 | 25 | @Override 26 | public void saveNewBadgesFromIssueList(final String userId, final Set issues) { 27 | final Set existingBadgeKeys = badgeRepository.findAllByUserId(userId) 28 | .map(BadgeCard::getBadgeKey).collect(Collectors.toSet()); 29 | // Filter out those badges already won 30 | final List applicableCalculators = badgeCalculators.stream() 31 | .filter(c -> !existingBadgeKeys.contains(c.badgeKey())).collect(Collectors.toList()); 32 | for (var badgeCalculator : applicableCalculators) { 33 | badgeCalculator.badgeFromIssueList(userId, issues).map(badgeRepository::save); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/stats/service/ScoreCardService.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.stats.service; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans.Issue; 4 | 5 | import java.util.Set; 6 | 7 | public interface ScoreCardService { 8 | void saveNewCardsFromIssueList(String userId, Set issues); 9 | } 10 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/stats/service/ScoreCardServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.stats.service; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans.Issue; 4 | import com.thepracticaldeveloper.devgame.modules.stats.domain.ScoreCard; 5 | import com.thepracticaldeveloper.devgame.modules.stats.domain.SeverityType; 6 | import com.thepracticaldeveloper.devgame.modules.stats.repository.ScoreCardMongoRepository; 7 | import com.thepracticaldeveloper.devgame.util.IssueDateFormatter; 8 | import com.thepracticaldeveloper.devgame.util.Utils; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.time.Duration; 15 | import java.time.Instant; 16 | import java.time.LocalDate; 17 | import java.util.Optional; 18 | import java.util.Set; 19 | import java.util.UUID; 20 | import java.util.stream.Collectors; 21 | 22 | @Service 23 | public class ScoreCardServiceImpl implements ScoreCardService { 24 | 25 | private static final String FIXED = "FIXED"; 26 | private static final Logger log = LoggerFactory.getLogger(ScoreCardServiceImpl.class); 27 | 28 | private final LocalDate legacyDate; 29 | private final LocalDate campaignStartDate; 30 | private final ScoreCardMongoRepository cardRepository; 31 | 32 | public ScoreCardServiceImpl(@Value("${game.dates.legacy}") final String legacyDate, 33 | @Value("${game.dates.campaignStart}") final String campaignStartDate, 34 | final ScoreCardMongoRepository cardRepository) { 35 | this.legacyDate = LocalDate.parse(legacyDate); 36 | this.campaignStartDate = LocalDate.parse(campaignStartDate); 37 | this.cardRepository = cardRepository; 38 | log.info("Legacy date is configured to {}", legacyDate); 39 | log.info("Campaign Start date is configured to {}", campaignStartDate); 40 | } 41 | 42 | @Override 43 | public void saveNewCardsFromIssueList(final String userId, final Set issues) { 44 | final Set fixedIssues = issues.stream().filter( 45 | i -> i.getResolution() != null && i.getResolution().equals(FIXED)). 46 | collect(Collectors.toSet()); 47 | log.trace("Fixed {} issues of {} issues assigned", fixedIssues.size(), issues.size()); 48 | 49 | // For the stats we only use those issues created before 'legacy date'... 50 | // ... and after 'campaign start date'. 51 | final Set issuesFilteredByLegacyDate = fixedIssues.stream() 52 | .filter(i -> IssueDateFormatter.format(i.getCreationDate()) 53 | .isBefore(legacyDate)) 54 | // closeDate might be null, see https://github.com/mechero/code-quality-game/issues/21 55 | .filter(i -> IssueDateFormatter.format(Optional.ofNullable(i.getCloseDate()).orElse(i.getUpdateDate())) 56 | .isAfter(campaignStartDate)) 57 | .collect(Collectors.toSet()); 58 | 59 | final long newResolvedIssues = issuesFilteredByLegacyDate.stream() 60 | // checks that the issue has not been mapped already to any user 61 | .filter(issue -> !cardRepository.existsBySonarId(issue.getKey())) 62 | .map(issue -> issueToScoreCard(userId, issue)) 63 | .map(cardRepository::save).count(); 64 | 65 | log.info("{} new score cards stored for user {}", newResolvedIssues, userId); 66 | } 67 | 68 | private static ScoreCard issueToScoreCard(final String userId, final Issue issue) { 69 | final long debt = Optional.ofNullable(issue.getDebt()) 70 | .map(Utils::durationTranslator).map(Duration::parse) 71 | .map(Duration::toMinutes).orElse(0L); 72 | final SeverityType severityType = Optional.ofNullable(issue.getSeverity()).map(SeverityType::valueOf) 73 | .orElse(SeverityType.MAJOR); 74 | return new ScoreCard(UUID.randomUUID().toString(), 75 | issue.getKey(), userId, Instant.now(), severityType.getScore(), (int) debt, severityType); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/stats/service/SonarStatsService.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.stats.service; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.stats.domain.SonarStatsRow; 4 | import com.thepracticaldeveloper.devgame.modules.users.dto.MessageResponseDTO; 5 | 6 | import java.util.Collection; 7 | 8 | public interface SonarStatsService { 9 | Collection getSortedStatsPerUser(); 10 | 11 | Collection getSortedStatsPerTeam(); 12 | 13 | MessageResponseDTO deleteAllStats(); 14 | } 15 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/stats/service/SonarStatsServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.stats.service; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.badges.domain.BadgeDetails; 4 | import com.thepracticaldeveloper.devgame.modules.badges.domain.SonarBadge; 5 | import com.thepracticaldeveloper.devgame.modules.stats.domain.SeverityType; 6 | import com.thepracticaldeveloper.devgame.modules.stats.domain.SonarStatsRow; 7 | import com.thepracticaldeveloper.devgame.modules.stats.repository.BadgeCardMongoRepository; 8 | import com.thepracticaldeveloper.devgame.modules.stats.repository.ScoreCardMongoRepository; 9 | import com.thepracticaldeveloper.devgame.modules.users.dao.UserMongoRepository; 10 | import com.thepracticaldeveloper.devgame.modules.users.domain.User; 11 | import com.thepracticaldeveloper.devgame.modules.users.dto.MessageResponseDTO; 12 | import org.apache.commons.logging.Log; 13 | import org.apache.commons.logging.LogFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.stereotype.Service; 16 | 17 | import java.util.*; 18 | import java.util.function.Function; 19 | import java.util.stream.Collectors; 20 | import java.util.stream.Stream; 21 | 22 | @Service 23 | final class SonarStatsServiceImpl implements SonarStatsService { 24 | 25 | private static final Log log = LogFactory.getLog(SonarStatsServiceImpl.class); 26 | 27 | private final UserMongoRepository userRepository; 28 | private final ScoreCardMongoRepository scoreCardMongoRepository; 29 | private final BadgeCardMongoRepository badgeCardMongoRepository; 30 | 31 | @Autowired 32 | public SonarStatsServiceImpl(final UserMongoRepository userRepository, 33 | final ScoreCardMongoRepository scoreCardMongoRepository, 34 | final BadgeCardMongoRepository badgeCardMongoRepository) { 35 | this.userRepository = userRepository; 36 | this.scoreCardMongoRepository = scoreCardMongoRepository; 37 | this.badgeCardMongoRepository = badgeCardMongoRepository; 38 | } 39 | 40 | @Override 41 | public List getSortedStatsPerUser() { 42 | final Stream users = userRepository.findAllUsersWithTeam(); 43 | return users.map(user -> { 44 | final SonarStatsRow statsRow = new SonarStatsRow(user.getAlias(), user.getTeam()); 45 | scoreCardMongoRepository.findAllByUserId(user.getId()).forEach(scoreCard -> { 46 | // The total score is proportional to the severity 47 | statsRow.addTotalPoints(scoreCard.getScore() * scoreCard.getSeverityType().getScore()); 48 | statsRow.addPaidDebt(scoreCard.getPaidDebtMinutes()); 49 | addSeverityTypeCounter(statsRow, scoreCard.getSeverityType()); 50 | }); 51 | badgeCardMongoRepository.findAllByUserId(user.getId()).forEach(badgeCard -> { 52 | final BadgeDetails badgeDetails = BadgeDetails.valueOf(badgeCard.getBadgeKey()); 53 | statsRow.addBadge(new SonarBadge(badgeDetails.getName(), badgeDetails.getDescription(), badgeCard.getScore())); 54 | statsRow.addTotalPoints(badgeCard.getScore()); 55 | }); 56 | return statsRow; 57 | }).sorted(Comparator.comparing(SonarStatsRow::getTotalPoints).reversed()).collect(Collectors.toList()); 58 | } 59 | 60 | private void addSeverityTypeCounter(SonarStatsRow statsRow, SeverityType severityType) { 61 | switch (severityType) { 62 | case BLOCKER: { 63 | statsRow.addBlocker(1); 64 | break; 65 | } 66 | case CRITICAL: { 67 | statsRow.addCritical(1); 68 | break; 69 | } 70 | case MAJOR: { 71 | statsRow.addMajor(1); 72 | break; 73 | } 74 | case MINOR: { 75 | statsRow.addMinor(1); 76 | break; 77 | } 78 | case INFO: { 79 | statsRow.addInfo(1); 80 | break; 81 | } 82 | } 83 | } 84 | 85 | @Override 86 | public Collection getSortedStatsPerTeam() { 87 | return getSortedStatsPerUser().stream().collect( 88 | Collectors.toMap(SonarStatsRow::getUserTeam, Function.identity(), SonarStatsServiceImpl::combine)) 89 | .values().stream() 90 | .sorted(Comparator.comparing(SonarStatsRow::getTotalPoints).reversed()) 91 | .collect(Collectors.toList()); 92 | } 93 | 94 | private static SonarStatsRow combine(final SonarStatsRow r1, final SonarStatsRow r2) { 95 | Set allBadges = new HashSet<>(); 96 | allBadges.addAll(r1.getBadges()); 97 | allBadges.addAll(r2.getBadges()); 98 | return new SonarStatsRow(r1.getUserAlias(), r1.getUserTeam(), r1.getTotalPoints() + r2.getTotalPoints(), 99 | r1.getTotalPaidDebt() + r2.getTotalPaidDebt(), r1.getBlocker() + r2.getBlocker(), 100 | r1.getCritical() + r2.getCritical(), r1.getMajor() + r2.getMajor(), 101 | r1.getMinor() + r2.getMinor(), r1.getInfo() + r2.getInfo(), allBadges); 102 | } 103 | 104 | @Override 105 | public MessageResponseDTO deleteAllStats() { 106 | try { 107 | badgeCardMongoRepository.deleteAll(); 108 | scoreCardMongoRepository.deleteAll(); 109 | return new MessageResponseDTO("Stats were deleted successfully"); 110 | } catch (final Exception e) { 111 | log.error("Error while trying to delete statistics", e); 112 | return new MessageResponseDTO("Error while trying to delete statistics", true); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/users/controller/TeamController.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.users.controller; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.users.dao.TeamMongoRepository; 4 | import com.thepracticaldeveloper.devgame.modules.users.domain.Team; 5 | import com.thepracticaldeveloper.devgame.modules.users.domain.User; 6 | import com.thepracticaldeveloper.devgame.modules.users.dto.CreateTeamDTO; 7 | import com.thepracticaldeveloper.devgame.modules.users.dto.MessageResponseDTO; 8 | import com.thepracticaldeveloper.devgame.modules.users.service.UserService; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | import java.util.List; 13 | import java.util.Optional; 14 | import java.util.UUID; 15 | 16 | @RestController 17 | @RequestMapping("/teams") 18 | public class TeamController { 19 | 20 | private final TeamMongoRepository repository; 21 | private final UserService userService; 22 | 23 | public TeamController(final TeamMongoRepository repository, final UserService userService) { 24 | this.repository = repository; 25 | this.userService = userService; 26 | } 27 | 28 | @GetMapping 29 | public ResponseEntity> getTeams() { 30 | return ResponseEntity.ok(repository.findAll()); 31 | } 32 | 33 | @PostMapping 34 | public ResponseEntity addTeam(@RequestBody final CreateTeamDTO teamDTO) { 35 | final Optional existingTeam = repository.findByName(teamDTO.getName()); 36 | if (existingTeam.isPresent()) { 37 | return ResponseEntity.unprocessableEntity().build(); 38 | } else { 39 | final Team team = repository.save(new Team(UUID.randomUUID().toString(), teamDTO.getName())); 40 | return ResponseEntity.ok(team); 41 | } 42 | } 43 | 44 | @DeleteMapping("/{id}") 45 | public ResponseEntity deleteTeam(@PathVariable("id") final String id) { 46 | final Optional existingTeam = repository.findById(id); 47 | if (existingTeam.isPresent()) { 48 | final List usersInTeam = userService.findUsersByTeam(existingTeam.get().getName()); 49 | if (usersInTeam.isEmpty()) { 50 | repository.deleteById(existingTeam.get().getId()); 51 | return ResponseEntity.ok(new MessageResponseDTO("Team deleted successfully")); 52 | } else { 53 | return ResponseEntity.unprocessableEntity() 54 | .body(new MessageResponseDTO("You can't delete a team if it still has players assigned to it.")); 55 | } 56 | } else { 57 | return ResponseEntity.notFound().build(); 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/users/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.users.controller; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.users.dao.UserMongoRepository; 4 | import com.thepracticaldeveloper.devgame.modules.users.domain.User; 5 | import com.thepracticaldeveloper.devgame.modules.users.dto.MessageResponseDTO; 6 | import com.thepracticaldeveloper.devgame.modules.users.dto.UserDTO; 7 | import com.thepracticaldeveloper.devgame.modules.users.service.YamlUserCreatorService; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.data.domain.Sort; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | import javax.validation.Valid; 15 | import java.util.*; 16 | 17 | @RestController 18 | @RequestMapping("/users") 19 | public class UserController { 20 | 21 | private static final Logger log = LoggerFactory.getLogger(UserController.class); 22 | 23 | private final UserMongoRepository userRepository; 24 | private final YamlUserCreatorService userCreatorService; 25 | 26 | public UserController(final UserMongoRepository userRepository, final YamlUserCreatorService userCreatorService) { 27 | this.userRepository = userRepository; 28 | this.userCreatorService = userCreatorService; 29 | } 30 | 31 | @GetMapping 32 | public ResponseEntity> getAll() { 33 | return ResponseEntity.ok(userRepository.findAll()); 34 | } 35 | 36 | @GetMapping(params = "assigned") 37 | public ResponseEntity> getAllAssignedUsersByTeam() { 38 | return ResponseEntity.ok(userRepository.findAll(Sort.by("team").ascending())); 39 | } 40 | 41 | @GetMapping(params = "unassigned") 42 | public ResponseEntity> getAllUnassignedUsers() { 43 | return ResponseEntity.ok(userRepository.findAllUnassigned(Sort.by("alias").ascending())); 44 | } 45 | 46 | @DeleteMapping(params = "unassigned") 47 | public ResponseEntity deleteAllUnassignedUsers() { 48 | userRepository.deleteAllByTeamIsNull(); 49 | return ResponseEntity.ok(new MessageResponseDTO("All unassigned users have been removed")); 50 | } 51 | 52 | public ResponseEntity createUser(@Valid @RequestBody final UserDTO userDTO) { 53 | final Optional existingUser = userRepository.findUserByLogin(userDTO.getLogin()); 54 | if (existingUser.isPresent()) { 55 | return ResponseEntity.unprocessableEntity().build(); 56 | } else { 57 | final User sonarUser = new User(UUID.randomUUID().toString(), userDTO.getLogin(), userDTO.getAlias(), userDTO.getTeam()); 58 | return ResponseEntity.ok(userRepository.save(sonarUser)); 59 | } 60 | } 61 | 62 | @PostMapping("/yaml") 63 | public ResponseEntity createUsers(@RequestBody final String yaml) { 64 | try { 65 | final String message = userCreatorService.createTeamsAndUsersFromYaml(yaml); 66 | return ResponseEntity.ok(message); 67 | } catch (final Exception e) { 68 | log.error("Error while trying to add YAML users, contents: {}", yaml, e); 69 | return ResponseEntity.unprocessableEntity().build(); 70 | } 71 | } 72 | 73 | @GetMapping(path = "/{id}") 74 | public ResponseEntity getUser(@PathVariable("id") final String id) { 75 | return userRepository.findById(id).map( 76 | ResponseEntity::ok 77 | ).orElse( 78 | ResponseEntity.notFound().build() 79 | ); 80 | } 81 | 82 | @PutMapping(path = "/{id}") 83 | public ResponseEntity updateUser(@PathVariable("id") final String id, 84 | @Valid @RequestBody final UserDTO userDTO) { 85 | log.info("Saving user {}", userDTO); 86 | return userRepository.findById(id).map( 87 | oldUser -> userRepository.save(new User(id, userDTO.getLogin(), userDTO.getAlias(), userDTO.getTeam())) 88 | ).map( 89 | ResponseEntity::ok 90 | ).orElse( 91 | ResponseEntity.notFound().build() 92 | ); 93 | } 94 | 95 | @DeleteMapping 96 | public ResponseEntity deleteAll() { 97 | userRepository.deleteAll(); 98 | return ResponseEntity.ok("All users deleted"); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/users/dao/TeamMongoRepository.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.users.dao; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.users.domain.Team; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface TeamMongoRepository extends CrudRepository { 9 | Optional findByName(String name); 10 | } 11 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/users/dao/UserMongoRepository.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.users.dao; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.users.domain.User; 4 | import org.springframework.data.domain.Sort; 5 | import org.springframework.data.mongodb.repository.Query; 6 | import org.springframework.data.repository.CrudRepository; 7 | 8 | import java.util.List; 9 | import java.util.Optional; 10 | import java.util.stream.Stream; 11 | 12 | public interface UserMongoRepository extends CrudRepository { 13 | 14 | Optional findUserByLogin(final String login); 15 | 16 | List findAllByTeam(final String team); 17 | 18 | @Query("{'team': {$not: {$eq: null}}}") 19 | Stream findAllUsersWithTeam(); 20 | 21 | @Query("{'team': {$not: {$eq: null}}}") 22 | Iterable findAll(final Sort sort); 23 | 24 | @Query("{'team': null}") 25 | Iterable findAllUnassigned(final Sort sort); 26 | 27 | void deleteAllByTeamIsNull(); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/users/domain/Team.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.users.domain; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.mongodb.core.mapping.Document; 5 | 6 | @Document(collection = "teams") 7 | public final class Team { 8 | 9 | @Id 10 | private String id; 11 | private String name; 12 | 13 | public Team() { 14 | } 15 | 16 | public Team(final String id, final String name) { 17 | this.id = id; 18 | this.name = name; 19 | } 20 | 21 | public String getId() { 22 | return id; 23 | } 24 | 25 | public String getName() { 26 | return name; 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return "Team{" + 32 | "id='" + id + '\'' + 33 | ", name='" + name + '\'' + 34 | '}'; 35 | } 36 | 37 | @Override 38 | public boolean equals(Object o) { 39 | if (this == o) return true; 40 | if (o == null || getClass() != o.getClass()) return false; 41 | 42 | Team team = (Team) o; 43 | 44 | if (id != null ? !id.equals(team.id) : team.id != null) return false; 45 | return name != null ? name.equals(team.name) : team.name == null; 46 | } 47 | 48 | @Override 49 | public int hashCode() { 50 | int result = id != null ? id.hashCode() : 0; 51 | result = 31 * result + (name != null ? name.hashCode() : 0); 52 | return result; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/users/domain/User.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.users.domain; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.mongodb.core.index.Indexed; 5 | import org.springframework.data.mongodb.core.mapping.Document; 6 | 7 | @Document(collection = "users") 8 | public final class User { 9 | 10 | @Id 11 | private String id; 12 | @Indexed 13 | private String login; 14 | private String alias; 15 | @Indexed 16 | private String team; 17 | 18 | public User() { 19 | } 20 | 21 | public User(String id, String login, String alias, String team) { 22 | super(); 23 | this.id = id; 24 | this.login = login; 25 | this.alias = alias; 26 | this.team = team; 27 | } 28 | 29 | public User(String id) { 30 | this(id, null, null, null); 31 | } 32 | 33 | public String getId() { 34 | return id; 35 | } 36 | 37 | public String getAlias() { 38 | return alias; 39 | } 40 | 41 | public String getTeam() { 42 | return team; 43 | } 44 | 45 | public String getLogin() { 46 | return login; 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return "SonarUser{" + 52 | "id='" + id + '\'' + 53 | ", login='" + login + '\'' + 54 | ", alias='" + alias + '\'' + 55 | ", team='" + team + '\'' + 56 | '}'; 57 | } 58 | 59 | @Override 60 | public boolean equals(Object o) { 61 | if (this == o) return true; 62 | if (o == null || getClass() != o.getClass()) return false; 63 | 64 | User sonarUser = (User) o; 65 | 66 | if (id != null ? !id.equals(sonarUser.id) : sonarUser.id != null) return false; 67 | if (login != null ? !login.equals(sonarUser.login) : sonarUser.login != null) return false; 68 | if (alias != null ? !alias.equals(sonarUser.alias) : sonarUser.alias != null) return false; 69 | return team != null ? team.equals(sonarUser.team) : sonarUser.team == null; 70 | } 71 | 72 | @Override 73 | public int hashCode() { 74 | int result = id != null ? id.hashCode() : 0; 75 | result = 31 * result + (login != null ? login.hashCode() : 0); 76 | result = 31 * result + (alias != null ? alias.hashCode() : 0); 77 | result = 31 * result + (team != null ? team.hashCode() : 0); 78 | return result; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/users/dto/CreateTeamDTO.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.users.dto; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | 5 | public class CreateTeamDTO { 6 | 7 | @NotBlank 8 | private String name; 9 | 10 | public CreateTeamDTO() { 11 | } 12 | 13 | public CreateTeamDTO(String name) { 14 | this.name = name; 15 | } 16 | 17 | public String getName() { 18 | return name; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/users/dto/MessageResponseDTO.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.users.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public final class MessageResponseDTO { 6 | @JsonProperty("message") 7 | private final String message; 8 | 9 | @JsonProperty("error") 10 | private final boolean error; 11 | 12 | public MessageResponseDTO(final String message, final boolean error) { 13 | this.message = message; 14 | this.error = error; 15 | } 16 | 17 | public MessageResponseDTO(final String message) { 18 | this(message, false); 19 | } 20 | 21 | @Override 22 | public String toString() { 23 | return "MessageResponseDTO{" + 24 | "message='" + message + '\'' + 25 | ", error=" + error + 26 | '}'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/users/dto/TeamsAndUsersDTO.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.users.dto; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | public class TeamsAndUsersDTO { 7 | private List teams; 8 | 9 | public TeamsAndUsersDTO() { 10 | } 11 | 12 | public TeamsAndUsersDTO(final List teams) { 13 | this.teams = teams; 14 | } 15 | 16 | public List getTeams() { 17 | return teams; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return "TeamsAndUsersDTO{" + 23 | "teams=" + teams + 24 | '}'; 25 | } 26 | 27 | public static class TeamAndUserDTO { 28 | private String name; 29 | private Map users; 30 | 31 | public TeamAndUserDTO() { 32 | } 33 | 34 | public TeamAndUserDTO(String name, Map users) { 35 | this.name = name; 36 | this.users = users; 37 | } 38 | 39 | public String getName() { 40 | return name; 41 | } 42 | 43 | public Map getUsers() { 44 | return users; 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return "TeamAndUserDTO{" + 50 | "name='" + name + '\'' + 51 | ", users=" + users + 52 | '}'; 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/users/dto/UserDTO.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.users.dto; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | 5 | public class UserDTO { 6 | 7 | @NotBlank 8 | private String alias; 9 | @NotBlank 10 | private String login; 11 | private String team; 12 | 13 | public UserDTO() { 14 | } 15 | 16 | public UserDTO(final String alias, final String login, final String team) { 17 | this.alias = alias; 18 | this.login = login; 19 | this.team = team; 20 | } 21 | 22 | public String getAlias() { 23 | return alias; 24 | } 25 | 26 | public String getLogin() { 27 | return login; 28 | } 29 | 30 | public String getTeam() { 31 | return team; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return "UserDTO{" + 37 | "alias='" + alias + '\'' + 38 | ", login='" + login + '\'' + 39 | ", team='" + team + '\'' + 40 | '}'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/users/service/SonarUsersRetriever.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.users.service; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.configuration.service.SonarServerConfigurationService; 4 | import com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans.Paging; 5 | import com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans.Users; 6 | import com.thepracticaldeveloper.devgame.modules.users.dao.UserMongoRepository; 7 | import com.thepracticaldeveloper.devgame.modules.users.domain.User; 8 | import com.thepracticaldeveloper.devgame.util.ApiHttpUtils; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.http.*; 13 | import org.springframework.scheduling.annotation.EnableAsync; 14 | import org.springframework.scheduling.annotation.EnableScheduling; 15 | import org.springframework.scheduling.annotation.Scheduled; 16 | import org.springframework.stereotype.Service; 17 | import org.springframework.web.client.HttpClientErrorException; 18 | import org.springframework.web.client.RestTemplate; 19 | import org.springframework.web.util.UriComponentsBuilder; 20 | 21 | import java.net.URI; 22 | import java.util.*; 23 | 24 | @Service 25 | @EnableAsync 26 | @EnableScheduling 27 | final class SonarUsersRetriever { 28 | 29 | private static final Logger log = LoggerFactory.getLogger(SonarUsersRetriever.class); 30 | private static final String GET_USERS_URL = "/api/users/search?p={page}&ps={pageSize}"; 31 | private static final String GET_USERS_BY_ORG_URL = "/api/organizations/search_members?organization={organization}&p={page}&ps={pageSize}"; 32 | 33 | private final UserMongoRepository repository; 34 | private final SonarServerConfigurationService configurationService; 35 | private final boolean shouldFilterByOrganization; 36 | 37 | SonarUsersRetriever(final UserMongoRepository repository, 38 | final SonarServerConfigurationService configurationService) { 39 | this.repository = repository; 40 | this.configurationService = configurationService; 41 | this.shouldFilterByOrganization = StringUtils.isNotEmpty(configurationService.getConfiguration().getToken()) && 42 | StringUtils.isNotEmpty(configurationService.getConfiguration().getOrganization()); 43 | } 44 | 45 | @Scheduled(fixedRate = 30 * 60000) 46 | public void retrieveData() { 47 | int pageIndex = 1; 48 | int pageTotal = 1; 49 | int pageSize; 50 | Set users = new HashSet<>(); 51 | int totalProcessed = 0; 52 | while (totalProcessed == 0 || totalProcessed < pageTotal) { 53 | log.trace("Retrieving users, page " + pageIndex); 54 | RestTemplate restTemplate = new RestTemplate(); 55 | HttpEntity request = new HttpEntity<>(getHeaders()); 56 | URI uri = buildGetUsersURI(pageIndex); 57 | log.trace("URI: " + uri); 58 | try { 59 | ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, request, Users.class); 60 | Paging paging = response.getBody().getPaging(); 61 | pageTotal = paging.getTotal(); 62 | pageSize = paging.getPageSize(); 63 | users.addAll(response.getBody().getUsers()); 64 | pageIndex++; 65 | totalProcessed += pageSize; 66 | if (pageSize == 0) break; 67 | } catch (final HttpClientErrorException httpEx) { 68 | if (httpEx.getStatusCode().is4xxClientError()) { 69 | log.warn("Server responded with client error status: {} {}", 70 | httpEx.getRawStatusCode(), httpEx.getResponseBodyAsString()); 71 | break; 72 | } 73 | } catch (final Exception e) { 74 | log.error("Error while trying to retrieve users from SonarQube, page {}", pageIndex, e); 75 | break; 76 | } 77 | } 78 | final int addedUsers = processUserList(users); 79 | log.info("Total users processed: {}", totalProcessed); 80 | log.info("Total added users: {}", addedUsers); 81 | 82 | } 83 | 84 | private int processUserList(final Set users) { 85 | var addedUsers = 0; 86 | for (final com.thepracticaldeveloper.devgame.modules.sonarapi.resultbeans.User user : users) { 87 | final Optional matchingUser = repository.findUserByLogin(user.getLogin()); 88 | if (!matchingUser.isPresent()) { 89 | log.info("Adding new user: {}", user.getLogin()); 90 | repository.save( 91 | new User(UUID.randomUUID().toString(), user.getLogin(), user.getName(), null) 92 | ); 93 | addedUsers++; 94 | } 95 | } 96 | return addedUsers; 97 | } 98 | 99 | private URI buildGetUsersURI(final int pageIndex) { 100 | if (shouldFilterByOrganization) { 101 | return UriComponentsBuilder.fromHttpUrl(configurationService.getConfiguration().getUrl() + GET_USERS_BY_ORG_URL) 102 | .buildAndExpand(configurationService.getConfiguration().getOrganization(), pageIndex, 500) 103 | .toUri(); 104 | } else { 105 | return UriComponentsBuilder.fromHttpUrl(configurationService.getConfiguration().getUrl() + GET_USERS_URL) 106 | .buildAndExpand(pageIndex, 500) 107 | .toUri(); 108 | } 109 | } 110 | 111 | private HttpHeaders getHeaders() { 112 | HttpHeaders httpHeaders; 113 | var token = configurationService.getConfiguration().getToken(); 114 | if (token != null && !token.trim().isEmpty()) { 115 | httpHeaders = (ApiHttpUtils.getHeaders(token)); 116 | } else { 117 | httpHeaders = new HttpHeaders(); 118 | } 119 | httpHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8)); 120 | return httpHeaders; 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/users/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.users.service; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.users.domain.User; 4 | 5 | import java.util.List; 6 | import java.util.stream.Stream; 7 | 8 | public interface UserService { 9 | Stream getAllActiveUsers(); 10 | List findUsersByTeam(String teamName); 11 | } 12 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/users/service/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.users.service; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.users.dao.UserMongoRepository; 4 | import com.thepracticaldeveloper.devgame.modules.users.domain.User; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.util.List; 8 | import java.util.stream.Stream; 9 | 10 | @Service 11 | public class UserServiceImpl implements UserService { 12 | 13 | private final UserMongoRepository userRepository; 14 | 15 | public UserServiceImpl(final UserMongoRepository userRepository) { 16 | this.userRepository = userRepository; 17 | } 18 | 19 | @Override 20 | public Stream getAllActiveUsers() { 21 | return userRepository.findAllUsersWithTeam(); 22 | } 23 | 24 | @Override 25 | public List findUsersByTeam(final String teamName) { 26 | return userRepository.findAllByTeam(teamName); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/modules/users/service/YamlUserCreatorService.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.users.service; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 5 | import com.thepracticaldeveloper.devgame.modules.users.dao.UserMongoRepository; 6 | import com.thepracticaldeveloper.devgame.modules.users.dao.TeamMongoRepository; 7 | import com.thepracticaldeveloper.devgame.modules.users.domain.User; 8 | import com.thepracticaldeveloper.devgame.modules.users.domain.Team; 9 | import com.thepracticaldeveloper.devgame.modules.users.dto.TeamsAndUsersDTO; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.io.IOException; 15 | import java.util.*; 16 | 17 | @Service 18 | public class YamlUserCreatorService { 19 | 20 | private static final Logger log = LoggerFactory.getLogger(YamlUserCreatorService.class); 21 | private static final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory()); 22 | 23 | private final UserMongoRepository userRepository; 24 | private final TeamMongoRepository teamRepository; 25 | 26 | public YamlUserCreatorService(final UserMongoRepository userRepository, final TeamMongoRepository teamRepository) { 27 | this.userRepository = userRepository; 28 | this.teamRepository = teamRepository; 29 | } 30 | 31 | public String createTeamsAndUsersFromYaml(final String yaml) throws IOException { 32 | try { 33 | final TeamsAndUsersDTO teamsAndUsersDTO = yamlMapper.readValue(yaml, TeamsAndUsersDTO.class); 34 | final Set teams = new HashSet<>(); 35 | final List addedUsers = new ArrayList<>(); 36 | final List addedTeams = new ArrayList<>(); 37 | // Add users 38 | for (TeamsAndUsersDTO.TeamAndUserDTO dto : teamsAndUsersDTO.getTeams()) { 39 | teams.add(dto.getName()); 40 | for (Map.Entry keyValue : dto.getUsers().entrySet()) { 41 | final Optional existingUser = userRepository.findUserByLogin(keyValue.getKey()); 42 | if (existingUser.isPresent()) { 43 | if (!existingUser.get().getAlias().equals(keyValue.getValue()) || 44 | !existingUser.get().getTeam().equals(dto.getName())) { 45 | userRepository.save(new User(existingUser.get().getId(), 46 | keyValue.getKey(), keyValue.getValue(), dto.getName())); 47 | addedUsers.add(keyValue.getKey()); 48 | log.info("User updated: {}", keyValue.getKey()); 49 | } else { 50 | log.info("Skipping user from YAML file because it already exists: {}", keyValue); 51 | } 52 | } else { 53 | final User sonarUser = new User(UUID.randomUUID().toString(), keyValue.getKey(), keyValue.getValue(), dto.getName()); 54 | userRepository.save(sonarUser); 55 | addedUsers.add(sonarUser.getLogin()); 56 | } 57 | } 58 | } 59 | // Add teams 60 | for (String team : teams) { 61 | final Optional existingTeam = teamRepository.findByName(team); 62 | if (!existingTeam.isPresent()) { 63 | teamRepository.save(new Team(UUID.randomUUID().toString(), team)); 64 | addedTeams.add(team); 65 | } 66 | } 67 | return String.format("Added teams [%s]: %s / Added or modified users [%s]: %s", 68 | addedTeams.size(), addedTeams, addedUsers.size(), addedUsers.toString()); 69 | } catch (final IOException e) { 70 | log.error("Error while trying to add YAML users, contents: {}", yaml, e); 71 | throw e; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/util/ApiHttpUtils.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.util; 2 | 3 | import org.apache.commons.codec.binary.Base64; 4 | import org.springframework.http.HttpHeaders; 5 | 6 | public final class ApiHttpUtils { 7 | 8 | private ApiHttpUtils() { 9 | } 10 | 11 | public static HttpHeaders getHeaders(String token) { 12 | HttpHeaders headers = new HttpHeaders(); 13 | String creds = new String(Base64.encodeBase64((token + ":").getBytes())); 14 | headers.add("Authorization", "Basic " + creds); 15 | headers.add("Accept", "application/json"); 16 | return headers; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/util/IssueDateFormatter.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.util; 2 | 3 | import java.time.LocalDate; 4 | import java.time.OffsetDateTime; 5 | import java.time.ZoneOffset; 6 | import java.time.format.DateTimeFormatter; 7 | 8 | public final class IssueDateFormatter { 9 | private static DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"); 10 | 11 | private IssueDateFormatter() { 12 | } 13 | 14 | public static LocalDate format(final String s) { 15 | return LocalDate.parse(s, f); 16 | } 17 | 18 | public static String toIssueDate(final LocalDate date) { 19 | final OffsetDateTime dateTime = date.atTime(0, 0, 0).atOffset(ZoneOffset.UTC); 20 | return f.format(dateTime); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /sonar-connector/src/main/java/com/thepracticaldeveloper/devgame/util/Utils.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.util; 2 | 3 | public final class Utils { 4 | 5 | private Utils() { 6 | } 7 | 8 | public static String durationTranslator(String sonarDuration) { 9 | String daysPart = "P" + (sonarDuration.contains("d") ? sonarDuration.substring(0, sonarDuration.indexOf('d')) + "D" : ""); 10 | String timePart = (sonarDuration.contains("d") ? sonarDuration.substring(sonarDuration.indexOf('d') + 1) : sonarDuration); 11 | return daysPart + (timePart.isEmpty() ? "" : "T" + timePart.replaceAll("min", "M").replaceAll("h", "H")); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /sonar-connector/src/main/resources/config/application.yml: -------------------------------------------------------------------------------- 1 | sonar: 2 | server: https://sonarcloud.io 3 | # If you specify organization, users will be filtered by those belonging to it. 4 | # This is useful when the server is shared (like SonarCloud). You must provide a token in that case. 5 | organization: thepracticaldeveloper 6 | token: 7 | 8 | game: 9 | dates: 10 | legacy: 2018-01-21 11 | earlyBird: 2018-01-30 12 | campaignStart: 1970-01-01 13 | code: 14 | 15 | spring: 16 | data: 17 | mongodb: 18 | database: code_quality_game 19 | 20 | logging: 21 | level: 22 | com.thepracticaldeveloper.devgame: TRACE 23 | -------------------------------------------------------------------------------- /sonar-connector/src/main/resources/data/users.yml: -------------------------------------------------------------------------------- 1 | teams: 2 | - name: Team PlaneaConLena.com 3 | users: 4 | scg01@github: Lena 5 | anotheruser@github: Nice Automat 6 | - name: Team TPD.io 7 | users: 8 | mechero@github: Mechero 9 | scg02@github: Cool Robot 10 | -------------------------------------------------------------------------------- /sonar-connector/src/test/java/com/thepracticaldeveloper/devgame/app/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.app; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.stats.controller.SonarStatsController; 4 | import com.thepracticaldeveloper.devgame.modules.stats.service.SonarStatsService; 5 | import org.mockito.Mockito; 6 | import org.springframework.context.annotation.Bean; 7 | 8 | public class ApplicationTest { 9 | 10 | @Bean 11 | public SonarStatsService service() { 12 | return Mockito.mock(SonarStatsService.class); 13 | } 14 | 15 | @Bean 16 | public SonarStatsController controller(SonarStatsService service) { 17 | return new SonarStatsController(service); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /sonar-connector/src/test/java/com/thepracticaldeveloper/devgame/modules/configuration/service/SonarServerConfigurationServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.modules.configuration.service; 2 | 3 | import com.thepracticaldeveloper.devgame.modules.configuration.dao.SonarServerConfigurationDao; 4 | import com.thepracticaldeveloper.devgame.modules.configuration.domain.SonarServerConfiguration; 5 | import com.thepracticaldeveloper.devgame.modules.configuration.domain.sonar.SonarAuthenticationResponse; 6 | import com.thepracticaldeveloper.devgame.modules.configuration.domain.sonar.SonarServerStatus; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.mockito.Mock; 10 | import org.mockito.MockitoAnnotations; 11 | import org.springframework.http.HttpEntity; 12 | import org.springframework.http.HttpMethod; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.web.client.HttpClientErrorException; 16 | import org.springframework.web.client.ResourceAccessException; 17 | import org.springframework.web.client.RestTemplate; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | import static org.junit.Assert.assertFalse; 21 | import static org.junit.Assert.assertTrue; 22 | import static org.mockito.ArgumentMatchers.any; 23 | import static org.mockito.ArgumentMatchers.eq; 24 | import static org.mockito.Mockito.when; 25 | 26 | public class SonarServerConfigurationServiceTest { 27 | 28 | @Mock 29 | private SonarServerConfigurationDao sonarServerConfigurationDaoMock; 30 | 31 | @Mock 32 | private RestTemplate restTemplateMock; 33 | 34 | private SonarServerConfigurationService configurationService; 35 | private SonarServerConfiguration config; 36 | 37 | @Before 38 | public void setUp() { 39 | MockitoAnnotations.initMocks(this); 40 | configurationService = new SonarServerConfigurationServiceImpl(sonarServerConfigurationDaoMock, restTemplateMock); 41 | config = new SonarServerConfiguration("http://localhost:9000", "token", null); 42 | } 43 | 44 | @Test 45 | public void testConnectsToServer() { 46 | final SonarServerStatus body = new SonarServerStatus(SonarServerStatus.Key.UP); 47 | when(restTemplateMock.exchange(any(String.class), eq(HttpMethod.GET), any(HttpEntity.class), eq(SonarServerStatus.class))) 48 | .thenReturn(new ResponseEntity<>(body, HttpStatus.OK)); 49 | final SonarServerStatus serverStatus = configurationService.checkServerDetails(config); 50 | assertEquals(SonarServerStatus.STATUS_UP, serverStatus.getStatus()); 51 | } 52 | 53 | @Test 54 | public void testServerIsDown() { 55 | when(restTemplateMock.exchange(any(String.class), eq(HttpMethod.GET), any(HttpEntity.class), eq(SonarServerStatus.class))) 56 | .thenThrow(new ResourceAccessException("Can't connect")); 57 | final SonarServerStatus serverStatus = configurationService.checkServerDetails(config); 58 | assertEquals(SonarServerStatus.Key.CONNECTION_ERROR.toString(), serverStatus.getStatus()); 59 | } 60 | 61 | @Test 62 | public void testBadAuthentication() { 63 | when(restTemplateMock.exchange(any(String.class), eq(HttpMethod.GET), any(HttpEntity.class), eq(SonarServerStatus.class))) 64 | .thenThrow(new HttpClientErrorException(HttpStatus.UNAUTHORIZED)); 65 | final SonarServerStatus serverStatus = configurationService.checkServerDetails(config); 66 | assertEquals(SonarServerStatus.Key.UNAUTHORIZED.toString(), serverStatus.getStatus()); 67 | } 68 | 69 | @Test 70 | public void testUnknownError() { 71 | when(restTemplateMock.exchange(any(String.class), eq(HttpMethod.GET), any(HttpEntity.class), eq(SonarServerStatus.class))) 72 | .thenThrow(new HttpClientErrorException(HttpStatus.BAD_REQUEST)); 73 | final SonarServerStatus serverStatus = configurationService.checkServerDetails(config); 74 | assertEquals(SonarServerStatus.Key.UNKNOWN_ERROR.toString(), serverStatus.getStatus()); 75 | } 76 | 77 | @Test 78 | public void testCheckAuthenticationIsValid() { 79 | final SonarAuthenticationResponse response = new SonarAuthenticationResponse(true); 80 | when(restTemplateMock.exchange(any(String.class), eq(HttpMethod.GET), any(HttpEntity.class), eq(SonarAuthenticationResponse.class))) 81 | .thenReturn(new ResponseEntity<>(response, HttpStatus.OK)); 82 | configurationService.checkServerAuthentication(config); 83 | assertTrue(response.isValid()); 84 | } 85 | 86 | @Test 87 | public void testCheckAuthenticationIsInvalid() { 88 | final SonarAuthenticationResponse response = new SonarAuthenticationResponse(false); 89 | when(restTemplateMock.exchange(any(String.class), eq(HttpMethod.GET), any(HttpEntity.class), eq(SonarAuthenticationResponse.class))) 90 | .thenReturn(new ResponseEntity<>(response, HttpStatus.OK)); 91 | assertFalse(response.isValid()); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /sonar-connector/src/test/java/com/thepracticaldeveloper/devgame/util/ApiHttpUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.util; 2 | 3 | import org.apache.commons.codec.binary.Base64; 4 | import org.junit.Test; 5 | import org.springframework.http.HttpHeaders; 6 | import org.springframework.http.MediaType; 7 | 8 | import java.util.Collections; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | 12 | public class ApiHttpUtilsTest { 13 | 14 | public static final String TOKEN_VALUE = "tokenvalue"; 15 | 16 | @Test 17 | public void testHeaders() { 18 | final HttpHeaders headers = ApiHttpUtils.getHeaders(TOKEN_VALUE); 19 | final String token64 = new String(Base64.encodeBase64((TOKEN_VALUE + ":").getBytes())); 20 | assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON), headers.getAccept()); 21 | assertEquals("Basic " + token64, headers.get("Authorization").get(0)); 22 | } 23 | } -------------------------------------------------------------------------------- /sonar-connector/src/test/java/com/thepracticaldeveloper/devgame/util/UtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.thepracticaldeveloper.devgame.util; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import org.junit.Test; 6 | 7 | public class UtilsTest { 8 | 9 | @Test 10 | public void testDateWithDaysWorks() { 11 | String input = "1d2h50min"; 12 | assertEquals("P1DT2H50M", Utils.durationTranslator(input)); 13 | } 14 | 15 | @Test 16 | public void testDateWithoutDaysWorks() { 17 | String input = "2h50min"; 18 | assertEquals("PT2H50M", Utils.durationTranslator(input)); 19 | } 20 | 21 | @Test 22 | public void testDateOnlyWithMin() { 23 | String input = "50min"; 24 | assertEquals("PT50M", Utils.durationTranslator(input)); 25 | } 26 | 27 | @Test 28 | public void testDateOnlyWithHours() { 29 | String input = "3h"; 30 | assertEquals("PT3H", Utils.durationTranslator(input)); 31 | } 32 | 33 | @Test 34 | public void testDateOnlyWithDays() { 35 | String input = "3d"; 36 | assertEquals("P3D", Utils.durationTranslator(input)); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /web-client/.dockerignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mechero/code-quality-game/1acd1e3f72f998bcda5877c9b628bd215e356188/web-client/.dockerignore -------------------------------------------------------------------------------- /web-client/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /web-client/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /web-client/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:10 as build-stage 3 | WORKDIR /app 4 | COPY package*.json /app/ 5 | RUN npm install 6 | COPY ./ /app/ 7 | ARG configuration=production 8 | RUN npm run build -- --output-path=./dist/out --configuration $configuration 9 | 10 | # Compiled app based on nginx 11 | FROM nginx:1.15 12 | COPY --from=build-stage /app/dist/out/ /usr/share/nginx/html 13 | COPY /nginx.conf /etc/nginx/conf.d/default.conf 14 | EXPOSE 1827 15 | -------------------------------------------------------------------------------- /web-client/README.md: -------------------------------------------------------------------------------- 1 | # FrontendV2 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.1.4. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /web-client/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "frontendV2": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/frontendV2", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.css", 27 | "node_modules/font-awesome/css/font-awesome.css" 28 | ], 29 | "scripts": [] 30 | }, 31 | "configurations": { 32 | "production": { 33 | "fileReplacements": [ 34 | { 35 | "replace": "src/environments/environment.ts", 36 | "with": "src/environments/environment.prod.ts" 37 | } 38 | ], 39 | "optimization": true, 40 | "outputHashing": "all", 41 | "sourceMap": false, 42 | "extractCss": true, 43 | "namedChunks": false, 44 | "aot": true, 45 | "extractLicenses": true, 46 | "vendorChunk": false, 47 | "buildOptimizer": true 48 | } 49 | } 50 | }, 51 | "serve": { 52 | "builder": "@angular-devkit/build-angular:dev-server", 53 | "options": { 54 | "browserTarget": "frontendV2:build" 55 | }, 56 | "configurations": { 57 | "production": { 58 | "browserTarget": "frontendV2:build:production" 59 | } 60 | } 61 | }, 62 | "extract-i18n": { 63 | "builder": "@angular-devkit/build-angular:extract-i18n", 64 | "options": { 65 | "browserTarget": "frontendV2:build" 66 | } 67 | }, 68 | "test": { 69 | "builder": "@angular-devkit/build-angular:karma", 70 | "options": { 71 | "main": "src/test.ts", 72 | "polyfills": "src/polyfills.ts", 73 | "tsConfig": "src/tsconfig.spec.json", 74 | "karmaConfig": "src/karma.conf.js", 75 | "styles": [ 76 | "src/styles.css" 77 | ], 78 | "scripts": [], 79 | "assets": [ 80 | "src/favicon.ico", 81 | "src/assets" 82 | ] 83 | } 84 | }, 85 | "lint": { 86 | "builder": "@angular-devkit/build-angular:tslint", 87 | "options": { 88 | "tsConfig": [ 89 | "src/tsconfig.app.json", 90 | "src/tsconfig.spec.json" 91 | ], 92 | "exclude": [ 93 | "**/node_modules/**" 94 | ] 95 | } 96 | } 97 | } 98 | }, 99 | "frontendV2-e2e": { 100 | "root": "e2e/", 101 | "projectType": "application", 102 | "architect": { 103 | "e2e": { 104 | "builder": "@angular-devkit/build-angular:protractor", 105 | "options": { 106 | "protractorConfig": "e2e/protractor.conf.js", 107 | "devServerTarget": "frontendV2:serve" 108 | }, 109 | "configurations": { 110 | "production": { 111 | "devServerTarget": "frontendV2:serve:production" 112 | } 113 | } 114 | }, 115 | "lint": { 116 | "builder": "@angular-devkit/build-angular:tslint", 117 | "options": { 118 | "tsConfig": "e2e/tsconfig.e2e.json", 119 | "exclude": [ 120 | "**/node_modules/**" 121 | ] 122 | } 123 | } 124 | } 125 | } 126 | }, 127 | "defaultProject": "frontendV2" 128 | } 129 | -------------------------------------------------------------------------------- /web-client/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const {SpecReporter} = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function () { 21 | } 22 | }, 23 | onPrepare() { 24 | require('ts-node').register({ 25 | project: require('path').join(__dirname, './tsconfig.e2e.json') 26 | }); 27 | jasmine.getEnv().addReporter(new SpecReporter({spec: {displayStacktrace: true}})); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /web-client/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import {AppPage} from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to frontendV2!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /web-client/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import {browser, by, element} from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /web-client/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /web-client/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 1827; 3 | location / { 4 | root /usr/share/nginx/html; 5 | index index.html index.htm; 6 | try_files $uri $uri/ /index.html =404; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /web-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "game-web-client", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^6.1.0", 15 | "@angular/common": "^6.1.0", 16 | "@angular/compiler": "^6.1.0", 17 | "@angular/core": "^6.1.0", 18 | "@angular/forms": "^6.1.0", 19 | "@angular/http": "^6.1.0", 20 | "@angular/platform-browser": "^6.1.0", 21 | "@angular/platform-browser-dynamic": "^6.1.0", 22 | "@angular/router": "^6.1.0", 23 | "angular-font-awesome": "^3.1.2", 24 | "core-js": "^2.5.4", 25 | "font-awesome": "^4.7.0", 26 | "rxjs": "^6.0.0", 27 | "zone.js": "~0.8.26" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "~0.7.0", 31 | "@angular/cli": "~6.1.4", 32 | "@angular/compiler-cli": "^6.1.0", 33 | "@angular/language-service": "^6.1.0", 34 | "@types/jasmine": "~2.8.6", 35 | "@types/jasminewd2": "~2.0.3", 36 | "@types/node": "~8.9.4", 37 | "codelyzer": "~4.2.1", 38 | "jasmine-core": "~2.99.1", 39 | "jasmine-spec-reporter": "~4.2.1", 40 | "karma": "~1.7.1", 41 | "karma-chrome-launcher": "~2.2.0", 42 | "karma-coverage-istanbul-reporter": "~2.0.0", 43 | "karma-jasmine": "~1.1.1", 44 | "karma-jasmine-html-reporter": "^0.2.2", 45 | "protractor": "~5.4.0", 46 | "ts-node": "~5.0.1", 47 | "tslint": "~5.9.1", 48 | "typescript": "~2.7.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /web-client/src/app/app-routing.module.spec.ts: -------------------------------------------------------------------------------- 1 | import {AppRoutingModule} from './app-routing.module'; 2 | 3 | describe('AppRoutingModule', () => { 4 | let appRoutingModule: AppRoutingModule; 5 | 6 | beforeEach(() => { 7 | appRoutingModule = new AppRoutingModule(); 8 | }); 9 | 10 | it('should create an instance', () => { 11 | expect(appRoutingModule).toBeTruthy(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /web-client/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {RouterModule, Routes} from '@angular/router'; 4 | import {TeamsComponent} from './teams/teams.component'; 5 | import {MembersComponent} from './members/members.component'; 6 | import {OrganizerComponent} from "./settings/organizer.component"; 7 | import {SettingsComponent} from "./settings/settings.component"; 8 | 9 | const routes: Routes = [ 10 | {path: '', redirectTo: '/teams', pathMatch: 'full'}, 11 | {path: 'teams', component: TeamsComponent}, 12 | {path: 'members', component: MembersComponent}, 13 | {path: 'organizer', component: OrganizerComponent}, 14 | {path: 'settings', component: SettingsComponent} 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [CommonModule, RouterModule.forRoot(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class AppRoutingModule { 22 | } 23 | -------------------------------------------------------------------------------- /web-client/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mechero/code-quality-game/1acd1e3f72f998bcda5877c9b628bd215e356188/web-client/src/app/app.component.css -------------------------------------------------------------------------------- /web-client/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 31 |
32 |
33 | 34 |
35 |
36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /web-client/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestBed, async} from '@angular/core/testing'; 2 | import {AppComponent} from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | it('should create the app', async(() => { 13 | const fixture = TestBed.createComponent(AppComponent); 14 | const app = fixture.debugElement.componentInstance; 15 | expect(app).toBeTruthy(); 16 | })); 17 | it(`should have as title 'frontendV2'`, async(() => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.debugElement.componentInstance; 20 | expect(app.title).toEqual('frontendV2'); 21 | })); 22 | it('should render title in a h1 tag', async(() => { 23 | const fixture = TestBed.createComponent(AppComponent); 24 | fixture.detectChanges(); 25 | const compiled = fixture.debugElement.nativeElement; 26 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to frontendV2!'); 27 | })); 28 | }); 29 | -------------------------------------------------------------------------------- /web-client/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {RetrieverService} from "./retriever/retriever.service"; 3 | import {FooterService} from "./footer/footer.service"; 4 | import {Code} from "./footer/Code"; 5 | 6 | @Component({ 7 | moduleId: module.id, 8 | selector: 'my-app', 9 | templateUrl: 'app.component.html' 10 | }) 11 | export class AppComponent implements OnInit { 12 | title = 'Quboo'; 13 | code: Code = new Code('-NOCODE-'); 14 | 15 | constructor(private retrieverService: RetrieverService, 16 | private footerService: FooterService) { 17 | } 18 | 19 | ngOnInit(): void { 20 | this.getCode(); 21 | } 22 | 23 | getCode(): void { 24 | this.footerService.getCode().then(code => this.code = code); 25 | } 26 | 27 | forceRetrieval() { 28 | this.retrieverService.forceRetrieval().then(); // does nothing, just wait for the UI to be refreshed automatically 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /web-client/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {BrowserModule} from '@angular/platform-browser'; 2 | import {NgModule} from '@angular/core'; 3 | 4 | import {AppComponent} from './app.component'; 5 | import {FormsModule} from '@angular/forms'; 6 | import {HttpClientModule} from '@angular/common/http'; 7 | import {AppRoutingModule} from './app-routing.module'; 8 | import {TeamsComponent} from './teams/teams.component'; 9 | import {MembersComponent} from './members/members.component'; 10 | import {TeamsService} from './teams/teams.service'; 11 | import {MemberService} from './members/member.service'; 12 | import {OrganizerComponent} from "./settings/organizer.component"; 13 | import {SettingsComponent} from "./settings/settings.component"; 14 | import {FooterComponent} from "./footer/footer.component"; 15 | import {FooterService} from "./footer/footer.service"; 16 | import {AngularFontAwesomeModule} from "angular-font-awesome"; 17 | import {RetrieverService} from "./retriever/retriever.service"; 18 | import {ServerUrlComponent} from "./settings/server-url.component"; 19 | import {ServerUrlService} from "./settings/server-url.service"; 20 | 21 | @NgModule({ 22 | declarations: [ 23 | AppComponent, 24 | MembersComponent, 25 | TeamsComponent, 26 | OrganizerComponent, 27 | SettingsComponent, 28 | FooterComponent, 29 | ServerUrlComponent 30 | ], 31 | imports: [ 32 | BrowserModule, 33 | FormsModule, 34 | HttpClientModule, 35 | AppRoutingModule, 36 | AngularFontAwesomeModule 37 | ], 38 | providers: [TeamsService, MemberService, FooterService, RetrieverService, ServerUrlService], 39 | bootstrap: [AppComponent] 40 | }) 41 | export class AppModule { 42 | } 43 | -------------------------------------------------------------------------------- /web-client/src/app/common/Badge.ts: -------------------------------------------------------------------------------- 1 | export class Badge { 2 | name: string; 3 | description: string; 4 | extraPoints: number; 5 | } 6 | -------------------------------------------------------------------------------- /web-client/src/app/common/MessageResponse.ts: -------------------------------------------------------------------------------- 1 | export class MessageResponse { 2 | constructor(message: string, error: boolean) { 3 | this.message = message; 4 | this.error = error; 5 | } 6 | error: boolean; 7 | message: string; 8 | } 9 | -------------------------------------------------------------------------------- /web-client/src/app/common/StatsRow.ts: -------------------------------------------------------------------------------- 1 | import {Badge} from './Badge'; 2 | 3 | export class StatsRow { 4 | userAlias: string; 5 | userTeam: string; 6 | totalPoints: number; 7 | totalPaidDebt: number; 8 | blocker: number; 9 | critical: number; 10 | major: number; 11 | minor: number; 12 | info: number; 13 | badges: Badge[]; 14 | } 15 | -------------------------------------------------------------------------------- /web-client/src/app/footer/Code.ts: -------------------------------------------------------------------------------- 1 | export class Code { 2 | name: string; 3 | constructor(name: string) { 4 | this.name = name; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /web-client/src/app/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /web-client/src/app/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {FooterService} from "./footer.service"; 3 | import {Code} from "./Code"; 4 | 5 | @Component({ 6 | moduleId: module.id, 7 | selector: 'footer', 8 | templateUrl: 'footer.component.html' 9 | }) 10 | export class FooterComponent implements OnInit { 11 | ngOnInit(): void { 12 | this.getCode(); 13 | } 14 | 15 | code: Code = new Code('-NOCODE-'); 16 | 17 | constructor(private footerService: FooterService) { 18 | } 19 | 20 | getCode(): void { 21 | this.footerService.getCode().then(code => this.code = code); 22 | } 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /web-client/src/app/footer/footer.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient, HttpErrorResponse} from '@angular/common/http'; 3 | import {Code} from "./Code"; 4 | import {getStoredServerUrl} from "../settings/Settings"; 5 | 6 | 7 | @Injectable() 8 | export class FooterService { 9 | 10 | private codeUrl = '/code'; 11 | 12 | constructor(private http: HttpClient) { 13 | } 14 | 15 | getCode(): Promise { 16 | return this.http.get(getStoredServerUrl() + this.codeUrl) 17 | .toPromise() 18 | .then(response => response as Code) 19 | .catch(this.handleError); 20 | } 21 | 22 | private handleError(error: any): Promise { 23 | console.error('An error occurred accessing ' + this.codeUrl, error); 24 | if (error instanceof HttpErrorResponse) { 25 | console.error("Response status: " + error.status + " | Message: " + error.message); 26 | } 27 | return Promise.resolve(new Code("-NOCODE-")); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /web-client/src/app/members/User.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | 3 | constructor(id: string, login: string, alias: string, team: string) { 4 | this.id = id; 5 | this.login = login; 6 | this.alias = alias; 7 | this.team = team; 8 | } 9 | 10 | id: string; 11 | login: string; 12 | alias: string; 13 | team: string; 14 | } 15 | -------------------------------------------------------------------------------- /web-client/src/app/members/member.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {StatsRow} from '../common/StatsRow'; 3 | import {HttpClient, HttpErrorResponse} from '@angular/common/http'; 4 | import {User} from "./User"; 5 | import {MessageResponse} from "../common/MessageResponse"; 6 | import {getStoredServerUrl} from "../settings/Settings"; 7 | 8 | 9 | @Injectable() 10 | export class MemberService { 11 | 12 | private memberStatsUrl = '/stats/users'; 13 | private usersUrl = '/users'; 14 | 15 | constructor(private http: HttpClient) { 16 | } 17 | 18 | getMemberStats(): Promise { 19 | return this.http.get(getStoredServerUrl() + this.memberStatsUrl) 20 | .toPromise() 21 | .then(response => response as StatsRow[]) 22 | .catch(this.handleError); 23 | } 24 | 25 | getUsers(): Promise { 26 | return this.http.get(getStoredServerUrl() + this.usersUrl + '?assigned') 27 | .toPromise() 28 | .then(response => response as User[]) 29 | .catch(this.handleError); 30 | } 31 | 32 | getUnassignedUsers(): Promise { 33 | return this.http.get(getStoredServerUrl() + this.usersUrl + '?unassigned') 34 | .toPromise() 35 | .then(response => response as User[]) 36 | .catch(this.handleError); 37 | } 38 | 39 | deleteUnassignedUsers(): Promise { 40 | return this.http.delete(getStoredServerUrl() + this.usersUrl + '?unassigned') 41 | .toPromise() 42 | .then(response => response as MessageResponse) 43 | .catch(this.handleError); 44 | } 45 | 46 | updateUser(user: User) { 47 | return this.http.put(getStoredServerUrl() + this.usersUrl + '/' + user.id, user) 48 | .toPromise() 49 | .then(response => response as User) 50 | .catch(this.handleError); 51 | } 52 | 53 | removeAllStats(): Promise { 54 | return this.http.delete(getStoredServerUrl() + this.memberStatsUrl) 55 | .toPromise() 56 | .then(response => response as MessageResponse) 57 | .catch(this.handleError); 58 | } 59 | 60 | private handleError(error: any): Promise { 61 | console.error('An error occurred accessing the server', error); 62 | if (error instanceof HttpErrorResponse) { 63 | console.error("Response status: " + error.status + " | Message: " + error.message); 64 | } 65 | return Promise.reject(error.message || error); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /web-client/src/app/members/members.component.html: -------------------------------------------------------------------------------- 1 |

Player Ranking

2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
RankingNameTeamScoreBadges
Golden Quboo{{i + 1}}{{member.userAlias}}{{member.userTeam}}{{member.totalPoints}}{{b.name}}{{member.blocker}}{{member.critical}}{{member.major}}{{member.minor}}{{member.info}}
34 |
35 | -------------------------------------------------------------------------------- /web-client/src/app/members/members.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {OnInit} from '@angular/core'; 3 | import {User} from './User'; 4 | import {MemberService} from './member.service'; 5 | import {StatsRow} from '../common/StatsRow'; 6 | import {ServerUrlService} from "../settings/server-url.service"; 7 | 8 | @Component({ 9 | moduleId: module.id, 10 | selector: 'members', 11 | templateUrl: 'members.component.html' 12 | }) 13 | export class MembersComponent implements OnInit { 14 | 15 | ngOnInit(): void { 16 | setInterval(() => this.getMembers(), 2 * 60 * 1000); 17 | this.getMembers(); 18 | this.serverUrlService.change.subscribe(ignore => this.getMembers()); 19 | } 20 | 21 | memberStats: StatsRow[]; 22 | selectedMember: User; 23 | 24 | constructor(private memberService: MemberService, private serverUrlService: ServerUrlService) { 25 | } 26 | 27 | onSelect(member: User): void { 28 | this.selectedMember = member; 29 | } 30 | 31 | getMembers(): void { 32 | this.memberService.getMemberStats().then(memberStats => this.memberStats = memberStats); 33 | } 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /web-client/src/app/retriever/retriever.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient, HttpErrorResponse} from '@angular/common/http'; 3 | import {getStoredServerUrl} from "../settings/Settings"; 4 | 5 | @Injectable() 6 | export class RetrieverService { 7 | 8 | private forceRetrievalUrl = '/retriever/now'; 9 | 10 | constructor(private http: HttpClient) { 11 | } 12 | 13 | forceRetrieval(): Promise { 14 | return this.http.post(getStoredServerUrl() + this.forceRetrievalUrl, {}) 15 | .toPromise() 16 | .catch(this.handleError); 17 | } 18 | 19 | private handleError(error: any): Promise { 20 | if (error instanceof HttpErrorResponse) { 21 | console.error("Response status: " + error.status + " | Message: " + error.message); 22 | } 23 | return Promise.reject(error.message || error); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web-client/src/app/settings/Settings.ts: -------------------------------------------------------------------------------- 1 | export const SERVER_URL_KEY = 'QUBOO_SERVER_URL'; 2 | 3 | export function getStoredServerUrl() : string { 4 | return localStorage.getItem(SERVER_URL_KEY); 5 | } 6 | 7 | export function isServerUrlStored() : boolean { 8 | return localStorage.getItem(SERVER_URL_KEY) !== null; 9 | } 10 | 11 | export function defaultServerUrl(): string { 12 | return window.location.protocol + '//' + window.location.hostname + ':8080'; 13 | } 14 | 15 | export function saveServerUrl(url: string) { 16 | localStorage.setItem(SERVER_URL_KEY, url); 17 | } 18 | -------------------------------------------------------------------------------- /web-client/src/app/settings/organizer.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | {{errorMessage}} 6 |
7 |
8 |

Teams

9 |
10 |
11 |
{{team.name}}
12 | 13 |
14 |
15 | 16 |
17 |
18 | 19 | 20 | 21 | 22 |
23 |
24 |

Players

25 |
26 |
27 |
28 | 31 | 32 |
33 |
34 | 37 | 38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 63 | 66 | 70 | 71 | 72 |
LoginAliasTeam
{{user.login}}{{user.alias}} 54 | 55 | {{user.team}} 58 | 62 | 64 | 65 | 67 |   68 | 69 |
73 |
74 |
75 | -------------------------------------------------------------------------------- /web-client/src/app/settings/organizer.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from "@angular/core"; 2 | import {TeamsService} from "../teams/teams.service"; 3 | import {Team} from "../teams/Team"; 4 | import {MemberService} from "../members/member.service"; 5 | import {User} from "../members/User"; 6 | 7 | @Component({ 8 | moduleId: module.id, 9 | selector: 'organizer', 10 | templateUrl: 'organizer.component.html' 11 | }) 12 | export class OrganizerComponent implements OnInit { 13 | 14 | readonly TEAM_NOT_ASSIGNED: string = '--NO-TEAM--'; 15 | 16 | constructor(private teamsService: TeamsService, 17 | private usersService: MemberService) { 18 | } 19 | 20 | teams: Team[] = []; 21 | users: User[] = []; 22 | assignedUsers: User[] = []; 23 | orphanUsers: User[] = []; 24 | editingId: string; 25 | showAssigned: boolean = true; 26 | showUnassigned: boolean = true; 27 | editingNewTeam: boolean = false; 28 | newTeamName: string = null; 29 | errorMessage: string = null; 30 | 31 | ngOnInit(): void { 32 | this.update(); 33 | this.noEditing(); 34 | } 35 | 36 | async update() { 37 | this.teamsService.getTeams().then(teams => this.teams = teams); 38 | let userPromise, orphanPromise; 39 | if (this.showAssigned) { 40 | userPromise = this.usersService.getUsers().then(users => this.assignedUsers = users); 41 | await userPromise; 42 | } else { 43 | this.assignedUsers = []; 44 | } 45 | if (this.showUnassigned) { 46 | orphanPromise = this.usersService.getUnassignedUsers().then(users => this.orphanUsers = users); 47 | await orphanPromise; 48 | } else { 49 | this.orphanUsers = []; 50 | } 51 | this.users = this.assignedUsers.concat(this.orphanUsers); 52 | } 53 | 54 | editUser(id: string) { 55 | this.editingId = id; 56 | } 57 | 58 | async saveUser(_user: User) { 59 | let user = new User(_user.id, _user.login, _user.alias, _user.team === this.TEAM_NOT_ASSIGNED ? null : _user.team); 60 | let userPromise = this.usersService.updateUser(user); 61 | await userPromise; 62 | this.noEditing(); 63 | } 64 | 65 | cancel(): void { 66 | this.noEditing(); 67 | this.update(); 68 | } 69 | 70 | noEditing(): void { 71 | this.editingId = null; 72 | } 73 | 74 | addNewTeam(): void { 75 | this.editingNewTeam = true; 76 | } 77 | 78 | async saveNewTeam() { 79 | let newTeamPromise = this.teamsService.createTeam(this.newTeamName); 80 | await newTeamPromise; 81 | this.editingNewTeam = false; 82 | this.update(); 83 | } 84 | 85 | cancelNewTeamEditing(): void { 86 | this.newTeamName = null; 87 | this.editingNewTeam = false; 88 | } 89 | 90 | updateErrorMessage(msg: string) { 91 | this.errorMessage = msg; 92 | } 93 | 94 | async deleteTeam(teamId: string) { 95 | let deleteTeamPromise = this.teamsService.deleteTeam(teamId) 96 | .then(response => { 97 | if(response.error) { 98 | this.updateErrorMessage(response.message); 99 | } 100 | }); 101 | // TODO proper error handling here and above 102 | await deleteTeamPromise; 103 | this.update(); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /web-client/src/app/settings/server-url.component.html: -------------------------------------------------------------------------------- 1 | 4 | 28 | -------------------------------------------------------------------------------- /web-client/src/app/settings/server-url.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from "@angular/core"; 2 | import {defaultServerUrl, getStoredServerUrl, isServerUrlStored, saveServerUrl} from "./Settings"; 3 | import {ServerUrlService} from "./server-url.service"; 4 | 5 | @Component({ 6 | moduleId: module.id, 7 | selector: 'server-url', 8 | templateUrl: 'server-url.component.html' 9 | }) 10 | export class ServerUrlComponent implements OnInit { 11 | 12 | url: string; 13 | serverUrlService: ServerUrlService; 14 | 15 | constructor(serverUrlService: ServerUrlService) { 16 | this.url = isServerUrlStored() ? getStoredServerUrl() : defaultServerUrl(); 17 | this.serverUrlService = serverUrlService; 18 | } 19 | 20 | saveServerUrl(): void { 21 | saveServerUrl(this.url); 22 | this.serverUrlService.serverUrlUpdated(this.url); 23 | } 24 | 25 | hasServerUrl(): boolean { 26 | return isServerUrlStored(); 27 | } 28 | 29 | ngOnInit(): void { 30 | if(!this.hasServerUrl()) { 31 | document.getElementById('showModalButton').click(); 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /web-client/src/app/settings/server-url.service.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter, Injectable, Output} from "@angular/core"; 2 | 3 | @Injectable() 4 | export class ServerUrlService { 5 | 6 | @Output() 7 | change: EventEmitter = new EventEmitter(); 8 | 9 | serverUrlUpdated(url: string) { 10 | // the url is not used, it's taken from memory instead 11 | this.change.emit(url); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /web-client/src/app/settings/settings.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | {{responseMessage.message}} 6 |
7 |
8 |
9 |
10 |

Quboo Service URL

11 |

Where is the Quboo backend service installed?

12 |

13 | 16 | Saved value: {{getCurrentServerUrl()}} 17 | 18 |

19 |
20 |
21 |
22 |
23 |

Delete all unassigned players

24 |

You can delete all players that are not assigned to a team.

25 |

This is useful when you want to do a cleaning of users. Bear in mind that, 26 | if you have enabled User Sync with the server, you may get again unassigned users if they 27 | still exist there.

28 |

This is a destructive action. Those players will also lose their statistics. 29 | Write below 'remove-all-unassigned' to enable the button.

30 |

31 |

32 | 35 |

36 |
37 |
38 |
39 |
40 |

Delete all statistics

41 |

You can delete all the statistics that are stored in this game: points and badges for all the 42 | players.

43 |

This is useful when you want to do a game reset and start over again. 44 | During the next synchronization with the server, you'll get only the history of resolved issues that is kept 45 | there.

46 |

This is a destructive action. All players will lose their statistics. 47 | Even after synchronization, the results may be different since the server might be storing data only 48 | for a limited period of time (e.g. the last month). Write below 'remove-all-stats' to enable the button.

49 |

50 |

51 | 54 |

55 |
56 |
57 |
58 | -------------------------------------------------------------------------------- /web-client/src/app/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from "@angular/core"; 2 | import {TeamsService} from "../teams/teams.service"; 3 | import {MemberService} from "../members/member.service"; 4 | import {MessageResponse} from "../common/MessageResponse"; 5 | import {SERVER_URL_KEY} from "./Settings"; 6 | 7 | @Component({ 8 | moduleId: module.id, 9 | selector: 'settings', 10 | templateUrl: 'settings.component.html' 11 | }) 12 | export class SettingsComponent { 13 | 14 | constructor(private teamsService: TeamsService, 15 | private usersService: MemberService) { 16 | } 17 | 18 | responseMessage: MessageResponse = null; 19 | removeAllUnassignedText: string = null; 20 | removeAllStatsText: string = null; 21 | 22 | setMessage(res: MessageResponse): void { 23 | this.responseMessage = res; 24 | } 25 | 26 | canRemoveAllUnassigned(): boolean { 27 | return this.removeAllUnassignedText === 'remove-all-unassigned'; 28 | } 29 | 30 | canRemoveAllStats(): boolean { 31 | return this.removeAllStatsText === 'remove-all-stats'; 32 | } 33 | 34 | removeAllUnnassignedUsers(): void { 35 | this.usersService.deleteUnassignedUsers().then(response => this.setMessage(response)); 36 | this.removeAllUnassignedText = null; 37 | } 38 | 39 | removeAllStats(): void { 40 | this.usersService.removeAllStats().then(response => this.setMessage(response)); 41 | this.removeAllStatsText = null; 42 | } 43 | 44 | getCurrentServerUrl(): string { 45 | return localStorage.getItem(SERVER_URL_KEY); 46 | } 47 | 48 | } 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /web-client/src/app/teams/Team.ts: -------------------------------------------------------------------------------- 1 | export class Team { 2 | constructor(name: string) { 3 | this.name = name; 4 | } 5 | id: number; 6 | name: string; 7 | } 8 | -------------------------------------------------------------------------------- /web-client/src/app/teams/mock-teams.ts: -------------------------------------------------------------------------------- 1 | import {Team} from "./Team"; 2 | 3 | export const TEAMS: Team[] = [ 4 | {id: 11, name: 'Senores Patatas'}, 5 | {id: 12, name: 'Totramusicos'} 6 | ]; 7 | -------------------------------------------------------------------------------- /web-client/src/app/teams/teams.component.html: -------------------------------------------------------------------------------- 1 |

Team Ranking

2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
RankingTeamTotal ScoreTotal Paid Debt (min)
Golden Quboo{{i + 1}}{{team.userTeam}}{{team.totalPoints}}{{team.totalPaidDebt}}
24 |
25 | -------------------------------------------------------------------------------- /web-client/src/app/teams/teams.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {TeamsService} from './teams.service'; 3 | import {StatsRow} from '../common/StatsRow'; 4 | import {ServerUrlService} from "../settings/server-url.service"; 5 | 6 | @Component({ 7 | moduleId: module.id, 8 | selector: 'teams', 9 | templateUrl: 'teams.component.html' 10 | }) 11 | export class TeamsComponent implements OnInit { 12 | 13 | constructor(private teamsService: TeamsService, private serverUrlService: ServerUrlService) { 14 | } 15 | 16 | teams: StatsRow[]; 17 | 18 | ngOnInit(): void { 19 | setInterval(() => this.getTeams(), 2 * 60 * 1000); 20 | this.getTeams(); 21 | this.serverUrlService.change.subscribe(ignore => this.getTeams()); 22 | } 23 | 24 | getTeams(): void { 25 | this.teamsService.getTeamStats().then(teams => this.teams = teams); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /web-client/src/app/teams/teams.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {StatsRow} from '../common/StatsRow'; 3 | import {HttpClient, HttpErrorResponse} from '@angular/common/http'; 4 | import {Team} from "./Team"; 5 | import {MessageResponse} from "../common/MessageResponse"; 6 | import {getStoredServerUrl} from "../settings/Settings"; 7 | 8 | @Injectable() 9 | export class TeamsService { 10 | 11 | private teamStatsUrl = '/stats/teams'; 12 | private teamsUrl = '/teams'; 13 | 14 | constructor(private http: HttpClient) { 15 | } 16 | 17 | getTeamStats(): Promise { 18 | return this.http.get(getStoredServerUrl() + this.teamStatsUrl) 19 | .toPromise() 20 | .then(response => response as StatsRow[]) 21 | .catch(this.handleError); 22 | } 23 | 24 | getTeams(): Promise { 25 | return this.http.get(getStoredServerUrl() + this.teamsUrl).toPromise() 26 | .then(response => response as Team[]) 27 | .catch(this.handleError); 28 | } 29 | 30 | createTeam(teamName): Promise { 31 | return this.http.post(getStoredServerUrl() + this.teamsUrl, new Team(teamName)).toPromise() 32 | .then(response => response as Team) 33 | .catch(this.handleError); 34 | } 35 | 36 | deleteTeam(teamId: string): Promise { 37 | return this.http.delete(getStoredServerUrl() + this.teamsUrl + '/' + teamId).toPromise() 38 | .then(response => response as MessageResponse) 39 | .catch(this.handleError) 40 | } 41 | 42 | private handleError(error: any): Promise { 43 | if (error.status === 422) { 44 | return Promise.resolve(new MessageResponse(error.error.message, true)); 45 | } else { 46 | console.error('An error occurred accessing the server', error); 47 | if (error instanceof HttpErrorResponse) { 48 | console.error("Response status: " + error.status + " | Message: " + error.message); 49 | } 50 | return Promise.reject(error.message || error); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /web-client/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mechero/code-quality-game/1acd1e3f72f998bcda5877c9b628bd215e356188/web-client/src/assets/.gitkeep -------------------------------------------------------------------------------- /web-client/src/assets/img/become_a_patron_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mechero/code-quality-game/1acd1e3f72f998bcda5877c9b628bd215e356188/web-client/src/assets/img/become_a_patron_button.png -------------------------------------------------------------------------------- /web-client/src/assets/img/monkey_logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mechero/code-quality-game/1acd1e3f72f998bcda5877c9b628bd215e356188/web-client/src/assets/img/monkey_logo.gif -------------------------------------------------------------------------------- /web-client/src/assets/img/quboo_gold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mechero/code-quality-game/1acd1e3f72f998bcda5877c9b628bd215e356188/web-client/src/assets/img/quboo_gold.png -------------------------------------------------------------------------------- /web-client/src/assets/img/quboo_logo_orange_250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mechero/code-quality-game/1acd1e3f72f998bcda5877c9b628bd215e356188/web-client/src/assets/img/quboo_logo_orange_250.png -------------------------------------------------------------------------------- /web-client/src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /web-client/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /web-client/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | }; 8 | 9 | /* 10 | * In development mode, for easier debugging, you can ignore zone related error 11 | * stack frames such as `zone.run`/`zoneDelegate.invokeTask` by importing the 12 | * below file. Don't forget to comment it out in production mode 13 | * because it will have a performance impact when errors are thrown 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /web-client/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mechero/code-quality-game/1acd1e3f72f998bcda5877c9b628bd215e356188/web-client/src/favicon.ico -------------------------------------------------------------------------------- /web-client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Quboo - The Code Quality Boosters 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Loading... 14 | 15 | 16 | -------------------------------------------------------------------------------- /web-client/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /web-client/src/main.ts: -------------------------------------------------------------------------------- 1 | import {enableProdMode} from '@angular/core'; 2 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 3 | 4 | import {AppModule} from './app/app.module'; 5 | import {environment} from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /web-client/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Web Animations `@angular/platform-browser/animations` 51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 53 | **/ 54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 55 | 56 | /** 57 | * By default, zone.js will patch all possible macroTask and DomEvents 58 | * user can disable parts of macroTask/DomEvents patch by setting following flags 59 | */ 60 | 61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 64 | 65 | /* 66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 68 | */ 69 | // (window as any).__Zone_enable_cross_context_check = true; 70 | 71 | /*************************************************************************************************** 72 | * Zone JS is required by default for Angular itself. 73 | */ 74 | import 'zone.js/dist/zone'; // Included with Angular CLI. 75 | 76 | 77 | /*************************************************************************************************** 78 | * APPLICATION IMPORTS 79 | */ 80 | -------------------------------------------------------------------------------- /web-client/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | html, body { 3 | height: 100%; 4 | } 5 | body { 6 | 7 | } 8 | .body-flex { 9 | height: 100%; 10 | display: flex; 11 | flex-direction: column; 12 | } 13 | .content { 14 | flex: 1 0 auto; 15 | } 16 | .footer { 17 | flex-shrink: 0; 18 | margin-bottom: 0; 19 | padding: 10px 0; 20 | } 21 | .footer-content { 22 | display: flex; 23 | align-items: baseline; 24 | justify-content: center; 25 | } 26 | .container-fluid { 27 | padding-left: 0; 28 | padding-right: 0; 29 | } 30 | .gameBackgroundPanel{ 31 | display: flex; 32 | justify-content: center; 33 | align-items: flex-start; 34 | margin-top: 1em; 35 | } 36 | .gamePageContent{ 37 | width: 1024px; 38 | max-width: 1024px; 39 | } 40 | .team-container { 41 | display: flex; 42 | flex-wrap: wrap; 43 | margin: 25px 0 25px 0; 44 | align-items: center; 45 | } 46 | .team-item { 47 | display: flex; 48 | margin-right: 10px; 49 | margin-bottom: 5px; 50 | align-items: center; 51 | } 52 | .team-item-label { 53 | padding-right: 10px; 54 | } 55 | .checks-container { 56 | display: flex; 57 | margin-top: 20px; 58 | margin-bottom: 10px; 59 | } 60 | .checkbox-and-label { 61 | margin-right: 15px; 62 | } 63 | .checkbox-in-flex { 64 | padding-right: 10px; 65 | } 66 | .players-container { 67 | margin: 25px 0 25px 0; 68 | } 69 | .sm-margin-right { 70 | margin-right: 5px; 71 | } 72 | .new-team-box { 73 | display: flex; 74 | margin: 10px 10px 10px 10px; 75 | align-items: center; 76 | } 77 | .badge-normal { 78 | font-size: 100%; 79 | } 80 | .badge-stats { 81 | font-size: 75%; 82 | font-weight: lighter; 83 | margin-right: 5px; 84 | } 85 | #players td { 86 | font-size: 18px; 87 | vertical-align: middle; 88 | } 89 | #teams td { 90 | font-size: 18px; 91 | vertical-align: middle; 92 | } 93 | .navbar-item-custom { 94 | font-size: 18px; 95 | margin-right: 1rem; 96 | } 97 | .navbar-nopadding { 98 | padding: 0 0.5rem; 99 | } 100 | .ptr { 101 | display: flex; 102 | align-items: center; 103 | } 104 | .ptr-text { 105 | font-size: 18px; 106 | margin-right: 10px; 107 | } 108 | -------------------------------------------------------------------------------- /web-client/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import {getTestBed} from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /web-client/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app" 5 | }, 6 | "exclude": [ 7 | "test.ts", 8 | "**/*.spec.ts" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /web-client/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /web-client/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /web-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2017", 18 | "dom" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /web-client/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "no-output-on-prefix": true, 120 | "use-input-property-decorator": true, 121 | "use-output-property-decorator": true, 122 | "use-host-property-decorator": true, 123 | "no-input-rename": true, 124 | "no-output-rename": true, 125 | "use-life-cycle-interface": true, 126 | "use-pipe-transform-interface": true, 127 | "component-class-suffix": true, 128 | "directive-class-suffix": true 129 | } 130 | } 131 | --------------------------------------------------------------------------------