first commit
This commit is contained in:
0
repositories/__init__.py
Normal file
0
repositories/__init__.py
Normal file
BIN
repositories/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
repositories/__pycache__/audit_log_repository.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/audit_log_repository.cpython-312.pyc
Normal file
Binary file not shown.
BIN
repositories/__pycache__/auth_repository.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/auth_repository.cpython-312.pyc
Normal file
Binary file not shown.
BIN
repositories/__pycache__/coach_repository.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/coach_repository.cpython-312.pyc
Normal file
Binary file not shown.
BIN
repositories/__pycache__/match_clock_repository.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/match_clock_repository.cpython-312.pyc
Normal file
Binary file not shown.
BIN
repositories/__pycache__/match_clock_repository.cpython-313.pyc
Normal file
BIN
repositories/__pycache__/match_clock_repository.cpython-313.pyc
Normal file
Binary file not shown.
BIN
repositories/__pycache__/match_coach_repository.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/match_coach_repository.cpython-312.pyc
Normal file
Binary file not shown.
BIN
repositories/__pycache__/match_coach_repository.cpython-313.pyc
Normal file
BIN
repositories/__pycache__/match_coach_repository.cpython-313.pyc
Normal file
Binary file not shown.
BIN
repositories/__pycache__/match_event_repository.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/match_event_repository.cpython-312.pyc
Normal file
Binary file not shown.
BIN
repositories/__pycache__/match_event_repository.cpython-313.pyc
Normal file
BIN
repositories/__pycache__/match_event_repository.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
repositories/__pycache__/match_lineup_repository.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/match_lineup_repository.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
repositories/__pycache__/match_repository.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/match_repository.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
repositories/__pycache__/match_view_repository.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/match_view_repository.cpython-312.pyc
Normal file
Binary file not shown.
BIN
repositories/__pycache__/player_repository.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/player_repository.cpython-312.pyc
Normal file
Binary file not shown.
BIN
repositories/__pycache__/referee_repository.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/referee_repository.cpython-312.pyc
Normal file
Binary file not shown.
BIN
repositories/__pycache__/stadium_repository.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/stadium_repository.cpython-312.pyc
Normal file
Binary file not shown.
BIN
repositories/__pycache__/standings_repository.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/standings_repository.cpython-312.pyc
Normal file
Binary file not shown.
BIN
repositories/__pycache__/team_coach_repository.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/team_coach_repository.cpython-312.pyc
Normal file
Binary file not shown.
BIN
repositories/__pycache__/team_repository.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/team_repository.cpython-312.pyc
Normal file
Binary file not shown.
BIN
repositories/__pycache__/team_squad_repository.cpython-312.pyc
Normal file
BIN
repositories/__pycache__/team_squad_repository.cpython-312.pyc
Normal file
Binary file not shown.
45
repositories/audit_log_repository.py
Normal file
45
repositories/audit_log_repository.py
Normal 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()
|
||||
159
repositories/auth_repository.py
Normal file
159
repositories/auth_repository.py
Normal 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()
|
||||
286
repositories/coach_repository.py
Normal file
286
repositories/coach_repository.py
Normal 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()
|
||||
178
repositories/match_clock_repository.py
Normal file
178
repositories/match_clock_repository.py
Normal 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)
|
||||
155
repositories/match_coach_repository.py
Normal file
155
repositories/match_coach_repository.py
Normal 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()
|
||||
184
repositories/match_event_repository.py
Normal file
184
repositories/match_event_repository.py
Normal 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()
|
||||
369
repositories/match_formation_repository.py
Normal file
369
repositories/match_formation_repository.py
Normal 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
|
||||
460
repositories/match_lineup_repository.py
Normal file
460
repositories/match_lineup_repository.py
Normal 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()
|
||||
130
repositories/match_referee_repository.py
Normal file
130
repositories/match_referee_repository.py
Normal 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()
|
||||
|
||||
|
||||
269
repositories/match_repository.py
Normal file
269
repositories/match_repository.py
Normal 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()
|
||||
209
repositories/match_session_repository.py
Normal file
209
repositories/match_session_repository.py
Normal 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()
|
||||
96
repositories/match_view_repository.py
Normal file
96
repositories/match_view_repository.py
Normal 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
|
||||
373
repositories/player_repository.py
Normal file
373
repositories/player_repository.py
Normal 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()
|
||||
268
repositories/referee_repository.py
Normal file
268
repositories/referee_repository.py
Normal 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()
|
||||
291
repositories/stadium_repository.py
Normal file
291
repositories/stadium_repository.py
Normal 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()
|
||||
153
repositories/standings_repository.py
Normal file
153
repositories/standings_repository.py
Normal 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()
|
||||
50
repositories/team_coach_repository.py
Normal file
50
repositories/team_coach_repository.py
Normal 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()
|
||||
240
repositories/team_repository.py
Normal file
240
repositories/team_repository.py
Normal 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()
|
||||
58
repositories/team_squad_repository.py
Normal file
58
repositories/team_squad_repository.py
Normal 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()
|
||||
Reference in New Issue
Block a user