1. добавил в админку сортировку игроков и Фото (если галочка стоит, то путь к фото генерируется автоматически)

2. фото обновляются прям в json
This commit is contained in:
2026-06-22 15:31:07 +03:00
parent cd370cf04f
commit f63061a06d
10 changed files with 620 additions and 98 deletions

121
app.py
View File

@@ -23,7 +23,7 @@ from pathlib import Path
import io
import asyncio
import time
from urllib.parse import quote
from urllib.parse import quote, urlencode
import traceback
import contextlib
import os
@@ -60,6 +60,8 @@ from repositories.player_repository import (
search_players_for_admin,
get_player_by_id,
update_player_admin,
update_player_photo_enabled,
ensure_player_photo_enabled_column,
)
from repositories.referee_repository import (
@@ -117,6 +119,8 @@ from repositories.match_lineup_repository import (
)
from services.vmix_json_service import (
EMPTY_PHOTO_PATH,
build_generated_player_photo_path,
build_lineup_json,
get_vmix_match_info_by_token,
get_vmix_standings,
@@ -124,6 +128,7 @@ from services.vmix_json_service import (
get_vmix_team_formations,
get_vmix_scoreboard_info,
get_vmix_players_goal,
resolve_player_photo_for_json,
)
from scheduler import run_scheduler
@@ -139,10 +144,50 @@ templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
PLAYER_ADMIN_SORT_COLUMNS = [
"id",
"full_name",
"first_name",
"last_name",
"external_id",
"position",
"team_name",
"photo",
]
def build_player_admin_sort_links(q: str, current_sort: str, current_direction: str) -> dict:
links = {}
current_sort = current_sort if current_sort in PLAYER_ADMIN_SORT_COLUMNS else "id"
current_direction = "asc" if current_direction == "asc" else "desc"
for column in PLAYER_ADMIN_SORT_COLUMNS:
next_direction = (
"desc"
if current_sort == column and current_direction == "asc"
else "asc"
)
indicator = ""
if current_sort == column:
indicator = "" if current_direction == "asc" else ""
links[column] = {
"url": f"/admin/db/players?{urlencode({'q': q or '', 'sort': column, 'direction': next_direction})}",
"indicator": indicator,
}
return links
@app.on_event("startup")
def start_scheduler():
import threading
try:
ensure_player_photo_enabled_column()
except Exception:
traceback.print_exc()
thread = threading.Thread(
target=run_scheduler, kwargs={"with_signal_handlers": False}, daemon=True
)
@@ -370,6 +415,12 @@ async def admin_auth_middleware(request: Request, call_next):
}
response = await call_next(request)
if path.startswith("/vmix/session/"):
response.headers["Cache-Control"] = "no-store, no-cache, max-age=0, must-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response
@@ -803,23 +854,57 @@ def admin_db_index(request: Request):
@app.get("/admin/db/players", response_class=HTMLResponse)
def admin_db_players(request: Request, q: str = Query(default="")):
def admin_db_players(
request: Request,
q: str = Query(default=""),
sort: str = Query(default="id"),
direction: str = Query(default="desc"),
):
# denied = require_role(request, {"admin"})
# if denied:
# return denied
players = search_players_for_admin(q)
allowed_sort = set(PLAYER_ADMIN_SORT_COLUMNS)
if sort not in allowed_sort:
sort = "id"
direction = "asc" if direction == "asc" else "desc"
players = search_players_for_admin(q, sort_by=sort, sort_dir=direction)
return templates.TemplateResponse(
name="admin_db_players.html",
request=request,
context={
"q": q,
"sort": sort,
"direction": direction,
"sort_links": build_player_admin_sort_links(q, sort, direction),
"players": players,
},
)
@app.post("/admin/db/players/{player_id}/photo-enabled")
def admin_db_player_photo_enabled_submit(
player_id: int,
q: str = Form(default=""),
sort: str = Form(default="id"),
direction: str = Form(default="desc"),
photo_enabled: bool = Form(default=False),
):
allowed_sort = set(PLAYER_ADMIN_SORT_COLUMNS)
if sort not in allowed_sort:
sort = "id"
direction = "asc" if direction == "asc" else "desc"
update_player_photo_enabled(player_id=player_id, photo_enabled=photo_enabled)
return RedirectResponse(
url=f"/admin/db/players?{urlencode({'q': q or '', 'sort': sort, 'direction': direction})}",
status_code=303,
)
@app.get("/admin/db/players/{player_id}/edit", response_class=HTMLResponse)
def admin_db_player_edit(request: Request, player_id: int):
player = get_player_by_id(player_id)
@@ -846,6 +931,7 @@ def admin_db_player_edit_submit(
birth_date: str = Form(default=""),
photo: str = Form(default=""),
video: str = Form(default=""),
photo_enabled: bool = Form(default=False),
):
update_player_admin(
player_id=player_id,
@@ -857,6 +943,7 @@ def admin_db_player_edit_submit(
birth_date=birth_date,
photo=photo,
video=video,
photo_enabled=photo_enabled,
)
return RedirectResponse(
@@ -1358,7 +1445,7 @@ EMPTY_PLAYER = {
"pos": " ",
"number_lastname_amp_K": " ",
"number_fullname": " ",
"photo": r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Photo\EMPTY.png",
"photo": EMPTY_PHOTO_PATH,
}
@@ -1586,7 +1673,7 @@ EMPTY_PLAYER_FORMATION = {
"number": " ",
"number_lastname_amp_K": " ",
"position": " ",
"photo": r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Photo\EMPTY.png",
"photo": EMPTY_PHOTO_PATH,
}
@@ -1613,13 +1700,23 @@ def build_vmix_formation_response(rows, team_id, team_name):
"number_lastname_amp_K": f"{number} {lastname}{suffix_str}".strip(),
"number": number,
"position": position,
"photo": (
r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Photo"
+ "\\"
+ team_name
+ "\\"
+ (p[0] + " " + p[4]).strip()
+ ".png"
"photo": resolve_player_photo_for_json(
{
"player_id": p[7] if len(p) > 7 else None,
"team_id": p[8] if len(p) > 8 else team_id,
"player_name": p[9] if len(p) > 9 else "",
"last_name": lastname,
"first_name": p[4] if len(p) > 4 else "",
"number": number,
"position": position,
"photo_enabled": p[6] if len(p) > 6 else False,
},
generated_photo=build_generated_player_photo_path(
team_name=team_name,
last_name=lastname,
first_name=p[4] if len(p) > 4 else "",
),
fallback_team_id=team_id,
),
}
)