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