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:
@@ -0,0 +1 @@
|
||||
# Import your view modules here
|
||||
+5
-2
@@ -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():
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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."
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user