Skip to content

Commit 880d212

Browse files
authored
Use uuids for logos (#488)
Otherwise uploads can replace current files with the same name. Also fixes and adds more tests for logo uploads.
1 parent b586bb6 commit 880d212

File tree

8 files changed

+51
-31
lines changed

8 files changed

+51
-31
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ coverage.xml
1515
coverage.json
1616

1717
backend/yarn.lock
18+
backend/static

backend/bracket/routes/tournaments.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import os
2+
from uuid import uuid4
3+
14
import aiofiles.os
25
import asyncpg # type: ignore[import-untyped]
36
from fastapi import APIRouter, Depends, HTTPException, UploadFile
@@ -9,7 +12,6 @@
912
from bracket.logic.subscriptions import check_requirement
1013
from bracket.logic.tournaments import get_tournament_logo_path
1114
from bracket.models.db.tournament import (
12-
Tournament,
1315
TournamentBody,
1416
TournamentToInsert,
1517
TournamentUpdateBody,
@@ -25,11 +27,11 @@
2527
from bracket.schema import tournaments
2628
from bracket.sql.tournaments import (
2729
sql_delete_tournament,
30+
sql_get_tournament,
2831
sql_get_tournament_by_endpoint_name,
2932
sql_get_tournaments,
3033
)
3134
from bracket.sql.users import get_user_access_to_club, get_which_clubs_has_user_access_to
32-
from bracket.utils.db import fetch_one_parsed_certain
3335
from bracket.utils.errors import (
3436
ForeignKey,
3537
UniqueIndex,
@@ -51,9 +53,7 @@
5153
async def get_tournament(
5254
tournament_id: int, user: UserPublic | None = Depends(user_authenticated_or_public_dashboard)
5355
) -> TournamentResponse:
54-
tournament = await fetch_one_parsed_certain(
55-
database, Tournament, tournaments.select().where(tournaments.c.id == tournament_id)
56-
)
56+
tournament = await sql_get_tournament(tournament_id)
5757
if user is None and not tournament.dashboard_public:
5858
raise unauthorized_exception
5959

@@ -154,13 +154,22 @@ async def upload_logo(
154154
tournament_id: int,
155155
file: UploadFile | None = None,
156156
_: UserPublic = Depends(user_authenticated_for_tournament),
157-
) -> SuccessResponse:
157+
) -> TournamentResponse:
158158
old_logo_path = await get_tournament_logo_path(tournament_id)
159-
new_logo_path = f"static/{file.filename}" if file is not None else None
159+
filename: str | None = None
160+
new_logo_path: str | None = None
160161

161-
if file and new_logo_path:
162-
async with aiofiles.open(new_logo_path, "wb") as f:
163-
await f.write(await file.read())
162+
if file:
163+
assert file.filename is not None
164+
extension = os.path.splitext(file.filename)[1]
165+
assert extension in (".png", ".jpg", ".jpeg")
166+
167+
filename = f"{uuid4()}{extension}"
168+
new_logo_path = f"static/{filename}" if file is not None else None
169+
170+
if new_logo_path:
171+
async with aiofiles.open(new_logo_path, "wb") as f:
172+
await f.write(await file.read())
164173

165174
if old_logo_path is not None and old_logo_path != new_logo_path:
166175
try:
@@ -170,6 +179,6 @@ async def upload_logo(
170179

171180
await database.execute(
172181
tournaments.update().where(tournaments.c.id == tournament_id),
173-
values={"logo_path": file.filename if file else None},
182+
values={"logo_path": filename},
174183
)
175-
return SuccessResponse()
184+
return TournamentResponse(data=await sql_get_tournament(tournament_id))

backend/tests/integration_tests/api/shared.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ async def down(self) -> None:
7070
async def send_request(
7171
method: HTTPMethod,
7272
endpoint: str,
73-
body: JsonDict | AsyncIterator[bytes] | None = None,
73+
body: JsonDict | AsyncIterator[bytes] | aiohttp.FormData | None = None,
7474
json: JsonDict | None = None,
7575
headers: JsonDict = {},
7676
) -> JsonDict:
@@ -111,7 +111,7 @@ async def send_tournament_request(
111111
method: HTTPMethod,
112112
endpoint: str,
113113
auth_context: AuthContext,
114-
body: JsonDict | AsyncIterator[bytes] | None = None,
114+
body: JsonDict | AsyncIterator[bytes] | aiohttp.FormData | None = None,
115115
json: JsonDict | None = None,
116116
) -> JsonDict:
117117
return await send_request(

backend/tests/integration_tests/api/tournaments_test.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
from collections.abc import AsyncIterator
2-
31
import aiofiles
2+
import aiohttp
43

54
from bracket.database import database
65
from bracket.models.db.tournament import Tournament
@@ -130,22 +129,31 @@ async def test_delete_tournament(
130129
await sql_delete_tournament(assert_some(tournament_inserted.id))
131130

132131

133-
async def file_sender(file_name: str) -> AsyncIterator[bytes]:
134-
async with aiofiles.open(file_name, "rb") as f:
135-
chunk = await f.read(64 * 1024)
136-
while chunk:
137-
yield chunk
138-
chunk = await f.read(64 * 1024)
139-
140-
141-
async def test_tournament_upload_logo(
132+
async def test_tournament_upload_and_remove_logo(
142133
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
143134
) -> None:
135+
test_file_path = "tests/integration_tests/assets/test_logo.png"
136+
data = aiohttp.FormData()
137+
data.add_field(
138+
"file",
139+
open(test_file_path, "rb"), # pylint: disable=consider-using-with
140+
filename="test_logo.png",
141+
content_type="image/png",
142+
)
143+
144144
response = await send_tournament_request(
145145
method=HTTPMethod.POST,
146146
endpoint="logo",
147147
auth_context=auth_context,
148-
body=file_sender(file_name="tests/integration_tests/assets/test_logo.png"),
148+
body=data,
149+
)
150+
151+
assert response["data"]["logo_path"], f"Response: {response}"
152+
assert await aiofiles.os.path.exists(f"static/{response['data']['logo_path']}")
153+
154+
response = await send_tournament_request(
155+
method=HTTPMethod.POST, endpoint="logo", auth_context=auth_context, body=aiohttp.FormData()
149156
)
150157

151-
assert response.get("success"), f"Response: {response}"
158+
assert response["data"]["logo_path"] is None, f"Response: {response}"
159+
assert not await aiofiles.os.path.exists(f"static/{response['data']['logo_path']}")

frontend/public/locales/en/common.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
"drop_match_alert_title": "Drop a match here",
7676
"dropzone_accept_text": "Drop files here",
7777
"dropzone_idle_text": "Upload Logo",
78-
"dropzone_reject_text": "Image must be less than 10MB",
78+
"dropzone_reject_text": "Image must be less than 5MB.",
7979
"duration_minutes_choose_title": "Please choose a duration of the matches",
8080
"edit_club_button": "Edit Club",
8181
"edit_details_tab_title": "Edit details",

frontend/public/locales/nl/common.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
"drop_match_alert_title": "Plaats hier een wedstrijd",
7676
"dropzone_accept_text": "Upload hier bestanden",
7777
"dropzone_idle_text": "Logo uploaden",
78-
"dropzone_reject_text": "De afbeelding moet kleiner zijn dan 10 MB",
78+
"dropzone_reject_text": "De afbeelding moet kleiner zijn dan 5 MB.",
7979
"duration_minutes_choose_title": "Vul de duur van de wedstrijden in",
8080
"edit_club_button": "Club bewerken",
8181
"edit_details_tab_title": "Details bewerken",

frontend/public/locales/zh-CN/common.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"drop_match_alert_title": "在这里放下一场比赛",
7272
"dropzone_accept_text": "在此处放置文件",
7373
"dropzone_idle_text": "上传标志",
74-
"dropzone_reject_text": "图片必须小于 10MB",
74+
"dropzone_reject_text": "图片必须小于 5MB",
7575
"duration_minutes_choose_title": "请选择比赛的持续时间",
7676
"edit_club_button": "编辑俱乐部",
7777
"edit_details_tab_title": "编辑详情",

frontend/src/components/utils/file_upload.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function DropzoneButton({
3232
// className={classes.dropzone}
3333
radius="md"
3434
accept={[MIME_TYPES.png, MIME_TYPES.jpeg]}
35-
maxSize={30 * 1024 ** 2}
35+
maxSize={5 * 1024 ** 2}
3636
>
3737
<div style={{ pointerEvents: 'none' }}>
3838
<Group justify="center">
@@ -54,6 +54,8 @@ export function DropzoneButton({
5454
</Text>
5555
<Text ta="center" size="sm" mt="xs" c="dimmed">
5656
{t('upload_placeholder')}
57+
<br />
58+
{t('dropzone_reject_text')}
5759
</Text>
5860
</div>
5961
</Dropzone>

0 commit comments

Comments
 (0)