Skip to content

Commit

Permalink
Fix bugs in presentation dashboard (#999)
Browse files Browse the repository at this point in the history
  • Loading branch information
evroon authored Nov 8, 2024
1 parent 7e8ee23 commit ed659ff
Show file tree
Hide file tree
Showing 20 changed files with 171 additions and 132 deletions.
15 changes: 8 additions & 7 deletions backend/bracket/logic/ranking/elo.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@ def set_statistics_for_stage_item_input(
match.stage_item_input1_score, match.stage_item_input2_score
)

# Set default for SWISS teams
if stage_item.type is StageType.SWISS and stage_item_input_id not in stats:
stats[stage_item_input_id].points = START_ELO

if has_won:
stats[stage_item_input_id].wins += 1
swiss_score_diff = ranking.win_points
Expand Down Expand Up @@ -73,7 +69,12 @@ def determine_ranking_for_stage_item(
stage_item: StageItemWithRounds,
ranking: Ranking,
) -> defaultdict[StageItemInputId, TeamStatistics]:
team_x_stats: defaultdict[StageItemInputId, TeamStatistics] = defaultdict(TeamStatistics)
input_x_stats: defaultdict[StageItemInputId, TeamStatistics] = defaultdict(TeamStatistics)

if stage_item.type is StageType.SWISS:
for input_ in stage_item.inputs:
input_x_stats[input_.id].points = START_ELO

matches = [
match
for round_ in stage_item.rounds
Expand All @@ -85,14 +86,14 @@ def determine_ranking_for_stage_item(
for team_index, stage_item_input in enumerate(match.stage_item_inputs):
set_statistics_for_stage_item_input(
team_index,
team_x_stats,
input_x_stats,
match,
stage_item_input.id,
ranking,
stage_item,
)

return team_x_stats
return input_x_stats


def determine_team_ranking_for_stage_item(
Expand Down
3 changes: 3 additions & 0 deletions backend/bracket/logic/scheduling/builder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fastapi import HTTPException

from bracket.logic.ranking.elo import recalculate_ranking_for_stage_item_id
from bracket.logic.scheduling.elimination import (
build_single_elimination_stage_item,
get_number_of_rounds_to_create_single_elimination,
Expand Down Expand Up @@ -71,6 +72,8 @@ async def build_matches_for_stage_item(stage_item: StageItem, tournament_id: Tou
400, f"Cannot automatically create matches for stage type {stage_item.type}"
)

await recalculate_ranking_for_stage_item_id(tournament_id, stage_item.id)


def determine_available_inputs(
teams: list[FullTeamWithPlayers],
Expand Down
5 changes: 5 additions & 0 deletions backend/bracket/routes/rankings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fastapi import APIRouter, Depends

from bracket.logic.ranking.elo import recalculate_ranking_for_stage_item_id
from bracket.logic.subscriptions import check_requirement
from bracket.models.db.ranking import RankingBody, RankingCreateBody
from bracket.models.db.user import UserPublic
Expand All @@ -17,6 +18,7 @@
sql_delete_ranking,
sql_update_ranking,
)
from bracket.sql.stage_item_inputs import get_stage_item_input_ids_by_ranking_id
from bracket.utils.id_types import RankingId, TournamentId

router = APIRouter()
Expand All @@ -42,6 +44,9 @@ async def update_ranking_by_id(
ranking_id=ranking_id,
ranking_body=ranking_body,
)
stage_item_ids = await get_stage_item_input_ids_by_ranking_id(ranking_id)
for stage_item_id in stage_item_ids:
await recalculate_ranking_for_stage_item_id(tournament_id, stage_item_id)
return SuccessResponse()


Expand Down
2 changes: 2 additions & 0 deletions backend/bracket/routes/stage_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
get_draft_round,
schedule_all_matches_for_swiss_round,
)
from bracket.logic.ranking.elo import recalculate_ranking_for_stage_item_id
from bracket.logic.scheduling.builder import (
build_matches_for_stage_item,
)
Expand Down Expand Up @@ -109,6 +110,7 @@ async def update_stage_item(
query=query,
values={"stage_item_id": stage_item_id, "name": stage_item_body.name},
)
await recalculate_ranking_for_stage_item_id(tournament_id, stage_item_id)
return SuccessResponse()


Expand Down
16 changes: 15 additions & 1 deletion backend/bracket/sql/stage_item_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
StageItemInputFinal,
)
from bracket.sql.teams import get_team_by_id
from bracket.utils.id_types import StageItemId, StageItemInputId, TeamId, TournamentId
from bracket.utils.id_types import RankingId, StageItemId, StageItemInputId, TeamId, TournamentId


async def get_stage_item_input_by_id(
Expand All @@ -37,6 +37,20 @@ async def get_stage_item_input_by_id(
return TypeAdapter(StageItemInput).validate_python(result)


async def get_stage_item_input_ids_by_ranking_id(ranking_id: RankingId) -> list[StageItemId]:
query = """
SELECT id
FROM stage_items
WHERE ranking_id = :ranking_id
"""
results = await database.fetch_all(
query=query,
values={"ranking_id": ranking_id},
)

return [StageItemId(result["id"]) for result in results]


async def sql_set_team_id_for_stage_item_input(
tournament_id: TournamentId, stage_item_input_id: StageItemInputId, team_id: TeamId | None
) -> None:
Expand Down
7 changes: 5 additions & 2 deletions backend/tests/unit_tests/ranking_calculation_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ def test_determine_ranking_for_stage_item_swiss_no_matches() -> None:
],
inputs=[stage_item_input1, stage_item_input2],
type_name="Swiss",
team_count=4,
team_count=2,
ranking_id=None,
id=StageItemId(-1),
stage_id=StageId(-1),
Expand All @@ -267,4 +267,7 @@ def test_determine_ranking_for_stage_item_swiss_no_matches() -> None:
),
)

assert not ranking
assert ranking == {
-2: TeamStatistics(wins=0, draws=0, losses=0, points=Decimal("1200")),
-1: TeamStatistics(wins=0, draws=0, losses=0, points=Decimal("1200")),
}
25 changes: 16 additions & 9 deletions frontend/src/components/brackets/courts_large.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,39 @@ export function CourtBadge({ name, color }: { name: string; color: MantineColor
);
}

function getRoundsGridCols(match: MatchInterface | null) {
if (match == null) {
return null;
}
return <MatchLarge key={match.id} match={match} />;
}

export default function CourtsLarge({
court,
activeMatch,
nextMatch,
stageItemsLookup,
}: {
court: Court;
activeMatch: MatchInterface | null;
nextMatch: MatchInterface | null;
stageItemsLookup: any;
}) {
return (
<Grid align="center" style={{ marginTop: '1rem' }} gutter="2rem">
<Grid.Col span={{ sm: 2 }}>
<CourtBadge name={court.name} color="indigo" />
</Grid.Col>
<Grid.Col span={{ sm: 5 }}>
<Grid>{getRoundsGridCols(activeMatch)}</Grid>
<Grid>
{activeMatch != null && (
<MatchLarge
key={activeMatch.id}
match={activeMatch}
stageItemsLookup={stageItemsLookup}
/>
)}
</Grid>
</Grid.Col>
<Grid.Col span={{ sm: 5 }}>
<Grid>{getRoundsGridCols(nextMatch)}</Grid>
<Grid>
{nextMatch != null && (
<MatchLarge key={nextMatch.id} match={nextMatch} stageItemsLookup={stageItemsLookup} />
)}
</Grid>
</Grid.Col>
</Grid>
);
Expand Down
21 changes: 14 additions & 7 deletions frontend/src/components/brackets/match_large.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import { Card, Center, Grid, Text } from '@mantine/core';
import assert from 'assert';
import React from 'react';

import { MatchInterface } from '../../interfaces/match';
import { formatStageItemInput } from '../../interfaces/stage_item_input';
import { Time } from '../utils/datetime';

export default function MatchLarge({ match }: { match: MatchInterface }) {
assert(match.stage_item_input1?.team != null);
assert(match.stage_item_input2?.team != null);

export default function MatchLarge({
match,
stageItemsLookup,
}: {
match: MatchInterface;
stageItemsLookup: any;
}) {
const bracket = (
<div>
<Card padding="md" shadow="sm" radius="lg" withBorder>
<Grid align="center">
<Grid.Col span={{ sm: 9 }}>
<Text lineClamp={1}>{match.stage_item_input1.team.name}</Text>
<Text lineClamp={1}>{match.stage_item_input2.team.name}</Text>
<Text lineClamp={1} inherit>
{formatStageItemInput(match.stage_item_input1, stageItemsLookup) || <i>N/A</i>}
</Text>
<Text lineClamp={1} inherit>
{formatStageItemInput(match.stage_item_input2, stageItemsLookup) || <i>N/A</i>}
</Text>
</Grid.Col>
<Grid.Col span={{ sm: 3 }}>
<Center>
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/info/player_list.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Text } from '@mantine/core';

import { TeamInterface } from '../../interfaces/team';

export default function PlayerList({ team }: { team: TeamInterface }) {
return <span>{team.name}</span>;
return <Text inherit>{team.name}</Text>;
}
15 changes: 12 additions & 3 deletions frontend/src/components/info/player_score.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@ interface ScoreProps {
min_score: number;
max_score: number;
decimals: number;
fontSizeInPixels: number;
}

export function PlayerScore({ score, min_score, max_score, decimals }: ScoreProps) {
export function PlayerScore({
score,
min_score,
max_score,
decimals,
fontSizeInPixels,
}: ScoreProps) {
const percentageScale = 100.0 / (max_score - min_score);
const empty = max_score - min_score === 0;

return (
<Progress.Root size={20}>
<Progress.Root size={fontSizeInPixels * 1.5}>
<Progress.Section value={empty ? 50 : percentageScale * (score - min_score)} color="indigo">
<Progress.Label>{Number(score).toFixed(decimals)}</Progress.Label>
<Progress.Label style={{ fontSize: fontSizeInPixels }}>
{Number(score).toFixed(decimals)}
</Progress.Label>
</Progress.Section>
</Progress.Root>
);
Expand Down
17 changes: 12 additions & 5 deletions frontend/src/components/info/player_statistics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,30 @@ interface PlayerStatisticsProps {
wins: number;
draws: number;
losses: number;
fontSizeInPixels: number;
}

export function WinDistribution({ wins, draws, losses }: PlayerStatisticsProps) {
export function WinDistribution({ wins, draws, losses, fontSizeInPixels }: PlayerStatisticsProps) {
const percentageScale = 100.0 / (wins + draws + losses);
const empty = wins + draws + losses === 0;

return (
<>
<Progress.Root size={20}>
<Progress.Root size={fontSizeInPixels * 1.5}>
<Progress.Section value={empty ? 33.3 : percentageScale * wins} color="teal">
<Progress.Label>{`${wins.toFixed(0)}`}</Progress.Label>
<Progress.Label style={{ fontSize: fontSizeInPixels }}>
{`${wins.toFixed(0)}`}
</Progress.Label>
</Progress.Section>
<Progress.Section value={empty ? 33.3 : percentageScale * draws} color="orange">
<Progress.Label>{`${draws.toFixed(0)}`}</Progress.Label>
<Progress.Label style={{ fontSize: fontSizeInPixels }}>
{`${draws.toFixed(0)}`}
</Progress.Label>
</Progress.Section>
<Progress.Section value={empty ? 33.3 : percentageScale * losses} color="red">
<Progress.Label>{`${losses.toFixed(0)}`}</Progress.Label>
<Progress.Label style={{ fontSize: fontSizeInPixels }}>
{`${losses.toFixed(0)}`}
</Progress.Label>
</Progress.Section>
</Progress.Root>
</>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/no_content/empty_table_info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function NoContent({
<Container mt="md">
<div className={classes.label}>{icon || <HiMiniWrenchScrewdriver />}</div>
<Title className={classes.title}>{title}</Title>
<Text size="lg" ta="center" className={classes.description}>
<Text size="lg" ta="center" className={classes.description} inherit>
{description}
</Text>
</Container>
Expand Down
Loading

0 comments on commit ed659ff

Please sign in to comment.