125 lines
4.1 KiB
Python
125 lines
4.1 KiB
Python
"""Раздел «Звания»: справочник магазина званий."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from fastapi import APIRouter, HTTPException, Request, Response
|
||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||
|
||
from admin.security import check_csrf
|
||
from common import __version__
|
||
from common.repositories import titles_repo
|
||
|
||
router = APIRouter(prefix="/titles", tags=["titles"])
|
||
|
||
MAX_TITLE_LENGTH = 100
|
||
|
||
|
||
def _parse(title: str, price: str, visible: bool) -> tuple[dict[str, object] | None, list[str]]:
|
||
errors: list[str] = []
|
||
title = title.strip()
|
||
if not title:
|
||
errors.append("Название не может быть пустым")
|
||
elif len(title) > MAX_TITLE_LENGTH:
|
||
errors.append(f"Название длиннее {MAX_TITLE_LENGTH} символов")
|
||
|
||
try:
|
||
price_value = int(price)
|
||
except (TypeError, ValueError):
|
||
price_value = -1
|
||
errors.append("Цена должна быть целым числом")
|
||
if price_value < 0 and "Цена должна быть целым числом" not in errors:
|
||
errors.append("Цена не может быть отрицательной")
|
||
|
||
if errors:
|
||
return None, errors
|
||
return {"title": title, "price": price_value, "visible": visible}, []
|
||
|
||
|
||
async def _render(
|
||
request: Request,
|
||
*,
|
||
errors: list[str] | None = None,
|
||
form: dict[str, str] | None = None,
|
||
saved: str | None = None,
|
||
status_code: int = 200,
|
||
) -> HTMLResponse:
|
||
return request.app.state.templates.TemplateResponse(
|
||
request=request,
|
||
name="titles/list.html",
|
||
context={
|
||
"version": __version__,
|
||
"titles": await titles_repo.list_titles(),
|
||
"errors": errors or [],
|
||
"form": form or {"title": "", "price": ""},
|
||
"saved": saved,
|
||
"max_title_length": MAX_TITLE_LENGTH,
|
||
},
|
||
status_code=status_code,
|
||
)
|
||
|
||
|
||
@router.get("", response_class=HTMLResponse)
|
||
async def list_titles(request: Request, saved: str | None = None) -> HTMLResponse:
|
||
return await _render(request, saved=saved)
|
||
|
||
|
||
@router.post("")
|
||
async def create_title(request: Request) -> Response:
|
||
form = await request.form()
|
||
check_csrf(request, str(form.get("csrf_token", "")))
|
||
|
||
raw_title = str(form.get("title", ""))
|
||
raw_price = str(form.get("price", ""))
|
||
data, errors = _parse(raw_title, raw_price, True)
|
||
if errors:
|
||
return await _render(
|
||
request,
|
||
errors=errors,
|
||
form={"title": raw_title, "price": raw_price},
|
||
status_code=400,
|
||
)
|
||
|
||
await titles_repo.create_title(str(data["title"]), int(data["price"]), True)
|
||
return RedirectResponse(url="/titles?saved=created", status_code=303)
|
||
|
||
|
||
@router.post("/{title_id}")
|
||
async def update_title(request: Request, title_id: int) -> Response:
|
||
form = await request.form()
|
||
check_csrf(request, str(form.get("csrf_token", "")))
|
||
|
||
data, errors = _parse(
|
||
str(form.get("title", "")),
|
||
str(form.get("price", "")),
|
||
form.get("visible") is not None,
|
||
)
|
||
if errors:
|
||
return await _render(request, errors=errors, status_code=400)
|
||
|
||
if not await titles_repo.update_title(
|
||
title_id,
|
||
str(data["title"]),
|
||
int(data["price"]),
|
||
bool(data["visible"]),
|
||
):
|
||
raise HTTPException(status_code=404, detail="Звание не найдено")
|
||
return RedirectResponse(url="/titles?saved=updated", status_code=303)
|
||
|
||
|
||
@router.post("/{title_id}/delete")
|
||
async def delete_title(request: Request, title_id: int) -> Response:
|
||
form = await request.form()
|
||
check_csrf(request, str(form.get("csrf_token", "")))
|
||
|
||
used = await titles_repo.usage_count(title_id)
|
||
if used:
|
||
return await _render(
|
||
request,
|
||
errors=[f"Звание используется {used} игроками — скройте его вместо удаления"],
|
||
status_code=400,
|
||
)
|
||
|
||
if not await titles_repo.delete_title(title_id):
|
||
raise HTTPException(status_code=404, detail="Звание не найдено")
|
||
return RedirectResponse(url="/titles?saved=deleted", status_code=303)
|