Skip to content

Commit

Permalink
Merge branch 'main' into tf-add-puzzle-ant
Browse files Browse the repository at this point in the history
  • Loading branch information
lsammut committed Oct 11, 2024
2 parents 03c8f6f + 93fd35d commit b62971b
Show file tree
Hide file tree
Showing 35 changed files with 445 additions and 66 deletions.
7 changes: 7 additions & 0 deletions .djlint_rules.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- rule:
name: A001
message: Consider adding a viewport meta tag.
flags: re.DOTALL|re.I
# <meta name="viewport" content="width=device-width, initial-scale=1"> is a good starting point
patterns:
- <html[^>]*?>(?:(?!<meta[^>]*?name=([\"|'])viewport\b).)*</html>
3 changes: 3 additions & 0 deletions .github/workflows/pytest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ jobs:
- name: Ruff Format
run: uvx ruff format

- name: HTML Template Lint (djlint)
run: uv run djlint openday_scavenger/

- name: Run tests
run: uv run pytest tests

Expand Down
8 changes: 7 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ repos:
- id: ruff
args: [ --fix ]
# Run the formatter.
- id: ruff-format
- id: ruff-format
- repo: https://github.com/djlint/djLint
rev: v1.35.2
hooks:
- id: djlint-jinja
files: "\\.html"
types_or: ["html"]
21 changes: 19 additions & 2 deletions openday_scavenger/api/puzzles/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import random
from datetime import datetime, timedelta
from io import BytesIO
from pathlib import Path
from sys import modules
from typing import Any

from fastapi.encoders import jsonable_encoder
Expand Down Expand Up @@ -452,7 +454,22 @@ def generate_puzzle_qr_code(name: str, as_file_buff: bool = False) -> str | Byte
def generate_puzzle_qr_codes_pdf(db_session: Session):
puzzles = get_all(db_session, only_active=False)

return generate_qr_codes_pdf([f"{config.BASE_URL}puzzles/{puzzle.name}/" for puzzle in puzzles])
module = modules["openday_scavenger"]
module_path = module.__file__
if module_path is not None:
logo_path = Path(module_path).parent / "static/images/qr_codes/lock.png"
if not logo_path.exists():
logo_path = None
else:
logo_path = None

return generate_qr_codes_pdf(
[f"{config.BASE_URL}puzzles/{puzzle.name}/" for puzzle in puzzles],
logo=logo_path,
title="You Found A Puzzle Lock!",
title_font_size=30,
url_font_size=12,
)


def generate_test_data(
Expand Down Expand Up @@ -529,7 +546,7 @@ def upsert_puzzle_json(db_session: Session, puzzle_json: PuzzleJson):
existing_puzzles_by_id = {item.id: item for item in get_all(db_session)}

for puzzle in puzzle_json.puzzles:
existing_puzzle = "id" in puzzle and existing_puzzles_by_id[puzzle["id"]]
existing_puzzle = "id" in puzzle and existing_puzzles_by_id.get(puzzle["id"])
if existing_puzzle:
_ = update(db_session, existing_puzzle.name, PuzzleUpdate(**puzzle))
else:
Expand Down
94 changes: 79 additions & 15 deletions openday_scavenger/api/qr_codes.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,67 @@
import logging
from io import BytesIO
from pathlib import Path

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

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

def generate_qr_code(url: str, as_file_buff: bool = False) -> str | BytesIO:

def generate_qr_code(
url: str, as_file_buff: bool = False, logo: Path | None = None
) -> 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.
logo (Path, optional): Pathlib Path to an image to use as a logo in middle of QR code. Must be
compatible with `PIL.Image.open`. Logo is only created with `as_file_buff=True`.
If the image is incompatible a warning is logged and QR code is created without the logo.
Currently the ratio of logo to QR size is fixed at 1/25th.
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")
qr_image = _qr.to_pil() # type: ignore

if logo is not None:
qr_image = qr_image.convert("RGB")
try:
logo_image = Image.open(logo)
qr_image = qr_image.resize((1000, 1000), Image.NEAREST)
qr_width, qr_height = qr_image.size
logo_width, logo_height = logo_image.size
max_logo_size = min(qr_width // 5, qr_height // 5)
ratio = min(max_logo_size / logo_width, max_logo_size / logo_height)
logo_image = logo_image.resize(
(int(logo_width * ratio), int(logo_height * ratio)), Image.BICUBIC
)

# Calculate the center position for the logo
logo_x = (qr_width - logo_image.width) // 2
logo_y = (qr_height - logo_image.height) // 2

# Paste the logo onto the QR code image
qr_image.paste(logo_image, (logo_x, logo_y))

except Exception as e:
logger.error(
f"Opening and merging Logo {logo} with the qr code raised an exception {e}"
)

if as_file_buff:
buff = BytesIO()
_qr.save(buff, kind="png")
qr_image.save(buff, format="png")
buff.seek(0)
qr = buff
else:
Expand All @@ -32,7 +70,15 @@ def generate_qr_code(url: str, as_file_buff: bool = False) -> str | BytesIO:
return qr


def generate_qr_codes_pdf(entries: list[str]) -> BytesIO:
def generate_qr_codes_pdf(
entries: list[str],
title: str = "blah",
title_font_size: int = 14,
url_font_size: int = 10,
columns: int = 1,
rows: int = 1,
logo: Path | None = None,
) -> BytesIO:
"""Generates a PDF document containing QR codes for each URL in the provided list.
Args:
Expand All @@ -47,30 +93,48 @@ def generate_qr_codes_pdf(entries: list[str]) -> 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
x_margin = 50 / columns
y_margin = 100

qr_size = 500 / (rows)

for i, entry in enumerate(entries):
logger.info(f"Entry number {i}")
col_index = i % columns
row_index = 0 if i % (columns * rows) < 2 else 1

x = x_margin + col_index * (qr_size + x_margin)
y = height - (row_index + 1) * (qr_size + y_margin)

# Set the font size for the Title text
c.setFillColorRGB(0, 0.46, 0.75)
c.setFont("Helvetica-Bold", title_font_size)

# Calculate the position to center the text
text_width = c.stringWidth(f"{title}", "Helvetica-Bold", title_font_size)
text_x = x + (qr_size - text_width) / 2

# Add the Title text above the QR code
c.drawString(text_x, y + 510 / rows, f"{title}")

for entry in entries:
# Draw the QR code image from BytesIO
qr_code = generate_qr_code(entry, as_file_buff=True)
qr_code = generate_qr_code(entry, as_file_buff=True, logo=logo)
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)
c.setFont("Helvetica", url_font_size)

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

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

# Create a new page for the next QR code
c.showPage()
if (i + 1) % (columns * rows) == 0:
c.showPage()

c.save()
pdf_io.seek(0)
Expand Down
18 changes: 16 additions & 2 deletions openday_scavenger/api/visitors/service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
from datetime import datetime
from io import BytesIO
from pathlib import Path
from sys import modules
from typing import Any
from uuid import uuid4

Expand Down Expand Up @@ -220,10 +222,22 @@ def generate_visitor_qr_code(uid: str, as_file_buff: bool = False) -> str | Byte


def generate_visitor_qr_codes_pdf(db_session: Session):
visitors = get_visitor_pool(db_session)
visitors = get_visitor_pool(db_session, limit=1000)

module = modules["openday_scavenger"]
module_path = module.__file__
if module_path is not None:
logo_path = Path(module_path).parent / "static/images/qr_codes/key.png"
else:
logo_path = None

return generate_qr_codes_pdf(
[f"{config.BASE_URL}register/{visitor.uid}" for visitor in visitors]
[f"{config.BASE_URL}register/{visitor.uid}" for visitor in visitors],
logo=logo_path,
rows=2,
columns=2,
title="Your Personal Adventure Key!",
url_font_size=8,
)


Expand Down
20 changes: 11 additions & 9 deletions openday_scavenger/puzzles/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from .ads_question_answer_matchup.views import router as ads_question_answer_matchup_router
from .ant.views import router as ant_puzzle
from .controls_game.views import router as puzzle_controls_router
from .cube.views import router as puzzle_cube_router
from .demo.views import router as puzzle_demo_router
from .element.views import router as puzzle_element_router
Expand All @@ -16,26 +17,27 @@
router = APIRouter()

# Include puzzle routes. Name entered into database should match the prefix.
router.include_router(ads_question_answer_matchup_router, prefix="/ads_question_answer_matchup")
router.include_router(new_buildings_router, prefix="/newbuildings")
router.include_router(puzzle_controls_router, prefix="/controls_game")
router.include_router(puzzle_cube_router, prefix="/cube")
router.include_router(puzzle_demo_router, prefix="/demo")
router.include_router(puzzle_element_router, prefix="/element_general")
router.include_router(puzzle_element_router, prefix="/element_mex")
router.include_router(puzzle_element_router, prefix="/element_xas")
router.include_router(puzzle_element_router, prefix="/element_ads")
router.include_router(puzzle_element_router, prefix="/element_bsx")
router.include_router(puzzle_element_router, prefix="/element_general")
router.include_router(puzzle_element_router, prefix="/element_mct")
router.include_router(puzzle_element_router, prefix="/element_mex")
router.include_router(puzzle_element_router, prefix="/element_mx")
router.include_router(puzzle_element_router, prefix="/element_pd")
router.include_router(new_buildings_router, prefix="/newbuildings")
router.include_router(puzzle_element_router, prefix="/element_xas")
router.include_router(ant_puzzle, prefix="/ant")
router.include_router(puzzle_shuffleanagram_router, prefix="/shuffleanagram-probations")
router.include_router(puzzle_shuffleanagram_router, prefix="/shuffleanagram-crumpets")
router.include_router(puzzle_shuffleanagram_router, prefix="/shuffleanagram-toerags")
router.include_router(puzzle_shuffleanagram_router, prefix="/shuffleanagram-reboots")
router.include_router(puzzle_fourbyfour_router, prefix="/fourbyfour")
router.include_router(puzzle_labelthemap_router, prefix="/labelthemap")
router.include_router(puzzle_labelthemap_router, prefix="/labelthemap-easy")
router.include_router(ads_question_answer_matchup_router, prefix="/ads_question_answer_matchup")
router.include_router(puzzle_shuffleanagram_router, prefix="/shuffleanagram-crumpets")
router.include_router(puzzle_shuffleanagram_router, prefix="/shuffleanagram-probations")
router.include_router(puzzle_shuffleanagram_router, prefix="/shuffleanagram-reboots")
router.include_router(puzzle_shuffleanagram_router, prefix="/shuffleanagram-toerags")
router.include_router(puzzle_xray_filters_router, prefix="/xray_filters")
for rr in puzzle_finder_routes:
router.include_router(puzzle_finder_router, prefix=rr)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<html>
<!DOCTYPE html>
<html lang="en">

<head>
<title>ADS Question Answer Matchup Puzzle</title>
<meta name="viewport" content="width=device-width, initial-scale=1">

<link id="favicon" rel="icon" type="image/x-icon" href="/static/favicon.ico">
Expand Down Expand Up @@ -50,7 +52,7 @@ <h1>ADS Question Answer Matchup Puzzle</h1>
<div class="list-group-item tinted list-row" id="data4">150</div>
<div class="list-group-item tinted list-row" id="data1">300</div>
<div class="finger-container">
<img src="static/img/tap.png" class="finger" />
<img src="static/img/tap.png" class="finger" alt="image indicating that items should be dragged with finger" />
</div>
</div>
</div>
Expand Down
Empty file.
45 changes: 45 additions & 0 deletions openday_scavenger/puzzles/controls_game/static/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Controls Puzzle Game</title>

<script src="/static/js/htmx.min.js"></script>
<script src="/static/js/json-enc.js"></script>

<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/openday.css" rel="stylesheet">
<link id="favicon" rel="icon" type="image/x-icon" href="/static/favicon.ico">

<style>
</style>
</head>

<body class="game-body">
<div class="container text-center my-3">
<h1 class="type-h1">Controls Puzzle Game</h1>
<div class="mt-5">
<p class="text-secondary fw-bold">Updating your status...</p>
<div class="spinner-border text-primary" role="presentation">

<span class="sr-only">Loading...</span>
</div>
</div>

<form id="form" action="/submission" method="POST" class="invisible">
<input hidden type="text" id="name" name="name" value="{{ puzzle }}">
<input type="text" id="answer" name="answer" value="true" hidden>
<button>Submit</button>
</form>
</div>

<script>
document.addEventListener('DOMContentLoaded', () => {
document.forms['form'].submit();
});
</script>
</body>

</html>
Loading

0 comments on commit b62971b

Please sign in to comment.