This is a Django app for running puzzlehunts, written by several members of ✈✈✈ Galactic Trendsetters ✈✈✈ for running the Galactic Puzzle Hunt.
This repository is not formally maintained; expect it to be based on an export of the most recently run GPH, with any updates at other times of year being best-effort. In particular, while we've stripped out most of the hunt-specific logic, some fundamental pieces are still in place: GPH 2019 unlocked puzzles sequentially using DEEP (a combination of time and solves), while GPH 2020 had puzzles divided by round, so those conditions are reflected in the model schema and unlocking code. If you want to do something different, say an Australian-style hunt, it should still be possible, but you'll have to do some legwork.
We will try to respond to emails or pull requests when we can, but this isn't guaranteed, especially during the months when we aren't actively working on the current GPH. As several other online hunts have used this codebase, you may be able to direct questions to one of them, although at this time there does not seem to be any listing of such teams.
pip
. (This may be named pip3
depending on your environment.)pip install virtualenv
. This allows you to install this project's dependencies into a "virtual environment" contained in this directory.virtualenv venv
-p
argument to virtualenv
to point to the correct Python runtime, for example: virtualenv venv -p python3
python -m virtualenv venv
instead if you don't want to do that.source venv/bin/activate
venv/bin/activate.csh
or venv/bin/activate.fish
if you're using csh or fish.venv\Scripts\activate
on Windows.deactivate
.pip install -r requirements.txt
fatal error: Python.h: No such file or directory
? Try installing python-dev
libraries first (e.g. sudo apt-get install python3-dev
)../manage.py runserver
./manage.py migrate
, then start it again..save()
, that will save all the fields of the object and possibly overwrite some other handler that ran in the meantime. Our schema so happens to be set up so that (apart from Hints) we don't often have to update existing objects at all, let alone within fractions of a second of each other in a non-idempotent way. But we could address this with transactions, shortening the time between read and write, and/or limiting the fields written....even?
./manage.py runserver
and make changes within the puzzles/
subdirectory. runserver
will watch for code changes and automatically restart if needed....set up the database?
db.sqlite3
file in the root of this repository as its database. If this doesn't exist, Django will create a new empty database when you run ./manage.py migrate
. It's perfectly fine to start with this, but you won't have any puzzles populated and you almost certainly want to create a superuser.If you just want to try out the website quickly with some sample data, you can run ./manage.py loaddata sample.yaml
(after ./manage.py migrate
) to load a sample hunt, an admin account (username and password admin
), and a test account with a team (username and password test
). You can view the templates used to render the puzzles in the puzzles/templates/puzzle_bodies and puzzles/templates/solution_bodies folders, which you can also base your puzzles/solutions off of.
...be a superuser?
/admin
control panel on the server. We have additionally set it to control access to certain site pages/actions, such as replying to hint requests from teams, or viewing solutions before the deadline. ./manage.py createsuperuser
will make a new superuser from the command line, but this user won't be associated with any team on the site (so it won't be able to e.g. solve puzzles). To fix this, you can either get a prepopulated db.sqlite3
from a friend, hit the Create team
button in the top bar on the main site to attach a new team to your user, or go into /admin
and swap out the user on an existing team for your new one....edit the database?
/admin
control panel lets you query and modify all of the objects in the database. It should be pretty straightforward to use. It does use the same login as the main site, so you won't be able to log in as a superuser for /admin
and a non-superuser for the main site in the same browser window....be a testsolver?
/admin
and set the relevant checkbox there. Or, to make yourself a prerelease testsolver as a superuser, use the Toggle testsolver
button in the top bar....set up a "real" testsolve?
/admin
and set a team's start offset. The greater this offset, the earlier that team will be able to start and progress in the hunt. This can be used to run a full-hunt testsolve to test the unlock structure....see some other team's view of the hunt?
/teams
and click on any Impersonate
button. Be careful with this, as you don't want to accidentally perform any actions on behalf of the team....add a "keep going" message? give a team more guesses? delete a team? etc.
/admin
....give myself hints for testing? reset my hints? show me a puzzle's answer? etc.
/admin
)....postprod a puzzle?
db.sqlite3
with the puzzles set up) for your puzzle. The body_template
field on the Puzzle defines which template file will be used (this doesn't have to match the slug
field, though it may be nice if it does). Put the body of the puzzle in a file under puzzles/templates/puzzle_bodies
. Put required static resources under puzzles/static/puzzle_resources/$PUZZLE
. Put solutions and their resources under puzzles/templates/solution_bodies
. See the sample files there as guides.Puzzles and solutions (but not other templates) support Markdown (though the library may or may not have some bugs). You'll override either puzzle-body-md
or puzzle-body-html
depending on whether you'd like to write Markdown or HTML. The same applies to solution bodies, author notes, and appendices.
...edit an email template?
...create a new model?
models.py
on the pattern of the ones already there. To make it show up in /admin
, add it to admin.py
as well. Finally, if you add or change any database model or field, you'll need to run ./manage.py makemigrations
to create a migration file, then check that in....use a model?
O(n)
(or worse) separate database lookups for one query; otherwise, don't worry about it too much. However, if you'd like to find opportunities for optimization, you can set up Django to print database queries to the console by changing the django.db.backends
log setting....create a new view?
views.py
that returns a response object (usually by rendering a template, but you can also create one and write to it directly). Check if you want to gate it behind any of the decorators used in the file. You will need to add your view to urls.py
as well to make it accessible. The name you put in urls.py
should be used with functions like {% url %}
(in templates) or reverse
and redirect
(in Python) to generate the URL for your page whenever you need to output it....create a view called by a puzzle?
puzzlehandlers/
. That directory also contains helpers for rate limiting so teams can't brute-force your puzzle. Then in your puzzle template, you can include Javascript or forms that call your new view however you wish....add CSS?
base.html
or appears in multiple separate pages, put it in base.css
. Otherwise, just put it inline in your template....add template context?
context.py
, which defines context shared between all page templates. Otherwise, put it in a dict passed to render
in your view.py
function....add template functions?
templatetags/
. (This is enforced by Django for some reason.) Then, in the template file you're changing, include {% load puzzle_tags %}
at the top....set up the unlock structure?
hunt_config.py
. You will probably just have to edit these case by case, but note that e.g. it is not necessary to make code changes in order to update puzzle unlock thresholds....enable the story or wrapup page?
*_PAGE_VISIBLE
flags in hunt_config.py
to true....do analysis of what teams do during the hunt?
messaging.log_puzzle_info
. For example, if you have a puzzle that's a game, you can set up an endpoint to log whenever a team wins. You can also set up whatever additional logs you wish (and if you want, expose them using a new view over the bridge). Then you can write your own scripts or spreadsheets to analyze them....time zones?
templatetags/
), you may have to adjust its time zone explicitly to prevent it from showing as UTC....issue errata?
/errata
as a superuser to create one. Errata can be shown on the puzzle page, the top-level updates page, or both; you can also create a general announcement that's not associated with a puzzle. If you save an erratum as unpublished, you can see how it looks before revealing it to solvers. The updates page won't be available to solvers until there's something they can see there....answer hints?
/hints
, through links in Discord hint messages, or via the red hint icon that appears for superusers browsing the site when there are unanswered hints. The interface lets you claim a hint, write a response, and send it off to the team. If a hint is marked as obsolete, that means the team solved the puzzle while it was open; if refunded, then the responder decided not to charge them a hint token. If a hint is a followup, that means it's part of a conversation thread with the team and doesn't cost a token either.... use websockets?
messaging.py
; there are prototypes for two-way communication with a single browser tab, or for broadcasting to all members of a team or all logged-in admins. If you want something different, say for a "Teamwork Time" puzzle where team members interact with each other, it shouldn't be hard to add. Then add your consumer to routing.py
and use openSocket
in JS to connect to it.... provide the site in my language?
lang_COUNTRY
(e.g. en_US):django-admin makemessages -e html,txt,py,svg -l lang_COUNTRY
django-admin makemessages -d djangojs -l lang_COUNTRY
lang_COUNTRY
django-admin compilemessages
lang
(e.g. en) folder and copy an existing one (e.g. en to be translated, see https://docs.djangoproject.com/en/4.0/ref/settings/#std:setting-FORMAT_MODULE_PATH). This contains the date/time formats used in django templates (see https://docs.djangoproject.com/en/4.0/ref/templates/builtins/#std:templatefilter-date)lang-country
(e.g. en-us)’
instead of \u2019
)The GPH server is built on Django. We use Ansible to manage deploys to our cloud VMs and nginx as the web server in production, but you're free to use whatever web server setup makes sense for you.
db.sqlite3
: This is the database used by Django. An empty one is automatically created if you start the server without it, but for testing many features, you may wish to get one with teams, puzzles, etc. populated.manage.py
: This is Django's way of administering the server from the command line. It includes help features that will tell you the things it can do. Common commands are createsuperuser
, shell
/dbshell
, migrate
/makemigrations
, and runserver
. There are also custom commands, defined in puzzles/management/commands
.README.md
: You're reading me.requirements.txt
: A file that pip
can read to install the Python packages needed by the server. If you want to add one, put it in the file. Locally, you'll need to run pip install -r requirements.txt
to pick it up (inside the virtualenv if you're using one). The production server will pick it up when it next gets deployed.gph/
: A catch-all for various configuration.
wsgi.py
: Boilerplate for hooking Django up to a web server in production.settings/
: Here are a few sets of Django settings depending on environment. Most of the options are built-in to Django, so you can consult the docs. You can also put new things here if they should be global or differ by environment. They'll be accessible anywhere in the Django project.urls.py
: Routing configuration for the server. If you add a new page in views.py
, you'll need to add it here as well.logs/
: Holds logs written by the server while it runs.static/
: If you run collectstatic
, which you probably should in production, Django gathers files from puzzles/static
and puts them here. If you're seeing weird static file caching behavior or files you thought you'd deleted still showing up, try clearing this out.venv/
: Contains the virtualenv if you're using one, including all the Python packages you installed for this project.This directory contains all of the business logic for the site.
admin.py
: Sets up custom logic for the interface on /admin
for managing the database objects defined in models.py
. If you add a new model, add it here too.context.py
: This file defines an object that gets attached to the request, encompassing data that can be calculated when responding to the request as well as accessed inside rendered templates.forms.py
: Configuration for various user-visible forms found throughout the site, including validation functions.hunt_config.py
: Intended to encapsulate all the numbers and details for one year's hunt progression, including the date and time for the start and end of hunt.messaging.py
: Functions for sending email and Discord messages.models.py
: Defines database objects.
Puzzle
: A puzzle.Team
: A team corresponds to a Django user, since it has a single login, but a team can list multiple names and emails. TeamMember objects are essentially just for display and email purposes.PuzzleUnlock
: Represents a team having access to a puzzle. Since this needs to be recalculated all the time anyway as teams progress, it's not that useful as a caching mechanism. It mostly allows analysis and statistics of when exactly unlocks happened.AnswerSubmission
: A guess by a team on a puzzle, either right or wrong.Hint
: A hint request initiated by a team. Has special listeners to send email and Discord messages when one is received or answered.shortcuts.py
: Defines a number of one-click actions available to superusers for use while developing the site.views.py
: Defines the handlers serving each page on the site. Makes heavy use of decorators for access control.management/
: Defines custom commands for manage.py
; see below. Generally, this includes any sort of administrative action you might want to automate with access to the database.migrations/
: If you ever change models.py
by deleting, removing, or modifying a database type or its fields, run ./manage.py makemigrations
to autogenerate a migration file that makes necessary changes to the database. This runs during deployment, or run ./manage.py migrate
locally.puzzlehandlers/
: If you write a puzzle that requires server code, put it in a new file here (and refer to it in views.py
and/or urls.py
). You can wrap it in a rate limiter and export it from __init__.py
.static/
: Any files to be served directly to the user's browser. Note: do NOT put anything used by a puzzle solution in here, as they should be locked until the hunt ends.templates/
: Generally, these get rendered from views.py
. Contains not only HTML files but also plain-text email bodies (side-by-side with HTML versions) and inline SVGs.
puzzle_bodies/
: All templates for individual puzzles. Put any static resources in static/puzzle_resources/$PUZZLE/
.solution_bodies/
: All templates for individual solutions. Put any static resources in templates/solution_bodies/$PUZZLE/
.templatetags/
: If you want to define a function callable from within a template, put it in puzzle_tags.py
. This is for stuff like formatting timestamps.If you are new to web development and deployment, you can check out DEPLOY.md for some work in progress suggestions on places to deploy this site and instructions on how to deploy them. Otherwise, here is a short list of things you should fix
(The most accurate way is probably just to grep for the string FIXME
.
Required:
puzzles/hunt_config.py
: hunt times, title, organizers, email, etc.gph/settings/prod.py
and gph/settings/staging.py
if you're using that.Optional:
settings/base.py
.puzzles/messaging.py
contains some configurable settings for Discord webhooks.Your main tool will be the Django admin panel, at /admin
on either a local or production server. Logging in with an admin account will let you edit any database object. Convenience commands are available in the shortcuts menu on the main site.
manage.py
is a command-line management tool. We've added some custom commands in puzzles/management/
. If you're running the site in a production environment, you'll need SSH access to the relevant server.
If something goes very wrong, you can try SSHing to the server and editing files or using Git commands directly. We recommend taking regular backups of the database that you can restore from if need be. We also recommend controlling which commits make it to the live site during the hunt, by creating a separate production
Git branch that lags behind master
, and verifying all changes on a staging deploy.
In addition to the hunt start and end time, there's also a somewhat non-obvious "hunt close time" in hunt_config.py
. Here's how it works:
You can, of course, set the hunt close time to be equal to the hunt end time to skip the in-between stage.