Add multi-league foundation
This commit is contained in:
+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