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)