Compare commits
5 Commits
52d8ea5988
...
b41dbefa55
Author | SHA1 | Date |
---|---|---|
|
b41dbefa55 | |
|
c7874d6d11 | |
|
c9547c43f6 | |
|
1d6dce5088 | |
|
13fd7fdfc0 |
|
@ -18,7 +18,7 @@ Scheduling for TF2
|
||||||
- **Database:** [PostgreSQL 17.1](https://www.postgresql.org/docs/17/index.html)
|
- **Database:** [PostgreSQL 17.1](https://www.postgresql.org/docs/17/index.html)
|
||||||
(production) / SQLite (development)
|
(production) / SQLite (development)
|
||||||
|
|
||||||
## Setup (dev)
|
## Setup (development, SQLite3)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker compose build
|
docker compose build
|
||||||
|
@ -28,7 +28,7 @@ DATABASE_URI=sqlite:///db.sqlite3 flask db upgrade
|
||||||
|
|
||||||
App will run at port 8000.
|
App will run at port 8000.
|
||||||
|
|
||||||
## Setup (production)
|
## Setup (production, Postgres)
|
||||||
|
|
||||||
Build the frontend app:
|
Build the frontend app:
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ export type { EventSchema } from './models/EventSchema';
|
||||||
export type { EventWithPlayerSchema } from './models/EventWithPlayerSchema';
|
export type { EventWithPlayerSchema } from './models/EventWithPlayerSchema';
|
||||||
export type { EventWithPlayerSchemaList } from './models/EventWithPlayerSchemaList';
|
export type { EventWithPlayerSchemaList } from './models/EventWithPlayerSchemaList';
|
||||||
export type { GetEventPlayersResponse } from './models/GetEventPlayersResponse';
|
export type { GetEventPlayersResponse } from './models/GetEventPlayersResponse';
|
||||||
|
export type { GetMatchQuery } from './models/GetMatchQuery';
|
||||||
export type { MatchSchema } from './models/MatchSchema';
|
export type { MatchSchema } from './models/MatchSchema';
|
||||||
export type { PlayerEventRolesSchema } from './models/PlayerEventRolesSchema';
|
export type { PlayerEventRolesSchema } from './models/PlayerEventRolesSchema';
|
||||||
export type { PlayerRoleSchema } from './models/PlayerRoleSchema';
|
export type { PlayerRoleSchema } from './models/PlayerRoleSchema';
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type GetMatchQuery = {
|
||||||
|
limit: (number | null);
|
||||||
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
export type MatchSchema = {
|
export type MatchSchema = {
|
||||||
blueScore: number;
|
blueScore: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
duration: string;
|
duration: number;
|
||||||
logsTfId: number;
|
logsTfId: number;
|
||||||
logsTfTitle: string;
|
logsTfTitle: string;
|
||||||
matchTime: string;
|
matchTime: string;
|
||||||
|
|
|
@ -4,5 +4,6 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
export type SubmitMatchJson = {
|
export type SubmitMatchJson = {
|
||||||
matchIds: Array<number>;
|
matchIds: Array<number>;
|
||||||
|
teamId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import type { MatchSchema } from './MatchSchema';
|
||||||
export type TeamMatchSchema = {
|
export type TeamMatchSchema = {
|
||||||
match: MatchSchema;
|
match: MatchSchema;
|
||||||
ourScore: number;
|
ourScore: number;
|
||||||
|
teamColor: string;
|
||||||
theirScore: number;
|
theirScore: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -348,11 +348,13 @@ export class DefaultService {
|
||||||
/**
|
/**
|
||||||
* get_matches_for_team <GET>
|
* get_matches_for_team <GET>
|
||||||
* @param teamId
|
* @param teamId
|
||||||
|
* @param limit
|
||||||
* @returns TeamMatchSchemaList OK
|
* @returns TeamMatchSchemaList OK
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public getMatchesForTeam(
|
public getMatchesForTeam(
|
||||||
teamId: number,
|
teamId: number,
|
||||||
|
limit: (number | null),
|
||||||
): CancelablePromise<TeamMatchSchemaList> {
|
): CancelablePromise<TeamMatchSchemaList> {
|
||||||
return this.httpRequest.request({
|
return this.httpRequest.request({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
@ -360,6 +362,9 @@ export class DefaultService {
|
||||||
path: {
|
path: {
|
||||||
'team_id': teamId,
|
'team_id': teamId,
|
||||||
},
|
},
|
||||||
|
query: {
|
||||||
|
'limit': limit,
|
||||||
|
},
|
||||||
errors: {
|
errors: {
|
||||||
422: `Unprocessable Content`,
|
422: `Unprocessable Content`,
|
||||||
},
|
},
|
||||||
|
@ -520,7 +525,7 @@ export class DefaultService {
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public deleteTeam(
|
public deleteTeam(
|
||||||
teamId: string,
|
teamId: number,
|
||||||
): CancelablePromise<any> {
|
): CancelablePromise<any> {
|
||||||
return this.httpRequest.request({
|
return this.httpRequest.request({
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
@ -542,7 +547,7 @@ export class DefaultService {
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public getTeam(
|
public getTeam(
|
||||||
teamId: string,
|
teamId: number,
|
||||||
): CancelablePromise<ViewTeamResponse> {
|
): CancelablePromise<ViewTeamResponse> {
|
||||||
return this.httpRequest.request({
|
return this.httpRequest.request({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
@ -590,7 +595,7 @@ export class DefaultService {
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public editMemberRoles(
|
public editMemberRoles(
|
||||||
teamId: string,
|
teamId: number,
|
||||||
targetPlayerId: string,
|
targetPlayerId: string,
|
||||||
requestBody?: EditMemberRolesJson,
|
requestBody?: EditMemberRolesJson,
|
||||||
): CancelablePromise<void> {
|
): CancelablePromise<void> {
|
||||||
|
@ -617,7 +622,7 @@ export class DefaultService {
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public getIntegrations(
|
public getIntegrations(
|
||||||
teamId: string,
|
teamId: number,
|
||||||
): CancelablePromise<TeamIntegrationSchema> {
|
): CancelablePromise<TeamIntegrationSchema> {
|
||||||
return this.httpRequest.request({
|
return this.httpRequest.request({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
@ -638,7 +643,7 @@ export class DefaultService {
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public updateIntegrations(
|
public updateIntegrations(
|
||||||
teamId: string,
|
teamId: number,
|
||||||
requestBody?: TeamIntegrationSchema,
|
requestBody?: TeamIntegrationSchema,
|
||||||
): CancelablePromise<TeamIntegrationSchema> {
|
): CancelablePromise<TeamIntegrationSchema> {
|
||||||
return this.httpRequest.request({
|
return this.httpRequest.request({
|
||||||
|
@ -661,7 +666,7 @@ export class DefaultService {
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public getInvites(
|
public getInvites(
|
||||||
teamId: string,
|
teamId: number,
|
||||||
): CancelablePromise<TeamInviteSchemaList> {
|
): CancelablePromise<TeamInviteSchemaList> {
|
||||||
return this.httpRequest.request({
|
return this.httpRequest.request({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
@ -682,7 +687,7 @@ export class DefaultService {
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public createInvite(
|
public createInvite(
|
||||||
teamId: string,
|
teamId: number,
|
||||||
): CancelablePromise<TeamInviteSchema> {
|
): CancelablePromise<TeamInviteSchema> {
|
||||||
return this.httpRequest.request({
|
return this.httpRequest.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -704,7 +709,7 @@ export class DefaultService {
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public revokeInvite(
|
public revokeInvite(
|
||||||
teamId: string,
|
teamId: number,
|
||||||
key: string,
|
key: string,
|
||||||
): CancelablePromise<void> {
|
): CancelablePromise<void> {
|
||||||
return this.httpRequest.request({
|
return this.httpRequest.request({
|
||||||
|
@ -729,7 +734,7 @@ export class DefaultService {
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public createOrUpdatePlayer(
|
public createOrUpdatePlayer(
|
||||||
teamId: string,
|
teamId: number,
|
||||||
playerId: string,
|
playerId: string,
|
||||||
requestBody?: AddPlayerJson,
|
requestBody?: AddPlayerJson,
|
||||||
): CancelablePromise<any> {
|
): CancelablePromise<any> {
|
||||||
|
@ -757,7 +762,7 @@ export class DefaultService {
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public removePlayerFromTeam(
|
public removePlayerFromTeam(
|
||||||
teamId: string,
|
teamId: number,
|
||||||
targetPlayerId: string,
|
targetPlayerId: string,
|
||||||
): CancelablePromise<any> {
|
): CancelablePromise<any> {
|
||||||
return this.httpRequest.request({
|
return this.httpRequest.request({
|
||||||
|
@ -781,7 +786,7 @@ export class DefaultService {
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public getTeamMembers(
|
public getTeamMembers(
|
||||||
teamId: string,
|
teamId: number,
|
||||||
): CancelablePromise<ViewTeamMembersResponseList> {
|
): CancelablePromise<ViewTeamMembersResponseList> {
|
||||||
return this.httpRequest.request({
|
return this.httpRequest.request({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useTeamDetails } from "@/composables/team-details";
|
||||||
import { useMatchesStore } from "@/stores/matches";
|
import { useMatchesStore } from "@/stores/matches";
|
||||||
import { DialogClose, DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle, DialogTrigger } from "radix-vue";
|
import { DialogClose, DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle, DialogTrigger } from "radix-vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
|
||||||
const matchesStore = useMatchesStore();
|
const matchesStore = useMatchesStore();
|
||||||
|
|
||||||
|
const { teamId } = useTeamDetails();
|
||||||
|
|
||||||
const urlsText = ref("");
|
const urlsText = ref("");
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
|
@ -15,7 +18,10 @@ function submit() {
|
||||||
})
|
})
|
||||||
.filter((id) => !isNaN(id));
|
.filter((id) => !isNaN(id));
|
||||||
|
|
||||||
matchesStore.submitMatches(ids);
|
matchesStore.submitMatches(ids, teamId.value)
|
||||||
|
.then(() => {
|
||||||
|
urlsText.value = "";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ function disableIntegration() {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h2>logs.tf Auto-Tracking</h2>
|
<h2>logs.tf Auto-Tracking</h2>
|
||||||
<p>Automatically fetch and track match history from logs.tf. (CURRENTLY NOT IMPLEMENTED)</p>
|
<p>Automatically fetch and track match history from logs.tf.</p>
|
||||||
<div v-if="model">
|
<div v-if="model">
|
||||||
<div class="form-group margin">
|
<div class="form-group margin">
|
||||||
<h3>logs.tf API key (optional)</h3>
|
<h3>logs.tf API key (optional)</h3>
|
||||||
|
|
|
@ -1,37 +1,91 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { TeamMatchSchema, TeamSchema } from '@/client';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const matchTime = computed(() => moment(props.teamMatch.match.matchTime)
|
||||||
|
.format("LL LT"));
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
team: TeamSchema,
|
||||||
|
teamMatch: TeamMatchSchema,
|
||||||
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="match-card">
|
<div class="match-card">
|
||||||
|
<div class="match-title">
|
||||||
|
<h3>
|
||||||
|
{{ teamMatch.match.logsTfTitle }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
<div class="match-scores">
|
<div class="match-scores">
|
||||||
<div class="team-and-score">
|
<div class="team-and-score">
|
||||||
<span class="team-name">
|
<span class="team-name">
|
||||||
NVBLU
|
<span v-if="teamMatch.teamColor == 'Blue'">
|
||||||
|
BLU
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
RED
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="score">
|
<span class="score">
|
||||||
3
|
{{ teamMatch.ourScore }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="score">-</span>
|
|
||||||
<div class="team-and-score">
|
<div class="team-and-score">
|
||||||
<span class="score">
|
<span class="score">
|
||||||
2
|
{{ teamMatch.theirScore }}
|
||||||
</span>
|
</span>
|
||||||
<span class="team-name">
|
<span class="team-name">
|
||||||
RED
|
<span v-if="teamMatch.teamColor == 'Blue'">
|
||||||
|
RED
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
BLU
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="bottom-row">
|
||||||
|
<div class="subtext">
|
||||||
|
{{ matchTime }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a :href="'https://logs.tf/' + teamMatch.match.logsTfId" target="_blank" class="button">
|
||||||
|
#{{ teamMatch.match.logsTfId }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.match-card {
|
.match-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border: 1px solid var(--text);
|
border: 1px solid var(--text);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-title {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-row > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.match-scores {
|
.match-scores {
|
||||||
|
|
|
@ -11,8 +11,8 @@ import TeamSettingsView from "@/views/TeamSettingsView.vue";
|
||||||
import TeamSettingsGeneralView from "@/views/TeamSettings/GeneralView.vue";
|
import TeamSettingsGeneralView from "@/views/TeamSettings/GeneralView.vue";
|
||||||
import TeamSettingsIntegrationsView from "@/views/TeamSettings/IntegrationsView.vue";
|
import TeamSettingsIntegrationsView from "@/views/TeamSettings/IntegrationsView.vue";
|
||||||
import TeamSettingsInvitesView from "@/views/TeamSettings/InvitesView.vue";
|
import TeamSettingsInvitesView from "@/views/TeamSettings/InvitesView.vue";
|
||||||
|
import TeamSettingsMatchesView from "@/views/TeamSettings/MatchesView.vue";
|
||||||
import UserSettingsView from "@/views/UserSettingsView.vue";
|
import UserSettingsView from "@/views/UserSettingsView.vue";
|
||||||
import MatchesView from "@/views/MatchesView.vue";
|
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
@ -72,6 +72,11 @@ const router = createRouter({
|
||||||
name: "team-settings/invites",
|
name: "team-settings/invites",
|
||||||
component: TeamSettingsInvitesView,
|
component: TeamSettingsInvitesView,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "matches",
|
||||||
|
name: "team-settings/matches",
|
||||||
|
component: TeamSettingsMatchesView,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -79,11 +84,6 @@ const router = createRouter({
|
||||||
name: "user-settings",
|
name: "user-settings",
|
||||||
component: UserSettingsView,
|
component: UserSettingsView,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/matches",
|
|
||||||
name: "matches",
|
|
||||||
component: MatchesView,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,9 @@ export const useMatchesStore = defineStore("matches", () => {
|
||||||
|
|
||||||
const matches = ref<{ [id: number]: MatchSchema }>({ });
|
const matches = ref<{ [id: number]: MatchSchema }>({ });
|
||||||
|
|
||||||
const teamMatches = ref<{ [id: number]: TeamMatchSchema }>({ });
|
const teamMatches = ref<{ [teamId: number]: TeamMatchSchema[] }>({ });
|
||||||
|
|
||||||
|
const recentMatches = ref<TeamMatchSchema[]>([]);
|
||||||
|
|
||||||
function fetchMatches() {
|
function fetchMatches() {
|
||||||
return clientStore.call(
|
return clientStore.call(
|
||||||
|
@ -18,21 +20,52 @@ export const useMatchesStore = defineStore("matches", () => {
|
||||||
(response) => {
|
(response) => {
|
||||||
response.forEach((match) => {
|
response.forEach((match) => {
|
||||||
matches.value[match.match.logsTfId] = match.match;
|
matches.value[match.match.logsTfId] = match.match;
|
||||||
teamMatches.value[match.match.logsTfId] = match;
|
//teamMatches.value[match.match.logsTfId] = match;
|
||||||
});
|
});
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitMatches(logsTfIds: number[]) {
|
function fetchMatchesForTeam(teamId: number) {
|
||||||
return client.default.submitMatch({ matchIds: logsTfIds });
|
return clientStore.call(
|
||||||
|
fetchMatchesForTeam.name,
|
||||||
|
() => client.default.getMatchesForTeam(teamId, 1024),
|
||||||
|
(response) => {
|
||||||
|
teamMatches.value[teamId] = [];
|
||||||
|
response.forEach((match) => {
|
||||||
|
matches.value[match.match.logsTfId] = match.match;
|
||||||
|
teamMatches.value[teamId].push(match);
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchRecentMatchesForTeam(teamId: number, limit: number) {
|
||||||
|
return clientStore.call(
|
||||||
|
fetchMatchesForTeam.name,
|
||||||
|
() => client.default.getMatchesForTeam(teamId, limit),
|
||||||
|
(response) => {
|
||||||
|
recentMatches.value = [];
|
||||||
|
response.forEach((match) => {
|
||||||
|
matches.value[match.match.logsTfId] = match.match;
|
||||||
|
recentMatches.value.push(match);
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitMatches(logsTfIds: number[], teamId: number) {
|
||||||
|
return client.default.submitMatch({ matchIds: logsTfIds, teamId });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
matches,
|
matches,
|
||||||
teamMatches,
|
teamMatches,
|
||||||
|
recentMatches,
|
||||||
fetchMatches,
|
fetchMatches,
|
||||||
|
fetchMatchesForTeam,
|
||||||
|
fetchRecentMatchesForTeam,
|
||||||
submitMatches,
|
submitMatches,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,7 +35,7 @@ export const useTeamsStore = defineStore("teams", () => {
|
||||||
async function fetchTeam(id: number) {
|
async function fetchTeam(id: number) {
|
||||||
const response = await clientStore.call(
|
const response = await clientStore.call(
|
||||||
fetchTeam.name,
|
fetchTeam.name,
|
||||||
() => client.default.getTeam(id.toString())
|
() => client.default.getTeam(id)
|
||||||
);
|
);
|
||||||
teams[response.team.id] = response.team;
|
teams[response.team.id] = response.team;
|
||||||
return response;
|
return response;
|
||||||
|
@ -44,7 +44,7 @@ export const useTeamsStore = defineStore("teams", () => {
|
||||||
async function fetchTeamMembers(id: number) {
|
async function fetchTeamMembers(id: number) {
|
||||||
const response = await clientStore.call(
|
const response = await clientStore.call(
|
||||||
fetchTeamMembers.name,
|
fetchTeamMembers.name,
|
||||||
() => client.default.getTeamMembers(id.toString())
|
() => client.default.getTeamMembers(id)
|
||||||
);
|
);
|
||||||
teamMembers[id] = response.map((member): ViewTeamMembersResponse => {
|
teamMembers[id] = response.map((member): ViewTeamMembersResponse => {
|
||||||
member.roles = member.roles.sort((a, b) => (a.isMain === b.isMain ? 0 : a.isMain ? -1 : 1));
|
member.roles = member.roles.sort((a, b) => (a.isMain === b.isMain ? 0 : a.isMain ? -1 : 1));
|
||||||
|
@ -68,11 +68,11 @@ export const useTeamsStore = defineStore("teams", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateRoles(teamId: number, playerId: string, roles: RoleSchema[]) {
|
async function updateRoles(teamId: number, playerId: string, roles: RoleSchema[]) {
|
||||||
return await client.default.editMemberRoles(teamId.toString(), playerId, { roles });
|
return await client.default.editMemberRoles(teamId, playerId, { roles });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function leaveTeam(teamId: number) {
|
async function leaveTeam(teamId: number) {
|
||||||
return client.default.removePlayerFromTeam(teamId.toString(), authStore.steamId);
|
return client.default.removePlayerFromTeam(teamId, authStore.steamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -18,7 +18,7 @@ export const useIntegrationsStore = defineStore("integrations", () => {
|
||||||
|
|
||||||
async function getIntegrations(teamId: number) {
|
async function getIntegrations(teamId: number) {
|
||||||
hasLoaded.value = false;
|
hasLoaded.value = false;
|
||||||
const response = await client.default.getIntegrations(teamId.toString());
|
const response = await client.default.getIntegrations(teamId);
|
||||||
setIntegrations(response);
|
setIntegrations(response);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ export const useIntegrationsStore = defineStore("integrations", () => {
|
||||||
discordIntegration: discordIntegration.value ?? null,
|
discordIntegration: discordIntegration.value ?? null,
|
||||||
logsTfIntegration: logsTfIntegration.value ?? null,
|
logsTfIntegration: logsTfIntegration.value ?? null,
|
||||||
};
|
};
|
||||||
const response = await client.default.updateIntegrations(teamId.toString(), body);
|
const response = await client.default.updateIntegrations(teamId, body);
|
||||||
setIntegrations(response);
|
setIntegrations(response);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ export const useInvitesStore = defineStore("invites", () => {
|
||||||
async function getInvites(teamId: number) {
|
async function getInvites(teamId: number) {
|
||||||
return clientStore.call(
|
return clientStore.call(
|
||||||
getInvites.name,
|
getInvites.name,
|
||||||
() => client.default.getInvites(teamId.toString()),
|
() => client.default.getInvites(teamId),
|
||||||
(response) => {
|
(response) => {
|
||||||
teamInvites[teamId] = response;
|
teamInvites[teamId] = response;
|
||||||
return response;
|
return response;
|
||||||
|
@ -21,7 +21,7 @@ export const useInvitesStore = defineStore("invites", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createInvite(teamId: number) {
|
async function createInvite(teamId: number) {
|
||||||
const response = await client.default.createInvite(teamId.toString());
|
const response = await client.default.createInvite(teamId);
|
||||||
teamInvites[teamId].push(response);
|
teamInvites[teamId].push(response);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ export const useInvitesStore = defineStore("invites", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function revokeInvite(teamId: number, key: string) {
|
async function revokeInvite(teamId: number, key: string) {
|
||||||
const response = await client.default.revokeInvite(teamId.toString(), key);
|
const response = await client.default.revokeInvite(teamId, key);
|
||||||
teamInvites[teamId] = teamInvites[teamId].filter((invite) => invite.key != key);
|
teamInvites[teamId] = teamInvites[teamId].filter((invite) => invite.key != key);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,12 @@ import moment from "moment";
|
||||||
import EventList from "@/components/EventList.vue";
|
import EventList from "@/components/EventList.vue";
|
||||||
import { useTeamsEventsStore } from "@/stores/teams/events";
|
import { useTeamsEventsStore } from "@/stores/teams/events";
|
||||||
import MatchCard from "@/components/MatchCard.vue";
|
import MatchCard from "@/components/MatchCard.vue";
|
||||||
|
import { useMatchesStore } from "@/stores/matches";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const teamsStore = useTeamsStore();
|
const teamsStore = useTeamsStore();
|
||||||
const invitesStore = useInvitesStore();
|
const invitesStore = useInvitesStore();
|
||||||
|
const matchesStore = useMatchesStore();
|
||||||
const { team, teamId } = useTeamDetails();
|
const { team, teamId } = useTeamDetails();
|
||||||
|
|
||||||
const creationDate = computed(() => {
|
const creationDate = computed(() => {
|
||||||
|
@ -25,6 +27,7 @@ const key = computed(() => route.query.key);
|
||||||
|
|
||||||
const teamsEventsStore = useTeamsEventsStore();
|
const teamsEventsStore = useTeamsEventsStore();
|
||||||
const events = computed(() => teamsEventsStore.teamEvents[teamId.value]);
|
const events = computed(() => teamsEventsStore.teamEvents[teamId.value]);
|
||||||
|
const matches = computed(() => matchesStore.recentMatches);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
let doFetchTeam = () => {
|
let doFetchTeam = () => {
|
||||||
|
@ -32,6 +35,7 @@ onMounted(() => {
|
||||||
.then(() => {
|
.then(() => {
|
||||||
teamsStore.fetchTeamMembers(teamId.value);
|
teamsStore.fetchTeamMembers(teamId.value);
|
||||||
teamsEventsStore.fetchTeamEvents(teamId.value);
|
teamsEventsStore.fetchTeamEvents(teamId.value);
|
||||||
|
matchesStore.fetchRecentMatchesForTeam(teamId.value, 5);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -76,14 +80,21 @@ onMounted(() => {
|
||||||
<EventList :events="events" :team-context="team" />
|
<EventList :events="events" :team-context="team" />
|
||||||
<h2 id="recent-matches-header">
|
<h2 id="recent-matches-header">
|
||||||
Recent Matches
|
Recent Matches
|
||||||
<!--RouterLink class="button" to="/">
|
<RouterLink class="button" :to="{ name: 'team-settings/matches' }">
|
||||||
<button class="icon" v-tooltip="'View all'">
|
<button class="icon" v-tooltip="'View all'">
|
||||||
<i class="bi bi-arrow-right-circle-fill"></i>
|
<i class="bi bi-arrow-right-circle-fill"></i>
|
||||||
</button>
|
</button>
|
||||||
</RouterLink-->
|
</RouterLink>
|
||||||
</h2>
|
</h2>
|
||||||
<em class="subtext" v-if="true">No recent matches.</em>
|
<em class="subtext" v-if="!matches">
|
||||||
<MatchCard v-else />
|
No recent matches.
|
||||||
|
</em>
|
||||||
|
<MatchCard
|
||||||
|
v-else
|
||||||
|
v-for="match in matches"
|
||||||
|
:team-match="match"
|
||||||
|
:team="team"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,12 +1,21 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AddMatchDialog from "@/components/AddMatchDialog.vue";
|
import AddMatchDialog from "@/components/AddMatchDialog.vue";
|
||||||
|
import { useTeamDetails } from "@/composables/team-details";
|
||||||
import { useMatchesStore } from "@/stores/matches";
|
import { useMatchesStore } from "@/stores/matches";
|
||||||
import { onMounted } from "vue";
|
import { useTeamsStore } from "@/stores/teams";
|
||||||
|
import moment from "moment";
|
||||||
|
import { computed, onMounted } from "vue";
|
||||||
|
|
||||||
const matchesStore = useMatchesStore();
|
const matchesStore = useMatchesStore();
|
||||||
|
const teamsStore = useTeamsStore();
|
||||||
|
|
||||||
|
const { team, teamId } = useTeamDetails();
|
||||||
|
|
||||||
|
const matches = computed(() => matchesStore.teamMatches[teamId.value]);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
matchesStore.fetchMatches();
|
teamsStore.fetchTeam(teamId.value)
|
||||||
|
.then(() => matchesStore.fetchMatchesForTeam(teamId.value));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -15,7 +24,7 @@ onMounted(() => {
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>
|
<h1>
|
||||||
<i class="bi bi-trophy-fill margin"></i>
|
<i class="bi bi-trophy-fill margin"></i>
|
||||||
Matches you've played
|
Matches
|
||||||
</h1>
|
</h1>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<AddMatchDialog />
|
<AddMatchDialog />
|
||||||
|
@ -26,18 +35,20 @@ onMounted(() => {
|
||||||
<tr>
|
<tr>
|
||||||
<th>RED</th>
|
<th>RED</th>
|
||||||
<th>BLU</th>
|
<th>BLU</th>
|
||||||
|
<th>Team</th>
|
||||||
<th>Match Date</th>
|
<th>Match Date</th>
|
||||||
<th>logs.tf URL</th>
|
<th>logs.tf URL</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="match in matchesStore.matches" :key="match.logsTfId">
|
<tr v-for="teamMatch in matches">
|
||||||
<td>{{ match.redScore }}</td>
|
<td>{{ teamMatch.match.redScore }}</td>
|
||||||
<td>{{ match.blueScore }}</td>
|
<td>{{ teamMatch.match.blueScore }}</td>
|
||||||
<td>{{ match.matchTime }}</td>
|
<td>{{ teamMatch.teamColor == 'Blue' ? 'BLU' : 'RED' }}</td>
|
||||||
|
<td>{{ moment(teamMatch.match.matchTime).format("LL LT") }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a :href="`https://logs.tf/${match.logsTfId}`" target="_blank">
|
<a :href="`https://logs.tf/${teamMatch.match.logsTfId}`" target="_blank">
|
||||||
#{{ match.logsTfId }}
|
#{{ teamMatch.match.logsTfId }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
|
@ -42,6 +42,9 @@ onMounted(() => {
|
||||||
<RouterLink class="tab" :to="{ name: 'team-settings/invites' }">
|
<RouterLink class="tab" :to="{ name: 'team-settings/invites' }">
|
||||||
Invites
|
Invites
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<RouterLink class="tab" :to="{ name: 'team-settings/matches' }">
|
||||||
|
Matches
|
||||||
|
</RouterLink>
|
||||||
<hr>
|
<hr>
|
||||||
<button class="destructive-on-hover icon-end" @click="leaveTeam">
|
<button class="destructive-on-hover icon-end" @click="leaveTeam">
|
||||||
Leave team
|
Leave team
|
||||||
|
|
|
@ -4,6 +4,7 @@ from flask_migrate import Migrate
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from sqlalchemy import MetaData
|
from sqlalchemy import MetaData
|
||||||
from sqlalchemy.orm import DeclarativeBase
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
from celery import Celery, Task
|
||||||
|
|
||||||
class BaseModel(DeclarativeBase):
|
class BaseModel(DeclarativeBase):
|
||||||
pass
|
pass
|
||||||
|
@ -33,9 +34,11 @@ def connect_db_with_app(database_uri: str | None, include_migrate=True):
|
||||||
print("Creating tables if they do not exist")
|
print("Creating tables if they do not exist")
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|
||||||
def connect_celery_with_app():
|
def connect_celery_with_app() -> Celery:
|
||||||
|
if "celery" in app.extensions:
|
||||||
|
return app.extensions["celery"]
|
||||||
|
|
||||||
def celery_init_app(app):
|
def celery_init_app(app):
|
||||||
from celery import Celery, Task
|
|
||||||
class FlaskTask(Task):
|
class FlaskTask(Task):
|
||||||
def __call__(self, *args: object, **kwargs: object) -> object:
|
def __call__(self, *args: object, **kwargs: object) -> object:
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
|
@ -55,7 +58,7 @@ def connect_celery_with_app():
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
app.config.from_prefixed_env()
|
app.config.from_prefixed_env()
|
||||||
celery_init_app(app)
|
return celery_init_app(app)
|
||||||
|
|
||||||
def create_app() -> Flask:
|
def create_app() -> Flask:
|
||||||
return Flask(__name__)
|
return Flask(__name__)
|
||||||
|
|
|
@ -14,6 +14,11 @@ from models.player_team import PlayerTeam
|
||||||
from models.team_integration import TeamLogsTfIntegration
|
from models.team_integration import TeamLogsTfIntegration
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
|
|
||||||
|
celery = app_db.connect_celery_with_app()
|
||||||
|
|
||||||
|
@celery.on_after_configure.connect
|
||||||
|
def setup_periodic_task(sender, **kwargs):
|
||||||
|
sender.add_periodic_task(300.0, etl_periodic.s(), name="Fetch logs every 5 minutes")
|
||||||
|
|
||||||
FETCH_URL = "https://logs.tf/api/v1/log/{}"
|
FETCH_URL = "https://logs.tf/api/v1/log/{}"
|
||||||
SEARCH_URL = "https://logs.tf/api/v1/log?limit=25?offset={}"
|
SEARCH_URL = "https://logs.tf/api/v1/log?limit=25?offset={}"
|
||||||
|
@ -26,10 +31,13 @@ def get_log_ids(last_log_id: int):
|
||||||
for summary in response.json()["logs"]:
|
for summary in response.json()["logs"]:
|
||||||
id: int = summary["id"]
|
id: int = summary["id"]
|
||||||
if id == last_log_id:
|
if id == last_log_id:
|
||||||
break
|
print("Reached last log ID", id)
|
||||||
|
return
|
||||||
# yield models.match.RawLogSummary.from_response(summary)
|
# yield models.match.RawLogSummary.from_response(summary)
|
||||||
yield id
|
yield id
|
||||||
current = id
|
current = id
|
||||||
|
sleep(5)
|
||||||
|
break
|
||||||
|
|
||||||
def extract(log_id: int) -> models.match.RawLogDetails:
|
def extract(log_id: int) -> models.match.RawLogDetails:
|
||||||
response = requests.get(FETCH_URL.format(log_id))
|
response = requests.get(FETCH_URL.format(log_id))
|
||||||
|
@ -68,20 +76,6 @@ def extract_steam_ids(players: dict[str, models.match.LogPlayer]):
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def update_playtime(steam_ids: list[int]):
|
def update_playtime(steam_ids: list[int]):
|
||||||
# update players with playtime (recalculate through aggregation)
|
|
||||||
#subquery = (
|
|
||||||
# app_db.db.session.query(
|
|
||||||
# PlayerTeam.id,
|
|
||||||
# func.sum(Match.duration).label("total_playtime")
|
|
||||||
# )
|
|
||||||
# .join(PlayerMatch, PlayerMatch.player_id == PlayerTeam.player_id)
|
|
||||||
# .join(Match, PlayerMatch.match_id == Match.logs_tf_id)
|
|
||||||
# .join(TeamMatch, TeamMatch.match_id == Match.logs_tf_id)
|
|
||||||
# .where(PlayerTeam.player_id.in_(steam_ids))
|
|
||||||
# .where(PlayerTeam.team_id == TeamMatch.team_id)
|
|
||||||
# .group_by(PlayerTeam.id)
|
|
||||||
# .subquery()
|
|
||||||
#)
|
|
||||||
steam_ids_int = list(map(lambda x: int(x), steam_ids))
|
steam_ids_int = list(map(lambda x: int(x), steam_ids))
|
||||||
ptp = (
|
ptp = (
|
||||||
select(
|
select(
|
||||||
|
@ -121,18 +115,13 @@ def update_playtime(steam_ids: list[int]):
|
||||||
app_db.db.session.commit()
|
app_db.db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
def get_common_teams(steam_ids: list[str]):
|
def get_common_teams(steam_ids: list[int]):
|
||||||
return (
|
return (
|
||||||
app_db.db.session.query(
|
app_db.db.session.query(
|
||||||
PlayerTeam.team_id,
|
PlayerTeam.team_id,
|
||||||
func.count(PlayerTeam.team_id),
|
func.count(PlayerTeam.team_id),
|
||||||
TeamLogsTfIntegration.min_team_member_count,
|
|
||||||
#aggregate_func
|
#aggregate_func
|
||||||
)
|
)
|
||||||
.outerjoin(
|
|
||||||
TeamLogsTfIntegration,
|
|
||||||
TeamLogsTfIntegration.team_id == PlayerTeam.team_id
|
|
||||||
)
|
|
||||||
.where(PlayerTeam.player_id.in_(steam_ids))
|
.where(PlayerTeam.player_id.in_(steam_ids))
|
||||||
.group_by(PlayerTeam.team_id)
|
.group_by(PlayerTeam.team_id)
|
||||||
.order_by(func.count(PlayerTeam.team_id).desc())
|
.order_by(func.count(PlayerTeam.team_id).desc())
|
||||||
|
@ -154,9 +143,6 @@ def transform(
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(players) == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not existing_match:
|
if not existing_match:
|
||||||
match = Match()
|
match = Match()
|
||||||
match.logs_tf_id = log_id
|
match.logs_tf_id = log_id
|
||||||
|
@ -169,7 +155,8 @@ def transform(
|
||||||
else:
|
else:
|
||||||
match = existing_match
|
match = existing_match
|
||||||
|
|
||||||
#app_db.db.session.add(match)
|
if len(players) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
for player in players:
|
for player in players:
|
||||||
player_data = details["players"][steam64_to_steam3(player.steam_id)]
|
player_data = details["players"][steam64_to_steam3(player.steam_id)]
|
||||||
|
@ -196,7 +183,9 @@ def transform(
|
||||||
row_tuple = tuple(row)
|
row_tuple = tuple(row)
|
||||||
team_id = row_tuple[0]
|
team_id = row_tuple[0]
|
||||||
player_count = row_tuple[1]
|
player_count = row_tuple[1]
|
||||||
log_min_player_count = row_tuple[2] or 100
|
log_min_player_count = app_db.db.session.query(
|
||||||
|
TeamLogsTfIntegration.min_team_member_count
|
||||||
|
).where(TeamLogsTfIntegration.team_id == team_id).one_or_none() or 100
|
||||||
|
|
||||||
should_create_team_match = False
|
should_create_team_match = False
|
||||||
|
|
||||||
|
@ -230,18 +219,22 @@ def load_specific_match(id: int, team_id: int | None):
|
||||||
)
|
)
|
||||||
|
|
||||||
raw_match = extract(id)
|
raw_match = extract(id)
|
||||||
|
print("Loading match: " + str(id))
|
||||||
app_db.db.session.bulk_save_objects(transform(id, raw_match, match, team_id))
|
app_db.db.session.bulk_save_objects(transform(id, raw_match, match, team_id))
|
||||||
app_db.db.session.commit()
|
app_db.db.session.commit()
|
||||||
sleep(3) # avoid rate limiting if multiple tasks are queued
|
sleep(3) # avoid rate limiting if multiple tasks are queued
|
||||||
|
|
||||||
|
|
||||||
def main():
|
@celery.task
|
||||||
|
def etl_periodic():
|
||||||
last: int = (
|
last: int = (
|
||||||
app_db.db.session.query(
|
app_db.db.session.query(
|
||||||
func.max(models.match.Match.logs_tf_id)
|
func.max(models.match.Match.logs_tf_id)
|
||||||
).scalar()
|
).scalar()
|
||||||
) or 3767233
|
) or 3768715
|
||||||
|
|
||||||
for summary in get_log_ids(last):
|
print("Last log ID: " + str(last))
|
||||||
print(summary)
|
|
||||||
sleep(3)
|
for id in get_log_ids(last):
|
||||||
|
print("Found log: " + str(id))
|
||||||
|
load_specific_match.delay(id, None)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from typing import Optional
|
||||||
from flask import Blueprint, abort
|
from flask import Blueprint, abort
|
||||||
from pydantic.v1 import validator
|
from pydantic.v1 import validator
|
||||||
#from pydantic.functional_validators import field_validator
|
#from pydantic.functional_validators import field_validator
|
||||||
|
@ -41,6 +42,7 @@ def get_match(player: Player, match_id: int, **_):
|
||||||
|
|
||||||
class SubmitMatchJson(BaseModel):
|
class SubmitMatchJson(BaseModel):
|
||||||
match_ids: list[int]
|
match_ids: list[int]
|
||||||
|
team_id: int
|
||||||
|
|
||||||
@validator("match_ids")
|
@validator("match_ids")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -59,15 +61,13 @@ class SubmitMatchJson(BaseModel):
|
||||||
)
|
)
|
||||||
@requires_authentication
|
@requires_authentication
|
||||||
def submit_match(json: SubmitMatchJson, **_):
|
def submit_match(json: SubmitMatchJson, **_):
|
||||||
import sys
|
|
||||||
print(json, file=sys.stderr)
|
|
||||||
if json.match_ids is None:
|
|
||||||
print("json.match_ids is None", file=sys.stderr)
|
|
||||||
|
|
||||||
for id in json.match_ids:
|
for id in json.match_ids:
|
||||||
load_specific_match.delay(id, None)
|
load_specific_match.delay(id, json.team_id)
|
||||||
return { }, 204
|
return { }, 204
|
||||||
|
|
||||||
|
class GetMatchQuery(BaseModel):
|
||||||
|
limit: Optional[int]
|
||||||
|
|
||||||
@api_match.get("/team/<int:team_id>")
|
@api_match.get("/team/<int:team_id>")
|
||||||
@spec.validate(
|
@spec.validate(
|
||||||
resp=Response(
|
resp=Response(
|
||||||
|
@ -77,14 +77,19 @@ def submit_match(json: SubmitMatchJson, **_):
|
||||||
)
|
)
|
||||||
@requires_authentication
|
@requires_authentication
|
||||||
@requires_team_membership()
|
@requires_team_membership()
|
||||||
def get_matches_for_team(team_id: Team, **_):
|
def get_matches_for_team(team_id: Team, query: GetMatchQuery, **_):
|
||||||
matches = (
|
q = (
|
||||||
db.session.query(TeamMatch)
|
db.session.query(TeamMatch)
|
||||||
.where(TeamMatch.team_id == team_id)
|
.where(TeamMatch.team_id == team_id)
|
||||||
.options(joinedload(TeamMatch.match))
|
.options(joinedload(TeamMatch.match))
|
||||||
.all()
|
.order_by(TeamMatch.match_id.desc())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if query and query.limit:
|
||||||
|
q = q.limit(query.limit)
|
||||||
|
|
||||||
|
matches = q.all()
|
||||||
|
|
||||||
return [TeamMatchSchema.from_model(match).dict(by_alias=True) for match in matches], 200
|
return [TeamMatchSchema.from_model(match).dict(by_alias=True) for match in matches], 200
|
||||||
|
|
||||||
@api_match.get("/player")
|
@api_match.get("/player")
|
||||||
|
|
|
@ -19,6 +19,7 @@ class TeamMatchSchema(spec.BaseModel):
|
||||||
match: "MatchSchema"
|
match: "MatchSchema"
|
||||||
our_score: int
|
our_score: int
|
||||||
their_score: int
|
their_score: int
|
||||||
|
team_color: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_model(cls, model: "TeamMatch"):
|
def from_model(cls, model: "TeamMatch"):
|
||||||
|
@ -29,6 +30,7 @@ class TeamMatchSchema(spec.BaseModel):
|
||||||
match=MatchSchema.from_model(model.match),
|
match=MatchSchema.from_model(model.match),
|
||||||
our_score=our_score,
|
our_score=our_score,
|
||||||
their_score=their_score,
|
their_score=their_score,
|
||||||
|
team_color=model.team_color,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -135,7 +135,7 @@ def delete_team(player: Player, team_id: int):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return make_response(200)
|
return make_response(200)
|
||||||
|
|
||||||
@api_team.delete("/id/<int:team_id>/player/<int:target_player_id>/")
|
@api_team.delete("/id/<int:team_id>/player/<target_player_id>/")
|
||||||
@spec.validate(
|
@spec.validate(
|
||||||
resp=Response(
|
resp=Response(
|
||||||
HTTP_200=None,
|
HTTP_200=None,
|
||||||
|
@ -145,7 +145,8 @@ def delete_team(player: Player, team_id: int):
|
||||||
operation_id="remove_player_from_team"
|
operation_id="remove_player_from_team"
|
||||||
)
|
)
|
||||||
@requires_authentication
|
@requires_authentication
|
||||||
def remove_player_from_team(player: Player, team_id: int, target_player_id: int, **kwargs):
|
def remove_player_from_team(player: Player, team_id: int, target_player_id: str, **kwargs):
|
||||||
|
target_player_id: int = int(target_player_id)
|
||||||
player_team = db.session.query(
|
player_team = db.session.query(
|
||||||
PlayerTeam
|
PlayerTeam
|
||||||
).where(
|
).where(
|
||||||
|
@ -202,7 +203,7 @@ class AddPlayerJson(BaseModel):
|
||||||
team_role: PlayerTeam.TeamRole = PlayerTeam.TeamRole.Player
|
team_role: PlayerTeam.TeamRole = PlayerTeam.TeamRole.Player
|
||||||
is_team_leader: bool = False
|
is_team_leader: bool = False
|
||||||
|
|
||||||
@api_team.put("/id/<int:team_id>/player/<int:player_id>/")
|
@api_team.put("/id/<int:team_id>/player/<player_id>/")
|
||||||
@spec.validate(
|
@spec.validate(
|
||||||
resp=Response(
|
resp=Response(
|
||||||
HTTP_200=None,
|
HTTP_200=None,
|
||||||
|
@ -211,7 +212,8 @@ class AddPlayerJson(BaseModel):
|
||||||
),
|
),
|
||||||
operation_id="create_or_update_player"
|
operation_id="create_or_update_player"
|
||||||
)
|
)
|
||||||
def add_player(player: Player, team_id: int, player_id: int, json: AddPlayerJson):
|
def add_player(player: Player, team_id: int, player_id: str, json: AddPlayerJson):
|
||||||
|
player_id: int = int(player_id)
|
||||||
player_team = db.session.query(
|
player_team = db.session.query(
|
||||||
PlayerTeam
|
PlayerTeam
|
||||||
).where(
|
).where(
|
||||||
|
@ -387,7 +389,7 @@ def view_team_members(player: Player, team_id: int, **kwargs):
|
||||||
class EditMemberRolesJson(BaseModel):
|
class EditMemberRolesJson(BaseModel):
|
||||||
roles: list[RoleSchema]
|
roles: list[RoleSchema]
|
||||||
|
|
||||||
@api_team.patch("/id/<int:team_id>/edit-player/<int:target_player_id>")
|
@api_team.patch("/id/<int:team_id>/edit-player/<target_player_id>")
|
||||||
@spec.validate(
|
@spec.validate(
|
||||||
resp=Response(
|
resp=Response(
|
||||||
HTTP_204=None,
|
HTTP_204=None,
|
||||||
|
@ -401,9 +403,10 @@ def edit_member_roles(
|
||||||
json: EditMemberRolesJson,
|
json: EditMemberRolesJson,
|
||||||
player: Player,
|
player: Player,
|
||||||
team_id: int,
|
team_id: int,
|
||||||
target_player_id: int,
|
target_player_id: str,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
target_player_id: int = int(target_player_id)
|
||||||
target_player = db.session.query(
|
target_player = db.session.query(
|
||||||
PlayerTeam
|
PlayerTeam
|
||||||
).where(
|
).where(
|
||||||
|
|
|
@ -20,7 +20,7 @@ services:
|
||||||
|
|
||||||
# Flask service
|
# Flask service
|
||||||
backend:
|
backend:
|
||||||
container_name: backend
|
container_name: backend-production
|
||||||
command: ["gunicorn", "-w", "4", "app:app", "-b", "0.0.0.0:5000"]
|
command: ["gunicorn", "-w", "4", "app:app", "-b", "0.0.0.0:5000"]
|
||||||
image: backend-flask-production
|
image: backend-flask-production
|
||||||
ports:
|
ports:
|
||||||
|
@ -42,13 +42,13 @@ services:
|
||||||
|
|
||||||
# ETL job (runs with the same source as the backend)
|
# ETL job (runs with the same source as the backend)
|
||||||
celery-worker:
|
celery-worker:
|
||||||
container_name: worker
|
container_name: worker-production
|
||||||
command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1
|
command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1 -B
|
||||||
|
image: backend-flask-production
|
||||||
environment:
|
environment:
|
||||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||||
- DATABASE_URI=postgresql+psycopg://db:5432
|
- DATABASE_URI=postgresql+psycopg://postgres:password@db:5432/availabilitf
|
||||||
image: backend-flask-production
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend-flask:/app
|
- ./backend-flask:/app
|
||||||
networks:
|
networks:
|
||||||
|
@ -68,7 +68,8 @@ services:
|
||||||
|
|
||||||
# Vue + Vite service
|
# Vue + Vite service
|
||||||
frontend:
|
frontend:
|
||||||
container_name: frontend
|
container_name: frontend-production
|
||||||
|
image: frontend-production
|
||||||
build:
|
build:
|
||||||
context: ./availabili.tf
|
context: ./availabili.tf
|
||||||
dockerfile: Dockerfile.prod
|
dockerfile: Dockerfile.prod
|
||||||
|
|
|
@ -22,7 +22,7 @@ services:
|
||||||
# ETL job (runs with the same source as the backend)
|
# ETL job (runs with the same source as the backend)
|
||||||
celery-worker:
|
celery-worker:
|
||||||
container_name: worker
|
container_name: worker
|
||||||
command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1
|
command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1 -B
|
||||||
environment:
|
environment:
|
||||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||||
|
@ -51,6 +51,8 @@ services:
|
||||||
context: ./availabili.tf
|
context: ./availabili.tf
|
||||||
environment:
|
environment:
|
||||||
VITE_API_URL: http://localhost:8000 # API endpoint
|
VITE_API_URL: http://localhost:8000 # API endpoint
|
||||||
|
ports:
|
||||||
|
- 5173:5173
|
||||||
volumes:
|
volumes:
|
||||||
- ./availabili.tf:/app
|
- ./availabili.tf:/app
|
||||||
networks:
|
networks:
|
||||||
|
@ -62,7 +64,7 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "8000:80"
|
- "8000:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ./nginx:/etc/nginx/conf.d
|
- ./nginx/development.conf:/etc/nginx/nginx.conf
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
- frontend
|
- frontend
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
|
|
||||||
# Proxy for the Vite frontend
|
|
||||||
location / {
|
|
||||||
proxy_pass http://frontend:5173;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Proxy for the Flask backend API
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://backend:5000;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /apidoc/ {
|
|
||||||
proxy_pass http://backend:5000;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
# Proxy for the Vite frontend
|
||||||
|
location / {
|
||||||
|
proxy_pass http://frontend:5173;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy for the Flask backend API
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://backend:5000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /apidoc/ {
|
||||||
|
proxy_pass http://backend:5000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue