Skip to content

Commit

Permalink
Visitor QR Code Export. Refactor qr code generator to be shared with …
Browse files Browse the repository at this point in the history
…puzzles.

Fixes #7
  • Loading branch information
stmudie committed Sep 30, 2024
1 parent 7ec71fa commit e6a8874
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 58 deletions.
61 changes: 7 additions & 54 deletions openday_scavenger/api/puzzles/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
from datetime import datetime
from io import BytesIO

from segno import make_qr
from sqlalchemy.orm import Session

from openday_scavenger.api.puzzles.models import Access, Puzzle, Response
from openday_scavenger.api.qr_codes import generate_qr_code, generate_qr_codes_pdf
from openday_scavenger.api.visitors.exceptions import VisitorUIDInvalidError
from openday_scavenger.api.visitors.models import Visitor
from openday_scavenger.api.visitors.schemas import VisitorPoolCreate
Expand All @@ -29,8 +29,8 @@
"update",
"compare_answer",
"record_access",
"generate_qr_code",
"generate_qr_codes_pdf",
"generate_puzzle_qr_code",
"generate_puzzle_qr_codes_pdf",
"generate_test_data",
)

Expand Down Expand Up @@ -282,61 +282,14 @@ def record_access(db_session: Session, puzzle_name: str, visitor_uid: str) -> Ac
return access


def generate_qr_code(name: str, as_file_buff: bool = False) -> str | BytesIO:
_qr = make_qr(f"puzzles/{name}", error="H")
def generate_puzzle_qr_code(name: str, as_file_buff: bool = False) -> str | BytesIO:
return generate_qr_code(f"puzzles/{name}", as_file_buff=as_file_buff)

if as_file_buff:
buff = BytesIO()
_qr.save(buff, kind="png")
buff.seek(0)
qr = buff
else:
qr = _qr.svg_data_uri()

return qr


def generate_qr_codes_pdf(db_session: Session):
from reportlab.lib.pagesizes import A4
from reportlab.lib.utils import ImageReader
from reportlab.pdfgen import canvas

def generate_puzzle_qr_codes_pdf(db_session: Session):
puzzles = get_all(db_session, only_active=False)

# Create a canvas object
pdf_io = BytesIO()
c = canvas.Canvas(pdf_io, pagesize=A4)
width, height = A4

# Calculate the position to center the QR code
qr_size = 400 # Size of the QR code
x = (width - qr_size) / 2
y = (height - qr_size) / 2

for puzzle in puzzles:
# Draw the QR code image from BytesIO
qr_code = generate_qr_code(puzzle.name, as_file_buff=True)
qr_image = ImageReader(qr_code)
c.drawImage(qr_image, x, y, width=qr_size, height=qr_size)

# Set the font size for the URL text
font_size = 24
c.setFont("Helvetica", font_size)

# Calculate the position to center the text
text_width = c.stringWidth(f"/puzzle/{puzzle.name}", "Helvetica", font_size)
text_x = (width - text_width) / 2

# Add the URL text below the QR code
c.drawString(text_x, y - 30, f"/puzzle/{puzzle.name}")

# Create a new page for the next QR code
c.showPage()

c.save()
pdf_io.seek(0)

return pdf_io
return generate_qr_codes_pdf([puzzle.name for puzzle in puzzles])


