Pageview beacony uz neposielaju player_id z klienta (nedovereny vstup na neautentifikovanom endpointe) -- prihlasenie sa eviduje server-side ako event "login" (aj z registracie), s player_id skutocneho uctu. Admin dashboard dostal prepinac scope vsetci/prihlaseni, ktory konzistentne pocita grafy aj tabulky (kazdy login samostatne, navstevnicky den pre anonymnu navstevnost). PageView.country teraz uklada cely anglicky nazov krajiny namiesto ISO kodu (potrebna zmena schemy). Pridane sledovanie klikov na "Pravidla hry" aj z GameList. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
113 lines
4.5 KiB
Python
113 lines
4.5 KiB
Python
"""ORM modely: Player, Game, Guess, PageView.
|
|
|
|
`won` sa neuklada -- vyplyva z `points > 0`
|
|
(trafeny tip = 10 + tip, inak 0; pozri Round.get_points_summary v bridzik.py).
|
|
"""
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import DateTime, ForeignKey, Integer, String, UniqueConstraint, func
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from db.db import Base
|
|
|
|
|
|
class Player(Base):
|
|
"""Trvaly ucet hraca + autentifikacia (TOTP)."""
|
|
|
|
__tablename__ = "players"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
username: Mapped[str] = mapped_column(String(40), unique=True, index=True)
|
|
# Sifrovany cez db/crypto.py (Fernet) -- nikdy neuklada plaintext secret.
|
|
totp_secret: Mapped[str] = mapped_column(String(255))
|
|
# Posledny pouzity TOTP casovy krok -- ochrana proti replay v ramci okna.
|
|
totp_last_step: Mapped[int] = mapped_column(Integer, default=0)
|
|
# Session token pre auto-reconnect (poslany v Socket.IO `auth`).
|
|
# Uklada sa SHA-256 hash (db/crypto.hash_token), nie surovy token.
|
|
auth_token: Mapped[str | None] = mapped_column(
|
|
String(64), unique=True, nullable=True
|
|
)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime, server_default=func.now()
|
|
)
|
|
|
|
|
|
class Game(Base):
|
|
"""Jedna partia. Drzi priamo 4 ID hracov podla sedadla (0-3)."""
|
|
|
|
__tablename__ = "games"
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
String(36), primary_key=True, default=lambda: str(uuid.uuid4())
|
|
)
|
|
name: Mapped[str] = mapped_column(String(40), default="")
|
|
player0_id: Mapped[int] = mapped_column(ForeignKey("players.id"))
|
|
player1_id: Mapped[int] = mapped_column(ForeignKey("players.id"))
|
|
player2_id: Mapped[int] = mapped_column(ForeignKey("players.id"))
|
|
player3_id: Mapped[int] = mapped_column(ForeignKey("players.id"))
|
|
# Aktualna pozicia hry. Spolu s rozdanim kariet nanovo plne urcuju stav hry
|
|
# po zaciatok kola -> staci na restore (pozri api/history.rebuild_core).
|
|
series: Mapped[int] = mapped_column(Integer, default=0)
|
|
round: Mapped[int] = mapped_column(Integer, default=0)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime, server_default=func.now()
|
|
)
|
|
ended_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
|
|
def player_id_for_seat(self, seat: int) -> int:
|
|
return (self.player0_id, self.player1_id, self.player2_id, self.player3_id)[seat]
|
|
|
|
|
|
class Guess(Base):
|
|
"""Tip a vysledok jedneho hraca v jednom kole.
|
|
|
|
Unikat (game_id, series_number, round_number, player_id) robi zapis
|
|
idempotentnym -- opakovany `record_completed_rounds` nezaklada duplikaty.
|
|
"""
|
|
|
|
__tablename__ = "guesses"
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"game_id",
|
|
"series_number",
|
|
"round_number",
|
|
"player_id",
|
|
name="uq_guess_round_player",
|
|
),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
game_id: Mapped[str] = mapped_column(ForeignKey("games.id"), index=True)
|
|
player_id: Mapped[int] = mapped_column(ForeignKey("players.id"), index=True)
|
|
series_number: Mapped[int] = mapped_column(Integer)
|
|
round_number: Mapped[int] = mapped_column(Integer)
|
|
guess: Mapped[int] = mapped_column(Integer)
|
|
points: Mapped[int] = mapped_column(Integer)
|
|
|
|
|
|
class PageView(Base):
|
|
"""Jedna navsteva stranky (pre /track). Bez schema migracii -- create_all only."""
|
|
|
|
__tablename__ = "page_views"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
# Konvencia: zacina "/" -> skutocna URL cesta; inak nazov eventu (napr.
|
|
# "rules_view", "login") -- ziadny osobitny stlpec, rovnaky stlpec pre oboje.
|
|
path: Mapped[str] = mapped_column(String(200))
|
|
# Vyplnene len ak je znamy prihlaseny hrac v momente zapisu (napr. event "login").
|
|
player_id: Mapped[int | None] = mapped_column(
|
|
ForeignKey("players.id"), nullable=True, index=True
|
|
)
|
|
referrer: Mapped[str] = mapped_column(String(300), default="")
|
|
user_agent: Mapped[str] = mapped_column(String(300), default="")
|
|
browser: Mapped[str] = mapped_column(String(40), default="")
|
|
os: Mapped[str] = mapped_column(String(40), default="")
|
|
device_type: Mapped[str] = mapped_column(String(20), default="") # mobile/tablet/pc/bot
|
|
ip: Mapped[str] = mapped_column(String(45), default="") # surova IP (IPv4/IPv6), "" ak nezname
|
|
country: Mapped[str] = mapped_column(String(60), default="") # anglicky nazov z GeoIP, "" ak nerozlusene
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime, server_default=func.now()
|
|
)
|