From 71cc25dbb2c9eb6d9a1874f31a2d76d33351f5ac Mon Sep 17 00:00:00 2001 From: HumanoidSandvichDispenser Date: Mon, 25 Nov 2024 20:16:01 -0800 Subject: [PATCH] refactor: Change team integrations structure Refactor the team integrations structure to use one-to-one relationships for Discord and logs.tf integrations. Update the frontend to handle the new structure and remove unused integration types. Adjust backend endpoints and models accordingly. Add migration scripts to update the database schema. --- availabili.tf/src/assets/main.css | 5 +- availabili.tf/src/client/index.ts | 3 +- .../models/AbstractTeamIntegrationSchema.ts | 8 - .../src/client/models/EventSchema.ts | 2 +- .../models/TeamDiscordIntegrationSchema.ts | 4 +- .../client/models/TeamIntegrationSchema.ts | 7 +- .../models/TeamIntegrationSchemaList.ts | 6 - .../models/TeamLogsTfIntegrationSchema.ts | 9 ++ .../src/client/services/DefaultService.ts | 66 +------- .../src/components/DiscordIntegrationForm.vue | 60 ++++++++ .../src/components/LogsTfIntegrationForm.vue | 64 ++++++++ .../src/stores/teams/integrations.ts | 53 ++++--- availabili.tf/src/views/TeamDetailsView.vue | 60 +++++--- .../views/TeamSettings/IntegrationsView.vue | 34 ++--- ...91293_change_integrations_to_one_to_one.py | 42 +++++ .../f802d763a7b4_drop_integrations_tables.py | 28 ++++ backend-flask/models/team.py | 69 ++++++++- backend-flask/models/team_integration.py | 81 +++++----- backend-flask/team_integration.py | 143 ++++-------------- 19 files changed, 435 insertions(+), 309 deletions(-) delete mode 100644 availabili.tf/src/client/models/AbstractTeamIntegrationSchema.ts delete mode 100644 availabili.tf/src/client/models/TeamIntegrationSchemaList.ts create mode 100644 availabili.tf/src/client/models/TeamLogsTfIntegrationSchema.ts create mode 100644 availabili.tf/src/components/DiscordIntegrationForm.vue create mode 100644 availabili.tf/src/components/LogsTfIntegrationForm.vue create mode 100644 backend-flask/migrations/versions/392454b91293_change_integrations_to_one_to_one.py create mode 100644 backend-flask/migrations/versions/f802d763a7b4_drop_integrations_tables.py diff --git a/availabili.tf/src/assets/main.css b/availabili.tf/src/assets/main.css index 2fbeaf2..4110ee0 100644 --- a/availabili.tf/src/assets/main.css +++ b/availabili.tf/src/assets/main.css @@ -247,8 +247,8 @@ input { } .form-group.margin { - margin-top: 16px; - margin-bottom: 16px; + margin-top: 1rem; + margin-bottom: 1rem; } .form-group.row { @@ -259,6 +259,7 @@ input { .form-group .action-buttons { display: flex; justify-content: end; + gap: 0.5rem; } hr { diff --git a/availabili.tf/src/client/index.ts b/availabili.tf/src/client/index.ts index 5e6f045..9033139 100644 --- a/availabili.tf/src/client/index.ts +++ b/availabili.tf/src/client/index.ts @@ -10,7 +10,6 @@ export { CancelablePromise, CancelError } from './core/CancelablePromise'; export { OpenAPI } from './core/OpenAPI'; export type { OpenAPIConfig } from './core/OpenAPI'; -export type { AbstractTeamIntegrationSchema } from './models/AbstractTeamIntegrationSchema'; export type { AddPlayerJson } from './models/AddPlayerJson'; export type { AvailabilitySchema } from './models/AvailabilitySchema'; export type { CreateEventJson } from './models/CreateEventJson'; @@ -25,9 +24,9 @@ export type { RoleSchema } from './models/RoleSchema'; export type { SetUsernameJson } from './models/SetUsernameJson'; export type { TeamDiscordIntegrationSchema } from './models/TeamDiscordIntegrationSchema'; export type { TeamIntegrationSchema } from './models/TeamIntegrationSchema'; -export type { TeamIntegrationSchemaList } from './models/TeamIntegrationSchemaList'; export type { TeamInviteSchema } from './models/TeamInviteSchema'; export type { TeamInviteSchemaList } from './models/TeamInviteSchemaList'; +export type { TeamLogsTfIntegrationSchema } from './models/TeamLogsTfIntegrationSchema'; export { TeamRole } from './models/TeamRole'; export type { TeamSchema } from './models/TeamSchema'; export type { ValidationError } from './models/ValidationError'; diff --git a/availabili.tf/src/client/models/AbstractTeamIntegrationSchema.ts b/availabili.tf/src/client/models/AbstractTeamIntegrationSchema.ts deleted file mode 100644 index ef29f3f..0000000 --- a/availabili.tf/src/client/models/AbstractTeamIntegrationSchema.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { TeamDiscordIntegrationSchema } from './TeamDiscordIntegrationSchema'; -import type { TeamIntegrationSchema } from './TeamIntegrationSchema'; -export type AbstractTeamIntegrationSchema = (TeamDiscordIntegrationSchema | TeamIntegrationSchema); - diff --git a/availabili.tf/src/client/models/EventSchema.ts b/availabili.tf/src/client/models/EventSchema.ts index e6d3f84..faba155 100644 --- a/availabili.tf/src/client/models/EventSchema.ts +++ b/availabili.tf/src/client/models/EventSchema.ts @@ -4,7 +4,7 @@ /* eslint-disable */ export type EventSchema = { createdAt: string; - description: string; + description?: string; id: number; name: string; startTime: string; diff --git a/availabili.tf/src/client/models/TeamDiscordIntegrationSchema.ts b/availabili.tf/src/client/models/TeamDiscordIntegrationSchema.ts index 2f44a58..32b5241 100644 --- a/availabili.tf/src/client/models/TeamDiscordIntegrationSchema.ts +++ b/availabili.tf/src/client/models/TeamDiscordIntegrationSchema.ts @@ -3,9 +3,7 @@ /* tslint:disable */ /* eslint-disable */ export type TeamDiscordIntegrationSchema = { - id: number; - integrationType: string; - teamId: number; + webhookBotName: string; webhookUrl: string; }; diff --git a/availabili.tf/src/client/models/TeamIntegrationSchema.ts b/availabili.tf/src/client/models/TeamIntegrationSchema.ts index 9bab4dc..89c6667 100644 --- a/availabili.tf/src/client/models/TeamIntegrationSchema.ts +++ b/availabili.tf/src/client/models/TeamIntegrationSchema.ts @@ -2,9 +2,10 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { TeamDiscordIntegrationSchema } from './TeamDiscordIntegrationSchema'; +import type { TeamLogsTfIntegrationSchema } from './TeamLogsTfIntegrationSchema'; export type TeamIntegrationSchema = { - id: number; - integrationType: string; - teamId: number; + discordIntegration?: TeamDiscordIntegrationSchema; + logsTfIntegration?: TeamLogsTfIntegrationSchema; }; diff --git a/availabili.tf/src/client/models/TeamIntegrationSchemaList.ts b/availabili.tf/src/client/models/TeamIntegrationSchemaList.ts deleted file mode 100644 index f48ebff..0000000 --- a/availabili.tf/src/client/models/TeamIntegrationSchemaList.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { TeamIntegrationSchema } from './TeamIntegrationSchema'; -export type TeamIntegrationSchemaList = Array; diff --git a/availabili.tf/src/client/models/TeamLogsTfIntegrationSchema.ts b/availabili.tf/src/client/models/TeamLogsTfIntegrationSchema.ts new file mode 100644 index 0000000..30b1d7e --- /dev/null +++ b/availabili.tf/src/client/models/TeamLogsTfIntegrationSchema.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type TeamLogsTfIntegrationSchema = { + logsTfApiKey: string; + minTeamMemberCount: number; +}; + diff --git a/availabili.tf/src/client/services/DefaultService.ts b/availabili.tf/src/client/services/DefaultService.ts index c5c1fb0..89f3271 100644 --- a/availabili.tf/src/client/services/DefaultService.ts +++ b/availabili.tf/src/client/services/DefaultService.ts @@ -2,7 +2,6 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import type { AbstractTeamIntegrationSchema } from '../models/AbstractTeamIntegrationSchema'; import type { AddPlayerJson } from '../models/AddPlayerJson'; import type { CreateEventJson } from '../models/CreateEventJson'; import type { CreateTeamJson } from '../models/CreateTeamJson'; @@ -13,7 +12,6 @@ import type { PlayerSchema } from '../models/PlayerSchema'; import type { PutScheduleForm } from '../models/PutScheduleForm'; import type { SetUsernameJson } from '../models/SetUsernameJson'; import type { TeamIntegrationSchema } from '../models/TeamIntegrationSchema'; -import type { TeamIntegrationSchemaList } from '../models/TeamIntegrationSchemaList'; import type { TeamInviteSchema } from '../models/TeamInviteSchema'; import type { TeamInviteSchemaList } from '../models/TeamInviteSchemaList'; import type { ViewAvailablePlayersResponse } from '../models/ViewAvailablePlayersResponse'; @@ -421,12 +419,12 @@ export class DefaultService { /** * get_integrations * @param teamId - * @returns TeamIntegrationSchemaList OK + * @returns TeamIntegrationSchema OK * @throws ApiError */ public getIntegrations( teamId: string, - ): CancelablePromise { + ): CancelablePromise { return this.httpRequest.request({ method: 'GET', url: '/api/team/id/{team_id}/integrations', @@ -434,53 +432,26 @@ export class DefaultService { 'team_id': teamId, }, errors: { - 404: `Not Found`, 422: `Unprocessable Entity`, }, }); } /** - * delete_integration + * update_integration * @param teamId - * @param integrationId - * @returns void - * @throws ApiError - */ - public deleteIntegration( - teamId: string, - integrationId: string, - ): CancelablePromise { - return this.httpRequest.request({ - method: 'DELETE', - url: '/api/team/id/{team_id}/integrations/{integration_id}', - path: { - 'team_id': teamId, - 'integration_id': integrationId, - }, - errors: { - 422: `Unprocessable Entity`, - }, - }); - } - /** - * update_integration - * @param teamId - * @param integrationId * @param requestBody * @returns TeamIntegrationSchema OK * @throws ApiError */ - public updateIntegration( + public updateIntegrations( teamId: string, - integrationId: string, - requestBody?: AbstractTeamIntegrationSchema, + requestBody?: TeamIntegrationSchema, ): CancelablePromise { return this.httpRequest.request({ - method: 'PATCH', - url: '/api/team/id/{team_id}/integrations/{integration_id}', + method: 'PUT', + url: '/api/team/id/{team_id}/integrations', path: { 'team_id': teamId, - 'integration_id': integrationId, }, body: requestBody, mediaType: 'application/json', @@ -489,29 +460,6 @@ export class DefaultService { }, }); } - /** - * create_integration - * @param teamId - * @param integrationType - * @returns TeamIntegrationSchema OK - * @throws ApiError - */ - public createIntegration( - teamId: string, - integrationType: string, - ): CancelablePromise { - return this.httpRequest.request({ - method: 'POST', - url: '/api/team/id/{team_id}/integrations/{integration_type}', - path: { - 'team_id': teamId, - 'integration_type': integrationType, - }, - errors: { - 422: `Unprocessable Entity`, - }, - }); - } /** * get_invites * @param teamId diff --git a/availabili.tf/src/components/DiscordIntegrationForm.vue b/availabili.tf/src/components/DiscordIntegrationForm.vue new file mode 100644 index 0000000..0eee1b8 --- /dev/null +++ b/availabili.tf/src/components/DiscordIntegrationForm.vue @@ -0,0 +1,60 @@ + + + diff --git a/availabili.tf/src/components/LogsTfIntegrationForm.vue b/availabili.tf/src/components/LogsTfIntegrationForm.vue new file mode 100644 index 0000000..113ebcb --- /dev/null +++ b/availabili.tf/src/components/LogsTfIntegrationForm.vue @@ -0,0 +1,64 @@ + + + diff --git a/availabili.tf/src/stores/teams/integrations.ts b/availabili.tf/src/stores/teams/integrations.ts index a084ec1..9a5da0b 100644 --- a/availabili.tf/src/stores/teams/integrations.ts +++ b/availabili.tf/src/stores/teams/integrations.ts @@ -1,44 +1,49 @@ import { defineStore } from "pinia"; -import { reactive, type Reactive } from "vue"; +import { ref } from "vue"; import { useClientStore } from "../client"; -import { type TeamIntegrationSchema, type AbstractTeamIntegrationSchema } from "@/client"; +import type { + TeamIntegrationSchema, + TeamDiscordIntegrationSchema, + TeamLogsTfIntegrationSchema +} from "@/client"; export const useIntegrationsStore = defineStore("integrations", () => { - const clientStore = useClientStore(); - const client = clientStore.client; + const hasLoaded = ref(false); - const teamIntegrations = reactive<{ [id: number]: TeamIntegrationSchema[] }>({}); + const client = useClientStore().client; + + const discordIntegration = ref(); + + const logsTfIntegration = ref(); async function getIntegrations(teamId: number) { + hasLoaded.value = false; const response = await client.default.getIntegrations(teamId.toString()); - teamIntegrations[teamId] = response; + setIntegrations(response); return response; } - async function createIntegration(teamId: number, integrationType: string) { - const response = await client.default.createIntegration(teamId.toString(), integrationType); - teamIntegrations[teamId].push(response); - return response; + function setIntegrations(schema: TeamIntegrationSchema) { + discordIntegration.value = schema.discordIntegration; + logsTfIntegration.value = schema.logsTfIntegration; + hasLoaded.value = true; } - async function deleteIntegration(teamId: number, integrationId: number) { - const response = await client.default.deleteIntegration(teamId.toString(), integrationId.toString()); - teamIntegrations[teamId] = teamIntegrations[teamId].filter((integration) => integration.id != integrationId); - return response; - } - - async function updateIntegration(teamId: number, integration: AbstractTeamIntegrationSchema) { - const response = await client.default.updateIntegration(teamId.toString(), integration.id.toString(), integration); - const index = teamIntegrations[teamId].findIndex((x) => x.id == integration.id); - teamIntegrations[teamId][index] = response; + async function updateIntegrations(teamId: number) { + const body: TeamIntegrationSchema = { + discordIntegration: discordIntegration.value, + logsTfIntegration: logsTfIntegration.value, + }; + const response = await client.default.updateIntegrations(teamId.toString(), body); + setIntegrations(response); return response; } return { - teamIntegrations, + hasLoaded, + discordIntegration, + logsTfIntegration, getIntegrations, - createIntegration, - deleteIntegration, - updateIntegration, + updateIntegrations, }; }); diff --git a/availabili.tf/src/views/TeamDetailsView.vue b/availabili.tf/src/views/TeamDetailsView.vue index 5b9b30b..2b33cca 100644 --- a/availabili.tf/src/views/TeamDetailsView.vue +++ b/availabili.tf/src/views/TeamDetailsView.vue @@ -46,32 +46,42 @@ onMounted(() => { diff --git a/backend-flask/migrations/versions/392454b91293_change_integrations_to_one_to_one.py b/backend-flask/migrations/versions/392454b91293_change_integrations_to_one_to_one.py new file mode 100644 index 0000000..4d3152e --- /dev/null +++ b/backend-flask/migrations/versions/392454b91293_change_integrations_to_one_to_one.py @@ -0,0 +1,42 @@ +"""Change integrations to one-to-one + +Revision ID: 392454b91293 +Revises: f802d763a7b4 +Create Date: 2024-11-25 18:36:15.293593 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '392454b91293' +down_revision = 'f802d763a7b4' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('team_discord_integrations', + sa.Column('team_id', sa.Integer(), nullable=False), + sa.Column('webhook_url', sa.String(), nullable=False), + sa.Column('webhook_bot_name', sa.String(), nullable=False), + sa.ForeignKeyConstraint(['team_id'], ['teams.id'], ), + sa.PrimaryKeyConstraint('team_id') + ) + op.create_table('team_logs_tf_integrations', + sa.Column('team_id', sa.Integer(), nullable=False), + sa.Column('logs_tf_api_key', sa.String(), nullable=True), + sa.Column('min_team_member_count', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['team_id'], ['teams.id'], ), + sa.PrimaryKeyConstraint('team_id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('team_logs_tf_integrations') + op.drop_table('team_discord_integrations') + # ### end Alembic commands ### diff --git a/backend-flask/migrations/versions/f802d763a7b4_drop_integrations_tables.py b/backend-flask/migrations/versions/f802d763a7b4_drop_integrations_tables.py new file mode 100644 index 0000000..b8fe1f2 --- /dev/null +++ b/backend-flask/migrations/versions/f802d763a7b4_drop_integrations_tables.py @@ -0,0 +1,28 @@ +"""Drop integrations tables + +Revision ID: f802d763a7b4 +Revises: dcf5ffd0ec73 +Create Date: 2024-11-25 18:34:08.136071 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f802d763a7b4' +down_revision = 'dcf5ffd0ec73' +branch_labels = None +depends_on = None + + +def upgrade(): + # drop integrations tables + op.drop_table("team_discord_integrations") + op.drop_table("team_logs_tf_integrations") + op.drop_table("team_integrations") + pass + + +def downgrade(): + pass diff --git a/backend-flask/models/team.py b/backend-flask/models/team.py index c4e4389..bf2893b 100644 --- a/backend-flask/models/team.py +++ b/backend-flask/models/team.py @@ -18,9 +18,68 @@ class Team(app_db.BaseModel): players: Mapped[list["PlayerTeam"]] = relationship(back_populates="team") invites: Mapped[list["TeamInvite"]] = relationship(back_populates="team") - integrations: Mapped[list["TeamIntegration"]] = relationship(back_populates="team") events: Mapped[list["Event"]] = relationship(back_populates="team") + discord_integration: Mapped["TeamDiscordIntegration"] = relationship( + "TeamDiscordIntegration", + back_populates="team", + uselist=False, + lazy="raise", + ) + + logs_tf_integration: Mapped["TeamLogsTfIntegration"] = relationship( + "TeamLogsTfIntegration", + back_populates="team", + uselist=False, + lazy="raise", + ) + + def update_integrations(self, integrations: "TeamIntegrationSchema"): + if integrations.discord_integration: + print("DISCORD!!!") + discord_integration = self.discord_integration \ + or TeamDiscordIntegration() + discord_integration.webhook_url = integrations \ + .discord_integration.webhook_url + discord_integration.webhook_bot_name = integrations \ + .discord_integration.webhook_bot_name + + if discord_integration.team_id is None: + discord_integration.team_id = self.id + app_db.db.session.add(discord_integration) + elif self.discord_integration: + app_db.db.session.delete(self.discord_integration) + + if integrations.logs_tf_integration: + logs_tf_integration = self.logs_tf_integration \ + or TeamLogsTfIntegration() + logs_tf_integration.logs_tf_api_key = integrations \ + .logs_tf_integration.logs_tf_api_key or "" + logs_tf_integration.min_team_member_count = integrations \ + .logs_tf_integration.min_team_member_count + + if logs_tf_integration.team_id is None: + logs_tf_integration.team_id = self.id + app_db.db.session.add(logs_tf_integration) + elif self.logs_tf_integration: + app_db.db.session.delete(self.logs_tf_integration) + + def get_integrations(self) -> "TeamIntegrationSchema": + discord_integration = None + logs_tf_integration = None + if self.discord_integration: + discord_integration = TeamDiscordIntegrationSchema.from_model( + self.discord_integration + ) + if self.logs_tf_integration: + logs_tf_integration = TeamLogsTfIntegrationSchema.from_model( + self.logs_tf_integration + ) + return TeamIntegrationSchema( + discord_integration=discord_integration, + logs_tf_integration=logs_tf_integration, + ) + created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now()) class TeamSchema(spec.BaseModel): @@ -41,6 +100,12 @@ class TeamSchema(spec.BaseModel): ) from models.player_team import PlayerTeam -from models.team_integration import TeamIntegration from models.team_invite import TeamInvite +from models.team_integration import ( + TeamDiscordIntegration, + TeamDiscordIntegrationSchema, + TeamIntegrationSchema, + TeamLogsTfIntegration, + TeamLogsTfIntegrationSchema, +) from models.event import Event diff --git a/backend-flask/models/team_integration.py b/backend-flask/models/team_integration.py index 9ab6764..d860d28 100644 --- a/backend-flask/models/team_integration.py +++ b/backend-flask/models/team_integration.py @@ -1,6 +1,3 @@ -#from typing import cast, override -from typing import TypeAlias, Union -from pydantic_core.core_schema import UnionSchema from sqlalchemy.orm import mapped_column, relationship from sqlalchemy.orm.attributes import Mapped from sqlalchemy.orm.properties import ForeignKey @@ -9,60 +6,52 @@ import app_db import spec -class TeamIntegration(app_db.BaseModel): - __tablename__ = "team_integrations" - - id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - team_id: Mapped[int] = mapped_column(Integer, ForeignKey("teams.id")) - integration_type: Mapped[str] - - team: Mapped["Team"] = relationship(back_populates="integrations") - - __mapper_args__ = { - "polymorphic_identity": "team_integrations", - "polymorphic_on": "integration_type", - } - -class TeamDiscordIntegration(TeamIntegration): +class TeamDiscordIntegration(app_db.BaseModel): __tablename__ = "team_discord_integrations" - integration_id: Mapped[int] = mapped_column(ForeignKey("team_integrations.id"), primary_key=True) - webhook_url: Mapped[str] = mapped_column(String(255), nullable=True) + team_id: Mapped[int] = mapped_column(ForeignKey("teams.id"), primary_key=True) + webhook_url: Mapped[str] = mapped_column(String) + webhook_bot_name: Mapped[str] = mapped_column(String) - __mapper_args__ = { - "polymorphic_identity": "team_discord_integrations", - } + team: Mapped["Team"] = relationship("Team", back_populates="discord_integration") -class TeamIntegrationSchema(spec.BaseModel): - id: int - team_id: int - integration_type: str - - @classmethod - def from_model(cls, model: TeamIntegration): - if model.integration_type == "team_discord_integrations": - if isinstance(model, TeamDiscordIntegration): - return TeamDiscordIntegrationSchema._from_model_discord(model) - raise TypeError() - -class TeamDiscordIntegrationSchema(TeamIntegrationSchema): +class TeamDiscordIntegrationSchema(spec.BaseModel): webhook_url: str + webhook_bot_name: str @classmethod - def _from_model_discord(cls, model: TeamDiscordIntegration): - assert model.integration_id != None + def from_model(cls, model: TeamDiscordIntegration) -> "TeamDiscordIntegrationSchema": return cls( - id=model.integration_id, - team_id=model.team_id, - integration_type=model.integration_type, - webhook_url=model.webhook_url + webhook_url=model.webhook_url, + webhook_bot_name=model.webhook_bot_name, ) -class ExampleIntegrationSchema(TeamIntegrationSchema): - test: str +class TeamLogsTfIntegration(app_db.BaseModel): + __tablename__ = "team_logs_tf_integrations" -class AbstractTeamIntegrationSchema(spec.BaseModel): - __root__: TeamDiscordIntegrationSchema | TeamIntegrationSchema + team_id: Mapped[int] = mapped_column(ForeignKey("teams.id"), primary_key=True) + logs_tf_api_key: Mapped[str | None] = mapped_column(String, nullable=True) + + # requires at least this many team members in a single team in the log to + # be automatically loaded into the database + min_team_member_count: Mapped[int] = mapped_column(Integer, default=4) + + team: Mapped["Team"] = relationship("Team", back_populates="logs_tf_integration") + +class TeamLogsTfIntegrationSchema(spec.BaseModel): + logs_tf_api_key: str | None + min_team_member_count: int + + @classmethod + def from_model(cls, model: TeamLogsTfIntegration) -> "TeamLogsTfIntegrationSchema": + return cls( + logs_tf_api_key=model.logs_tf_api_key, + min_team_member_count=model.min_team_member_count, + ) + +class TeamIntegrationSchema(spec.BaseModel): + discord_integration: TeamDiscordIntegrationSchema | None + logs_tf_integration: TeamLogsTfIntegrationSchema | None from models.team import Team diff --git a/backend-flask/team_integration.py b/backend-flask/team_integration.py index a323eb5..212024d 100644 --- a/backend-flask/team_integration.py +++ b/backend-flask/team_integration.py @@ -1,14 +1,12 @@ -from flask import Blueprint, abort, make_response +from flask import Blueprint from spectree import Response -from typing import cast - - -from app_db import db +from sqlalchemy.orm import joinedload from middleware import assert_team_authority, requires_authentication, requires_team_membership -from models.player import Player from models.player_team import PlayerTeam -from models.team_integration import AbstractTeamIntegrationSchema, TeamDiscordIntegration, TeamIntegration, TeamIntegrationSchema +from models.team import Team +from models.team_integration import TeamIntegrationSchema from spec import spec +from app_db import db api_team_integration = Blueprint("team_integration", __name__) @@ -16,130 +14,47 @@ api_team_integration = Blueprint("team_integration", __name__) @api_team_integration.get("/id//integrations") @spec.validate( resp=Response( - HTTP_200=list[TeamIntegrationSchema], - HTTP_404=None, + HTTP_200=TeamIntegrationSchema, ), operation_id="get_integrations" ) @requires_authentication -def get_integrations(player: Player, team_id: int, **_): - player_team = db.session.query( - PlayerTeam +@requires_team_membership() +def get_integrations(player_team: PlayerTeam, **_): + team = db.session.query( + Team ).where( - PlayerTeam.player_id == player.steam_id - ).where( - PlayerTeam.team_id == team_id - ).one_or_none() + Team.id == player_team.team_id + ).options( + joinedload(Team.discord_integration), + joinedload(Team.logs_tf_integration), + ).one() - if not player_team: - abort(404) + return team.get_integrations().dict(by_alias=True) - integrations = db.session.query( - TeamIntegration - ).where( - TeamIntegration.team_id == team_id - ).all() - - def map_integration_to_schema(integration: TeamIntegration): - return TeamIntegrationSchema.from_model( - integration - ).dict(by_alias=True) - - return list(map(map_integration_to_schema, integrations)) - -@api_team_integration.post("/id//integrations/") +@api_team_integration.put("/id//integrations") @spec.validate( resp=Response( HTTP_200=TeamIntegrationSchema, ), - operation_id="create_integration" + operation_id="update_integrations" ) @requires_authentication @requires_team_membership() -def create_integration(player_team: PlayerTeam, integration_type: str, **_): - assert_team_authority(player_team) - - if integration_type == "discord": - integration = TeamDiscordIntegration() - integration.team_id = player_team.team_id - integration.webhook_url = "" - else: - abort(404) - - db.session.add(integration) - db.session.commit() - - return TeamIntegrationSchema.from_model( - integration - ).dict(by_alias=True), 200 - -@api_team_integration.delete("/id//integrations/") -@spec.validate( - resp=Response( - HTTP_204=None, - ), - operation_id="delete_integration" -) -@requires_authentication -@requires_team_membership() -def delete_integration(player_team: PlayerTeam, integration_id: int, **_): - assert_team_authority(player_team) - - integration = db.session.query( - TeamIntegration - ).where( - TeamIntegration.team_id == player_team.team_id - ).where( - TeamIntegration.id == integration_id - ).one_or_none() - - if not integration: - abort(404) - - db.session.delete(integration) - db.session.commit() - - return make_response({ }, 204) - -@api_team_integration.patch("/id//integrations/") -@spec.validate( - resp=Response( - HTTP_200=TeamIntegrationSchema, - ), - operation_id="update_integration" -) -@requires_authentication -@requires_team_membership() -def update_integration( +def update_integrations( player_team: PlayerTeam, - integration_id: int, - json: AbstractTeamIntegrationSchema, + json: TeamIntegrationSchema, **_ ): assert_team_authority(player_team) - - integration = db.session.query( - TeamIntegration + team = db.session.query( + Team ).where( - TeamIntegration.team_id == player_team.team_id - ).where( - TeamIntegration.id == integration_id - ).one_or_none() - - if not integration: - abort(404) - - if isinstance(integration, TeamDiscordIntegration): - if json.__root__.integration_type == "team_discord_integrations": - discord_integration = cast(TeamDiscordIntegration, json.__root__) - integration.webhook_url = discord_integration.webhook_url - else: - abort(400) - else: - abort(404) - + Team.id == player_team.team_id + ).options( + joinedload(Team.discord_integration), + joinedload(Team.logs_tf_integration), + ).one() + team.update_integrations(json) db.session.commit() - - return TeamIntegrationSchema.from_model( - integration - ).dict(by_alias=True), 200 + return json.dict(by_alias=True)