Implement leaving team

master
John Montagu, the 4th Earl of Sandvich 2024-11-10 01:40:03 -08:00
parent 050a012318
commit f5bcbb85b5
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
18 changed files with 312 additions and 37 deletions

View File

@ -8,8 +8,11 @@
"name": "availabili.tf", "name": "availabili.tf",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@jamescoyle/vue-icon": "^0.1.2",
"@mdi/js": "^7.4.47",
"axios": "^1.7.7", "axios": "^1.7.7",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"css.gg": "^2.1.4",
"moment": "^2.30.1", "moment": "^2.30.1",
"moment-timezone": "^0.5.46", "moment-timezone": "^0.5.46",
"pinia": "^2.2.4", "pinia": "^2.2.4",
@ -838,6 +841,12 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@jamescoyle/vue-icon": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@jamescoyle/vue-icon/-/vue-icon-0.1.2.tgz",
"integrity": "sha512-KFrImXx5TKIi6iQXlnyLEBl4rNosNKbTeRnr70ucTdUaciVmd9qK9d/pZAaKt1Ob/8xNnX2GMp8LisyHdKtEgw==",
"license": "MIT"
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
@ -897,6 +906,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@mdi/js": {
"version": "7.4.47",
"resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz",
"integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==",
"license": "Apache-2.0"
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -2816,6 +2831,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/css.gg": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/css.gg/-/css.gg-2.1.4.tgz",
"integrity": "sha512-7eyhXQLNJus5q3AVlYhDFjvVkB1ng1D9EjaBJzvboLfNx60RcFdZ1NinEgJMEA8bkwPwRLfbZ0ADTBXsbdrRgw==",
"license": "SEE LICENSE"
},
"node_modules/cssesc": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",

View File

