Merge pull request 'stats/runners/laptime feature/190-runners_laptime' (#191) from feature/190-runners_laptime into dev
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
Reviewed-on: #191
This commit is contained in:
commit
a694ad225c
15
.drone.yml
15
.drone.yml
|
@ -19,10 +19,17 @@ get:
|
|||
path: odit-git-bot
|
||||
name: sshkey
|
||||
|
||||
---
|
||||
kind: secret
|
||||
name: ci_token
|
||||
get:
|
||||
path: odit-ci-bot
|
||||
name: apikey
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: kubernetes
|
||||
name: tests:node_latest
|
||||
name: tests:node_14.15.1-alpine3.12
|
||||
clone:
|
||||
disable: true
|
||||
steps:
|
||||
|
@ -32,7 +39,7 @@ steps:
|
|||
- git clone $DRONE_REMOTE_URL .
|
||||
- git checkout $DRONE_SOURCE_BRANCH
|
||||
- name: run tests
|
||||
image: node:latest
|
||||
image: node:14.15.1-alpine3.12
|
||||
commands:
|
||||
- yarn
|
||||
- yarn test:ci
|
||||
|
@ -176,13 +183,13 @@ steps:
|
|||
settings:
|
||||
urls: https://ci.odit.services/api/repos/lfk/lfk-client-node/builds?SOURCE_TAG=${DRONE_TAG}
|
||||
bearer:
|
||||
from_secret: BOT_DRONE_KEY
|
||||
from_secret: ci_token
|
||||
- name: trigger js lib build
|
||||
image: idcooldi/drone-webhook
|
||||
settings:
|
||||
urls: https://ci.odit.services/api/repos/lfk/lfk-client-js/builds?SOURCE_TAG=${DRONE_TAG}
|
||||
bearer:
|
||||
from_secret: BOT_DRONE_KEY
|
||||
from_secret: ci_token
|
||||
trigger:
|
||||
event:
|
||||
- tag
|
|
@ -11,8 +11,12 @@ services:
|
|||
DB_PORT: bla
|
||||
DB_USER: bla
|
||||
DB_PASSWORD: bla
|
||||
DB_NAME: dev.sqlite
|
||||
DB_NAME: ./db.sqlite
|
||||
NODE_ENV: production
|
||||
POSTALCODE_COUNTRYCODE: DE
|
||||
SEED_TEST_DATA: "false"
|
||||
MAILER_URL: https://dev.lauf-fuer-kaya.de/mailer
|
||||
MAILER_KEY: asdasd
|
||||
# APP_PORT: 4010
|
||||
# DB_TYPE: postgres
|
||||
# DB_HOST: backend_db
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Get, JsonController, UseBefore } from 'routing-controllers';
|
||||
import { Get, JsonController, QueryParam, UseBefore } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnection } from 'typeorm';
|
||||
import StatsAuth from '../middlewares/StatsAuth';
|
||||
|
@ -7,6 +7,7 @@ import { Runner } from '../models/entities/Runner';
|
|||
import { RunnerOrganization } from '../models/entities/RunnerOrganization';
|
||||
import { RunnerTeam } from '../models/entities/RunnerTeam';
|
||||
import { Scan } from '../models/entities/Scan';
|
||||
import { TrackScan } from '../models/entities/TrackScan';
|
||||
import { User } from '../models/entities/User';
|
||||
import { ResponseStats } from '../models/responses/ResponseStats';
|
||||
import { ResponseStatsOrgnisation } from '../models/responses/ResponseStatsOrganization';
|
||||
|
@ -36,7 +37,10 @@ export class StatsController {
|
|||
@OpenAPI({ description: "Returns the top ten runners by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopRunnersByDistance() {
|
||||
let runners = await getConnection().getRepository(Runner).find({ relations: ['scans', 'group', 'distanceDonations', 'scans.track'] });
|
||||
let topRunners = runners.sort((runner1, runner2) => runner1.distance - runner2.distance).slice(0, 9);
|
||||
if (!runners || runners.length == 0) {
|
||||
return [];
|
||||
}
|
||||
let topRunners = runners.sort((runner1, runner2) => runner2.distance - runner1.distance).slice(0, 10);
|
||||
let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
|
||||
topRunners.forEach(runner => {
|
||||
responseRunners.push(new ResponseStatsRunner(runner));
|
||||
|
@ -49,8 +53,11 @@ export class StatsController {
|
|||
@ResponseSchema(ResponseStatsRunner, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten runners by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopRunnersByDonations() {
|
||||
let runners = await getConnection().getRepository(Runner).find({ relations: ['scans', 'group', 'distanceDonations', 'scans.track'] });
|
||||
let topRunners = runners.sort((runner1, runner2) => runner1.distanceDonationAmount - runner2.distanceDonationAmount).slice(0, 9);
|
||||
let runners = await getConnection().getRepository(Runner).find({ relations: ['group', 'distanceDonations', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] });
|
||||
if (!runners || runners.length == 0) {
|
||||
return [];
|
||||
}
|
||||
let topRunners = runners.sort((runner1, runner2) => runner2.distanceDonationAmount - runner1.distanceDonationAmount).slice(0, 10);
|
||||
let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
|
||||
topRunners.forEach(runner => {
|
||||
responseRunners.push(new ResponseStatsRunner(runner));
|
||||
|
@ -58,6 +65,34 @@ export class StatsController {
|
|||
return responseRunners;
|
||||
}
|
||||
|
||||
@Get("/runners/laptime")
|
||||
@UseBefore(StatsAuth)
|
||||
@ResponseSchema(ResponseStatsRunner, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten runners by fastest laptime on your selected track (track by id).", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopRunnersByLaptime(@QueryParam("track") track: number) {
|
||||
let scans = await getConnection().getRepository(TrackScan).find({ relations: ['track', 'runner', 'runner.group', 'runner.scans', 'runner.scans.track', 'runner.distanceDonations'] });
|
||||
if (!scans || scans.length == 0) {
|
||||
return [];
|
||||
}
|
||||
scans = scans.filter((s) => { return s.track.id == track && s.valid == true && s.lapTime != 0 }).sort((scan1, scan2) => scan1.lapTime - scan2.lapTime);
|
||||
|
||||
let topScans = new Array<TrackScan>();
|
||||
let knownRunners = new Array<number>();
|
||||
for (let i = 0; i < scans.length && topScans.length < 10; i++) {
|
||||
const element = scans[i];
|
||||
if (!knownRunners.includes(element.runner.id)) {
|
||||
topScans.push(element);
|
||||
knownRunners.push(element.runner.id);
|
||||
}
|
||||
}
|
||||
|
||||
let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
|
||||
topScans.forEach(scan => {
|
||||
responseRunners.push(new ResponseStatsRunner(scan.runner, scan.lapTime));
|
||||
});
|
||||
return responseRunners;
|
||||
}
|
||||
|
||||
@Get("/scans")
|
||||
@UseBefore(StatsAuth)
|
||||
@ResponseSchema(ResponseStatsRunner, { isArray: true })
|
||||
|
@ -71,8 +106,11 @@ export class StatsController {
|
|||
@ResponseSchema(ResponseStatsTeam, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten teams by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopTeamsByDistance() {
|
||||
let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] });
|
||||
let topTeams = teams.sort((team1, team2) => team1.distance - team2.distance).slice(0, 9);
|
||||
let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['parentGroup', 'runners', 'runners.scans', 'runners.scans.track'] });
|
||||
if (!teams || teams.length == 0) {
|
||||
return [];
|
||||
}
|
||||
let topTeams = teams.sort((team1, team2) => team2.distance - team1.distance).slice(0, 10);
|
||||
let responseTeams: ResponseStatsTeam[] = new Array<ResponseStatsTeam>();
|
||||
topTeams.forEach(team => {
|
||||
responseTeams.push(new ResponseStatsTeam(team));
|
||||
|
@ -85,8 +123,11 @@ export class StatsController {
|
|||
@ResponseSchema(ResponseStatsTeam, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten teams by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopTeamsByDonations() {
|
||||
let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] });
|
||||
let topTeams = teams.sort((team1, team2) => team1.distanceDonationAmount - team2.distanceDonationAmount).slice(0, 9);
|
||||
let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['parentGroup', 'runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] });
|
||||
if (!teams || teams.length == 0) {
|
||||
return [];
|
||||
}
|
||||
let topTeams = teams.sort((team1, team2) => team2.distanceDonationAmount - team1.distanceDonationAmount).slice(0, 10);
|
||||
let responseTeams: ResponseStatsTeam[] = new Array<ResponseStatsTeam>();
|
||||
topTeams.forEach(team => {
|
||||
responseTeams.push(new ResponseStatsTeam(team));
|
||||
|
@ -100,7 +141,10 @@ export class StatsController {
|
|||
@OpenAPI({ description: "Returns the top ten organizations by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopOrgsByDistance() {
|
||||
let orgs = await getConnection().getRepository(RunnerOrganization).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.distanceDonations', 'teams.runners.scans.track'] });
|
||||
let topOrgs = orgs.sort((org1, org2) => org1.distance - org2.distance).slice(0, 9);
|
||||
if (!orgs || orgs.length == 0) {
|
||||
return [];
|
||||
}
|
||||
let topOrgs = orgs.sort((org1, org2) => org2.distance - org1.distance).slice(0, 10);
|
||||
let responseOrgs: ResponseStatsOrgnisation[] = new Array<ResponseStatsOrgnisation>();
|
||||
topOrgs.forEach(org => {
|
||||
responseOrgs.push(new ResponseStatsOrgnisation(org));
|
||||
|
@ -113,8 +157,11 @@ export class StatsController {
|
|||
@ResponseSchema(ResponseStatsOrgnisation, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten organizations by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopOrgsByDonations() {
|
||||
let orgs = await getConnection().getRepository(RunnerOrganization).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.distanceDonations', 'teams.runners.scans.track'] });
|
||||
let topOrgs = orgs.sort((org1, org2) => org1.distanceDonationAmount - org2.distanceDonationAmount).slice(0, 9);
|
||||
let orgs = await getConnection().getRepository(RunnerOrganization).find({ relations: ['runners', 'runners.distanceDonations', 'runners.distanceDonations.runner', 'runners.distanceDonations.runner.scans', 'runners.distanceDonations.runner.scans.track', 'teams', 'teams.runners', 'teams.runners.distanceDonations', 'teams.runners.distanceDonations.runner', 'teams.runners.distanceDonations.runner.scans', 'teams.runners.distanceDonations.runner.scans.track'] });
|
||||
if (!orgs || orgs.length == 0) {
|
||||
return [];
|
||||
}
|
||||
let topOrgs = orgs.sort((org1, org2) => org2.distanceDonationAmount - org1.distanceDonationAmount).slice(0, 10);
|
||||
let responseOrgs: ResponseStatsOrgnisation[] = new Array<ResponseStatsOrgnisation>();
|
||||
topOrgs.forEach(org => {
|
||||
responseOrgs.push(new ResponseStatsOrgnisation(org));
|
||||
|
|
|
@ -51,6 +51,9 @@ export abstract class RunnerGroup {
|
|||
*/
|
||||
@IsInt()
|
||||
public get distance(): number {
|
||||
if (!this.runners || this.runners.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
return this.runners.reduce((sum, current) => sum + current.distance, 0);
|
||||
}
|
||||
|
||||
|
|
|
@ -49,7 +49,15 @@ export class ResponseStatsOrgnisation implements IResponse {
|
|||
public constructor(org: RunnerOrganization) {
|
||||
this.name = org.name;
|
||||
this.id = org.id;
|
||||
this.distance = org.distance;
|
||||
this.donationAmount = org.distanceDonationAmount;
|
||||
try {
|
||||
this.distance = org.distance;
|
||||
} catch {
|
||||
this.distance = -1;
|
||||
}
|
||||
try {
|
||||
this.donationAmount = org.distanceDonationAmount;
|
||||
} catch {
|
||||
this.donationAmount = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
IsInt,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString
|
||||
} from "class-validator";
|
||||
import { Runner } from '../entities/Runner';
|
||||
|
@ -55,6 +56,13 @@ export class ResponseStatsRunner implements IResponse {
|
|||
@IsInt()
|
||||
donationAmount: number;
|
||||
|
||||
/**
|
||||
* The runner's fastest laptime in seconds.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
minLaptime?: number;
|
||||
|
||||
/**
|
||||
* The runner's group.
|
||||
*/
|
||||
|
@ -65,13 +73,28 @@ export class ResponseStatsRunner implements IResponse {
|
|||
* Creates a new runner stats response from a runner
|
||||
* @param runner The runner whoes response shall be generated - the following relations have to be resolved: scans, group, distanceDonations, scans.track
|
||||
*/
|
||||
public constructor(runner: Runner) {
|
||||
public constructor(runner: Runner, laptime?: number) {
|
||||
this.id = runner.id;
|
||||
this.firstname = runner.firstname;
|
||||
this.middlename = runner.middlename;
|
||||
if (runner.firstname) {
|
||||
this.middlename = runner.middlename;
|
||||
}
|
||||
this.lastname = runner.lastname;
|
||||
this.distance = runner.distance;
|
||||
this.donationAmount = runner.distanceDonationAmount;
|
||||
try {
|
||||
this.distance = runner.distance;
|
||||
}
|
||||
catch {
|
||||
this.distance = -1;
|
||||
}
|
||||
try {
|
||||
this.donationAmount = runner.distanceDonationAmount;
|
||||
}
|
||||
catch {
|
||||
this.donationAmount = -1;
|
||||
}
|
||||
if (laptime) {
|
||||
this.minLaptime = laptime;
|
||||
}
|
||||
this.group = runner.group.toResponse();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,15 @@ export class ResponseStatsTeam implements IResponse {
|
|||
this.name = team.name;
|
||||
this.id = team.id;
|
||||
this.parent = team.parentGroup.toResponse();
|
||||
this.distance = team.distance;
|
||||
this.donationAmount = team.distanceDonationAmount;
|
||||
try {
|
||||
this.distance = team.distance;
|
||||
} catch {
|
||||
this.distance = -1;
|
||||
}
|
||||
try {
|
||||
this.donationAmount = team.distanceDonationAmount;
|
||||
} catch {
|
||||
this.donationAmount = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
import axios from 'axios';
|
||||
import { config } from '../../config';
|
||||
const base = "http://localhost:" + config.internal_port
|
||||
|
||||
let axios_config_full;
|
||||
let axios_config_stats;
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setTimeout(20000);
|
||||
const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" });
|
||||
let access_token = res.data["access_token"];
|
||||
axios_config_full = {
|
||||
headers: { "authorization": "Bearer " + access_token },
|
||||
validateStatus: undefined
|
||||
};
|
||||
const res2 = await axios.post(base + '/api/statsclients', { username: "demo", password: "demo" }, axios_config_full);
|
||||
access_token = res2.data["key"];
|
||||
axios_config_stats = {
|
||||
headers: { "authorization": "Bearer " + access_token },
|
||||
validateStatus: undefined
|
||||
};
|
||||
});
|
||||
|
||||
describe('GET /api/stats/runners/distance w/o auth should return 200', () => {
|
||||
it('get with invalid token should return 401', async () => {
|
||||
const res = await axios.get(base + '/api/stats/runners/distance', {
|
||||
headers: { "authorization": "Bearer 123123123123123123" },
|
||||
validateStatus: undefined
|
||||
});
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
});
|
||||
// ---------------
|
||||
describe('GET /api/stats should return 200', () => {
|
||||
it('get w/o auth should return 200', async () => {
|
||||
const res = await axios.get(base + '/api/stats', { validateStatus: undefined });
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers['content-type']).toContain("application/json");
|
||||
});
|
||||
it('get w/ auth should return 200', async () => {
|
||||
const res = await axios.get(base + '/api/stats', axios_config_stats);
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers['content-type']).toContain("application/json");
|
||||
});
|
||||
});
|
||||
// ---------------
|
||||
describe('GET /api/stats/runners/* should return 200', () => {
|
||||
it('get by distance w/ auth should return 200', async () => {
|
||||
const res = await axios.get(base + '/api/stats/runners/distance', axios_config_stats);
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers['content-type']).toContain("application/json");
|
||||
});
|
||||
it('get by donations w/ auth should return 200', async () => {
|
||||
const res = await axios.get(base + '/api/stats/runners/donations', axios_config_stats);
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers['content-type']).toContain("application/json");
|
||||
});
|
||||
it('get by laptime w/ auth should return 200', async () => {
|
||||
const res = await axios.get(base + '/api/stats/runners/laptime', axios_config_stats);
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers['content-type']).toContain("application/json");
|
||||
});
|
||||
});
|
||||
// ---------------
|
||||
describe('GET /api/stats/teams/* should return 200', () => {
|
||||
it('get by distance w/ auth should return 200', async () => {
|
||||
const res = await axios.get(base + '/api/stats/teams/distance', axios_config_stats);
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers['content-type']).toContain("application/json");
|
||||
});
|
||||
it('get by donations w/ auth should return 200', async () => {
|
||||
const res = await axios.get(base + '/api/stats/teams/donations', axios_config_stats);
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers['content-type']).toContain("application/json");
|
||||
});
|
||||
});
|
||||
// ---------------
|
||||
describe('GET /api/stats/organizations/* should return 200', () => {
|
||||
it('get by distance w/ auth should return 200', async () => {
|
||||
const res = await axios.get(base + '/api/stats/organizations/distance', axios_config_stats);
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers['content-type']).toContain("application/json");
|
||||
});
|
||||
it('get by donations w/ auth should return 200', async () => {
|
||||
const res = await axios.get(base + '/api/stats/organizations/donations', axios_config_stats);
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers['content-type']).toContain("application/json");
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue