mirror of
https://github.com/evroon/bracket.git
synced 2026-08-07 05:33:12 -04:00
Make dashboard endpoint unique (#454)
This commit is contained in:
1 parent
8ff8831e90
commit
14728a62bb
8 files changed
+104
-7
No files matched your search
@@ -0,0 +1,29 @@
|
||||
"""tournaments.dashboard_endpoint unique
|
||||
|
||||
Revision ID: c08e04993dd7
|
||||
Revises: 39ec08a054af
|
||||
Create Date: 2024-02-09 18:44:32.138133
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str | None = "c08e04993dd7"
|
||||
down_revision: str | None = "39ec08a054af"
|
||||
branch_labels: str | None = None
|
||||
depends_on: str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_index(
|
||||
op.f("ix_tournaments_dashboard_endpoint"),
|
||||
"tournaments",
|
||||
["dashboard_endpoint"],
|
||||
unique=True,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(op.f("ix_tournaments_dashboard_endpoint"), table_name="tournaments")
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncpg # type: ignore[import-untyped]
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile
|
||||
from heliclockter import datetime_utc
|
||||
from starlette import status
|
||||
@@ -27,6 +28,7 @@ from bracket.sql.tournaments import (
|
||||
)
|
||||
from bracket.sql.users import get_user_access_to_club, get_which_clubs_has_user_access_to
|
||||
from bracket.utils.db import fetch_one_parsed_certain
|
||||
from bracket.utils.errors import check_constraint_and_raise_http_exception
|
||||
from bracket.utils.types import assert_some
|
||||
|
||||
router = APIRouter()
|
||||
@@ -79,10 +81,14 @@ async def update_tournament_by_id(
|
||||
tournament_body: TournamentUpdateBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
) -> SuccessResponse:
|
||||
await database.execute(
|
||||
query=tournaments.update().where(tournaments.c.id == tournament_id),
|
||||
values=tournament_body.model_dump(),
|
||||
)
|
||||
try:
|
||||
await database.execute(
|
||||
query=tournaments.update().where(tournaments.c.id == tournament_id),
|
||||
values=tournament_body.model_dump(),
|
||||
)
|
||||
except asyncpg.exceptions.UniqueViolationError as exc:
|
||||
check_constraint_and_raise_http_exception(exc)
|
||||
|
||||
await update_start_times_of_matches(tournament_id)
|
||||
return SuccessResponse()
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ tournaments = Table(
|
||||
Column("club_id", BigInteger, ForeignKey("clubs.id"), index=True, nullable=False),
|
||||
Column("dashboard_public", Boolean, nullable=False),
|
||||
Column("logo_path", String, nullable=True),
|
||||
Column("dashboard_endpoint", String, nullable=True),
|
||||
Column("dashboard_endpoint", String, nullable=True, index=True, unique=True),
|
||||
Column("players_can_be_in_multiple_teams", Boolean, nullable=False, server_default="f"),
|
||||
Column("auto_assign_courts", Boolean, nullable=False, server_default="f"),
|
||||
Column("duration_minutes", Integer, nullable=False, server_default="15"),
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
from enum import auto
|
||||
from typing import NoReturn
|
||||
|
||||
import asyncpg # type: ignore[import-untyped]
|
||||
from fastapi import HTTPException
|
||||
from starlette import status
|
||||
|
||||
from bracket.utils.types import EnumAutoStr
|
||||
|
||||
|
||||
class UniqueIndex(EnumAutoStr):
|
||||
ix_tournaments_dashboard_endpoint = auto()
|
||||
ix_users_email = auto()
|
||||
|
||||
|
||||
unique_index_violation_error_lookup = {
|
||||
UniqueIndex.ix_tournaments_dashboard_endpoint: "This dashboard link is already taken",
|
||||
UniqueIndex.ix_users_email: "This email is already taken",
|
||||
}
|
||||
|
||||
|
||||
def check_constraint_and_raise_http_exception(
|
||||
exc: asyncpg.exceptions.UniqueViolationError,
|
||||
) -> NoReturn:
|
||||
constraint_name = exc.as_dict()["constraint_name"]
|
||||
assert constraint_name, "UniqueViolationError occurred but no constraint_name defined"
|
||||
|
||||
if constraint_name in unique_index_violation_error_lookup:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=unique_index_violation_error_lookup[constraint_name],
|
||||
)
|
||||
|
||||
raise exc
|
||||
@@ -68,6 +68,7 @@ disable = [
|
||||
'wrong-import-position',
|
||||
'fixme',
|
||||
'broad-except',
|
||||
'consider-iterating-dictionary',
|
||||
]
|
||||
|
||||
[tool.bandit]
|
||||
|
||||
@@ -83,7 +83,9 @@ async def test_not_authenticated_for_tournament(
|
||||
) -> None:
|
||||
async with inserted_club(DUMMY_CLUB) as club_inserted:
|
||||
async with inserted_tournament(
|
||||
DUMMY_TOURNAMENT.model_copy(update={"club_id": club_inserted.id})
|
||||
DUMMY_TOURNAMENT.model_copy(
|
||||
update={"club_id": club_inserted.id, "dashboard_endpoint": "some-slug"}
|
||||
)
|
||||
) as tournament_inserted:
|
||||
response = JsonDict(
|
||||
await send_auth_request(
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from bracket.database import database
|
||||
from bracket.utils.errors import (
|
||||
unique_index_violation_error_lookup,
|
||||
)
|
||||
|
||||
|
||||
async def test_all_unique_indices_in_lookup() -> None:
|
||||
query = """
|
||||
SELECT
|
||||
idx.relname AS index_name
|
||||
FROM pg_index pgi
|
||||
JOIN pg_class idx ON idx.oid = pgi.indexrelid
|
||||
JOIN pg_namespace insp ON insp.oid = idx.relnamespace
|
||||
JOIN pg_class tbl ON tbl.oid = pgi.indrelid
|
||||
JOIN pg_namespace tnsp ON tnsp.oid = tbl.relnamespace
|
||||
WHERE pgi.indisunique
|
||||
AND tnsp.nspname = 'public'
|
||||
AND idx.relname NOT LIKE '%_pkey'
|
||||
AND idx.relname !='alembic_version_pkc'
|
||||
"""
|
||||
result = await database.fetch_all(query)
|
||||
indices = {ix.index_name for ix in result} # type: ignore[attr-defined]
|
||||
|
||||
expected_indices = {ix.name for ix in unique_index_violation_error_lookup.keys()}
|
||||
assert indices == expected_indices
|
||||
Reference in new issue
Block a user