@ -15,8 +15,11 @@
"openapi-generate": "openapi --input 'http://localhost:8000/apidoc/openapi.json' --output src/client --name AvailabilitfClient" "openapi-generate": "openapi --input 'http://localhost:8000/apidoc/openapi.json' --output src/client --name AvailabilitfClient"
}, },
"dependencies": { "dependencies": {
"@jamescoyle/vue-icon": "^0.1.2",
"@mdi/js": "^7.4.47",
"axios": "^1.7.7", "axios": "^1.7.7",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"css.gg": "^2.1.4",
"moment": "^2.30.1", "moment": "^2.30.1",
"moment-timezone": "^0.5.46", "moment-timezone": "^0.5.46",
"pinia": "^2.2.4", "pinia": "^2.2.4",

View File

@ -1,5 +1,7 @@
@import url("tf2icons.css"); @import url("tf2icons.css");
@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"); @import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css");
/*@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css");*/
@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/solid.min.css");
/* color palette from <https://github.com/vuejs/theme> */ /* color palette from <https://github.com/vuejs/theme> */
:root { :root {
@ -39,9 +41,20 @@
--flamingo: #dd7878; --flamingo: #dd7878;
--flamingo-transparent: #f0c6c655; --flamingo-transparent: #f0c6c655;
--green: #40a02b;
--peach: #fe640b; --peach: #fe640b;
--green: #40a02b;
--yellow: #df8e1d; --yellow: #df8e1d;
/*
--green: #a6e3a1;
--yellow: #f9e2af;
*/
--green-transparent: color-mix(in srgb, var(--green), transparent 80%);
--yellow-transparent: color-mix(in srgb, var(--yellow), transparent 80%);
/*
--green-transparent-50: #a6e3a1;
--yellow-transparent-50: #f9e2af;
*/
--lavender: #7287fd; --lavender: #7287fd;
--accent: var(--lavender); --accent: var(--lavender);
--accent-transparent-80: color-mix(in srgb, var(--accent), transparent 80%); --accent-transparent-80: color-mix(in srgb, var(--accent), transparent 80%);

View File

@ -15,6 +15,10 @@ a,
padding: 3px; padding: 3px;
} }
a.button {
padding: unset;
}
@media (hover: hover) { @media (hover: hover) {
a:hover { a:hover {
background-color: var(--accent-transparent); background-color: var(--accent-transparent);

View File

@ -13,6 +13,7 @@ export type { OpenAPIConfig } from './core/OpenAPI';
export type { AddPlayerJson } from './models/AddPlayerJson'; export type { AddPlayerJson } from './models/AddPlayerJson';
export type { CreateTeamJson } from './models/CreateTeamJson'; export type { CreateTeamJson } from './models/CreateTeamJson';
export type { EditMemberRolesJson } from './models/EditMemberRolesJson'; export type { EditMemberRolesJson } from './models/EditMemberRolesJson';
export type { PlayerSchema } from './models/PlayerSchema';
export type { PutScheduleForm } from './models/PutScheduleForm'; export type { PutScheduleForm } from './models/PutScheduleForm';
export type { RoleSchema } from './models/RoleSchema'; export type { RoleSchema } from './models/RoleSchema';
export type { TeamInviteSchema } from './models/TeamInviteSchema'; export type { TeamInviteSchema } from './models/TeamInviteSchema';

View File

@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type PlayerSchema = {
steamId: string;
username: string;
};

View File

@ -4,8 +4,9 @@
/* eslint-disable */ /* eslint-disable */
import type { RoleSchema } from './RoleSchema'; import type { RoleSchema } from './RoleSchema';
export type ViewTeamMembersResponse = { export type ViewTeamMembersResponse = {
availability: number; availability: Array<number>;
createdAt: string; createdAt: string;
isTeamLeader?: boolean;
playtime: number; playtime: number;
roles: Array<RoleSchema>; roles: Array<RoleSchema>;
steamId: string; steamId: string;

View File

@ -5,6 +5,7 @@
import type { AddPlayerJson } from '../models/AddPlayerJson'; import type { AddPlayerJson } from '../models/AddPlayerJson';
import type { CreateTeamJson } from '../models/CreateTeamJson'; import type { CreateTeamJson } from '../models/CreateTeamJson';
import type { EditMemberRolesJson } from '../models/EditMemberRolesJson'; import type { EditMemberRolesJson } from '../models/EditMemberRolesJson';
import type { PlayerSchema } from '../models/PlayerSchema';
import type { PutScheduleForm } from '../models/PutScheduleForm'; import type { PutScheduleForm } from '../models/PutScheduleForm';
import type { TeamInviteSchema } from '../models/TeamInviteSchema'; import type { TeamInviteSchema } from '../models/TeamInviteSchema';
import type { TeamInviteSchemaList } from '../models/TeamInviteSchemaList'; import type { TeamInviteSchemaList } from '../models/TeamInviteSchemaList';
@ -71,6 +72,21 @@ export class DefaultService {
url: '/api/login/authenticate', url: '/api/login/authenticate',
}); });
} }
/**
* get_user <GET>
* @returns PlayerSchema OK
* @throws ApiError
*/
public getUser(): CancelablePromise<PlayerSchema> {
return this.httpRequest.request({
method: 'GET',
url: '/api/login/get-user',
errors: {
401: `Unauthorized`,
422: `Unprocessable Entity`,
},
});
}
/** /**
* get <GET> * get <GET>
* @param windowStart * @param windowStart
@ -361,6 +377,31 @@ export class DefaultService {
}, },
}); });
} }
/**
* remove_player_from_team <DELETE>
* @param teamId
* @param targetPlayerId
* @returns any OK
* @throws ApiError
*/
public removePlayerFromTeam(
teamId: string,
targetPlayerId: string,
): CancelablePromise<any> {
return this.httpRequest.request({
method: 'DELETE',
url: '/api/team/id/{team_id}/player/{target_player_id}/',
path: {
'team_id': teamId,
'target_player_id': targetPlayerId,
},
errors: {
403: `Forbidden`,
404: `Not Found`,
422: `Unprocessable Entity`,
},
});
}
/** /**
* view_team_members <GET> * view_team_members <GET>
* @param teamId * @param teamId

View File

@ -272,10 +272,12 @@ onUnmounted(() => {
} }
.time-slot[selection="1"] { .time-slot[selection="1"] {
background-color: var(--accent-transparent-50); /*background-color: var(--accent-transparent-50);*/
background-color: var(--yellow-transparent);
} }
.time-slot[selection="2"] { .time-slot[selection="2"] {
background-color: var(--accent); /*background-color: var(--accent);*/
background-color: var(--green-transparent);
} }
</style> </style>

View File

