все остальные апдейты на Кубок России
This commit is contained in:
@@ -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:
|
||||
|
||||
295
repositories/match_penalty_repository.py
Normal file
295
repositories/match_penalty_repository.py
Normal 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()
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user