first commit

This commit is contained in:
2026-04-20 11:56:11 +03:00
commit 8d80fadb56
103 changed files with 19756 additions and 0 deletions

0
repositories/__init__.py Normal file
View File

Binary file not shown.

View File

@@ -0,0 +1,45 @@
# repositories/audit_log_repository.py
import json
from db import get_connection
def create_audit_log(
user_id=None,
username=None,
role=None,
action="",
entity_type=None,
entity_id=None,
match_id=None,
session_token=None,
ip_address=None,
user_agent=None,
details=None,
):
conn = get_connection()
try:
with conn:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO audit_logs (
user_id, username, role, action, entity_type, entity_id,
match_id, session_token, ip_address, user_agent, details
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb)
""",
(
user_id,
username,
role,
action,
entity_type,
entity_id,
match_id,
session_token,
ip_address,
user_agent,
json.dumps(details or {}, ensure_ascii=False),
),
)
finally:
conn.close()

View File

@@ -0,0 +1,159 @@
from db import get_connection
def get_user_by_username(username: str):
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, username, password_hash, is_active, created_at
FROM admin_users
WHERE username = %s
LIMIT 1
""",
(username,),
)
return cur.fetchone()
finally:
conn.close()
def get_user_by_id(user_id: int):
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, username, password_hash, is_active, created_at
FROM admin_users
WHERE id = %s
LIMIT 1
""",
(user_id,),
)
return cur.fetchone()
finally:
conn.close()
def create_auth_session_record(
user_id: int,
session_token: str,
expires_at,
ip_address: str | None = None,
user_agent: str | None = None,
):
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO auth_sessions (
user_id,
session_token,
last_activity_at,
expires_at,
ip_address,
user_agent
)
VALUES (%s, %s, NOW(), %s, %s, %s)
RETURNING id
""",
(user_id, session_token, expires_at, ip_address, user_agent),
)
row = cur.fetchone()
conn.commit()
return row[0] if row else None
finally:
conn.close()
def get_auth_session_by_token(session_token: str):
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
s.id,
s.user_id,
s.session_token,
s.created_at,
s.last_activity_at,
s.expires_at,
s.revoked_at,
s.ip_address,
s.user_agent,
u.username,
u.is_active,
u.role
FROM auth_sessions s
JOIN admin_users u ON u.id = s.user_id
WHERE s.session_token = %s
LIMIT 1
""",
(session_token,),
)
return cur.fetchone()
finally:
conn.close()
def touch_auth_session_if_needed(session_token: str, expires_at, throttle_seconds: int = 60):
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE auth_sessions
SET last_activity_at = NOW(),
expires_at = %s
WHERE session_token = %s
AND revoked_at IS NULL
AND last_activity_at < NOW() - (%s || ' seconds')::interval
""",
(expires_at, session_token, str(throttle_seconds)),
)
conn.commit()
return cur.rowcount
finally:
conn.close()
def revoke_auth_session(session_token: str):
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE auth_sessions
SET revoked_at = NOW()
WHERE session_token = %s
AND revoked_at IS NULL
""",
(session_token,),
)
conn.commit()
return cur.rowcount
finally:
conn.close()
def revoke_all_user_sessions(user_id: int):
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE auth_sessions
SET revoked_at = NOW()
WHERE user_id = %s
AND revoked_at IS NULL
""",
(user_id,),
)
conn.commit()
return cur.rowcount
finally:
conn.close()

View File

@@ -0,0 +1,286 @@
from db import get_connection
from repositories.team_repository import get_team_id_by_external_id
def upsert_coach(
external_id: str,
team_external_id: str,
player: str,
lastname: str = "",
name: str = "",
born: str = "",
amplua: str = "",
is_active: bool = True,
) -> None:
team_id = get_team_id_by_external_id(str(team_external_id).strip())
if team_id is None:
raise ValueError(f"Team not found by external_id: {team_external_id}")
query = """
INSERT INTO coaches (
external_id,
team_id,
player,
lastname,
name,
born,
amplua,
is_active,
created_at,
updated_at
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
ON CONFLICT (external_id)
DO UPDATE SET
team_id = EXCLUDED.team_id,
player = EXCLUDED.player,
lastname = EXCLUDED.lastname,
name = EXCLUDED.name,
born = EXCLUDED.born,
amplua = EXCLUDED.amplua,
is_active = EXCLUDED.is_active,
updated_at = NOW();
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
query,
(
str(external_id).strip(),
team_id,
player.strip(),
lastname.strip(),
name.strip(),
born.strip(),
amplua.strip(),
is_active,
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_player_id_by_name_and_team(full_name: str, team_id: int) -> int | None:
query = """
SELECT id
FROM players
WHERE team_id = %s
AND LOWER(full_name) = LOWER(%s)
LIMIT 1;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (team_id, full_name.strip()))
row = cur.fetchone()
return row[0] if row else None
finally:
conn.close()
def get_coach_id_by_name_and_team(full_name: str, team_id: int) -> int | None:
query = """
SELECT id
FROM coaches
WHERE team_id = %s
AND LOWER(player) = LOWER(%s)
LIMIT 1;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (team_id, full_name.strip()))
row = cur.fetchone()
return row[0] if row else None
finally:
conn.close()
def get_coach_id_by_external_id(external_id: str) -> int | None:
query = """
SELECT id
FROM coaches
WHERE external_id = %s
LIMIT 1;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (str(external_id).strip(),))
row = cur.fetchone()
return row[0] if row else None
finally:
conn.close()
def search_coaches_for_admin(q: str = "") -> list[dict]:
conn = get_connection()
try:
with conn.cursor() as cur:
if q.strip():
pattern = f"%{q.strip()}%"
cur.execute(
"""
SELECT
c.id,
c.player,
c.external_id,
c.amplua,
t.name AS team_name
FROM coaches c
LEFT JOIN teams t ON t.id = c.team_id
WHERE
c.player ILIKE %s
OR COALESCE(c.external_id, '') ILIKE %s
OR COALESCE(c.amplua, '') ILIKE %s
OR COALESCE(t.name, '') ILIKE %s
ORDER BY c.player ASC, c.id ASC
LIMIT 200
""",
(pattern, pattern, pattern, pattern),
)
else:
cur.execute(
"""
SELECT
c.id,
c.player,
c.external_id,
c.amplua,
t.name AS team_name
FROM coaches c
LEFT JOIN teams t ON t.id = c.team_id
ORDER BY c.id DESC
LIMIT 200
"""
)
rows = cur.fetchall()
return [
{
"id": row[0],
"full_name": row[1] or "",
"external_id": row[2] or "",
"role": row[3] or "",
"team_name": row[4] or "",
}
for row in rows
]
finally:
conn.close()
def get_coach_by_id(coach_id: int) -> dict | None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
c.id,
c.player,
c.external_id,
c.amplua,
c.team_id,
t.name AS team_name
FROM coaches c
LEFT JOIN teams t ON t.id = c.team_id
WHERE c.id = %s
LIMIT 1
""",
(coach_id,),
)
row = cur.fetchone()
if not row:
return None
return {
"id": row[0],
"full_name": row[1] or "",
"external_id": row[2] or "",
"role": row[3] or "",
"team_id": row[4],
"team_name": row[5] or "",
}
finally:
conn.close()
def get_coach_by_id(coach_id: int) -> dict | None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
c.id,
c.player,
c.external_id,
c.amplua,
c.team_id,
t.name AS team_name
FROM coaches c
LEFT JOIN teams t ON t.id = c.team_id
WHERE c.id = %s
LIMIT 1
""",
(coach_id,),
)
row = cur.fetchone()
if not row:
return None
return {
"id": row[0],
"full_name": row[1] or "",
"external_id": row[2] or "",
"role": row[3] or "",
"team_id": row[4],
"team_name": row[5] or "",
}
finally:
conn.close()
def update_coach_admin(
coach_id: int,
full_name: str = "",
external_id: str = "",
role: str = "",
) -> None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE coaches
SET
player = %s,
external_id = NULLIF(%s, ''),
amplua = %s
WHERE id = %s
""",
(
full_name.strip(),
external_id.strip(),
role.strip(),
coach_id,
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()

View File

@@ -0,0 +1,178 @@
from db import get_connection
def create_match_clock_table() -> None:
query = """
CREATE TABLE IF NOT EXISTS match_clocks (
match_id BIGINT PRIMARY KEY REFERENCES matches(id) ON DELETE CASCADE,
current_period VARCHAR(10) NOT NULL DEFAULT '1H',
timer_running BOOLEAN NOT NULL DEFAULT FALSE,
accumulated_seconds INTEGER NOT NULL DEFAULT 0,
period_started_at TIMESTAMP NULL,
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query)
conn.commit()
finally:
conn.close()
def ensure_match_clock(match_id: int) -> None:
query = """
INSERT INTO match_clocks (match_id, current_period, timer_running, accumulated_seconds, period_started_at, updated_at)
VALUES (%s, '1H', FALSE, 0, NULL, NOW())
ON CONFLICT (match_id) DO NOTHING;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (match_id,))
conn.commit()
finally:
conn.close()
def get_match_clock(match_id: int) -> dict:
ensure_match_clock(match_id)
query = """
SELECT
match_id,
current_period,
timer_running,
accumulated_seconds,
period_started_at,
CASE
WHEN timer_running = TRUE AND period_started_at IS NOT NULL
THEN accumulated_seconds + FLOOR(EXTRACT(EPOCH FROM (NOW() - period_started_at)))::INT
ELSE accumulated_seconds
END AS current_seconds
FROM match_clocks
WHERE match_id = %s;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (match_id,))
row = cur.fetchone()
finally:
conn.close()
return {
"match_id": row[0],
"current_period": row[1],
"timer_running": bool(row[2]),
"accumulated_seconds": row[3] or 0,
"period_started_at": row[4].isoformat() if row[4] else None,
"current_seconds": row[5] or 0,
}
def update_match_clock(match_id: int, action: str, seconds: int | None = None) -> dict:
ensure_match_clock(match_id)
conn = get_connection()
try:
with conn.cursor() as cur:
if action == "start_1h":
cur.execute(
"""
UPDATE match_clocks
SET current_period = '1H', timer_running = TRUE, accumulated_seconds = 0,
period_started_at = NOW(), updated_at = NOW()
WHERE match_id = %s;
""",
(match_id,),
)
elif action == "pause":
cur.execute(
"""
UPDATE match_clocks
SET accumulated_seconds = CASE
WHEN timer_running = TRUE AND period_started_at IS NOT NULL
THEN accumulated_seconds + FLOOR(EXTRACT(EPOCH FROM (NOW() - period_started_at)))::INT
ELSE accumulated_seconds
END,
timer_running = FALSE,
period_started_at = NULL,
updated_at = NOW()
WHERE match_id = %s;
""",
(match_id,),
)
elif action == "halftime":
cur.execute(
"""
UPDATE match_clocks
SET accumulated_seconds = CASE
WHEN timer_running = TRUE AND period_started_at IS NOT NULL
THEN accumulated_seconds + FLOOR(EXTRACT(EPOCH FROM (NOW() - period_started_at)))::INT
ELSE accumulated_seconds
END,
current_period = 'HT',
timer_running = FALSE,
period_started_at = NULL,
updated_at = NOW()
WHERE match_id = %s;
""",
(match_id,),
)
elif action == "start_2h":
cur.execute(
"""
UPDATE match_clocks
SET current_period = '2H', timer_running = TRUE,
accumulated_seconds = CASE WHEN accumulated_seconds < 2700 THEN 2700 ELSE accumulated_seconds END,
period_started_at = NOW(),
updated_at = NOW()
WHERE match_id = %s;
""",
(match_id,),
)
elif action == "finish":
cur.execute(
"""
UPDATE match_clocks
SET accumulated_seconds = CASE
WHEN timer_running = TRUE AND period_started_at IS NOT NULL
THEN accumulated_seconds + FLOOR(EXTRACT(EPOCH FROM (NOW() - period_started_at)))::INT
ELSE accumulated_seconds
END,
current_period = 'FT',
timer_running = FALSE,
period_started_at = NULL,
updated_at = NOW()
WHERE match_id = %s;
""",
(match_id,),
)
elif action == "set_time":
if seconds is None or not isinstance(seconds, int) or seconds < 0:
raise ValueError("invalid_seconds")
cur.execute(
"""
UPDATE match_clocks
SET accumulated_seconds = %s,
period_started_at = CASE WHEN timer_running THEN NOW() ELSE NULL END,
updated_at = NOW()
WHERE match_id = %s;
""",
(seconds, match_id),
)
else:
raise ValueError("invalid_clock_action")
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
return get_match_clock(match_id)