@ -4,6 +4,8 @@ import { computed, type PropType, ref, watch } from "vue";
import { useTeamsStore } from "../stores/teams"; import { useTeamsStore } from "../stores/teams";
import { useRosterStore } from "../stores/roster"; import { useRosterStore } from "../stores/roster";
import { type ViewTeamMembersResponse, type TeamSchema } from "@/client"; import { type ViewTeamMembersResponse, type TeamSchema } from "@/client";
import SvgIcon from "@jamescoyle/vue-icon";
import { mdiCrown } from "@mdi/js";
import RoleTag from "../components/RoleTag.vue"; import RoleTag from "../components/RoleTag.vue";
const props = defineProps({ const props = defineProps({
@ -69,19 +71,34 @@ function updateRoles() {
console.log(updatedRoles.value); console.log(updatedRoles.value);
teamsStore.updateRoles(props.team.id, props.player.steamId, updatedRoles.value); teamsStore.updateRoles(props.team.id, props.player.steamId, updatedRoles.value);
} }
const isUnavailable = computed(() => {
return props.player?.availability[0] == 0 &&
props.player?.availability[1] == 0;
});
</script> </script>
<template> <template>
<tr class="player-card"> <tr class="player-card">
<td> <td>
<div class="status flex-middle" :availability="player.availability"> <div
class="status flex-middle"
:is-unavailable="isUnavailable"
>
<div class="status-indicators"> <div class="status-indicators">
<span class="indicator left-indicator" /> <span
<span class="indicator right-indicator" /> class="indicator left-indicator"
:availability="player.availability[0]"
/>
<span
class="indicator right-indicator"
:availability="player.availability[1]"
/>
</div> </div>
<h3> <h3>
{{ player.username }} {{ player.username }}
</h3> </h3>
<svg-icon v-if="player.isTeamLeader" type="mdi" :path="mdiCrown" />
</div> </div>
</td> </td>
<td> <td>
@ -164,16 +181,16 @@ function updateRoles() {
border-radius: 0 8px 8px 0; border-radius: 0 8px 8px 0;
} }
.status[availability="0"] h3 { .status[is-unavailable="true"] {
color: var(--overlay-0); color: var(--overlay-0);
font-weight: 400; font-weight: 400;
} }
.status[availability="1"] .indicator { .status .indicator[availability="1"] {
background-color: var(--yellow); background-color: var(--yellow);
} }
.status[availability="2"] .indicator { .status .indicator[availability="2"] {
background-color: var(--green); background-color: var(--green);
} }

View File

@ -5,6 +5,7 @@ import RosterBuilderView from "../views/RosterBuilderView.vue";
import LoginView from "../views/LoginView.vue"; import LoginView from "../views/LoginView.vue";
import TeamRegistrationView from "../views/TeamRegistrationView.vue"; import TeamRegistrationView from "../views/TeamRegistrationView.vue";
import TeamDetailsView from "../views/TeamDetailsView.vue"; import TeamDetailsView from "../views/TeamDetailsView.vue";
import { useAuthStore } from "@/stores/auth";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -40,6 +41,16 @@ const router = createRouter({
component: TeamDetailsView component: TeamDetailsView
}, },
] ]
}) });
export default router
router
.beforeEach(async (to, from) => {
const authStore = useAuthStore();
console.log("test");
if (!authStore.isLoggedIn && !authStore.hasCheckedAuth) {
await authStore.getUser();
}
});
export default router;

View File

