Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Team logos #529

Merged
merged 14 commits into from
Feb 28, 2024
25 changes: 25 additions & 0 deletions backend/alembic/versions/19ddf67a4eeb_adding_teams_logo_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Adding teams.logo_paths

Revision ID: 19ddf67a4eeb
Revises: c08e04993dd7
Create Date: 2024-02-24 12:14:16.037628

"""

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str | None = "19ddf67a4eeb"
down_revision: str | None = "c08e04993dd7"
branch_labels: str | None = None
depends_on: str | None = None


def upgrade() -> None:
op.add_column("teams", sa.Column("logo_path", sa.String(), nullable=True))

Check warning on line 21 in backend/alembic/versions/19ddf67a4eeb_adding_teams_logo_paths.py

View check run for this annotation

Codecov / codecov/patch

backend/alembic/versions/19ddf67a4eeb_adding_teams_logo_paths.py#L21

Added line #L21 was not covered by tests


def downgrade() -> None:
op.drop_column("teams", "logo_path")

Check warning on line 25 in backend/alembic/versions/19ddf67a4eeb_adding_teams_logo_paths.py

View check run for this annotation

Codecov / codecov/patch

backend/alembic/versions/19ddf67a4eeb_adding_teams_logo_paths.py#L25

Added line #L25 was not covered by tests
14 changes: 14 additions & 0 deletions backend/bracket/logic/teams.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import aiofiles.os

from bracket.sql.teams import get_team_by_id
from bracket.utils.id_types import TeamId, TournamentId


async def get_team_logo_path(tournament_id: TournamentId, team_id: TeamId) -> str | None:
team = await get_team_by_id(team_id, tournament_id)
logo_path = (
f"static/team-logos/{team.logo_path}"
if team is not None and team.logo_path is not None
else None # pylint: disable=line-too-long
)
return logo_path if logo_path is not None and await aiofiles.os.path.exists(logo_path) else None
2 changes: 1 addition & 1 deletion backend/bracket/logic/tournaments.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

async def get_tournament_logo_path(tournament_id: TournamentId) -> str | None:
tournament = await sql_get_tournament(tournament_id)
logo_path = f"static/{tournament.logo_path}" if tournament.logo_path else None
logo_path = f"static/tournament-logos/{tournament.logo_path}" if tournament.logo_path else None
return logo_path if logo_path is not None and await aiofiles.os.path.exists(logo_path) else None


Expand Down
2 changes: 2 additions & 0 deletions backend/bracket/models/db/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Team(BaseModelORM):
wins: int = 0
draws: int = 0
losses: int = 0
logo_path: str | None = None


class TeamWithPlayers(BaseModel):
Expand All @@ -37,6 +38,7 @@ class TeamWithPlayers(BaseModel):
draws: int = 0
losses: int = 0
name: str
logo_path: str | None = None

@property
def player_ids(self) -> list[PlayerId]:
Expand Down
47 changes: 46 additions & 1 deletion backend/bracket/routes/teams.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
from fastapi import APIRouter, Depends
import os
from uuid import uuid4

import aiofiles
import aiofiles.os
from fastapi import APIRouter, Depends, UploadFile
from heliclockter import datetime_utc

from bracket.database import database
from bracket.logic.ranking.elo import recalculate_ranking_for_tournament_id
from bracket.logic.subscriptions import check_requirement
from bracket.logic.teams import get_team_logo_path
from bracket.models.db.team import FullTeamWithPlayers, Team, TeamBody, TeamMultiBody, TeamToInsert
from bracket.models.db.user import UserPublic
from bracket.routes.auth import (
Expand All @@ -28,6 +34,7 @@
from bracket.utils.db import fetch_one_parsed
from bracket.utils.errors import ForeignKey, check_foreign_key_violation
from bracket.utils.id_types import PlayerId, TeamId, TournamentId
from bracket.utils.logging import logger
from bracket.utils.pagination import PaginationTeams
from bracket.utils.types import assert_some

Expand Down Expand Up @@ -102,6 +109,44 @@
)


@router.post("/tournaments/{tournament_id}/teams/{team_id}/logo", response_model=SingleTeamResponse)
async def update_team_logo(
tournament_id: TournamentId,
file: UploadFile | None = None,
_: UserPublic = Depends(user_authenticated_for_tournament),
team: Team = Depends(team_dependency),
) -> SingleTeamResponse:
team_id = assert_some(team.id)
old_logo_path = await get_team_logo_path(tournament_id, team_id)
filename: str | None = None
new_logo_path: str | None = None

if file:
assert file.filename is not None
extension = os.path.splitext(file.filename)[1]
assert extension in (".png", ".jpg", ".jpeg")

filename = f"{uuid4()}{extension}"
new_logo_path = f"static/team-logos/{filename}" if file is not None else None

if new_logo_path:
await aiofiles.os.makedirs("static/team-logos", exist_ok=True)
async with aiofiles.open(new_logo_path, "wb") as f:
await f.write(await file.read())

if old_logo_path is not None and old_logo_path != new_logo_path:
try:
await aiofiles.os.remove(old_logo_path)
except Exception as exc:
logger.error(f"Could not remove logo that should still exist: {old_logo_path}\n{exc}")

Check warning on line 141 in backend/bracket/routes/teams.py

View check run for this annotation

Codecov / codecov/patch

backend/bracket/routes/teams.py#L138-L141

Added lines #L138 - L141 were not covered by tests

await database.execute(
teams.update().where(teams.c.id == team_id),
values={"logo_path": filename},
)
return SingleTeamResponse(data=assert_some(await get_team_by_id(team_id, tournament_id)))


@router.delete("/tournaments/{tournament_id}/teams/{team_id}", response_model=SuccessResponse)
async def delete_team(
tournament_id: TournamentId,
Expand Down
3 changes: 2 additions & 1 deletion backend/bracket/routes/tournaments.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,10 @@ async def upload_logo(
assert extension in (".png", ".jpg", ".jpeg")

filename = f"{uuid4()}{extension}"
new_logo_path = f"static/{filename}" if file is not None else None
new_logo_path = f"static/tournament-logos/{filename}" if file is not None else None

if new_logo_path:
await aiofiles.os.makedirs("static/tournament-logos", exist_ok=True)
async with aiofiles.open(new_logo_path, "wb") as f:
await f.write(await file.read())

Expand Down
1 change: 1 addition & 0 deletions backend/bracket/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
Column("wins", Integer, nullable=False, server_default="0"),
Column("draws", Integer, nullable=False, server_default="0"),
Column("losses", Integer, nullable=False, server_default="0"),
Column("logo_path", String, nullable=True),
)

players = Table(
Expand Down
2 changes: 2 additions & 0 deletions backend/tests/integration_tests/api/matches_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ async def test_upcoming_matches_endpoint(
"wins": 0,
"draws": 0,
"losses": 0,
"logo_path": None,
},
"team2": {
"id": team2_inserted.id,
Expand Down Expand Up @@ -309,6 +310,7 @@ async def test_upcoming_matches_endpoint(
"wins": 0,
"draws": 0,
"losses": 0,
"logo_path": None,
},
"elo_diff": "200",
"swiss_diff": "0",
Expand Down
42 changes: 42 additions & 0 deletions backend/tests/integration_tests/api/teams_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import aiofiles.os
import aiohttp

from bracket.database import database
from bracket.models.db.team import Team
from bracket.schema import teams
Expand Down Expand Up @@ -30,6 +33,7 @@ async def test_teams_endpoint(
"wins": 0,
"draws": 0,
"losses": 0,
"logo_path": None,
}
],
"count": 1,
Expand Down Expand Up @@ -102,3 +106,41 @@ async def test_update_team_invalid_players(
HTTPMethod.PUT, f"teams/{team_inserted.id}", auth_context, None, body
)
assert response == {"detail": "Could not find Player(s) with ID {-1}"}


async def test_team_upload_and_remove_logo(
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
) -> None:
test_file_path = "tests/integration_tests/assets/test_logo.png"
data = aiohttp.FormData()
data.add_field(
"file",
open(test_file_path, "rb"), # pylint: disable=consider-using-with
filename="test_logo.png",
content_type="image/png",
)

async with inserted_team(
DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id})
) as team_inserted:
response = await send_tournament_request(
method=HTTPMethod.POST,
endpoint=f"teams/{team_inserted.id}/logo",
auth_context=auth_context,
body=data,
)

assert response["data"]["logo_path"], f"Response: {response}"
assert await aiofiles.os.path.exists(f"static/team-logos/{response['data']['logo_path']}")

response = await send_tournament_request(
method=HTTPMethod.POST,
endpoint="logo",
auth_context=auth_context,
body=aiohttp.FormData(),
)

assert response["data"]["logo_path"] is None, f"Response: {response}"
assert not await aiofiles.os.path.exists(
f"static/team-logos/{response['data']['logo_path']}"
)
7 changes: 5 additions & 2 deletions backend/tests/integration_tests/api/tournaments_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import aiofiles
import aiofiles.os
import aiohttp

from bracket.database import database
Expand Down Expand Up @@ -149,11 +150,13 @@ async def test_tournament_upload_and_remove_logo(
)

assert response["data"]["logo_path"], f"Response: {response}"
assert await aiofiles.os.path.exists(f"static/{response['data']['logo_path']}")
assert await aiofiles.os.path.exists(f"static/tournament-logos/{response['data']['logo_path']}")

response = await send_tournament_request(
method=HTTPMethod.POST, endpoint="logo", auth_context=auth_context, body=aiohttp.FormData()
)

assert response["data"]["logo_path"] is None, f"Response: {response}"
assert not await aiofiles.os.path.exists(f"static/{response['data']['logo_path']}")
assert not await aiofiles.os.path.exists(
f"static/tournament-logos/{response['data']['logo_path']}"
)
4 changes: 3 additions & 1 deletion frontend/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"invalid_password_validation": "Invalid password",
"iterations_input_label": "Iterations",
"login_success_title": "Login successful",
"logo_settings_title": "Logo Settings",
"logout_success_title": "Logout successful",
"logout_title": "Logout",
"lowercase_required": "Includes lowercase letter",
Expand Down Expand Up @@ -212,7 +213,8 @@
"tournament_title": "tournament",
"tournaments_title": "tournaments",
"upcoming_matches_empty_table_info": "upcoming matches",
"upload_placeholder": "Drop a file here to upload as tournament logo.",
"upload_placeholder_tournament": "Drop a file here to upload as tournament logo.",
"upload_placeholder_team": "Drop a file here to upload as team logo.",
"uppercase_required": "Includes uppercase letter",
"user_settings_spotlight_description": "Change name, email, password etc.",
"user_settings_title": "User Settings",
Expand Down
8 changes: 5 additions & 3 deletions frontend/public/locales/nl/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@
"invalid_password_validation": "Ongeldig wachtwoord",
"iterations_input_label": "Iteraties",
"login_success_title": "Login succesvol",
"logout_success_title": "Uitloggen succesvol",
"logo_settings_title": "Logo-instellingen",
"logout_success_title": "Uitloggen succesvol",
"logout_title": "Uitloggen",
"lowercase_required": "Heeft een kleine letter",
"margin_minutes_choose_title": "Vul de marge tussen wedstrijden in",
Expand Down Expand Up @@ -212,8 +213,9 @@
"tournament_title": "toernooi",
"tournaments_title": "toernooien",
"upcoming_matches_empty_table_info": "komende wedstrijden",
"upload_placeholder": "Plaats hier een bestand om te uploaden als toernooilogo.",
"uppercase_required": "Heeft een hoofdletter",
"upload_placeholder_tournament": "Plaats hier een bestand om te uploaden als toernooilogo.",
"upload_placeholder_team": "Drop hier een bestand om te uploaden als teamlogo.",
"uppercase_required": "Heeft een hoofdletter",
"user_settings_spotlight_description": "Wijzig naam, e-mailadres, wachtwoord etc.",
"user_settings_title": "Gebruikersinstellingen",
"user_title": "Gebruiker",
Expand Down
4 changes: 3 additions & 1 deletion frontend/public/locales/zh/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"invalid_password_validation": "无效的密码",
"iterations_input_label": "迭代",
"login_success_title": "登录成功",
"logo_settings_title": "徽标设置",
"logout_success_title": "登出成功",
"logout_title": "登出",
"lowercase_required": "包含小写字母",
Expand Down Expand Up @@ -212,7 +213,8 @@
"tournament_title": "锦标赛",
"tournaments_title": "锦标赛",
"upcoming_matches_empty_table_info": "即将进行的比赛",
"upload_placeholder": "在此处放置文件以上传为锦标赛标志。",
"upload_placeholder_tournament": "在此处放置文件以上传为锦标赛标志。",
"upload_placeholder_team": "在此上传文件作为队徽。",
"uppercase_required": "包含大写字母",
"user_settings_spotlight_description": "更改名称、电子邮件、密码等",
"user_settings_title": "用户设置",
Expand Down
31 changes: 28 additions & 3 deletions frontend/src/components/modals/team_update_modal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, Checkbox, Modal, MultiSelect, TextInput } from '@mantine/core';
import { Button, Center, Checkbox, Fieldset, Modal, MultiSelect, TextInput, Image } from '@mantine/core';
import { useForm } from '@mantine/form';
import { BiEditAlt } from '@react-icons/all-files/bi/BiEditAlt';
import { useTranslation } from 'next-i18next';
Expand All @@ -7,8 +7,14 @@ import { SWRResponse } from 'swr';

import { Player } from '../../interfaces/player';
import { TeamInterface } from '../../interfaces/team';
import { getPlayers, requestSucceeded } from '../../services/adapter';
import { getBaseApiUrl, getPlayers, removeTeamLogo, requestSucceeded } from '../../services/adapter';
import { updateTeam } from '../../services/team';
import { DropzoneButton } from '../utils/file_upload';

function TeamLogo({ team }: { team: TeamInterface | null }) {
if (team == null || team.logo_path == null) return null;
return <Image radius="md" src={`${getBaseApiUrl()}/static/${team.logo_path}`} />;
}

export default function TeamUpdateModal({
tournament_id,
Expand Down Expand Up @@ -73,12 +79,31 @@ export default function TeamUpdateModal({
placeholder={t('team_member_select_placeholder')}
maxDropdownHeight={160}
searchable
mb="12rem"
mt={12}
limit={25}
{...form.getInputProps('player_ids')}
/>

<Fieldset legend={t('logo_settings_title')} mt={12} radius="md">
<DropzoneButton tournamentId={tournament_id} teamId={team.id} swrResponse={swrTeamsResponse} variant="team" />
<Center my="lg">
<div style={{ width: '50%' }}>
<TeamLogo team={team} />
</div>
</Center>
<Button
variant="outline"
color="red"
fullWidth
onClick={async () => {
await removeTeamLogo(tournament_id, team.id);
await swrTeamsResponse.mutate();
}}
>
{t('remove_logo')}
</Button>
</Fieldset>

<Button fullWidth style={{ marginTop: 10 }} color="green" type="submit">
{t('save_button')}
</Button>
Expand Down
Loading