296 lines
9.3 KiB
Python
296 lines
9.3 KiB
Python
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()
|