@ -1,11 +1,29 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { ref } from "vue"; import { ref } from "vue";
import { useClientStore } from "./client";
export const useAuthStore = defineStore("auth", () => { export const useAuthStore = defineStore("auth", () => {
const steamId = ref(NaN); const clientStore = useClientStore();
const client = clientStore.client;
const steamId = ref("");
const username = ref(""); const username = ref("");
const isLoggedIn = ref(false); const isLoggedIn = ref(false);
const isRegistering = ref(false); const isRegistering = ref(false);
const hasCheckedAuth = ref(false);
async function getUser() {
hasCheckedAuth.value = true;
return clientStore.call(
getUser.name,
() => client.default.getUser(),
(response) => {
steamId.value = response.steamId;
username.value = username.value;
return response;
}
);
}
async function login(queryParams: { [key: string]: string }) { async function login(queryParams: { [key: string]: string }) {
return fetch(import.meta.env.VITE_API_BASE_URL + "/login/authenticate", { return fetch(import.meta.env.VITE_API_BASE_URL + "/login/authenticate", {
@ -32,7 +50,9 @@ export const useAuthStore = defineStore("auth", () => {
steamId, steamId,
username, username,
isLoggedIn, isLoggedIn,
hasCheckedAuth,
isRegistering, isRegistering,
getUser,
login, login,
} }
}); });

View File

@ -1,4 +1,4 @@
import { AvailabilitfClient } from "@/client"; import { AvailabilitfClient, CancelablePromise } from "@/client";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
export const useClientStore = defineStore("client", () => { export const useClientStore = defineStore("client", () => {
@ -10,7 +10,7 @@ export const useClientStore = defineStore("client", () => {
function call<T>( function call<T>(
key: string, key: string,
apiCall: () => Promise<T>, apiCall: () => CancelablePromise<T>,
thenOnce?: (result: T) => T thenOnce?: (result: T) => T
): Promise<T> { ): Promise<T> {
console.log("Fetching call " + key); console.log("Fetching call " + key);

View File

@ -3,10 +3,13 @@ import { AvailabilitfClient, type TeamInviteSchema, type RoleSchema, type TeamSc
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { computed, reactive, ref, type Reactive, type Ref } from "vue"; import { computed, reactive, ref, type Reactive, type Ref } from "vue";
import { useClientStore } from "./client"; import { useClientStore } from "./client";
import { useAuthStore } from "./auth";
export type TeamMap = { [id: number]: TeamSchema }; export type TeamMap = { [id: number]: TeamSchema };
export const useTeamsStore = defineStore("teams", () => { export const useTeamsStore = defineStore("teams", () => {
const authStore = useAuthStore();
const clientStore = useClientStore(); const clientStore = useClientStore();
const client = clientStore.client; const client = clientStore.client;
@ -14,8 +17,6 @@ export const useTeamsStore = defineStore("teams", () => {
const teamMembers: Reactive<{ [id: number]: ViewTeamMembersResponse[] }> = reactive({ }); const teamMembers: Reactive<{ [id: number]: ViewTeamMembersResponse[] }> = reactive({ });
const teamInvites: Reactive<{ [id: number]: TeamInviteSchema[] }> = reactive({ }); const teamInvites: Reactive<{ [id: number]: TeamInviteSchema[] }> = reactive({ });
const isFetchingTeams = ref(false);
async function fetchTeams() { async function fetchTeams() {
return clientStore.call( return clientStore.call(
fetchTeams.name, fetchTeams.name,
@ -116,6 +117,11 @@ export const useTeamsStore = defineStore("teams", () => {
}); });
} }
async function leaveTeam(teamId: number) {
return client.default
.removePlayerFromTeam(teamId.toString(), authStore.steamId);
}
return { return {
teams, teams,
teamInvites, teamInvites,
@ -129,5 +135,6 @@ export const useTeamsStore = defineStore("teams", () => {
createInvite, createInvite,
consumeInvite, consumeInvite,
revokeInvite, revokeInvite,
leaveTeam,
}; };
}); });

View File

@ -164,17 +164,27 @@ button.radio:hover {
button.radio.selected { button.radio.selected {
color: var(--accent); color: var(--accent);
background-color: var(--accent-transparent); color: var(--accent-transparent);
} }
button.left { button.left {
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
} }
button.radio.left.selected {
color: var(--yellow);
background-color: var(--yellow-transparent);
}
button.right { button.right {
border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0;
} }
button.right.radio.selected {
color: var(--green);
background-color: var(--green-transparent);
}
.v-select { .v-select {
display: inline-block; display: inline-block;
width: auto; width: auto;

View File

@ -19,7 +19,12 @@ const invites = computed(() => {
const availableMembers = computed(() => { const availableMembers = computed(() => {
return teamsStore.teamMembers[route.params.id] return teamsStore.teamMembers[route.params.id]
.filter((member) => member.availability > 0); .filter((member) => member.availability[0] > 0);
});
const availableMembersNextHour = computed(() => {
return teamsStore.teamMembers[route.params.id]
.filter((member) => member.availability[1] > 0);
}); });
function createInvite() { function createInvite() {
@ -30,17 +35,33 @@ function revokeInvite(key) {
teamsStore.revokeInvite(team.value.id, key) teamsStore.revokeInvite(team.value.id, key)
} }
function leaveTeam() {
teamsStore.leaveTeam(team.value.id)
.then(() => {
teamsStore.fetchTeams()
.then(() => {
router.push("/");
})
});
}
onMounted(async () => { onMounted(async () => {
let key = route.query.key; let key = route.query.key;
let teamId = route.params.id; let teamId = route.params.id;
if (key) { let doFetchTeam = () => {
await teamsStore.consumeInvite(teamId, key);
}
teamsStore.fetchTeam(teamId) teamsStore.fetchTeam(teamId)
.then(() => teamsStore.fetchTeamMembers(teamId)) .then(() => teamsStore.fetchTeamMembers(teamId))
.then(() => teamsStore.getInvites(teamId)); .then(() => teamsStore.getInvites(teamId));
};
if (key) {
teamsStore.consumeInvite(teamId, key)
.finally(doFetchTeam);
} else {
doFetchTeam();
}
}); });
</script> </script>
@ -49,17 +70,24 @@ onMounted(async () => {
<template v-if="team"> <template v-if="team">
<h1> <h1>
{{ team.teamName }} {{ team.teamName }}
<RouterLink :to="'/schedule?teamId=' + team.id">
</RouterLink>
<em class="aside" v-if="teamsStore.teamMembers[route.params.id]"> <em class="aside" v-if="teamsStore.teamMembers[route.params.id]">
{{ teamsStore.teamMembers[route.params.id]?.length }} member(s), {{ teamsStore.teamMembers[route.params.id]?.length }} member(s),
{{ availableMembers?.length }} currently available {{ availableMembers?.length }} currently available,
{{ availableMembersNextHour?.length }} available in the next hour
</em> </em>
<div class="team-details-button-group"> <div class="team-details-button-group">
<RouterLink class="button" :to="'/schedule?teamId=' + team.id">
<button class="accent"> <button class="accent">
<i class="bi bi-calendar-fill margin"></i> <i class="bi bi-calendar-fill margin"></i>
View schedule View schedule
</button> </button>
</RouterLink>
<button
class="destructive"
@click="leaveTeam"
>
Leave
</button>
</div> </div>
</h1> </h1>
<table class="member-table"> <table class="member-table">
@ -175,6 +203,7 @@ th {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: end; justify-content: end;
gap: 4px;
} }
.create-invite-group { .create-invite-group {

View File

@ -4,8 +4,10 @@ import string
import urllib.parse import urllib.parse
from flask import Blueprint, abort, make_response, redirect, request, url_for from flask import Blueprint, abort, make_response, redirect, request, url_for
import requests import requests
from spectree import Response
from spec import spec
import models import models
from models import AuthSession, Player, db from models import AuthSession, Player, PlayerSchema, db
from middleware import requires_authentication from middleware import requires_authentication
api_login = Blueprint("login", __name__, url_prefix="/login") api_login = Blueprint("login", __name__, url_prefix="/login")
@ -16,6 +18,21 @@ STEAM_OPENID_URL = "https://steamcommunity.com/openid/login"
def index(): def index():
return "test" return "test"
@api_login.get("/get-user")
@requires_authentication
@spec.validate(
resp=Response(
HTTP_200=PlayerSchema,
HTTP_401=None,
),
operation_id="get_user"
)
def get_user(player: Player, auth_session: AuthSession):
return PlayerSchema(
steam_id=str(player.steam_id),
username=player.username,
).dict(by_alias=True), 200
@api_login.post("/authenticate") @api_login.post("/authenticate")
def steam_authenticate(): def steam_authenticate():
params = request.get_json() params = request.get_json()

View File

@ -1,4 +1,4 @@
from datetime import datetime, timezone from datetime import datetime, timedelta, timezone
from random import randint, random from random import randint, random
import sys import sys
import time import time
@ -116,6 +116,69 @@ def delete_team(player: Player, team_id: int):
db.session.commit() db.session.commit()
return make_response(200) return make_response(200)
@api_team.delete("/id/<team_id>/player/<target_player_id>/")
@spec.validate(
resp=Response(
HTTP_200=None,
HTTP_403=None,
HTTP_404=None,
),
operation_id="remove_player_from_team"
)
@requires_authentication
def remove_player_from_team(player: Player, team_id: int, target_player_id: int, **kwargs):
player_team = db.session.query(
PlayerTeam
).where(
PlayerTeam.team_id == team_id
).where(
PlayerTeam.player_id == player.steam_id
).one_or_none()
if not player_team:
abort(404)
target_player_team = db.session.query(
PlayerTeam
).where(
PlayerTeam.team_id == team_id
).where(
PlayerTeam.player_id == target_player_id
).one_or_none()
if not target_player_team:
abort(404)
is_team_leader = player_team.is_team_leader
if not is_team_leader and player_team != target_player_team:
abort(403)
team = target_player_team.team
db.session.delete(target_player_team)
db.session.refresh(team)
if len(team.players) == 0:
# delete the team if the only member
db.session.delete(team)
else:
# if there doesn't exist another team leader, promote the first player
team_leaders = db.session.query(
PlayerTeam
).where(
PlayerTeam.team_id == team_id
).where(
PlayerTeam.is_team_leader == True
).all()
if len(team_leaders) == 0:
team.players[0].is_team_leader = True
db.session.commit()
return make_response({ }, 200)
class AddPlayerJson(BaseModel): class AddPlayerJson(BaseModel):
team_role: PlayerTeam.TeamRole = PlayerTeam.TeamRole.Player team_role: PlayerTeam.TeamRole = PlayerTeam.TeamRole.Player
is_team_leader: bool = False is_team_leader: bool = False
@ -238,9 +301,10 @@ class ViewTeamMembersResponse(PlayerSchema):
is_main: bool is_main: bool
roles: list[RoleSchema] roles: list[RoleSchema]
availability: int availability: list[int]
playtime: float playtime: float
created_at: datetime created_at: datetime
is_team_leader: bool = False
@api_team.get("/id/<team_id>/players") @api_team.get("/id/<team_id>/players")
@spec.validate( @spec.validate(
@ -254,6 +318,7 @@ class ViewTeamMembersResponse(PlayerSchema):
@requires_authentication @requires_authentication
def view_team_members(player: Player, team_id: int, **kwargs): def view_team_members(player: Player, team_id: int, **kwargs):
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
next_hour = now + timedelta(hours=1)
player_teams_query = db.session.query( player_teams_query = db.session.query(
PlayerTeam PlayerTeam
@ -264,7 +329,7 @@ def view_team_members(player: Player, team_id: int, **kwargs):
joinedload(PlayerTeam.player), joinedload(PlayerTeam.player),
joinedload(PlayerTeam.player_roles), joinedload(PlayerTeam.player_roles),
joinedload(PlayerTeam.availability.and_( joinedload(PlayerTeam.availability.and_(
(PlayerTeamAvailability.start_time <= now) & (PlayerTeamAvailability.start_time <= next_hour) &
(PlayerTeamAvailability.end_time > now) (PlayerTeamAvailability.end_time > now)
)), )),
) )
@ -284,10 +349,13 @@ def view_team_members(player: Player, team_id: int, **kwargs):
roles = player_team.player_roles roles = player_team.player_roles
player = player_team.player player = player_team.player
availability = 0 availability = [0, 0]
if len(player_team.availability) > 0:
print(player_team.availability) for record in player_team.availability:
availability = player_team.availability[0].availability if record.start_time <= now < record.end_time:
availability[0] = record.availability
if record.start_time <= next_hour < record.end_time:
availability[1] = record.availability
return ViewTeamMembersResponse( return ViewTeamMembersResponse(
username=player.username, username=player.username,
@ -296,6 +364,7 @@ def view_team_members(player: Player, team_id: int, **kwargs):
availability=availability, availability=availability,
playtime=player_team.playtime.total_seconds() / 3600, playtime=player_team.playtime.total_seconds() / 3600,
created_at=player_team.created_at, created_at=player_team.created_at,
is_team_leader=player_team.is_team_leader,
).dict(by_alias=True) ).dict(by_alias=True)
return list(map(map_to_response, player_teams)) return list(map(map_to_response, player_teams))