View File

@@ -0,0 +1,155 @@
from db import get_connection
def get_match_coaches_grouped(
match_id: int,
home_team_id: int | None = None,
away_team_id: int | None = None,
) -> dict:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
mc.side,
mc.coach_id,
COALESCE(c.player, c.name, '') AS coach_name,
COALESCE(mc.role, c.amplua, '') AS role
FROM match_coaches mc
JOIN coaches c
ON c.id = mc.coach_id
WHERE mc.match_id = %s
ORDER BY mc.side, mc.sort_order, mc.id
""",
(match_id,),
)
rows = cur.fetchall()
result = {
"home": [],
"away": [],
}
for row in rows:
item = {
"coach_id": row[1],
"coach_name": row[2] or "",
"role": row[3] or "",
}
if row[0] == "home":
result["home"].append(item)
elif row[0] == "away":
result["away"].append(item)
return result
finally:
conn.close()
def replace_match_coaches(match_id: int, *args) -> None:
"""
Поддерживает оба варианта вызова:
1) replace_match_coaches(match_id, coach_rows)
2) replace_match_coaches(match_id, home_team_id, away_team_id, home_coaches, away_coaches)
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"DELETE FROM match_coaches WHERE match_id = %s",
(match_id,),
)
if len(args) == 1:
coach_rows = args[0] or []
sort_counters = {
"home": 1,
"away": 1,
}
for coach in coach_rows:
side = coach.get("side")
if side not in ("home", "away"):
team_side = coach.get("team_id")
if team_side in ("home", "away"):
side = team_side
else:
continue
coach_id = coach.get("coach_id")
if not coach_id:
continue
cur.execute(
"""
INSERT INTO match_coaches (
match_id,
side,
sort_order,
coach_id,
role
)
VALUES (%s, %s, %s, %s, %s)
""",
(
match_id,
side,
sort_counters[side],
coach_id,
coach.get("role") or coach.get("amplua") or None,
),
)
sort_counters[side] += 1
elif len(args) == 4:
_home_team_id, _away_team_id, home_coaches, away_coaches = args
def insert_side(side: str, coaches: list[dict]) -> None:
sort_order = 1
for coach in coaches or []:
coach_id = coach.get("coach_id")
if not coach_id:
continue
cur.execute(
"""
INSERT INTO match_coaches (
match_id,
side,
sort_order,
coach_id,
amplua
)
VALUES (%s, %s, %s, %s, %s)
""",
(
match_id,
side,
sort_order,
coach_id,
coach.get("role") or coach.get("amplua") or None,
),
)
sort_order += 1
insert_side("home", home_coaches)
insert_side("away", away_coaches)
else:
raise TypeError(
"replace_match_coaches() expected either "
"(match_id, coach_rows) or "
"(match_id, home_team_id, away_team_id, home_coaches, away_coaches)"
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()

View File

@@ -0,0 +1,184 @@
from db import get_connection
def create_event(
match_id,
side,
type_,
player_name,
minute,
seconds,
meta=None,
player_id=None,
player_out_id=None,
player_in_id=None,
):
query = """
INSERT INTO match_events_ui (
match_id,
side,
type,
player_name,
minute,
seconds,
meta,
player_id,
player_out_id,
player_in_id,
created_at
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW())
RETURNING id;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
query,
(
match_id,
side,
type_,
player_name,
minute,
seconds,
meta,
player_id,
player_out_id,
player_in_id,
),
)
event_id = cur.fetchone()[0]
conn.commit()
return event_id
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_events(match_id):
query = """
SELECT
id,
side,
type,
player_name,
minute,
seconds,
meta,
player_id,
player_out_id,
player_in_id
FROM match_events_ui
WHERE match_id = %s
ORDER BY seconds, id;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (match_id,))
rows = cur.fetchall()
return [
{
"id": r[0],
"side": r[1],
"type": r[2],
"player_name": r[3],
"minute": r[4],
"seconds": r[5],
"meta": r[6],
"player_id": r[7],
"player_out_id": r[8],
"player_in_id": r[9],
}
for r in rows
]
finally:
conn.close()
def update_event(
event_id,
side,
type_,
player_name,
minute,
seconds,
meta=None,
player_id=None,
player_out_id=None,
player_in_id=None,
):
query = """
UPDATE match_events_ui
SET
side = %s,
type = %s,
player_name = %s,
minute = %s,
seconds = %s,
meta = %s,
player_id = %s,
player_out_id = %s,
player_in_id = %s
WHERE id = %s;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
query,
(
side,
type_,
player_name,
minute,
seconds,
meta,
player_id,
player_out_id,
player_in_id,
event_id,
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def delete_event(event_id):
query = "DELETE FROM match_events_ui WHERE id = %s;"
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (event_id,))
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def clear_events(match_id):
query = "DELETE FROM match_events_ui WHERE match_id = %s;"
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (match_id,))
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()

View File

@@ -0,0 +1,369 @@
from itertools import count
from db import get_connection
FORMATION_PRESETS = {
"4-4-2": {
"gk": [{"x": 50, "y": 10}],
"def": [
{"x": 18, "y": 28},
{"x": 39, "y": 24},
{"x": 61, "y": 24},
{"x": 82, "y": 28},
],
"mid": [
{"x": 18, "y": 48},
{"x": 39, "y": 44},
{"x": 61, "y": 44},
{"x": 82, "y": 48},
],
"fwd": [
{"x": 38, "y": 70},
{"x": 62, "y": 70},
],
},
"4-3-3": {
"gk": [{"x": 50, "y": 10}],
"def": [
{"x": 18, "y": 28},
{"x": 39, "y": 24},
{"x": 61, "y": 24},
{"x": 82, "y": 28},
],
"mid": [
{"x": 30, "y": 47},
{"x": 50, "y": 42},
{"x": 70, "y": 47},
],
"fwd": [
{"x": 20, "y": 72},
{"x": 50, "y": 66},
{"x": 80, "y": 72},
],
},
"4-2-3-1": {
"gk": [{"x": 0, "y": 0}],
"def": [
{"x": 18, "y": 28},
{"x": 39, "y": 24},
{"x": 61, "y": 24},
{"x": 82, "y": 28},
],
"mid": [
{"x": 35, "y": 42}, # опорник
{"x": 65, "y": 42}, # опорник
{"x": 20, "y": 58}, # левый
{"x": 50, "y": 52}, # центр
{"x": 80, "y": 58}, # правый
],
"fwd": [
{"x": 50, "y": 72},
],
},
"3-5-2": {
"gk": [{"x": 50, "y": 10}],
"def": [
{"x": 30, "y": 26},
{"x": 50, "y": 22},
{"x": 70, "y": 26},
],
"mid": [
{"x": 10, "y": 50}, # левый фланг
{"x": 35, "y": 46},
{"x": 50, "y": 42},
{"x": 65, "y": 46},
{"x": 90, "y": 50}, # правый фланг
],
"fwd": [
{"x": 38, "y": 72},
{"x": 62, "y": 72},
],
},
"5-3-2": {
"gk": [{"x": 50, "y": 10}],
"def": [
{"x": 10, "y": 30}, # левый латераль
{"x": 30, "y": 26},
{"x": 50, "y": 22},
{"x": 70, "y": 26},
{"x": 90, "y": 30}, # правый латераль
],
"mid": [
{"x": 30, "y": 48},
{"x": 50, "y": 44},
{"x": 70, "y": 48},
],
"fwd": [
{"x": 38, "y": 72},
{"x": 62, "y": 72},
],
},
}
def detect_player_line(position: str) -> str:
pos = (position or "").strip().lower()
if not pos:
return "mid"
if "вр" in pos or "gk" in pos or "goalkeeper" in pos:
return "gk"
defender_markers = ["цз", "лз", "пз", "з", "def", "cb", "lb", "rb", "wb"]
midfielder_markers = [
"цп",
"цоп",
"оп",
"п",
"пзщ",
"mid",
"cm",
"dm",
"am",
"lm",
"rm",
]
forward_markers = ["н", "цф", "ф", "lf", "rf", "fw", "st", "cf", "нап"]
if any(marker in pos for marker in defender_markers):
return "def"
if any(marker in pos for marker in midfielder_markers):
return "mid"
if any(marker in pos for marker in forward_markers):
return "fwd"
return "mid"
def get_match_formations(match_id: int, team_id: int) -> list[dict]:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
mf.player_id,
mf.player_name,
mf.number,
mf.position,
mf.is_captain,
mf.x,
mf.y,
p.last_name
FROM match_formations mf
LEFT JOIN players p ON p.id = mf.player_id
WHERE mf.match_id = %s
AND mf.team_id = %s
ORDER BY mf.id
""",
(match_id, team_id),
)
rows = cur.fetchall()
return [
{
"player_id": row[0],
"player_name": row[1] or "",
"number": row[2] or "",
"position": row[3] or "",
"is_captain": bool(row[4]),
"x": float(row[5]),
"y": float(row[6]),
"last_name": row[7] or "",
}
for row in rows
]
finally:
conn.close()
def replace_match_formations(match_id: int, team_id: int, players: list[dict]) -> None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
DELETE FROM match_formations
WHERE match_id = %s AND team_id = %s
""",
(match_id, team_id),
)
for player in players:
cur.execute(
"""
INSERT INTO match_formations (
match_id,
team_id,
player_id,
player_name,
number,
position,
is_captain,
x,
y,
created_at,
updated_at
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
""",
(
match_id,
team_id,
player.get("player_id"),
(player.get("player_name") or "").strip(),
(player.get("number") or "").strip(),
(player.get("position") or "").strip(),
bool(player.get("is_captain")),
float(player.get("x", 50)),
float(player.get("y", 50)),
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def parse_position(position: str) -> tuple[str, str]:
"""
Возвращает:
line: gk / def / mid / fwd
side: left / center / right
"""
pos = (position or "").strip().lower()
if not pos:
return "mid", "center"
# Вратарь
if "вр" in pos or "gk" in pos or "goalkeeper" in pos:
return "gk", "center"
# Сторона
if pos.startswith("л"):
side = "left"
elif pos.startswith("п"):
side = "right"
else:
side = "center"
# Линия
# Защита: ЛЗ, ПЗ, ЦЗ, ЛЦЗ, ПЦЗ и т.п.
if "з" in pos:
return "def", side
# Нападение: Н, Ф, ЦФ, ЛФ, ПФ и т.п.
if "ф" in pos or "н" in pos:
return "fwd", side
# Полузащита: П, ЦП, ЦОП, ЛП, ПП, ПЦП, ЛЦП и т.п.
if "п" in pos:
return "mid", side
return "mid", side
def sort_players_by_side(players: list[dict]) -> list[dict]:
side_order = {
"left": 0,
"center": 1,
"right": 2,
}
return sorted(
players,
key=lambda p: (
side_order.get(parse_position(p.get("position", ""))[1], 1),
str(p.get("number", "")),
str(p.get("last_name", "") or p.get("player_name", "")),
),
)
def apply_formation_preset_to_players(
players: list[dict], preset_name: str
) -> list[dict]:
preset = FORMATION_PRESETS.get(preset_name)
if not preset:
raise ValueError(f"Unknown formation preset: {preset_name}")
players = players[:11]
gk, defs, mids, fwds = [], [], [], []
for p in players:
line, _ = parse_position(p.get("position"))
if line == "gk":
gk.append(p)
elif line == "def":
defs.append(p)
elif line == "mid":
mids.append(p)
elif line == "fwd":
fwds.append(p)
else:
mids.append(p)
defs = sort_players_by_side(defs)
mids = sort_players_by_side(mids)
fwds = sort_players_by_side(fwds)
leftovers = []
def trim_or_collect(group: list[dict], count: int) -> list[dict]:
if len(group) > count:
leftovers.extend(group[count:])
return group[:count]
return group
gk = trim_or_collect(gk, len(preset["gk"]))
defs = trim_or_collect(defs, len(preset["def"]))
mids = trim_or_collect(mids, len(preset["mid"]))
fwds = trim_or_collect(fwds, len(preset["fwd"]))
# если вратарь не найден — берём первого доступного
if not gk:
source = defs or mids or fwds or leftovers
if source:
gk = [source.pop(0)]
def fill(group: list[dict], count: int) -> list[dict]:
while len(group) < count and leftovers:
group.append(leftovers.pop(0))
return group
gk = fill(gk, len(preset["gk"]))
defs = fill(defs, len(preset["def"]))
mids = fill(mids, len(preset["mid"]))
fwds = fill(fwds, len(preset["fwd"]))
result = []
def assign(group: list[dict], coords: list[dict]):
for p, c in zip(group, coords):
result.append(
{
"player_id": p.get("player_id"),
"player_name": p.get("player_name") or "",
"last_name": p.get("last_name") or "",
"number": p.get("number") or "",
"position": p.get("position") or "",
"is_captain": bool(p.get("is_captain")),
"x": c["x"],
"y": c["y"],
}
)
assign(gk, preset["gk"])
assign(defs, preset["def"])
assign(mids, preset["mid"])
assign(fwds, preset["fwd"])
return result

View File

@@ -0,0 +1,460 @@
from db import get_connection
from repositories.match_view_repository import get_match_lineups_grouped
from repositories.match_coach_repository import get_match_coaches_grouped
def replace_match_lineups(match_id: int, rows: list[dict]) -> None:
delete_query = "DELETE FROM match_lineups WHERE match_id = %s;"
insert_query = """
INSERT INTO match_lineups (
match_id,
team_id,
player_id,
player_name,
number,
position,
is_captain,
lineup_type,
source,
created_at,
updated_at
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW());
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(delete_query, (match_id,))
for row in rows:
cur.execute(
insert_query,
(
row["match_id"],
row["team_id"],
row.get("player_id"),
row["player_name"],
row.get("number"),
row.get("position"),
row.get("is_captain"),
row["lineup_type"],
row.get("source", "parser"),
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def _player_to_editor_dict(p: dict) -> dict:
return {
"player_id": p.get("player_id"),
"player_name": p.get("player_name")
or f"{p.get('last_name', '')} {p.get('first_name', '')}".strip(),
"last_name": p.get("last_name", "") or p.get("player_name", "") or "",
"first_name": p.get("first_name", "") or "",
"number": str(p.get("number", "") or ""),
"position": p.get("position", "") or "",
"is_captain": bool(p.get("is_captain")),
}
def _coach_to_editor_dict(c: dict) -> dict:
return {
"coach_id": c.get("coach_id"),
"coach_name": c.get("coach_name", "") or "",
"role": c.get("role", "") or "",
}
def get_match_lineup_for_editor(
match_id: int,
home_team_id: int,
away_team_id: int,
) -> dict:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
mlp.side,
mlp.role,
mlp.sort_order,
p.id AS player_id,
COALESCE(p.full_name, TRIM(COALESCE(p.last_name, '') || ' ' || COALESCE(p.first_name, ''))) AS player_name,
COALESCE(p.last_name, '') AS last_name,
COALESCE(p.first_name, '') AS first_name,
COALESCE(mlp.number::text, '') AS number,
COALESCE(mlp.position, '') AS position,
COALESCE(mlp.is_captain, FALSE) AS is_captain
FROM match_lineup_players mlp
JOIN players p
ON p.id = mlp.player_id
WHERE mlp.match_id = %s
ORDER BY mlp.side, mlp.role, mlp.sort_order, mlp.id
""",
(match_id,),
)
player_rows = cur.fetchall()
cur.execute(
"""
SELECT
mc.side,
mc.sort_order,
c.id AS coach_id,
COALESCE(c.player, '') AS coach_name,
COALESCE(mc.role, c.amplua, '') AS role
FROM match_coaches mc
JOIN coaches c
ON c.id = mc.coach_id
WHERE mc.match_id = %s
ORDER BY mc.side, mc.sort_order, mc.id
""",
(match_id,),
)
coach_rows = cur.fetchall()
finally:
conn.close()
result = {
"home_starting": [],
"home_bench": [],
"away_starting": [],
"away_bench": [],
"home_coaches": [],
"away_coaches": [],
}
# Если редактор уже сохранял состав — берём игроков из новой таблицы
if player_rows:
for row in player_rows:
player = {
"player_id": row[3],
"player_name": row[4] or "",
"last_name": row[5] or "",
"first_name": row[6] or "",
"number": row[7] or "",
"position": row[8] or "",
"is_captain": bool(row[9]),
}
side = row[0]
role = row[1]
if side == "home" and role == "starting":
result["home_starting"].append(player)
elif side == "home" and role == "bench":
result["home_bench"].append(player)
elif side == "away" and role == "starting":
result["away_starting"].append(player)
elif side == "away" and role == "bench":
result["away_bench"].append(player)
else:
# Иначе берём загруженный с сайта состав из старой таблицы
lineups = get_match_lineups_grouped(
match_id=match_id,
home_team_id=home_team_id,
away_team_id=away_team_id,
)
result["home_starting"] = [
_player_to_editor_dict(p) for p in lineups.get("home_starting", [])
]
result["home_bench"] = [
_player_to_editor_dict(p) for p in lineups.get("home_bench", [])
]
result["away_starting"] = [
_player_to_editor_dict(p) for p in lineups.get("away_starting", [])
]
result["away_bench"] = [
_player_to_editor_dict(p) for p in lineups.get("away_bench", [])
]
# Тренеров берём из match_coaches, если они уже есть
if coach_rows:
for row in coach_rows:
coach = {
"coach_id": row[2],
"coach_name": row[3] or "",
"role": row[4] or "",
}
side = row[0]
if side == "home":
result["home_coaches"].append(coach)
elif side == "away":
result["away_coaches"].append(coach)
else:
coaches = get_match_coaches_grouped(
match_id=match_id,
home_team_id=home_team_id,
away_team_id=away_team_id,
)
result["home_coaches"] = [
_coach_to_editor_dict(c) for c in coaches.get("home", [])
]
result["away_coaches"] = [
_coach_to_editor_dict(c) for c in coaches.get("away", [])
]
return result
def get_match_lineup_for_vmix(
match_id: int,
home_team_id: int,
away_team_id: int,
) -> dict:
conn = get_connection()
try:
with conn.cursor() as cur:
# Сначала пробуем взять ручную версию из редактора
cur.execute(
"""
SELECT
mlp.side,
mlp.role,
mlp.sort_order,
p.id AS player_id,
COALESCE(
p.full_name,
TRIM(COALESCE(p.last_name, '') || ' ' || COALESCE(p.first_name, ''))
) AS player_name,
COALESCE(p.last_name, '') AS last_name,
COALESCE(p.first_name, '') AS first_name,
COALESCE(mlp.number::text, '') AS number,
COALESCE(mlp.position, '') AS position,
COALESCE(mlp.is_captain, FALSE) AS is_captain,
COALESCE(p.position, '') AS pos
FROM match_lineup_players mlp
JOIN players p
ON p.id = mlp.player_id
WHERE mlp.match_id = %s
ORDER BY
CASE
WHEN LOWER(COALESCE(p.position, '')) IN ('вр', 'вр.', 'gk', 'goalkeeper', 'вратарь') THEN 0
ELSE 1
END,
CASE
WHEN COALESCE(p.number::text, '') ~ '^[0-9]+$' THEN p.number::integer
ELSE 999
END,
COALESCE(p.last_name, ''),
COALESCE(p.first_name, ''),
p.id
""",
(match_id,),
)
player_rows = cur.fetchall()
cur.execute(
"""
SELECT
mc.side,
mc.sort_order,
c.id AS coach_id,
COALESCE(c.player, '') AS coach_name,
COALESCE(mc.role, c.amplua, '') AS role
FROM match_coaches mc
JOIN coaches c
ON c.id = mc.coach_id
WHERE mc.match_id = %s
ORDER BY mc.side, mc.sort_order, mc.id
""",
(match_id,),
)
coach_rows = cur.fetchall()
finally:
conn.close()
result = {
"home_starting": [],
"home_bench": [],
"away_starting": [],
"away_bench": [],
"home_coaches": [],
"away_coaches": [],
}
# 1. Если есть ручной состав — берём его
if player_rows:
for row in player_rows:
player = {
"player_id": row[3],
"player_name": row[4] or "",
"last_name": row[5] or "",
"first_name": row[6] or "",
"number": row[7] or "",
"position": row[8] or "",
"is_captain": bool(row[9]),
"pos": row[10] or "",
}
side = row[0]
role = row[1]
if side == "home" and role == "starting":
result["home_starting"].append(player)
elif side == "home" and role == "bench":
result["home_bench"].append(player)
elif side == "away" and role == "starting":
result["away_starting"].append(player)
elif side == "away" and role == "bench":
result["away_bench"].append(player)
else:
# 2. Иначе — fallback на сайт
lineups = get_match_lineups_grouped(
match_id=match_id,
home_team_id=home_team_id,
away_team_id=away_team_id,
)
result["home_starting"] = lineups.get("home_starting", [])
result["home_bench"] = lineups.get("home_bench", [])
result["away_starting"] = lineups.get("away_starting", [])
result["away_bench"] = lineups.get("away_bench", [])
if coach_rows:
for row in coach_rows:
coach = {
"coach_id": row[2],
"coach_name": row[3] or "",
"role": row[4] or "",
}
if row[0] == "home":
result["home_coaches"].append(coach)
elif row[0] == "away":
result["away_coaches"].append(coach)
else:
coaches = get_match_coaches_grouped(
match_id=match_id,
home_team_id=home_team_id,
away_team_id=away_team_id,
)
result["home_coaches"] = coaches.get("home", [])
result["away_coaches"] = coaches.get("away", [])
return result
def save_match_lineup_for_editor(
match_id: int,
home_team_id: int,
away_team_id: int,
home_starting: list[dict],
home_bench: list[dict],
away_starting: list[dict],
away_bench: list[dict],
home_coaches: list[dict],
away_coaches: list[dict],
) -> None:
def is_goalkeeper(position: str) -> bool:
if not position:
return False
return str(position).strip().lower() in {
"вр", "вр.", "вратарь", "gk", "goalkeeper"
}
def safe_number(value) -> int:
try:
return int(str(value).strip())
except Exception:
return 9999
def lineup_sort_key(player: dict):
return (
0 if is_goalkeeper(player.get("position", "")) else 1,
safe_number(player.get("number")),
(player.get("last_name") or player.get("player_name") or "").lower(),
(player.get("first_name") or "").lower(),
player.get("player_id") or 0,
)
# Пересортировка перед сохранением
home_starting = sorted(home_starting, key=lineup_sort_key)
home_bench = sorted(home_bench, key=lineup_sort_key)
away_starting = sorted(away_starting, key=lineup_sort_key)
away_bench = sorted(away_bench, key=lineup_sort_key)
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"DELETE FROM match_lineup_players WHERE match_id = %s",
(match_id,),
)
cur.execute(
"DELETE FROM match_coaches WHERE match_id = %s",
(match_id,),
)
def insert_players(side: str, role: str, players: list[dict]) -> None:
for sort_order, p in enumerate(players, start=1):
cur.execute(
"""
INSERT INTO match_lineup_players (
match_id,
side,
role,
sort_order,
player_id,
number,
position,
is_captain
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
""",
(
match_id,
side,
role,
sort_order,
p.get("player_id"),
p.get("number", "") or None,
p.get("position", "") or None,
bool(p.get("is_captain")),
),
)
def insert_coaches(side: str, coaches: list[dict]) -> None:
for sort_order, c in enumerate(coaches, start=1):
cur.execute(
"""
INSERT INTO match_coaches (
match_id,
side,
sort_order,
coach_id,
role
)
VALUES (%s, %s, %s, %s, %s)
""",
(
match_id,
side,
sort_order,
c.get("coach_id"),
c.get("role", "") or None,
),
)
insert_players("home", "starting", home_starting)
insert_players("home", "bench", home_bench)
insert_players("away", "starting", away_starting)
insert_players("away", "bench", away_bench)
insert_coaches("home", home_coaches)
insert_coaches("away", away_coaches)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()

View File

@@ -0,0 +1,130 @@
from db import get_connection
def replace_match_referees2(match_id: int, rows: list[dict]) -> None:
delete_query = "DELETE FROM match_referees WHERE match_id = %s;"
insert_query = """
INSERT INTO match_referees (
match_id,
referee_id,
referee_name,
role,
source,
created_at,
updated_at
)
VALUES (%s, %s, %s, %s, %s, NOW(), NOW());
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(delete_query, (match_id,))
for row in rows:
cur.execute(
insert_query,
(
row["match_id"],
row.get("referee_id"),
row["referee_name"],
row.get("role"),
row.get("source", "parser"),
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def replace_match_referees(match_id: int, rows: list[dict]) -> None:
delete_query = "DELETE FROM match_referees WHERE match_id = %s;"
insert_query = """
INSERT INTO match_referees (
match_id,
referee_id,
referee_name,
role,
source,
created_at,
updated_at
)
VALUES (%s, %s, %s, %s, %s, NOW(), NOW());
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(delete_query, (match_id,))
for row in rows or []:
referee_id = row.get("referee_id") or row.get("id")
referee_name = (
row.get("referee_name")
or row.get("full_name")
or row.get("name")
or ""
).strip()
role = (row.get("role") or "").strip()
if not referee_name or not role:
continue
cur.execute(
insert_query,
(
match_id,
referee_id,
referee_name,
role,
row.get("source", "admin"),
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_match_referees(match_id: int) -> list[dict]:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
mr.referee_name,
mr.role
FROM match_referees mr
WHERE mr.match_id = %s
ORDER BY
CASE mr.role
WHEN 'Главный судья' THEN 1
WHEN 'Ассистент судьи №1' THEN 2
WHEN 'Ассистент судьи №2' THEN 3
WHEN 'Резервный судья' THEN 4
WHEN 'Инспектор' THEN 5
WHEN 'Делегат' THEN 6
ELSE 99
END,
mr.referee_name
""",
(match_id,),
)
rows = cur.fetchall()
return [
{
"referee_name": row[0] or "",
"role": row[1] or "",
}
for row in rows
]
finally:
conn.close()

View File

@@ -0,0 +1,269 @@
from numpy import place
from db import get_connection
from repositories.team_repository import get_team_id_by_external_id
from repositories.stadium_repository import get_or_create_stadium
def upsert_match(
external_id: str,
home_team_id: int,
away_team_id: int,
match_date=None,
status: str = "scheduled",
home_score: int | None = None,
away_score: int | None = None,
tour: str | None = None,
season: str | None = None,
place: str | None = None,
stadium_id: int | None = None,
date_raw: str | None = None,
score_add: str | None = None,
) -> None:
query = """
INSERT INTO matches (
external_id,
home_team_id,
away_team_id,
match_date,
status,
home_score,
away_score,
tour,
season,
place,
stadium_id,
date_raw,
score_add,
created_at,
updated_at
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
ON CONFLICT (external_id)
DO UPDATE SET
home_team_id = EXCLUDED.home_team_id,
away_team_id = EXCLUDED.away_team_id,
match_date = EXCLUDED.match_date,
status = EXCLUDED.status,
home_score = EXCLUDED.home_score,
away_score = EXCLUDED.away_score,
tour = EXCLUDED.tour,
season = EXCLUDED.season,
place = EXCLUDED.place,
stadium_id = EXCLUDED.stadium_id,
date_raw = EXCLUDED.date_raw,
score_add = EXCLUDED.score_add,
updated_at = NOW();
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
query,
(
external_id,
home_team_id,
away_team_id,
match_date,
status,
home_score,
away_score,
tour,
season,
place,
stadium_id,
date_raw,
score_add,
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def upsert_match_by_team_external_ids(
external_id: str,
home_team_external_id: str,
away_team_external_id: str,
match_date=None,
status: str = "scheduled",
home_score: int | None = None,
away_score: int | None = None,
tour: str | None = None,
season: str | None = None,
place: str | None = None,
stadium_id: int | None = None,
date_raw: str | None = None,
score_add: str | None = None,
) -> None:
home_team_id = get_team_id_by_external_id(home_team_external_id)
away_team_id = get_team_id_by_external_id(away_team_external_id)
stadium_id = get_or_create_stadium(place)
if home_team_id is None:
raise ValueError(f"Home team not found by external_id: {home_team_external_id}")
if away_team_id is None:
raise ValueError(f"Away team not found by external_id: {away_team_external_id}")
upsert_match(
external_id=external_id,
home_team_id=home_team_id,
away_team_id=away_team_id,
match_date=match_date,
status=status,
home_score=home_score,
away_score=away_score,
tour=tour,
season=season,
place=place,
stadium_id=stadium_id,
date_raw=date_raw,
score_add=score_add,
)
def clear_match_squad_data(match_id: int) -> None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute("DELETE FROM match_lineup_players WHERE match_id = %s", (match_id,))
cur.execute("DELETE FROM match_coaches WHERE match_id = %s", (match_id,))
cur.execute("DELETE FROM match_lineups WHERE match_id = %s", (match_id,))
# если есть таблица судей матча
cur.execute("DELETE FROM match_referees WHERE match_id = %s", (match_id,))
# если есть матчевые расстановки
cur.execute("DELETE FROM match_formations WHERE match_id = %s", (match_id,))
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_match_by_external_id(external_id: str):
query = """
SELECT id, external_id, home_team_id, away_team_id
FROM matches
WHERE external_id = %s
LIMIT 1;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (str(external_id).strip(),))
return cur.fetchone()
finally:
conn.close()
def mark_match_parsed(external_id: str) -> None:
query = """
UPDATE matches
SET parsed = TRUE,
parse_error = NULL,
updated_at = NOW()
WHERE external_id = %s;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (str(external_id).strip(),))
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def mark_match_parse_error(external_id: str, error_text: str) -> None:
query = """
UPDATE matches
SET parsed = FALSE,
parse_error = %s,
updated_at = NOW()
WHERE external_id = %s;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (str(error_text)[:2000], str(external_id).strip()))
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_tour_schedule_by_match_id(match_id: int) -> list[dict]:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT m.tour
FROM matches m
WHERE m.id = %s
""",
(match_id,),
)
row = cur.fetchone()
if not row or not row[0]:
return []
tour = row[0]
cur.execute(
"""
SELECT
m.id,
m.external_id,
m.match_date,
m.place,
m.status,
m.home_score,
m.away_score,
ht.name AS home_team_name,
at.name AS away_team_name
FROM matches m
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
ORDER BY
m.match_date NULLS LAST,
m.id
""",
(tour,),
)
rows = cur.fetchall()
return [
{
"match_id": r[0],
"match_external_id": r[1],
"match_date": r[2],
"stadium_name": r[3] or "",
"status": r[4] or "",
"home_score": r[5],
"away_score": r[6],
"home_team_name": r[7] or "",
"away_team_name": r[8] or "",
}
for r in rows
]
finally:
conn.close()

View File

@@ -0,0 +1,209 @@
import secrets
from db import get_connection
def create_match_session(
match_id: int,
operator_name: str | None = None,
vmix_project_path: str | None = None,
):
session_token = secrets.token_urlsafe(24)
query = """
INSERT INTO match_sessions (
match_id,
operator_name,
session_token,
vmix_project_path,
is_active,
created_at,
updated_at
)
VALUES (%s, %s, %s, %s, TRUE, NOW(), NOW())
RETURNING id, match_id, operator_name, session_token, vmix_project_path, is_active, created_at, updated_at;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
query, (match_id, operator_name, session_token, vmix_project_path)
)
row = cur.fetchone()
conn.commit()
return row
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_match_session_by_token(session_token: str):
query = """
SELECT
ms.id,
ms.match_id,
ms.operator_name,
ms.session_token,
ms.vmix_project_path,
ms.is_active,
ms.created_at,
ms.updated_at,
m.external_id AS match_external_id,
m.match_date,
m.tour,
m.season,
m.place,
ht.id AS home_team_id,
ht.name AS home_team_name,
ht.logo_url AS home_team_logo,
at.id AS away_team_id,
at.name AS away_team_name,
at.logo_url AS away_team_logo
FROM match_sessions ms
JOIN matches m ON m.id = ms.match_id
JOIN teams ht ON ht.id = m.home_team_id
JOIN teams at ON at.id = m.away_team_id
WHERE ms.session_token = %s
LIMIT 1;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (session_token,))
return cur.fetchone()
finally:
conn.close()
def deactivate_match_session(session_token: str) -> None:
query = """
UPDATE match_sessions
SET is_active = FALSE,
updated_at = NOW()
WHERE session_token = %s;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (session_token,))
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def update_match_session_vmix_path(session_token: str, vmix_project_path: str) -> None:
query = """
UPDATE match_sessions
SET vmix_project_path = %s,
updated_at = NOW()
WHERE session_token = %s;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (vmix_project_path, session_token))
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_match_by_id(match_id: int):
query = """
SELECT
m.id,
m.external_id,
m.match_date,
m.tour,
m.season,
ht.name AS home_team_name,
at.name AS away_team_name
FROM matches m
JOIN teams ht ON ht.id = m.home_team_id
JOIN teams at ON at.id = m.away_team_id
WHERE m.id = %s
LIMIT 1;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (match_id,))
return cur.fetchone()
finally:
conn.close()
def list_matches_for_admin(today_only: bool = False, tour: str | None = None):
query = """
SELECT
m.id,
m.external_id,
m.match_date,
m.status,
m.tour,
m.season,
ht.name AS home_team_name,
at.name AS away_team_name
FROM matches m
JOIN teams ht ON ht.id = m.home_team_id
JOIN teams at ON at.id = m.away_team_id
WHERE 1=1
"""
params = []
if today_only:
query += " AND DATE(m.match_date) = CURRENT_DATE "
if tour:
query += " AND m.tour = %s "
params.append(tour)
query += " ORDER BY m.match_date ASC NULLS LAST, m.id ASC; "
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, params)
return cur.fetchall()
finally:
conn.close()
def list_available_tours():
query = """
SELECT DISTINCT tour
FROM matches
WHERE tour IS NOT NULL AND tour <> '';
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query)
tours = [row[0] for row in cur.fetchall()]
def extract_tour_number(value: str):
value = str(value).strip()
digits = "".join(ch for ch in value if ch.isdigit())
return int(digits) if digits else 999999
tours.sort(key=lambda x: (extract_tour_number(x), str(x)))
return tours
finally:
conn.close()

View File

@@ -0,0 +1,96 @@
from db import get_connection
def get_match_lineups_grouped(match_id: int, home_team_id: int, away_team_id: int):
query = """
SELECT
ml.team_id,
ml.player_id,
ml.player_name,
ml.number,
ml.position,
ml.lineup_type,
ml.is_captain,
p.last_name,
p.first_name,
p.position as pos
FROM match_lineups ml
LEFT JOIN players p ON p.id = ml.player_id
WHERE ml.match_id = %s;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (match_id,))
rows = cur.fetchall()
finally:
conn.close()
grouped = {
"home_starting": [],
"away_starting": [],
"home_bench": [],
"away_bench": [],
}
for row in rows:
(
team_id,
player_id,
player_name,
number,
position,
lineup_type,
is_captain,
last_name,
first_name,
pos,
) = row
item = {
"player_id": player_id,
"number": number or "",
"last_name": last_name or "",
"first_name": first_name or "",
"player_name": player_name or "",
"position": position or "",
"is_captain": bool(is_captain),
"pos": pos,
}
if team_id == home_team_id and lineup_type == "starting":
grouped["home_starting"].append(item)
elif team_id == away_team_id and lineup_type == "starting":
grouped["away_starting"].append(item)
elif team_id == home_team_id and lineup_type == "bench":
grouped["home_bench"].append(item)
elif team_id == away_team_id and lineup_type == "bench":
grouped["away_bench"].append(item)
def is_goalkeeper(position: str) -> int:
pos = (position or "").strip().lower()
goalkeeper_values = {"вр.", "вр", "вратарь", "goalkeeper", "gk"}
return 0 if pos in goalkeeper_values else 1
def player_number_value(number: str) -> int:
number = str(number or "").strip()
return int(number) if number.isdigit() else 999
def sort_players(players: list[dict]) -> list[dict]:
return sorted(
players,
key=lambda p: (
is_goalkeeper(p.get("position", "")),
player_number_value(p.get("number", "")),
p.get("last_name", "") or p.get("player_name", ""),
p.get("first_name", ""),
),
)
grouped["home_starting"] = sort_players(grouped["home_starting"])
grouped["away_starting"] = sort_players(grouped["away_starting"])
grouped["home_bench"] = sort_players(grouped["home_bench"])
grouped["away_bench"] = sort_players(grouped["away_bench"])
return grouped

