все остальные апдейты на Кубок России

This commit is contained in:
2026-07-02 17:00:15 +03:00
parent 03a68ee8ca
commit e7b215af5e
11 changed files with 1644 additions and 180 deletions

View File

@@ -122,6 +122,82 @@ def get_coach_id_by_external_id(external_id: str) -> int | None:
def _split_coach_name_for_autocreate(full_name: str) -> tuple[str, str]:
parts = [p.strip() for p in str(full_name or "").replace("\xa0", " ").split() if p.strip()]
if not parts:
return "", ""
if len(parts) == 1:
return "", parts[0]
first_name = parts[0]
last_name = " ".join(parts[1:])
return first_name, last_name
def create_coach_from_lineup(
team_id: int,
external_id: str = "",
full_name: str = "",
role: str = "",
) -> int | None:
"""Создаёт минимальную карточку тренера из протокола матча."""
full_name = str(full_name or "").strip()
if not full_name:
return None
first_name, last_name = _split_coach_name_for_autocreate(full_name)
external_id = str(external_id or "").strip()
role = str(role or "").strip()
query = """
INSERT INTO coaches (
external_id,
team_id,
player,
lastname,
name,
amplua,
is_active,
created_at,
updated_at
)
VALUES (NULLIF(%s, ''), %s, %s, %s, %s, %s, TRUE, NOW(), NOW())
ON CONFLICT (external_id)
DO UPDATE SET
team_id = EXCLUDED.team_id,
player = COALESCE(NULLIF(EXCLUDED.player, ''), coaches.player),
lastname = COALESCE(NULLIF(EXCLUDED.lastname, ''), coaches.lastname),
name = COALESCE(NULLIF(EXCLUDED.name, ''), coaches.name),
amplua = COALESCE(NULLIF(EXCLUDED.amplua, ''), coaches.amplua),
is_active = TRUE,
updated_at = NOW()
RETURNING id;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
query,
(
external_id,
team_id,
full_name,
last_name,
first_name,
role,
),
)
row = cur.fetchone()
conn.commit()
return row[0] if row else None
except Exception:
conn.rollback()
raise
finally:
conn.close()
def search_coaches_for_admin(q: str = "") -> list[dict]:
conn = get_connection()
try:

View File

@@ -0,0 +1,295 @@
from db import get_connection
VALID_SIDES = {"home", "away"}
VALID_RESULTS = {"scored", "missed"}
def ensure_match_penalty_tables() -> None:
"""Создает таблицы для серии пенальти."""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
CREATE TABLE IF NOT EXISTS match_penalty_settings (
match_id INTEGER PRIMARY KEY REFERENCES matches(id) ON DELETE CASCADE,
max_rounds INTEGER NOT NULL DEFAULT 5,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
"""
)
cur.execute(
"""
CREATE TABLE IF NOT EXISTS match_penalties (
id SERIAL PRIMARY KEY,
match_id INTEGER NOT NULL REFERENCES matches(id) ON DELETE CASCADE,
side VARCHAR(10) NOT NULL CHECK (side IN ('home', 'away')),
shot_number INTEGER NOT NULL CHECK (shot_number > 0),
result VARCHAR(20) NOT NULL CHECK (result IN ('scored', 'missed')),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE (match_id, side, shot_number)
);
"""
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def _normalize_side(side: str) -> str:
side = str(side or "").strip().lower()
if side not in VALID_SIDES:
raise ValueError("Некорректная сторона пенальти")
return side
def _normalize_result(result: str) -> str:
result = str(result or "").strip().lower()
if result not in VALID_RESULTS:
raise ValueError("Некорректный результат пенальти")
return result
def _ensure_penalty_rounds(cur, match_id: int, max_rounds: int) -> None:
safe_rounds = max(5, int(max_rounds or 5))
cur.execute(
"""
INSERT INTO match_penalty_settings (match_id, max_rounds, created_at, updated_at)
VALUES (%s, %s, NOW(), NOW())
ON CONFLICT (match_id)
DO UPDATE SET
max_rounds = GREATEST(match_penalty_settings.max_rounds, EXCLUDED.max_rounds),
updated_at = NOW();
""",
(match_id, safe_rounds),
)
def get_penalty_state(match_id: int, home_team_name: str = "", away_team_name: str = "") -> dict:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT COALESCE(max_rounds, 5)
FROM match_penalty_settings
WHERE match_id = %s;
""",
(match_id,),
)
settings_row = cur.fetchone()
cur.execute(
"""
SELECT side, shot_number, result
FROM match_penalties
WHERE match_id = %s
ORDER BY shot_number, side;
""",
(match_id,),
)
rows = cur.fetchall()
finally:
conn.close()
max_rounds = int(settings_row[0]) if settings_row else 5
for _, shot_number, _ in rows:
max_rounds = max(max_rounds, int(shot_number or 0), 5)
shots_map = {
(str(side), int(shot_number)): str(result)
for side, shot_number, result in rows
}
rounds = []
totals = {"home": 0, "away": 0}
completed = {"home": 0, "away": 0}
for number in range(1, max_rounds + 1):
row = {"number": number}
for side in ("home", "away"):
result = shots_map.get((side, number), "")
if result:
completed[side] += 1
if result == "scored":
totals[side] += 1
row[side] = result
rounds.append(row)
shots = [
{"side": str(side), "shot_number": int(shot_number), "result": str(result)}
for side, shot_number, result in rows
]
return {
"match_id": match_id,
"home_team": home_team_name or "Хозяева",
"away_team": away_team_name or "Гости",
"max_rounds": max_rounds,
"totals": totals,
"completed": completed,
"rounds": rounds,
"shots": shots,
}
def set_penalty_shot(match_id: int, side: str, shot_number: int, result: str) -> None:
side = _normalize_side(side)
result = _normalize_result(result)
safe_shot_number = max(1, int(shot_number or 1))
conn = get_connection()
try:
with conn.cursor() as cur:
_ensure_penalty_rounds(cur, match_id, safe_shot_number)
cur.execute(
"""
INSERT INTO match_penalties (match_id, side, shot_number, result, created_at, updated_at)
VALUES (%s, %s, %s, %s, NOW(), NOW())
ON CONFLICT (match_id, side, shot_number)
DO UPDATE SET
result = EXCLUDED.result,
updated_at = NOW();
""",
(match_id, side, safe_shot_number, result),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def delete_penalty_shot(match_id: int, side: str, shot_number: int) -> None:
side = _normalize_side(side)
safe_shot_number = max(1, int(shot_number or 1))
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
DELETE FROM match_penalties
WHERE match_id = %s AND side = %s AND shot_number = %s;
""",
(match_id, side, safe_shot_number),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def add_penalty_round(match_id: int) -> int:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO match_penalty_settings (match_id, max_rounds, created_at, updated_at)
VALUES (%s, 6, NOW(), NOW())
ON CONFLICT (match_id)
DO UPDATE SET
max_rounds = GREATEST(match_penalty_settings.max_rounds + 1, 6),
updated_at = NOW()
RETURNING max_rounds;
""",
(match_id,),
)
max_rounds = int(cur.fetchone()[0])
conn.commit()
return max_rounds
except Exception:
conn.rollback()
raise
finally:
conn.close()
def delete_last_penalty_round(match_id: int) -> int:
"""Удаляет последнюю добавленную строку серии пенальти и ее данные.
Первые 5 строк считаются базовыми и не удаляются. Если добавлена 6-я,
7-я и т.д. строка, удаляется самая последняя строка вместе с ударами
обеих команд в этой строке.
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT COALESCE(max_rounds, 5)
FROM match_penalty_settings
WHERE match_id = %s;
""",
(match_id,),
)
settings_row = cur.fetchone()
settings_max = int(settings_row[0]) if settings_row else 5
cur.execute(
"""
SELECT COALESCE(MAX(shot_number), 0)
FROM match_penalties
WHERE match_id = %s;
""",
(match_id,),
)
shots_max = int(cur.fetchone()[0] or 0)
current_max = max(5, settings_max, shots_max)
if current_max <= 5:
_ensure_penalty_rounds(cur, match_id, 5)
conn.commit()
return 5
new_max = max(5, current_max - 1)
cur.execute(
"""
DELETE FROM match_penalties
WHERE match_id = %s AND shot_number = %s;
""",
(match_id, current_max),
)
cur.execute(
"""
INSERT INTO match_penalty_settings (match_id, max_rounds, created_at, updated_at)
VALUES (%s, %s, NOW(), NOW())
ON CONFLICT (match_id)
DO UPDATE SET
max_rounds = EXCLUDED.max_rounds,
updated_at = NOW();
""",
(match_id, new_max),
)
conn.commit()
return new_max
except Exception:
conn.rollback()
raise
finally:
conn.close()
def clear_penalties(match_id: int) -> None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute("DELETE FROM match_penalties WHERE match_id = %s;", (match_id,))
cur.execute("DELETE FROM match_penalty_settings WHERE match_id = %s;", (match_id,))
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()

View File

@@ -355,7 +355,7 @@ def get_tour_schedule_by_match_id(match_id: int) -> list[dict]:
with conn.cursor() as cur:
cur.execute(
"""
SELECT m.tour, m.season, m.source_key
SELECT m.tour
FROM matches m
WHERE m.id = %s
""",
@@ -366,8 +366,6 @@ def get_tour_schedule_by_match_id(match_id: int) -> list[dict]:
return []
tour = row[0]
season = row[1]
source_key = resolve_match_source_key(match_id, row[2] if len(row) > 2 else None)
cur.execute(
"""
@@ -386,13 +384,11 @@ def get_tour_schedule_by_match_id(match_id: int) -> list[dict]:
LEFT JOIN teams ht ON ht.id = m.home_team_id
LEFT JOIN teams at ON at.id = m.away_team_id
WHERE m.tour = %s
AND (%s IS NULL OR m.season = %s)
AND COALESCE(NULLIF(m.source_key, ''), %s) = %s
ORDER BY
m.match_date NULLS LAST,
m.id
""",
(tour, season, season, source_key, source_key),
(tour,),
)
rows = cur.fetchall()

View File

@@ -222,6 +222,104 @@ def get_player_id_by_external_id(external_id: str) -> int | None:
conn.close()
def _split_full_name_for_autocreate(full_name: str) -> tuple[str, str]:
"""Аккуратно делит имя с сайта на first_name / last_name для новых записей."""
parts = [p.strip() for p in str(full_name or "").replace("\xa0", " ").split() if p.strip()]
if not parts:
return "", ""
if len(parts) == 1:
return "", parts[0]
# На сайте чаще приходит "Имя Фамилия". Полное имя всё равно сохраняем отдельно,
# поэтому даже при другом порядке данные можно быстро поправить в справочнике.
first_name = parts[0]
last_name = " ".join(parts[1:])
return first_name, last_name
def create_player_from_lineup(
team_id: int,
external_id: str = "",
full_name: str = "",
number: str = "",
position: str = "",
) -> tuple[int | None, str | None]:
"""Создаёт минимальную карточку игрока из протокола матча и возвращает (id, position).
Используется при загрузке составов с сайта, когда игрока ещё нет в справочнике.
"""
full_name = str(full_name or "").strip()
if not full_name:
return None, None
first_name, last_name = _split_full_name_for_autocreate(full_name)
external_id = str(external_id or "").strip()
number = str(number or "").strip()
position = str(position or "").strip()
query = """
INSERT INTO players (
external_id,
team_id,
full_name,
first_name,
last_name,
number,
position,
pos,
amplua,
is_active,
photo_enabled,
created_at,
updated_at
)
VALUES (
NULLIF(%s, ''), %s, %s, %s, %s, %s, %s, %s, %s, TRUE, FALSE,
NOW(), NOW()
)
ON CONFLICT (external_id)
DO UPDATE SET
team_id = EXCLUDED.team_id,
full_name = COALESCE(NULLIF(EXCLUDED.full_name, ''), players.full_name),
first_name = COALESCE(NULLIF(EXCLUDED.first_name, ''), players.first_name),
last_name = COALESCE(NULLIF(EXCLUDED.last_name, ''), players.last_name),
number = COALESCE(NULLIF(EXCLUDED.number, ''), players.number),
position = COALESCE(NULLIF(EXCLUDED.position, ''), players.position),
pos = COALESCE(NULLIF(EXCLUDED.pos, ''), players.pos),
amplua = COALESCE(NULLIF(EXCLUDED.amplua, ''), players.amplua),
is_active = TRUE,
updated_at = NOW()
RETURNING id, position;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
query,
(
external_id,
team_id,
full_name,
first_name,
last_name,
number,
position,
position,
position,
),
)
row = cur.fetchone()
conn.commit()
return row if row else (None, None)
except Exception:
conn.rollback()
raise
finally:
conn.close()
PLAYER_ADMIN_SORT_COLUMNS = {
"id": "p.id",
"full_name": "p.full_name",