Add Impressum and QR code management templates

- Created a new Impressum page with legal information and contact details.
- Developed admin link page for linking QR codes to events with form functionality.
- Implemented QR code dashboard for managing QR code sets, including creation and quick actions.
- Added detailed view for QR code sets, allowing addition of QR codes and management actions.
- Introduced static file serving for favicon and related images.
- Established views for static content pages (about, contact, privacy, terms).
- Implemented translation management scripts for compiling and updating translations.
This commit is contained in:
Christian Krakau-Louis
2025-04-14 03:19:41 +02:00
parent 9eef726d9b
commit d595b468e9
43 changed files with 2895 additions and 391 deletions
+1
View File
@@ -0,0 +1 @@
# Import your view modules here
+5 -2
View File
@@ -11,7 +11,7 @@ from typing import Dict, Any, List, Type, Optional
import inspect as py_inspect
from ..db import SessionLocal, Base
from ..models import User, Team, TeamMembership, QRTicket
from ..models import User, Team, TeamMembership, QRCode, QRSet, TeamAchievement, Event
from ..templates_config import templates
router = APIRouter()
@@ -21,7 +21,10 @@ MODELS = {
'user': (User, "Users"),
'team': (Team, "Teams"),
'team_membership': (TeamMembership, "Team Memberships"),
'qr_ticket': (QRTicket, "QR Tickets"),
'qr_code': (QRCode, "QR Codes"),
'qr_set': (QRSet, "QR Sets"),
'team_achievement': (TeamAchievement, "Team Achievements"),
'event': (Event, "Events"),
}
def get_db():
+6 -9
View File
@@ -9,7 +9,7 @@ from sqlalchemy import func, desc
from datetime import datetime, timedelta
from ..db import SessionLocal
from ..models import Team, TeamMembership, QRTicket
from ..models import Team, TeamMembership, QRCode
from ..templates_config import templates
router = APIRouter()
@@ -45,20 +45,17 @@ async def show_leaderboard(
query = db.query(
Team.id,
Team.name,
func.coalesce(func.sum(QRTicket.points), 0).label('total_points')
func.coalesce(func.sum(QRCode.points), 0).label('total_points')
).join(
QRTicket,
QRTicket.redeemed_at_team == Team.id,
QRCode,
QRCode.redeemed_at_team == Team.id,
isouter=True
)
# Apply time filter if needed
if cutoff_date:
# Note: This assumes QRTicket has a created_at or similar timestamp field
# If not, you would need to add one to track when points were added
# For now, this is a placeholder that assumes all tickets are from "now"
# query = query.filter(QRTicket.created_at >= cutoff_date)
pass
# Filter by redeemed_at timestamp if available
query = query.filter(QRCode.redeemed_at >= cutoff_date)
# Group and order
teams_ranking = query.group_by(Team.id).order_by(desc('total_points')).all()
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""
Router for static content pages like about, contact, privacy, and terms.
"""
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from ..templates_config import templates
from datetime import datetime
router = APIRouter()
@router.get("/", response_class=HTMLResponse)
def index(request: Request):
"""Home page."""
return templates.TemplateResponse("index.html", {
"request": request,
"now": datetime.now
})
@router.get("/about", response_class=HTMLResponse)
def about(request: Request):
"""About page."""
return templates.TemplateResponse("about.html", {
"request": request
})
@router.get("/contact", response_class=HTMLResponse)
def contact(request: Request):
"""Contact page."""
return templates.TemplateResponse("contact.html", {
"request": request
})
@router.get("/privacy", response_class=HTMLResponse)
def privacy(request: Request):
"""Privacy policy page."""
return templates.TemplateResponse("privacy.html", {
"request": request
})
@router.get("/terms", response_class=HTMLResponse)
def terms(request: Request):
"""Terms and conditions page."""
return templates.TemplateResponse("terms.html", {
"request": request
})
+391 -10
View File
@@ -4,15 +4,31 @@ Generate QR codes for top teams (quiz master).
"""
import qrcode
import io
from fastapi import APIRouter, Depends
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from ..db import SessionLocal
from ..models import QRTicket
import uuid
import os
from typing import List, Optional
from datetime import datetime
from fastapi import APIRouter, Depends, Request, Form, HTTPException, Query
from fastapi.responses import StreamingResponse, HTMLResponse, FileResponse
from sqlalchemy.orm import Session
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image as ReportLabImage
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch, cm
from reportlab.lib.enums import TA_CENTER
from pydantic import BaseModel
from ..db import SessionLocal
from ..models import QRCode, QRSet, Event
from ..templates_config import templates
router = APIRouter()
# Get base URL from environment variable or use default
BASE_URL = os.environ.get("LEAGUELEDGER_BASE_URL", "https://rover.leagueledger.net")
def get_db():
db = SessionLocal()
try:
@@ -20,22 +36,387 @@ def get_db():
finally:
db.close()
# Models for request validation
class QRSetRequest(BaseModel):
name: str
description: Optional[str] = None
class QRCodeRequest(BaseModel):
title: str
points: float
achievement_name: Optional[str] = None
is_achievement_only: bool = False
max_uses: Optional[int] = None
description: Optional[str] = None
@router.get("/", response_class=HTMLResponse)
async def qr_dashboard(request: Request, db: Session = Depends(get_db)):
"""QR code management dashboard."""
# Get all QR sets
qr_sets = db.query(QRSet).all()
# Get all events for linking
events = db.query(Event).all()
return templates.TemplateResponse("qr/dashboard.html", {
"request": request,
"qr_sets": qr_sets,
"events": events
})
@router.post("/sets")
async def create_qr_set(
request: Request,
db: Session = Depends(get_db)
):
"""Create a new QR set."""
# Process form data
form_data = await request.form()
name = form_data.get("name")
description = form_data.get("description", "")
if not name:
raise HTTPException(status_code=400, detail="Set name is required")
# Create QR set
qr_set = QRSet(name=name, description=description)
db.add(qr_set)
db.commit()
db.refresh(qr_set)
return {"id": qr_set.id, "name": qr_set.name, "message": f"QR Set '{name}' created successfully"}
@router.get("/sets/{set_id}", response_class=HTMLResponse)
async def view_qr_set(request: Request, set_id: int, db: Session = Depends(get_db)):
"""View details of a QR set."""
qr_set = db.query(QRSet).filter(QRSet.id == set_id).first()
if not qr_set:
raise HTTPException(status_code=404, detail="QR Set not found")
# Get all QR codes in this set
qr_codes = db.query(QRCode).filter(QRCode.qr_set_id == set_id).all()
return templates.TemplateResponse("qr/set_detail.html", {
"request": request,
"qr_set": qr_set,
"qr_codes": qr_codes,
"base_url": BASE_URL
})
@router.post("/sets/{set_id}/codes")
async def add_qr_code_to_set(
request: Request,
set_id: int,
db: Session = Depends(get_db)
):
"""Add a QR code to a set."""
# Check if set exists
qr_set = db.query(QRSet).filter(QRSet.id == set_id).first()
if not qr_set:
raise HTTPException(status_code=404, detail="QR Set not found")
# Process form data
form_data = await request.form()
title = form_data.get("title")
points = float(form_data.get("points", 0))
achievement_name = form_data.get("achievement_name")
is_achievement_only = form_data.get("is_achievement_only", "").lower() in ("true", "yes", "1", "on")
description = form_data.get("description", "")
# Generate a unique code
code_str = str(uuid.uuid4())
# Create QR code
qr_code = QRCode(
code=code_str,
title=title,
points=points,
qr_set_id=set_id,
achievement_name=achievement_name,
is_achievement_only=is_achievement_only,
description=description
)
db.add(qr_code)
db.commit()
db.refresh(qr_code)
return {"id": qr_code.id, "code": code_str, "message": "QR Code added to set"}
@router.get("/generate/{points}")
def generate_qr(points: int, db: Session = Depends(get_db)):
"""
Generate a QR code for awarding `points` points.
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())
ticket = QRTicket(code=code_str, points=points)
db.add(ticket)
qr_code = QRCode(code=code_str, points=points)
db.add(qr_code)
db.commit()
db.refresh(ticket)
db.refresh(qr_code)
qr_img = qrcode.make(code_str)
qr_img = qrcode.make(f"{BASE_URL}/redeem/{code_str}")
buf = io.BytesIO()
qr_img.save(buf, format="PNG")
buf.seek(0)
return StreamingResponse(buf, media_type="image/png")
@router.get("/code/{code}")
def get_qr_image(code: str):
"""
Generate a QR code image from a code string without creating a database record.
Useful for viewing existing codes.
"""
qr_img = qrcode.make(f"{BASE_URL}/redeem/{code}")
buf = io.BytesIO()
qr_img.save(buf, format="PNG")
buf.seek(0)
return StreamingResponse(buf, media_type="image/png")
@router.get("/sets/{set_id}/generate-admin")
def generate_admin_qr(set_id: int, db: Session = Depends(get_db)):
"""Generate admin QR code for linking to events."""
qr_set = db.query(QRSet).filter(QRSet.id == set_id).first()
if not qr_set:
raise HTTPException(status_code=404, detail="QR Set not found")
# Create unique admin code
admin_code = f"admin-{set_id}-{uuid.uuid4()}"
# Generate QR code with special admin URL
qr_img = qrcode.make(f"{BASE_URL}/qr/admin-link/{admin_code}")
buf = io.BytesIO()
qr_img.save(buf, format="PNG")
buf.seek(0)
return StreamingResponse(buf, media_type="image/png",
headers={"Content-Disposition": f"inline; filename=admin-{set_id}.png"})
@router.get("/admin-link/{admin_code}", response_class=HTMLResponse)
async def admin_link_page(request: Request, admin_code: str, db: Session = Depends(get_db)):
"""Page for linking QR sets to events via admin code."""
# Extract set_id from admin code
try:
set_id = int(admin_code.split("-")[1])
except (IndexError, ValueError):
raise HTTPException(status_code=400, detail="Invalid admin code")
qr_set = db.query(QRSet).filter(QRSet.id == set_id).first()
if not qr_set:
raise HTTPException(status_code=404, detail="QR Set not found")
# Fetch available events
events = db.query(Event).all()
return templates.TemplateResponse("qr/admin_link.html", {
"request": request,
"qr_set": qr_set,
"admin_code": admin_code,
"events": events
})
@router.post("/admin-link/{admin_code}")
async def process_admin_link(
request: Request,
admin_code: str,
db: Session = Depends(get_db)
):
"""Process linking QR codes to an event."""
# Extract set_id from admin code
try:
set_id = int(admin_code.split("-")[1])
except (IndexError, ValueError):
raise HTTPException(status_code=400, detail="Invalid admin code")
# Get form data
form_data = await request.form()
event_id = form_data.get("event_id")
event_name = form_data.get("new_event_name")
# If no event ID provided, create a new event with the given name
if not event_id and event_name:
# Create new event
event_date = datetime.now() # Default to current date, can be improved
new_event = Event(
name=event_name,
description=f"Created via QR admin link on {event_date.strftime('%Y-%m-%d')}",
event_date=event_date
)
db.add(new_event)
db.commit()
db.refresh(new_event)
event_id = new_event.id
if not event_id:
raise HTTPException(status_code=400, detail="Event ID or new event name is required")
# Update all QR codes in the set
qr_codes = db.query(QRCode).filter(QRCode.qr_set_id == set_id).all()
for qr_code in qr_codes:
qr_code.event_id = event_id
db.commit()
return {"message": f"Successfully linked {len(qr_codes)} QR codes to event ID {event_id}"}
@router.get("/sets/{set_id}/pdf")
def generate_pdf(set_id: int, db: Session = Depends(get_db)):
"""
Generate a PDF with QR codes for a set.
"""
# Get QR set
qr_set = db.query(QRSet).filter(QRSet.id == set_id).first()
if not qr_set:
raise HTTPException(status_code=404, detail="QR Set not found")
# Get QR codes for this set
qr_codes = db.query(QRCode).filter(QRCode.qr_set_id == set_id).all()
if not qr_codes:
raise HTTPException(status_code=404, detail="No QR codes found in this set")
# Create PDF buffer
buffer = io.BytesIO()
# Create PDF document
doc = SimpleDocTemplate(
buffer,
pagesize=A4,
title=f"QR Codes - {qr_set.name}",
rightMargin=1*cm,
leftMargin=1*cm,
topMargin=1*cm,
bottomMargin=1*cm
)
# Container for elements
elements = []
# Add styles
styles = getSampleStyleSheet()
title_style = ParagraphStyle(
'TitleStyle',
parent=styles['Heading1'],
alignment=TA_CENTER,
fontName='Helvetica-Bold'
)
subtitle_style = ParagraphStyle(
'SubtitleStyle',
parent=styles['Heading2'],
alignment=TA_CENTER
)
code_style = ParagraphStyle(
'CodeStyle',
parent=styles['Normal'],
alignment=TA_CENTER,
fontName='Courier'
)
# Add title
elements.append(Paragraph(f"QR Codes for {qr_set.name}", title_style))
today = datetime.now().strftime('%Y-%m-%d')
elements.append(Paragraph(f"Generated on {today}", subtitle_style))
elements.append(Spacer(1, 0.5*inch))
# Generate admin QR code
admin_code = f"admin-{set_id}-{uuid.uuid4()}"
admin_qr = qrcode.make(f"{BASE_URL}/qr/admin-link/{admin_code}")
admin_img_io = io.BytesIO()
admin_qr.save(admin_img_io, format="PNG")
admin_img_io.seek(0)
# Add admin QR code to first page
admin_width = 2 * inch
admin_img = ReportLabImage(admin_img_io, width=admin_width, height=admin_width)
# Create a 1x3 table for admin QR code
admin_data = [[admin_img],
[Paragraph("Admin QR Code", subtitle_style)],
[Paragraph("Scan to link these QR codes to an event", styles['Normal'])]]
admin_table = Table(admin_data, colWidths=[4*inch])
admin_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('BOX', (0, 0), (-1, -1), 1, colors.black),
('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey)
]))
elements.append(admin_table)
elements.append(Spacer(1, 0.5*inch))
# Create a table with 2 QR codes per row
table_data = []
row = []
for idx, qr_code in enumerate(qr_codes):
# Generate QR code image
qr_img = qrcode.make(f"{BASE_URL}/redeem/{qr_code.code}")
img_io = io.BytesIO()
qr_img.save(img_io, format="PNG")
img_io.seek(0)
# Create image element
img_width = 2.5 * inch
img = ReportLabImage(img_io, width=img_width, height=img_width)
# Create cell content
title = qr_code.title if qr_code.title else f"{qr_code.points} Points"
cell_content = [
img,
Paragraph(title, subtitle_style),
Paragraph(f"{qr_code.points} Points", styles['Normal']),
Paragraph(qr_code.code[:8] + "...", code_style),
Paragraph(f"{BASE_URL}/redeem/{qr_code.code[:8]}...", code_style)
]
row.append(cell_content)
# Create a new row after every 2 cells
if len(row) == 2 or idx == len(qr_codes) - 1:
# If we have an odd number at the end, add an empty cell
if len(row) == 1:
row.append([])
table_data.append(row)
row = []
# Create the table
col_width = doc.width / 2
qr_table = Table(table_data, colWidths=[col_width, col_width])
# Style the table
table_style = [
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
('TOPPADDING', (0, 0), (-1, -1), 10),
('BOTTOMPADDING', (0, 0), (-1, -1), 10),
]
qr_table.setStyle(TableStyle(table_style))
elements.append(qr_table)
# Build the PDF
doc.build(elements)
buffer.seek(0)
# Return PDF as a download
return StreamingResponse(
buffer,
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename=qr_codes_{set_id}.pdf"}
)
+111 -28
View File
@@ -2,15 +2,20 @@
"""
Redeem a QR code and attribute points to a team.
"""
import os
from fastapi import APIRouter, Depends, Request, Form, HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy.orm import Session
from datetime import datetime
from ..db import SessionLocal
from ..models import QRTicket, User, Team, TeamMembership
from ..models import QRCode, User, Team, TeamMembership, TeamAchievement
from ..templates_config import templates
router = APIRouter()
# Get base URL from environment variable or use default
BASE_URL = os.environ.get("LEAGUELEDGER_BASE_URL", "https://rover.leagueledger.net")
def get_db():
db = SessionLocal()
try:
@@ -24,14 +29,38 @@ def redeem_code(code: str, request: Request, db: Session = Depends(get_db)):
Display a page to let the user choose which team to apply points to.
No login required.
"""
ticket = db.query(QRTicket).filter_by(code=code, used=False).first()
if not ticket:
# Find the QR code record
qr_code = db.query(QRCode).filter_by(code=code).first()
# Handle invalid or already used codes
if not qr_code:
return templates.TemplateResponse(
"error.html",
{
"request": request,
"error_title": "Invalid Code",
"error_message": "This code is invalid or has already been used."
"error_message": "This QR code is invalid or does not exist."
}
)
if qr_code.used and (not qr_code.max_uses or qr_code.max_uses <= 1):
return templates.TemplateResponse(
"error.html",
{
"request": request,
"error_title": "Code Already Used",
"error_message": "This QR code has already been redeemed."
}
)
# Check expiration if applicable
if qr_code.expires_at and qr_code.expires_at < datetime.now():
return templates.TemplateResponse(
"error.html",
{
"request": request,
"error_title": "Expired Code",
"error_message": "This QR code has expired and can no longer be redeemed."
}
)
@@ -40,8 +69,19 @@ def redeem_code(code: str, request: Request, db: Session = Depends(get_db)):
return templates.TemplateResponse("redeem.html", {
"request": request,
"ticket": ticket,
"user_teams": all_teams # Now showing all teams
"ticket": qr_code, # Using the same template variable name for compatibility
"user_teams": all_teams,
"has_achievement": bool(qr_code.achievement_name),
"base_url": BASE_URL
})
@router.get("/scan", response_class=HTMLResponse)
def scan_qr_code(request: Request):
"""
Display the QR code scanner page
"""
return templates.TemplateResponse("scan_qr.html", {
"request": request
})
@router.post("/apply/{code}")
@@ -51,7 +91,7 @@ async def apply_code(
db: Session = Depends(get_db)
):
"""
Apply the QR code to a selected team (without authentication)
Apply the QR code to a selected team and award points and/or achievements
"""
# Get form data
form_data = await request.form()
@@ -67,19 +107,41 @@ async def apply_code(
}
)
ticket = db.query(QRTicket).filter_by(code=code, used=False).first()
if not ticket:
# Get the QR code
qr_code = db.query(QRCode).filter_by(code=code).first()
if not qr_code:
return templates.TemplateResponse(
"error.html",
{
"request": request,
"error_title": "Invalid Code",
"error_message": "This code is invalid or has already been used."
"error_message": "This QR code is invalid or does not exist."
}
)
# Check if the code is already used (for single-use codes)
if qr_code.used and (not qr_code.max_uses or qr_code.max_uses <= 1):
return templates.TemplateResponse(
"error.html",
{
"request": request,
"error_title": "Code Already Used",
"error_message": "This QR code has already been redeemed."
}
)
# Check expiration if applicable
if qr_code.expires_at and qr_code.expires_at < datetime.now():
return templates.TemplateResponse(
"error.html",
{
"request": request,
"error_title": "Expired Code",
"error_message": "This QR code has expired and can no longer be redeemed."
}
)
team = db.query(Team).filter_by(id=team_id).first()
if not team:
return templates.TemplateResponse(
"error.html",
@@ -90,24 +152,35 @@ async def apply_code(
}
)
# Redeem without checking membership
ticket.redeemed_at_team = team.id
ticket.used = True
# If we have redeemed_at column, update it
if hasattr(ticket, 'redeemed_at'):
from datetime import datetime
ticket.redeemed_at = datetime.now()
# Mark the QR code as redeemed
qr_code.redeemed_at_team = team.id
qr_code.redeemed_at = datetime.now()
qr_code.used = True
# Handle achievements if present
if qr_code.achievement_name:
achievement = TeamAchievement(
team_id=team.id,
name=qr_code.achievement_name,
description=qr_code.description,
event_id=qr_code.event_id,
qr_code_id=qr_code.id,
achieved_at=datetime.now()
)
db.add(achievement)
db.commit()
# Redirect to success page or dashboard
# Return the success page with appropriate information
return templates.TemplateResponse(
"redeem_success.html",
{
"request": request,
"points": ticket.points,
"team": team
"points": qr_code.points,
"achievement": qr_code.achievement_name if qr_code.achievement_name else None,
"team": team,
"event": qr_code.event if qr_code.event else None,
"base_url": BASE_URL
}
)
@@ -122,16 +195,26 @@ async def manual_code_entry(
This redirects to the normal redeem flow after validating the code.
"""
# Check if the code exists
ticket = db.query(QRTicket).filter_by(code=code, used=False).first()
qr_code = db.query(QRCode).filter_by(code=code).first()
if not ticket:
# In a real app, add a flash message or error handling
if not qr_code:
return templates.TemplateResponse(
"error.html",
{
"request": request,
"error_title": "Invalid Code",
"error_message": "The code you entered is invalid or has already been used."
"error_message": "The code you entered is invalid or does not exist."
}
)
# Check if already used (for single-use codes)
if qr_code.used and (not qr_code.max_uses or qr_code.max_uses <= 1):
return templates.TemplateResponse(
"error.html",
{
"request": request,
"error_title": "Code Already Used",
"error_message": "This code has already been redeemed."
}
)
+61
View File
@@ -0,0 +1,61 @@
from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
import os
router = APIRouter()
# Function to setup static files that will be called from main.py
def configure_static_files(app):
"""Configure static files mounting for the application."""
static_dir = os.path.join(os.path.dirname(__file__), "..", "static")
app.mount("/static", StaticFiles(directory=static_dir), name="static")
# Serve favicon.ico
@router.get("/favicon.ico")
async def serve_favicon():
"""Serve favicon.ico."""
file_path = os.path.join(os.path.dirname(__file__), "..", "static", "images", "favicon", "favicon.ico")
return FileResponse(file_path)
# Serve android-chrome-192x192.png
@router.get("/android-chrome-192x192.png")
async def serve_android_chrome_192():
"""Serve android-chrome-192x192.png."""
file_path = os.path.join(os.path.dirname(__file__), "..", "static", "images", "favicon", "android-chrome-192x192.png")
return FileResponse(file_path)
# Serve android-chrome-512x512.png
@router.get("/android-chrome-512x512.png")
async def serve_android_chrome_512():
"""Serve android-chrome-512x512.png."""
file_path = os.path.join(os.path.dirname(__file__), "..", "static", "images", "favicon", "android-chrome-512x512.png")
return FileResponse(file_path)
# Serve apple-touch-icon.png
@router.get("/apple-touch-icon.png")
async def serve_apple_touch_icon():
"""Serve apple-touch-icon.png."""
file_path = os.path.join(os.path.dirname(__file__), "..", "static", "images", "favicon", "apple-touch-icon.png")
return FileResponse(file_path)
# Serve favicon-16x16.png
@router.get("/favicon-16x16.png")
async def serve_favicon_16():
"""Serve favicon-16x16.png."""
file_path = os.path.join(os.path.dirname(__file__), "..", "static", "images", "favicon", "favicon-16x16.png")
return FileResponse(file_path)
# Serve favicon-32x32.png
@router.get("/favicon-32x32.png")
async def serve_favicon_32():
"""Serve favicon-32x32.png."""
file_path = os.path.join(os.path.dirname(__file__), "..", "static", "images", "favicon", "favicon-32x32.png")
return FileResponse(file_path)
# Serve site.webmanifest
@router.get("/images/favicon/site.webmanifest")
async def serve_site_webmanifest():
"""Serve site.webmanifest."""
file_path = os.path.join(os.path.dirname(__file__), "..", "static", "images", "favicon", "site.webmanifest")
return FileResponse(file_path)
+11 -11
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Create or join a team, manage membership.
Teams management for users and admins.
"""
from fastapi import APIRouter, Depends, Request, Form, HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse
@@ -10,7 +10,7 @@ from datetime import datetime, timedelta
import random # For demo data
from ..db import SessionLocal
from ..models import Team, TeamMembership, User, QRTicket, TeamAchievement
from ..models import Team, TeamMembership, User, QRCode, TeamAchievement
from ..schemas import TeamCreate
from ..templates_config import templates
@@ -106,17 +106,17 @@ def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)):
})
# Get total points
total_points = db.query(func.sum(QRTicket.points)).filter(
QRTicket.redeemed_at_team == team_id
total_points = db.query(func.sum(QRCode.points)).filter(
QRCode.redeemed_at_team == team_id
).scalar() or 0
# Calculate rank based on points
higher_teams = db.query(func.count(Team.id)).join(
QRTicket,
QRTicket.redeemed_at_team == Team.id,
QRCode,
QRCode.redeemed_at_team == Team.id,
isouter=True
).group_by(Team.id).having(
func.sum(QRTicket.points) > total_points
func.sum(QRCode.points) > total_points
).scalar() or 0
team_rank = higher_teams + 1
@@ -134,13 +134,13 @@ def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)):
# Use raw SQL to check if column exists and get points
has_redeemed_at = False
inspector = inspect(db.bind)
if 'redeemed_at' in [col['name'] for col in inspector.get_columns('qr_tickets')]:
if 'redeemed_at' in [col['name'] for col in inspector.get_columns('qr_codes')]:
has_redeemed_at = True
if has_redeemed_at:
points_this_month = db.query(func.sum(QRTicket.points)).filter(
QRTicket.redeemed_at_team == team_id,
QRTicket.redeemed_at >= first_day_of_month
points_this_month = db.query(func.sum(QRCode.points)).filter(
QRCode.redeemed_at_team == team_id,
QRCode.redeemed_at >= first_day_of_month
).scalar() or points_this_month
except Exception as e:
print(f"Error calculating monthly points: {e}")