From 104ae25f2c8d9b0a3da8cb661d42ee1578715f0f Mon Sep 17 00:00:00 2001 From: root Date: Wed, 1 Jul 2026 17:58:05 +0300 Subject: [PATCH] Initial commit --- .gitignore | 7 + README.md | 104 +++++++++ __init__.py | 2 + auth.py | 75 +++++++ db.py | 217 +++++++++++++++++++ deploy/install.sh | 41 ++++ deploy/nginx.conf | 23 ++ deploy/voley.service | 16 ++ main.py | 335 +++++++++++++++++++++++++++++ requirements.txt | 5 + static/app.js | 295 +++++++++++++++++++++++++ templates/admin_login.html | 37 ++++ templates/admin_players.html | 79 +++++++ templates/base.html | 69 ++++++ templates/bg.jpg | Bin 0 -> 69860 bytes templates/index.html | 206 ++++++++++++++++++ templates/partials/player_row.html | 27 +++ templates/partials/teams.html | 47 ++++ 18 files changed, 1585 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __init__.py create mode 100644 auth.py create mode 100644 db.py create mode 100755 deploy/install.sh create mode 100644 deploy/nginx.conf create mode 100644 deploy/voley.service create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 static/app.js create mode 100644 templates/admin_login.html create mode 100644 templates/admin_players.html create mode 100644 templates/base.html create mode 100644 templates/bg.jpg create mode 100644 templates/index.html create mode 100644 templates/partials/player_row.html create mode 100644 templates/partials/teams.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3729fea --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.venv/ +__pycache__/ +data/ +.pytest_cache/ +.ruff_cache/ +.DS_Store +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..94a2c7c --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# Voley — учет посещаемости волейбольной тренировки + +Веб‑приложение для отметки посещения игроков по воскресеньям. + +## Возможности + +- Публичная главная страница: выбор воскресенья, отметка посещения, график за месяц/год. +- Админ‑интерфейс под паролем: добавление игроков, управление активностью. +- Хранение данных в SQLite. + +## Запуск + +1) Создайте виртуальное окружение и установите зависимости: + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +2) Задайте переменные окружения (можно через `.env` в вашей оболочке): + +- `SECRET_KEY` — секрет для подписи cookies (любая длинная строка). +- `ADMIN_PASSWORD` — пароль админки. + +Пример: + +```bash +export SECRET_KEY="change-me" +export ADMIN_PASSWORD="change-me" +``` + +3) Запустите сервер: + +```bash +uvicorn main:app --reload --port 8000 +``` + +Откройте: + +- Главная: `http://localhost:8000/` +- Админка: `http://localhost:8000/admin` + +## Данные + +SQLite база хранится в `data/voley.sqlite3` (создаётся автоматически при первом запуске). + +## Деплой на сервер + +Файлы для продакшн-деплоя находятся в `deploy/`. + +### Быстрая установка (Ubuntu/Debian) + +```bash +# На сервере: +sudo apt update && sudo apt install -y python3-venv nginx + +# Скопируйте проект на сервер, затем: +cd /path/to/voley/deploy +chmod +x install.sh +./install.sh +``` + +### Ручная установка + +1. Скопируйте проект в `/opt/voley` +2. Создайте `.env` из `.env.example` и заполните переменные +3. Создайте venv и установите зависимости: + ```bash + python3 -m venv .venv + .venv/bin/pip install -r requirements.txt + ``` +4. Скопируйте `deploy/voley.service` в `/etc/systemd/system/` +5. Запустите: `sudo systemctl enable --now voley` +6. Настройте nginx (см. `deploy/nginx.conf`) +7. SSL: `sudo certbot --nginx -d your-domain.com` + +### Структура на сервере + +``` +/opt/voley/ +├── .env # Переменные окружения +├── .venv/ # Виртуальное окружение +├── data/ # SQLite база +│ └── voley.sqlite3 +├── main.py +├── db.py +├── auth.py +├── templates/ +└── static/ +``` + +### Полезные команды + +```bash +# Статус +sudo systemctl status voley + +# Логи +sudo journalctl -u voley -f + +# Перезапуск +sudo systemctl restart voley +``` diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..9c4c32b --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +# Package marker for running as `uvicorn voley.main:app` + diff --git a/auth.py b/auth.py new file mode 100644 index 0000000..dc52105 --- /dev/null +++ b/auth.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import base64 +import hmac +import hashlib +import os +import time +from dataclasses import dataclass +from typing import Optional + + +COOKIE_NAME = "voley_admin" + + +def _get_secret() -> bytes: + secret = os.getenv("SECRET_KEY", "").strip() + if not secret: + # Разрешаем запуск без секрета, но с сильно сниженной безопасностью. + # Для реального деплоя SECRET_KEY обязателен. + secret = "dev-unsafe-secret" + return secret.encode("utf-8") + + +def _b64url(b: bytes) -> str: + return base64.urlsafe_b64encode(b).decode("ascii").rstrip("=") + + +def _b64url_decode(s: str) -> bytes: + pad = "=" * (-len(s) % 4) + return base64.urlsafe_b64decode((s + pad).encode("ascii")) + + +def _sign(payload: bytes) -> str: + mac = hmac.new(_get_secret(), payload, hashlib.sha256).digest() + return _b64url(mac) + + +def make_session(ttl_seconds: int = 60 * 60 * 12) -> str: + exp = int(time.time()) + int(ttl_seconds) + payload = f"exp={exp}".encode("utf-8") + sig = _sign(payload) + return f"{_b64url(payload)}.{sig}" + + +def verify_session(token: str) -> bool: + try: + payload_b64, sig = token.split(".", 1) + payload = _b64url_decode(payload_b64) + except Exception: + return False + if not hmac.compare_digest(_sign(payload), sig): + return False + try: + parts = payload.decode("utf-8").split("=", 1) + if len(parts) != 2 or parts[0] != "exp": + return False + exp = int(parts[1]) + except Exception: + return False + return time.time() < exp + + +def check_admin_password(password: str) -> bool: + expected = os.getenv("ADMIN_PASSWORD", "") + if not expected: + # Чтобы админка не осталась открытой по умолчанию. + return False + return hmac.compare_digest(password or "", expected) + + +@dataclass(frozen=True) +class AdminAuth: + is_admin: bool + token: Optional[str] = None + diff --git a/db.py b/db.py new file mode 100644 index 0000000..64d4a4d --- /dev/null +++ b/db.py @@ -0,0 +1,217 @@ +from __future__ import annotations + +import os +import sqlite3 +from contextlib import contextmanager +from dataclasses import dataclass +from datetime import date, datetime +from typing import Any, Iterable, Iterator, Optional + + +DB_PATH = os.getenv("DB_PATH", os.path.join("data", "voley.sqlite3")) + + +def _ensure_parent_dir(path: str) -> None: + parent = os.path.dirname(path) + if parent: + os.makedirs(parent, exist_ok=True) + + +def connect() -> sqlite3.Connection: + _ensure_parent_dir(DB_PATH) + conn = sqlite3.connect(DB_PATH, check_same_thread=False) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = ON;") + return conn + + +@contextmanager +def get_conn() -> Iterator[sqlite3.Connection]: + conn = connect() + try: + yield conn + conn.commit() + finally: + conn.close() + + +def init_db() -> None: + with get_conn() as conn: + conn.executescript( + """ + CREATE TABLE IF NOT EXISTS players ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + active INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS attendance ( + player_id INTEGER NOT NULL, + session_date TEXT NOT NULL, + present INTEGER NOT NULL DEFAULT 1, + updated_at TEXT NOT NULL, + PRIMARY KEY (player_id, session_date), + FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_attendance_date ON attendance(session_date); + """ + ) + + +def now_iso() -> str: + return datetime.utcnow().replace(microsecond=0).isoformat() + "Z" + + +def iso_date(d: date) -> str: + return d.isoformat() + + +@dataclass(frozen=True) +class Player: + id: int + name: str + active: bool + + +def list_players(active_only: bool = True) -> list[Player]: + sql = "SELECT id, name, active FROM players" + params: list[Any] = [] + if active_only: + sql += " WHERE active=1" + sql += " ORDER BY name COLLATE NOCASE ASC" + with get_conn() as conn: + rows = conn.execute(sql, params).fetchall() + return [Player(id=int(r["id"]), name=str(r["name"]), active=bool(r["active"])) for r in rows] + + +def list_players_all() -> list[Player]: + with get_conn() as conn: + rows = conn.execute( + "SELECT id, name, active FROM players ORDER BY active DESC, name COLLATE NOCASE ASC" + ).fetchall() + return [Player(id=int(r["id"]), name=str(r["name"]), active=bool(r["active"])) for r in rows] + + +def add_player(name: str) -> int: + name = (name or "").strip() + if not name: + raise ValueError("name is required") + with get_conn() as conn: + cur = conn.execute( + "INSERT INTO players(name, active, created_at) VALUES (?, 1, ?)", + (name, now_iso()), + ) + return int(cur.lastrowid) + + +def set_player_active(player_id: int, active: bool) -> None: + with get_conn() as conn: + conn.execute("UPDATE players SET active=? WHERE id=?", (1 if active else 0, int(player_id))) + + +def rename_player(player_id: int, name: str) -> None: + name = (name or "").strip() + if not name: + raise ValueError("name is required") + with get_conn() as conn: + conn.execute("UPDATE players SET name=? WHERE id=?", (name, int(player_id))) + + +def get_attendance_map(session_date: str) -> dict[int, bool]: + with get_conn() as conn: + rows = conn.execute( + "SELECT player_id, present FROM attendance WHERE session_date=?", + (session_date,), + ).fetchall() + return {int(r["player_id"]): bool(r["present"]) for r in rows} + + +def toggle_attendance(player_id: int, session_date: str) -> bool: + with get_conn() as conn: + row = conn.execute( + "SELECT present FROM attendance WHERE player_id=? AND session_date=?", + (int(player_id), session_date), + ).fetchone() + if row is None: + conn.execute( + "INSERT INTO attendance(player_id, session_date, present, updated_at) VALUES (?, ?, 1, ?)", + (int(player_id), session_date, now_iso()), + ) + return True + new_present = 0 if bool(row["present"]) else 1 + conn.execute( + "UPDATE attendance SET present=?, updated_at=? WHERE player_id=? AND session_date=?", + (new_present, now_iso(), int(player_id), session_date), + ) + return bool(new_present) + + +def list_present_players(session_date: str, active_only: bool = True) -> list[Player]: + sql = """ + SELECT p.id, p.name, p.active + FROM attendance a + JOIN players p ON p.id = a.player_id + WHERE a.session_date = ? AND a.present = 1 + """ + params: list[Any] = [session_date] + if active_only: + sql += " AND p.active = 1" + sql += " ORDER BY p.name COLLATE NOCASE ASC" + with get_conn() as conn: + rows = conn.execute(sql, params).fetchall() + return [Player(id=int(r["id"]), name=str(r["name"]), active=bool(r["active"])) for r in rows] + + +def attendance_counts_by_sunday_in_month(year: int, month: int) -> list[tuple[str, int]]: + # Возвращает список (YYYY-MM-DD, count) для воскресений в указанном месяце. + # Считаем количество присутствий (present=1). + y = int(year) + m = int(month) + with get_conn() as conn: + rows = conn.execute( + """ + SELECT session_date, SUM(CASE WHEN present=1 THEN 1 ELSE 0 END) AS cnt + FROM attendance + WHERE substr(session_date, 1, 7) = ? + GROUP BY session_date + ORDER BY session_date ASC + """, + (f"{y:04d}-{m:02d}",), + ).fetchall() + return [(str(r["session_date"]), int(r["cnt"] or 0)) for r in rows] + + +def attendance_counts_by_month_in_year(year: int) -> list[tuple[str, int]]: + # Возвращает список (YYYY-MM, count) — суммарное число посещений по месяцам. + y = int(year) + with get_conn() as conn: + rows = conn.execute( + """ + SELECT substr(session_date, 1, 7) AS ym, + SUM(CASE WHEN present=1 THEN 1 ELSE 0 END) AS cnt + FROM attendance + WHERE substr(session_date, 1, 4) = ? + GROUP BY ym + ORDER BY ym ASC + """, + (f"{y:04d}",), + ).fetchall() + return [(str(r["ym"]), int(r["cnt"] or 0)) for r in rows] + + +def get_attendance_history() -> list[dict]: + """Возвращает историю посещаемости: (дата, имя игрока, присутствовал).""" + with get_conn() as conn: + rows = conn.execute( + """ + SELECT a.session_date, p.name, a.present + FROM attendance a + JOIN players p ON p.id = a.player_id + WHERE a.present = 1 + ORDER BY a.session_date DESC, p.name COLLATE NOCASE ASC + """ + ).fetchall() + return [{"date": str(r["session_date"]), "name": str(r["name"]), "present": bool(r["present"])} for r in rows] + diff --git a/deploy/install.sh b/deploy/install.sh new file mode 100755 index 0000000..728c9a0 --- /dev/null +++ b/deploy/install.sh @@ -0,0 +1,41 @@ +#!/bin/bash +set -e + +APP_DIR="/opt/voley" +APP_USER="www-data" + +echo "=== Установка Voley ===" + +# Создание директории +sudo mkdir -p $APP_DIR +sudo mkdir -p $APP_DIR/data + +# Копирование файлов +sudo cp -r ../*.py ../requirements.txt ../templates ../static $APP_DIR/ +sudo cp ../.env $APP_DIR/.env + +# Виртуальное окружение +sudo python3 -m venv $APP_DIR/.venv +sudo $APP_DIR/.venv/bin/pip install --upgrade pip +sudo $APP_DIR/.venv/bin/pip install -r $APP_DIR/requirements.txt + +# Права +sudo chown -R $APP_USER:$APP_USER $APP_DIR + +# systemd +sudo cp voley.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable voley +sudo systemctl start voley + +# nginx +sudo cp nginx.conf /etc/nginx/sites-available/voley +sudo ln -sf /etc/nginx/sites-available/voley /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx + +echo "" +echo "=== Готово! ===" +echo "1. Отредактируйте $APP_DIR/.env (SECRET_KEY, ADMIN_PASSWORD)" +echo "2. Измените домен в /etc/nginx/sites-available/voley" +echo "3. Перезапустите: sudo systemctl restart voley" +echo "4. SSL: sudo certbot --nginx -d voley.example.com" diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..df54bd1 --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,23 @@ +server { + listen 80; + server_name volleyball.my.to; # Замените на ваш домен + + location / { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /static { + alias /opt/voley/static; + expires 7d; + add_header Cache-Control "public, immutable"; + } + + # Опционально: SSL через certbot + # listen 443 ssl; + # ssl_certificate /etc/letsencrypt/live/volleyball.my.to/fullchain.pem; + # ssl_certificate_key /etc/letsencrypt/live/volleyball.my.to/privkey.pem; +} diff --git a/deploy/voley.service b/deploy/voley.service new file mode 100644 index 0000000..f9fdf85 --- /dev/null +++ b/deploy/voley.service @@ -0,0 +1,16 @@ +[Unit] +Description=Voley Attendance App +After=network.target + +[Service] +Type=simple +User=www-data +Group=www-data +WorkingDirectory=/opt/voley +EnvironmentFile=/opt/voley/.env +ExecStart=/opt/voley/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000 --workers 2 +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/main.py b/main.py new file mode 100644 index 0000000..06e0ad3 --- /dev/null +++ b/main.py @@ -0,0 +1,335 @@ +from __future__ import annotations + +import json +import os +import random +from datetime import date, datetime, timedelta +from typing import Optional + +from fastapi import Cookie, FastAPI, Form, HTTPException, Request +from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse, StreamingResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +try: + # запуск как пакет: `uvicorn voley.main:app` + from . import db # type: ignore + from .auth import COOKIE_NAME, check_admin_password, make_session, verify_session # type: ignore +except Exception: + # запуск из папки voley: `uvicorn main:app` + import db # type: ignore + from auth import COOKIE_NAME, check_admin_password, make_session, verify_session # type: ignore + + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +TEMPLATES_DIR = os.path.join(BASE_DIR, "templates") +STATIC_DIR = os.path.join(BASE_DIR, "static") + +app = FastAPI(title="Voley Attendance") +templates = Jinja2Templates(directory=TEMPLATES_DIR) +app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") + +BG_JPG_PATH = os.path.join(TEMPLATES_DIR, "bg.jpg") + + +@app.on_event("startup") +def _startup() -> None: + db.init_db() + try: + templates.env.globals["bg_version"] = int(os.path.getmtime(BG_JPG_PATH)) + except Exception: + templates.env.globals["bg_version"] = 0 + + +def _parse_date_yyyy_mm_dd(s: str) -> Optional[date]: + try: + return datetime.strptime(s, "%Y-%m-%d").date() + except Exception: + return None + + +def _nearest_sunday(d: date) -> date: + # Python: Monday=0 ... Sunday=6 + delta = (d.weekday() - 6) % 7 + return d - timedelta(days=delta) + + +def _selected_sunday(date_q: Optional[str]) -> date: + today = date.today() + if date_q: + parsed = _parse_date_yyyy_mm_dd(date_q) + if parsed: + return _nearest_sunday(parsed) + return _nearest_sunday(today) + + +def _is_admin(token: Optional[str]) -> bool: + return bool(token) and verify_session(token) + + +@app.get("/assets/bg.jpg") +def bg_image() -> FileResponse: + if not os.path.exists(BG_JPG_PATH): + raise HTTPException(status_code=404, detail="bg.jpg not found") + return FileResponse(BG_JPG_PATH, media_type="image/jpeg") + + +def _sundays_in_month(year: int, month: int) -> list[date]: + y = int(year) + m = int(month) + first = date(y, m, 1) + # первый день следующего месяца + if m == 12: + nxt = date(y + 1, 1, 1) + else: + nxt = date(y, m + 1, 1) + + # weekday: Monday=0 ... Sunday=6 + offset = (6 - first.weekday()) % 7 + d = first + timedelta(days=offset) + out: list[date] = [] + while d < nxt: + out.append(d) + d = d + timedelta(days=7) + return out + + +@app.get("/", response_class=HTMLResponse) +def index(request: Request, date: Optional[str] = None, view: str = "month") -> HTMLResponse: + selected = _selected_sunday(date) + session_date = selected.isoformat() + players = db.list_players(active_only=True) + present_map = db.get_attendance_map(session_date=session_date) + + # Данные для графика. + if view not in ("month", "year"): + view = "month" + if view == "month": + counts = db.attendance_counts_by_sunday_in_month(selected.year, selected.month) + counts_map = {d: c for (d, c) in counts} + sundays = _sundays_in_month(selected.year, selected.month) + chart_labels = [d.isoformat() for d in sundays] + chart_values = [int(counts_map.get(d.isoformat(), 0)) for d in sundays] + chart_title = f"Посещаемость за {selected.strftime('%m.%Y')} (по воскресеньям)" + else: + counts = db.attendance_counts_by_month_in_year(selected.year) + counts_map = {ym: c for (ym, c) in counts} + chart_labels = [f"{selected.year:04d}-{m:02d}" for m in range(1, 13)] + chart_values = [int(counts_map.get(ym, 0)) for ym in chart_labels] + chart_title = f"Посещаемость за {selected.year} (по месяцам)" + + n = max(0, len(chart_labels)) + if n <= 8: + chart_label_step = 1 + elif n <= 16: + chart_label_step = 2 + else: + chart_label_step = 3 + + return templates.TemplateResponse( + "index.html", + { + "request": request, + "selected_date": session_date, + "players": players, + "present_map": present_map, + "chart_view": view, + "chart_title": chart_title, + "chart_labels": chart_labels, + "chart_values": chart_values, + "chart_label_step": chart_label_step, + "chart_labels_json": json.dumps(chart_labels, ensure_ascii=False), + "chart_values_json": json.dumps(chart_values, ensure_ascii=False), + }, + ) + + +@app.post("/api/attendance/toggle", response_class=HTMLResponse) +def toggle_attendance( + request: Request, + player_id: int = Form(...), + session_date: str = Form(...), +) -> HTMLResponse: + d = _parse_date_yyyy_mm_dd(session_date) + if not d: + raise HTTPException(status_code=400, detail="bad session_date") + session_date = _nearest_sunday(d).isoformat() + + new_present = db.toggle_attendance(player_id=player_id, session_date=session_date) + + # Рендерим одну строку игрока для обновления на клиенте. + # Имя тянем из списка активных + всех, чтобы корректно показать даже если игрок деактивирован. + players = db.list_players_all() + player = next((p for p in players if p.id == int(player_id)), None) + if not player: + raise HTTPException(status_code=404, detail="player not found") + + return templates.TemplateResponse( + "partials/player_row.html", + { + "request": request, + "player": player, + "session_date": session_date, + "present": new_present, + }, + ) + + +@app.post("/api/teams/random", response_class=HTMLResponse) +def random_teams(request: Request, session_date: str = Form(...)) -> HTMLResponse: + d = _parse_date_yyyy_mm_dd(session_date) + if not d: + raise HTTPException(status_code=400, detail="bad session_date") + session_date = _nearest_sunday(d).isoformat() + + present_players = db.list_present_players(session_date=session_date, active_only=True) + names = [p.name for p in present_players] + random.shuffle(names) + mid = (len(names) + 1) // 2 + team_a = names[:mid] + team_b = names[mid:] + + return templates.TemplateResponse( + "partials/teams.html", + { + "request": request, + "session_date": session_date, + "team_a": team_a, + "team_b": team_b, + "count": len(names), + }, + ) + + +@app.get("/admin", response_class=HTMLResponse) +def admin(request: Request, voley_admin: Optional[str] = Cookie(default=None, alias=COOKIE_NAME)) -> HTMLResponse: + if not _is_admin(voley_admin): + return RedirectResponse(url="/admin/login", status_code=302) + players = db.list_players_all() + return templates.TemplateResponse( + "admin_players.html", + {"request": request, "players": players}, + ) + + +@app.get("/admin/login", response_class=HTMLResponse) +def admin_login_get(request: Request) -> HTMLResponse: + return templates.TemplateResponse("admin_login.html", {"request": request, "error": None}) + + +@app.post("/admin/login") +def admin_login_post(password: str = Form(...)) -> RedirectResponse: + if not check_admin_password(password): + # Возвращаемся на логин с флагом. + return RedirectResponse(url="/admin/login?error=1", status_code=302) + resp = RedirectResponse(url="/admin", status_code=302) + resp.set_cookie( + COOKIE_NAME, + make_session(), + httponly=True, + samesite="lax", + secure=False, # включите True за HTTPS + path="/", + max_age=60 * 60 * 12, + ) + return resp + + +@app.get("/admin/logout") +def admin_logout() -> RedirectResponse: + resp = RedirectResponse(url="/", status_code=302) + resp.delete_cookie(COOKIE_NAME, path="/") + return resp + + +@app.post("/admin/players/add") +def admin_add_player( + voley_admin: Optional[str] = Cookie(default=None, alias=COOKIE_NAME), + name: str = Form(...), +) -> RedirectResponse: + if not _is_admin(voley_admin): + raise HTTPException(status_code=401, detail="not authorized") + db.add_player(name) + return RedirectResponse(url="/admin", status_code=303) + + +@app.post("/admin/players/{player_id}/toggle-active") +def admin_toggle_active( + player_id: int, + voley_admin: Optional[str] = Cookie(default=None, alias=COOKIE_NAME), +) -> RedirectResponse: + if not _is_admin(voley_admin): + raise HTTPException(status_code=401, detail="not authorized") + players = db.list_players_all() + p = next((x for x in players if x.id == int(player_id)), None) + if not p: + raise HTTPException(status_code=404, detail="player not found") + db.set_player_active(player_id, not p.active) + return RedirectResponse(url="/admin", status_code=303) + + +@app.post("/admin/players/{player_id}/rename") +def admin_rename_player( + player_id: int, + voley_admin: Optional[str] = Cookie(default=None, alias=COOKIE_NAME), + name: str = Form(...), +) -> RedirectResponse: + if not _is_admin(voley_admin): + raise HTTPException(status_code=401, detail="not authorized") + db.rename_player(player_id, name) + return RedirectResponse(url="/admin", status_code=303) + + +@app.get("/api/export/attendance.xlsx") +def export_attendance_xlsx() -> StreamingResponse: + """Выгрузка истории посещаемости в Excel.""" + from collections import defaultdict + from io import BytesIO + + from openpyxl import Workbook + from openpyxl.styles import Alignment, Font, PatternFill + + history = db.get_attendance_history() + + # Группируем по датам: {date: [names]} + by_date: dict[str, list[str]] = defaultdict(list) + for rec in history: + by_date[rec["date"]].append(rec["name"]) + + wb = Workbook() + ws = wb.active + ws.title = "Посещаемость" + + # Заголовки + ws["A1"] = "Дата" + ws["B1"] = "Игрок" + header_font = Font(bold=True) + header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") + header_font_white = Font(bold=True, color="FFFFFF") + for cell in [ws["A1"], ws["B1"]]: + cell.font = header_font_white + cell.fill = header_fill + cell.alignment = Alignment(horizontal="center") + + row = 2 + for dt in sorted(by_date.keys(), reverse=True): + names = by_date[dt] + for name in names: + ws.cell(row=row, column=1, value=dt) + ws.cell(row=row, column=2, value=name) + row += 1 + + # Ширина колонок + ws.column_dimensions["A"].width = 14 + ws.column_dimensions["B"].width = 25 + + buffer = BytesIO() + wb.save(buffer) + buffer.seek(0) + + return StreamingResponse( + buffer, + media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + headers={"Content-Disposition": "attachment; filename=attendance.xlsx"}, + ) + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c391245 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi>=0.110 +uvicorn[standard]>=0.27 +jinja2>=3.1 +python-multipart>=0.0.9 +openpyxl>=3.1 diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..0e94212 --- /dev/null +++ b/static/app.js @@ -0,0 +1,295 @@ +/* global flatpickr, Chart */ + +(function () { + function qs(sel) { + return document.querySelector(sel); + } + + function isoFromDate(d) { + // d is Date + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + const dd = String(d.getDate()).padStart(2, "0"); + return `${yyyy}-${mm}-${dd}`; + } + + function nearestSunday(d) { + // JS Date: Sunday=0 ... Saturday=6 + const day = d.getDay(); + const delta = day === 0 ? 0 : day; // go back to Sunday + const out = new Date(d); + out.setDate(d.getDate() - delta); + out.setHours(12, 0, 0, 0); + return out; + } + + function initDatePicker() { + const el = qs("#datePicker"); + if (!el || typeof flatpickr === "undefined") return; + + flatpickr.localize(flatpickr.l10ns.ru); + + flatpickr(el, { + dateFormat: "Y-m-d", + defaultDate: el.value || null, + disableMobile: false, + disable: [ + function (date) { + // allow only Sundays (0) + return date.getDay() !== 0; + }, + ], + onChange: function (selectedDates) { + if (!selectedDates || !selectedDates.length) return; + const sunday = nearestSunday(selectedDates[0]); + const iso = isoFromDate(sunday); + const url = new URL(window.location.href); + url.searchParams.set("date", iso); + // сохраняем текущий view графика, если есть + if (!url.searchParams.get("view") && window.__VOLEY__ && window.__VOLEY__.chartView) { + url.searchParams.set("view", window.__VOLEY__.chartView); + } + window.location.href = url.toString(); + }, + }); + } + + async function handleAttendanceSubmit(ev) { + const form = ev.target.closest("form.attendance-form"); + if (!form) return; + ev.preventDefault(); + + const fd = new FormData(form); + const row = form.closest(".player-row"); + if (!row) return; + + // небольшая блокировка кнопки + const btn = form.querySelector("button[type='submit']"); + if (btn) { + btn.disabled = true; + btn.classList.add("opacity-70"); + } + + try { + const resp = await fetch(form.action, { method: "POST", body: fd }); + if (!resp.ok) throw new Error("network"); + const html = await resp.text(); + + // заменяем целиком строку игрока + const tmp = document.createElement("div"); + tmp.innerHTML = html; + const newRow = tmp.firstElementChild; + if (newRow) row.replaceWith(newRow); + + // после отметки обновим команды (если блок есть) + refreshTeams(); + } catch (e) { + alert("Не удалось сохранить отметку. Попробуйте ещё раз."); + if (btn) { + btn.disabled = false; + btn.classList.remove("opacity-70"); + } + } + } + + async function refreshTeams() { + const box = qs("#teamsBox"); + if (!box || !window.__VOLEY__ || !window.__VOLEY__.selectedDate) return; + try { + const fd = new FormData(); + fd.set("session_date", window.__VOLEY__.selectedDate); + const resp = await fetch("/api/teams/random", { method: "POST", body: fd }); + if (!resp.ok) return; + box.innerHTML = await resp.text(); + } catch (_e) { + // молча + } + } + + function initAttendance() { + document.addEventListener("submit", function (ev) { + const t = ev.target; + if (t && t.matches && t.matches("form.attendance-form")) { + handleAttendanceSubmit(ev); + } + }); + } + + function initTeams() { + document.addEventListener("submit", function (ev) { + const t = ev.target; + if (t && t.matches && t.matches("form.teams-form")) { + ev.preventDefault(); + const fd = new FormData(t); + // защита от отсутствующего hidden input (или пустого значения) + if ((!fd.get("session_date") || String(fd.get("session_date")).trim() === "") && window.__VOLEY__ && window.__VOLEY__.selectedDate) { + fd.set("session_date", window.__VOLEY__.selectedDate); + } + fetch(t.action, { method: "POST", body: fd }) + .then((r) => r.text()) + .then((html) => { + const box = qs("#teamsBox"); + if (box) box.innerHTML = html; + }) + .catch(function () {}); + } + }); + } + + function initChart() { + const canvas = qs("#attendanceChart"); + if (!canvas || !window.__VOLEY__) return; + + const labels = window.__VOLEY__.chartLabels || []; + const values = window.__VOLEY__.chartValues || []; + + const ctx = canvas.getContext("2d"); + + function formatLabel(s) { + // month: YYYY-MM-DD -> DD.MM + // year: YYYY-MM -> MM + if (typeof s !== "string") return String(s); + if (s.length === 10 && s[4] === "-" && s[7] === "-") return s.slice(8, 10) + "." + s.slice(5, 7); + if (s.length === 7 && s[4] === "-") return s.slice(5, 7); + return s; + } + + function renderVanillaBarChart() { + // Простая отрисовка без зависимостей (если CDN недоступен) + const dpr = window.devicePixelRatio || 1; + const rect = canvas.getBoundingClientRect(); + const w = Math.max(240, Math.floor(rect.width)); + const h = Math.max(160, Math.floor(rect.height)); + canvas.width = Math.floor(w * dpr); + canvas.height = Math.floor(h * dpr); + canvas.style.width = w + "px"; + canvas.style.height = h + "px"; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + + // clear + ctx.clearRect(0, 0, w, h); + + // palette + const grid = "rgba(148, 163, 184, 0.15)"; + const text = "rgba(226, 232, 240, 0.85)"; + const bar = "rgba(99, 102, 241, 0.78)"; + const barStroke = "rgba(99, 102, 241, 1)"; + + const padL = 40; + const padR = 10; + const padT = 10; + const padB = 28; + + const plotW = w - padL - padR; + const plotH = h - padT - padB; + const maxV = Math.max(1, ...values.map((x) => (Number.isFinite(+x) ? +x : 0))); + + // grid lines + y labels (0..max) + ctx.font = "12px system-ui, -apple-system, Segoe UI, Roboto, Arial"; + ctx.fillStyle = text; + ctx.strokeStyle = grid; + ctx.lineWidth = 1; + + const steps = Math.min(5, maxV); + for (let i = 0; i <= steps; i++) { + const yVal = Math.round((maxV * i) / steps); + const y = padT + plotH - (plotH * i) / steps; + ctx.beginPath(); + ctx.moveTo(padL, y); + ctx.lineTo(w - padR, y); + ctx.stroke(); + ctx.fillText(String(yVal), 6, y + 4); + } + + const n = Math.max(0, labels.length); + if (n === 0) { + ctx.fillText("Нет данных для графика", padL, padT + 18); + return; + } + + const gap = Math.min(10, Math.max(4, Math.floor(plotW / (n * 6)))); + const barW = Math.max(6, Math.floor((plotW - gap * (n - 1)) / n)); + + for (let i = 0; i < n; i++) { + const v = Number.isFinite(+values[i]) ? +values[i] : 0; + const bh = Math.round((v / maxV) * plotH); + const x = padL + i * (barW + gap); + const y = padT + plotH - bh; + + // bar + ctx.fillStyle = bar; + ctx.strokeStyle = barStroke; + ctx.beginPath(); + ctx.rect(x, y, barW, bh); + ctx.fill(); + ctx.stroke(); + + // x label (every Nth to avoid overlap) + const every = n <= 8 ? 1 : n <= 16 ? 2 : 3; + if (i % every === 0) { + ctx.fillStyle = text; + const lab = formatLabel(labels[i]); + ctx.fillText(lab, x, h - 10); + } + } + } + + // Пытаемся Chart.js, но если не загрузился — рисуем fallback. + try { + if (typeof Chart === "undefined") { + renderVanillaBarChart(); + } else { + // eslint-disable-next-line no-new + new Chart(ctx, { + type: "bar", + data: { + labels: labels, + datasets: [ + { + label: "Посещений", + data: values, + borderWidth: 1, + backgroundColor: "rgba(99, 102, 241, 0.75)", + borderColor: "rgba(99, 102, 241, 1)", + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false }, + tooltip: { enabled: true }, + }, + scales: { + x: { + ticks: { color: "rgba(226, 232, 240, 0.8)" }, + grid: { color: "rgba(148, 163, 184, 0.15)" }, + }, + y: { + beginAtZero: true, + ticks: { color: "rgba(226, 232, 240, 0.8)", precision: 0 }, + grid: { color: "rgba(148, 163, 184, 0.15)" }, + }, + }, + }, + }); + } + } catch (e) { + renderVanillaBarChart(); + } + + // на ресайзе перерисуем только fallback (Chart.js сам умеет) + if (typeof Chart === "undefined") { + window.addEventListener("resize", function () { + renderVanillaBarChart(); + }); + } + } + + initDatePicker(); + initAttendance(); + initTeams(); + initChart(); +})(); + diff --git a/templates/admin_login.html b/templates/admin_login.html new file mode 100644 index 0000000..824165b --- /dev/null +++ b/templates/admin_login.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% block title %}Voley — вход в админку{% endblock %} +{% block header %}Админка{% endblock %} + +{% block nav %} + На главную +{% endblock %} + +{% block content %} +
+