View File

@@ -0,0 +1,373 @@
from db import get_connection
from repositories.team_repository import get_team_id_by_external_id
def normalize_birth_date_for_input(value) -> str:
if not value:
return ""
text = str(value).strip()
if not text:
return ""
# Уже нормальный формат для input[type=date]
if len(text) == 10 and text[4] == "-" and text[7] == "-":
return text
# Формат ДД.ММ.ГГГГ -> YYYY-MM-DD
if len(text) == 10 and text[2] == "." and text[5] == ".":
dd, mm, yyyy = text.split(".")
return f"{yyyy}-{mm}-{dd}"
return ""
def upsert_player(
external_id: str,
team_external_id: str,
player: str,
lastname: str = "",
name: str = "",
number: str = "",
pos: str = "",
amplua: str = "",
born: str = "",
games: int = 0,
goals: int = 0,
penaltys: int = 0,
assists: int = 0,
yellows: int = 0,
reds: int = 0,
is_active: bool = True,
) -> None:
team_id = get_team_id_by_external_id(str(team_external_id).strip())
if team_id is None:
raise ValueError(f"Team not found by external_id: {team_external_id}")
query = """
INSERT INTO players (
external_id,
team_id,
full_name,
first_name,
last_name,
number,
position,
is_active,
pos,
amplua,
born,
games,
goals,
penaltys,
assists,
yellows,
reds,
created_at,
updated_at
)
VALUES (
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
NOW(), NOW()
)
ON CONFLICT (external_id)
DO UPDATE SET
team_id = EXCLUDED.team_id,
full_name = EXCLUDED.full_name,
first_name = EXCLUDED.first_name,
last_name = EXCLUDED.last_name,
number = EXCLUDED.number,
position = EXCLUDED.position,
is_active = EXCLUDED.is_active,
pos = EXCLUDED.pos,
amplua = EXCLUDED.amplua,
born = EXCLUDED.born,
games = EXCLUDED.games,
goals = EXCLUDED.goals,
penaltys = EXCLUDED.penaltys,
assists = EXCLUDED.assists,
yellows = EXCLUDED.yellows,
reds = EXCLUDED.reds,
updated_at = NOW();
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
query,
(
str(external_id).strip(),
team_id,
player.strip(),
name.strip(),
lastname.strip(),
str(number).strip(),
amplua.strip(),
is_active,
pos.strip(),
amplua.strip(),
born.strip(),
games,
goals,
penaltys,
assists,
yellows,
reds,
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_match_by_external_id(external_id: str):
query = """
SELECT id, external_id, home_team_id, away_team_id
FROM matches
WHERE external_id = %s
LIMIT 1;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (str(external_id).strip(),))
return cur.fetchone()
finally:
conn.close()
def mark_match_parsed(external_id: str) -> None:
query = """
UPDATE matches
SET parsed = TRUE,
parse_error = NULL,
updated_at = NOW()
WHERE external_id = %s;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (str(external_id).strip(),))
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def mark_match_parse_error(external_id: str, error_text: str) -> None:
query = """
UPDATE matches
SET parsed = FALSE,
parse_error = %s,
updated_at = NOW()
WHERE external_id = %s;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (str(error_text)[:2000], str(external_id).strip()))
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_player_id_by_name_and_team(full_name: str, team_id: int) -> int | None:
query = """
SELECT id
FROM players
WHERE team_id = %s
AND LOWER(full_name) = LOWER(%s)
LIMIT 1;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (team_id, full_name.strip()))
row = cur.fetchone()
return row[0] if row else None
finally:
conn.close()
def get_player_id_by_external_id(external_id: str) -> int | None:
query = """
SELECT id
FROM players
WHERE external_id = %s
LIMIT 1;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (str(external_id).strip(),))
row = cur.fetchone()
return row[0] if row else None
finally:
conn.close()
from db import get_connection
def search_players_for_admin(q: str = "") -> list[dict]:
conn = get_connection()
try:
with conn.cursor() as cur:
if q.strip():
pattern = f"%{q.strip()}%"
cur.execute(
"""
SELECT
p.id,
p.full_name,
p.first_name,
p.last_name,
p.external_id,
p.position
FROM players p
WHERE
p.full_name ILIKE %s
OR p.first_name ILIKE %s
OR p.last_name ILIKE %s
OR COALESCE(p.external_id, '') ILIKE %s
ORDER BY p.full_name ASC, p.id ASC
LIMIT 200
""",
(pattern, pattern, pattern, pattern),
)
else:
cur.execute(
"""
SELECT
p.id,
p.full_name,
p.first_name,
p.last_name,
p.external_id,
p.position
FROM players p
ORDER BY p.id DESC
LIMIT 200
"""
)
rows = cur.fetchall()
return [
{
"id": row[0],
"full_name": row[1] or "",
"first_name": row[2] or "",
"last_name": row[3] or "",
"external_id": row[4] or "",
"position": row[5] or "",
}
for row in rows
]
finally:
conn.close()
def get_player_by_id(player_id: int) -> dict | None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
p.id,
p.full_name,
p.first_name,
p.last_name,
p.external_id,
p.position,
p.born,
p.photo,
p.video
FROM players p
WHERE p.id = %s
LIMIT 1
""",
(player_id,),
)
row = cur.fetchone()
if not row:
return None
return {
"id": row[0],
"full_name": row[1] or "",
"first_name": row[2] or "",
"last_name": row[3] or "",
"external_id": row[4] or "",
"position": row[5] or "",
"birth_date": normalize_birth_date_for_input(row[6]),
"photo": row[7] or "",
"video": row[8] or "",
}
finally:
conn.close()
def update_player_admin(
player_id: int,
full_name: str = "",
first_name: str = "",
last_name: str = "",
external_id: str = "",
position: str = "",
birth_date: str = "",
photo: str = "",
video: str = "",
) -> None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE players
SET
full_name = %s,
first_name = %s,
last_name = %s,
external_id = NULLIF(%s, ''),
position = %s,
born = NULLIF(%s, '')::date,
photo = NULLIF(%s, ''),
video = NULLIF(%s, '')
WHERE id = %s
""",
(
full_name.strip(),
first_name.strip(),
last_name.strip(),
external_id.strip(),
position.strip(),
birth_date.strip(),
photo.strip(),
video.strip(),
player_id,
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()

View File

@@ -0,0 +1,268 @@
from db import get_connection
def upsert_referee(
full_name: str,
lastname: str = "",
name: str = "",
middle_name: str = "",
city: str = "",
external_id: str | None = None,
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 (%s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
ON CONFLICT (external_id)
DO UPDATE SET
full_name = EXCLUDED.full_name,
lastname = EXCLUDED.lastname,
name = EXCLUDED.name,
middle_name = EXCLUDED.middle_name,
city = EXCLUDED.city,
is_active = EXCLUDED.is_active,
updated_at = NOW()
RETURNING id;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
query,
(
str(external_id).strip() if external_id else None,
full_name.strip(),
lastname.strip(),
name.strip(),
middle_name.strip(),
city.strip(),
is_active,
),
)
row = cur.fetchone()
conn.commit()
return row[0]
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_referee_id_by_name(full_name: str) -> int | None:
query = """
SELECT id
FROM referees
WHERE LOWER(full_name) = LOWER(%s)
LIMIT 1;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (full_name.strip(),))
row = cur.fetchone()
return row[0] if row else None
finally:
conn.close()
def search_referees_for_admin(q: str = "") -> list[dict]:
conn = get_connection()
try:
with conn.cursor() as cur:
if q.strip():
pattern = f"%{q.strip()}%"
cur.execute(
"""
SELECT
r.id,
r.full_name,
r.external_id
FROM referees r
WHERE
r.full_name ILIKE %s
OR COALESCE(r.external_id, '') ILIKE %s
ORDER BY r.full_name ASC, r.id ASC
LIMIT 200
""",
(pattern, pattern),
)
else:
cur.execute(
"""
SELECT
r.id,
r.full_name,
r.external_id
FROM referees r
ORDER BY r.id DESC
LIMIT 200
"""
)
rows = cur.fetchall()
return [
{
"id": row[0],
"full_name": row[1] or "",
"external_id": row[2] or "",
}
for row in rows
]
finally:
conn.close()
def get_referee_by_id(referee_id: int) -> dict | None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
r.id,
r.full_name,
r.external_id
FROM referees r
WHERE r.id = %s
LIMIT 1
""",
(referee_id,),
)
row = cur.fetchone()
if not row:
return None
return {
"id": row[0],
"full_name": row[1] or "",
"external_id": row[2] or "",
}
finally:
conn.close()
def update_referee_admin(
referee_id: int,
full_name: str = "",
external_id: str = "",
) -> None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE referees
SET
full_name = %s,
external_id = NULLIF(%s, '')
WHERE id = %s
""",
(
full_name.strip(),
external_id.strip(),
referee_id,
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_all_referees() -> list[dict]:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
r.id,
r.full_name,
r.external_id
FROM referees r
WHERE COALESCE(r.is_active, TRUE) = TRUE
ORDER BY r.full_name ASC, r.id ASC
"""
)
rows = cur.fetchall()
return [
{
"referee_id": row[0],
"id": row[0],
"referee_name": row[1] or "",
"full_name": row[1] or "",
"external_id": row[2] or "",
}
for row in rows
]
finally:
conn.close()
def replace_match_referees(match_id: int, rows: list[dict]) -> None:
delete_query = "DELETE FROM match_referees WHERE match_id = %s;"
insert_query = """
INSERT INTO match_referees (
match_id,
referee_id,
referee_name,
role,
source,
created_at,
updated_at
)
VALUES (%s, %s, %s, %s, %s, NOW(), NOW());
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(delete_query, (match_id,))
for row in rows or []:
referee_id = row.get("referee_id") or row.get("id")
referee_name = (
row.get("referee_name")
or row.get("full_name")
or row.get("name")
or ""
).strip()
role = (row.get("role") or "").strip()
if not referee_id or not referee_name or not role:
continue
cur.execute(
insert_query,
(
match_id,
referee_id,
referee_name,
role,
row.get("source", "admin"),
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()

View File

@@ -0,0 +1,291 @@
from db import get_connection
def search_stadiums_for_admin(q: str = "") -> list[dict]:
conn = get_connection()
try:
with conn.cursor() as cur:
if q.strip():
pattern = f"%{q.strip()}%"
cur.execute(
"""
SELECT
s.id,
s.name,
s.stadium_gfx,
s.city,
s.address,
s.external_id
FROM stadiums s
WHERE
s.name ILIKE %s
OR COALESCE(s.stadium_gfx, '') ILIKE %s
OR COALESCE(s.city, '') ILIKE %s
OR COALESCE(s.address, '') ILIKE %s
OR COALESCE(s.external_id, '') ILIKE %s
ORDER BY s.name ASC, s.id ASC
LIMIT 200
""",
(pattern, pattern, pattern, pattern, pattern),
)
else:
cur.execute(
"""
SELECT
s.id,
s.name,
s.stadium_gfx,
s.city,
s.address,
s.external_id
FROM stadiums s
ORDER BY s.id DESC
LIMIT 200
"""
)
rows = cur.fetchall()
return [
{
"id": row[0],
"name": row[1] or "",
"stadium_gfx": row[2] or "",
"city": row[3] or "",
"address": row[4] or "",
"external_id": row[5] or "",
}
for row in rows
]
finally:
conn.close()
def get_stadium_by_id(stadium_id: int) -> dict | None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
s.id,
s.name,
s.stadium_gfx,
s.city,
s.address,
s.external_id
FROM stadiums s
WHERE s.id = %s
LIMIT 1
""",
(stadium_id,),
)
row = cur.fetchone()
if not row:
return None
return {
"id": row[0],
"name": row[1] or "",
"stadium_gfx": row[2] or "",
"city": row[3] or "",
"address": row[4] or "",
"external_id": row[5] or "",
}
finally:
conn.close()
def get_stadium_by_name(name: str):
stadium_name = (name or "").strip()
if not stadium_name:
return None
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
s.id,
s.name,
s.stadium_gfx,
s.city,
s.address,
s.external_id
FROM stadiums s
WHERE LOWER(s.name) = LOWER(%s)
LIMIT 1
""",
(stadium_name,),
)
return cur.fetchone()
finally:
conn.close()
def get_stadium_by_external_id(external_id: str):
ext_id = (external_id or "").strip()
if not ext_id:
return None
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
s.id,
s.name,
s.stadium_gfx,
s.city,
s.address,
s.external_id
FROM stadiums s
WHERE s.external_id = %s
LIMIT 1
""",
(ext_id,),
)
return cur.fetchone()
finally:
conn.close()
def create_stadium(
name: str,
stadium_gfx: str | None = None,
external_id: str | None = None,
city: str | None = None,
address: str | None = None,
) -> int:
stadium_name = (name or "").strip()
if not stadium_name:
raise ValueError("Stadium name is required")
ext_value = (external_id or "").strip() or None
city_value = (city or "").strip() or None
address_value = (address or "").strip() or None
gfx_value = (stadium_gfx or "").strip() or None
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO stadiums (
name,
stadium_gfx,
external_id,
city,
address,
created_at,
updated_at
)
VALUES (%s, %s, %s, %s, %s, NOW(), NOW())
RETURNING id
""",
(
stadium_name,
gfx_value,
ext_value,
city_value,
address_value,
),
)
stadium_id = cur.fetchone()[0]
conn.commit()
return stadium_id
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_or_create_stadium(
name: str,
external_id: str | None = None,
city: str | None = None,
address: str | None = None,
) -> int | None:
stadium_name = (name or "").strip()
if not stadium_name:
return None
ext_id = (external_id or "").strip()
if ext_id:
existing_by_external = get_stadium_by_external_id(ext_id)
if existing_by_external:
return existing_by_external[0]
existing_by_name = get_stadium_by_name(stadium_name)
if existing_by_name:
return existing_by_name[0]
return create_stadium(
name=stadium_name,
stadium_gfx=stadium_name,
external_id=ext_id or None,
city=city,
address=address,
)
def update_stadium_admin(
stadium_id: int,
stadium_gfx: str = "",
city: str = "",
address: str = "",
external_id: str = "",
) -> None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE stadiums
SET
stadium_gfx = %s,
city = %s,
address = %s,
external_id = NULLIF(%s, ''),
updated_at = NOW()
WHERE id = %s
""",
(
stadium_gfx.strip(),
city.strip(),
address.strip(),
external_id.strip(),
stadium_id,
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_stadium_display_name_by_id(stadium_id: int) -> str | None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT COALESCE(NULLIF(stadium_gfx, ''), name)
FROM stadiums
WHERE id = %s
LIMIT 1
""",
(stadium_id,),
)
row = cur.fetchone()
return row[0] if row else None
finally:
conn.close()

View File

@@ -0,0 +1,153 @@
from db import get_connection
from repositories.team_repository import get_team_id_by_external_id
def replace_standings_for_season(season: str, standings_rows: list[dict]) -> None:
delete_query = """
DELETE FROM standings
WHERE season = %s;
"""
insert_query = """
INSERT INTO standings (
team_id,
season,
played,
wins,
losses,
draws,
points_for,
points_against,
points,
position,
created_at,
updated_at
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW());
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(delete_query, (season,))
for row in standings_rows:
team_external_id = row["team_external_id"]
team_id = get_team_id_by_external_id(team_external_id)
if team_id is None:
raise ValueError(f"Team not found by external_id: {team_external_id}")
cur.execute(
insert_query,
(
team_id,
season,
row.get("played", 0),
row.get("wins", 0),
row.get("losses", 0),
row.get("draws", 0),
row.get("points_for", 0),
row.get("points_against", 0),
row.get("points", 0),
row.get("position"),
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_standings_by_season(season: str):
query = """
SELECT
s.id,
s.team_id,
t.name AS team_name,
s.season,
s.played,
s.wins,
s.losses,
s.draws,
s.points_for,
s.points_against,
s.points,
s.position,
s.created_at,
s.updated_at
FROM standings s
JOIN teams t ON t.id = s.team_id
WHERE s.season = %s
ORDER BY s.position ASC NULLS LAST, s.id ASC;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (season,))
return cur.fetchall()
finally:
conn.close()
def get_standings_by_match_id(match_id: int) -> list[dict]:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT m.season, m.tour
FROM matches m
WHERE m.id = %s
""",
(match_id,),
)
base = cur.fetchone()
if not base:
return []
season = base[0]
cur.execute(
"""
SELECT
s.position,
t.logo_url,
t.name,
s.played,
s.wins,
s.draws,
s.losses,
s.points_for,
s.points_against,
s.points
FROM standings s
JOIN teams t ON t.id = s.team_id
WHERE s.season = %s
ORDER BY s.position ASC, s.points DESC, s.team_id ASC
""",
(season,),
)
rows = cur.fetchall()
return [
{
"position": r[0],
"team_logo": r[1] or "",
"team_name": r[2] or "",
"played": r[3] or 0,
"wins": r[4] or 0,
"draws": r[5] or 0,
"losses": r[6] or 0,
"goals_for": r[7] or 0,
"goals_against": r[8] or 0,
"points": r[9] or 0,
}
for r in rows
]
finally:
conn.close()

View File

@@ -0,0 +1,50 @@
from repositories.match_coach_repository import get_match_coaches_grouped
from db import get_connection
def _normalize_coach(c: dict) -> dict:
return {
"coach_id": c.get("coach_id"),
"coach_name": c.get("coach_name", "") or "",
"role": c.get("role", "") or "",
}
def get_team_coaches_for_match_editor(
team_id: int,
match_id: int | None = None,
home_team_id: int | None = None,
away_team_id: int | None = None,
) -> list[dict]:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
c.id AS coach_id,
COALESCE(c.player, c.name, '') AS coach_name,
COALESCE(c.amplua, '') AS role
FROM coaches c
WHERE c.team_id = %s
ORDER BY
CASE
WHEN LOWER(COALESCE(c.amplua, '')) LIKE '%%глав%%' THEN 0
ELSE 1
END,
COALESCE(c.player, c.name, ''),
c.id
""",
(team_id,),
)
rows = cur.fetchall()
return [
{
"coach_id": row[0],
"coach_name": row[1] or "",
"role": row[2] or "",
}
for row in rows
]
finally:
conn.close()

View File

@@ -0,0 +1,240 @@
from db import get_connection
def upsert_team(
external_id: str,
name: str,
short_name: str | None = None,
logo_url: str | None = None,
games: int | None = 0,
wins: int | None = 0,
goals: int | None = 0,
tournaments: int | None = 0,
) -> None:
query = """
INSERT INTO teams (
external_id,
name,
short_name,
logo_url,
games,
wins,
goals,
tournaments,
created_at,
updated_at
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
ON CONFLICT (external_id)
DO UPDATE SET
name = EXCLUDED.name,
short_name = EXCLUDED.short_name,
logo_url = EXCLUDED.logo_url,
games = EXCLUDED.games,
wins = EXCLUDED.wins,
goals = EXCLUDED.goals,
tournaments = EXCLUDED.tournaments,
updated_at = NOW();
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
query,
(
external_id,
name,
short_name,
logo_url,
games,
wins,
goals,
tournaments,
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def get_team_external_id_by_name(name: str) -> str | None:
query = """
SELECT external_id
FROM teams
WHERE LOWER(name) = LOWER(%s)
LIMIT 1;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (name,))
row = cur.fetchone()
return row[0] if row else None
finally:
conn.close()
def get_team_id_by_external_id(external_id: str) -> int | None:
external_id = str(external_id).strip()
query = """
SELECT id
FROM teams
WHERE TRIM(external_id) = %s
LIMIT 1;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
print(f"[get_team_id_by_external_id] search external_id = '{external_id}'")
cur.execute(query, (external_id,))
row = cur.fetchone()
print(f"[get_team_id_by_external_id] result = {row}")
return row[0] if row else None
finally:
conn.close()
def search_teams_for_admin(q: str = "") -> list[dict]:
conn = get_connection()
try:
with conn.cursor() as cur:
if q.strip():
pattern = f"%{q.strip()}%"
cur.execute(
"""
SELECT
t.id,
t.name,
t.full_name,
t.short_name_3,
t.city,
t.logo_path,
t.external_id
FROM teams t
WHERE
t.name ILIKE %s
OR COALESCE(t.full_name, '') ILIKE %s
OR COALESCE(t.short_name_3, '') ILIKE %s
OR COALESCE(t.city, '') ILIKE %s
OR COALESCE(t.external_id, '') ILIKE %s
ORDER BY t.name ASC, t.id ASC
LIMIT 200
""",
(pattern, pattern, pattern, pattern, pattern),
)
else:
cur.execute(
"""
SELECT
t.id,
t.name,
t.full_name,
t.short_name_3,
t.city,
t.logo_path,
t.external_id
FROM teams t
ORDER BY t.id DESC
LIMIT 200
"""
)
rows = cur.fetchall()
return [
{
"id": row[0],
"name": row[1] or "",
"full_name": row[2] or "",
"short_name_3": row[3] or "",
"city": row[4] or "",
"logo_path": row[5] or "",
"external_id": row[6] or "",
}
for row in rows
]
finally:
conn.close()
def get_team_by_id(team_id: int) -> dict | None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
t.id,
t.name,
t.full_name,
t.short_name_3,
t.city,
t.logo_path,
t.external_id
FROM teams t
WHERE t.id = %s
LIMIT 1
""",
(team_id,),
)
row = cur.fetchone()
if not row:
return None
return {
"id": row[0],
"name": row[1] or "",
"full_name": row[2] or "",
"short_name_3": row[3] or "",
"city": row[4] or "",
"logo_path": row[5] or "",
"external_id": row[6] or "",
}
finally:
conn.close()
def update_team_admin(
team_id: int,
name: str = "",
full_name: str = "",
short_name_3: str = "",
city: str = "",
logo_path: str = "",
external_id: str = "",
) -> None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE teams
SET
name = %s,
full_name = %s,
short_name_3 = %s,
city = NULLIF(%s, ''),
logo_path = %s,
external_id = NULLIF(%s, '')
WHERE id = %s
""",
(
name.strip(),
full_name.strip(),
short_name_3.strip().upper(),
city.strip(),
logo_path.strip(),
external_id.strip(),
team_id,
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()

View File

@@ -0,0 +1,58 @@
from db import get_connection
def get_team_players_for_match_editor(
team_id: int,
match_id: int | None = None,
home_team_id: int | None = None,
away_team_id: int | None = None,
) -> list[dict]:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
p.id AS player_id,
COALESCE(
p.full_name,
TRIM(COALESCE(p.last_name, '') || ' ' || COALESCE(p.first_name, ''))
) AS player_name,
COALESCE(p.last_name, '') AS last_name,
COALESCE(p.first_name, '') AS first_name,
COALESCE(p.number::text, '') AS number,
COALESCE(p.position, '') AS position,
FALSE AS is_captain
FROM players p
WHERE p.team_id = %s
ORDER BY
CASE
WHEN LOWER(COALESCE(p.position, '')) IN ('вр', 'вр.', 'gk', 'goalkeeper', 'вратарь') THEN 0
ELSE 1
END,
CASE
WHEN COALESCE(p.number::text, '') ~ '^[0-9]+$' THEN p.number::integer
ELSE 999
END,
COALESCE(p.last_name, ''),
COALESCE(p.first_name, ''),
p.id
""",
(team_id,),
)
rows = cur.fetchall()
return [
{
"player_id": row[0],
"player_name": row[1] or "",
"last_name": row[2] or "",
"first_name": row[3] or "",
"number": row[4] or "",
"position": row[5] or "",
"is_captain": bool(row[6]),
}
for row in rows
]
finally:
conn.close()