def generate_test_data(
Expand Down
78 changes: 78 additions & 0 deletions openday_scavenger/api/qr_codes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from io import BytesIO

from reportlab.lib.pagesizes import A4
from reportlab.lib.utils import ImageReader
from reportlab.pdfgen import canvas
from segno import make_qr


def generate_qr_code(url: str, as_file_buff: bool = False) -> str | BytesIO:
"""Generates a QR code for the provided URL.
Args:
url (str): The URL to be encoded in the QR code.
as_file_buff (bool, optional): If True, returns the QR code as a BytesIO object
containing the PNG image data. Defaults to False, which returns the QR code as
an SVG data URI string.
Returns:
str | BytesIO: The QR code representation. If `as_file_buff` is True, a BytesIO
object; otherwise, an SVG data URI string.
"""
_qr = make_qr(f"{url}", error="H")

if as_file_buff:
buff = BytesIO()
_qr.save(buff, kind="png")
buff.seek(0)
qr = buff
else:
qr = _qr.svg_data_uri()

return qr


def generate_qr_codes_pdf(entries: list[str]) -> BytesIO:
"""Generates a PDF document containing QR codes for each URL in the provided list.
Args:
entries (list[str]): A list of URLs to be encoded as QR codes and included in
the PDF.
Returns:
BytesIO: A BytesIO object containing the generated PDF document.
"""
# Create a canvas object
pdf_io = BytesIO()
c = canvas.Canvas(pdf_io, pagesize=A4)
width, height = A4

# Calculate the position to center the QR code
qr_size = 400 # Size of the QR code
x = (width - qr_size) / 2
y = (height - qr_size) / 2

for entry in entries:
# Draw the QR code image from BytesIO
qr_code = generate_qr_code(entry, as_file_buff=True)
qr_image = ImageReader(qr_code)
c.drawImage(qr_image, x, y, width=qr_size, height=qr_size)

# Set the font size for the URL text
font_size = 24
c.setFont("Helvetica", font_size)

# Calculate the position to center the text
text_width = c.stringWidth(f"{entry}", "Helvetica", font_size)
text_x = (width - text_width) / 2

# Add the URL text below the QR code
c.drawString(text_x, y - 30, f"{entry}")

# Create a new page for the next QR code
c.showPage()

c.save()
pdf_io.seek(0)

return pdf_io
17 changes: 17 additions & 0 deletions openday_scavenger/api/visitors/service.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import json
from datetime import datetime
from io import BytesIO
from typing import Any
from uuid import uuid4

from sqlalchemy import Integer, Row, and_, cast, func
from sqlalchemy.orm import Query, Session

from openday_scavenger.api.puzzles.models import Puzzle, Response
from openday_scavenger.api.qr_codes import generate_qr_code, generate_qr_codes_pdf
from openday_scavenger.config import get_settings

from .exceptions import VisitorExistsError, VisitorUIDInvalidError
from .models import Visitor, VisitorPool
from .schemas import VisitorPoolCreate

config = get_settings()


def get_all(
db_session: Session,
Expand Down Expand Up @@ -186,6 +191,18 @@ def create_visitor_pool(db_session: Session, pool_in: VisitorPoolCreate) -> None
raise


def generate_visitor_qr_code(uid: str, as_file_buff: bool = False) -> str | BytesIO:
return generate_qr_code(f"{config.BASE_URL}register/{uid}", as_file_buff=as_file_buff)


def generate_visitor_qr_codes_pdf(db_session: Session):
visitors = get_visitor_pool(db_session)

return generate_qr_codes_pdf(
[f"{config.BASE_URL}register/{visitor.uid}" for visitor in visitors]
)


def _filter(
query: Query, *, uid_filter: str | None = None, still_playing: bool | None = None
) -> Query:
Expand Down
8 changes: 4 additions & 4 deletions openday_scavenger/views/admin/puzzles.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from openday_scavenger.api.puzzles.schemas import PuzzleCreate, PuzzleUpdate
from openday_scavenger.api.puzzles.service import (
create,
generate_qr_code,
generate_qr_codes_pdf,
generate_puzzle_qr_code,
generate_puzzle_qr_codes_pdf,
get_all,
update,
)
Expand Down Expand Up @@ -57,14 +57,14 @@ async def update_puzzle(

@router.get("/{puzzle_name}/qr")
async def render_qr_code(puzzle_name: str, request: Request):
qr = generate_qr_code(puzzle_name)
qr = generate_puzzle_qr_code(puzzle_name)

return templates.TemplateResponse(request=request, name="qr.html", context={"qr": qr})


@router.get("/download-pdf")
async def download_qr_codes(db: Annotated["Session", Depends(get_db)]):
pdf_io = generate_qr_codes_pdf(db)
pdf_io = generate_puzzle_qr_codes_pdf(db)

return StreamingResponse(
pdf_io,
Expand Down
4 changes: 4 additions & 0 deletions openday_scavenger/views/admin/static/visitors.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ <h5 class="border-bottom pb-2 mb-0">Visitor Pool</h5>
</button>
</form>
<div id="visitor-pool-table" hx-get="/admin/visitors/pool" hx-trigger="load" hx-swap="innerHTML"></div>
<a class="btn btn-primary" href="/admin/visitors/download-pdf">
<i class="fa-regular fa-qrcode"></i>
Download All QR Codes
</a>
</div>

<script src="/static/js/html5-qrcode.js"></script>
Expand Down
21 changes: 21 additions & 0 deletions openday_scavenger/views/admin/visitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Annotated

from fastapi import APIRouter, Depends, Request
from fastapi.responses import StreamingResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session

Expand All @@ -13,6 +14,8 @@
check_out,
create,
create_visitor_pool,
generate_visitor_qr_code,
generate_visitor_qr_codes_pdf,
get_visitor_pool,
)
from openday_scavenger.api.visitors.service import get_all as get_all_visitors
Expand Down Expand Up @@ -68,6 +71,13 @@ async def initialise_visitor_pool(
return await _render_visitor_pool_table(request, db)


@router.get("/{visitor_uid}/qr")
async def render_qr_code(visitor_uid: str, request: Request):
qr = generate_visitor_qr_code(visitor_uid)

return templates.TemplateResponse(request=request, name="qr.html", context={"qr": qr})


@router.get("/pool")
async def render_visitor_pool_table(
request: Request, db: Annotated["Session", Depends(get_db)], limit: int = 10
Expand All @@ -76,6 +86,17 @@ async def render_visitor_pool_table(
return await _render_visitor_pool_table(request, db, limit)


@router.get("/download-pdf")
async def download_qr_codes(db: Annotated["Session", Depends(get_db)]):
pdf_io = generate_visitor_qr_codes_pdf(db)

return StreamingResponse(
pdf_io,
media_type="application/pdf",
headers={"Content-Disposition": "attachment; filename=visitor_qr_codes.pdf"},
)


async def _render_visitor_table(
request: Request,
db: Annotated["Session", Depends(get_db)],
Expand Down

0 comments on commit e6a8874

Please sign in to comment.