diff --git a/app.py b/app.py index 8c64919..e06b8e7 100644 --- a/app.py +++ b/app.py @@ -75,6 +75,7 @@ from repositories.player_repository import ( update_player_admin, update_player_photo_enabled, ensure_player_photo_enabled_column, + create_player_admin, ) from repositories.referee_repository import ( @@ -82,6 +83,7 @@ from repositories.referee_repository import ( get_referee_by_id, update_referee_admin, get_all_referees, + create_referee_admin, # replace_match_referees, ) @@ -91,6 +93,7 @@ from repositories.coach_repository import ( search_coaches_for_admin, get_coach_by_id, update_coach_admin, + create_coach_admin, ) from repositories.stadium_repository import ( @@ -103,6 +106,7 @@ from repositories.team_repository import ( search_teams_for_admin, get_team_by_id, update_team_admin, + list_teams_for_admin_select, ) from repositories.match_event_repository import ( @@ -894,6 +898,188 @@ def close_session(session_token: str): + +def _admin_create_optional_int(value, field_title: str, minimum: int = 0) -> int | None: + text = str(value or "").strip() + if not text: + return None + try: + number = int(text) + except ValueError as exc: + raise ValueError(f"Поле «{field_title}» должно быть целым числом.") from exc + if number < minimum: + raise ValueError(f"Поле «{field_title}» не может быть меньше {minimum}.") + return number + + +def _admin_create_required_int(value, field_title: str, minimum: int = 0) -> int: + number = _admin_create_optional_int(value, field_title, minimum) + if number is None: + raise ValueError(f"Заполните поле «{field_title}».") + return number + + +def _render_admin_entity_create( + request: Request, + entity_type: str = "player", + error: str | None = None, + created_id: int | None = None, + form_values: dict | None = None, + status_code: int = 200, +): + allowed_entities = {"player", "coach", "referee"} + entity_type = entity_type if entity_type in allowed_entities else "player" + + try: + teams = list_teams_for_admin_select() + except Exception: + teams = [] + + return templates.TemplateResponse( + name="admin_db_create.html", + request=request, + context={ + "entity_type": entity_type, + "teams": teams, + "error": error, + "created_id": created_id, + "form_values": form_values or {}, + }, + status_code=status_code, + ) + + +@app.get("/admin/db/create", response_class=HTMLResponse) +def admin_db_create_page( + request: Request, + entity: str = Query(default="player"), + created_id: int | None = Query(default=None), +): + denied = require_role(request, {"admin"}) + if denied: + return denied + return _render_admin_entity_create( + request=request, + entity_type=entity, + created_id=created_id, + ) + + +@app.post("/admin/db/create", response_class=HTMLResponse) +async def admin_db_create_submit(request: Request): + denied = require_role(request, {"admin"}) + if denied: + return denied + + form = await request.form() + values = {key: str(value) for key, value in form.items()} + entity_type = values.get("entity_type", "player").strip().lower() + if entity_type not in {"player", "coach", "referee"}: + entity_type = "player" + + try: + full_name = values.get("full_name", "").strip() + if not full_name: + raise ValueError("Заполните поле «Полное имя / ФИО».") + + external_id = values.get("external_id", "").strip() + first_name = values.get("first_name", "").strip() + last_name = values.get("last_name", "").strip() + is_active = "is_active" in form + + if entity_type == "player": + team_id = _admin_create_required_int(values.get("team_id"), "Команда", 1) + position = values.get("position", "").strip() + if not position: + raise ValueError("Выберите или укажите амплуа игрока.") + + created_id = create_player_admin( + team_id=team_id, + full_name=full_name, + first_name=first_name, + last_name=last_name, + external_id=external_id, + number=values.get("number", ""), + position=position, + birth_date=values.get("birth_date", ""), + photo=values.get("photo", ""), + video=values.get("video", ""), + height_cm=_admin_create_optional_int(values.get("height_cm"), "Рост", 1), + weight_kg=_admin_create_optional_int(values.get("weight_kg"), "Вес", 1), + games=_admin_create_optional_int(values.get("games"), "Игры", 0) or 0, + goals=_admin_create_optional_int(values.get("goals"), "Голы", 0) or 0, + penaltys=_admin_create_optional_int(values.get("penaltys"), "Голы с пенальти", 0) or 0, + assists=_admin_create_optional_int(values.get("assists"), "Передачи", 0) or 0, + yellows=_admin_create_optional_int(values.get("yellows"), "Жёлтые карточки", 0) or 0, + reds=_admin_create_optional_int(values.get("reds"), "Красные карточки", 0) or 0, + is_active=is_active, + photo_enabled="photo_enabled" in form, + ) + redirect_url = f"/admin/db/create?entity=player&created_id={created_id}" + + elif entity_type == "coach": + team_id = _admin_create_required_int(values.get("team_id"), "Команда", 1) + role = values.get("role", "").strip() + if not role: + raise ValueError("Заполните должность / амплуа тренера.") + + created_id = create_coach_admin( + team_id=team_id, + full_name=full_name, + first_name=first_name, + last_name=last_name, + external_id=external_id, + birth_date=values.get("birth_date", ""), + role=role, + is_active=is_active, + ) + redirect_url = f"/admin/db/create?entity=coach&created_id={created_id}" + + else: + created_id = create_referee_admin( + full_name=full_name, + first_name=first_name, + last_name=last_name, + middle_name=values.get("middle_name", ""), + city=values.get("city", ""), + external_id=external_id, + is_active=is_active, + ) + redirect_url = f"/admin/db/create?entity=referee&created_id={created_id}" + + log_action( + request, + action="admin_db_entity_created", + entity_type=entity_type, + entity_id=created_id, + details={"full_name": full_name, "external_id": external_id}, + ) + return RedirectResponse(url=redirect_url, status_code=303) + + except ValueError as exc: + return _render_admin_entity_create( + request=request, + entity_type=entity_type, + error=str(exc), + form_values=values, + status_code=400, + ) + except Exception as exc: + traceback.print_exc() + error_text = "Не удалось создать запись." + if "duplicate key" in str(exc).lower() or "unique constraint" in str(exc).lower(): + error_text += " Запись с таким External ID уже существует." + else: + error_text += f" Ошибка базы данных: {str(exc)}" + return _render_admin_entity_create( + request=request, + entity_type=entity_type, + error=error_text, + form_values=values, + status_code=500, + ) + + @app.get("/admin/db/players", response_class=HTMLResponse) def admin_db_players( request: Request, diff --git a/repositories/coach_repository.py b/repositories/coach_repository.py index 402be54..724c70f 100644 --- a/repositories/coach_repository.py +++ b/repositories/coach_repository.py @@ -359,4 +359,59 @@ def update_coach_admin( conn.rollback() raise finally: - conn.close() \ No newline at end of file + conn.close() + +def create_coach_admin( + team_id: int, + full_name: str, + first_name: str = "", + last_name: str = "", + external_id: str = "", + birth_date: str = "", + role: str = "", + is_active: bool = True, +) -> int: + """Создаёт тренера вручную из административного раздела.""" + query = """ + INSERT INTO coaches ( + external_id, + team_id, + player, + lastname, + name, + born, + amplua, + is_active, + created_at, + updated_at + ) + VALUES ( + NULLIF(%s, ''), %s, %s, %s, %s, NULLIF(%s, ''), %s, %s, NOW(), NOW() + ) + RETURNING id; + """ + + conn = get_connection() + try: + with conn.cursor() as cur: + cur.execute( + query, + ( + external_id.strip(), + int(team_id), + full_name.strip(), + last_name.strip(), + first_name.strip(), + birth_date.strip(), + role.strip(), + bool(is_active), + ), + ) + row = cur.fetchone() + conn.commit() + return int(row[0]) + except Exception: + conn.rollback() + raise + finally: + conn.close() diff --git a/repositories/player_repository.py b/repositories/player_repository.py index 47329f3..a7c19bc 100644 --- a/repositories/player_repository.py +++ b/repositories/player_repository.py @@ -544,3 +544,101 @@ def update_player_photo_enabled(player_id: int, photo_enabled: bool = False) -> raise finally: conn.close() + + +def create_player_admin( + team_id: int, + full_name: str, + first_name: str = "", + last_name: str = "", + external_id: str = "", + number: str = "", + position: str = "", + birth_date: str = "", + photo: str = "", + video: str = "", + height_cm: int | None = None, + weight_kg: int | None = None, + games: int = 0, + goals: int = 0, + penaltys: int = 0, + assists: int = 0, + yellows: int = 0, + reds: int = 0, + is_active: bool = True, + photo_enabled: bool = False, +) -> int: + """Создаёт игрока вручную из административного раздела.""" + query = """ + INSERT INTO players ( + external_id, + team_id, + full_name, + first_name, + last_name, + number, + position, + pos, + amplua, + born, + photo, + video, + height_cm, + weight_kg, + games, + goals, + penaltys, + assists, + yellows, + reds, + is_active, + photo_enabled, + created_at, + updated_at + ) + VALUES ( + NULLIF(%s, ''), %s, %s, %s, %s, NULLIF(%s, ''), %s, %s, %s, + NULLIF(%s, '')::date, NULLIF(%s, ''), NULLIF(%s, ''), %s, %s, + %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW() + ) + RETURNING id; + """ + + conn = get_connection() + try: + with conn.cursor() as cur: + cur.execute( + query, + ( + external_id.strip(), + int(team_id), + full_name.strip(), + first_name.strip(), + last_name.strip(), + number.strip(), + position.strip(), + position.strip(), + position.strip(), + birth_date.strip(), + photo.strip(), + video.strip(), + height_cm, + weight_kg, + int(games), + int(goals), + int(penaltys), + int(assists), + int(yellows), + int(reds), + bool(is_active), + bool(photo_enabled), + ), + ) + row = cur.fetchone() + conn.commit() + return int(row[0]) + except Exception: + conn.rollback() + raise + finally: + conn.close() diff --git a/repositories/referee_repository.py b/repositories/referee_repository.py index 74335c2..50d6d97 100644 --- a/repositories/referee_repository.py +++ b/repositories/referee_repository.py @@ -265,4 +265,56 @@ def replace_match_referees(match_id: int, rows: list[dict]) -> None: conn.rollback() raise finally: - conn.close() \ No newline at end of file + conn.close() + +def create_referee_admin( + full_name: str, + first_name: str = "", + last_name: str = "", + middle_name: str = "", + city: str = "", + external_id: str = "", + is_active: bool = True, +) -> int: + """Создаёт судью вручную из административного раздела.""" + query = """ + INSERT INTO referees ( + external_id, + full_name, + lastname, + name, + middle_name, + city, + is_active, + created_at, + updated_at + ) + VALUES ( + NULLIF(%s, ''), %s, %s, %s, %s, NULLIF(%s, ''), %s, NOW(), NOW() + ) + RETURNING id; + """ + + conn = get_connection() + try: + with conn.cursor() as cur: + cur.execute( + query, + ( + external_id.strip(), + full_name.strip(), + last_name.strip(), + first_name.strip(), + middle_name.strip(), + city.strip(), + bool(is_active), + ), + ) + row = cur.fetchone() + conn.commit() + return int(row[0]) + except Exception: + conn.rollback() + raise + finally: + conn.close() diff --git a/repositories/team_repository.py b/repositories/team_repository.py index 3382291..536e2b8 100644 --- a/repositories/team_repository.py +++ b/repositories/team_repository.py @@ -238,4 +238,30 @@ def update_team_admin( conn.rollback() raise finally: - conn.close() \ No newline at end of file + conn.close() + +def list_teams_for_admin_select() -> list[dict]: + """Возвращает полный компактный список команд для выпадающих списков админки.""" + conn = get_connection() + try: + with conn.cursor() as cur: + cur.execute( + """ + SELECT id, name, short_name_3, city + FROM teams + ORDER BY name ASC, id ASC + """ + ) + rows = cur.fetchall() + + return [ + { + "id": row[0], + "name": row[1] or "", + "short_name_3": row[2] or "", + "city": row[3] or "", + } + for row in rows + ] + finally: + conn.close() diff --git a/templates/admin_db_coaches.html b/templates/admin_db_coaches.html index 6d7b971..43ab7b0 100644 --- a/templates/admin_db_coaches.html +++ b/templates/admin_db_coaches.html @@ -149,6 +149,9 @@
diff --git a/templates/admin_db_create.html b/templates/admin_db_create.html new file mode 100644 index 0000000..c5ee9de --- /dev/null +++ b/templates/admin_db_create.html @@ -0,0 +1,459 @@ + + + + + +