455 lines
14 KiB
Python
455 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate QR codes for top teams (quiz master).
|
|
"""
|
|
import qrcode
|
|
import io
|
|
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, User
|
|
from ..league_context import get_active_leagues, parse_league_id, resolve_selected_league
|
|
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:
|
|
yield 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 user from session for navbar
|
|
user = None
|
|
user_id = request.session.get("user_id")
|
|
if user_id:
|
|
user = db.query(User).get(user_id)
|
|
|
|
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()
|
|
|
|
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
|
|
})
|
|
|
|
|
|
@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", "")
|
|
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, league_id=selected_league.id)
|
|
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."""
|
|
# Get user from session for navbar
|
|
user = None
|
|
user_id = request.session.get("user_id")
|
|
if user_id:
|
|
user = db.query(User).get(user_id)
|
|
|
|
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,
|
|
"user": user # Add user to the context
|
|
})
|
|
|
|
|
|
@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(
|
|
league_id=qr_set.league_id,
|
|
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, 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())
|
|
|
|
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)
|
|
|
|
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."""
|
|
# Get user from session for navbar
|
|
user = None
|
|
user_id = request.session.get("user_id")
|
|
if user_id:
|
|
user = db.query(User).get(user_id)
|
|
|
|
# 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).filter(Event.league_id == qr_set.league_id).all()
|
|
|
|
return templates.TemplateResponse("qr/admin_link.html", {
|
|
"request": request,
|
|
"qr_set": qr_set,
|
|
"admin_code": admin_code,
|
|
"events": events,
|
|
"user": user # Add user to the context
|
|
})
|
|
|
|
|
|
@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(
|
|
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
|
|
)
|
|
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"}
|
|
)
|