Вход

+

Введите пароль администратора.

+ + {% if request.query_params.get('error') %} +
+ Неверный пароль. +
+ {% endif %} + +
+ + +
+
+{% endblock %} diff --git a/templates/admin_players.html b/templates/admin_players.html new file mode 100644 index 0000000..9c34378 --- /dev/null +++ b/templates/admin_players.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} + +{% block title %}Voley — админка{% endblock %} +{% block header %}Админка{% endblock %} + +{% block nav %} +
+ На главную + Выйти +
+{% endblock %} + +{% block content %} +
+

Добавить игрока

+
+ + +
+
+ +
+
+

Игроки

+
{{ players|length }} всего
+
+ +
+ {% for p in players %} +
+
+
+
+ {{ p.name }} + {% if not p.active %} + неактивен + {% endif %} +
+
ID: {{ p.id }}
+
+ +
+ +
+
+ +
+ + +
+
+ {% endfor %} +
+
+{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..603f5a6 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,69 @@ + + + + + + {% block title %}Voley{% endblock %} + + + + + + + + + + + + + +
+
+ + + + + +
+
+
+ +
+
+
+
Волейбол
+

{% block header %}Учет посещаемости{% endblock %}

+
+ +
+ + {% block content %}{% endblock %} +
+ + + {% block scripts %}{% endblock %} + + diff --git a/templates/bg.jpg b/templates/bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..757d859cae2d60209be7fa9c146302f26ba89fb9 GIT binary patch literal 69860 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 Q4mwers4Q(0`#;nF2UZCr=>Px# literal 0 HcmV?d00001 diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..a185dcf --- /dev/null +++ b/templates/index.html @@ -0,0 +1,206 @@ +{% extends "base.html" %} + +{% block title %}Voley — отметка посещения{% endblock %} +{% block header %}Отметка посещения{% endblock %} + +{% block content %} +
+
+
+
Выбранное воскресенье
+
{{ selected_date }}
+
+ +
+ + +
+
+
+ +
+
+

Игроки

+
{{ players|length }} активн.
+
+ +
+ {% for player in players %} + {% set present = present_map.get(player.id, False) %} + {% include "partials/player_row.html" %} + {% endfor %} +
+
+ +
+
+ {# По умолчанию рендерим команды детерминированно (по алфавиту): #} + {% set present_names = [] %} + {% for player in players %} + {% if present_map.get(player.id, False) %} + {% set _ = present_names.append(player.name) %} + {% endif %} + {% endfor %} + {% set count = present_names|length %} + {% set mid = (count + 1) // 2 %} + {% set team_a = present_names[:mid] %} + {% set team_b = present_names[mid:] %} + {% include "partials/teams.html" %} +
+
+ +
+
+
+

График

+
{{ chart_title }}
+
+ +
+ +
+ {# SVG-график без JS/CDN — всегда отображается #} + {% set max_v = (chart_values|max) if chart_values else 0 %} + {% if max_v < 1 %}{% set max_v = 1 %}{% endif %} + {% set n = chart_labels|length %} + + + + + + + + + + + + + + + + + + + + + + {# параметры области построения #} + {% set padL = 16 %} + {% set padR = 3 %} + {% set padT = 8 %} + {% set padB = 18 %} + {% set plotW = 200 - padL - padR %} + {% set plotH = 100 - padT - padB %} + + + + + + + {% for p in [0,25,50,75,100] %} + {% set y = padT + plotH - (plotH * p / 100) %} + + {{ (max_v * p / 100)|round|int }} + {% endfor %} + + {% if n == 0 %} + Нет данных + {% else %} + {% set barGap = 1.4 %} + {% set slot = plotW / n %} + {% set barW = slot - barGap %} + {% if barW < 0.9 %}{% set barW = 0.9 %}{% endif %} + + {% for lab in chart_labels %} + {% set i = loop.index0 %} + {% set v = chart_values[i] %} + {% set h = (v / max_v) * plotH %} + {% set x = padL + (i * slot) + (barGap/2) %} + {% set y = padT + plotH - h %} + {% set cx = x + (barW/2) %} + + {{ lab }}: {{ v }} + + + {# значение над столбцом (если место позволяет) #} + {% if v > 0 and barW >= 2.2 %} + {{ v }} + {% endif %} + + {% if (i % chart_label_step) == 0 %} + {% if chart_view == 'month' and lab|length == 10 %} + {% set show = lab[8:10] ~ '.' ~ lab[5:7] %} + {% elif chart_view == 'year' and lab|length == 7 %} + {% set show = lab[5:7] %} + {% else %} + {% set show = lab %} + {% endif %} + {{ show }} + {% endif %} + {% endfor %} + {% endif %} + +
+
+ +
+
+
+

Выгрузка данных

+
История посещений игроков по датам
+
+ Скачать Excel +
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/partials/player_row.html b/templates/partials/player_row.html new file mode 100644 index 0000000..0510070 --- /dev/null +++ b/templates/partials/player_row.html @@ -0,0 +1,27 @@ +
+
+
{{ player.name }}
+
Нажмите, чтобы {% if present %}снять отметку{% else %}отметиться{% endif %}
+
+ +
+ + + + +
+
diff --git a/templates/partials/teams.html b/templates/partials/teams.html new file mode 100644 index 0000000..20bb557 --- /dev/null +++ b/templates/partials/teams.html @@ -0,0 +1,47 @@ +
+
+
+

Команды

+
Отметились: {{ count }} · Дата: {{ session_date }}
+
+
+ + +
+
+ + {% if count < 2 %} +
+ Нужно минимум 2 отметившихся, чтобы разделить на команды. +
+ {% else %} +
+
+
Команда A
+
    + {% for n in team_a %} +
  1. + {{ loop.index }}. {{ n }} +
  2. + {% endfor %} +
+
+
+
Команда B
+
    + {% for n in team_b %} +
  1. + {{ loop.index }}. {{ n }} +
  2. + {% endfor %} +
+
+
+ {% endif %} +
+