The Open Day Scavenger Hunt is an interactive activity that the Scientific Computing team of the Australian Synchrotron is organising for the 2024 Open Day. This repository contains the complete web application for this activity.
Visitors of the Open Day will be tasked to solve little puzzles, scattered around the Australian Synchrotron facility. Each puzzle is connected to a QR code which, after scanning the code, is presented as a webpage to the visitor on their phone. By entering the correct answers, visitors solve the puzzles and can win prices.
During the Open Day a visitor is guided through the following steps:
- The visitor registers at the registration desk by scanning a personalised QR code with their phone's camera or a QR scanner app. The code directs them to the scavenger hunt web application, generates an anonymous session and registers them in the application's database.
- The visitor explores the facility and keeps an eye out for QR codes. If they find a QR code, they scan it with the scavenger hunt web application. The puzzle is displayed in the application.
- The visitor solves the puzzle and enters their answer into the web application. The answer is compared with the correct answer stored in the database and they receive immediate feedback in the web application.
- Once the visitor has solved all puzzles, they return to the registration desk and pick up their prize.
The web application is written in Python, the language of choice for the Scientific Computing team at the Australian Synchrotron. The following sections explain two methods to install and run the application locally.
Before choosing one of the methods, make sure you have the source code of the application. Currently there is no released version of the project, therefore the first step is to clone the repo:
git clone [email protected]:AustralianSynchrotron/openday-scavenger.git # Using ssh key (preferred)
or
git clone https://github.com/AustralianSynchrotron/openday-scavenger.git # Using https with username/password credentials
If testing the application, use the main
branch.
This is the quickest and easiest method to run the scavenger hunt web application. It uses a local database and demonstrates the power of virtual environments for local Python development.
The web application makes use of uv
for installing dependencies and creating the virtual environment.
If you don't have uv
installed yet, get it on Linux with:
curl -LsSf https://astral.sh/uv/install.sh | sh
For other operating systems please see here.
The application requires Python 3.12. If you don't have this version installed yet, the easiest way is to use uv
:
uv python install 3.12
Install all the dependencies inside a virtual environment with:
uv sync
These instructions describe using uvx
to run pre-commit. pre-commit
is a tool to configure pre-commit git
hooks. uvx
is not the only viable method to run pre-commit
(an alternative is e.g., pipx
)
Install pre-commit
as a uv
tool:
uv tool install pre-commit
This installs pre-commit
in its own, persistent, virtual environment so that it can be used for any project.
cd
to the repo and run:
uvx pre-commit install
This installs the pre-commit hook in .git/hooks
.
Finally, to test, run the hook against all the files:
uvx pre-commit run --all-files
Now whenever a commit is made ruff
will format, lint, and sort imports, for any changed files.
Run the web application using its internal development server with:
uv run fastapi dev ./openday_scavenger/main.py
If you want to modify the code, start the server with auto-reloading to avoid having to manually restart the application after code changes:
uv run fastapi dev ./openday_scavenger/main.py --reload
Open a web browser and go to http://localhost:8000
.
By default, the web application will use an in-memory sqlite database. If you like to persist the data over restarts of the application, use a file based sqlite database. Create a .env
file in the repository's root folder with the following content (you can adjust the filename to your liking):
DATABASE_SCHEME=sqlite
DATABASE_NAME="scavenger_hunt.db"
Restart the server and it will pick up the settings automatically.
If you would like a setup which is similar to a real production environment, use the provided devcontainer. It uses a proper PostgreSQL database server instead of sqlite.
Before you can use devcontainers make sure you have Docker as well as the VSCode Remote Development extensions installed. Please refer to the dev container setup documentation on the VSCode page.
Open the cloned repository in VSCode. Usually it will detect that a .devcontainer
folder is present and ask you whether you'd like to "Reopen folder to develop in a container". Press "Reopen in Container" and wait.
If VSCode does not prompt you, click on the button on the very bottom left of VSCode and select "Reopen in Container" from the drop down menu at the top.
Once the containers have finished starting up, browse to http://localhost:8000
in a web browser.
If you would like to see the content of the database with a database client (a GUI client we like is DBeaver), use the following settings:
Setting | Value |
---|---|
host | localhost |
port | 5432 |
db | postgres |
user | postgres |
password | postgres |
TBD
We would be delighted to receive contributions, especially in the form of puzzles. Please note that we are preparing for the Open Day 2024 and thus will only be able to accept contributions from Australian Synchrotron facility staff at this point in time.
All contributions are done via Pull Requests (PR). You can either fork the repository and send a PR through or create a branch in the upstream repository and send a merge request via PR. In both cases, please create a branch starting with your initials and a few words about the main work topics. For example am-puzzle-admin-edit-fields
.
For the Pull Request please request either Andreas or Stephen as a reviewer.
Along with the PR requirement, the main
branch is also protected by a status check. The status check runs a GitHub Action that checks the linting and formatting with ruff
and runs the tests with pytest
. The use of the pre-commit hook should ensure the linter and formatter pass.
The heart and soul of the scavenger hunt web application are its puzzles. This sections describes the process of adding a puzzle and integrating it into the user experience and administration flow.
You will find a demo
puzzle in the repository that you can use as an example or a basis for your own puzzle.
The web application uses a simple Cookie-based session management to track visitors. This system is a little bit annoying when developing new puzzles. We recommend to turn it off during development by adding the following parameter to your .env
file:
SESSIONS_ENABLED="false"
Pick a name for your puzzle. It should be one word (no whitespaces), can be a bit cryptic and needs to be unique among all puzzles. You will use the name in a few places, so coming up with a good initial name will make your life a lot easier.
We will use the very creative puzzle name of demo
to illustrate the process in the following.
Create a folder under openday_scavenger/puzzles
with your puzzle name.
In our example you will find the demo
puzzle under openday_scavenger/puzzles/demo
In your puzzle folder create a __init__.py
file and a file that will host the routes for your puzzle, for instance views.py
. You can create as many files in your folder as you like, but these two are the minimum you will need.
Within your routes file define the root route for your puzzle. You can define as many additional routes as you like, but we need at least one root route.
The following example doesn't implement a rendered puzzle page yet, but demonstrates the basics of a root route :
from fastapi import APIRouter
router = APIRouter()
@router.get('/')
async def index():
return {"test": "test"}
In order to make your puzzle visible to the world, register its API router. Attach it to the overarching puzzle router in the file openday_scavenger/puzzles/__init__.py
.
Add an import
and a include_router
statement like the following to the list of puzzle routes:
from .[your puzzle name].views import router as puzzle_[your puzzle name]_router
router.include_router(puzzle_[your puzzle name]_router, prefix='/[your puzzle name]')
With your puzzle root route created and registered your puzzle will be available under http://localhost:8000/puzzles/[your puzzle name]
However, when you try to browse to your puzzle, you will be greeted with a "Unknown Puzzle" error. This is because we will need to add your puzzle to the database. While this seems like an unnecessary step first, having the puzzle added to the database allows us to not only store the correct answer to the puzzle in a central place but also enables us to take puzzles offline with the click of a button in case something is not working.
Browse to the puzzle administration area http://localhost:8000/admin/puzzles
and add a your puzzle to the database by entering the name of your puzzle and click "Add new Puzzle".
Then click on the "Enable" button next to your puzzle. Now when you browse to http://localhost:8000/puzzles/[your puzzle name]
you will see your puzzle.
The steps above are only the beginning of your puzzle implementation journey. You will most likely want to serve rendered web pages, possibly dynamic content via JavaScript and more.
The demo
example in the repository uses a folder called static
to host static assets such as the puzzle html page and jinja2 to render the page. Feel free to use this code as a starting point for your own puzzle.
At some point your puzzle will need to submit the visitor's answer and display whether it is correct or not. This is accomplished by sending a POST
request to the endpoint /submission
with the following content encoded as multipart/form-data
:
Key | Value |
---|---|
visitor | The uid of the visitor submitting the answer |
name | The name of your puzzle |
answer | The answer that the visitor entered |
While the name of the puzzle and the answer is easy to collect, the visitor uid is a bit more complicated. You get the visitor uid from the authenticated visitor using the get_auth_visitor
dependency:
from typing import Annotated
from fastapi import APIRouter, Request, Depends
from openday_scavenger.api.visitors.schemas import VisitorAuth
from openday_scavenger.api.visitors.dependencies import get_auth_visitor
router = APIRouter()
@router.get('/')
async def index(request: Request, visitor: Annotated[VisitorAuth, Depends(get_auth_visitor)]):
pass
The reason for storing the correct answer in the database is twofold:
- we don't want to reveal the correct answer in the source code
- we can quickly fix an incorrect answer by modifying the database entry
For the time being the application only supports a case-sensitive, direct string comparison. If you need a more complex comparison method, such as a fuzzy comparison mode or computational methods (e.g. Levenshtein distance), please let Andreas or Stephen know.
The architecture of the web application is informed by the Scientific Computing standard for RESTful API services. The standard the team follows is very closely modelled after structures such as the Netflix Dispatch service or the abstraction model described here.
The following diagram illustrates the two layers our services are generally split into and the data protocols that are used between these layers.
Requests to a service are made using parameters defined in pydantic
schemas. The routes responding to requests are implemented in a view module focusing solely on the validation of input and assembly of the response. In order to accomplish this, routes make calls to functions defined in the service module, which hosts the business logic and data access calls. This allows the business logic to be used across multiple routes and easy to test. The service functions take pydantic
schemas as inputs which can, but don't need to be, the same schemas as the ones used in the view module. In order to make things easier, often the service functions use standard function arguments in addition or instead of pydantic
models as input.
A service function interacts with the database using SQLAlchemy
. The result of a transaction with the database is a SQLAlchemy
model which is returned to the route in the view model. The route then translates the SQLAlchemy
model into a pydantic
schema for the response.
We don't tend to use a CRUD layer between the service layer and the database as we found it added unnecessary complexity.
The scavenger hunt application differs to a Scientific Computing standard RESTful API service in that it renders web pages and provides a way for developers to extend it with puzzles. This resulted in a slightly different folder structure compared to our standard:
|- openday_scavenger
| |- api #RESTful API like structure
| | |- puzzles #service functions, db models and dependencies for puzzle management
| | |- visitors #service functions, db models and dependencies for visitor handling
| |- puzzles #folder for the puzzles
| | |- demo #demo puzzle folder
| | | |- static #folder for all static assets for the demo puzzle
| | | |- views.py #folder for all the routes of the demo puzzle
| |- static
| | |- css #common CSS files
| | |- html #common HTML files
| | |- js #common JavaScript files
| | |- webfonts #common fonts
| |- views
| | |- admin #routes that render all admin HTML pages
| | |- game #routes that render all game related HTML pages
The scavenger hunt web application makes use of a mix of our standard libraries that we use in Scientific Computing and a few new ones that we wanted to try out.
Library | Reason |
---|---|
uv | An extremely fast alternative to Poetry. Poetry is our standard for dependency management and virtual environments, but this project provided a great opportunity to try out uv . |
FastAPI | Our standard web framework for all RESTful and websocket APIs. |
SQLAlchemy | Our standard ORM for SQL databases. |
jinja2 | Our standard for templating, including the rendering of web pages. |
reportlab | Our preferred library for generating PDF files. |
segno | A QR code generator that we like. |
ruff | Our standard linter and code formatter for Python. |
mypy | Our standard type checker for Python. |
htmx | We usually use ReactJS/NextJS for our web frontends. But those frameworks seemed too heavy for this project, thus we decided to give htmx a go for the common game and admin pages. |
html5-qrcode | The QR scanner JavaScript library. This one seemed to work nicely and was least out of date of all the available QR scanner libraries. |
Bootstrap | We use a variety of styling libraries for our web services but for this project Bootstrap was the easiest and quickest way to introduce some decently styled components. |