Implement basic team features
parent
2fda11bc9a
commit
050a012318
|
@ -35,9 +35,12 @@
|
|||
--mantle: #e6e9ef;
|
||||
--crust: #dce0e8;
|
||||
|
||||
--flamingo: #f0c6c6;
|
||||
--red: #d20f39;
|
||||
|
||||
--flamingo: #dd7878;
|
||||
--flamingo-transparent: #f0c6c655;
|
||||
--green: #40a02b;
|
||||
--peach: #fe640b;
|
||||
--yellow: #df8e1d;
|
||||
--lavender: #7287fd;
|
||||
--accent: var(--lavender);
|
||||
|
|
|
@ -70,6 +70,12 @@ button.accent.dark {
|
|||
color: var(--base);
|
||||
}
|
||||
|
||||
button.destructive {
|
||||
background-color: var(--flamingo);
|
||||
color: var(--base);
|
||||
}
|
||||
|
||||
|
||||
button.accent:hover {
|
||||
background-color: var(--text);
|
||||
color: var(--base);
|
||||
|
@ -108,10 +114,14 @@ h1 {
|
|||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
em.aside {
|
||||
span.small {
|
||||
font-size: 9pt;
|
||||
}
|
||||
|
||||
em.aside, span.aside {
|
||||
color: var(--overlay-0);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@ export type { CreateTeamJson } from './models/CreateTeamJson';
|
|||
export type { EditMemberRolesJson } from './models/EditMemberRolesJson';
|
||||
export type { PutScheduleForm } from './models/PutScheduleForm';
|
||||
export type { RoleSchema } from './models/RoleSchema';
|
||||
export type { TeamInviteSchema } from './models/TeamInviteSchema';
|
||||
export type { TeamInviteSchemaList } from './models/TeamInviteSchemaList';
|
||||
export { TeamRole } from './models/TeamRole';
|
||||
export type { TeamSchema } from './models/TeamSchema';
|
||||
export type { ValidationError } from './models/ValidationError';
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type TeamInviteSchema = {
|
||||
createdAt: string;
|
||||
key: string;
|
||||
teamId: number;
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { TeamInviteSchema } from './TeamInviteSchema';
|
||||
export type TeamInviteSchemaList = Array<TeamInviteSchema>;
|
|
@ -6,6 +6,8 @@ import type { AddPlayerJson } from '../models/AddPlayerJson';
|
|||
import type { CreateTeamJson } from '../models/CreateTeamJson';
|
||||
import type { EditMemberRolesJson } from '../models/EditMemberRolesJson';
|
||||
import type { PutScheduleForm } from '../models/PutScheduleForm';
|
||||
import type { TeamInviteSchema } from '../models/TeamInviteSchema';
|
||||
import type { TeamInviteSchemaList } from '../models/TeamInviteSchemaList';
|
||||
import type { ViewScheduleResponse } from '../models/ViewScheduleResponse';
|
||||
import type { ViewTeamMembersResponseList } from '../models/ViewTeamMembersResponseList';
|
||||
import type { ViewTeamResponse } from '../models/ViewTeamResponse';
|
||||
|
@ -211,6 +213,30 @@ export class DefaultService {
|
|||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* consume_invite <POST>
|
||||
* @param teamId
|
||||
* @param key
|
||||
* @returns void
|
||||
* @throws ApiError
|
||||
*/
|
||||
public consumeInvite(
|
||||
teamId: string,
|
||||
key: string,
|
||||
): CancelablePromise<void> {
|
||||
return this.httpRequest.request({
|
||||
method: 'POST',
|
||||
url: '/api/team/id/{team_id}/consume-invite/{key}',
|
||||
path: {
|
||||
'team_id': teamId,
|
||||
'key': key,
|
||||
},
|
||||
errors: {
|
||||
404: `Not Found`,
|
||||
422: `Unprocessable Entity`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* edit_member_roles <PATCH>
|
||||
* @param teamId
|
||||
|
@ -240,6 +266,72 @@ export class DefaultService {
|
|||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* get_invites <GET>
|
||||
* @param teamId
|
||||
* @returns TeamInviteSchemaList OK
|
||||
* @throws ApiError
|
||||
*/
|
||||
public getInvites(
|
||||
teamId: string,
|
||||
): CancelablePromise<TeamInviteSchemaList> {
|
||||
return this.httpRequest.request({
|
||||
method: 'GET',
|
||||
url: '/api/team/id/{team_id}/invite',
|
||||
path: {
|
||||
'team_id': teamId,
|
||||
},
|
||||
errors: {
|
||||
404: `Not Found`,
|
||||
422: `Unprocessable Entity`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* create_invite <POST>
|
||||
* @param teamId
|
||||
* @returns TeamInviteSchema OK
|
||||
* @throws ApiError
|
||||
*/
|
||||
public createInvite(
|
||||
teamId: string,
|
||||
): CancelablePromise<TeamInviteSchema> {
|
||||
return this.httpRequest.request({
|
||||
method: 'POST',
|
||||
url: '/api/team/id/{team_id}/invite',
|
||||
path: {
|
||||
'team_id': teamId,
|
||||
},
|
||||
errors: {
|
||||
404: `Not Found`,
|
||||
422: `Unprocessable Entity`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* revoke_invite <DELETE>
|
||||
* @param teamId
|
||||
* @param key
|
||||
* @returns void
|
||||
* @throws ApiError
|
||||
*/
|
||||
public revokeInvite(
|
||||
teamId: string,
|
||||
key: string,
|
||||
): CancelablePromise<void> {
|
||||
return this.httpRequest.request({
|
||||
method: 'DELETE',
|
||||
url: '/api/team/id/{team_id}/invite/{key}',
|
||||
path: {
|
||||
'team_id': teamId,
|
||||
'key': key,
|
||||
},
|
||||
errors: {
|
||||
404: `Not Found`,
|
||||
422: `Unprocessable Entity`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* add_player <PUT>
|
||||
* @param teamId
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
<script setup lang="ts">
|
||||
import { type TeamInviteSchema } from "../client";
|
||||
import { useTeamsStore } from "../stores/teams";
|
||||
import { computed } from "vue";
|
||||
|
||||
const teamsStore = useTeamsStore();
|
||||
|
||||
const props = defineProps({
|
||||
invite: Object as PropType<TeamInviteSchema>,
|
||||
});
|
||||
|
||||
const inviteLink = computed(() => {
|
||||
let teamId = props.invite.teamId;
|
||||
let key = props.invite.key;
|
||||
return `${window.location.origin}/team/id/${teamId}?key=${key}`;
|
||||
})
|
||||
|
||||
function copyLink() {
|
||||
navigator.clipboard.writeText(inviteLink.value);
|
||||
}
|
||||
|
||||
function revokeInvite() {
|
||||
teamsStore.revokeInvite(props.invite.teamId, props.invite.key);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr>
|
||||
<td>
|
||||
<a class="key" :href="inviteLink">
|
||||
<code>
|
||||
{{ invite.key }}
|
||||
</code>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ invite.createdAt }}
|
||||
</td>
|
||||
<td class="buttons">
|
||||
<button @click="copyLink">
|
||||
<i class="bi bi-link margin" />
|
||||
Copy Link
|
||||
</button>
|
||||
<button class="destructive" @click="revokeInvite">
|
||||
<i class="bi bi-trash margin" />
|
||||
Revoke
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
tr .buttons {
|
||||
opacity: 0;
|
||||
transition-duration: 200ms;
|
||||
}
|
||||
|
||||
tr:hover .buttons {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.key {
|
||||
color: var(--text);
|
||||
background-color: var(--text);
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.key:hover {
|
||||
color: var(--base);
|
||||
transition-duration: 200ms;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
|
@ -75,7 +75,10 @@ function updateRoles() {
|
|||
<tr class="player-card">
|
||||
<td>
|
||||
<div class="status flex-middle" :availability="player.availability">
|
||||
<span class="dot"></span>
|
||||
<div class="status-indicators">
|
||||
<span class="indicator left-indicator" />
|
||||
<span class="indicator right-indicator" />
|
||||
</div>
|
||||
<h3>
|
||||
{{ player.username }}
|
||||
</h3>
|
||||
|
@ -129,8 +132,6 @@ function updateRoles() {
|
|||
user-select: none;
|
||||
gap: 1em;
|
||||
align-items: center;
|
||||
border: 2px solid white;
|
||||
box-shadow: 1px 1px 8px var(--surface-0);
|
||||
}
|
||||
|
||||
.player-card > td {
|
||||
|
@ -142,24 +143,37 @@ function updateRoles() {
|
|||
font-size: 12pt;
|
||||
}
|
||||
|
||||
.dot {
|
||||
.status-indicators {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.status-indicators > .indicator {
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
width: 12px;
|
||||
background-color: var(--overlay-0);
|
||||
}
|
||||
|
||||
.left-indicator {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
.right-indicator {
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.status[availability="0"] h3 {
|
||||
color: var(--overlay-0);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.status[availability="1"] .dot {
|
||||
.status[availability="1"] .indicator {
|
||||
background-color: var(--yellow);
|
||||
}
|
||||
|
||||
.status[availability="2"] .dot {
|
||||
.status[availability="2"] .indicator {
|
||||
background-color: var(--green);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import Cacheable from "@/cacheable";
|
||||
import { AvailabilitfClient, type RoleSchema, type TeamSpec, type ViewTeamMembersResponse, type ViewTeamResponse, type ViewTeamsResponse } from "@/client";
|
||||
import { AvailabilitfClient, type TeamInviteSchema, type RoleSchema, type TeamSchema, type ViewTeamMembersResponse, type ViewTeamResponse, type ViewTeamsResponse } from "@/client";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, reactive, ref, type Reactive, type Ref } from "vue";
|
||||
import { useClientStore } from "./client";
|
||||
|
||||
export type TeamMap = { [id: number]: TeamSpec };
|
||||
export type TeamMap = { [id: number]: TeamSchema };
|
||||
|
||||
export const useTeamsStore = defineStore("teams", () => {
|
||||
const clientStore = useClientStore();
|
||||
const client = clientStore.client;
|
||||
|
||||
const teams: Reactive<{ [id: number]: TeamSpec }> = reactive({ });
|
||||
const teams: Reactive<{ [id: number]: TeamSchema }> = reactive({ });
|
||||
const teamMembers: Reactive<{ [id: number]: ViewTeamMembersResponse[] }> = reactive({ });
|
||||
const teamInvites: Reactive<{ [id: number]: TeamInviteSchema[] }> = reactive({ });
|
||||
|
||||
const isFetchingTeams = ref(false);
|
||||
|
||||
|
@ -78,13 +79,55 @@ export const useTeamsStore = defineStore("teams", () => {
|
|||
});
|
||||
}
|
||||
|
||||
async function getInvites(teamId: number) {
|
||||
return clientStore.call(
|
||||
getInvites.name,
|
||||
() => client.default.getInvites(teamId.toString()),
|
||||
(response) => {
|
||||
teamInvites[teamId] = response;
|
||||
return response;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function createInvite(teamId: number) {
|
||||
return client.default.createInvite(teamId.toString())
|
||||
.then((response) => {
|
||||
teamInvites[teamId].push(response);
|
||||
return response;
|
||||
})
|
||||
}
|
||||
|
||||
async function consumeInvite(teamId: number, key: string) {
|
||||
return client.default.consumeInvite(teamId.toString(), key)
|
||||
.then((response) => {
|
||||
teamInvites[teamId] = teamInvites[teamId]
|
||||
.filter((invite) => invite.key != key);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
async function revokeInvite(teamId: number, key: string) {
|
||||
return client.default.revokeInvite(teamId.toString(), key)
|
||||
.then((response) => {
|
||||
teamInvites[teamId] = teamInvites[teamId]
|
||||
.filter((invite) => invite.key != key);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
teams,
|
||||
teamInvites,
|
||||
teamMembers,
|
||||
fetchTeams,
|
||||
fetchTeam,
|
||||
fetchTeamMembers,
|
||||
createTeam,
|
||||
updateRoles
|
||||
updateRoles,
|
||||
getInvites,
|
||||
createInvite,
|
||||
consumeInvite,
|
||||
revokeInvite,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { useRoute, useRouter, RouterLink } from "vue-router";
|
||||
import { useTeamsStore } from "../stores/teams";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import PlayerTeamCard from "../components/PlayerTeamCard.vue";
|
||||
import InviteEntry from "../components/InviteEntry.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
@ -12,14 +13,34 @@ const team = computed(() => {
|
|||
return teamsStore.teams[route.params.id];
|
||||
});
|
||||
|
||||
const invites = computed(() => {
|
||||
return teamsStore.teamInvites[route.params.id];
|
||||
});
|
||||
|
||||
const availableMembers = computed(() => {
|
||||
return teamsStore.teamMembers[route.params.id]
|
||||
.filter((member) => member.availability > 0);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
teamsStore.fetchTeam(route.params.id)
|
||||
.then(() => teamsStore.fetchTeamMembers(route.params.id));
|
||||
function createInvite() {
|
||||
teamsStore.createInvite(team.value.id);
|
||||
}
|
||||
|
||||
function revokeInvite(key) {
|
||||
teamsStore.revokeInvite(team.value.id, key)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
let key = route.query.key;
|
||||
let teamId = route.params.id;
|
||||
|
||||
if (key) {
|
||||
await teamsStore.consumeInvite(teamId, key);
|
||||
}
|
||||
|
||||
teamsStore.fetchTeam(teamId)
|
||||
.then(() => teamsStore.fetchTeamMembers(teamId))
|
||||
.then(() => teamsStore.getInvites(teamId));
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -29,15 +50,17 @@ onMounted(() => {
|
|||
<h1>
|
||||
{{ team.teamName }}
|
||||
<RouterLink :to="'/schedule?teamId=' + team.id">
|
||||
<button class="accent">
|
||||
<i class="bi bi-calendar-fill margin"></i>
|
||||
View schedule
|
||||
</button>
|
||||
</RouterLink>
|
||||
<em class="aside" v-if="teamsStore.teamMembers[route.params.id]">
|
||||
{{ teamsStore.teamMembers[route.params.id]?.length }} member(s),
|
||||
{{ availableMembers?.length }} currently available
|
||||
</em>
|
||||
<div class="team-details-button-group">
|
||||
<button class="accent">
|
||||
<i class="bi bi-calendar-fill margin"></i>
|
||||
View schedule
|
||||
</button>
|
||||
</div>
|
||||
</h1>
|
||||
<table class="member-table">
|
||||
<!--thead>
|
||||
|
@ -65,6 +88,42 @@ onMounted(() => {
|
|||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>Active Invites</h2>
|
||||
<div>
|
||||
<details>
|
||||
<summary>View all invites</summary>
|
||||
<span v-if="invites?.length == 0">
|
||||
There are currently no active invites to this team.
|
||||
</span>
|
||||
<table id="invite-table" v-else>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Key (hover to reveal)
|
||||
</th>
|
||||
<th>
|
||||
Creation time
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<InviteEntry
|
||||
v-for="invite in invites"
|
||||
:invite="invite"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="create-invite-group">
|
||||
<button class="accent" @click="createInvite">
|
||||
<i class="bi bi-person-fill-add margin" />
|
||||
Create Invite
|
||||
</button>
|
||||
<span class="small aside">
|
||||
Invites are usable once and expire after 24 hours.
|
||||
</span>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
</main>
|
||||
</template>
|
||||
|
@ -98,4 +157,29 @@ div.member-grid {
|
|||
flex-wrap: wrap;
|
||||
}
|
||||
*/
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#invite-table {
|
||||
width: 100%;
|
||||
border: 1px solid var(--text);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.team-details-button-group {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.create-invite-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
"""Add TeamInvite
|
||||
|
||||
Revision ID: 65714d7e78f8
|
||||
Revises: f50a79c4ae22
|
||||
Create Date: 2024-11-08 23:16:04.669526
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '65714d7e78f8'
|
||||
down_revision = 'f50a79c4ae22'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('team_invites',
|
||||
sa.Column('key', sa.String(length=31), nullable=False),
|
||||
sa.Column('team_id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.TIMESTAMP(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.Column('delete_on_use', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['team_id'], ['teams.id'], ),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('team_invites')
|
||||
# ### end Alembic commands ###
|
|
@ -50,6 +50,7 @@ class Team(db.Model):
|
|||
minute_offset: Mapped[int] = mapped_column(SmallInteger, default=0)
|
||||
|
||||
players: Mapped[List["PlayerTeam"]] = relationship(back_populates="team")
|
||||
invites: Mapped[List["TeamInvite"]] = relationship(back_populates="team")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
|
||||
|
||||
|
@ -145,6 +146,21 @@ class PlayerTeamAvailability(db.Model):
|
|||
),
|
||||
)
|
||||
|
||||
class TeamInvite(db.Model):
|
||||
__tablename__ = "team_invites"
|
||||
|
||||
key: Mapped[str] = mapped_column(String(31), primary_key=True)
|
||||
team_id: Mapped[int] = mapped_column(ForeignKey("teams.id"))
|
||||
delete_on_use: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
|
||||
|
||||
team: Mapped["Team"] = relationship(back_populates="invites")
|
||||
|
||||
class TeamInviteSchema(spec.BaseModel):
|
||||
key: str
|
||||
team_id: int
|
||||
created_at: datetime
|
||||
|
||||
class AuthSession(db.Model):
|
||||
__tablename__ = "auth_sessions"
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import datetime
|
||||
from datetime import datetime, timezone
|
||||
from random import randint, random
|
||||
import sys
|
||||
import time
|
||||
from typing import List
|
||||
from flask import Blueprint, abort, jsonify, make_response, request
|
||||
from pydantic.v1 import validator
|
||||
from spectree import Response
|
||||
from sqlalchemy.orm import joinedload, subqueryload
|
||||
from models import Player, PlayerSchema, PlayerTeam, PlayerTeamAvailability, PlayerTeamRole, PlayerTeamSchema, Team, TeamSchema, db
|
||||
from models import Player, PlayerSchema, PlayerTeam, PlayerTeamAvailability, PlayerTeamRole, PlayerTeamSchema, Team, TeamInvite, TeamInviteSchema, TeamSchema, db
|
||||
from middleware import requires_authentication
|
||||
import models
|
||||
from spec import spec, BaseModel
|
||||
|
@ -238,7 +240,7 @@ class ViewTeamMembersResponse(PlayerSchema):
|
|||
roles: list[RoleSchema]
|
||||
availability: int
|
||||
playtime: float
|
||||
created_at: datetime.datetime
|
||||
created_at: datetime
|
||||
|
||||
@api_team.get("/id/<team_id>/players")
|
||||
@spec.validate(
|
||||
|
@ -251,7 +253,7 @@ class ViewTeamMembersResponse(PlayerSchema):
|
|||
)
|
||||
@requires_authentication
|
||||
def view_team_members(player: Player, team_id: int, **kwargs):
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
player_teams_query = db.session.query(
|
||||
PlayerTeam
|
||||
|
@ -318,7 +320,6 @@ def edit_member_roles(
|
|||
target_player_id: int,
|
||||
**kwargs,
|
||||
):
|
||||
print("hiiii lol")
|
||||
target_player = db.session.query(
|
||||
PlayerTeam
|
||||
).where(
|
||||
|
@ -353,3 +354,166 @@ def edit_member_roles(
|
|||
db.session.commit()
|
||||
|
||||
return make_response({ }, 204)
|
||||
|
||||
@api_team.get("/id/<team_id>/invite")
|
||||
@spec.validate(
|
||||
resp=Response(
|
||||
HTTP_200=list[TeamInviteSchema],
|
||||
HTTP_404=None,
|
||||
),
|
||||
operation_id="get_invites"
|
||||
)
|
||||
@requires_authentication
|
||||
def get_invites(player: Player, team_id: int, **kwargs):
|
||||
player_team = db.session.query(
|
||||
PlayerTeam
|
||||
).where(
|
||||
PlayerTeam.player_id == player.steam_id
|
||||
).where(
|
||||
PlayerTeam.team_id == team_id
|
||||
).one_or_none()
|
||||
|
||||
if not player_team:
|
||||
abort(404)
|
||||
|
||||
invites = db.session.query(
|
||||
TeamInvite
|
||||
).where(
|
||||
TeamInvite.team_id == team_id
|
||||
).all()
|
||||
|
||||
def map_invite_to_schema(invite: TeamInvite):
|
||||
return TeamInviteSchema(
|
||||
key=invite.key,
|
||||
team_id=invite.team_id,
|
||||
created_at=invite.created_at,
|
||||
).dict(by_alias=True)
|
||||
|
||||
return list(map(map_invite_to_schema, invites)), 200
|
||||
|
||||
@api_team.post("/id/<team_id>/invite")
|
||||
@spec.validate(
|
||||
resp=Response(
|
||||
HTTP_200=TeamInviteSchema,
|
||||
HTTP_404=None,
|
||||
),
|
||||
operation_id="create_invite"
|
||||
)
|
||||
@requires_authentication
|
||||
def create_invite(player: Player, team_id: int, **kwargs):
|
||||
player_team = db.session.query(
|
||||
PlayerTeam
|
||||
).where(
|
||||
PlayerTeam.player_id == player.steam_id
|
||||
).where(
|
||||
PlayerTeam.team_id == team_id
|
||||
).one_or_none()
|
||||
|
||||
if not player_team:
|
||||
abort(404)
|
||||
|
||||
team_id_shifted = int(team_id) << 48
|
||||
random_value_shifted = int(randint(0, (1 << 16) - 1)) << 32
|
||||
timestamp = int(time.time()) & ((1 << 32) - 1)
|
||||
|
||||
key_int = timestamp | team_id_shifted | random_value_shifted
|
||||
key_hex = "%0.16X" % key_int
|
||||
|
||||
invite = TeamInvite()
|
||||
invite.team_id = team_id
|
||||
invite.key = key_hex
|
||||
|
||||
db.session.add(invite)
|
||||
db.session.flush()
|
||||
db.session.refresh(invite)
|
||||
|
||||
response = TeamInviteSchema(
|
||||
key=key_hex,
|
||||
team_id=team_id,
|
||||
created_at=invite.created_at
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return response.dict(by_alias=True), 200
|
||||
|
||||
@api_team.post("/id/<team_id>/consume-invite/<key>")
|
||||
@spec.validate(
|
||||
resp=Response(
|
||||
HTTP_204=None,
|
||||
HTTP_404=None,
|
||||
),
|
||||
operation_id="consume_invite"
|
||||
)
|
||||
@requires_authentication
|
||||
def consume_invite(player: Player, team_id: int, key: str, **kwargs):
|
||||
invite = db.session.query(
|
||||
TeamInvite
|
||||
).where(
|
||||
TeamInvite.team_id == team_id
|
||||
).where(
|
||||
TeamInvite.key == key
|
||||
).one_or_none()
|
||||
|
||||
if not invite:
|
||||
abort(404)
|
||||
|
||||
player_team = db.session.query(
|
||||
PlayerTeam
|
||||
).where(
|
||||
PlayerTeam.player_id == player.steam_id
|
||||
).where(
|
||||
PlayerTeam.team_id == team_id
|
||||
).one_or_none()
|
||||
|
||||
if player_team:
|
||||
abort(409)
|
||||
|
||||
player_team = PlayerTeam()
|
||||
player_team.player = player
|
||||
player_team.team_id = team_id
|
||||
|
||||
db.session.add(player_team)
|
||||
|
||||
if invite.delete_on_use:
|
||||
db.session.delete(invite)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return make_response({ }, 204)
|
||||
|
||||
@api_team.delete("/id/<team_id>/invite/<key>")
|
||||
@spec.validate(
|
||||
resp=Response(
|
||||
HTTP_204=None,
|
||||
HTTP_404=None,
|
||||
),
|
||||
operation_id="revoke_invite"
|
||||
)
|
||||
@requires_authentication
|
||||
def revoke_invite(player: Player, team_id: int, key: str, **kwargs):
|
||||
player_team = db.session.query(
|
||||
PlayerTeam
|
||||
).where(
|
||||
PlayerTeam.player_id == player.steam_id
|
||||
).where(
|
||||
PlayerTeam.team_id == team_id
|
||||
).one_or_none()
|
||||
|
||||
if not player_team:
|
||||
abort(404)
|
||||
|
||||
invite = db.session.query(
|
||||
TeamInvite
|
||||
).where(
|
||||
TeamInvite.team_id == team_id
|
||||
).where(
|
||||
TeamInvite.key == key
|
||||
).one_or_none()
|
||||
|
||||
if not invite:
|
||||
abort(404)
|
||||
|
||||
db.session.delete(invite)
|
||||
db.session.commit()
|
||||
return make_response({ }, 204)
|
||||
|
|
Loading…
Reference in New Issue