Compare commits
No commits in common. "45ac071a7f9e20db769ff3cbbbef1b577cf8f206" and "242562d66284a9634625275b52285448588d8758" have entirely different histories.
45ac071a7f
...
242562d662
|
@ -76,17 +76,6 @@ i.bi.margin {
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.checkbox, button.checkbox.icon {
|
|
||||||
outline: 1px solid var(--overlay-0);
|
|
||||||
font-size: 1rem;
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: var(--surface-0);
|
background-color: var(--surface-0);
|
||||||
}
|
}
|
||||||
|
@ -217,7 +206,7 @@ main {
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
input, textarea {
|
input {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
@ -244,10 +233,6 @@ input, textarea {
|
||||||
sans-serif;
|
sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -322,33 +307,6 @@ hr {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-overlay {
|
|
||||||
background-color: #00000055;
|
|
||||||
z-index: 1;
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
animation: smooth-appear 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
[role="dialog"] {
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%) translateY(-50%);
|
|
||||||
background-color: var(--base);
|
|
||||||
z-index: 10;
|
|
||||||
animation: smooth-appear 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes smooth-appear {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
div.banner {
|
div.banner {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
|
|
|
@ -13,7 +13,6 @@ export type { OpenAPIConfig } from './core/OpenAPI';
|
||||||
export type { AddPlayerJson } from './models/AddPlayerJson';
|
export type { AddPlayerJson } from './models/AddPlayerJson';
|
||||||
export type { AttendanceJson } from './models/AttendanceJson';
|
export type { AttendanceJson } from './models/AttendanceJson';
|
||||||
export type { AvailabilitySchema } from './models/AvailabilitySchema';
|
export type { AvailabilitySchema } from './models/AvailabilitySchema';
|
||||||
export type { ConsumeInviteResponse } from './models/ConsumeInviteResponse';
|
|
||||||
export type { CreateEventJson } from './models/CreateEventJson';
|
export type { CreateEventJson } from './models/CreateEventJson';
|
||||||
export type { CreateTeamJson } from './models/CreateTeamJson';
|
export type { CreateTeamJson } from './models/CreateTeamJson';
|
||||||
export type { EditMemberRolesJson } from './models/EditMemberRolesJson';
|
export type { EditMemberRolesJson } from './models/EditMemberRolesJson';
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
/* generated using openapi-typescript-codegen -- do not edit */
|
|
||||||
/* istanbul ignore file */
|
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
export type ConsumeInviteResponse = {
|
|
||||||
teamId: number;
|
|
||||||
};
|
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import type { AddPlayerJson } from '../models/AddPlayerJson';
|
import type { AddPlayerJson } from '../models/AddPlayerJson';
|
||||||
import type { AttendanceJson } from '../models/AttendanceJson';
|
import type { AttendanceJson } from '../models/AttendanceJson';
|
||||||
import type { ConsumeInviteResponse } from '../models/ConsumeInviteResponse';
|
|
||||||
import type { CreateEventJson } from '../models/CreateEventJson';
|
import type { CreateEventJson } from '../models/CreateEventJson';
|
||||||
import type { CreateTeamJson } from '../models/CreateTeamJson';
|
import type { CreateTeamJson } from '../models/CreateTeamJson';
|
||||||
import type { EditMemberRolesJson } from '../models/EditMemberRolesJson';
|
import type { EditMemberRolesJson } from '../models/EditMemberRolesJson';
|
||||||
|
@ -416,27 +415,6 @@ export class DefaultService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* consume_invite <POST>
|
|
||||||
* @param key
|
|
||||||
* @returns ConsumeInviteResponse OK
|
|
||||||
* @throws ApiError
|
|
||||||
*/
|
|
||||||
public consumeInvite(
|
|
||||||
key: string,
|
|
||||||
): CancelablePromise<ConsumeInviteResponse> {
|
|
||||||
return this.httpRequest.request({
|
|
||||||
method: 'POST',
|
|
||||||
url: '/api/team/consume-invite/{key}',
|
|
||||||
path: {
|
|
||||||
'key': key,
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
404: `Not Found`,
|
|
||||||
422: `Unprocessable Content`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* delete_team <DELETE>
|
* delete_team <DELETE>
|
||||||
* @param teamId
|
* @param teamId
|
||||||
|
@ -505,6 +483,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 Content`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* edit_member_roles <PATCH>
|
* edit_member_roles <PATCH>
|
||||||
* @param teamId
|
* @param teamId
|
||||||
|
|
|
@ -9,8 +9,6 @@ const model = defineModel();
|
||||||
|
|
||||||
const selectedTime = defineModel("selectedTime");
|
const selectedTime = defineModel("selectedTime");
|
||||||
|
|
||||||
const selectedIndex = defineModel("selectedIndex");
|
|
||||||
|
|
||||||
const hoveredIndex = defineModel("hoveredIndex");
|
const hoveredIndex = defineModel("hoveredIndex");
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
@ -158,7 +156,6 @@ function onSlotClick(dayIndex, hour) {
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedTime.value = getTimeAtCell(dayIndex, hour);
|
selectedTime.value = getTimeAtCell(dayIndex, hour);
|
||||||
scheduleStore.selectIndex(24 * dayIndex + hour);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeyUp($event) {
|
function onKeyUp($event) {
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { EventWithPlayerSchema, TeamSchema } from "@/client";
|
import type { EventWithPlayerSchema } from "@/client";
|
||||||
import EventCard from "./EventCard.vue";
|
import EventCard from "./EventCard.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
events: EventWithPlayerSchema[];
|
events: EventWithPlayerSchema[];
|
||||||
teamContext: TeamSchema;
|
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -13,20 +12,7 @@ const props = defineProps<{
|
||||||
<EventCard v-for="event in props.events" :key="event.event.id" :event="event" />
|
<EventCard v-for="event in props.events" :key="event.event.id" :event="event" />
|
||||||
</div>
|
</div>
|
||||||
<div class="events-list" v-else>
|
<div class="events-list" v-else>
|
||||||
<em class="subtext">
|
<em class="subtext">No upcoming events.</em>
|
||||||
No upcoming events. Create one in the
|
|
||||||
<router-link
|
|
||||||
:to="{
|
|
||||||
name: 'schedule',
|
|
||||||
query: {
|
|
||||||
teamId: props.teamContext.id
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
schedule
|
|
||||||
</router-link>
|
|
||||||
page.
|
|
||||||
</em>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,40 +1,16 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useEventForm } from "@/composables/event-form";
|
import { useEventForm } from "@/composables/event-form";
|
||||||
import { useTeamDetails } from "@/composables/team-details";
|
|
||||||
import { useRosterStore } from "@/stores/roster";
|
import { useRosterStore } from "@/stores/roster";
|
||||||
import { useTeamsStore } from "@/stores/teams";
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { computed, onMounted } from "vue";
|
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const rosterStore = useRosterStore();
|
const rosterStore = useRosterStore();
|
||||||
const teamsStore = useTeamsStore();
|
|
||||||
|
|
||||||
const { eventId } = useEventForm();
|
const { eventId } = useEventForm();
|
||||||
|
|
||||||
const { team, teamId } = useTeamDetails();
|
|
||||||
|
|
||||||
const startTime = computed(() => {
|
|
||||||
if (rosterStore.startTime) {
|
|
||||||
return moment.unix(rosterStore.startTime).format("LL LT z");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTimeTeamTz = computed(() => {
|
|
||||||
if (rosterStore.startTime) {
|
|
||||||
// if team timezone is the same as ours, then do nothing
|
|
||||||
if (team.value?.tzTimezone === moment.tz.guess()) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return moment.unix(rosterStore.startTime)
|
|
||||||
.tz(team.value?.tzTimezone)
|
|
||||||
.format("LL LT z");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function saveRoster() {
|
function saveRoster() {
|
||||||
if (eventId.value) {
|
if (eventId.value) {
|
||||||
rosterStore.updateRoster(eventId.value);
|
rosterStore.updateRoster(eventId.value);
|
||||||
|
@ -50,26 +26,16 @@ function saveRoster() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (!team.value) {
|
|
||||||
teamsStore.fetchTeam(teamId.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="event-scheduler-container">
|
<div class="event-scheduler-container">
|
||||||
<h1 class="roster-title">
|
<h1 class="roster-title">
|
||||||
Roster for {{ team?.teamName }}
|
Roster for Snus Brotherhood
|
||||||
</h1>
|
</h1>
|
||||||
<div v-if="rosterStore.startTime">
|
<div v-if="rosterStore.startTime">
|
||||||
<span class="aside date">
|
<span class="aside date">
|
||||||
{{ startTime }}
|
{{ moment.unix(rosterStore.startTime).format("LL LT") }}
|
||||||
</span>
|
|
||||||
<br v-if="startTimeTeamTz">
|
|
||||||
<span class="aside date">
|
|
||||||
{{ startTimeTeamTz }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group margin">
|
<div class="form-group margin">
|
||||||
|
|
|
@ -13,7 +13,7 @@ onMounted(() => {
|
||||||
<template>
|
<template>
|
||||||
<div class="commit-history">
|
<div class="commit-history">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h2>Changelog/Commit History</h2>
|
<h2>Commit History</h2>
|
||||||
<a
|
<a
|
||||||
class="icon"
|
class="icon"
|
||||||
href="https://github.com/HumanoidSandvichDispenser/availabili.tf/commits"
|
href="https://github.com/HumanoidSandvichDispenser/availabili.tf/commits"
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { useInvitesStore } from "@/stores/teams/invites";
|
|
||||||
import { DialogClose, DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle, DialogTrigger } from "radix-vue";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { useRouter } from "vue-router";
|
|
||||||
|
|
||||||
const invitesStore = useInvitesStore();
|
|
||||||
|
|
||||||
const key = ref("");
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
function submit() {
|
|
||||||
invitesStore.consumeInvite(key.value)
|
|
||||||
.then((response) => {
|
|
||||||
console.log(response);
|
|
||||||
router.push({
|
|
||||||
name: "team-details",
|
|
||||||
params: {
|
|
||||||
id: response.teamId,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<DialogRoot>
|
|
||||||
<DialogTrigger>
|
|
||||||
<i class="bi bi-person-plus-fill margin" />
|
|
||||||
Join a team
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogPortal>
|
|
||||||
<DialogOverlay class="dialog-overlay" />
|
|
||||||
<DialogContent>
|
|
||||||
<DialogTitle>Join a team</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
<p>
|
|
||||||
Enter the invite key to join a team. Don't have an invite key? Ask
|
|
||||||
your team leader to send you one.
|
|
||||||
</p>
|
|
||||||
</DialogDescription>
|
|
||||||
<div class="form-group margin">
|
|
||||||
<h3>Invite key</h3>
|
|
||||||
<input type="text" placeholder="Invite key or URL" v-model="key" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="action-buttons">
|
|
||||||
<DialogClose class="accent" aria-label="Close" @click="submit">
|
|
||||||
<i class="bi bi-check" />
|
|
||||||
Join
|
|
||||||
</DialogClose>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</DialogPortal>
|
|
||||||
</DialogRoot>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
[role="dialog"] {
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -28,7 +28,7 @@ function disableIntegration() {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h2>logs.tf Auto-Tracking</h2>
|
<h2>logs.tf Auto-Tracking</h2>
|
||||||
<p>Automatically fetch and track match history from logs.tf. (CURRENTLY NOT IMPLEMENTED)</p>
|
<p>Automatically fetch and track match history from logs.tf.</p>
|
||||||
<div v-if="model">
|
<div v-if="model">
|
||||||
<div class="form-group margin">
|
<div class="form-group margin">
|
||||||
<h3>logs.tf API key (optional)</h3>
|
<h3>logs.tf API key (optional)</h3>
|
||||||
|
|
|
@ -76,15 +76,13 @@ const isUnavailable = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const nextHour = computed(() => {
|
const nextHour = computed(() => {
|
||||||
const now = moment();
|
const now = moment().utc();
|
||||||
const time = now.clone().tz(props.team.tzTimezone);
|
const time = now.clone().tz(props.team.tzTimezone);
|
||||||
|
|
||||||
let minute = time.minute();
|
if (time.minute() >= props.team.minuteOffset) {
|
||||||
let minuteOffset = props.team.minuteOffset;
|
|
||||||
if (minute >= minuteOffset) {
|
|
||||||
time.add(1, "hour");
|
time.add(1, "hour");
|
||||||
|
time.minute(props.team.minuteOffset);
|
||||||
}
|
}
|
||||||
time.minute(minuteOffset);
|
|
||||||
|
|
||||||
const diff = time.utc().diff(now, "minutes", false);
|
const diff = time.utc().diff(now, "minutes", false);
|
||||||
|
|
||||||
|
@ -217,7 +215,7 @@ const rightIndicator = computed(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-card > td {
|
.player-card > td {
|
||||||
padding: 0.5em 1em;
|
padding: 1em 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-card h3 {
|
.player-card h3 {
|
||||||
|
@ -316,10 +314,4 @@ a.player-name:hover {
|
||||||
.edit-group > button.editing {
|
.edit-group > button.editing {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.player-card > td {
|
|
||||||
padding: 0.5em 0em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -33,10 +33,10 @@ function logout() {
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<RouterLink class="button" to="/schedule">
|
<RouterLink class="button" to="/teams">
|
||||||
<button>
|
<button>
|
||||||
<i class="bi bi-calendar-fill margin" />
|
<i class="bi bi-people margin" />
|
||||||
Schedule
|
Teams
|
||||||
</button>
|
</button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
|
@ -15,20 +15,11 @@ const isTeamTzLocal = computed(() => {
|
||||||
return selectedTimeTz.value.utcOffset() == props.selectedTime.utcOffset();
|
return selectedTimeTz.value.utcOffset() == props.selectedTime.utcOffset();
|
||||||
});
|
});
|
||||||
|
|
||||||
//const props = defineProps({
|
const props = defineProps({
|
||||||
// selectedTime: Object
|
selectedTime: Object
|
||||||
//});
|
});
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
selectedTime?: moment.Moment;
|
|
||||||
selectedIndex?: number;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
function scheduleRoster() {
|
function scheduleRoster() {
|
||||||
if (!props.selectedTime) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push({
|
router.push({
|
||||||
name: "roster-builder",
|
name: "roster-builder",
|
||||||
params: {
|
params: {
|
||||||
|
@ -48,20 +39,19 @@ function scheduleRoster() {
|
||||||
:player="record"
|
:player="record"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h4 v-if="selectedTime">
|
<h4>
|
||||||
<div>
|
<template v-if="selectedTime">
|
||||||
{{ selectedTime.format("L LT z") }}
|
<div>
|
||||||
</div>
|
{{ selectedTime.format("L LT z") }}
|
||||||
<div v-if="!isTeamTzLocal">
|
</div>
|
||||||
{{ selectedTimeTz.format("L LT z") }}
|
<div v-if="!isTeamTzLocal">
|
||||||
</div>
|
{{ selectedTimeTz.format("L LT z") }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</h4>
|
</h4>
|
||||||
<button @click="scheduleRoster" v-if="selectedTime">
|
<button @click="scheduleRoster" v-if="selectedTime">
|
||||||
Schedule for {{ selectedTime.format("L LT") }}
|
Schedule for {{ selectedTime.format("L LT") }}
|
||||||
</button>
|
</button>
|
||||||
<div v-else class="subtext">
|
|
||||||
<em>Select a time to schedule</em>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -2,41 +2,22 @@
|
||||||
import { useScheduleStore } from "../stores/schedule";
|
import { useScheduleStore } from "../stores/schedule";
|
||||||
import { computed, type PropType } from "vue";
|
import { computed, type PropType } from "vue";
|
||||||
import { type AvailabilitySchema } from "@/client";
|
import { type AvailabilitySchema } from "@/client";
|
||||||
import { CheckboxIndicator, CheckboxRoot } from "radix-vue";
|
|
||||||
|
|
||||||
const scheduleStore = useScheduleStore();
|
const scheduleStore = useScheduleStore();
|
||||||
|
|
||||||
const hoveredIndex = computed(() => scheduleStore.hoveredIndex);
|
const hoveredIndex = computed(() => scheduleStore.hoveredIndex);
|
||||||
|
|
||||||
const selectedIndex = computed(() => scheduleStore.selectedIndex);
|
|
||||||
|
|
||||||
const availabilityAtHoveredIndex = computed(() => {
|
const availabilityAtHoveredIndex = computed(() => {
|
||||||
if (props.player?.availability) {
|
if (hoveredIndex.value && props.player?.availability) {
|
||||||
if (hoveredIndex.value) {
|
return props.player.availability[hoveredIndex.value] ?? 0;
|
||||||
return props.player.availability[hoveredIndex.value] ?? 0;
|
|
||||||
}
|
|
||||||
if (scheduleStore.selectedIndexAvailability[props.player.steamId] != undefined) {
|
|
||||||
return scheduleStore.selectedIndexAvailability[props.player.steamId] ?? 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedMember = computed({
|
const props = defineProps({
|
||||||
get: () => scheduleStore.selectedMembers[props.player.steamId] ?? false,
|
player: Object as PropType<AvailabilitySchema>,
|
||||||
set: (value: boolean) => {
|
|
||||||
console.log("set", value);
|
|
||||||
scheduleStore.selectedMembers[props.player.steamId] = value;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
player: AvailabilitySchema;
|
|
||||||
}>();
|
|
||||||
//const props = defineProps({
|
|
||||||
// player: Object as PropType<AvailabilitySchema>,
|
|
||||||
//});
|
|
||||||
|
|
||||||
function onMouseOver() {
|
function onMouseOver() {
|
||||||
if (props.player) {
|
if (props.player) {
|
||||||
scheduleStore.hoveredMember = props.player;
|
scheduleStore.hoveredMember = props.player;
|
||||||
|
@ -57,25 +38,16 @@ function onMouseLeave() {
|
||||||
@mouseover="onMouseOver"
|
@mouseover="onMouseOver"
|
||||||
@mouseleave="onMouseLeave"
|
@mouseleave="onMouseLeave"
|
||||||
>
|
>
|
||||||
<!--input
|
<input
|
||||||
class="player-checkbox"
|
class="player-checkbox"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="scheduleStore.selectedMembers[player.steamId]"
|
v-model="scheduleStore.selectedMembers[player.steamId]"
|
||||||
:value="player"
|
:value="player"
|
||||||
:id="player.steamId"
|
:id="player.steamId"
|
||||||
/-->
|
/>
|
||||||
<label
|
<label
|
||||||
:for="player.steamId"
|
:for="player.steamId"
|
||||||
>
|
>
|
||||||
<!-- checkbox for pinning players -->
|
|
||||||
<CheckboxRoot
|
|
||||||
v-model:checked="selectedMember"
|
|
||||||
class="checkbox icon"
|
|
||||||
>
|
|
||||||
<CheckboxIndicator>
|
|
||||||
<i class="bi bi-pin-fill"></i>
|
|
||||||
</CheckboxIndicator>
|
|
||||||
</CheckboxRoot>
|
|
||||||
<span v-if="availabilityAtHoveredIndex ?? 0 > 0">
|
<span v-if="availabilityAtHoveredIndex ?? 0 > 0">
|
||||||
<span v-if="availabilityAtHoveredIndex == 1" class="can-be-available">
|
<span v-if="availabilityAtHoveredIndex == 1" class="can-be-available">
|
||||||
{{ player.username }}
|
{{ player.username }}
|
||||||
|
@ -126,15 +98,4 @@ input {
|
||||||
.player s {
|
.player s {
|
||||||
color: var(--overlay-0);
|
color: var(--overlay-0);
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.player-checkbox.icon i.bi {
|
|
||||||
/* dont take up space */
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,13 +2,9 @@
|
||||||
import { onMounted } from "vue";
|
import { onMounted } from "vue";
|
||||||
import { useTeamsStore } from "../stores/teams";
|
import { useTeamsStore } from "../stores/teams";
|
||||||
import { RouterLink } from "vue-router";
|
import { RouterLink } from "vue-router";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
import InviteKeyDialog from "./InviteKeyDialog.vue";
|
|
||||||
|
|
||||||
const teams = useTeamsStore();
|
const teams = useTeamsStore();
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
teams.fetchTeams();
|
teams.fetchTeams();
|
||||||
});
|
});
|
||||||
|
@ -21,8 +17,11 @@ onMounted(() => {
|
||||||
<i class="bi bi-people-fill margin"></i>
|
<i class="bi bi-people-fill margin"></i>
|
||||||
Your Teams
|
Your Teams
|
||||||
</h2>
|
</h2>
|
||||||
<div class="button-group" v-if="authStore.isLoggedIn">
|
<div class="button-group">
|
||||||
<InviteKeyDialog />
|
<button class="small">
|
||||||
|
<i class="bi bi-person-plus-fill margin" />
|
||||||
|
Join a team
|
||||||
|
</button>
|
||||||
<RouterLink class="button" to="/team/register">
|
<RouterLink class="button" to="/team/register">
|
||||||
<button class="small accent">
|
<button class="small accent">
|
||||||
<i class="bi bi-plus-circle-fill margin"></i>
|
<i class="bi bi-plus-circle-fill margin"></i>
|
||||||
|
@ -31,9 +30,6 @@ onMounted(() => {
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!authStore.isLoggedIn">
|
|
||||||
Log in to view your teams.
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="teams.teamsWithRole"
|
v-if="teams.teamsWithRole"
|
||||||
v-for="(team, _, i) in teams.teamsWithRole"
|
v-for="(team, _, i) in teams.teamsWithRole"
|
||||||
|
|
|
@ -8,7 +8,7 @@ export function useTeamDetails() {
|
||||||
const teamsStore = useTeamsStore();
|
const teamsStore = useTeamsStore();
|
||||||
const invitesStore = useInvitesStore();
|
const invitesStore = useInvitesStore();
|
||||||
|
|
||||||
const teamId = computed(() => Number(route.params.id ?? route.params.teamId));
|
const teamId = computed(() => Number(route.params.id));
|
||||||
|
|
||||||
const team = computed(() => {
|
const team = computed(() => {
|
||||||
return teamsStore.teams[teamId.value];
|
return teamsStore.teams[teamId.value];
|
||||||
|
|
|
@ -60,16 +60,7 @@ export const useScheduleStore = defineStore("schedule", () => {
|
||||||
|
|
||||||
const selectedMembers = reactive<{ [id: string]: boolean }>({ });
|
const selectedMembers = reactive<{ [id: string]: boolean }>({ });
|
||||||
|
|
||||||
const hoveredIndex = ref<number | undefined>();
|
const hoveredIndex: Ref<number | undefined> = ref();
|
||||||
const selectedIndexAvailability = ref<{ [id: string]: number; }>({ });
|
|
||||||
|
|
||||||
function selectIndex(index: number) {
|
|
||||||
playerAvailability.value.forEach((value) => {
|
|
||||||
if (value.availability) {
|
|
||||||
selectedIndexAvailability.value[value.steamId] = value.availability[index] ?? 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const team = ref();
|
const team = ref();
|
||||||
|
|
||||||
|
@ -92,12 +83,10 @@ export const useScheduleStore = defineStore("schedule", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(dateStart, () => {
|
watch(dateStart, () => {
|
||||||
selectedIndexAvailability.value = { };
|
|
||||||
fetchTeamSchedule();
|
fetchTeamSchedule();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(team, () => {
|
watch(team, () => {
|
||||||
selectedIndexAvailability.value = { };
|
|
||||||
dateStart.value = getWindowStart(team.value);
|
dateStart.value = getWindowStart(team.value);
|
||||||
console.log(dateStart.value);
|
console.log(dateStart.value);
|
||||||
});
|
});
|
||||||
|
@ -156,8 +145,6 @@ export const useScheduleStore = defineStore("schedule", () => {
|
||||||
hoveredMember,
|
hoveredMember,
|
||||||
selectedMembers,
|
selectedMembers,
|
||||||
hoveredIndex,
|
hoveredIndex,
|
||||||
selectedIndexAvailability,
|
|
||||||
selectIndex,
|
|
||||||
fetchSchedule,
|
fetchSchedule,
|
||||||
fetchTeamSchedule,
|
fetchTeamSchedule,
|
||||||
saveSchedule,
|
saveSchedule,
|
||||||
|
|
|
@ -26,13 +26,9 @@ export const useInvitesStore = defineStore("invites", () => {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function consumeInvite(key: string) {
|
async function consumeInvite(teamId: number, key: string) {
|
||||||
const response = await client.default.consumeInvite(key);
|
const response = await client.default.consumeInvite(teamId.toString(), key);
|
||||||
const teamId = response.teamId;
|
teamInvites[teamId] = teamInvites[teamId].filter((invite) => invite.key != key);
|
||||||
if (teamInvites[teamId]) {
|
|
||||||
teamInvites[teamId] = teamInvites[teamId]
|
|
||||||
.filter((invite) => invite.key != key);
|
|
||||||
}
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import PlayerCard from "../components/PlayerCard.vue";
|
import PlayerCard from "../components/PlayerCard.vue";
|
||||||
import { computed, reactive, onMounted, ref } from "vue";
|
import { computed, reactive, onMounted, ref } from "vue";
|
||||||
import { useRosterStore } from "../stores/roster";
|
import { useRosterStore } from "../stores/roster";
|
||||||
import { RouterLink, useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useEventsStore } from "@/stores/events";
|
import { useEventsStore } from "@/stores/events";
|
||||||
import EventSchedulerForm from "@/components/EventSchedulerForm.vue";
|
import EventSchedulerForm from "@/components/EventSchedulerForm.vue";
|
||||||
|
@ -60,6 +60,10 @@ onMounted(async () => {
|
||||||
</main>
|
</main>
|
||||||
<main v-else>
|
<main v-else>
|
||||||
<div class="top">
|
<div class="top">
|
||||||
|
<a>
|
||||||
|
<i class="bi bi-arrow-left" />
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="form-group margin column">
|
<div class="form-group margin column">
|
||||||
|
|
|
@ -27,7 +27,6 @@ const availability = schedule.availability;
|
||||||
const selectionMode = ref(1);
|
const selectionMode = ref(1);
|
||||||
|
|
||||||
const selectedTime = ref(undefined);
|
const selectedTime = ref(undefined);
|
||||||
const selectedIndex = ref(undefined);
|
|
||||||
|
|
||||||
const availabilityOverlay = computed(() => schedule.overlay);
|
const availabilityOverlay = computed(() => schedule.overlay);
|
||||||
|
|
||||||
|
@ -138,12 +137,6 @@ onMounted(() => {
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-group" v-if="isEditing">
|
|
||||||
<span class="subtext">
|
|
||||||
CTRL + Click to fill entire visible column,
|
|
||||||
Shift + Click to fill entire row
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
|
|
@ -36,7 +36,7 @@ onMounted(() => {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (key.value) {
|
if (key.value) {
|
||||||
invitesStore.consumeInvite(key.value.toString())
|
invitesStore.consumeInvite(teamId.value, key.value.toString())
|
||||||
.finally(doFetchTeam);
|
.finally(doFetchTeam);
|
||||||
} else {
|
} else {
|
||||||
doFetchTeam();
|
doFetchTeam();
|
||||||
|
@ -73,7 +73,7 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<h2>Upcoming Events</h2>
|
<h2>Upcoming Events</h2>
|
||||||
<EventList :events="events" :team-context="team" />
|
<EventList :events="events" />
|
||||||
<h2 id="recent-matches-header">
|
<h2 id="recent-matches-header">
|
||||||
Recent Matches
|
Recent Matches
|
||||||
<RouterLink class="button" to="/">
|
<RouterLink class="button" to="/">
|
||||||
|
@ -82,7 +82,7 @@ onMounted(() => {
|
||||||
</button>
|
</button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</h2>
|
</h2>
|
||||||
<em class="subtext" v-if="true">No recent matches.</em>
|
<em class="subtext" v-if="false">No recent matches.</em>
|
||||||
<MatchCard v-else />
|
<MatchCard v-else />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# Use an official Python runtime as a parent image
|
# Use an official Python runtime as a parent image
|
||||||
FROM python:3.12-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
COPY requirements.txt /
|
COPY requirements.txt /
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
from flask import Blueprint, make_response, request
|
from flask import Blueprint, make_response, request
|
||||||
|
|
||||||
from app_db import app, connect_celery_with_app, connect_db_with_app
|
from app_db import app, connect_db_with_app
|
||||||
import login
|
import login
|
||||||
import schedule
|
import schedule
|
||||||
import team
|
import team
|
||||||
from spec import spec
|
from spec import spec
|
||||||
import user
|
import user
|
||||||
import events
|
import events
|
||||||
import match
|
|
||||||
|
|
||||||
connect_db_with_app()
|
connect_db_with_app()
|
||||||
connect_celery_with_app()
|
|
||||||
|
|
||||||
api = Blueprint("api", __name__, url_prefix="/api")
|
api = Blueprint("api", __name__, url_prefix="/api")
|
||||||
api.register_blueprint(login.api_login)
|
api.register_blueprint(login.api_login)
|
||||||
|
@ -18,7 +16,6 @@ api.register_blueprint(schedule.api_schedule)
|
||||||
api.register_blueprint(team.api_team)
|
api.register_blueprint(team.api_team)
|
||||||
api.register_blueprint(user.api_user)
|
api.register_blueprint(user.api_user)
|
||||||
api.register_blueprint(events.api_events)
|
api.register_blueprint(events.api_events)
|
||||||
api.register_blueprint(match.api_match)
|
|
||||||
|
|
||||||
@api.get("/debug/set-cookie")
|
@api.get("/debug/set-cookie")
|
||||||
@api.post("/debug/set-cookie")
|
@api.post("/debug/set-cookie")
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from os import environ
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
@ -16,46 +15,12 @@ convention = {
|
||||||
"pk": "pk_%(table_name)s"
|
"pk": "pk_%(table_name)s"
|
||||||
}
|
}
|
||||||
|
|
||||||
def connect_db_with_app(database_uri = "sqlite:///db.sqlite3", include_migrate=True):
|
def connect_db_with_app():
|
||||||
print("Connecting to database: " + database_uri)
|
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///db.sqlite3"
|
||||||
app.config["SQLALCHEMY_DATABASE_URI"] = database_uri
|
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
if include_migrate:
|
migrate.init_app(app, db)
|
||||||
migrate.init_app(app, db)
|
|
||||||
with app.app_context():
|
|
||||||
print("Running dialect: " + db.engine.dialect.name)
|
|
||||||
import models.match
|
|
||||||
import models.team_match
|
|
||||||
import models.player_match
|
|
||||||
|
|
||||||
def connect_celery_with_app():
|
|
||||||
def celery_init_app(app):
|
|
||||||
from celery import Celery, Task
|
|
||||||
class FlaskTask(Task):
|
|
||||||
def __call__(self, *args: object, **kwargs: object) -> object:
|
|
||||||
with app.app_context():
|
|
||||||
return self.run(*args, **kwargs)
|
|
||||||
|
|
||||||
celery_app = Celery(app.name, task_cls=FlaskTask, broker=app.config["CELERY"]["broker_url"])
|
|
||||||
celery_app.config_from_object(app.config["CELERY"])
|
|
||||||
celery_app.set_default()
|
|
||||||
app.extensions["celery"] = celery_app
|
|
||||||
return celery_app
|
|
||||||
|
|
||||||
app.config.from_mapping(
|
|
||||||
CELERY=dict(
|
|
||||||
broker_url=environ.get("CELERY_BROKER_URL", "redis://redis:6379/0"),
|
|
||||||
result_backend=environ.get("CELERY_RESULT_BACKEND", "redis://redis:6379/0"),
|
|
||||||
task_ignore_result=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
app.config.from_prefixed_env()
|
|
||||||
celery_init_app(app)
|
|
||||||
|
|
||||||
def create_app() -> Flask:
|
|
||||||
return Flask(__name__)
|
|
||||||
|
|
||||||
metadata = MetaData(naming_convention=convention)
|
metadata = MetaData(naming_convention=convention)
|
||||||
app = create_app()
|
app = Flask(__name__)
|
||||||
db = SQLAlchemy(model_class=BaseModel, metadata=metadata)
|
db = SQLAlchemy(model_class=BaseModel, metadata=metadata)
|
||||||
migrate = Migrate(app, db, render_as_batch=True)
|
migrate = Migrate(app, db, render_as_batch=True)
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
from models.player import Player
|
|
||||||
from models.team import Team
|
|
||||||
from models.player_team import PlayerTeam
|
|
||||||
from models.team_integration import TeamDiscordIntegration, TeamLogsTfIntegration
|
|
||||||
from flask_testing import TestCase
|
|
||||||
from app_db import app, db, connect_db_with_app
|
|
||||||
|
|
||||||
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
|
|
||||||
connect_db_with_app(SQLALCHEMY_DATABASE_URI, False)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseTestCase(TestCase):
|
|
||||||
TESTING = True
|
|
||||||
|
|
||||||
def create_app(self):
|
|
||||||
return app
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
db.create_all()
|
|
||||||
self.populate_db()
|
|
||||||
return app
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
from app_db import db
|
|
||||||
db.session.remove()
|
|
||||||
db.drop_all()
|
|
||||||
|
|
||||||
def populate_db(self):
|
|
||||||
print(list(map(lambda x: x.username, db.session.query(Player).all())))
|
|
||||||
player = Player(steam_id=76561198248436608, username="pyro from csgo")
|
|
||||||
team = Team(team_name="Team Pepeja", tz_timezone="America/New_York", minute_offset=30)
|
|
||||||
|
|
||||||
db.session.add(player)
|
|
||||||
db.session.add(team)
|
|
||||||
|
|
||||||
db.session.flush()
|
|
||||||
|
|
||||||
|
|
||||||
player_team = PlayerTeam(
|
|
||||||
player_id=player.steam_id,
|
|
||||||
team_id=team.id,
|
|
||||||
team_role=PlayerTeam.TeamRole.Player,
|
|
||||||
is_team_leader=True,
|
|
||||||
)
|
|
||||||
logs_tf_integration = TeamLogsTfIntegration(
|
|
||||||
team_id=team.id,
|
|
||||||
min_team_member_count=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
db.session.add(player_team)
|
|
||||||
db.session.add(logs_tf_integration)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
|
@ -1,227 +0,0 @@
|
||||||
from collections.abc import Generator
|
|
||||||
from datetime import timedelta, datetime
|
|
||||||
from time import sleep
|
|
||||||
import requests
|
|
||||||
from sqlalchemy.sql import func, update
|
|
||||||
from sqlalchemy.types import DATETIME, Interval
|
|
||||||
import app_db
|
|
||||||
import models.match
|
|
||||||
from models.match import Match
|
|
||||||
from models.team_match import TeamMatch
|
|
||||||
from models.player import Player
|
|
||||||
from models.player_match import PlayerMatch
|
|
||||||
from models.player_team import PlayerTeam
|
|
||||||
from models.team_integration import TeamLogsTfIntegration
|
|
||||||
from celery import shared_task
|
|
||||||
|
|
||||||
|
|
||||||
FETCH_URL = "https://logs.tf/api/v1/log/{}"
|
|
||||||
SEARCH_URL = "https://logs.tf/api/v1/log?limit=25?offset={}"
|
|
||||||
|
|
||||||
|
|
||||||
def get_log_ids(last_log_id: int):
|
|
||||||
current: int = 2147483647
|
|
||||||
while current > last_log_id:
|
|
||||||
response = requests.get(SEARCH_URL.format(current))
|
|
||||||
for summary in response.json()["logs"]:
|
|
||||||
id: int = summary["id"]
|
|
||||||
if id == last_log_id:
|
|
||||||
break
|
|
||||||
# yield models.match.RawLogSummary.from_response(summary)
|
|
||||||
yield id
|
|
||||||
current = id
|
|
||||||
|
|
||||||
def extract(log_id: int) -> models.match.RawLogDetails:
|
|
||||||
response = requests.get(FETCH_URL.format(log_id))
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def steam3_to_steam64(steam3_id: str) -> int:
|
|
||||||
if steam3_id.startswith("[U:1:") and steam3_id.endswith("]"):
|
|
||||||
numeric_id = int(steam3_id[5:-1])
|
|
||||||
steam64_id = numeric_id + 76561197960265728
|
|
||||||
return steam64_id
|
|
||||||
else:
|
|
||||||
raise ValueError("Invalid Steam3 ID format")
|
|
||||||
|
|
||||||
def steam64_to_steam3(steam64_id: int) -> str:
|
|
||||||
if steam64_id >= 76561197960265728:
|
|
||||||
numeric_id = steam64_id - 76561197960265728
|
|
||||||
steam3_id = f"[U:1:{numeric_id}]"
|
|
||||||
return steam3_id
|
|
||||||
else:
|
|
||||||
raise ValueError("Invalid Steam64 ID format")
|
|
||||||
|
|
||||||
def extract_steam_ids(players: dict[str, models.match.LogPlayer]):
|
|
||||||
blue_steam_ids: list[int] = []
|
|
||||||
red_steam_ids: list[int] = []
|
|
||||||
steam_ids: list[int] = []
|
|
||||||
|
|
||||||
for steam_id, player in players.items():
|
|
||||||
steam64_id = steam3_to_steam64(steam_id)
|
|
||||||
steam_ids.append(steam64_id)
|
|
||||||
if player["team"] == "Red":
|
|
||||||
red_steam_ids.append(steam64_id)
|
|
||||||
elif player["team"] == "Blue":
|
|
||||||
blue_steam_ids.append(steam64_id)
|
|
||||||
|
|
||||||
return steam_ids, blue_steam_ids, red_steam_ids
|
|
||||||
|
|
||||||
@shared_task
|
|
||||||
def update_playtime(steam_ids: list[int]):
|
|
||||||
# update players with playtime (recalculate through aggregation)
|
|
||||||
subquery = (
|
|
||||||
app_db.db.session.query(
|
|
||||||
PlayerTeam.id,
|
|
||||||
#func.datetime(func.sum(Match.duration), "unixepoch").label("total_playtime")
|
|
||||||
func.sum(Match.duration).label("total_playtime")
|
|
||||||
)
|
|
||||||
.join(PlayerMatch, PlayerMatch.player_id == PlayerTeam.player_id)
|
|
||||||
.join(TeamMatch, TeamMatch.team_id == PlayerTeam.team_id)
|
|
||||||
.join(Match, Match.logs_tf_id == TeamMatch.match_id)
|
|
||||||
.where(PlayerTeam.player_id.in_(steam_ids))
|
|
||||||
.group_by(PlayerTeam.id)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
|
|
||||||
update_query = app_db.db.session.execute(
|
|
||||||
update(PlayerTeam)
|
|
||||||
.where(PlayerTeam.id == subquery.c.id)
|
|
||||||
.values(playtime=subquery.c.total_playtime)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_common_teams(steam_ids: list[int]):
|
|
||||||
#aggregate_func = None
|
|
||||||
|
|
||||||
#with app_db.app.app_context():
|
|
||||||
# if app_db.db.engine.name == "postgresql":
|
|
||||||
# aggregate_func = func.array_agg(PlayerTeam.player_id)
|
|
||||||
# else:
|
|
||||||
# aggregate_func = func.group_concat(PlayerTeam.player_id, ",")
|
|
||||||
|
|
||||||
#if aggregate_func is None:
|
|
||||||
# raise NotImplementedError("Unsupported database engine")
|
|
||||||
|
|
||||||
return (
|
|
||||||
app_db.db.session.query(
|
|
||||||
PlayerTeam.team_id,
|
|
||||||
func.count(PlayerTeam.team_id),
|
|
||||||
TeamLogsTfIntegration.min_team_member_count,
|
|
||||||
#aggregate_func
|
|
||||||
)
|
|
||||||
.outerjoin(
|
|
||||||
TeamLogsTfIntegration,
|
|
||||||
TeamLogsTfIntegration.team_id == PlayerTeam.team_id
|
|
||||||
)
|
|
||||||
.where(PlayerTeam.player_id.in_(steam_ids))
|
|
||||||
.group_by(PlayerTeam.team_id)
|
|
||||||
.order_by(func.count(PlayerTeam.team_id).desc())
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
def transform(
|
|
||||||
log_id: int,
|
|
||||||
details: models.match.RawLogDetails,
|
|
||||||
existing_match: Match | None = None,
|
|
||||||
invoked_by_team_id: int | None = None
|
|
||||||
):
|
|
||||||
steam_ids, blue_steam_ids, red_steam_ids = extract_steam_ids(details["players"])
|
|
||||||
|
|
||||||
# fetch players in steam_ids if they exist
|
|
||||||
players = (
|
|
||||||
app_db.db.session.query(Player)
|
|
||||||
.where(Player.steam_id.in_(steam_ids))
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(players) == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not existing_match:
|
|
||||||
match = Match()
|
|
||||||
match.logs_tf_id = log_id
|
|
||||||
match.logs_tf_title = details["info"]["title"]
|
|
||||||
match.blue_score = details["teams"]["Blue"]["score"]
|
|
||||||
match.red_score = details["teams"]["Red"]["score"]
|
|
||||||
match.duration = details["length"]
|
|
||||||
match.match_time = datetime.fromtimestamp(details["info"]["date"])
|
|
||||||
yield match
|
|
||||||
else:
|
|
||||||
match = existing_match
|
|
||||||
|
|
||||||
#app_db.db.session.add(match)
|
|
||||||
|
|
||||||
for player in players:
|
|
||||||
player_data = details["players"][steam64_to_steam3(player.steam_id)]
|
|
||||||
|
|
||||||
if not player_data:
|
|
||||||
print(f"Player {player.steam_id} not found in log {log_id}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
player_match = PlayerMatch()
|
|
||||||
player_match.player_id = player.steam_id
|
|
||||||
player_match.match_id = match.logs_tf_id
|
|
||||||
player_match.kills = player_data["kills"]
|
|
||||||
player_match.deaths = player_data["deaths"]
|
|
||||||
player_match.assists = player_data["assists"]
|
|
||||||
player_match.damage = player_data["dmg"]
|
|
||||||
player_match.damage_taken = player_data["dt"]
|
|
||||||
|
|
||||||
yield player_match
|
|
||||||
|
|
||||||
# get common teams
|
|
||||||
# if common teams exist, automatically create a TeamMatch for the match
|
|
||||||
for team, ids in { "Blue": blue_steam_ids, "Red": red_steam_ids }.items():
|
|
||||||
for row in get_common_teams(ids):
|
|
||||||
row_tuple = tuple(row)
|
|
||||||
team_id = row_tuple[0]
|
|
||||||
player_count = row_tuple[1]
|
|
||||||
log_min_player_count = row_tuple[2] or 100
|
|
||||||
|
|
||||||
should_create_team_match = False
|
|
||||||
|
|
||||||
|
|
||||||
if invoked_by_team_id and team_id == invoked_by_team_id:
|
|
||||||
# if manually uploading a log, then add TeamMatch for the team
|
|
||||||
# that uploaded the log
|
|
||||||
should_create_team_match = True
|
|
||||||
elif not invoked_by_team_id and player_count >= log_min_player_count:
|
|
||||||
# if automatically fetching logs, then add TeamMatch for teams
|
|
||||||
# with player count >= log_min_player_count
|
|
||||||
should_create_team_match = True
|
|
||||||
|
|
||||||
if should_create_team_match:
|
|
||||||
team_match = TeamMatch()
|
|
||||||
team_match.team_id = team_id
|
|
||||||
team_match.match_id = match.logs_tf_id
|
|
||||||
team_match.team_color = team
|
|
||||||
yield team_match
|
|
||||||
|
|
||||||
#app_db.db.session.flush()
|
|
||||||
update_playtime.delay(list(map(lambda x: x.steam_id, players)))
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
|
||||||
def load_specific_match(id: int, team_id: int | None):
|
|
||||||
match = (
|
|
||||||
app_db.db.session.query(Match)
|
|
||||||
.where(Match.logs_tf_id == id)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
raw_match = extract(id)
|
|
||||||
app_db.db.session.bulk_save_objects(transform(id, raw_match, match, team_id))
|
|
||||||
app_db.db.session.commit()
|
|
||||||
sleep(3) # avoid rate limiting if multiple tasks are queued
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
last: int = (
|
|
||||||
app_db.db.session.query(
|
|
||||||
func.max(models.match.Match.logs_tf_id)
|
|
||||||
).scalar()
|
|
||||||
) or 3767233
|
|
||||||
|
|
||||||
for summary in get_log_ids(last):
|
|
||||||
print(summary)
|
|
||||||
sleep(3)
|
|
|
@ -1,8 +0,0 @@
|
||||||
from app_db import connect_celery_with_app, app, connect_db_with_app
|
|
||||||
|
|
||||||
connect_db_with_app("sqlite:///db.sqlite3", False)
|
|
||||||
connect_celery_with_app()
|
|
||||||
|
|
||||||
celery_app = app.extensions["celery"]
|
|
||||||
|
|
||||||
import jobs.fetch_logstf
|
|
|
@ -1,108 +0,0 @@
|
||||||
from flask import Blueprint, abort
|
|
||||||
from pydantic.v1 import validator
|
|
||||||
#from pydantic.functional_validators import field_validator
|
|
||||||
from spectree import Response
|
|
||||||
from sqlalchemy.orm import joinedload
|
|
||||||
from jobs.fetch_logstf import load_specific_match
|
|
||||||
from models.player_team import PlayerTeam
|
|
||||||
from models.team import Team
|
|
||||||
from models.team_match import TeamMatch, TeamMatchSchema
|
|
||||||
from spec import BaseModel, spec
|
|
||||||
from middleware import requires_authentication, requires_team_membership
|
|
||||||
from models.match import Match, MatchSchema
|
|
||||||
from app_db import db
|
|
||||||
from models.player import Player
|
|
||||||
from models.player_match import PlayerMatch
|
|
||||||
|
|
||||||
|
|
||||||
api_match = Blueprint("match", __name__, url_prefix="/match")
|
|
||||||
|
|
||||||
@api_match.get("/id/<int:match_id>")
|
|
||||||
@spec.validate(
|
|
||||||
resp=Response(
|
|
||||||
HTTP_200=MatchSchema,
|
|
||||||
),
|
|
||||||
|
|
||||||
)
|
|
||||||
@requires_authentication
|
|
||||||
def get_match(player: Player, match_id: int, **_):
|
|
||||||
match = (
|
|
||||||
db.session.query(Match)
|
|
||||||
.join(PlayerMatch)
|
|
||||||
.where(Match.logs_tf_id == match_id)
|
|
||||||
.where(PlayerMatch.player_id == player.steam_id)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
return MatchSchema.from_model(match).dict(by_alias=True), 200
|
|
||||||
|
|
||||||
class SubmitMatchJson(BaseModel):
|
|
||||||
match_ids: list[int]
|
|
||||||
|
|
||||||
@validator("match_ids")
|
|
||||||
@classmethod
|
|
||||||
def validate_match_ids(cls, match_ids):
|
|
||||||
if len(match_ids) < 1:
|
|
||||||
raise ValueError("match_ids must contain at least one match id")
|
|
||||||
if len(match_ids) > 10:
|
|
||||||
raise ValueError("match_ids must contain at most 10 match ids")
|
|
||||||
|
|
||||||
@api_match.put("/")
|
|
||||||
@spec.validate(
|
|
||||||
resp=Response(
|
|
||||||
HTTP_204=None,
|
|
||||||
),
|
|
||||||
operation_id="submit_match",
|
|
||||||
)
|
|
||||||
@requires_authentication
|
|
||||||
def submit_match(json: SubmitMatchJson, **_):
|
|
||||||
import sys
|
|
||||||
print(json, file=sys.stderr)
|
|
||||||
if json.match_ids is None:
|
|
||||||
print("json.match_ids is None", file=sys.stderr)
|
|
||||||
|
|
||||||
for id in json.match_ids:
|
|
||||||
load_specific_match.delay(id, None)
|
|
||||||
return { }, 204
|
|
||||||
|
|
||||||
@api_match.get("/team/<int:team_id>")
|
|
||||||
@spec.validate(
|
|
||||||
resp=Response(
|
|
||||||
HTTP_200=list[TeamMatchSchema],
|
|
||||||
),
|
|
||||||
operation_id="get_matches_for_team",
|
|
||||||
)
|
|
||||||
@requires_authentication
|
|
||||||
@requires_team_membership()
|
|
||||||
def get_matches_for_team(team_id: Team, **_):
|
|
||||||
matches = (
|
|
||||||
db.session.query(TeamMatch)
|
|
||||||
.where(TeamMatch.team_id == team_id)
|
|
||||||
.options(joinedload(TeamMatch.match))
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
return [TeamMatchSchema.from_model(match).dict(by_alias=True) for match in matches], 200
|
|
||||||
|
|
||||||
@api_match.get("/player")
|
|
||||||
@spec.validate(
|
|
||||||
resp=Response(
|
|
||||||
HTTP_200=list[TeamMatchSchema],
|
|
||||||
),
|
|
||||||
operation_id="get_matches_for_player_teams",
|
|
||||||
)
|
|
||||||
@requires_authentication
|
|
||||||
def get_matches_for_player_teams(player: Player, **_):
|
|
||||||
matches = (
|
|
||||||
db.session.query(TeamMatch)
|
|
||||||
.join(PlayerTeam, PlayerTeam.team_id == TeamMatch.team_id)
|
|
||||||
.join(Match)
|
|
||||||
.join(PlayerMatch)
|
|
||||||
.where(PlayerMatch.player_id == player.steam_id)
|
|
||||||
.options(joinedload(TeamMatch.match))
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
return [TeamMatchSchema.from_model(match).dict(by_alias=True) for match in matches], 200
|
|
|
@ -34,8 +34,8 @@ def get_engine_url():
|
||||||
|
|
||||||
# add your model's MetaData object here
|
# add your model's MetaData object here
|
||||||
# for 'autogenerate' support
|
# for 'autogenerate' support
|
||||||
from app_db import BaseModel
|
# from myapp import mymodel
|
||||||
target_metadata = BaseModel.metadata
|
# target_metadata = mymodel.Base.metadata
|
||||||
config.set_main_option('sqlalchemy.url', get_engine_url())
|
config.set_main_option('sqlalchemy.url', get_engine_url())
|
||||||
target_db = current_app.extensions['migrate'].db
|
target_db = current_app.extensions['migrate'].db
|
||||||
|
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
"""Add the rest of the match tables
|
|
||||||
|
|
||||||
Revision ID: 7995474ef2cc
|
|
||||||
Revises: fda727438444
|
|
||||||
Create Date: 2024-12-09 16:17:25.518959
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '7995474ef2cc'
|
|
||||||
down_revision = 'fda727438444'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('players_matches',
|
|
||||||
sa.Column('player_id', sa.BigInteger(), nullable=False),
|
|
||||||
sa.Column('match_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('kills', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('deaths', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('assists', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('damage', sa.BigInteger(), nullable=False),
|
|
||||||
sa.Column('damage_taken', sa.BigInteger(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['match_id'], ['matches.logs_tf_id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['player_id'], ['players.steam_id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('player_id', 'match_id')
|
|
||||||
)
|
|
||||||
op.create_table('teams_matches',
|
|
||||||
sa.Column('team_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('match_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('team_color', sa.String(length=4), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['match_id'], ['matches.logs_tf_id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['team_id'], ['teams.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('team_id', 'match_id')
|
|
||||||
)
|
|
||||||
with op.batch_alter_table('matches', schema=None) as batch_op:
|
|
||||||
batch_op.add_column(sa.Column('blue_score', sa.Integer(), nullable=False))
|
|
||||||
batch_op.add_column(sa.Column('red_score', sa.Integer(), nullable=False))
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('matches', schema=None) as batch_op:
|
|
||||||
batch_op.drop_column('red_score')
|
|
||||||
batch_op.drop_column('blue_score')
|
|
||||||
|
|
||||||
op.drop_table('teams_matches')
|
|
||||||
op.drop_table('players_matches')
|
|
||||||
# ### end Alembic commands ###
|
|
|
@ -1,50 +0,0 @@
|
||||||
"""Change intervals to integers
|
|
||||||
|
|
||||||
Revision ID: d15570037f47
|
|
||||||
Revises: 7995474ef2cc
|
|
||||||
Create Date: 2024-12-09 20:16:18.385467
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'd15570037f47'
|
|
||||||
down_revision = '7995474ef2cc'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('matches', schema=None) as batch_op:
|
|
||||||
batch_op.alter_column('duration',
|
|
||||||
existing_type=sa.DATETIME(),
|
|
||||||
type_=sa.Integer(),
|
|
||||||
existing_nullable=False)
|
|
||||||
|
|
||||||
with op.batch_alter_table('players_teams', schema=None) as batch_op:
|
|
||||||
batch_op.alter_column('playtime',
|
|
||||||
existing_type=sa.DATETIME(),
|
|
||||||
type_=sa.Integer(),
|
|
||||||
existing_nullable=False)
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('players_teams', schema=None) as batch_op:
|
|
||||||
batch_op.alter_column('playtime',
|
|
||||||
existing_type=sa.Integer(),
|
|
||||||
type_=sa.DATETIME(),
|
|
||||||
existing_nullable=False)
|
|
||||||
|
|
||||||
with op.batch_alter_table('matches', schema=None) as batch_op:
|
|
||||||
batch_op.alter_column('duration',
|
|
||||||
existing_type=sa.Integer(),
|
|
||||||
type_=sa.DATETIME(),
|
|
||||||
existing_nullable=False)
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
|
@ -1,35 +0,0 @@
|
||||||
"""Add match table
|
|
||||||
|
|
||||||
Revision ID: fda727438444
|
|
||||||
Revises: c242e3f99c64
|
|
||||||
Create Date: 2024-12-09 12:45:16.974122
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'fda727438444'
|
|
||||||
down_revision = 'c242e3f99c64'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('matches',
|
|
||||||
sa.Column('logs_tf_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('logs_tf_title', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('duration', sa.Interval(), nullable=False),
|
|
||||||
sa.Column('match_time', sa.TIMESTAMP(), nullable=False),
|
|
||||||
sa.Column('created_at', sa.TIMESTAMP(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('logs_tf_id')
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_table('matches')
|
|
||||||
# ### end Alembic commands ###
|
|
|
@ -1,99 +0,0 @@
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import TypedDict
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
from sqlalchemy.types import TIMESTAMP, Integer, Interval, String
|
|
||||||
import app_db
|
|
||||||
import spec
|
|
||||||
|
|
||||||
|
|
||||||
class Match(app_db.BaseModel):
|
|
||||||
__tablename__ = "matches"
|
|
||||||
|
|
||||||
logs_tf_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
||||||
logs_tf_title: Mapped[str] = mapped_column(String(255))
|
|
||||||
duration: Mapped[int] = mapped_column(Integer)
|
|
||||||
match_time: Mapped[datetime] = mapped_column(TIMESTAMP)
|
|
||||||
blue_score: Mapped[int] = mapped_column(Integer)
|
|
||||||
red_score: Mapped[int] = mapped_column(Integer)
|
|
||||||
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
|
|
||||||
|
|
||||||
teams: Mapped["TeamMatch"] = relationship("TeamMatch", back_populates="match")
|
|
||||||
players: Mapped["PlayerMatch"] = relationship("PlayerMatch", back_populates="match")
|
|
||||||
|
|
||||||
class MatchSchema(spec.BaseModel):
|
|
||||||
logs_tf_id: int
|
|
||||||
logs_tf_title: str
|
|
||||||
duration: int
|
|
||||||
match_time: datetime
|
|
||||||
blue_score: int
|
|
||||||
red_score: int
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_model(cls, model: Match):
|
|
||||||
return cls(
|
|
||||||
logs_tf_id=model.logs_tf_id,
|
|
||||||
logs_tf_title=model.logs_tf_title,
|
|
||||||
duration=model.duration,
|
|
||||||
match_time=model.match_time,
|
|
||||||
blue_score=model.blue_score,
|
|
||||||
red_score=model.red_score,
|
|
||||||
created_at=model.created_at
|
|
||||||
)
|
|
||||||
|
|
||||||
class RawLogSummary:
|
|
||||||
id: int
|
|
||||||
title: str
|
|
||||||
map: str
|
|
||||||
date: int
|
|
||||||
players: int
|
|
||||||
views: int
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_response(cls, response: dict):
|
|
||||||
object = cls()
|
|
||||||
object.id = response["id"]
|
|
||||||
object.title = response["title"]
|
|
||||||
object.map = response["map"]
|
|
||||||
object.date = response["date"]
|
|
||||||
object.players = response["players"]
|
|
||||||
object.views = response["views"]
|
|
||||||
return object
|
|
||||||
|
|
||||||
class LogTeam(TypedDict):
|
|
||||||
score: int
|
|
||||||
#kills: int
|
|
||||||
#deaths: int
|
|
||||||
#dmg: int
|
|
||||||
#charges: int
|
|
||||||
#drops: int
|
|
||||||
#firstcaps: int
|
|
||||||
#caps: int
|
|
||||||
|
|
||||||
class LogPlayer(TypedDict):
|
|
||||||
team: str
|
|
||||||
kills: int
|
|
||||||
deaths: int
|
|
||||||
assists: int
|
|
||||||
dmg: int
|
|
||||||
dt: int
|
|
||||||
|
|
||||||
class LogInfo(TypedDict):
|
|
||||||
title: str
|
|
||||||
map: str
|
|
||||||
date: int
|
|
||||||
|
|
||||||
class LogRound(TypedDict):
|
|
||||||
length: int
|
|
||||||
|
|
||||||
class RawLogDetails(TypedDict):
|
|
||||||
teams: dict[str, LogTeam]
|
|
||||||
players: dict[str, LogPlayer]
|
|
||||||
#rounds: list[LogRound]
|
|
||||||
info: LogInfo
|
|
||||||
length: int
|
|
||||||
|
|
||||||
|
|
||||||
from models.team_match import TeamMatch
|
|
||||||
from models.player_match import PlayerMatch
|
|
|
@ -16,7 +16,6 @@ class Player(app_db.BaseModel):
|
||||||
teams: Mapped[list["PlayerTeam"]] = relationship(back_populates="player")
|
teams: Mapped[list["PlayerTeam"]] = relationship(back_populates="player")
|
||||||
auth_sessions: Mapped[list["AuthSession"]] = relationship(back_populates="player")
|
auth_sessions: Mapped[list["AuthSession"]] = relationship(back_populates="player")
|
||||||
events: Mapped[list["PlayerEvent"]] = relationship(back_populates="player")
|
events: Mapped[list["PlayerEvent"]] = relationship(back_populates="player")
|
||||||
matches: Mapped[list["PlayerMatch"]] = relationship(back_populates="player")
|
|
||||||
|
|
||||||
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
|
||||||
|
|
||||||
|
@ -32,4 +31,3 @@ class PlayerSchema(spec.BaseModel):
|
||||||
from models.auth_session import AuthSession
|
from models.auth_session import AuthSession
|
||||||
from models.player_event import PlayerEvent
|
from models.player_event import PlayerEvent
|
||||||
from models.player_team import PlayerTeam
|
from models.player_team import PlayerTeam
|
||||||
from models.player_match import PlayerMatch
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ class PlayerEventRolesSchema(spec.BaseModel):
|
||||||
role=RoleSchema.from_model(player_event.role) if player_event.role else None,
|
role=RoleSchema.from_model(player_event.role) if player_event.role else None,
|
||||||
roles=[RoleSchema.from_model(role) for role in player_team.player_roles],
|
roles=[RoleSchema.from_model(role) for role in player_team.player_roles],
|
||||||
has_confirmed=player_event.has_confirmed,
|
has_confirmed=player_event.has_confirmed,
|
||||||
playtime=player_team.playtime,
|
playtime=int(player_team.playtime.total_seconds()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
from sqlalchemy.schema import ForeignKey
|
|
||||||
from sqlalchemy.types import BigInteger, Integer
|
|
||||||
import app_db
|
|
||||||
|
|
||||||
|
|
||||||
class PlayerMatch(app_db.BaseModel):
|
|
||||||
__tablename__ = "players_matches"
|
|
||||||
|
|
||||||
player_id: Mapped[int] = mapped_column(ForeignKey("players.steam_id"), primary_key=True)
|
|
||||||
match_id: Mapped[int] = mapped_column(ForeignKey("matches.logs_tf_id"), primary_key=True)
|
|
||||||
|
|
||||||
kills: Mapped[int] = mapped_column(Integer)
|
|
||||||
deaths: Mapped[int] = mapped_column(Integer)
|
|
||||||
assists: Mapped[int] = mapped_column(Integer)
|
|
||||||
damage: Mapped[int] = mapped_column(BigInteger)
|
|
||||||
damage_taken: Mapped[int] = mapped_column(BigInteger)
|
|
||||||
|
|
||||||
player: Mapped["Player"] = relationship("Player", back_populates="matches")
|
|
||||||
match: Mapped["Match"] = relationship("Match", back_populates="players")
|
|
||||||
|
|
||||||
|
|
||||||
from models.match import Match
|
|
||||||
from models.player import Player
|
|
|
@ -34,7 +34,7 @@ class PlayerTeam(app_db.BaseModel):
|
||||||
availability: Mapped[list["PlayerTeamAvailability"]] = relationship(back_populates="player_team")
|
availability: Mapped[list["PlayerTeamAvailability"]] = relationship(back_populates="player_team")
|
||||||
|
|
||||||
team_role: Mapped[TeamRole] = mapped_column(Enum(TeamRole), default=TeamRole.Player)
|
team_role: Mapped[TeamRole] = mapped_column(Enum(TeamRole), default=TeamRole.Player)
|
||||||
playtime: Mapped[int] = mapped_column(Integer, default=0)
|
playtime: Mapped[timedelta] = mapped_column(Interval, default=timedelta(0))
|
||||||
is_team_leader: Mapped[bool] = mapped_column(Boolean, default=False)
|
is_team_leader: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
|
||||||
|
|
||||||
|
|
|
@ -34,8 +34,6 @@ class Team(app_db.BaseModel):
|
||||||
lazy="raise",
|
lazy="raise",
|
||||||
)
|
)
|
||||||
|
|
||||||
matches: Mapped[list["TeamMatch"]] = relationship(back_populates="team")
|
|
||||||
|
|
||||||
def update_integrations(self, integrations: "TeamIntegrationSchema"):
|
def update_integrations(self, integrations: "TeamIntegrationSchema"):
|
||||||
if integrations.discord_integration:
|
if integrations.discord_integration:
|
||||||
discord_integration = self.discord_integration \
|
discord_integration = self.discord_integration \
|
||||||
|
@ -132,4 +130,3 @@ from models.team_integration import (
|
||||||
TeamLogsTfIntegrationSchema,
|
TeamLogsTfIntegrationSchema,
|
||||||
)
|
)
|
||||||
from models.event import Event
|
from models.event import Event
|
||||||
from models.team_match import TeamMatch
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
from sqlalchemy.schema import ForeignKey
|
|
||||||
from sqlalchemy.types import Integer, String
|
|
||||||
import app_db
|
|
||||||
import spec
|
|
||||||
|
|
||||||
|
|
||||||
class TeamMatch(app_db.BaseModel):
|
|
||||||
__tablename__ = "teams_matches"
|
|
||||||
|
|
||||||
team_id: Mapped[int] = mapped_column(ForeignKey("teams.id"), primary_key=True)
|
|
||||||
match_id: Mapped[int] = mapped_column(ForeignKey("matches.logs_tf_id"), primary_key=True)
|
|
||||||
team_color: Mapped[str] = mapped_column(String(4))
|
|
||||||
|
|
||||||
team: Mapped["Team"] = relationship("Team", back_populates="matches")
|
|
||||||
match: Mapped["Match"] = relationship("Match", back_populates="teams")
|
|
||||||
|
|
||||||
class TeamMatchSchema(spec.BaseModel):
|
|
||||||
match: "MatchSchema"
|
|
||||||
our_score: int
|
|
||||||
their_score: int
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_model(cls, model: "TeamMatch"):
|
|
||||||
our_score = model.match.blue_score if model.team_color == "Blue" else model.match.red_score
|
|
||||||
their_score = model.match.red_score if model.team_color == "Blue" else model.match.blue_score
|
|
||||||
|
|
||||||
return cls(
|
|
||||||
match=MatchSchema.from_model(model.match),
|
|
||||||
our_score=our_score,
|
|
||||||
their_score=their_score,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
from models.match import Match, MatchSchema
|
|
||||||
from models.team import Team
|
|
|
@ -9,9 +9,9 @@ Flask-SQLAlchemy
|
||||||
SQLAlchemy-Utc
|
SQLAlchemy-Utc
|
||||||
|
|
||||||
# form/data validation
|
# form/data validation
|
||||||
pydantic==2.9.2
|
pydantic
|
||||||
spectree==1.4.1 # generates OpenAPI documents for us to make TypeScript API
|
spectree # generates OpenAPI documents for us to make TypeScript API clients
|
||||||
# clients based on our pydantic models
|
# based on our pydantic models
|
||||||
|
|
||||||
# DB migrations
|
# DB migrations
|
||||||
alembic
|
alembic
|
||||||
|
@ -22,7 +22,3 @@ requests
|
||||||
pytz # timezone handling
|
pytz # timezone handling
|
||||||
|
|
||||||
discord-webhook # for sending messages to Discord webhooks
|
discord-webhook # for sending messages to Discord webhooks
|
||||||
|
|
||||||
celery[redis]
|
|
||||||
|
|
||||||
Flask-Testing
|
|
||||||
|
|
|
@ -283,7 +283,7 @@ def view_available_at_time(query: ViewAvailablePlayersQuery, player: Player, **k
|
||||||
|
|
||||||
return PlayerTeamAvailabilityRoleSchema(
|
return PlayerTeamAvailabilityRoleSchema(
|
||||||
player=PlayerSchema.from_model(player),
|
player=PlayerSchema.from_model(player),
|
||||||
playtime=player_team.playtime,
|
playtime=int(player_team.playtime.total_seconds()),
|
||||||
availability=player_avail.availability,
|
availability=player_avail.availability,
|
||||||
roles=list(map(RoleSchema.from_model, player_roles)),
|
roles=list(map(RoleSchema.from_model, player_roles)),
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,7 +9,7 @@ from models.player import Player, PlayerSchema
|
||||||
from models.player_team import PlayerTeam
|
from models.player_team import PlayerTeam
|
||||||
from models.player_team_availability import PlayerTeamAvailability
|
from models.player_team_availability import PlayerTeamAvailability
|
||||||
from models.player_team_role import PlayerTeamRole, RoleSchema
|
from models.player_team_role import PlayerTeamRole, RoleSchema
|
||||||
from models.team import Team, TeamSchema, TeamWithRoleSchema
|
from models.team import Team, TeamSchema
|
||||||
from middleware import assert_team_authority, requires_authentication, requires_team_membership
|
from middleware import assert_team_authority, requires_authentication, requires_team_membership
|
||||||
from spec import spec, BaseModel
|
from spec import spec, BaseModel
|
||||||
from team_invite import api_team_invite
|
from team_invite import api_team_invite
|
||||||
|
@ -51,7 +51,7 @@ class ViewTeamResponse(BaseModel):
|
||||||
team: TeamSchema
|
team: TeamSchema
|
||||||
|
|
||||||
class ViewTeamsResponse(BaseModel):
|
class ViewTeamsResponse(BaseModel):
|
||||||
teams: list[TeamWithRoleSchema]
|
teams: list[TeamSchema]
|
||||||
|
|
||||||
@api_team.post("/")
|
@api_team.post("/")
|
||||||
@spec.validate(
|
@spec.validate(
|
||||||
|
@ -290,7 +290,7 @@ def view_team(team_id: int, **kwargs):
|
||||||
|
|
||||||
def fetch_teams_for_player(player: Player, team_id: int | None):
|
def fetch_teams_for_player(player: Player, team_id: int | None):
|
||||||
q = db.session.query(
|
q = db.session.query(
|
||||||
Team, PlayerTeam
|
Team
|
||||||
).join(
|
).join(
|
||||||
PlayerTeam
|
PlayerTeam
|
||||||
).join(
|
).join(
|
||||||
|
@ -303,15 +303,15 @@ def fetch_teams_for_player(player: Player, team_id: int | None):
|
||||||
q = q.where(PlayerTeam.team_id == team_id)
|
q = q.where(PlayerTeam.team_id == team_id)
|
||||||
|
|
||||||
if team_id is None:
|
if team_id is None:
|
||||||
players_teams = list(map(lambda x: x.tuple()[1], q.all()))
|
teams = q.all()
|
||||||
return ViewTeamsResponse(
|
return ViewTeamsResponse(
|
||||||
teams=list(map(TeamWithRoleSchema.from_player_team, players_teams))
|
teams=list(map(TeamSchema.from_model, teams))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
team = q.one_or_none()
|
team = q.one_or_none()
|
||||||
if team:
|
if team:
|
||||||
return ViewTeamResponse(
|
return ViewTeamResponse(
|
||||||
team=TeamSchema.from_model(team.tuple()[0])
|
team=TeamSchema.from_model(team)
|
||||||
)
|
)
|
||||||
|
|
||||||
class ViewTeamMembersResponse(PlayerSchema):
|
class ViewTeamMembersResponse(PlayerSchema):
|
||||||
|
@ -377,7 +377,7 @@ def view_team_members(player: Player, team_id: int, **kwargs):
|
||||||
steam_id=str(player.steam_id),
|
steam_id=str(player.steam_id),
|
||||||
roles=list(map(map_role_to_schema, roles)),
|
roles=list(map(map_role_to_schema, roles)),
|
||||||
availability=availability,
|
availability=availability,
|
||||||
playtime=player_team.playtime / 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,
|
is_team_leader=player_team.is_team_leader,
|
||||||
).dict(by_alias=True)
|
).dict(by_alias=True)
|
||||||
|
|
|
@ -8,7 +8,7 @@ from middleware import requires_authentication, requires_team_membership
|
||||||
from models.player import Player
|
from models.player import Player
|
||||||
from models.player_team import PlayerTeam
|
from models.player_team import PlayerTeam
|
||||||
from models.team_invite import TeamInvite, TeamInviteSchema
|
from models.team_invite import TeamInvite, TeamInviteSchema
|
||||||
from spec import BaseModel, spec
|
from spec import spec
|
||||||
|
|
||||||
|
|
||||||
api_team_invite = Blueprint("team_invite", __name__)
|
api_team_invite = Blueprint("team_invite", __name__)
|
||||||
|
@ -75,31 +75,27 @@ def create_invite(team_id: int, **_):
|
||||||
|
|
||||||
return response.dict(by_alias=True), 200
|
return response.dict(by_alias=True), 200
|
||||||
|
|
||||||
class ConsumeInviteResponse(BaseModel):
|
@api_team_invite.post("/id/<team_id>/consume-invite/<key>")
|
||||||
team_id: int
|
|
||||||
|
|
||||||
@api_team_invite.post("/consume-invite/<key>")
|
|
||||||
@spec.validate(
|
@spec.validate(
|
||||||
resp=Response(
|
resp=Response(
|
||||||
HTTP_200=ConsumeInviteResponse,
|
HTTP_204=None,
|
||||||
HTTP_404=None,
|
HTTP_404=None,
|
||||||
),
|
),
|
||||||
operation_id="consume_invite"
|
operation_id="consume_invite"
|
||||||
)
|
)
|
||||||
@requires_authentication
|
@requires_authentication
|
||||||
def consume_invite(player: Player, key: str, **_):
|
def consume_invite(player: Player, team_id: int, key: str, **_):
|
||||||
invite = db.session.query(
|
invite = db.session.query(
|
||||||
TeamInvite
|
TeamInvite
|
||||||
|
).where(
|
||||||
|
TeamInvite.team_id == team_id
|
||||||
).where(
|
).where(
|
||||||
TeamInvite.key == key
|
TeamInvite.key == key
|
||||||
).one_or_none()
|
).one_or_none()
|
||||||
|
|
||||||
|
|
||||||
if not invite:
|
if not invite:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
team_id = invite.team_id
|
|
||||||
|
|
||||||
player_team = db.session.query(
|
player_team = db.session.query(
|
||||||
PlayerTeam
|
PlayerTeam
|
||||||
).where(
|
).where(
|
||||||
|
@ -122,7 +118,7 @@ def consume_invite(player: Player, key: str, **_):
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return ConsumeInviteResponse(team_id=team_id).dict(by_alias=True), 200
|
return make_response({ }, 204)
|
||||||
|
|
||||||
@api_team_invite.delete("/id/<team_id>/invite/<key>")
|
@api_team_invite.delete("/id/<team_id>/invite/<key>")
|
||||||
@spec.validate(
|
@spec.validate(
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import unittest
|
|
||||||
import flask_testing
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
suite = unittest.TestLoader().discover("tests")
|
|
||||||
unittest.TextTestRunner(verbosity=1).run(suite)
|
|
|
@ -1,104 +0,0 @@
|
||||||
from datetime import timedelta
|
|
||||||
from base_test_case import BaseTestCase
|
|
||||||
|
|
||||||
from app_db import db
|
|
||||||
from models.match import Match, RawLogDetails
|
|
||||||
from models.player import Player
|
|
||||||
from models.player_match import PlayerMatch
|
|
||||||
from models.player_team import PlayerTeam
|
|
||||||
from models.team_match import TeamMatch
|
|
||||||
|
|
||||||
|
|
||||||
class TestLogstfJob(BaseTestCase):
|
|
||||||
def populate_db(self):
|
|
||||||
from app_db import db
|
|
||||||
|
|
||||||
super().populate_db()
|
|
||||||
|
|
||||||
wesker_u = Player(steam_id=76561198024482308, username="Wesker U")
|
|
||||||
wesker_u_pt = PlayerTeam(
|
|
||||||
player_id=wesker_u.steam_id,
|
|
||||||
team_id=1,
|
|
||||||
team_role=PlayerTeam.TeamRole.Player,
|
|
||||||
is_team_leader=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
db.session.add(wesker_u)
|
|
||||||
db.session.add(wesker_u_pt)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
def test_get_common_teams(self):
|
|
||||||
from jobs.fetch_logstf import get_common_teams
|
|
||||||
|
|
||||||
rows = get_common_teams([76561198248436608, 76561198024482308])
|
|
||||||
assert len(rows) == 1
|
|
||||||
assert rows == [(1, 2, 2)]
|
|
||||||
|
|
||||||
def test_transform(self):
|
|
||||||
from jobs.fetch_logstf import transform
|
|
||||||
|
|
||||||
details: RawLogDetails = {
|
|
||||||
"players": {
|
|
||||||
"[U:1:288170880]": {
|
|
||||||
"team": "Red",
|
|
||||||
"kills": 1,
|
|
||||||
"deaths": 2,
|
|
||||||
"assists": 3,
|
|
||||||
"dmg": 4,
|
|
||||||
"dt": 5,
|
|
||||||
},
|
|
||||||
"[U:1:64216580]": {
|
|
||||||
"team": "Red",
|
|
||||||
"kills": 6,
|
|
||||||
"deaths": 7,
|
|
||||||
"assists": 8,
|
|
||||||
"dmg": 9,
|
|
||||||
"dt": 10,
|
|
||||||
},
|
|
||||||
"[U:1:64216581]": {
|
|
||||||
"team": "Blue",
|
|
||||||
"kills": 6,
|
|
||||||
"deaths": 7,
|
|
||||||
"assists": 8,
|
|
||||||
"dmg": 9,
|
|
||||||
"dt": 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"info": {
|
|
||||||
"title": "I LOVE DUSTBOWL",
|
|
||||||
"map": "cp_dustbowl",
|
|
||||||
"date": 1614547200,
|
|
||||||
},
|
|
||||||
"teams": {
|
|
||||||
"Blue": {
|
|
||||||
"score": 1
|
|
||||||
},
|
|
||||||
"Red": {
|
|
||||||
"score": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"length": 3025,
|
|
||||||
}
|
|
||||||
|
|
||||||
for instance in transform(1, details):
|
|
||||||
db.session.add(instance)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
assert db.session.query(Player).count() == 2
|
|
||||||
assert db.session.query(PlayerMatch).count() == 2
|
|
||||||
assert db.session.query(TeamMatch).count() == 1
|
|
||||||
assert db.session.query(Match).count() == 1
|
|
||||||
assert db.session.query(PlayerTeam).count() == 2
|
|
||||||
player_team = db.session.query(PlayerTeam).first()
|
|
||||||
assert player_team is not None
|
|
||||||
print(player_team.playtime)
|
|
||||||
assert player_team.playtime == 3025
|
|
||||||
|
|
||||||
def test_steam3_to_steam64(self):
|
|
||||||
from jobs.fetch_logstf import steam3_to_steam64
|
|
||||||
assert steam3_to_steam64("[U:1:123456]") == 76561197960265728 + 123456
|
|
||||||
|
|
||||||
def test_steam64_to_steam3(self):
|
|
||||||
from jobs.fetch_logstf import steam64_to_steam3
|
|
||||||
assert steam64_to_steam3(76561197960265728 + 123456) == "[U:1:123456]"
|
|
|
@ -1,83 +0,0 @@
|
||||||
from collections import deque, defaultdict
|
|
||||||
|
|
||||||
from models.player_team import PlayerTeam
|
|
||||||
from models.player_team_role import PlayerTeamRole
|
|
||||||
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
|
|
||||||
class BipartiteGraph:
|
|
||||||
graph: dict[int, list[PlayerTeamRole.Role]] = {}
|
|
||||||
pair_u: dict[int, PlayerTeamRole.Role | None] = {}
|
|
||||||
pair_v: dict[PlayerTeamRole.Role, int | None] = {}
|
|
||||||
dist: dict[int | None, float] = {}
|
|
||||||
U: set[int] = set()
|
|
||||||
V: set[PlayerTeamRole.Role] = set()
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
ids_to_roles: dict[int, list[PlayerTeamRole.Role]],
|
|
||||||
required_roles: list[PlayerTeamRole.Role],
|
|
||||||
):
|
|
||||||
self.graph = self.build_graph(ids_to_roles, required_roles)
|
|
||||||
self.pair_u = {}
|
|
||||||
self.pair_v = {}
|
|
||||||
self.dist = {}
|
|
||||||
self.U = set(ids_to_roles.keys())
|
|
||||||
self.V = set(
|
|
||||||
role
|
|
||||||
for roles in ids_to_roles.values()
|
|
||||||
for role in roles
|
|
||||||
if role in required_roles
|
|
||||||
)
|
|
||||||
|
|
||||||
def build_graph(
|
|
||||||
self,
|
|
||||||
ids_to_roles: dict[int, list[PlayerTeamRole.Role]],
|
|
||||||
required_roles: list[PlayerTeamRole.Role],
|
|
||||||
):
|
|
||||||
graph = {}
|
|
||||||
for u, roles in ids_to_roles.items():
|
|
||||||
graph[u] = [v for v in roles if v in required_roles]
|
|
||||||
return graph
|
|
||||||
|
|
||||||
def bfs(self):
|
|
||||||
queue = deque()
|
|
||||||
for u in self.U:
|
|
||||||
if u not in self.pair_u:
|
|
||||||
self.dist[u] = 0
|
|
||||||
queue.append(u)
|
|
||||||
else:
|
|
||||||
self.dist[u] = float("inf")
|
|
||||||
self.dist[None] = float("inf")
|
|
||||||
|
|
||||||
while queue:
|
|
||||||
u = queue.popleft()
|
|
||||||
if self.dist[u] < self.dist[None]:
|
|
||||||
for v in self.graph[u]:
|
|
||||||
if self.dist[self.pair_v.get(v, None)] == float("inf"):
|
|
||||||
self.dist[self.pair_v.get(v, None)] = self.dist[u] + 1
|
|
||||||
queue.append(self.pair_v.get(v, None))
|
|
||||||
|
|
||||||
return self.dist[None] != float("inf")
|
|
||||||
|
|
||||||
def dfs(self, u):
|
|
||||||
if u is not None:
|
|
||||||
for v in self.graph[u]:
|
|
||||||
if self.dist[self.pair_v.get(v, None)] == self.dist[u] + 1:
|
|
||||||
if self.dfs(self.pair_v.get(v, None)):
|
|
||||||
self.pair_u[u] = v
|
|
||||||
self.pair_v[v] = u
|
|
||||||
return True
|
|
||||||
self.dist[u] = float("inf")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def hopcroft_karp(self):
|
|
||||||
matching = 0
|
|
||||||
while self.bfs():
|
|
||||||
for u in self.U:
|
|
||||||
if u not in self.pair_u:
|
|
||||||
if self.dfs(u):
|
|
||||||
matching += 1
|
|
||||||
return matching
|
|
|
@ -4,49 +4,23 @@ services:
|
||||||
# Flask service
|
# Flask service
|
||||||
backend:
|
backend:
|
||||||
container_name: backend
|
container_name: backend
|
||||||
image: backend-flask
|
|
||||||
build:
|
build:
|
||||||
context: ./backend-flask
|
context: ./backend-flask
|
||||||
volumes:
|
#image: jazzdd/alpine-flask:python3
|
||||||
- ./backend-flask:/app
|
|
||||||
networks:
|
|
||||||
- dev-network
|
|
||||||
environment:
|
|
||||||
- FLASK_DEBUG=1
|
|
||||||
- FLASK_CELERY_BROKER_URL=redis://redis:6379/0
|
|
||||||
- FLASK_CELERY_RESULT_BACKEND=redis://redis:6379/0
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
|
|
||||||
# ETL job (runs with the same source as the backend)
|
|
||||||
celery-worker:
|
|
||||||
container_name: worker
|
|
||||||
command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1
|
|
||||||
environment:
|
|
||||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
|
||||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
|
||||||
image: backend-flask
|
|
||||||
volumes:
|
|
||||||
- ./backend-flask:/app
|
|
||||||
networks:
|
|
||||||
- dev-network
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
|
|
||||||
# message broker
|
|
||||||
redis:
|
|
||||||
image: redis:alpine
|
|
||||||
container_name: redis
|
|
||||||
networks:
|
|
||||||
- dev-network
|
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- ":5000"
|
||||||
|
volumes:
|
||||||
|
- ./backend-flask:/app
|
||||||
|
networks:
|
||||||
|
- dev-network
|
||||||
|
|
||||||
# Vue + Vite service
|
# Vue + Vite service
|
||||||
frontend:
|
frontend:
|
||||||
container_name: frontend
|
container_name: frontend
|
||||||
build:
|
build:
|
||||||
context: ./availabili.tf
|
context: ./availabili.tf
|
||||||
|
ports:
|
||||||
|
- ":5173"
|
||||||
environment:
|
environment:
|
||||||
VITE_API_URL: http://localhost:8000 # API endpoint
|
VITE_API_URL: http://localhost:8000 # API endpoint
|
||||||
volumes:
|
volumes:
|
||||||
|
|
Loading…
Reference in New Issue