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.master
parent
77aff078da
commit
71cc25dbb2
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
/* eslint-disable */
|
||||
export type EventSchema = {
|
||||
createdAt: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
id: number;
|
||||
name: string;
|
||||
startTime: string;
|
||||
|
|
|
@ -3,9 +3,7 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type TeamDiscordIntegrationSchema = {
|
||||
id: number;
|
||||
integrationType: string;
|
||||
teamId: number;
|
||||
webhookBotName: string;
|
||||
webhookUrl: string;
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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<TeamIntegrationSchema>;
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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 <GET>
|
||||
* @param teamId
|
||||
* @returns TeamIntegrationSchemaList OK
|
||||
* @returns TeamIntegrationSchema OK
|
||||
* @throws ApiError
|
||||
*/
|
||||
public getIntegrations(
|
||||
teamId: string,
|
||||
): CancelablePromise<TeamIntegrationSchemaList> {
|
||||
): CancelablePromise<TeamIntegrationSchema> {
|
||||
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 <DELETE>
|
||||
* update_integration <PUT>
|
||||
* @param teamId
|
||||
* @param integrationId
|
||||
* @returns void
|
||||
* @throws ApiError
|
||||
*/
|
||||
public deleteIntegration(
|
||||
teamId: string,
|
||||
integrationId: string,
|
||||
): CancelablePromise<void> {
|
||||
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 <PATCH>
|
||||
* @param teamId
|
||||
* @param integrationId
|
||||
* @param requestBody
|
||||
* @returns TeamIntegrationSchema OK
|
||||
* @throws ApiError
|
||||
*/
|
||||
public updateIntegration(
|
||||
public updateIntegrations(
|
||||
teamId: string,
|
||||
integrationId: string,
|
||||
requestBody?: AbstractTeamIntegrationSchema,
|
||||
requestBody?: TeamIntegrationSchema,
|
||||
): CancelablePromise<TeamIntegrationSchema> {
|
||||
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 <POST>
|
||||
* @param teamId
|
||||
* @param integrationType
|
||||
* @returns TeamIntegrationSchema OK
|
||||
* @throws ApiError
|
||||
*/
|
||||
public createIntegration(
|
||||
teamId: string,
|
||||
integrationType: string,
|
||||
): CancelablePromise<TeamIntegrationSchema> {
|
||||
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 <GET>
|
||||
* @param teamId
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
<script setup lang="ts">
|
||||
import { type TeamDiscordIntegrationSchema } from "@/client";
|
||||
import { useTeamDetails } from "@/composables/team-details";
|
||||
import { useIntegrationsStore } from "@/stores/teams/integrations";
|
||||
|
||||
const model = defineModel<TeamDiscordIntegrationSchema>();
|
||||
const integrationsStore = useIntegrationsStore();
|
||||
|
||||
const { teamId } = useTeamDetails();
|
||||
|
||||
function saveIntegration() {
|
||||
integrationsStore.updateIntegrations(teamId.value);
|
||||
}
|
||||
|
||||
function enableIntegration() {
|
||||
model.value = {
|
||||
webhookUrl: "",
|
||||
webhookBotName: "",
|
||||
};
|
||||
saveIntegration();
|
||||
}
|
||||
|
||||
function disableIntegration() {
|
||||
model.value = undefined;
|
||||
saveIntegration();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>Discord Webhook</h2>
|
||||
<p>Receive notifications in Discord for event updates.</p>
|
||||
<div v-if="model">
|
||||
<div class="form-group margin">
|
||||
<h3>Webhook URL</h3>
|
||||
<input v-model="model.webhookUrl">
|
||||
</div>
|
||||
<div class="form-group margin">
|
||||
<h3>Webhook Bot Name</h3>
|
||||
<input v-model="model.webhookBotName">
|
||||
</div>
|
||||
<div class="form-group margin">
|
||||
<div class="action-buttons">
|
||||
<button class="destructive-on-hover" @click="disableIntegration">
|
||||
<i class="bi bi-trash" />
|
||||
Disable integration
|
||||
</button>
|
||||
<button class="accent" @click="saveIntegration">
|
||||
<i class="bi bi-check" />
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<button class="accent" @click="enableIntegration">
|
||||
<i class="bi bi-check" />
|
||||
Enable Discord Integration
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,64 @@
|
|||
<script setup lang="ts">
|
||||
import { type TeamLogsTfIntegrationSchema } from "@/client";
|
||||
import { useTeamDetails } from "@/composables/team-details";
|
||||
import { useIntegrationsStore } from "@/stores/teams/integrations";
|
||||
|
||||
const model = defineModel<TeamLogsTfIntegrationSchema>();
|
||||
const integrationsStore = useIntegrationsStore();
|
||||
|
||||
const { teamId } = useTeamDetails();
|
||||
|
||||
function saveIntegration() {
|
||||
integrationsStore.updateIntegrations(teamId.value);
|
||||
}
|
||||
|
||||
function enableIntegration() {
|
||||
model.value = {
|
||||
logsTfApiKey: "",
|
||||
minTeamMemberCount: 4,
|
||||
};
|
||||
integrationsStore.updateIntegrations(teamId.value);
|
||||
}
|
||||
|
||||
function disableIntegration() {
|
||||
model.value = undefined;
|
||||
integrationsStore.updateIntegrations(teamId.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>logs.tf Integration</h2>
|
||||
<p>Automatically track match history from logs.tf.</p>
|
||||
<div v-if="model">
|
||||
<div class="form-group margin">
|
||||
<h3>logs.tf API key (optional)</h3>
|
||||
<input v-model="model.logsTfApiKey">
|
||||
</div>
|
||||
<div class="form-group margin">
|
||||
<h3>Minimum Team Members</h3>
|
||||
<p>
|
||||
Minimum number of team members needed to appear in the logs.tf match to
|
||||
automatically be included in the team match history.
|
||||
</p>
|
||||
<input v-model="model.minTeamMemberCount" type="number">
|
||||
</div>
|
||||
<div class="form-group margin">
|
||||
<div class="action-buttons">
|
||||
<button class="destructive-on-hover" @click="disableIntegration">
|
||||
<i class="bi bi-trash" />
|
||||
Disable integration
|
||||
</button>
|
||||
<button class="accent" @click="saveIntegration">
|
||||
<i class="bi bi-check" />
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<button class="accent" @click="enableIntegration">
|
||||
<i class="bi bi-check" />
|
||||
Enable logs.tf Integration
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
|
@ -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<TeamDiscordIntegrationSchema | undefined>();
|
||||
|
||||
const logsTfIntegration = ref<TeamLogsTfIntegrationSchema | undefined>();
|
||||
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -46,32 +46,42 @@ onMounted(() => {
|
|||
<template>
|
||||
<main>
|
||||
<template v-if="team">
|
||||
<center class="margin">
|
||||
<h1>
|
||||
{{ team.teamName }}
|
||||
</h1>
|
||||
<span class="aside">
|
||||
Formed on {{ creationDate }}
|
||||
</span>
|
||||
<div class="icons">
|
||||
<RouterLink class="button" :to="'/schedule?teamId=' + team.id">
|
||||
<button class="icon" v-tooltip="'Schedule'">
|
||||
<i class="bi bi-calendar-fill"></i>
|
||||
</button>
|
||||
</RouterLink>
|
||||
<RouterLink class="button" :to="{ name: 'team-settings/' }">
|
||||
<button class="icon" v-tooltip="'Settings'">
|
||||
<i class="bi bi-gear-fill"></i>
|
||||
</button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</center>
|
||||
<div class="content-container">
|
||||
<div class="left">
|
||||
<center class="margin">
|
||||
<h1>
|
||||
{{ team.teamName }}
|
||||
</h1>
|
||||
<span class="aside">
|
||||
Formed on {{ creationDate }}
|
||||
</span>
|
||||
<div class="icons">
|
||||
<RouterLink class="button" :to="'/schedule?teamId=' + team.id">
|
||||
<button class="icon" v-tooltip="'Schedule'">
|
||||
<i class="bi bi-calendar-fill"></i>
|
||||
</button>
|
||||
</RouterLink>
|
||||
<RouterLink class="button" :to="{ name: 'team-settings/' }">
|
||||
<button class="icon" v-tooltip="'Settings'">
|
||||
<i class="bi bi-gear-fill"></i>
|
||||
</button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</center>
|
||||
<MembersList />
|
||||
</div>
|
||||
<div class="right">
|
||||
<h2>Upcoming Events</h2>
|
||||
<EventList :events="events" />
|
||||
<h2 id="recent-matches-header">
|
||||
Recent Matches
|
||||
<RouterLink class="button" to="/">
|
||||
<button class="icon" v-tooltip="'View all'">
|
||||
<i class="bi bi-arrow-right-circle-fill"></i>
|
||||
</button>
|
||||
</RouterLink>
|
||||
</h2>
|
||||
<em class="subtext">No recent matches.</em>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -79,6 +89,12 @@ onMounted(() => {
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
#recent-matches-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -89,7 +105,11 @@ onMounted(() => {
|
|||
}
|
||||
|
||||
.content-container > div.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
margin-top: 4em;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.margin {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import DiscordIntegrationForm from "@/components/DiscordIntegrationForm.vue";
|
||||
import IntegrationDetails from "@/components/IntegrationDetails.vue";
|
||||
import LogsTfIntegrationForm from "@/components/LogsTfIntegrationForm.vue";
|
||||
import { useTeamDetails } from "@/composables/team-details";
|
||||
import { useTeamsStore } from "@/stores/teams";
|
||||
import { useIntegrationsStore } from "@/stores/teams/integrations";
|
||||
|
@ -9,11 +11,9 @@ const teamsStore = useTeamsStore();
|
|||
const integrationsStore = useIntegrationsStore();
|
||||
const { teamId } = useTeamDetails();
|
||||
|
||||
const integrations = computed(() => integrationsStore.teamIntegrations[teamId.value]);
|
||||
|
||||
function createIntegration() {
|
||||
integrationsStore.createIntegration(teamId.value, "discord");
|
||||
}
|
||||
//function createIntegration() {
|
||||
// integrationsStore.createIntegration(teamId.value, "discord");
|
||||
//}
|
||||
|
||||
onMounted(() => {
|
||||
teamsStore.fetchTeam(teamId.value)
|
||||
|
@ -23,19 +23,15 @@ onMounted(() => {
|
|||
|
||||
<template>
|
||||
<div class="team-integrations">
|
||||
<h2>Team Integrations</h2>
|
||||
<div v-if="integrations?.length == 0">
|
||||
This team currently does not have any integrations.
|
||||
</div>
|
||||
<div v-else>
|
||||
<IntegrationDetails
|
||||
v-for="integration in integrations"
|
||||
:integration="integration"
|
||||
/>
|
||||
</div>
|
||||
<button class="accent" @click="createIntegration">
|
||||
<i class="bi bi-database-fill-add margin" />
|
||||
Create Integration
|
||||
</button>
|
||||
<DiscordIntegrationForm v-model="integrationsStore.discordIntegration" />
|
||||
<LogsTfIntegrationForm v-model="integrationsStore.logsTfIntegration" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.team-integrations {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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 ###
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/<team_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/<team_id>/integrations/<integration_type>")
|
||||
@api_team_integration.put("/id/<team_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/<team_id>/integrations/<integration_id>")
|
||||
@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/<team_id>/integrations/<integration_id>")
|
||||
@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)
|
||||
|
|
Loading…
Reference in New Issue