Add multi-league foundation
This commit is contained in:
@@ -77,6 +77,11 @@ def migrate_schema():
|
||||
# Check Team table
|
||||
if 'teams' in tables:
|
||||
columns = [col['name'] for col in inspector.get_columns('teams')]
|
||||
if 'league_id' not in columns:
|
||||
print("Adding league_id column to teams table")
|
||||
connection.execute(text(
|
||||
"ALTER TABLE teams ADD COLUMN league_id INT NULL"
|
||||
))
|
||||
if 'is_public' not in columns:
|
||||
print("Adding is_public column to teams table")
|
||||
connection.execute(text(
|
||||
@@ -109,6 +114,11 @@ def migrate_schema():
|
||||
# Check QRCode table (formerly QRTicket)
|
||||
if 'qr_codes' in tables:
|
||||
columns = [col['name'] for col in inspector.get_columns('qr_codes')]
|
||||
if 'league_id' not in columns:
|
||||
print("Adding league_id column to qr_codes table")
|
||||
connection.execute(text(
|
||||
"ALTER TABLE qr_codes ADD COLUMN league_id INT NULL"
|
||||
))
|
||||
if 'created_at' not in columns:
|
||||
print("Adding created_at column to qr_codes table")
|
||||
connection.execute(text(
|
||||
@@ -121,6 +131,22 @@ def migrate_schema():
|
||||
))
|
||||
else:
|
||||
print("QRCodes table doesn't exist yet, skipping QRCode table migrations")
|
||||
|
||||
if 'events' in tables:
|
||||
columns = [col['name'] for col in inspector.get_columns('events')]
|
||||
if 'league_id' not in columns:
|
||||
print("Adding league_id column to events table")
|
||||
connection.execute(text(
|
||||
"ALTER TABLE events ADD COLUMN league_id INT NULL"
|
||||
))
|
||||
|
||||
if 'qr_sets' in tables:
|
||||
columns = [col['name'] for col in inspector.get_columns('qr_sets')]
|
||||
if 'league_id' not in columns:
|
||||
print("Adding league_id column to qr_sets table")
|
||||
connection.execute(text(
|
||||
"ALTER TABLE qr_sets ADD COLUMN league_id INT NULL"
|
||||
))
|
||||
|
||||
# Handle legacy QRTicket table migration if it exists
|
||||
if 'qr_tickets' in tables and 'qr_codes' in tables:
|
||||
@@ -177,6 +203,26 @@ def migrate_schema():
|
||||
)
|
||||
"""))
|
||||
connection.commit()
|
||||
|
||||
if 'leagues' not in tables:
|
||||
print("Creating leagues table")
|
||||
connection.execute(text("""
|
||||
CREATE TABLE leagues (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
slug VARCHAR(120) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
publisher_name VARCHAR(100),
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
connection.execute(text("""
|
||||
INSERT INTO leagues (name, slug, description, publisher_name, is_active)
|
||||
VALUES ('Default League', 'default', 'Default league for existing LeagueLedger data.', 'LeagueLedger', TRUE)
|
||||
"""))
|
||||
connection.commit()
|
||||
|
||||
# Create team_members table if it doesn't exist and teams table exists
|
||||
if 'team_members' not in tables and 'teams' in tables:
|
||||
@@ -216,6 +262,7 @@ def migrate_schema():
|
||||
connection.execute(text("""
|
||||
CREATE TABLE events (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
league_id INT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
location VARCHAR(200),
|
||||
@@ -262,6 +309,7 @@ def migrate_schema():
|
||||
connection.execute(text("""
|
||||
CREATE TABLE qr_sets (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
league_id INT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
+28
-2
@@ -10,7 +10,7 @@ from sqlalchemy.orm import Session
|
||||
from passlib.context import CryptContext
|
||||
import asyncio
|
||||
|
||||
from .models import User, Team, TeamMembership, QRCode, QRSet, TeamAchievement, Event, SystemSettings
|
||||
from .models import User, League, Team, TeamMembership, QRCode, QRSet, TeamAchievement, Event, SystemSettings
|
||||
from .db import SessionLocal, engine
|
||||
from .db_migrations import run_migrations
|
||||
|
||||
@@ -150,6 +150,16 @@ def seed_db():
|
||||
]
|
||||
db.add_all(users)
|
||||
db.commit()
|
||||
|
||||
default_league = League(
|
||||
name="Default League",
|
||||
slug="default",
|
||||
description="Default league for LeagueLedger demo data.",
|
||||
publisher_name="LeagueLedger",
|
||||
is_active=True
|
||||
)
|
||||
db.add(default_league)
|
||||
db.commit()
|
||||
|
||||
# Create teams
|
||||
has_is_public = table_has_column(db.bind, 'teams', 'is_public')
|
||||
@@ -158,7 +168,7 @@ def seed_db():
|
||||
|
||||
teams = []
|
||||
for i, name in enumerate(["Quiz Wizards", "Trivia Titans", "Beer Brainiacs", "Knowledge Knights"]):
|
||||
team_attrs = {"name": name}
|
||||
team_attrs = {"name": name, "league_id": default_league.id}
|
||||
if has_is_public:
|
||||
team_attrs["is_public"] = i % 2 == 1 # Alternate public/private
|
||||
if has_description:
|
||||
@@ -203,21 +213,27 @@ def seed_db():
|
||||
# Create events for QR code linking
|
||||
events = [
|
||||
Event(name="Music Trivia Night", description="A night of musical quizzes",
|
||||
league_id=default_league.id,
|
||||
event_date=datetime.now() - timedelta(days=60),
|
||||
location="Irish Rover Pub"),
|
||||
Event(name="History Night", description="Test your history knowledge",
|
||||
league_id=default_league.id,
|
||||
event_date=datetime.now() - timedelta(days=45),
|
||||
location="Irish Rover Pub"),
|
||||
Event(name="Movie Trivia Night", description="All about cinema",
|
||||
league_id=default_league.id,
|
||||
event_date=datetime.now() - timedelta(days=30),
|
||||
location="Irish Rover Pub"),
|
||||
Event(name="Sports Quiz", description="For sports enthusiasts",
|
||||
league_id=default_league.id,
|
||||
event_date=datetime.now() - timedelta(days=15),
|
||||
location="Irish Rover Pub"),
|
||||
Event(name="General Knowledge", description="A bit of everything",
|
||||
league_id=default_league.id,
|
||||
event_date=datetime.now() - timedelta(days=7),
|
||||
location="Irish Rover Pub"),
|
||||
Event(name="Irish Rover Pub Quiz April 2025", description="Monthly pub quiz",
|
||||
league_id=default_league.id,
|
||||
event_date=datetime.now(),
|
||||
location="Irish Rover Pub")
|
||||
]
|
||||
@@ -227,11 +243,13 @@ def seed_db():
|
||||
# Create QR sets
|
||||
qr_sets = [
|
||||
QRSet(
|
||||
league_id=default_league.id,
|
||||
name="Standard Pub Quiz",
|
||||
description="Contains QR codes for 1st place (25 points), 2nd place (15 points), 3rd place (10 points), and 4th place (5 points)",
|
||||
created_by=1 # Admin user
|
||||
),
|
||||
QRSet(
|
||||
league_id=default_league.id,
|
||||
name="Trivia Night with Achievements",
|
||||
description="Special trivia night with QR codes for winners and achievement codes for trivia categories",
|
||||
created_by=2 # John the quizmaster
|
||||
@@ -248,6 +266,7 @@ def seed_db():
|
||||
qr_set_1 = qr_sets[0]
|
||||
qr_codes.extend([
|
||||
QRCode(
|
||||
league_id=default_league.id,
|
||||
code=str(uuid.uuid4()),
|
||||
points=25,
|
||||
title="1st Place",
|
||||
@@ -257,6 +276,7 @@ def seed_db():
|
||||
used=False
|
||||
),
|
||||
QRCode(
|
||||
league_id=default_league.id,
|
||||
code=str(uuid.uuid4()),
|
||||
points=15,
|
||||
title="2nd Place",
|
||||
@@ -266,6 +286,7 @@ def seed_db():
|
||||
used=False
|
||||
),
|
||||
QRCode(
|
||||
league_id=default_league.id,
|
||||
code=str(uuid.uuid4()),
|
||||
points=10,
|
||||
title="3rd Place",
|
||||
@@ -275,6 +296,7 @@ def seed_db():
|
||||
used=False
|
||||
),
|
||||
QRCode(
|
||||
league_id=default_league.id,
|
||||
code=str(uuid.uuid4()),
|
||||
points=5,
|
||||
title="4th Place",
|
||||
@@ -289,6 +311,7 @@ def seed_db():
|
||||
qr_set_2 = qr_sets[1]
|
||||
qr_codes.extend([
|
||||
QRCode(
|
||||
league_id=default_league.id,
|
||||
code=str(uuid.uuid4()),
|
||||
points=20,
|
||||
title="Trivia Champion",
|
||||
@@ -298,6 +321,7 @@ def seed_db():
|
||||
used=False
|
||||
),
|
||||
QRCode(
|
||||
league_id=default_league.id,
|
||||
code=str(uuid.uuid4()),
|
||||
points=0,
|
||||
title="Estimate Winner",
|
||||
@@ -308,6 +332,7 @@ def seed_db():
|
||||
used=False
|
||||
),
|
||||
QRCode(
|
||||
league_id=default_league.id,
|
||||
code=str(uuid.uuid4()),
|
||||
points=0,
|
||||
title="Film Buff",
|
||||
@@ -334,6 +359,7 @@ def seed_db():
|
||||
|
||||
qr_codes.append(
|
||||
QRCode(
|
||||
league_id=default_league.id,
|
||||
code=f"TICKET{i:03d}",
|
||||
points=points,
|
||||
title=f"{points} Points Ticket",
|
||||
|
||||
@@ -104,6 +104,9 @@ def run_migrations(engine):
|
||||
print("Running migrations...")
|
||||
|
||||
# Check if the columns already exist before adding them
|
||||
# Create league structures and attach existing records to a default league
|
||||
add_league_support(connection)
|
||||
|
||||
# Add additional_oauth_providers column if it doesn't exist
|
||||
add_oauth_providers_column(connection)
|
||||
|
||||
@@ -151,6 +154,156 @@ def add_oauth_providers_column(connection):
|
||||
except Exception as e:
|
||||
print(f"Error adding additional_oauth_providers column: {str(e)}")
|
||||
|
||||
def add_league_support(connection):
|
||||
"""Add leagues and backfill existing single-league data."""
|
||||
try:
|
||||
inspector = inspect(engine)
|
||||
tables = inspector.get_table_names()
|
||||
|
||||
if 'leagues' not in tables:
|
||||
print("Creating leagues table")
|
||||
if engine.name == 'sqlite':
|
||||
connection.execute(text("""
|
||||
CREATE TABLE leagues (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
slug VARCHAR(120) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
publisher_name VARCHAR(100),
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
else:
|
||||
connection.execute(text("""
|
||||
CREATE TABLE leagues (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
slug VARCHAR(120) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
publisher_name VARCHAR(100),
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
connection.commit()
|
||||
|
||||
league_id = ensure_default_league(connection)
|
||||
|
||||
for table_name in ('teams', 'qr_sets', 'qr_codes', 'events'):
|
||||
add_nullable_league_id(connection, table_name)
|
||||
|
||||
backfill_league_ids(connection, league_id)
|
||||
drop_global_team_name_unique(connection)
|
||||
except Exception as e:
|
||||
print(f"Error adding league support: {str(e)}")
|
||||
|
||||
def ensure_default_league(connection):
|
||||
"""Return the default league id, creating it if needed."""
|
||||
row = connection.execute(
|
||||
text("SELECT id FROM leagues WHERE slug = :slug"),
|
||||
{"slug": "default"}
|
||||
).first()
|
||||
if row:
|
||||
return row[0]
|
||||
|
||||
connection.execute(
|
||||
text("""
|
||||
INSERT INTO leagues (name, slug, description, publisher_name, is_active)
|
||||
VALUES (:name, :slug, :description, :publisher_name, :is_active)
|
||||
"""),
|
||||
{
|
||||
"name": "Default League",
|
||||
"slug": "default",
|
||||
"description": "Default league for existing LeagueLedger data.",
|
||||
"publisher_name": "LeagueLedger",
|
||||
"is_active": True,
|
||||
}
|
||||
)
|
||||
connection.commit()
|
||||
return connection.execute(
|
||||
text("SELECT id FROM leagues WHERE slug = :slug"),
|
||||
{"slug": "default"}
|
||||
).scalar()
|
||||
|
||||
def add_nullable_league_id(connection, table_name):
|
||||
"""Add a nullable league_id column to an existing table."""
|
||||
inspector = inspect(engine)
|
||||
if table_name not in inspector.get_table_names():
|
||||
return
|
||||
|
||||
columns = [col['name'] for col in inspector.get_columns(table_name)]
|
||||
if 'league_id' in columns:
|
||||
print(f"Column league_id already exists in {table_name}")
|
||||
return
|
||||
|
||||
print(f"Adding league_id column to {table_name}")
|
||||
if engine.name == 'sqlite':
|
||||
connection.execute(text(f"ALTER TABLE {table_name} ADD COLUMN league_id INTEGER NULL"))
|
||||
else:
|
||||
connection.execute(text(f"ALTER TABLE {table_name} ADD COLUMN league_id INT NULL"))
|
||||
connection.commit()
|
||||
|
||||
def backfill_league_ids(connection, default_league_id):
|
||||
"""Attach legacy data to the default league."""
|
||||
inspector = inspect(engine)
|
||||
tables = inspector.get_table_names()
|
||||
|
||||
for table_name in ('teams', 'qr_sets', 'events'):
|
||||
if table_name in tables and 'league_id' in [col['name'] for col in inspector.get_columns(table_name)]:
|
||||
connection.execute(
|
||||
text(f"UPDATE {table_name} SET league_id = :league_id WHERE league_id IS NULL"),
|
||||
{"league_id": default_league_id}
|
||||
)
|
||||
|
||||
if 'qr_codes' in tables and 'league_id' in [col['name'] for col in inspector.get_columns('qr_codes')]:
|
||||
connection.execute(text("""
|
||||
UPDATE qr_codes
|
||||
SET league_id = (
|
||||
SELECT qr_sets.league_id
|
||||
FROM qr_sets
|
||||
WHERE qr_sets.id = qr_codes.qr_set_id
|
||||
)
|
||||
WHERE league_id IS NULL
|
||||
AND qr_set_id IS NOT NULL
|
||||
"""))
|
||||
connection.execute(text("""
|
||||
UPDATE qr_codes
|
||||
SET league_id = (
|
||||
SELECT events.league_id
|
||||
FROM events
|
||||
WHERE events.id = qr_codes.event_id
|
||||
)
|
||||
WHERE league_id IS NULL
|
||||
AND event_id IS NOT NULL
|
||||
"""))
|
||||
connection.execute(
|
||||
text("UPDATE qr_codes SET league_id = :league_id WHERE league_id IS NULL"),
|
||||
{"league_id": default_league_id}
|
||||
)
|
||||
|
||||
connection.commit()
|
||||
|
||||
def drop_global_team_name_unique(connection):
|
||||
"""Best-effort removal of the legacy global team-name uniqueness constraint."""
|
||||
if engine.name == 'sqlite':
|
||||
return
|
||||
|
||||
inspector = inspect(engine)
|
||||
if 'teams' not in inspector.get_table_names():
|
||||
return
|
||||
|
||||
for constraint in inspector.get_unique_constraints('teams'):
|
||||
if constraint.get('column_names') == ['name']:
|
||||
constraint_name = constraint.get('name')
|
||||
if constraint_name:
|
||||
print(f"Dropping global teams.name unique constraint {constraint_name}")
|
||||
connection.execute(text(f"ALTER TABLE teams DROP INDEX {constraint_name}"))
|
||||
connection.commit()
|
||||
break
|
||||
|
||||
def add_name_columns(connection):
|
||||
"""Add first_name and last_name columns to users table"""
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .models import League, QRCode
|
||||
|
||||
DEFAULT_LEAGUE_NAME = "Default League"
|
||||
DEFAULT_LEAGUE_SLUG = "default"
|
||||
|
||||
|
||||
def slugify(value: str) -> str:
|
||||
slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
|
||||
return slug or DEFAULT_LEAGUE_SLUG
|
||||
|
||||
|
||||
def get_default_league(db: Session) -> League:
|
||||
league = db.query(League).filter(League.slug == DEFAULT_LEAGUE_SLUG).first()
|
||||
if league:
|
||||
return league
|
||||
|
||||
league = db.query(League).order_by(League.id).first()
|
||||
if league:
|
||||
return league
|
||||
|
||||
league = League(
|
||||
name=DEFAULT_LEAGUE_NAME,
|
||||
slug=DEFAULT_LEAGUE_SLUG,
|
||||
description="Default league for existing LeagueLedger data.",
|
||||
publisher_name="LeagueLedger",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(league)
|
||||
db.commit()
|
||||
db.refresh(league)
|
||||
return league
|
||||
|
||||
|
||||
def get_active_leagues(db: Session):
|
||||
leagues = db.query(League).filter(League.is_active == True).order_by(League.name).all()
|
||||
if leagues:
|
||||
return leagues
|
||||
return [get_default_league(db)]
|
||||
|
||||
|
||||
def parse_league_id(value) -> Optional[int]:
|
||||
try:
|
||||
return int(value) if value else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def resolve_selected_league(db: Session, league_id: Optional[int]) -> League:
|
||||
if league_id:
|
||||
league = db.query(League).filter(League.id == league_id, League.is_active == True).first()
|
||||
if league:
|
||||
return league
|
||||
return get_default_league(db)
|
||||
|
||||
|
||||
def qr_code_league_id(qr_code: QRCode) -> Optional[int]:
|
||||
if qr_code.league_id:
|
||||
return qr_code.league_id
|
||||
if qr_code.qr_set and qr_code.qr_set.league_id:
|
||||
return qr_code.qr_set.league_id
|
||||
if qr_code.event and qr_code.event.league_id:
|
||||
return qr_code.event.league_id
|
||||
return None
|
||||
+31
-1
@@ -114,10 +114,31 @@ class OAuthAccount(Base):
|
||||
user = relationship("User")
|
||||
|
||||
|
||||
class League(Base):
|
||||
__tablename__ = "leagues"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), unique=True, nullable=False)
|
||||
slug = Column(String(120), unique=True, nullable=False, index=True)
|
||||
description = Column(Text, nullable=True)
|
||||
publisher_name = Column(String(100), nullable=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
teams = relationship("Team", back_populates="league")
|
||||
qr_sets = relationship("QRSet", back_populates="league")
|
||||
qr_codes = relationship("QRCode", back_populates="league")
|
||||
events = relationship("Event", back_populates="league")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<League {self.name}>"
|
||||
|
||||
|
||||
class Team(Base):
|
||||
__tablename__ = "teams"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), unique=True, nullable=False)
|
||||
league_id = Column(Integer, ForeignKey("leagues.id"), nullable=True, index=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
logo_url = Column(String(255), nullable=True) # Add logo URL field
|
||||
is_public = Column(Boolean, default=False) # For team privacy setting
|
||||
@@ -128,9 +149,12 @@ class Team(Base):
|
||||
owner_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
# Relationships
|
||||
league = relationship("League", back_populates="teams")
|
||||
members = relationship("TeamMembership", back_populates="team", cascade="all, delete-orphan")
|
||||
owner = relationship("User", back_populates="owned_teams")
|
||||
|
||||
__table_args__ = (UniqueConstraint('league_id', 'name', name='_league_team_name_uc'),)
|
||||
|
||||
|
||||
class TeamJoinRequest(Base):
|
||||
__tablename__ = "team_join_requests"
|
||||
@@ -169,12 +193,14 @@ class QRSet(Base):
|
||||
"""A set of related QR codes, such as codes for different placements in a quiz"""
|
||||
__tablename__ = "qr_sets"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
league_id = Column(Integer, ForeignKey("leagues.id"), nullable=True, index=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
# Relationships
|
||||
league = relationship("League", back_populates="qr_sets")
|
||||
qr_codes = relationship("QRCode", back_populates="qr_set")
|
||||
creator = relationship("User")
|
||||
|
||||
@@ -183,12 +209,14 @@ class QRCode(Base):
|
||||
"""Unified QR code model that includes all the functionality of the old QRTicket and QRCode models"""
|
||||
__tablename__ = "qr_codes"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
league_id = Column(Integer, ForeignKey("leagues.id"), nullable=True, index=True)
|
||||
code = Column(String(128), unique=True, index=True, nullable=False) # Unique token
|
||||
points = Column(Float, default=0, nullable=False)
|
||||
title = Column(String(100), nullable=True) # e.g., "1st Place", "2nd Place"
|
||||
description = Column(String(255), nullable=True)
|
||||
|
||||
# Set relationship
|
||||
league = relationship("League", back_populates="qr_codes")
|
||||
qr_set_id = Column(Integer, ForeignKey("qr_sets.id"), nullable=True)
|
||||
qr_set = relationship("QRSet", back_populates="qr_codes")
|
||||
|
||||
@@ -236,6 +264,7 @@ class TeamAchievement(Base):
|
||||
class Event(Base):
|
||||
__tablename__ = "events"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
league_id = Column(Integer, ForeignKey("leagues.id"), nullable=True, index=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
location = Column(String(200), nullable=True)
|
||||
@@ -244,6 +273,7 @@ class Event(Base):
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
league = relationship("League", back_populates="events")
|
||||
attendees = relationship("EventAttendee", back_populates="event")
|
||||
|
||||
|
||||
|
||||
@@ -4,11 +4,22 @@
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl md:text-3xl font-garamond font-bold text-irish-green">League Leaderboard</h1>
|
||||
<p class="text-gray-600">See how your team ranks against the competition</p>
|
||||
<p class="text-gray-600">
|
||||
See how your team ranks in {{ selected_league.name if selected_league else "your league" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 md:mt-0">
|
||||
<form method="get" action="/leaderboard/">
|
||||
<form method="get" action="/leaderboard/" class="flex flex-col sm:flex-row gap-3">
|
||||
<select name="league_id"
|
||||
onchange="this.form.submit()"
|
||||
class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-irish-green bg-white">
|
||||
{% for league in leagues %}
|
||||
<option value="{{ league.id }}" {% if selected_league and selected_league.id == league.id %}selected{% endif %}>
|
||||
{{ league.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="timeframe"
|
||||
onchange="this.form.submit()"
|
||||
class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-irish-green bg-white">
|
||||
|
||||
@@ -6,6 +6,20 @@
|
||||
<p class="text-gray-600">Create and manage QR code sets for your pub quiz events</p>
|
||||
</div>
|
||||
|
||||
<form method="get" action="/qr/" class="mb-6 flex flex-col sm:flex-row sm:items-end gap-3">
|
||||
<div>
|
||||
<label for="league_filter" class="block text-sm font-medium text-gray-700">League</label>
|
||||
<select id="league_filter" name="league_id" onchange="this.form.submit()"
|
||||
class="mt-1 border border-gray-300 rounded-md px-3 py-2 bg-white focus:outline-none focus:ring-2 focus:ring-irish-green">
|
||||
{% for league in leagues %}
|
||||
<option value="{{ league.id }}" {% if selected_league and selected_league.id == league.id %}selected{% endif %}>
|
||||
{{ league.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-8">
|
||||
<!-- QR Set Creation -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
@@ -15,6 +29,7 @@
|
||||
</p>
|
||||
|
||||
<form id="qrSetForm" action="/qr/sets" method="post" class="space-y-4">
|
||||
<input type="hidden" name="league_id" value="{{ selected_league.id }}">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Set Name</label>
|
||||
<input type="text" name="name" id="name" required
|
||||
@@ -140,7 +155,8 @@
|
||||
<div class="px-6 py-4">
|
||||
<div class="text-sm text-gray-500">
|
||||
<p>Created: {{ qr_set.created_at.strftime('%Y-%m-%d') }}</p>
|
||||
<p>QR Codes: {% if qr_set.qr_codes %}{{ qr_set.qr_codes | length }}{% else %}0{% endif %}</p>
|
||||
<p>League: {{ qr_set.league.name if qr_set.league else "Default League" }}</p>
|
||||
<p>QR Codes: {% if qr_set.qr_codes %}{{ qr_set.qr_codes | length }}{% else %}0{% endif %}</p>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-between">
|
||||
<a href="/qr/sets/{{ qr_set.id }}" class="text-irish-green hover:underline">
|
||||
@@ -199,11 +215,12 @@
|
||||
quickQrForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const points = document.getElementById('qr-points').value;
|
||||
const leagueId = '{{ selected_league.id }}';
|
||||
|
||||
// Generate QR code
|
||||
qrImage.innerHTML = '<img src="/qr/generate/' + points + '" alt="QR Code" class="mx-auto" />';
|
||||
qrImage.innerHTML = '<img src="/qr/generate/' + points + '?league_id=' + leagueId + '" alt="QR Code" class="mx-auto" />';
|
||||
qrResult.classList.remove('hidden');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="max-w-6xl mx-auto p-4">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center">
|
||||
<a href="/qr" class="text-irish-green hover:underline mr-4">
|
||||
<a href="/qr{% if qr_set.league_id %}/?league_id={{ qr_set.league_id }}{% endif %}" class="text-irish-green hover:underline mr-4">
|
||||
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||
</a>
|
||||
<h1 class="text-3xl font-bold text-irish-green">{{ qr_set.name }}</h1>
|
||||
@@ -11,7 +11,9 @@
|
||||
{% if qr_set.description %}
|
||||
<p class="text-gray-600 mt-2">{{ qr_set.description }}</p>
|
||||
{% endif %}
|
||||
<p class="text-sm text-gray-500 mt-1">Created on {{ qr_set.created_at.strftime('%Y-%m-%d') }}</p>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
{{ qr_set.league.name if qr_set.league else "Default League" }} • Created on {{ qr_set.created_at.strftime('%Y-%m-%d') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-8">
|
||||
@@ -270,4 +272,4 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<div class="flex flex-col md:flex-row justify-between items-center">
|
||||
<div class="mb-4 md:mb-0">
|
||||
<h1 class="text-2xl md:text-3xl font-bold text-white">{{ team.name }}</h1>
|
||||
<p class="text-green-100 mb-1">{{ team.league.name if team.league else "Default League" }}</p>
|
||||
<div class="flex items-center space-x-2 text-green-100">
|
||||
<span><i class="fas fa-trophy mr-1"></i> Rank #{{ team_rank }}</span>
|
||||
<span class="hidden md:inline">•</span>
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-2xl font-bold mb-6">Teams</h2>
|
||||
<div class="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold">Teams</h2>
|
||||
{% if selected_league %}
|
||||
<p class="text-gray-600">{{ selected_league.name }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<form method="get" action="/teams/">
|
||||
<label for="league_id" class="block text-sm font-medium text-gray-700">League</label>
|
||||
<select id="league_id" name="league_id" onchange="this.form.submit()"
|
||||
class="mt-1 border border-gray-300 rounded-md px-3 py-2 bg-white focus:outline-none focus:ring-2 focus:ring-irish-green">
|
||||
{% for league in leagues %}
|
||||
<option value="{{ league.id }}" {% if selected_league and selected_league.id == league.id %}selected{% endif %}>
|
||||
{{ league.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-6" role="alert">
|
||||
@@ -48,6 +66,7 @@
|
||||
<h3 class="text-xl font-semibold mb-4" style="color: var(--irish-green);">Create New Team</h3>
|
||||
{% if user %}
|
||||
<form action="/teams/create" method="post" class="mt-4">
|
||||
<input type="hidden" name="league_id" value="{{ selected_league.id }}">
|
||||
<div class="mb-4">
|
||||
<label for="name" class="block text-sm font-medium mb-1">Team Name</label>
|
||||
<input type="text" name="name" id="name" placeholder="Enter team name" required
|
||||
|
||||
+2
-1
@@ -17,7 +17,7 @@ from dateutil.relativedelta import relativedelta
|
||||
|
||||
from ..db import SessionLocal, Base
|
||||
from ..models import (
|
||||
User, Team, TeamMembership, QRCode, QRSet, TeamAchievement, Event,
|
||||
User, League, Team, TeamMembership, QRCode, QRSet, TeamAchievement, Event,
|
||||
OAuthAccount, TeamJoinRequest, EventAttendee, UserPoints
|
||||
)
|
||||
from ..templates_config import templates
|
||||
@@ -28,6 +28,7 @@ router = APIRouter()
|
||||
# Dictionary of model classes with their display names
|
||||
MODELS = {
|
||||
'user': (User, "Users"),
|
||||
'league': (League, "Leagues"),
|
||||
'team': (Team, "Teams"),
|
||||
'team_membership': (TeamMembership, "Team Memberships"),
|
||||
'qr_code': (QRCode, "QR Codes"),
|
||||
|
||||
+15
-8
@@ -96,21 +96,27 @@ def user_dashboard(request: Request, db: Session = Depends(get_db)):
|
||||
user_teams_enhanced = []
|
||||
|
||||
if user_teams:
|
||||
# Get all team points to calculate ranks
|
||||
# Get all team points to calculate ranks within each league
|
||||
team_points_query = db.query(
|
||||
models.Team.id,
|
||||
models.Team.league_id,
|
||||
func.sum(models.QRCode.points).label('total_points')
|
||||
).outerjoin(
|
||||
models.QRCode,
|
||||
models.QRCode.redeemed_at_team == models.Team.id
|
||||
).group_by(models.Team.id)
|
||||
).group_by(models.Team.id, models.Team.league_id)
|
||||
|
||||
# Get results and sort by points in descending order
|
||||
team_points = {row.id: row.total_points or 0 for row in team_points_query.all()}
|
||||
ranked_teams = sorted(team_points.items(), key=lambda x: x[1], reverse=True)
|
||||
|
||||
# Create a dictionary mapping team ID to rank
|
||||
team_ranks = {team_id: i+1 for i, (team_id, _) in enumerate(ranked_teams)}
|
||||
team_points = {}
|
||||
points_by_league = {}
|
||||
for row in team_points_query.all():
|
||||
points = row.total_points or 0
|
||||
team_points[row.id] = points
|
||||
points_by_league.setdefault(row.league_id, []).append((row.id, points))
|
||||
|
||||
team_ranks = {}
|
||||
for teams_in_league in points_by_league.values():
|
||||
ranked_teams = sorted(teams_in_league, key=lambda x: x[1], reverse=True)
|
||||
team_ranks.update({team_id: i + 1 for i, (team_id, _) in enumerate(ranked_teams)})
|
||||
|
||||
# Enhance user teams with rank and points data
|
||||
for team in user_teams:
|
||||
@@ -120,6 +126,7 @@ def user_dashboard(request: Request, db: Session = Depends(get_db)):
|
||||
user_teams_enhanced.append({
|
||||
'id': team.id,
|
||||
'name': team.name,
|
||||
'league_name': team.league.name if team.league else "Default League",
|
||||
'points': points,
|
||||
'rank': rank,
|
||||
'description': getattr(team, 'description', None)
|
||||
|
||||
@@ -10,6 +10,7 @@ from datetime import datetime, timedelta
|
||||
|
||||
from ..db import SessionLocal
|
||||
from ..models import Team, TeamMembership, QRCode, User
|
||||
from ..league_context import get_active_leagues, resolve_selected_league
|
||||
from ..templates_config import templates
|
||||
|
||||
router = APIRouter()
|
||||
@@ -25,6 +26,7 @@ def get_db():
|
||||
async def show_leaderboard(
|
||||
request: Request,
|
||||
timeframe: str = Query("all", regex="^(week|month|all)$"),
|
||||
league_id: int = Query(None),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Show the leaderboard with team rankings."""
|
||||
@@ -34,6 +36,9 @@ async def show_leaderboard(
|
||||
user_id = request.session.get("user_id")
|
||||
if user_id:
|
||||
user = db.query(User).get(user_id)
|
||||
|
||||
selected_league = resolve_selected_league(db, league_id)
|
||||
leagues = get_active_leagues(db)
|
||||
|
||||
# Define cutoff date based on timeframe
|
||||
cutoff_date = None
|
||||
@@ -56,7 +61,7 @@ async def show_leaderboard(
|
||||
QRCode,
|
||||
QRCode.redeemed_at_team == Team.id,
|
||||
isouter=True
|
||||
)
|
||||
).filter(Team.league_id == selected_league.id)
|
||||
|
||||
# Apply time filter if needed
|
||||
if cutoff_date:
|
||||
@@ -64,7 +69,7 @@ async def show_leaderboard(
|
||||
query = query.filter(QRCode.redeemed_at >= cutoff_date)
|
||||
|
||||
# Group and order
|
||||
teams_ranking = query.group_by(Team.id).order_by(desc('total_points')).all()
|
||||
teams_ranking = query.group_by(Team.id, Team.name).order_by(desc('total_points')).all()
|
||||
|
||||
# Add ranks
|
||||
ranked_teams = []
|
||||
@@ -88,6 +93,8 @@ async def show_leaderboard(
|
||||
"top_teams": top_teams,
|
||||
"timeframe": timeframe,
|
||||
"time_label": time_label,
|
||||
"leagues": leagues,
|
||||
"selected_league": selected_league,
|
||||
"user": user # Add user to the context
|
||||
}
|
||||
)
|
||||
|
||||
+19
-8
@@ -21,6 +21,7 @@ from pydantic import BaseModel
|
||||
|
||||
from ..db import SessionLocal
|
||||
from ..models import QRCode, QRSet, Event, User
|
||||
from ..league_context import get_active_leagues, parse_league_id, resolve_selected_league
|
||||
from ..templates_config import templates
|
||||
|
||||
router = APIRouter()
|
||||
@@ -61,16 +62,21 @@ async def qr_dashboard(request: Request, db: Session = Depends(get_db)):
|
||||
if user_id:
|
||||
user = db.query(User).get(user_id)
|
||||
|
||||
# Get all QR sets
|
||||
qr_sets = db.query(QRSet).all()
|
||||
league_id = request.query_params.get("league_id")
|
||||
selected_league = resolve_selected_league(db, parse_league_id(league_id))
|
||||
leagues = get_active_leagues(db)
|
||||
|
||||
# Get QR sets and events for the selected league
|
||||
qr_sets = db.query(QRSet).filter(QRSet.league_id == selected_league.id).all()
|
||||
|
||||
# Get all events for linking
|
||||
events = db.query(Event).all()
|
||||
events = db.query(Event).filter(Event.league_id == selected_league.id).all()
|
||||
|
||||
return templates.TemplateResponse("qr/dashboard.html", {
|
||||
"request": request,
|
||||
"qr_sets": qr_sets,
|
||||
"events": events,
|
||||
"leagues": leagues,
|
||||
"selected_league": selected_league,
|
||||
"user": user # Add user to the context
|
||||
})
|
||||
|
||||
@@ -85,12 +91,14 @@ async def create_qr_set(
|
||||
form_data = await request.form()
|
||||
name = form_data.get("name")
|
||||
description = form_data.get("description", "")
|
||||
league_id = form_data.get("league_id")
|
||||
selected_league = resolve_selected_league(db, parse_league_id(league_id))
|
||||
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="Set name is required")
|
||||
|
||||
# Create QR set
|
||||
qr_set = QRSet(name=name, description=description)
|
||||
qr_set = QRSet(name=name, description=description, league_id=selected_league.id)
|
||||
db.add(qr_set)
|
||||
db.commit()
|
||||
db.refresh(qr_set)
|
||||
@@ -148,6 +156,7 @@ async def add_qr_code_to_set(
|
||||
|
||||
# Create QR code
|
||||
qr_code = QRCode(
|
||||
league_id=qr_set.league_id,
|
||||
code=code_str,
|
||||
title=title,
|
||||
points=points,
|
||||
@@ -165,14 +174,15 @@ async def add_qr_code_to_set(
|
||||
|
||||
|
||||
@router.get("/generate/{points}")
|
||||
def generate_qr(points: int, db: Session = Depends(get_db)):
|
||||
def generate_qr(points: int, league_id: Optional[int] = Query(None), db: Session = Depends(get_db)):
|
||||
"""
|
||||
Generate a single QR code for awarding `points` points.
|
||||
Saves a record in the DB, returns the PNG as streaming response.
|
||||
"""
|
||||
code_str = str(uuid.uuid4())
|
||||
|
||||
qr_code = QRCode(code=code_str, points=points)
|
||||
selected_league = resolve_selected_league(db, league_id)
|
||||
qr_code = QRCode(code=code_str, points=points, league_id=selected_league.id)
|
||||
db.add(qr_code)
|
||||
db.commit()
|
||||
db.refresh(qr_code)
|
||||
@@ -239,7 +249,7 @@ async def admin_link_page(request: Request, admin_code: str, db: Session = Depen
|
||||
raise HTTPException(status_code=404, detail="QR Set not found")
|
||||
|
||||
# Fetch available events
|
||||
events = db.query(Event).all()
|
||||
events = db.query(Event).filter(Event.league_id == qr_set.league_id).all()
|
||||
|
||||
return templates.TemplateResponse("qr/admin_link.html", {
|
||||
"request": request,
|
||||
@@ -273,6 +283,7 @@ async def process_admin_link(
|
||||
# Create new event
|
||||
event_date = datetime.now() # Default to current date, can be improved
|
||||
new_event = Event(
|
||||
league_id=qr_set.league_id,
|
||||
name=event_name,
|
||||
description=f"Created via QR admin link on {event_date.strftime('%Y-%m-%d')}",
|
||||
event_date=event_date
|
||||
|
||||
+28
-3
@@ -10,6 +10,7 @@ from datetime import datetime
|
||||
from ..db import SessionLocal
|
||||
from ..models import QRCode, User, Team, TeamMembership, TeamAchievement
|
||||
from ..templates_config import templates
|
||||
from ..league_context import get_default_league, qr_code_league_id
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -73,14 +74,21 @@ def redeem_code(code: str, request: Request, db: Session = Depends(get_db)):
|
||||
}
|
||||
)
|
||||
|
||||
# Get only teams the user is a member of, if logged in
|
||||
effective_league_id = qr_code_league_id(qr_code)
|
||||
if not effective_league_id:
|
||||
effective_league_id = get_default_league(db).id
|
||||
|
||||
# Get only teams the user is a member of in the QR code's league, if logged in
|
||||
user_teams = []
|
||||
if user:
|
||||
# Query teams where the user is a member using TeamMembership relation
|
||||
user_teams = (
|
||||
db.query(Team)
|
||||
.join(TeamMembership, Team.id == TeamMembership.team_id)
|
||||
.filter(TeamMembership.user_id == user.id)
|
||||
.filter(
|
||||
TeamMembership.user_id == user.id,
|
||||
Team.league_id == effective_league_id
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
@@ -101,7 +109,7 @@ def redeem_code(code: str, request: Request, db: Session = Depends(get_db)):
|
||||
{
|
||||
"request": request,
|
||||
"error_title": "No Teams Available",
|
||||
"error_message": "You are not a member of any teams. Please join or create a team before redeeming QR codes.",
|
||||
"error_message": "You are not a member of any teams in this league. Please join or create a team before redeeming this QR code.",
|
||||
"user": user
|
||||
}
|
||||
)
|
||||
@@ -220,6 +228,21 @@ async def apply_code(
|
||||
"user": user # Add user to the context
|
||||
}
|
||||
)
|
||||
|
||||
effective_league_id = qr_code_league_id(qr_code)
|
||||
if not effective_league_id:
|
||||
effective_league_id = get_default_league(db).id
|
||||
|
||||
if team.league_id != effective_league_id:
|
||||
return templates.TemplateResponse(
|
||||
"error.html",
|
||||
{
|
||||
"request": request,
|
||||
"error_title": "Wrong League",
|
||||
"error_message": "This QR code can only be redeemed by a team in its league.",
|
||||
"user": user
|
||||
}
|
||||
)
|
||||
|
||||
# Verify the user is a member of the selected team
|
||||
is_team_member = db.query(TeamMembership).filter_by(
|
||||
@@ -239,6 +262,8 @@ async def apply_code(
|
||||
)
|
||||
|
||||
# Mark the QR code as redeemed
|
||||
qr_code.league_id = effective_league_id
|
||||
qr_code.redeemed_by = user.id
|
||||
qr_code.redeemed_at_team = team.id
|
||||
qr_code.redeemed_at = datetime.now()
|
||||
qr_code.used = True
|
||||
|
||||
@@ -7,6 +7,7 @@ from starlette.status import HTTP_303_SEE_OTHER
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from ...models import Team, TeamMembership, User, TeamJoinRequest
|
||||
from ...league_context import resolve_selected_league
|
||||
from ...utils.auth import get_current_user
|
||||
from ...utils.mail import send_team_join_request_notification, send_join_request_response
|
||||
from ...templates_config import templates
|
||||
@@ -19,6 +20,7 @@ async def create_team_post(
|
||||
description: str = Form(""),
|
||||
logo_url: str = Form(""),
|
||||
is_open: bool = Form(False),
|
||||
league_id: int = Form(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@@ -26,14 +28,23 @@ async def create_team_post(
|
||||
if not current_user:
|
||||
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
# Check if team name already exists
|
||||
existing_team = db.query(Team).filter(Team.name == name).first()
|
||||
selected_league = resolve_selected_league(db, league_id)
|
||||
|
||||
# Check if team name already exists in this league
|
||||
existing_team = db.query(Team).filter(
|
||||
Team.name == name,
|
||||
Team.league_id == selected_league.id
|
||||
).first()
|
||||
if existing_team:
|
||||
return RedirectResponse("/teams/?error=Team+name+already+exists", status_code=HTTP_303_SEE_OTHER)
|
||||
return RedirectResponse(
|
||||
f"/teams/?league_id={selected_league.id}&error=Team+name+already+exists",
|
||||
status_code=HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
# Create team with only the fields that exist in the model
|
||||
team_data = {
|
||||
"name": name,
|
||||
"league_id": selected_league.id,
|
||||
"description": description,
|
||||
"is_open": is_open,
|
||||
"owner_id": current_user.id
|
||||
@@ -60,7 +71,7 @@ async def create_team_post(
|
||||
db.add(team_membership)
|
||||
db.commit()
|
||||
|
||||
return RedirectResponse("/teams/", status_code=HTTP_303_SEE_OTHER)
|
||||
return RedirectResponse(f"/teams/?league_id={selected_league.id}", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
async def edit_team_post(
|
||||
request: Request,
|
||||
|
||||
@@ -73,13 +73,20 @@ def get_team_total_points(db: Session, team_id: int):
|
||||
def calculate_team_rank(db: Session, team_id: int):
|
||||
"""Calculate team rank based on points"""
|
||||
try:
|
||||
# First, get the aggregated points for all teams
|
||||
team = db.query(Team).filter(Team.id == team_id).first()
|
||||
if not team:
|
||||
return 1
|
||||
|
||||
# First, get the aggregated points for teams in the same league
|
||||
team_points = db.query(
|
||||
QRCode.redeemed_at_team,
|
||||
Team.id.label("team_id"),
|
||||
func.sum(QRCode.points).label('total')
|
||||
).outerjoin(
|
||||
QRCode,
|
||||
QRCode.redeemed_at_team == Team.id
|
||||
).filter(
|
||||
QRCode.redeemed_at_team != None
|
||||
).group_by(QRCode.redeemed_at_team).all()
|
||||
Team.league_id == team.league_id
|
||||
).group_by(Team.id).all()
|
||||
|
||||
# Sort them by points (descending)
|
||||
sorted_teams = sorted(team_points, key=lambda x: x.total or 0, reverse=True)
|
||||
@@ -87,7 +94,7 @@ def calculate_team_rank(db: Session, team_id: int):
|
||||
# Find our team's position
|
||||
team_rank = 1
|
||||
for idx, team_data in enumerate(sorted_teams):
|
||||
if team_data.redeemed_at_team == team_id:
|
||||
if team_data.team_id == team_id:
|
||||
team_rank = idx + 1
|
||||
break
|
||||
|
||||
|
||||
@@ -8,13 +8,17 @@ import random
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from ...models import Team, TeamMembership, User, QRCode, TeamJoinRequest
|
||||
from ...league_context import get_active_leagues, parse_league_id, resolve_selected_league
|
||||
from ...templates_config import templates
|
||||
from ...utils.auth import get_current_user
|
||||
from . import utils
|
||||
|
||||
def list_teams_view(request: Request, db: Session):
|
||||
"""Render the teams list view"""
|
||||
teams = db.query(Team).all()
|
||||
league_id = request.query_params.get("league_id")
|
||||
selected_league = resolve_selected_league(db, parse_league_id(league_id))
|
||||
leagues = get_active_leagues(db)
|
||||
teams = db.query(Team).filter(Team.league_id == selected_league.id).all()
|
||||
|
||||
# Get the user's teams to highlight teams they're already in
|
||||
user_team_ids = []
|
||||
@@ -25,7 +29,15 @@ def list_teams_view(request: Request, db: Session):
|
||||
if user_id:
|
||||
user = db.query(User).get(user_id)
|
||||
# Get teams that user is a member of
|
||||
memberships = db.query(TeamMembership).filter(TeamMembership.user_id == user_id).all()
|
||||
memberships = (
|
||||
db.query(TeamMembership)
|
||||
.join(Team)
|
||||
.filter(
|
||||
TeamMembership.user_id == user_id,
|
||||
Team.league_id == selected_league.id
|
||||
)
|
||||
.all()
|
||||
)
|
||||
user_team_ids = [membership.team_id for membership in memberships]
|
||||
|
||||
# Get error message if present
|
||||
@@ -36,6 +48,8 @@ def list_teams_view(request: Request, db: Session):
|
||||
{
|
||||
"request": request,
|
||||
"teams": teams,
|
||||
"leagues": leagues,
|
||||
"selected_league": selected_league,
|
||||
"user_team_ids": user_team_ids,
|
||||
"user": user,
|
||||
"error": error,
|
||||
|
||||
Reference in New Issue
Block a user