├── .dockerignore ├── huntserver ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0051_remove_hunt_last_update_time.py │ ├── 0052_remove_puzzle_num_pages.py │ ├── 0060_remove_puzzle_is_html_puzzle.py │ ├── 0005_remove_person_email.py │ ├── 0058_auto_20200507_2138.py │ ├── 0010_auto_20160112_0928.py │ ├── 0019_auto_20171008_1553.py │ ├── 0056_auto_20200418_1151.py │ ├── 0016_team_playtester.py │ ├── 0007_person_allergies.py │ ├── 0015_auto_20161226_0907.py │ ├── 0014_hunt_is_current_hunt.py │ ├── 0002_puzzle_num_pages.py │ ├── 0076_team_is_local.py │ ├── 0043_puzzle_doesnt_count.py │ ├── 0011_team_join_code.py │ ├── 0025_puzzle_is_meta.py │ ├── 0067_auto_20201020_2211.py │ ├── 0044_auto_20200207_0936.py │ ├── 0071_auto_20201021_2143.py │ ├── 0008_hunt_location.py │ ├── 0030_prepuzzle_released.py │ ├── 0020_auto_20171008_2203.py │ ├── 0026_auto_20181021_1548.py │ ├── 0040_auto_20200108_1126.py │ ├── 0062_auto_20201012_1018.py │ ├── 0061_puzzle_puzzle_file.py │ ├── 0009_auto_20160104_1317.py │ ├── 0003_auto_20151226_0710.py │ ├── 0035_hunt_resource_link.py │ ├── 0077_hint_responder.py │ ├── 0034_puzzle_solution_link.py │ ├── 0037_auto_20190925_1733.py │ ├── 0070_auto_20201021_2042.py │ ├── 0006_auto_20160102_1418.py │ ├── 0012_submission_modified_date.py │ ├── 0064_auto_20201020_2151.py │ ├── 0004_auto_20151226_0715.py │ ├── 0031_auto_20190110_0902.py │ ├── 0041_auto_20200110_1408.py │ ├── 0036_auto_20190310_0900.py │ ├── 0032_auto_20190110_1518.py │ ├── 0048_auto_20200215_1743.py │ ├── 0047_auto_20200210_2201.py │ ├── 0055_flatpageproxyobject.py │ ├── 0013_response.py │ ├── 0057_auto_20200424_1159.py │ ├── 0039_auto_20200108_1124.py │ ├── 0053_auto_20200310_1503.py │ ├── 0049_auto_20200215_1930.py │ ├── 0018_auto_20171008_1552.py │ ├── 0075_auto_20210208_0939.py │ ├── 0074_auto_20210207_2304.py │ ├── 0033_auto_20190117_2118.py │ ├── 0072_auto_20201103_1539.py │ ├── 0063_auto_20201012_1143.py │ ├── 0029_auto_20190108_1429.py │ ├── 0023_userproxyobject.py │ ├── 0068_auto_20201021_1932.py │ ├── 0073_auto_20210207_2300.py │ ├── 0027_auto_20181023_2303.py │ ├── 0050_auto_20200305_1208.py │ ├── 0066_auto_20201020_2200.py │ ├── 0065_auto_20201020_2200.py │ ├── 0069_auto_20201021_1932.py │ ├── 0028_prepuzzle.py │ ├── 0045_auto_20200209_0856.py │ ├── 0038_hint.py │ ├── 0059_puzzle_puzzle_page_type.py │ ├── 0024_auto_20181021_0015.py │ ├── 0078_auto_20220205_2106.py │ ├── 0054_auto_20200318_2145.py │ └── 0046_auto_20200209_1112.py ├── templatetags │ ├── __init__.py │ ├── prepuzzle_tags.py │ ├── bootstrap_tags.py │ └── hunt_tags.py ├── static │ ├── huntserver │ │ ├── base.css │ │ ├── 404.jpg │ │ ├── 500.png │ │ ├── goat.mp3 │ │ ├── favicon.ico │ │ ├── background.jpg │ │ ├── cmu_login.png │ │ ├── phcmulogo.png │ │ ├── phcmulong.png │ │ ├── pitt_login.png │ │ ├── background1.jpg │ │ ├── phcmu_login.png │ │ ├── phcmulong_bw.png │ │ ├── admin_addon.css │ │ ├── admin_change_puzzle.js │ │ ├── staff_chat.js │ │ ├── chat.css │ │ ├── chat_poll.js │ │ ├── info_base.css │ │ ├── hunt_base.css │ │ ├── chat.js │ │ ├── queue.js │ │ ├── hint.js │ │ └── admin.css │ ├── sample.pdf │ ├── codemirror_html.js │ └── js.cookie.js ├── templates │ ├── puzzle_sub_row.html │ ├── flatpages │ │ └── default.html │ ├── attribute_error.html │ ├── chat_messages.html │ ├── user_profile.html │ ├── create_account.html │ ├── contact_us.html │ ├── 404.html │ ├── prepuzzle.html │ ├── registration │ │ └── login.html │ ├── 500.html │ ├── staff_base.html │ ├── prepuzzle_answerbox.html │ ├── shib_register.html │ ├── chat.html │ ├── charts.html │ ├── unlockables.html │ ├── queue_row.html │ ├── access_error.html │ ├── resources.html │ ├── leaderboard.html │ ├── info_base.html │ ├── hunt_base.html │ ├── login_selection.html │ ├── staff_chat.html │ ├── puzzle_hint.html │ ├── hunt_info.html │ ├── hunt_example.html │ ├── index.html │ ├── queue.html │ ├── previous_hunts.html │ ├── staff_hunt_info.html │ ├── email.html │ ├── hint_row.html │ └── progress.html ├── management │ └── commands │ │ └── runupdates.py ├── widgets.py └── fixtures │ └── initial_hunt.json ├── puzzlehunt_server ├── __init__.py ├── settings │ ├── __init__.py │ ├── local_settings.py.template │ ├── travis_settings.py │ └── env_settings.py ├── templates │ └── registration │ │ ├── password_reset_subject.txt │ │ ├── password_reset_complete.html │ │ ├── password_reset_form.html │ │ ├── password_reset_done.html │ │ ├── password_reset_confirm.html │ │ └── password_reset_email.html ├── wsgi.py └── urls.py ├── .flake8 ├── docker ├── volumes │ ├── logs │ │ └── README │ ├── ssl-certs │ │ └── README │ └── shib-certs │ │ └── README ├── local_override.yml ├── shib_override.yml ├── mysql_override.yml ├── apacheShibForeground ├── apacheDockerfile ├── configs │ ├── puzzlehunt_apache.conf │ ├── puzzlehunt_apache_shib.conf │ └── inc-md-cert-mdq.pem ├── apacheShibDockerfile └── proxy_override.yml ├── docs ├── models.rst ├── images │ ├── hunt_1.png │ ├── main_1.png │ └── progress_1.png ├── index.rst ├── views.rst ├── basics.rst └── setup.rst ├── .coveragerc ├── config ├── puzzlehunt_mysql.cnf └── puzzlehunt_apache_ssl.conf ├── manage.py ├── requirements.txt ├── Dockerfile ├── .gitignore ├── sample.env ├── .travis.yml ├── LICENSE ├── .github └── workflows │ └── main.yml ├── docker-compose.yml ├── locust └── reset_data.py └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | docker/ -------------------------------------------------------------------------------- /huntserver/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /huntserver/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /puzzlehunt_server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /huntserver/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /puzzlehunt_server/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | exclude = huntserver/migrations 4 | -------------------------------------------------------------------------------- /docker/volumes/logs/README: -------------------------------------------------------------------------------- 1 | This directory will hold all of the logs from the various services -------------------------------------------------------------------------------- /huntserver/static/huntserver/base.css: -------------------------------------------------------------------------------- 1 | body { padding-top: 70px; } 2 | nav { padding-right: 10px; } -------------------------------------------------------------------------------- /puzzlehunt_server/templates/registration/password_reset_subject.txt: -------------------------------------------------------------------------------- 1 | Puzzle Hunt CMU Password Reset -------------------------------------------------------------------------------- /docker/volumes/ssl-certs/README: -------------------------------------------------------------------------------- 1 | This directory will hold lets-encrypt data when using shib-override.yml -------------------------------------------------------------------------------- /docs/models.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | Models 3 | ====== 4 | 5 | .. automodule:: huntserver.models 6 | :members: -------------------------------------------------------------------------------- /docker/volumes/shib-certs/README: -------------------------------------------------------------------------------- 1 | Put sp-key.pem and sp-cert.pem in this directory when deploying shibboleth -------------------------------------------------------------------------------- /docs/images/hunt_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlareau/puzzlehunt_server/HEAD/docs/images/hunt_1.png -------------------------------------------------------------------------------- /docs/images/main_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlareau/puzzlehunt_server/HEAD/docs/images/main_1.png -------------------------------------------------------------------------------- /docs/images/progress_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlareau/puzzlehunt_server/HEAD/docs/images/progress_1.png -------------------------------------------------------------------------------- /huntserver/static/sample.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlareau/puzzlehunt_server/HEAD/huntserver/static/sample.pdf -------------------------------------------------------------------------------- /docker/local_override.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | web: 5 | ports: 6 | - "80:80" 7 | - "443:443" -------------------------------------------------------------------------------- /huntserver/static/huntserver/404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlareau/puzzlehunt_server/HEAD/huntserver/static/huntserver/404.jpg -------------------------------------------------------------------------------- /huntserver/static/huntserver/500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlareau/puzzlehunt_server/HEAD/huntserver/static/huntserver/500.png -------------------------------------------------------------------------------- /huntserver/static/huntserver/goat.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlareau/puzzlehunt_server/HEAD/huntserver/static/huntserver/goat.mp3 -------------------------------------------------------------------------------- /huntserver/static/huntserver/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlareau/puzzlehunt_server/HEAD/huntserver/static/huntserver/favicon.ico -------------------------------------------------------------------------------- /huntserver/static/huntserver/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlareau/puzzlehunt_server/HEAD/huntserver/static/huntserver/background.jpg -------------------------------------------------------------------------------- /huntserver/static/huntserver/cmu_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlareau/puzzlehunt_server/HEAD/huntserver/static/huntserver/cmu_login.png -------------------------------------------------------------------------------- /huntserver/static/huntserver/phcmulogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlareau/puzzlehunt_server/HEAD/huntserver/static/huntserver/phcmulogo.png -------------------------------------------------------------------------------- /huntserver/static/huntserver/phcmulong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlareau/puzzlehunt_server/HEAD/huntserver/static/huntserver/phcmulong.png -------------------------------------------------------------------------------- /huntserver/static/huntserver/pitt_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlareau/puzzlehunt_server/HEAD/huntserver/static/huntserver/pitt_login.png -------------------------------------------------------------------------------- /huntserver/static/huntserver/background1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlareau/puzzlehunt_server/HEAD/huntserver/static/huntserver/background1.jpg -------------------------------------------------------------------------------- /huntserver/static/huntserver/phcmu_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlareau/puzzlehunt_server/HEAD/huntserver/static/huntserver/phcmu_login.png -------------------------------------------------------------------------------- /huntserver/static/huntserver/phcmulong_bw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlareau/puzzlehunt_server/HEAD/huntserver/static/huntserver/phcmulong_bw.png -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | command_line = 3 | manage.py test 4 | 5 | source = 6 | huntserver 7 | 8 | omit = 9 | */tests/* 10 | */migrations/* 11 | 12 | [paths] 13 | source = 14 | ./huntserver 15 | /code/huntserver -------------------------------------------------------------------------------- /config/puzzlehunt_mysql.cnf: -------------------------------------------------------------------------------- 1 | [client] 2 | default-character-set = utf8mb4 3 | 4 | [mysql] 5 | default-character-set = utf8mb4 6 | 7 | [mysqld] 8 | character-set-client-handshake = FALSE 9 | character-set-server = utf8mb4 10 | collation-server = utf8mb4_unicode_ci 11 | -------------------------------------------------------------------------------- /docker/shib_override.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | web: 5 | build: 6 | dockerfile: apacheShibDockerfile 7 | volumes: 8 | - ./docker/volumes/shib-certs/:/etc/shibboleth/certs/ 9 | app: 10 | environment: 11 | - DJANGO_USE_SHIBBOLETH=True -------------------------------------------------------------------------------- /huntserver/templates/puzzle_sub_row.html: -------------------------------------------------------------------------------- 1 | 2 | {{ submission.submission_time|time:"h:i a" }} 3 | {{ submission.submission_text }} 4 | {{ submission.convert_markdown_response|safe }} 5 | -------------------------------------------------------------------------------- /huntserver/templatetags/prepuzzle_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | register = template.Library() 3 | 4 | 5 | @register.simple_tag(takes_context=True) 6 | def prepuzzle_static(context): 7 | from django.conf import settings 8 | return settings.MEDIA_URL + "prepuzzles/" + str(context['puzzle'].pk) + "/" 9 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "puzzlehunt_server.settings.local_settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) -------------------------------------------------------------------------------- /huntserver/management/commands/runupdates.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from huntserver.utils import update_time_items 3 | 4 | 5 | class RunUpdates(BaseCommand): 6 | help = 'Runs all time related updates for the huntserver app' 7 | 8 | def handle(self, *args, **options): 9 | update_time_items() 10 | -------------------------------------------------------------------------------- /puzzlehunt_server/templates/registration/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends 'info_base.html' %} 2 | 3 | {% block content %} 4 |
5 |

6 | Your password has been set. You may go ahead and sign in now. 7 |

8 |
9 | {% endblock %} -------------------------------------------------------------------------------- /huntserver/static/codemirror_html.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var $ = django.jQuery; 3 | $(document).ready(function(){ 4 | $('textarea.html-editor').each(function(idx, el){ 5 | CodeMirror.fromTextArea(el, { 6 | lineNumbers: true, 7 | mode: 'htmlmixed' 8 | }); 9 | }); 10 | }); 11 | })(); -------------------------------------------------------------------------------- /huntserver/templates/flatpages/default.html: -------------------------------------------------------------------------------- 1 | {% extends "info_base.html" %} 2 | {% load hunt_tags %} 3 | 4 | {% block includes %} 5 | 6 | {% endblock includes %} 7 | 8 | {% block content %} 9 | 10 |
11 | {{ flatpage.content|render_with_context }} 12 |
13 | {% endblock content %} -------------------------------------------------------------------------------- /puzzlehunt_server/templates/registration/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'info_base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Forgot password

6 | 7 |
8 | {% csrf_token %} 9 | {{ form.as_p }} 10 | 11 |
12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /docker/mysql_override.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | image: mariadb 6 | environment: 7 | MYSQL_DATABASE: ${DB_USER} 8 | MYSQL_USER: ${DB_USER} 9 | MYSQL_PASSWORD: ${DB_PASSWORD} 10 | MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} 11 | 12 | app: 13 | environment: 14 | - DATABASE_URL=mysql://${DB_USER}:${DB_PASSWORD}@db/${DB_USER} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==2.2.24 2 | decorator==4.1.2 3 | bootstrap-admin==0.4.* 4 | django-debug-toolbar==1.11.1 5 | python-dateutil==2.6.1 6 | sqlparse==0.2.3 7 | sphinx 8 | sphinx_rtd_theme 9 | django-ratelimit==1.1.0 10 | coverage 11 | django-crispy-forms==1.8.1 12 | gunicorn 13 | psycopg2==2.8.6 14 | dj-database-url 15 | django-redis 16 | redis 17 | huey 18 | sentry-sdk==0.14.2 19 | -------------------------------------------------------------------------------- /docker/apacheShibForeground: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -f /etc/apache2/run/httpd.pid /var/lock/subsys/shibd 4 | cp /etc/shibboleth/certs/sp-* /etc/shibboleth/ 5 | chmod 644 /etc/shibboleth/sp-cert.pem 6 | chmod 600 /etc/shibboleth/sp-key.pem 7 | chown _shibd:_shibd /etc/shibboleth/sp-* 8 | 9 | service shibd start 10 | service apache2 stop 11 | 12 | exec /usr/sbin/apache2ctl -D FOREGROUND 13 | -------------------------------------------------------------------------------- /huntserver/templates/attribute_error.html: -------------------------------------------------------------------------------- 1 | {% extends "info_base.html" %} 2 | 3 | {% block title %}Forbidden{% endblock %} 4 | 5 | {% block content %} 6 | 7 |

Your IdP is not releasing all the required attributes for this service.

8 | 9 | 14 | 15 | {% endblock %} 16 | 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | ENV DJANGO_ENABLE_DEBUG False 5 | ENV DJANGO_USE_SHIBBOLETH False 6 | ENV DJANGO_SETTINGS_MODULE puzzlehunt_server.settings.env_settings 7 | 8 | RUN mkdir /code 9 | WORKDIR /code 10 | 11 | COPY requirements.txt requirements.txt 12 | RUN pip install -r requirements.txt 13 | 14 | COPY . . 15 | 16 | EXPOSE 8000 17 | CMD ["gunicorn", "--workers=5", "--bind=0.0.0.0:8000", "puzzlehunt_server.wsgi:application"] -------------------------------------------------------------------------------- /huntserver/migrations/0051_remove_hunt_last_update_time.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-03-05 21:30 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0050_auto_20200305_1208'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='hunt', 15 | name='last_update_time', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /huntserver/migrations/0052_remove_puzzle_num_pages.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-03-06 19:12 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0051_remove_hunt_last_update_time'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='puzzle', 15 | name='num_pages', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /huntserver/migrations/0060_remove_puzzle_is_html_puzzle.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-10-12 02:31 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0059_puzzle_puzzle_page_type'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='puzzle', 15 | name='is_html_puzzle', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /huntserver/migrations/0005_remove_person_email.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0004_auto_20151226_0715'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='person', 16 | name='email', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /huntserver/migrations/0058_auto_20200507_2138.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-05-08 01:38 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0057_auto_20200424_1159'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='puzzle', 15 | options={'ordering': ['-hunt', 'puzzle_number']}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /docker/apacheDockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:10 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y apache2 libapache2-mod-xsendfile 5 | 6 | COPY configs/puzzlehunt_apache.conf /etc/apache2/sites-available/puzzlehunt.conf 7 | RUN rm /etc/apache2/sites-enabled/* && \ 8 | a2enmod proxy proxy_http proxy_html xsendfile && \ 9 | a2ensite puzzlehunt && \ 10 | mkdir -p /static && \ 11 | mkdir -p /media 12 | 13 | EXPOSE 80 14 | 15 | ENTRYPOINT ["/usr/sbin/apache2ctl"] 16 | CMD ["-D", "FOREGROUND"] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.*~ 2 | *.pyc 3 | /static/ 4 | /media/ 5 | /huntserver/static/huntserver/*.pdf 6 | /huntserver/static/huntserver/puzzles/* 7 | /huntserver/fixtures/final* 8 | /puzzlehunt_server/settings/local_settings.py 9 | /puzzlehunt_server/settings/dev_settings.py 10 | /docs/_build/ 11 | db.sqlite3 12 | /env/ 13 | /venv/ 14 | setup.cfg 15 | /cover/ 16 | .coverage 17 | debug.log 18 | /docker/volumes/ssl-certs/ 19 | /docker/volumes/shib-certs/ 20 | /docker/volumes/redis_data/ 21 | /docker/volumes/logs/ 22 | *.env -------------------------------------------------------------------------------- /puzzlehunt_server/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for puzzlehunt_server project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "puzzlehunt_server.settings.local_settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /huntserver/migrations/0010_auto_20160112_0928.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0009_auto_20160104_1317'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RenameField( 15 | model_name='person', 16 | old_name='is_andrew_acct', 17 | new_name='is_shib_acct', 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /huntserver/migrations/0019_auto_20171008_1553.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0018_auto_20171008_1552'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='hunt', 16 | name='template', 17 | field=models.TextField(default=b''), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /huntserver/migrations/0056_auto_20200418_1151.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-04-18 15:51 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0055_flatpageproxyobject'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='userproxyobject', 15 | options={'ordering': ['-pk'], 'verbose_name': 'user', 'verbose_name_plural': 'users'}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /huntserver/migrations/0016_team_playtester.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0015_auto_20161226_0907'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='team', 16 | name='playtester', 17 | field=models.BooleanField(default=False), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /puzzlehunt_server/templates/registration/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'info_base.html' %} 2 | 3 | {% block content %} 4 |
5 |

6 | We've emailed you instructions for setting your password, if an account exists with the email you entered. 7 | You should receive them shortly. 8 |

9 |

10 | If you don't receive an email, please make sure you've entered the address you registered with, 11 | and check your spam folder. 12 |

13 |
14 | {% endblock %} -------------------------------------------------------------------------------- /huntserver/migrations/0007_person_allergies.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0006_auto_20160102_1418'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='person', 16 | name='allergies', 17 | field=models.CharField(max_length=400, blank=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /huntserver/migrations/0015_auto_20161226_0907.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0014_hunt_is_current_hunt'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='hunt', 16 | name='is_current_hunt', 17 | field=models.BooleanField(default=False), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /huntserver/migrations/0014_hunt_is_current_hunt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0013_response'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='hunt', 16 | name='is_current_hunt', 17 | field=models.NullBooleanField(default=None, unique=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /huntserver/templates/chat_messages.html: -------------------------------------------------------------------------------- 1 | {% for message in messages %} 2 | {% if message.is_response %} 3 |
Admin : {{ message.text }}
{{message.time|date:"g:i a"}}
4 | {% else %} 5 |
{{ team_name|slice:":10"|ljust:10 }}: {{ message.text }}
{{message.time|date:"g:i a"}}
6 | {% endif %} 7 | {% endfor %} -------------------------------------------------------------------------------- /huntserver/migrations/0002_puzzle_num_pages.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='puzzle', 16 | name='num_pages', 17 | field=models.IntegerField(default=0), 18 | preserve_default=False, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /huntserver/migrations/0076_team_is_local.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2021-02-10 21:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0075_auto_20210208_0939'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='team', 15 | name='is_local', 16 | field=models.BooleanField(default=False, help_text='Is this team from CMU (or your organization)'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /huntserver/migrations/0043_puzzle_doesnt_count.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-02-06 02:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0042_auto_20200204_2304'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='puzzle', 15 | name='doesnt_count', 16 | field=models.BooleanField(default=False, help_text='Should this puzzle not count towards scoring?'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /huntserver/migrations/0011_team_join_code.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0010_auto_20160112_0928'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='team', 16 | name='join_code', 17 | field=models.CharField(default='FFFFF', max_length=5), 18 | preserve_default=False, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /huntserver/migrations/0025_puzzle_is_meta.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.15 on 2018-10-21 19:39 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('huntserver', '0024_auto_20181021_0015'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='puzzle', 17 | name='is_meta', 18 | field=models.BooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /huntserver/migrations/0067_auto_20201020_2211.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-10-21 02:11 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0066_auto_20201020_2200'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='puzzle', 15 | name='resource_link', 16 | ), 17 | migrations.RemoveField( 18 | model_name='puzzle', 19 | name='solution_link', 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /huntserver/migrations/0044_auto_20200207_0936.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-02-07 14:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0043_puzzle_doesnt_count'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='hintunlockplan', 15 | name='num_triggered', 16 | field=models.IntegerField(default=0, help_text='Number of times this Unlock Plan has given a hint'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /huntserver/migrations/0071_auto_20201021_2143.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-10-22 01:43 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0070_auto_20201021_2042'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='hunt', 15 | name='resource_link', 16 | ), 17 | migrations.RemoveField( 18 | model_name='prepuzzle', 19 | name='resource_link', 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /huntserver/migrations/0008_hunt_location.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0007_person_allergies'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='hunt', 16 | name='location', 17 | field=models.CharField(default='Porter Hall 100', max_length=100), 18 | preserve_default=False, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /huntserver/migrations/0030_prepuzzle_released.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.15 on 2019-01-09 16:55 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('huntserver', '0029_auto_20190108_1429'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='prepuzzle', 17 | name='released', 18 | field=models.BooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /huntserver/templates/user_profile.html: -------------------------------------------------------------------------------- 1 | {% extends "info_base.html" %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block content %} 5 |
6 |

Change User Details

7 |
8 | {% csrf_token %} 9 |

None of the information collected here will be displayed publicly.

10 |
11 | {{ user_form|crispy }} 12 | {{ person_form|crispy }} 13 |
14 |
15 | 16 |
17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /huntserver/migrations/0020_auto_20171008_2203.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import huntserver.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('huntserver', '0019_auto_20171008_1553'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='huntassetfile', 17 | name='file', 18 | field=models.FileField(storage=huntserver.models.OverwriteStorage(), upload_to=b'hunt/assets/'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /huntserver/static/huntserver/admin_addon.css: -------------------------------------------------------------------------------- 1 | 2 | .CodeMirror.cm-s-default { 3 | height: 600px; 4 | border: 1px solid black; 5 | } 6 | 7 | td.original p { 8 | visibility: hidden; 9 | width: 0px; 10 | height: 0px; 11 | } 12 | 13 | .sidebar-menu .django-admin-logo { 14 | background: #efefef; 15 | border-right: 1px solid #ddd; 16 | height: 70px; 17 | padding-top: 5px; 18 | padding-bottom: 5px; 19 | } 20 | 21 | .formset_border { 22 | border: 1px solid lightgrey; 23 | padding: 15px; 24 | padding-bottom: 0px; 25 | margin-bottom: 15px; 26 | } 27 | 28 | .field-is_current_hunt { 29 | margin-top: -20px; 30 | } -------------------------------------------------------------------------------- /huntserver/migrations/0026_auto_20181021_1548.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.15 on 2018-10-21 19:48 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('huntserver', '0025_puzzle_is_meta'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='puzzle', 17 | name='is_meta', 18 | field=models.BooleanField(default=False, help_text=b'Is this puzzle a meta-puzzle?'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /puzzlehunt_server/templates/registration/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends 'info_base.html' %} 2 | 3 | {% block content %} 4 |
5 | {% if validlink %} 6 |

Change password

7 |
8 | {% csrf_token %} 9 | {{ form.as_p }} 10 | 11 |
12 | {% else %} 13 |

14 | The password reset link was invalid, possibly because it has already been used. 15 | Please request a new password reset. 16 |

17 | {% endif %} 18 |
19 | {% endblock %} -------------------------------------------------------------------------------- /puzzlehunt_server/settings/local_settings.py.template: -------------------------------------------------------------------------------- 1 | from .base_settings import * 2 | 3 | DEBUG = False 4 | 5 | SECRET_KEY = 'this is not the secret key, use your own' 6 | 7 | DATABASES = { 8 | 'default': { 9 | 'ENGINE': 'django.db.backends.mysql', 10 | 'NAME': 'puzzlehunt_db', 11 | 'HOST': 'localhost', 12 | 'PORT': '3306', 13 | 'USER': 'nottherealusername', 14 | 'PASSWORD': 'nottherealpassword', 15 | 'OPTIONS': {'charset': 'utf8mb4'}, 16 | } 17 | } 18 | 19 | INTERNAL_IPS = '' 20 | 21 | EMAIL_HOST_USER = '' 22 | EMAIL_HOST_PASSWORD = '' 23 | ALLOWED_HOSTS = ['*'] 24 | -------------------------------------------------------------------------------- /huntserver/migrations/0040_auto_20200108_1126.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.23 on 2020-01-08 16:26 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('huntserver', '0039_auto_20200108_1124'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='hint', 17 | name='response_time', 18 | field=models.DateTimeField(blank=True, help_text=b'Hint response time', null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /huntserver/migrations/0062_auto_20201012_1018.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-10-12 14:18 2 | 3 | from django.db import migrations, models 4 | import huntserver.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0061_puzzle_puzzle_file'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='puzzle', 16 | name='puzzle_file', 17 | field=models.FileField(blank=True, storage=huntserver.models.OverwriteStorage(), upload_to=huntserver.models.get_puzzle_file_path), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /huntserver/migrations/0061_puzzle_puzzle_file.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-10-12 14:11 2 | 3 | from django.db import migrations, models 4 | import huntserver.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0060_remove_puzzle_is_html_puzzle'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='puzzle', 16 | name='puzzle_file', 17 | field=models.FileField(blank=True, storage=huntserver.models.OverwriteStorage, upload_to=huntserver.models.get_puzzle_file_path), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /huntserver/migrations/0009_auto_20160104_1317.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0008_hunt_location'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterUniqueTogether( 15 | name='solve', 16 | unique_together=set([('puzzle', 'team')]), 17 | ), 18 | migrations.AlterUniqueTogether( 19 | name='unlock', 20 | unique_together=set([('puzzle', 'team')]), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /huntserver/migrations/0003_auto_20151226_0710.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0002_puzzle_num_pages'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='person', 16 | name='year', 17 | ), 18 | migrations.AddField( 19 | model_name='person', 20 | name='andrewid', 21 | field=models.CharField(max_length=8, blank=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /huntserver/migrations/0035_hunt_resource_link.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.18 on 2019-03-09 13:14 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('huntserver', '0034_puzzle_solution_link'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='hunt', 17 | name='resource_link', 18 | field=models.URLField(blank=True, help_text=b'The full link (needs http://) to a folder of additional resources.'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /huntserver/migrations/0077_hint_responder.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2021-03-26 00:12 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0076_team_is_local'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='hint', 16 | name='responder', 17 | field=models.ForeignKey(blank=True, help_text='Staff member that has claimed the hint.', null=True, on_delete=django.db.models.deletion.CASCADE, to='huntserver.Person'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /huntserver/templatetags/bootstrap_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | register = template.Library() 3 | 4 | 5 | @register.simple_tag 6 | def active_page(request, view_name): 7 | from django.urls import resolve, Resolver404 8 | if not request: 9 | return "" 10 | try: 11 | r = resolve(request.path_info) 12 | url_name_bool = r.url_name == view_name 13 | if("url" in r.kwargs): 14 | url_val_bool = view_name.strip("/") in r.kwargs["url"] 15 | else: 16 | url_val_bool = False 17 | return "active" if (url_name_bool or url_val_bool) else "" 18 | except Resolver404: 19 | return "" 20 | -------------------------------------------------------------------------------- /huntserver/migrations/0034_puzzle_solution_link.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.15 on 2019-01-18 17:49 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('huntserver', '0033_auto_20190117_2118'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='puzzle', 17 | name='solution_link', 18 | field=models.URLField(blank=True, help_text=b'The full link (needs http://) to a publicly accessible PDF of the solution'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /huntserver/migrations/0037_auto_20190925_1733.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.18 on 2019-09-25 21:33 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('huntserver', '0036_auto_20190310_0900'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='puzzle', 17 | name='puzzle_id', 18 | field=models.CharField(help_text=b'A 3-5 character hex string that uniquely identifies the puzzle', max_length=8, unique=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /huntserver/templates/create_account.html: -------------------------------------------------------------------------------- 1 | {% extends "info_base.html" %} 2 | {% load crispy_forms_tags %} 3 | {% block title %}Registration{% endblock title %} 4 | 5 | {% block includes %} 6 | 7 | {% endblock includes %} 8 | 9 | {% block content %} 10 |
11 |

Registration

12 |

None of the information collected here will be displayed publicly.

13 |
14 | {% csrf_token %} 15 | {{uf|crispy}} 16 | {{pf|crispy}} 17 | 18 |
19 |
20 | {% endblock content %} 21 | -------------------------------------------------------------------------------- /huntserver/templates/contact_us.html: -------------------------------------------------------------------------------- 1 | 3 | 4 |

Contact Us

5 |
6 |

7 | Email the HALP?! LINE ({% contact_email %}) with "Puzzle Hunt" somewhere in the subject line. 8 |

9 |

Visit us on our Facebook Page. 10 | You can also find us on The Bridge.

11 |
-------------------------------------------------------------------------------- /huntserver/migrations/0070_auto_20201021_2042.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-10-22 00:42 2 | 3 | from django.db import migrations, models 4 | import huntserver.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0069_auto_20201021_1932'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='hunt', 16 | name='resource_file', 17 | field=models.FileField(blank=True, help_text='Hunt resources, MUST BE A ZIP FILE.', storage=huntserver.models.PuzzleOverwriteStorage(), upload_to=huntserver.models.get_hunt_file_path), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /huntserver/static/huntserver/admin_change_puzzle.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", function() { 2 | (function($) { 3 | function update_hidden(argument) { 4 | var radio_val = $('input[name="unlock_type"]:checked').val(); 5 | console.log(radio_val); 6 | $('.points_unlocking').show(); 7 | $('.solve_unlocking').show(); 8 | if(radio_val == 'SOL'){ 9 | $('.points_unlocking').hide(); 10 | } 11 | if(radio_val == 'POT'){ 12 | $('.solve_unlocking').hide(); 13 | } 14 | } 15 | $('input[type=radio][name=unlock_type]').change(function() { 16 | update_hidden(); 17 | }); 18 | update_hidden(); 19 | })(django.jQuery); 20 | }); -------------------------------------------------------------------------------- /huntserver/migrations/0006_auto_20160102_1418.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0005_remove_person_email'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='person', 16 | name='andrewid', 17 | ), 18 | migrations.AddField( 19 | model_name='person', 20 | name='is_andrew_acct', 21 | field=models.BooleanField(default=False), 22 | preserve_default=False, 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /huntserver/migrations/0012_submission_modified_date.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | from django.utils.timezone import utc 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('huntserver', '0011_team_join_code'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='submission', 18 | name='modified_date', 19 | field=models.DateTimeField(default=datetime.datetime(2016, 5, 19, 22, 32, 7, 46283, tzinfo=utc)), 20 | preserve_default=False, 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /puzzlehunt_server/templates/registration/password_reset_email.html: -------------------------------------------------------------------------------- 1 | {% autoescape off %} 2 | 3 | We have received a password reset request for the user: {{ user.get_username }} 4 | 5 | If this is your account, and you have requested this password reset, you can 6 | click the link below to initiate the password reset process for your {{ site_name }} account: 7 | 8 | {{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} 9 | 10 | If clicking the link above doesn't work, please copy and paste the URL in a new browser 11 | window instead. 12 | 13 | If you did not request this password reset, just ignore this email. 14 | 15 | Sincerely, 16 | {{ site_name }} Staff 17 | {% endautoescape %} 18 | -------------------------------------------------------------------------------- /huntserver/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "info_base.html" %} 2 | {% load static %} 3 | 4 | {% block title %}404 Error{% endblock title %} 5 | 6 | {% block left-header %}{% endblock left-header %} 7 | 8 | {% block content %} 9 |
10 |

404 - Page Not Found

11 |
12 |

The page you are looking for either does not exist.

13 |
14 |
15 |

Consider heading back to the main page.

16 |
This is not a puzzle.
17 |
18 | {% endblock content %} 19 | {% block footer %}{% endblock footer %} 20 | -------------------------------------------------------------------------------- /huntserver/migrations/0064_auto_20201020_2151.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-10-21 01:51 2 | 3 | from django.db import migrations, models 4 | import huntserver.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0063_auto_20201012_1143'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='puzzle', 16 | name='link', 17 | ), 18 | migrations.AlterField( 19 | model_name='puzzle', 20 | name='puzzle_file', 21 | field=models.FileField(blank=True, storage=huntserver.models.PuzzleOverwriteStorage(), upload_to=huntserver.models.get_puzzle_file_path), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /huntserver/migrations/0004_auto_20151226_0715.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0003_auto_20151226_0710'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RenameField( 15 | model_name='person', 16 | old_name='login_info', 17 | new_name='user', 18 | ), 19 | migrations.RemoveField( 20 | model_name='person', 21 | name='first_name', 22 | ), 23 | migrations.RemoveField( 24 | model_name='person', 25 | name='last_name', 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /huntserver/migrations/0031_auto_20190110_0902.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.15 on 2019-01-10 14:02 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('huntserver', '0030_prepuzzle_released'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='prepuzzle', 17 | name='template', 18 | field=models.TextField(default='{% extends "prepuzzle.html" %}\r\n{% load prepuzzle_tags %}\r\n\r\n{% block content %}\r\n{% endblock content %}', help_text=b'The template string to be rendered to HTML on the hunt page'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /huntserver/migrations/0041_auto_20200110_1408.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.23 on 2020-01-10 19:08 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('huntserver', '0040_auto_20200108_1126'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='hunt', 17 | name='hint_lockout', 18 | field=models.IntegerField(default=60), 19 | ), 20 | migrations.AddField( 21 | model_name='team', 22 | name='num_available_hints', 23 | field=models.IntegerField(default=0), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /huntserver/migrations/0036_auto_20190310_0900.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.18 on 2019-03-10 13:00 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('huntserver', '0035_hunt_resource_link'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='team', 17 | name='last_received_message', 18 | field=models.IntegerField(default=0), 19 | ), 20 | migrations.AddField( 21 | model_name='team', 22 | name='last_seen_message', 23 | field=models.IntegerField(default=0), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /huntserver/static/huntserver/staff_chat.js: -------------------------------------------------------------------------------- 1 | function map_buttons() { 2 | $('.chatselect').click(function() { 3 | $('.chatselect').each(function() { 4 | $("#chat_" + $(this).data('id')).hide(); 5 | $(this).removeClass('active') 6 | }); 7 | $("#chat_" + $(this).data('id')).show(); 8 | curr_team = $(this).data('id'); 9 | full_name = $(this).data('full_name'); 10 | $("button[data-id=" + curr_team + "]").addClass('active') 11 | $("button[data-id=" + curr_team + "]").css("background-color", ""); 12 | $("#team_label").html("Chatting with: " + full_name); 13 | $("#chatcontainer").scrollTop($("#chatcontainer")[0].scrollHeight); 14 | }); 15 | } 16 | $(document).ready(function() { 17 | map_buttons(); 18 | }); 19 | -------------------------------------------------------------------------------- /huntserver/migrations/0032_auto_20190110_1518.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.15 on 2019-01-10 20:18 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('huntserver', '0031_auto_20190110_0902'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='prepuzzle', 18 | name='hunt', 19 | field=models.OneToOneField(blank=True, help_text=b'The hunt that this puzzle is a part of, leave blank for no associated hunt.', null=True, on_delete=django.db.models.deletion.CASCADE, to='huntserver.Hunt'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /docker/configs/puzzlehunt_apache.conf: -------------------------------------------------------------------------------- 1 | # An apache configuration file meant to be paired with puzzlehunt_setup.sh 2 | # Meant for quick developement. Does not do shibboleth or ssl. 3 | 4 | 5 | 6 | ServerAdmin webmaster@localhost 7 | DocumentRoot /var/www/html 8 | 9 | Alias /static /static 10 | 11 | Require all granted 12 | 13 | 14 | Alias /media /media 15 | XSendFile On 16 | XSendFilePath /media 17 | 18 | Require all granted 19 | 20 | 21 | Require all denied 22 | 23 | 24 | ProxyPass /static/ ! 25 | ProxyPass /media/ ! 26 | 27 | ProxyPass / http://app:8000/ 28 | 29 | -------------------------------------------------------------------------------- /huntserver/migrations/0048_auto_20200215_1743.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-02-15 22:43 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0047_auto_20200210_2201'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='hint', 15 | name='request', 16 | field=models.TextField(help_text='The text of the request for the hint', max_length=1000), 17 | ), 18 | migrations.AlterField( 19 | model_name='hint', 20 | name='response', 21 | field=models.TextField(blank=True, help_text='The text of the response to the hint request', max_length=1000), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /huntserver/migrations/0047_auto_20200210_2201.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-02-11 03:01 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0046_auto_20200209_1112'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='puzzle', 15 | name='points_cost', 16 | field=models.IntegerField(default=0, help_text='The number of points needed to unlock this puzzle.'), 17 | ), 18 | migrations.AlterField( 19 | model_name='puzzle', 20 | name='points_value', 21 | field=models.IntegerField(default=0, help_text='The number of points this puzzle grants upon solving.'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /huntserver/migrations/0055_flatpageproxyobject.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-03-19 23:30 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('flatpages', '0001_initial'), 10 | ('huntserver', '0054_auto_20200318_2145'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='FlatPageProxyObject', 16 | fields=[ 17 | ], 18 | options={ 19 | 'verbose_name': 'info page', 20 | 'verbose_name_plural': 'info pages', 21 | 'proxy': True, 22 | 'indexes': [], 23 | 'constraints': [], 24 | }, 25 | bases=('flatpages.flatpage',), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /huntserver/migrations/0013_response.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0012_submission_modified_date'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Response', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 18 | ('regex', models.CharField(max_length=400)), 19 | ('text', models.CharField(max_length=400)), 20 | ('puzzle', models.ForeignKey(to='huntserver.Puzzle', on_delete=models.CASCADE)), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /huntserver/migrations/0057_auto_20200424_1159.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-04-24 15:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0056_auto_20200418_1151'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='team', 15 | name='last_received_message', 16 | ), 17 | migrations.RemoveField( 18 | model_name='team', 19 | name='last_seen_message', 20 | ), 21 | migrations.AddField( 22 | model_name='team', 23 | name='num_waiting_messages', 24 | field=models.IntegerField(default=0, help_text='The number of unseen messages a team has waiting'), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /huntserver/migrations/0039_auto_20200108_1124.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.23 on 2020-01-08 16:24 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('huntserver', '0038_hint'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='hint', 17 | name='response', 18 | field=models.CharField(blank=True, help_text=b'The text of the response to the hint request', max_length=400), 19 | ), 20 | migrations.AlterField( 21 | model_name='hint', 22 | name='response_time', 23 | field=models.DateTimeField(help_text=b'Hint request time', null=True), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /huntserver/static/huntserver/chat.css: -------------------------------------------------------------------------------- 1 | #chatcontainer { 2 | border: 1px solid black; 3 | height: 600px; 4 | padding: 15px; 5 | background-color: lightgrey; 6 | border-radius:3px; 7 | overflow: auto; 8 | } 9 | 10 | .chatselect { 11 | background-color: white; 12 | width:45%; 13 | } 14 | 15 | .active.chatselect { 16 | background-color: DarkTurquoise; 17 | } 18 | 19 | .admin-message { 20 | color:red; 21 | } 22 | 23 | .chatwindow { 24 | display:none; 25 | overflow-y: auto; 26 | padding:10px; 27 | font-family: Lucida Console; 28 | } 29 | 30 | #messagebox { 31 | border-radius:3px; 32 | height: 25px; 33 | width: 70%; 34 | margin-right: 10px; 35 | } 36 | 37 | #sendbutton { 38 | height: 25px; 39 | } 40 | 41 | .message_text { 42 | float: left; 43 | clear: both; 44 | } 45 | 46 | .message_time { 47 | float: right; 48 | } -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | DB_NAME=puzzlehunt_db 2 | DB_PASSWORD=my_password 3 | DB_USER=user 4 | DJANGO_SECRET_KEY=mysecretkey 5 | DOMAIN=your.domain-here.com 6 | SITE_TITLE="Puzzlehunt CMU" 7 | CONTACT_EMAIL=sample_email@example.com 8 | PROJECT_NAME=puzzlehunt_site 9 | # PUZZLEHUNT_CHAT_ENABLED=True 10 | 11 | # DJANGO_EMAIL_HOST=email_host 12 | # DJANGO_EMAIL_PORT=email_host_port 13 | # DJANGO_EMAIL_USER=email_user 14 | # DJANGO_EMAIL_PASSWORD=email_password 15 | # DJANGO_EMAIL_FROM=email_to_send_as 16 | 17 | DJANGO_ENABLE_DEBUG=False 18 | # DJANGO_USE_SHIBBOLETH=True 19 | 20 | # SENTRY_DSN=https://some_long_hex_string@sentry.io/some_number 21 | 22 | COMPOSE_FILE=docker-compose.yml:docker/local_override.yml 23 | # COMPOSE_FILE=docker-compose.yml:docker/shib_override.yml:docker/proxy_override.yml 24 | # COMPOSE_FILE=docker-compose.yml:docker/shib_override.yml:docker/local_override.yml -------------------------------------------------------------------------------- /huntserver/migrations/0053_auto_20200310_1503.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-03-10 19:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0052_remove_puzzle_num_pages'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='team', 15 | name='playtest_end_date', 16 | field=models.DateTimeField(blank=True, help_text='The date/time at which a hunt will no longer be available to playtesters', null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='team', 20 | name='playtest_start_date', 21 | field=models.DateTimeField(blank=True, help_text='The date/time at which a hunt will become to the playtesters', null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /huntserver/migrations/0049_auto_20200215_1930.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-02-16 00:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0048_auto_20200215_1743'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='team', 15 | name='playtest_end_date', 16 | field=models.DateTimeField(blank=True, help_text='The date/time at which a hunt will be archived and available to the public', null=True), 17 | ), 18 | migrations.AddField( 19 | model_name='team', 20 | name='playtest_start_date', 21 | field=models.DateTimeField(blank=True, help_text='The date/time at which a hunt will become visible to registered users', null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /huntserver/templates/prepuzzle.html: -------------------------------------------------------------------------------- 1 | {% extends "info_base.html" %} 2 | {% load prepuzzle_tags %} 3 | {% load static %} 4 | 5 | {% block title %}{{ puzzle.puzzle_name }}{% endblock title %} 6 | 7 | {% block base_includes %} 8 | 9 | 10 | 27 | {% endblock base_includes %} -------------------------------------------------------------------------------- /huntserver/migrations/0018_auto_20171008_1552.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0017_auto_20170707_1000'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='HuntAssetFile', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 18 | ('file', models.FileField(upload_to=b'hunt/assets/')), 19 | ], 20 | ), 21 | migrations.AddField( 22 | model_name='hunt', 23 | name='template', 24 | field=models.TextField(default=''), 25 | preserve_default=False, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /huntserver/migrations/0075_auto_20210208_0939.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2021-02-08 14:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0074_auto_20210207_2304'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='puzzle', 15 | name='doesnt_count', 16 | ), 17 | migrations.RemoveField( 18 | model_name='puzzle', 19 | name='is_meta', 20 | ), 21 | migrations.AlterField( 22 | model_name='puzzle', 23 | name='puzzle_type', 24 | field=models.CharField(choices=[('STD', 'Standard'), ('MET', 'Meta'), ('FIN', 'Final'), ('NON', 'Non-puzzle')], default='STD', help_text='The type of puzzle.', max_length=3), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /huntserver/widgets.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class HtmlEditor(forms.Textarea): 5 | def __init__(self, *args, **kwargs): 6 | super(HtmlEditor, self).__init__(*args, **kwargs) 7 | self.attrs['class'] = 'html-editor' 8 | 9 | class Media: 10 | css = { 11 | 'all': ( 12 | 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.9.0/codemirror.css', 13 | ) 14 | } 15 | js = ( 16 | 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.9.0/codemirror.js', 17 | 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.9.0/mode/xml/xml.js', 18 | 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.9.0/mode/htmlmixed/htmlmixed.js', 19 | 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.9.0/mode/css/css.js', 20 | '/static/codemirror_html.js' 21 | ) 22 | -------------------------------------------------------------------------------- /huntserver/migrations/0074_auto_20210207_2304.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2021-02-08 04:04 2 | 3 | from django.db import migrations 4 | 5 | 6 | def set_puzzle_type(apps, schema_editor): 7 | Puzzle = apps.get_model('huntserver', 'Puzzle') 8 | for puzzle in Puzzle.objects.all().iterator(): 9 | if(puzzle.is_meta): 10 | puzzle.puzzle_type = "MET" 11 | elif(puzzle.doesnt_count): 12 | puzzle.puzzle_type = "NON" 13 | else: 14 | puzzle.puzzle_type = "STD" 15 | puzzle.save() 16 | 17 | 18 | def reverse_func(apps, schema_editor): 19 | pass # code for reverting migration, if any 20 | 21 | 22 | class Migration(migrations.Migration): 23 | 24 | dependencies = [ 25 | ('huntserver', '0073_auto_20210207_2300'), 26 | ] 27 | 28 | operations = [ 29 | migrations.RunPython(set_puzzle_type, reverse_func) 30 | ] 31 | -------------------------------------------------------------------------------- /huntserver/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "info_base.html" %} 2 | {% load crispy_forms_tags %} 3 | {% load i18n %} 4 | 5 | {% block content %} 6 |
7 |
8 |

Please enter your Puzzle Hunt CMU username and password:

9 |

10 | {% csrf_token %} 11 | {{ form|crispy }} 12 | 13 | 14 | 15 |
16 |
17 |

18 | Don't have an account? You can create an account here. 19 |
20 |
21 | Forgot your password? You can reset your password here. 22 |

23 |
24 |
25 | {% endblock %} -------------------------------------------------------------------------------- /huntserver/migrations/0033_auto_20190117_2118.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.15 on 2019-01-18 02:18 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('huntserver', '0032_auto_20190110_1518'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='hunt', 17 | name='extra_data', 18 | field=models.CharField(blank=True, help_text=b'A misc. field for any extra data to be stored with the hunt.', max_length=200), 19 | ), 20 | migrations.AddField( 21 | model_name='puzzle', 22 | name='extra_data', 23 | field=models.CharField(blank=True, help_text=b'A misc. field for any extra data to be stored with the puzzle.', max_length=200), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /huntserver/migrations/0072_auto_20201103_1539.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-11-03 20:39 2 | 3 | from django.db import migrations, models 4 | import huntserver.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0071_auto_20201021_2143'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='puzzle', 16 | name='solution_is_webpage', 17 | field=models.BooleanField(default=False, help_text='Is this solution an html webpage?'), 18 | ), 19 | migrations.AddField( 20 | model_name='puzzle', 21 | name='solution_resource_file', 22 | field=models.FileField(blank=True, help_text='Puzzle solution resources, MUST BE A ZIP FILE.', storage=huntserver.models.PuzzleOverwriteStorage(), upload_to=huntserver.models.get_solution_file_path), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /huntserver/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "info_base.html" %} 2 | {% load hunt_tags %} 3 | {% load static %} 4 | 5 | {% block title %}500 Error{% endblock title %} 6 | 7 | {% block left-header %}{% endblock left-header %} 8 | 9 | {% block content %} 10 |
11 |

500 - Internal Server Error

12 |
13 |

If you're seeing this page, something has gone wrong on our server.

14 |
If there is a puzzlehunt occuring right now, consider sending an email to the puzzlehunt staff.
15 |
16 |
17 |

Consider heading back to the main page.

18 |
This is not a puzzle.
19 |
20 | {% endblock content %} 21 | {% block footer %}{% endblock footer %} 22 | -------------------------------------------------------------------------------- /huntserver/migrations/0063_auto_20201012_1143.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-10-12 15:43 2 | 3 | from django.db import migrations 4 | from django.conf import settings 5 | from os import path 6 | 7 | 8 | def set_file(apps, schema_editor): 9 | puzzle_folder = "puzzles/" 10 | 11 | Puzzle = apps.get_model('huntserver', 'Puzzle') 12 | for puzzle in Puzzle.objects.all().iterator(): 13 | puzzle_path = puzzle_folder + puzzle.puzzle_id + ".pdf" 14 | if(puzzle.link and path.exists(settings.MEDIA_ROOT + puzzle_path)): 15 | puzzle.puzzle_file = puzzle_path 16 | puzzle.save() 17 | 18 | 19 | def reverse_func(apps, schema_editor): 20 | pass # code for reverting migration, if any 21 | 22 | 23 | class Migration(migrations.Migration): 24 | 25 | dependencies = [ 26 | ('huntserver', '0062_auto_20201012_1018'), 27 | ] 28 | 29 | operations = [ 30 | migrations.RunPython(set_file, reverse_func) 31 | ] 32 | -------------------------------------------------------------------------------- /huntserver/migrations/0029_auto_20190108_1429.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.15 on 2019-01-08 19:29 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('huntserver', '0028_prepuzzle'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='prepuzzle', 18 | name='response_string', 19 | field=models.TextField(default=b'', help_text=b'Data returned to the webpage for use upon solving.'), 20 | ), 21 | migrations.AlterField( 22 | model_name='prepuzzle', 23 | name='hunt', 24 | field=models.OneToOneField(blank=True, help_text=b'The hunt that this puzzle is a part of', null=True, on_delete=django.db.models.deletion.CASCADE, to='huntserver.Hunt'), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /puzzlehunt_server/settings/travis_settings.py: -------------------------------------------------------------------------------- 1 | from .base_settings import * 2 | import os 3 | 4 | # SECURITY WARNING: don't run with debug turned on in production! 5 | DEBUG = False 6 | 7 | SECRET_KEY = '$1B&VUf$OdUEfMJXd40qdakA36@%2NE_41Dz9tFs6l=z4v_3P-' 8 | DATABASES = { 9 | 'default': { 10 | 'ENGINE': 'django.db.backends.postgresql', 11 | 'NAME': 'puzzlehunt_db', 12 | 'HOST': '127.0.0.1', 13 | 'USER': 'root', 14 | 'PASSWORD': '', 15 | } 16 | } 17 | INTERNAL_IPS = '' 18 | EMAIL_HOST_USER = '' 19 | EMAIL_HOST_PASSWORD = '' 20 | ALLOWED_HOSTS = ['*'] 21 | 22 | LOGGING = { 23 | 'version': 1, 24 | 'disable_existing_loggers': False, 25 | 'handlers': { 26 | 'console': { 27 | 'class': 'logging.StreamHandler', 28 | }, 29 | }, 30 | 'loggers': { 31 | 'django': { 32 | 'handlers': ['console'], 33 | 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'), 34 | }, 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /huntserver/templates/staff_base.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | 3 | {% block title %} Puzzlehunt Admin {% endblock title %} 4 | 5 | {% block extrahead %} 6 | 7 | 8 | 9 | {% block includes %} {% endblock %} 10 | {% endblock %} 11 | 12 | {% block js %} 13 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /huntserver/migrations/0023_userproxyobject.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.15 on 2018-09-09 17:14 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.auth.models 6 | from django.db import migrations 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('auth', '0008_alter_user_username_max_length'), 13 | ('huntserver', '0022_switch_to_utf8mb4_columns'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='UserProxyObject', 19 | fields=[ 20 | ], 21 | options={ 22 | 'verbose_name': 'user', 23 | 'proxy': True, 24 | 'verbose_name_plural': 'users', 25 | 'indexes': [], 26 | }, 27 | bases=('auth.user',), 28 | managers=[ 29 | ('objects', django.contrib.auth.models.UserManager()), 30 | ], 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | services: 4 | - docker 5 | 6 | sudo: required 7 | 8 | env: 9 | global: 10 | - DB_NAME=puzzlehunt_db 11 | - DB_USER=hunt 12 | - DB_PASSWORD=test 13 | - DJANGO_SECRET_KEY=test_secret_key 14 | - DOCKER_COMPOSE_VERSION=1.23.2 15 | 16 | before_install: 17 | - sudo rm /usr/local/bin/docker-compose 18 | - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose 19 | - chmod +x docker-compose 20 | - sudo mv docker-compose /usr/local/bin 21 | 22 | install: 23 | - pip install coveralls flake8 24 | - docker-compose build 25 | - docker-compose up -d 26 | 27 | script: 28 | - docker-compose exec app coverage run 29 | - docker-compose exec app coverage report 30 | - flake8 huntserver 31 | 32 | after_script: 33 | - docker-compose stop 34 | - docker-compose rm -f 35 | 36 | after_success: 37 | - cp .coverage .coverage.extra 38 | - coverage combine 39 | - coveralls -------------------------------------------------------------------------------- /huntserver/migrations/0068_auto_20201021_1932.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-10-21 23:32 2 | 3 | from django.db import migrations, models 4 | import huntserver.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0067_auto_20201020_2211'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='hunt', 16 | name='resource_file', 17 | field=models.FileField(blank=True, help_text='Hunt resources, MUST BE A ZIP FILE.', storage=huntserver.models.PuzzleOverwriteStorage(), upload_to=huntserver.models.get_prepuzzle_file_path), 18 | ), 19 | migrations.AddField( 20 | model_name='prepuzzle', 21 | name='resource_file', 22 | field=models.FileField(blank=True, help_text='Prepuzzle resources, MUST BE A ZIP FILE.', storage=huntserver.models.PuzzleOverwriteStorage(), upload_to=huntserver.models.get_prepuzzle_file_path), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /huntserver/templates/prepuzzle_answerbox.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 |
8 |   9 |
10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /huntserver/migrations/0073_auto_20210207_2300.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2021-02-08 04:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0072_auto_20201103_1539'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='puzzle', 15 | name='puzzle_type', 16 | field=models.CharField(choices=[('STD', 'A standard puzzle'), ('MET', 'A meta puzzle'), ('NON', 'An unscored puzzle')], default='STD', help_text='The type of puzzle.', max_length=3), 17 | ), 18 | migrations.AlterField( 19 | model_name='puzzle', 20 | name='puzzle_page_type', 21 | field=models.CharField(choices=[('PDF', 'Puzzle page displays a PDF'), ('LNK', 'Puzzle page links a webpage'), ('WEB', 'Puzzle page displays a webpage'), ('EMB', 'Puzzle is html embedded in the webpage')], default='WEB', help_text='The type of webpage for this puzzle.', max_length=3), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Puzzlehunt_server documentation master file, created by 2 | sphinx-quickstart on Wed Aug 19 08:57:21 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Puzzle Hunt CMU's Server documentation! 7 | ================================================ 8 | 9 | If you're here as a user, start with the "How to Create a Hunt" and "How to Run 10 | a Hunt" sections. If you are here as a developer, start with the "Setup" and 11 | "Basics" sections. 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | :numbered: 16 | 17 | hunt_creation 18 | hunt_running 19 | setup 20 | basics 21 | models 22 | views 23 | 24 | :doc:`Changelog Here ` 25 | 26 | Contribute 27 | ---------- 28 | 29 | Source Code: http://www.github.com/dlareau/puzzlehunt_server 30 | 31 | Issue Tracker: http://www.github.com/dlareau/puzzlehunt_server/issues 32 | 33 | If you are having issues, please let us know. 34 | Email dlareau@cmu.edu 35 | 36 | The project is licensed under the MIT license. -------------------------------------------------------------------------------- /docker/configs/puzzlehunt_apache_shib.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName https://REPLACE_DOMAIN_STR 3 | 4 | ServerAdmin webmaster@localhost 5 | DocumentRoot /var/www/html 6 | 7 | Alias /static /static 8 | 9 | Require all granted 10 | 11 | 12 | Alias /media /media 13 | Alias /media/puzzles /media/puzzles 14 | XSendFile On 15 | XSendFilePath /media 16 | 17 | Require all granted 18 | 19 | 20 | Require all denied 21 | 22 | 23 | 24 | AuthType Shibboleth 25 | ShibUseHeaders On 26 | ShibRequireSession On 27 | ShibApplicationId default 28 | ShibExportAssertion On 29 | require valid-user 30 | 31 | 32 | 33 | Options +Indexes 34 | SetHandler shib 35 | 36 | 37 | ProxyPass /static/ ! 38 | ProxyPass /media/ ! 39 | ProxyPass /Shibboleth.sso/ ! 40 | 41 | ProxyPass / http://app:8000/ 42 | ProxyPassReverse / http://app:8000/ 43 | ProxyPreserveHost On 44 | -------------------------------------------------------------------------------- /puzzlehunt_server/settings/env_settings.py: -------------------------------------------------------------------------------- 1 | from .base_settings import * 2 | import dj_database_url 3 | import os 4 | 5 | DEBUG = os.getenv("DJANGO_ENABLE_DEBUG", default="False").lower() == "true" 6 | SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY") 7 | DATABASES = {'default': dj_database_url.config(conn_max_age=600)} 8 | 9 | if(DATABASES['default']['ENGINE'] == 'django.db.backends.mysql'): 10 | DATABASES['default']['OPTIONS'] = {'charset': 'utf8mb4'} 11 | 12 | INTERNAL_IPS = ['127.0.0.1', 'localhost'] 13 | EMAIL_HOST = os.environ.get("DJANGO_EMAIL_HOST") 14 | EMAIL_PORT = int(os.environ.get("DJANGO_EMAIL_PORT", default="587")) 15 | EMAIL_HOST_USER = os.environ.get("DJANGO_EMAIL_USER") 16 | EMAIL_HOST_PASSWORD = os.environ.get("DJANGO_EMAIL_PASSWORD") 17 | EMAIL_FROM = os.environ.get("DJANGO_EMAIL_FROM") 18 | DEFAULT_FROM_EMAIL = EMAIL_FROM 19 | SERVER_EMAIL = EMAIL_FROM 20 | DOMAIN = os.getenv("DOMAIN", default="default.com") 21 | CHAT_ENABLED = os.getenv("PUZZLEHUNT_CHAT_ENABLED", default="True").lower() == "true" 22 | 23 | if "SITE_TITLE" in os.environ: 24 | SITE_TITLE = os.getenv("SITE_TITLE") 25 | 26 | ALLOWED_HOSTS = ['*'] 27 | -------------------------------------------------------------------------------- /huntserver/static/huntserver/chat_poll.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | function is_visible(){ 3 | var stateKey, keys = { 4 | hidden: "visibilitychange", 5 | webkitHidden: "webkitvisibilitychange", 6 | mozHidden: "mozvisibilitychange", 7 | msHidden: "msvisibilitychange" 8 | }; 9 | for (stateKey in keys) { 10 | if (stateKey in document) { 11 | return !document[stateKey]; 12 | } 13 | } 14 | return true; 15 | } 16 | 17 | var get_posts = function() { 18 | if(is_visible()){ 19 | $.getJSON("/chat/status/") 20 | .done(function(result){ 21 | num_messages = result['num_messages'] 22 | if(num_messages > 0) { 23 | $("#num_messages").css("background-color", "indianred"); 24 | } else { 25 | $("#num_messages").css("background-color", ""); 26 | } 27 | $("#num_messages").text(num_messages); 28 | }) 29 | .fail( function(xhr, textStatus, errorThrown) { 30 | console.log(xhr); 31 | }); 32 | } 33 | } 34 | 35 | setInterval(get_posts, 120000); //Two minutes 36 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dillon Lareau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /huntserver/templates/shib_register.html: -------------------------------------------------------------------------------- 1 | {% extends "info_base.html" %} 2 | 3 | {% block content %} 4 |
5 |

Register for Puzzle Hunt CMU

6 | {% if user_form.subject.errors %} 7 |
    8 | {% for error in user_form.subject.errors %} 9 |
  1. {{ error|escape }}
  2. 10 | {% endfor %} 11 |
12 | {% endif %} 13 | {% if person_form.subject.errors %} 14 |
    15 | {% for error in person_form.subject.errors %} 16 |
  1. {{ error|escape }}
  2. 17 | {% endfor %} 18 |
19 | {% endif %} 20 |
21 | {% csrf_token %} 22 |
23 |

By continuing, you agree to release the following attributes to Puzzle Hunt CMU.

24 |

None of the information collected here will be displayed publicly.

25 | {{ user_form.as_p }} 26 | {{ person_form.as_p }} 27 |
28 | 29 |
30 | 31 |
32 |
33 |
34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /huntserver/migrations/0027_auto_20181023_2303.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.15 on 2018-10-24 03:03 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('huntserver', '0026_auto_20181021_1548'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='puzzle', 17 | name='is_html_puzzle', 18 | field=models.BooleanField(default=False, help_text=b"Does this puzzle use an HTML folder as it's source?"), 19 | ), 20 | migrations.AddField( 21 | model_name='puzzle', 22 | name='resource_link', 23 | field=models.URLField(blank=True, help_text=b'The full link (needs http://) to a folder of additional resources.'), 24 | ), 25 | migrations.AlterField( 26 | model_name='puzzle', 27 | name='link', 28 | field=models.URLField(blank=True, help_text=b'The full link (needs http://) to a publicly accessible PDF of the puzzle'), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /huntserver/migrations/0050_auto_20200305_1208.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-03-05 17:08 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0049_auto_20200215_1930'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='team', 15 | name='last_received_message', 16 | field=models.IntegerField(default=0, help_text='The PK of the last message the team has received'), 17 | ), 18 | migrations.AlterField( 19 | model_name='team', 20 | name='last_seen_message', 21 | field=models.IntegerField(default=0, help_text='The PK of the last message the team has seen'), 22 | ), 23 | migrations.AddIndex( 24 | model_name='hunt', 25 | index=models.Index(fields=['hunt_number'], name='huntserver__hunt_nu_0fecf6_idx'), 26 | ), 27 | migrations.AddIndex( 28 | model_name='puzzle', 29 | index=models.Index(fields=['puzzle_id'], name='huntserver__puzzle__bc16c3_idx'), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /huntserver/templates/chat.html: -------------------------------------------------------------------------------- 1 | {% extends "hunt_base.html" %} 2 | {% block title %} Chat with Staff{% endblock title %} 3 | 4 | {% block includes %} 5 | 6 | 13 | 14 | 15 | {% endblock includes %} 16 | 17 | {% block content %} 18 |
19 |
20 |
21 |

Chat with staff

22 |
23 | {% for team, dict in message_dict.items %} 24 |
25 | {{dict.messages}} 26 |
27 | {% endfor %} 28 |
29 |
30 | 31 | 32 |
33 |
34 |
35 |
36 | {% endblock content %} 37 | -------------------------------------------------------------------------------- /huntserver/templates/charts.html: -------------------------------------------------------------------------------- 1 | {% extends "staff_base.html" %} 2 | {% block title %}Charts!{% endblock title %} 3 | 4 | {% block includes %} 5 | 6 | 7 | {% endblock includes %} 8 | 9 | {% block content %} 10 |

Charts!

11 |

These charts will not automatically refresh

12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |

Puzzle stats

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% for row in chart_rows %} 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% endfor %} 36 |
PuzzleFirst TeamFirst TimeNum Soves
{{ row.1 }}{{ row.2 }}{{ row.3|time:"h:i a" }}{{ row.4 }}
37 |
38 | 39 | {% endblock content %} 40 | -------------------------------------------------------------------------------- /huntserver/migrations/0066_auto_20201020_2200.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-10-21 02:00 2 | 3 | 4 | from django.db import migrations 5 | from django.conf import settings 6 | from os import path 7 | 8 | 9 | def set_files(apps, schema_editor): 10 | puzzle_folder = "puzzles/" 11 | solution_folder = "solutions/" 12 | 13 | Puzzle = apps.get_model('huntserver', 'Puzzle') 14 | for puzzle in Puzzle.objects.all().iterator(): 15 | resource_path = puzzle_folder + puzzle.puzzle_id + ".zip" 16 | if(puzzle.resource_link and path.exists(settings.MEDIA_ROOT + resource_path)): 17 | puzzle.resource_file = resource_path 18 | puzzle.save() 19 | 20 | solution_path = solution_folder + puzzle.puzzle_id + "_sol.pdf" 21 | if(puzzle.solution_link and path.exists(settings.MEDIA_ROOT + solution_path)): 22 | puzzle.solution_file = solution_path 23 | puzzle.save() 24 | 25 | 26 | def reverse_func(apps, schema_editor): 27 | pass # code for reverting migration, if any 28 | 29 | 30 | class Migration(migrations.Migration): 31 | 32 | dependencies = [ 33 | ('huntserver', '0065_auto_20201020_2200'), 34 | ] 35 | 36 | operations = [ 37 | migrations.RunPython(set_files, reverse_func) 38 | ] 39 | -------------------------------------------------------------------------------- /huntserver/static/huntserver/info_base.css: -------------------------------------------------------------------------------- 1 | /* Generic */ 2 | .container { font-family: Helvetica, Verdana, Arial, sans-serif; } 3 | 4 | body { 5 | background: url(/static/huntserver/background1.jpg); 6 | background-size: 2000px 2000px; 7 | } 8 | 9 | .container:not(.no_outline){ 10 | background: white; 11 | padding: 20px 40px; 12 | margin: 10px auto; 13 | border: 5px solid black; 14 | border-radius: 10px; 15 | } 16 | .container.no_outline { 17 | padding-left: 0px; 18 | padding-right: 0px; 19 | } 20 | 21 | #phlogo { 22 | width: 50%; 23 | } 24 | 25 | 26 | /* Registration/forms */ 27 | .title { 28 | text-align: center; 29 | } 30 | 31 | h1.title { 32 | font-size: 60px; 33 | } 34 | 35 | .field_errors { 36 | margin-bottom: 10px; 37 | } 38 | 39 | .helptext { 40 | font-size: 10px; 41 | color: gray; 42 | } 43 | 44 | input[type="text"] { 45 | width:300px; 46 | } 47 | 48 | input[type="email"] { 49 | width:300px; 50 | } 51 | 52 | input[type="password"] { 53 | width:300px; 54 | } 55 | 56 | #room_select { 57 | width: auto; 58 | } 59 | 60 | input[readonly] { 61 | background: #dddddd; 62 | opacity: 0.8; 63 | } 64 | 65 | 66 | /* Hunt info */ 67 | #qa h2 { 68 | font-size: 16px; 69 | border-top: 2px dashed #ccc; 70 | padding-top: 10px; 71 | } -------------------------------------------------------------------------------- /huntserver/migrations/0065_auto_20201020_2200.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-10-21 02:00 2 | 3 | from django.db import migrations, models 4 | import huntserver.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0064_auto_20201020_2151'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='puzzle', 16 | name='resource_file', 17 | field=models.FileField(blank=True, help_text='Puzzle resources, MUST BE A ZIP FILE.', storage=huntserver.models.PuzzleOverwriteStorage(), upload_to=huntserver.models.get_puzzle_file_path), 18 | ), 19 | migrations.AddField( 20 | model_name='puzzle', 21 | name='solution_file', 22 | field=models.FileField(blank=True, help_text='Puzzle solution. MUST BE A PDF.', storage=huntserver.models.PuzzleOverwriteStorage(), upload_to=huntserver.models.get_solution_file_path), 23 | ), 24 | migrations.AlterField( 25 | model_name='puzzle', 26 | name='puzzle_file', 27 | field=models.FileField(blank=True, help_text='Puzzle file. MUST BE A PDF', storage=huntserver.models.PuzzleOverwriteStorage(), upload_to=huntserver.models.get_puzzle_file_path), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /huntserver/templates/unlockables.html: -------------------------------------------------------------------------------- 1 | {% extends "hunt_base.html" %} 2 | {% load hunt_tags %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |

Unlocked Objects

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% for unlockable in unlockables %} 18 | 19 | 22 | 29 | {% endfor %} 30 | 31 |
Puzzle NameObject unlocked
20 |

P{{unlockable.puzzle.puzzle_number}} - {{ unlockable.puzzle.puzzle_name }}

21 |
23 | {% if unlockable.content_type == "TXT" %} 24 | {{unlockable.content}} 25 | {% elif unlockable.content_type == "WEB" %} 26 | {{unlockable.content}} 27 | {% endif %} 28 |
32 |
33 |

For assistance, email {% contact_email %}.

34 |
35 |
36 | {% endblock content %} 37 | -------------------------------------------------------------------------------- /huntserver/migrations/0069_auto_20201021_1932.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-10-21 23:32 2 | 3 | from django.db import migrations 4 | from django.conf import settings 5 | from os import path 6 | 7 | 8 | def set_files(apps, schema_editor): 9 | hunt_folder = "hunt/" 10 | prepuzzle_folder = "prepuzzles/" 11 | 12 | Hunt = apps.get_model('huntserver', 'Hunt') 13 | for hunt in Hunt.objects.all().iterator(): 14 | resource_path = hunt_folder + str(hunt.hunt_number) + ".zip" 15 | if(hunt.resource_link and path.exists(settings.MEDIA_ROOT + resource_path)): 16 | hunt.resource_file = resource_path 17 | hunt.save() 18 | 19 | Prepuzzle = apps.get_model('huntserver', 'Prepuzzle') 20 | for puzzle in Prepuzzle.objects.all().iterator(): 21 | resource_path = prepuzzle_folder + str(puzzle.pk) + ".zip" 22 | if(puzzle.resource_link and path.exists(settings.MEDIA_ROOT + resource_path)): 23 | puzzle.resource_file = resource_path 24 | puzzle.save() 25 | 26 | 27 | def reverse_func(apps, schema_editor): 28 | pass # code for reverting migration, if any 29 | 30 | 31 | class Migration(migrations.Migration): 32 | 33 | dependencies = [ 34 | ('huntserver', '0068_auto_20201021_1932'), 35 | ] 36 | 37 | operations = [ 38 | migrations.RunPython(set_files, reverse_func) 39 | ] 40 | -------------------------------------------------------------------------------- /huntserver/migrations/0028_prepuzzle.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.15 on 2019-01-04 19:50 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('huntserver', '0027_auto_20181023_2303'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Prepuzzle', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('puzzle_name', models.CharField(help_text=b'The name of the puzzle as it will be seen by hunt participants', max_length=200)), 21 | ('answer', models.CharField(help_text=b'The answer to the puzzle, not case sensitive', max_length=100)), 22 | ('template', models.TextField(default=b'', help_text=b'The template string to be rendered to HTML on the hunt page')), 23 | ('resource_link', models.URLField(blank=True, help_text=b'The full link (needs http://) to a folder of additional resources.')), 24 | ('hunt', models.OneToOneField(help_text=b'The hunt that this puzzle is a part of', on_delete=django.db.models.deletion.CASCADE, to='huntserver.Hunt')), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /huntserver/templates/queue_row.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | {{ submission.team.team_name|truncatechars:40}} 11 | 12 | {{ submission.puzzle.puzzle_name }} 13 | {{ submission.submission_text }} 14 | {{ submission.submission_time|time:"h:i a" }} 15 | 16 | {% if submission.response_text == '' %} 17 | [manual response] 18 | [canned response] 19 | {% else%} 20 | {{submission.response_text}} Fix 21 | {% endif %} 22 | 31 | 32 | -------------------------------------------------------------------------------- /huntserver/templates/access_error.html: -------------------------------------------------------------------------------- 1 | {% extends "info_base.html" %} 2 | {% load hunt_tags %} 3 | {% block title %}Not Available{% endblock title %} 4 | 5 | {% block content %} 6 |
7 |

Not Available

8 | 9 |
10 | {% if reason == "hunt" %} 11 |

12 | The resource or page you are attempting to access is associated with a 13 | puzzlehunt that is not yet started. Please refresh this page after the 14 | hunt has started. 15 |

16 | {% endif %} 17 | 18 | {% if reason == "team" %} 19 |

20 | You must be participating in the current hunt to view this resource or 21 | page, and it does not appear you are on a team for this hunt. Please 22 | ensure you are signed in correctly and that you have properly added 23 | yourself to a team on the 24 | registration page. 25 |

26 | {% endif %} 27 | 28 | {% if reason == "puzzle" %} 29 |

30 | This resource or page is part of an ongoing puzzlehunt and is not yet 31 | unlocked for your team. Please either continue playing to unlock it or 32 | wait until the hunt is public. 33 |

34 | {% endif %} 35 | 36 |
37 |

For assistance, email puzzlehunt staff.

38 |
39 |
40 | {% endblock content %} 41 | {% block footer %}{% endblock footer %} 42 | -------------------------------------------------------------------------------- /huntserver/templates/resources.html: -------------------------------------------------------------------------------- 1 | 3 | 4 |

Puzzle Resources

5 | Here are some resources to help you get started on some puzzles: 6 | 32 | -------------------------------------------------------------------------------- /huntserver/migrations/0045_auto_20200209_0856.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-02-09 13:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0044_auto_20200207_0936'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='puzzle', 15 | name='points_cost', 16 | field=models.IntegerField(default=0, help_text='The number of points needed to unlock this puzzle.'), 17 | preserve_default=False, 18 | ), 19 | migrations.AddField( 20 | model_name='puzzle', 21 | name='points_value', 22 | field=models.IntegerField(default=0, help_text='The number of points this puzzle grants upon solving.'), 23 | preserve_default=False, 24 | ), 25 | migrations.AddField( 26 | model_name='puzzle', 27 | name='unlock_type', 28 | field=models.CharField(choices=[('TIM', 'Points Based Unlock'), ('SOL', 'Solves Based Unlock'), ('ETH', 'Either Unlocking Method'), ('BTH', 'Both Unlocking Methods')], default='SOL', help_text='The type of puzzle unlocking scheme', max_length=3), 29 | ), 30 | migrations.AlterField( 31 | model_name='puzzle', 32 | name='is_meta', 33 | field=models.BooleanField(default=False, help_text='Is this puzzle a meta-puzzle?', verbose_name='Is a metapuzzle'), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /docker/apacheShibDockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:10 2 | 3 | ARG DOMAIN 4 | 5 | # System setup 6 | RUN apt-get update && \ 7 | apt-get install -y apache2 gnupg curl ntp && \ 8 | apt-get install -y certbot python-certbot-apache && \ 9 | curl --fail --remote-name https://pkg.switch.ch/switchaai/debian/dists/buster/main/binary-all/misc/switchaai-apt-source_1.0.0_all.deb && \ 10 | apt-get install -y ./switchaai-apt-source_1.0.0_all.deb && \ 11 | rm ./switchaai-apt-source_1.0.0_all.deb && \ 12 | apt-get update && \ 13 | apt-get install -y --install-recommends shibboleth 14 | 15 | # Shibboleth setup 16 | RUN mkdir /etc/shibboleth/certs 17 | COPY configs/shibboleth2.xml /etc/shibboleth/shibboleth2.xml 18 | COPY configs/inc-md-cert-mdq.pem /etc/shibboleth/inc-md-cert-mdq.pem 19 | 20 | # Server setup 21 | COPY configs/puzzlehunt_apache_shib.conf /etc/apache2/sites-available/puzzlehunt.conf 22 | RUN rm /etc/apache2/sites-enabled/* && \ 23 | sed -i -e "s/REPLACE_DOMAIN_STR/$DOMAIN/g" /etc/apache2/sites-available/puzzlehunt.conf && \ 24 | sed -i -e "s/REPLACE_DOMAIN_STR/$DOMAIN/g" /etc/shibboleth/shibboleth2.xml && \ 25 | apt-get install -y libapache2-mod-xsendfile libapache2-mod-shib && \ 26 | a2enmod proxy proxy_http proxy_html xsendfile shib && \ 27 | a2ensite puzzlehunt && \ 28 | mkdir -p /static && \ 29 | mkdir -p /media 30 | 31 | COPY apacheShibForeground /usr/local/bin/ 32 | RUN chmod +x /usr/local/bin/apacheShibForeground 33 | 34 | EXPOSE 80 35 | 36 | CMD ["/usr/local/bin/apacheShibForeground"] -------------------------------------------------------------------------------- /huntserver/migrations/0038_hint.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.23 on 2020-01-07 19:37 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('huntserver', '0037_auto_20190925_1733'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Hint', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('request', models.CharField(help_text=b'The text of the request for the hint', max_length=400)), 21 | ('request_time', models.DateTimeField(help_text=b'Hint request time')), 22 | ('response', models.CharField(help_text=b'The text of the response to the hint request', max_length=400)), 23 | ('response_time', models.DateTimeField(help_text=b'Hint request time')), 24 | ('last_modified_time', models.DateTimeField(help_text=b'Last time of modification')), 25 | ('puzzle', models.ForeignKey(help_text=b'The puzzle that this hint is related to', on_delete=django.db.models.deletion.CASCADE, to='huntserver.Puzzle')), 26 | ('team', models.ForeignKey(help_text=b'The team that requested the hint', on_delete=django.db.models.deletion.CASCADE, to='huntserver.Team')), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /huntserver/fixtures/initial_hunt.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "username": "hunt", 5 | "first_name": "Root", 6 | "last_name": "User", 7 | "is_active": true, 8 | "is_superuser": true, 9 | "is_staff": true, 10 | "last_login": "2017-12-29T16:25:54Z", 11 | "groups": [], 12 | "user_permissions": [], 13 | "password": "pbkdf2_sha256$20000$GLdG9P4Fdk25$/MUEQESmazezJ8xe6A87Jm5sBl/JIhk3skYQD20Dnn8=", 14 | "email": "example@example.com", 15 | "date_joined": "2017-12-27T18:39:26Z" 16 | }, 17 | "model": "auth.user", 18 | "pk": 1 19 | }, 20 | { 21 | "fields": { 22 | "is_shib_acct": false, 23 | "allergies": "", 24 | "comments": "", 25 | "teams": [], 26 | "phone": "000-000-0000", 27 | "user": 1 28 | }, 29 | "model": "huntserver.person", 30 | "pk": 1 31 | }, 32 | { 33 | "fields": { 34 | "team_size": 5, 35 | "display_end_date": "2000-01-02T05:00:00Z", 36 | "display_start_date": "2000-01-01T05:00:00Z", 37 | "end_date": "2000-01-02T05:00:00Z", 38 | "hunt_name": "Example Hunt 1", 39 | "is_current_hunt": true, 40 | "location": "Example Location 1", 41 | "template": "{% extends \"hunt_base.html\" %}\r\n{% block title %}Puzzles 1{% endblock title %}\r\n\r\n{% block content %}\r\nExample Template 1\r\n{% endblock content %}", 42 | "hunt_number": 1, 43 | "start_date": "2000-01-01T05:00:00Z" 44 | }, 45 | "model": "huntserver.hunt", 46 | "pk": 1 47 | } 48 | ] 49 | -------------------------------------------------------------------------------- /huntserver/migrations/0059_puzzle_puzzle_page_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-10-12 01:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def set_defaults(apps, schema_editor): 7 | Puzzle = apps.get_model('huntserver', 'Puzzle') 8 | for puzzle in Puzzle.objects.all().iterator(): 9 | if(puzzle.is_html_puzzle): 10 | puzzle.puzzle_page_type = 'LNK' 11 | else: 12 | puzzle.puzzle_page_type = 'PDF' 13 | puzzle.save() 14 | 15 | 16 | def reverse_func(apps, schema_editor): 17 | pass # code for reverting migration, if any 18 | 19 | 20 | class Migration(migrations.Migration): 21 | 22 | dependencies = [ 23 | ('huntserver', '0058_auto_20200507_2138'), 24 | ] 25 | 26 | operations = [ 27 | migrations.AddField( 28 | model_name='puzzle', 29 | name='puzzle_page_type', 30 | field=models.CharField(choices=[('PDF', 'Puzzle page displays a PDF'), ('LNK', 'Puzzle page links a webpage'), ('WEB', 'Puzzle page displays a webpage')], null=True, help_text='The type of webpage for this puzzle.', max_length=3), 31 | ), 32 | migrations.RunPython(set_defaults, reverse_func), 33 | migrations.AlterField( 34 | model_name='puzzle', 35 | name='puzzle_page_type', 36 | field=models.CharField(choices=[('PDF', 'Puzzle page displays a PDF'), ('LNK', 'Puzzle page links a webpage'), ('WEB', 'Puzzle page displays a webpage')], default='WEB', help_text='The type of webpage for this puzzle.', max_length=3), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /config/puzzlehunt_apache_ssl.conf: -------------------------------------------------------------------------------- 1 | # An apache configuration file meant to be paired with puzzlehunt_setup.sh 2 | # Meant for quick developement. Does not do shibboleth or ssl. 3 | 4 | Define hostname replacename 5 | 6 | 7 | ServerName ${hostname} 8 | 9 | ServerAdmin webmaster@localhost 10 | DocumentRoot /var/www/html 11 | Redirect / https://${hostname}/ 12 | 13 | 14 | 15 | 16 | ServerName ${hostname} 17 | 18 | ServerAdmin webmaster@localhost 19 | DocumentRoot /var/www/html 20 | 21 | Alias /static /static 22 | 23 | Require all granted 24 | 25 | 26 | Alias /media /media 27 | XSendFile On 28 | XSendFilePath /media 29 | 30 | Require all granted 31 | 32 | 33 | Require all denied 34 | 35 | 36 | ProxyPass /static/ ! 37 | ProxyPass /media/ ! 38 | 39 | 40 | 41 | AuthType Shibboleth 42 | ShibRequireSession On 43 | ShibApplicationId default 44 | ShibExportAssertion On 45 | require valid-user 46 | 47 | 48 | 49 | Options +Indexes 50 | SetHandler shib 51 | 52 | 53 | ProxyPass /Shibboleth.sso/ ! 54 | 55 | 56 | ProxyPass / http://web:8000/ 57 | ProxyPreserveHost On 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /huntserver/static/huntserver/hunt_base.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: url(/static/huntserver/background.jpg); 3 | } 4 | 5 | #footer { 6 | text-align: center; 7 | padding-top: 10px; 8 | margin-bottom: 10px; 9 | font-size: 90%; 10 | } 11 | 12 | .content { 13 | font-family: verdana; 14 | font-size: 14px; 15 | padding: 10px; 16 | padding-top: 0px; 17 | margin: 10px; 18 | margin-top: 0px; 19 | background: white; 20 | border: 3px solid black; 21 | } 22 | 23 | th { 24 | text-align: left; 25 | } 26 | 27 | #plot { 28 | margin-top: 10px; 29 | } 30 | 31 | .puzzle td img{ 32 | margin-left: 15px; 33 | width: 30px; 34 | height: 30px; 35 | } 36 | 37 | /*for puzzle page*/ 38 | .puzzle, .info { 39 | overflow: auto; 40 | } 41 | 42 | .puzzle-title { 43 | display: inline; 44 | } 45 | 46 | .title-number,.title-name { 47 | display: inline-block; 48 | } 49 | 50 | .leftinfo { 51 | float: left; 52 | } 53 | 54 | .puzzle .info a { 55 | margin-right: 10px; 56 | margin-bottom: 10px; 57 | } 58 | 59 | .puzzle fieldset { 60 | margin: 10px; 61 | } 62 | 63 | .puzzle fieldset td,th { 64 | padding-right: 10px; 65 | } 66 | 67 | .puzzle #puzzleimg { 68 | width: 100%; 69 | max-width: 900px; 70 | border: 1px solid black; 71 | } 72 | 73 | .helptext { 74 | font-size: 10px; 75 | color: gray; 76 | } 77 | 78 | .hint_table > tr > td { 79 | vertical-align: middle; 80 | } 81 | 82 | /* Override bootstrap's xs and sm inline form behavior */ 83 | @media (max-width: 767px) { 84 | .form-inline .form-control { 85 | display: inline-block; 86 | width: auto; 87 | vertical-align: middle; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /huntserver/templates/leaderboard.html: -------------------------------------------------------------------------------- 1 | {% extends "hunt_base.html" %} 2 | {% block title %} Team Leaderboard {% endblock title %} 3 | 4 | {% block includes %} 5 | 6 | 7 | {% endblock includes %} 8 | 9 | {% block content %} 10 |
11 |
12 |
13 |

Team Leaderboard

14 |
15 |
16 | All Teams 17 | CMU Teams 18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for team in team_data %} 30 | 31 | 32 | 33 | 34 | 41 | 42 | 43 | 44 | {% endfor %} 45 |
RankTeamFinish TimeMeta SolvesSolvesLast Solve Time
{{forloop.counter}}{{team.team_name|truncatechars:30}}{{team.finals|date:"M dS, h:i a" }} 35 | {% if team.metas %} 36 | {{team.metas}} 37 | {% else %} 38 | 0 39 | {% endif %} 40 | {{team.solves}}{{team.last_time|date:"M dS, h:i a" }}
46 |
47 |
48 |
49 | {% endblock content %} 50 | -------------------------------------------------------------------------------- /huntserver/migrations/0024_auto_20181021_0015.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.15 on 2018-10-21 04:15 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | def set_my_defaults(apps, schema_editor): 9 | Hunt = apps.get_model('huntserver', 'Hunt') 10 | for hunt in Hunt.objects.all().iterator(): 11 | hunt.display_end_date = hunt.end_date 12 | hunt.display_start_date = hunt.start_date 13 | hunt.save() 14 | 15 | 16 | def reverse_set_default(apps, schema_editor): 17 | pass 18 | 19 | 20 | class Migration(migrations.Migration): 21 | 22 | dependencies = [ 23 | ('huntserver', '0023_userproxyobject'), 24 | ] 25 | 26 | operations = [ 27 | migrations.AddField( 28 | model_name='hunt', 29 | name='display_end_date', 30 | field=models.DateTimeField(null=True, help_text=b'The end date/time displayed to users'), 31 | ), 32 | migrations.AddField( 33 | model_name='hunt', 34 | name='display_start_date', 35 | field=models.DateTimeField(null=True, help_text=b'The start date/time displayed to users'), 36 | ), 37 | 38 | migrations.RunPython(set_my_defaults, reverse_set_default), 39 | 40 | migrations.AlterField( 41 | model_name='hunt', 42 | name='display_end_date', 43 | field=models.DateTimeField(help_text=b'The end date/time displayed to users'), 44 | ), 45 | migrations.AlterField( 46 | model_name='hunt', 47 | name='display_start_date', 48 | field=models.DateTimeField(help_text=b'The start date/time displayed to users'), 49 | ), 50 | ] 51 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master, development ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | env: 20 | DB_NAME: puzzlehunt_db 21 | DB_USER: hunt 22 | DB_PASSWORD: test 23 | DJANGO_SECRET_KEY: test_secret_key 24 | 25 | # Steps represent a sequence of tasks that will be executed as part of the job 26 | steps: 27 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 28 | - uses: actions/checkout@v2 29 | 30 | # Runs a single command using the runners shell 31 | - name: compose-build 32 | run: docker-compose build 33 | 34 | # Runs a single command using the runners shell 35 | - name: compose-version 36 | run: docker-compose --version 37 | 38 | # Runs a single command using the runners shell 39 | - name: compose-up 40 | run: docker-compose up -d 41 | 42 | # Runs a single command using the runners shell 43 | - name: get-container-status 44 | run: docker ps 45 | 46 | # Runs a set of commands using the runners shell 47 | - name: run-tests 48 | run: docker-compose exec -T app coverage run 49 | 50 | # Runs a set of commands using the runners shell 51 | - name: get-results 52 | run: docker-compose exec -T app coverage report 53 | -------------------------------------------------------------------------------- /docker/configs/inc-md-cert-mdq.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEvjCCAyagAwIBAgIJANpi9/mkU/zoMA0GCSqGSIb3DQEBCwUAMHQxCzAJBgNV 3 | BAYTAlVTMQswCQYDVQQIDAJNSTESMBAGA1UEBwwJQW5uIEFyYm9yMRYwFAYDVQQK 4 | DA1JbnRlcm5ldDIuZWR1MREwDwYDVQQLDAhJbkNvbW1vbjEZMBcGA1UEAwwQbWRx 5 | LmluY29tbW9uLm9yZzAeFw0xODExMTMxNDI5NDNaFw0zODExMTAxNDI5NDNaMHQx 6 | CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNSTESMBAGA1UEBwwJQW5uIEFyYm9yMRYw 7 | FAYDVQQKDA1JbnRlcm5ldDIuZWR1MREwDwYDVQQLDAhJbkNvbW1vbjEZMBcGA1UE 8 | AwwQbWRxLmluY29tbW9uLm9yZzCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoC 9 | ggGBAJ0+fUTzYVSP6ZOutOEhNdp3WPCPOYqnB4sQFz7IeGbFL1o0lZjx5Izm4Yho 10 | 4wNDd0h486iSkHxNf5dDhCqgz7ZRSmbusOl98SYn70PrUQj/Nzs3w47dPg9Tpb/x 11 | y44PvNLS/rE56hPgCz/fbHoTTiJt5eosysa1ZebQ3LEyW3jGm+LGtLbdIfkynKVQ 12 | vpp1FVeCamzdeB3ZRICAvqTYQKE1JQDGlWrEsSW0VVEGNjfbzMzr/g4l8JRdMabQ 13 | Jig8tj3UIXnu7A2CKSMJSy3WZ3HX+85oHEbL+EV4PtpQz765c69tUIdNTJax9jQ2 14 | 1c3wL0K27HE8jSRlrXImD50R3dXQBKH+iiynBWxRPdyMBa1YfK+zZEWPbLHshSTc 15 | 9hkylQv3awmPR/+Plz5AtTpe5yss/Ifyp01wz1jt42R+6jDE+WbUjp5XDBCAjGEE 16 | 0FPaYtxjZLkmNl367bdTN12OIn/ixPNH+Z/S/4skdBB9Gc4lb2fEBywJQY0OYNOd 17 | WOxmPwIDAQABo1MwUTAdBgNVHQ4EFgQUMHZuwMaYSJM5mlu3Wc4Ts5xq4/swHwYD 18 | VR0jBBgwFoAUMHZuwMaYSJM5mlu3Wc4Ts5xq4/swDwYDVR0TAQH/BAUwAwEB/zAN 19 | BgkqhkiG9w0BAQsFAAOCAYEAMr4wfLrSoPTzfpXtvL+2vrKBJNnRfuJpOYTbPKUc 20 | DOP2QfzRlczi7suYJvd5rLiRonq8rjyPUyM8gvTfbTps+JhJ6S9mS6dTBxOV1qPZ 21 | 3Ab+XKmq8LUtguGRabKgJgmJH0+inR/wVoal7EVHcWXfij9AT8DZOXW88shc6grh 22 | jUaFZBu/2+q8c8ee0e4ip8B+CVEnCwDKI0d+nTcSmPvAE34CNa33F+QGpXawv5yv 23 | VvIpSaLAeFQhc/jKcnNHfy+Zi7JmSnKZiMvQCbWANQmDjHg7pGmBW9nyQcm6P2/B 24 | 0AVcEj1YTpAR8Mbh1pUdIhoB+chaNnFEIZsXeRsdbbAFpxodInlJ7WekfuvSQ6sU 25 | EXpoyBGOeuuTmR1va8k3QeL8Wc4yNu/g5LwjmtvPrh2jBF8xujc4J6VzP8K2BjA4 26 | xk4LnXgjHOT93dBAJhVYJkykDHwyvHUvsBHoP6lfjrt5P8zunK2mdP/AZKik+Rdt 27 | 1GGlErV2AyWShTOaDLW6NxdP 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /huntserver/migrations/0078_auto_20220205_2106.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2022-02-06 02:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('huntserver', '0077_hint_responder'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='prepuzzle', 15 | name='answer_validation_type', 16 | field=models.CharField(choices=[('STR', 'Strict: Only uppercase A-Z'), ('ACA', 'Case sensitive, no spaces'), ('CAS', 'Case sensitive, spaces allowed'), ('ANY', 'Anything: Full unicode, any case')], default='STR', help_text='The type of answer validation used for this puzzle.', max_length=3), 17 | ), 18 | migrations.AddField( 19 | model_name='puzzle', 20 | name='answer_validation_type', 21 | field=models.CharField(choices=[('STR', 'Strict: Only uppercase A-Z'), ('ACA', 'Case sensitive, no spaces'), ('CAS', 'Case sensitive, spaces allowed'), ('ANY', 'Anything: Full unicode, any case')], default='STR', help_text='The type of answer validation used for this puzzle.', max_length=3), 22 | ), 23 | migrations.AlterField( 24 | model_name='puzzle', 25 | name='puzzle_page_type', 26 | field=models.CharField(choices=[('PDF', 'Puzzle page displays a PDF'), ('LNK', 'Puzzle page links a webpage'), ('WEB', 'Puzzle page displays a webpage'), ('EMB', 'Puzzle is html embedded in the webpage')], default='EMB', help_text='The type of webpage for this puzzle.', max_length=3), 27 | ), 28 | migrations.AlterField( 29 | model_name='puzzle', 30 | name='solution_is_webpage', 31 | field=models.BooleanField(default=True, help_text='Is this solution an html webpage?'), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /huntserver/migrations/0054_auto_20200318_2145.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-03-19 01:45 2 | 3 | from django.db import migrations 4 | from django.template.loader import get_template 5 | from django.conf import settings 6 | 7 | 8 | def setup_pages(apps, schema_editor): 9 | FlatPage = apps.get_model('flatpages', 'FlatPage') 10 | Site = apps.get_model('sites', 'Site') 11 | 12 | try: 13 | our_site = Site.objects.get(pk=1) 14 | except: 15 | our_site = Site.objects.create(domain=settings.DOMAIN, name=settings.SITE_TITLE) 16 | 17 | source = get_template("contact_us.html").template.source.split(" -->")[1] 18 | fp = FlatPage.objects.create(url='/contact-us/', title='Contact Us', content=source) 19 | fp.sites.add(our_site) 20 | 21 | source = get_template("resources.html").template.source.split(" -->")[1] 22 | fp = FlatPage.objects.create(url='/extra/resources/', title='Resources', content=source) 23 | fp.sites.add(our_site) 24 | 25 | source = get_template("hunt_info.html").template.source.split(" -->")[1] 26 | fp = FlatPage.objects.create(url='/hunt-info/', title='Current Hunt Info', content=source) 27 | fp.sites.add(our_site) 28 | 29 | 30 | def remove_pages(apps, schema_editor): 31 | FlatPage = apps.get_model('flatpages', 'FlatPage') 32 | FlatPage.objects.filter(url='/contact-us/').delete() 33 | FlatPage.objects.filter(url='/extra/resources/').delete() 34 | FlatPage.objects.filter(url='/hunt-info/').delete() 35 | 36 | 37 | class Migration(migrations.Migration): 38 | 39 | dependencies = [ 40 | ('huntserver', '0053_auto_20200310_1503'), 41 | ('flatpages', '0001_initial'), 42 | ('sites', '0002_alter_domain_unique'), 43 | ] 44 | 45 | operations = [ 46 | migrations.RunPython(setup_pages, remove_pages) 47 | ] 48 | -------------------------------------------------------------------------------- /huntserver/templates/info_base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap_tags %} 3 | {% load hunt_tags %} 4 | {% load static %} 5 | {% load flatpages %} 6 | 7 | {% block base_includes %} 8 | 9 | {% endblock base_includes %} 10 | 11 | {% block left-header %} 12 | {% get_flatpages '/extra/' as flatpages %} 13 | 29 | 30 |
  • 31 | Hunt Details 32 |
  • 33 | {% for page in flatpages|dictsort:"url" %} 34 |
  • 35 | {{ page.title }} 36 |
  • 37 | {% endfor %} 38 |
  • 39 | Contact Us 40 |
  • 41 | 42 |
  • 43 | {% set_curr_hunt %} 44 | Latest Hunt 45 |
  • 46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /huntserver/migrations/0046_auto_20200209_1112.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-02-09 16:12 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('huntserver', '0045_auto_20200209_0856'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='hunt', 16 | name='last_update_time', 17 | field=models.DateTimeField(default=django.utils.timezone.now, help_text='The last time that the periodic update management command was ran'), 18 | ), 19 | migrations.AddField( 20 | model_name='hunt', 21 | name='points_per_minute', 22 | field=models.IntegerField(default=0, help_text='The number of points granted per minute during the hunt'), 23 | ), 24 | migrations.AddField( 25 | model_name='team', 26 | name='num_unlock_points', 27 | field=models.IntegerField(default=0, help_text='The number of points the team has earned'), 28 | ), 29 | migrations.AlterField( 30 | model_name='hunt', 31 | name='hint_lockout', 32 | field=models.IntegerField(default=60, help_text='The number of minutes before a hint can be used on a newly unlocked puzzle'), 33 | ), 34 | migrations.AlterField( 35 | model_name='puzzle', 36 | name='unlock_type', 37 | field=models.CharField(choices=[('SOL', 'Solves Based Unlock'), ('POT', 'Points Based Unlock'), ('ETH', 'Either (OR) Unlocking Method'), ('BTH', 'Both (AND) Unlocking Methods')], default='SOL', help_text='The type of puzzle unlocking scheme', max_length=3), 38 | ), 39 | migrations.AlterField( 40 | model_name='team', 41 | name='num_available_hints', 42 | field=models.IntegerField(default=0, help_text='The number of hints the team has available to use'), 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /huntserver/templates/hunt_base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load bootstrap_tags %} 3 | {% load hunt_tags %} 4 | 5 | {% block base_includes %} 6 | 7 | {% endblock base_includes %} 8 | 9 | {% block left-header %} 10 | {% set_hunt_from_context %} 11 |
  • 12 | Puzzles 13 |
  • 14 | {% chat_enabled as chat_enabled_bool %} 15 | {% if not tmpl_hunt.is_public and chat_enabled_bool %} 16 | 17 | 18 |
  • 19 | 20 | Chat 21 | {% if team %} 22 | {% if team.num_waiting_messages %} 23 | 24 | {{team.num_waiting_messages}} 25 | 26 | {% else %} 27 | 0 28 | {% endif %} 29 | {% endif %} 30 | 31 |
  • 32 | {% endif %} 33 |
  • Hunt Info
  • 34 |
  • 35 | Leaderboard 36 |
  • 37 | {% endblock %} 38 | 39 | {% block content_wrapper %} 40 | {% if hunt.is_public or puzzle.hunt.is_public %} 41 |
    42 |
    43 |
    44 |

    This is an archived puzzle hunt. All parts may not work properly. We apologize for any issues you encounter.

    45 |
    46 |
    47 |
    48 | {% endif %} 49 | {% block content %} {% endblock content %} 50 | {% endblock content_wrapper %} 51 | 52 | -------------------------------------------------------------------------------- /huntserver/templates/login_selection.html: -------------------------------------------------------------------------------- 1 | {% extends "info_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |
    6 |
    7 |
    8 |

    Select Your Login Service

    9 |

    (Please use a university login if possible)

    10 | 30 |
    31 |
    32 |

    Don't have an account?

    33 |
    34 |

    35 | If you don't have an account with either Carnegie Mellon University or 36 | the University of Pittsburgh, you can 37 | create a Puzzlehunt Local Login account. 38 |
    39 | (Again, this does not need to be done for CMU or Pitt students) 40 |

    41 | 42 |

    Forgot your password?

    43 |
    44 |

    45 | If you are using a university account, please use their services to reset your password. 46 | If you are using a Puzzle Hunt CMU account, you can reset your password here. 47 |

    48 |
    49 |
    50 |
    51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /huntserver/templates/staff_chat.html: -------------------------------------------------------------------------------- 1 | {% extends "staff_base.html" %} 2 | 3 | {% block includes %} 4 | 11 | 12 | 13 | 14 | {% endblock includes %} 15 | 16 | {% block content %} 17 |

    Staff Chat

    18 |
    19 |
    20 |
    21 | {% for team in teams %} 22 | 25 | 26 | {% endfor %} 27 |
    28 |
    29 |

    Chatting with: Team Name

    30 |
    31 | {% for team, dict in message_dict.items %} 32 |
    33 | {{dict.messages}} 34 |
    35 | {% endfor %} 36 | {% for team in teams %} 37 | {% if team.team_name not in message_dict %} 38 |
    39 | {% endif %} 40 | {% endfor %} 41 |
    42 |
    43 | 44 | 45 |
    46 | 47 | 50 |
    51 |
    52 |
    53 |
    54 | {% endblock content %} 55 | -------------------------------------------------------------------------------- /docs/views.rst: -------------------------------------------------------------------------------- 1 | ***** 2 | Views 3 | ***** 4 | 5 | Hunt Views 6 | ========== 7 | 8 | .. autofunction:: huntserver.hunt_views.protected_static(request, file_path) 9 | .. autofunction:: huntserver.hunt_views.hunt(request, hunt_num) 10 | .. autofunction:: huntserver.hunt_views.current_hunt(request) 11 | .. autofunction:: huntserver.hunt_views.prepuzzle(request, puzzle_id) 12 | .. autofunction:: huntserver.hunt_views.hunt_prepuzzle(request, puzzle_id) 13 | .. autofunction:: huntserver.hunt_views.current_prepuzzle(request, puzzle_id) 14 | .. autofunction:: huntserver.hunt_views.puzzle_view(request, puzzle_id) 15 | .. autofunction:: huntserver.hunt_views.puzzle_hint(request, puzzle_id) 16 | .. autofunction:: huntserver.hunt_views.chat(request) 17 | .. autofunction:: huntserver.hunt_views.chat_status(request) 18 | .. autofunction:: huntserver.hunt_views.unlockables(request) 19 | 20 | Info Views 21 | ========== 22 | 23 | .. autofunction:: huntserver.info_views.index(request) 24 | .. autofunction:: huntserver.info_views.previous_hunts(request) 25 | .. autofunction:: huntserver.info_views.registration(request) 26 | .. autofunction:: huntserver.info_views.user_profile(request) 27 | 28 | Staff Views 29 | =========== 30 | 31 | .. autofunction:: huntserver.staff_views.queue(request, page_num) 32 | .. autofunction:: huntserver.staff_views.progress(request) 33 | .. autofunction:: huntserver.staff_views.charts(request) 34 | .. autofunction:: huntserver.staff_views.admin_chat(request) 35 | .. autofunction:: huntserver.staff_views.hunt_management(request) 36 | .. autofunction:: huntserver.staff_views.hunt_info(request) 37 | .. autofunction:: huntserver.staff_views.control(request) 38 | .. autofunction:: huntserver.staff_views.staff_hints_text(request) 39 | .. autofunction:: huntserver.staff_views.staff_hints_control(request) 40 | .. autofunction:: huntserver.staff_views.emails(request) 41 | .. autofunction:: huntserver.staff_views.lookup(request) 42 | 43 | Auth Views 44 | ========== 45 | 46 | .. autofunction:: huntserver.auth_views.login_selection(request) 47 | .. autofunction:: huntserver.auth_views.create_account(request) 48 | .. autofunction:: huntserver.auth_views.account_logout(request) 49 | .. autofunction:: huntserver.auth_views.shib_login(request) 50 | -------------------------------------------------------------------------------- /huntserver/templates/puzzle_hint.html: -------------------------------------------------------------------------------- 1 | {% extends "hunt_base.html" %} 2 | {% load crispy_forms_tags %} 3 | {% block title %}Hints for {{ puzzle.puzzle_name }}{% endblock title %} 4 | 5 | {% block includes %} 6 | 7 | {% if not puzzle.hunt.is_public %} 8 | 12 | 13 | {% endif %} 14 | {% endblock includes %} 15 | 16 | {% block content %} 17 |
    18 |
    19 |
    20 |
    21 |

    Hints for P{{ puzzle.puzzle_number }} - {{ puzzle.puzzle_name}}

    22 | {% if not puzzle.hunt.is_public %} 23 |

    You currently have {{team.num_available_hints}} available hint{{team.num_available_hints|pluralize}} for this hunt

    24 | {% endif %} 25 | 26 |

    Back to Puzzle Page

    27 |
    28 |
    29 |
    30 | {% if not puzzle.hunt.is_public %} 31 |
    32 |
    33 | Request a Hint 34 |
    35 | {% csrf_token %} 36 |
    37 | {{ form|crispy }} 38 | 39 |
    40 |
    41 |
    42 |
    43 | {% endif %} 44 |
    45 |
    46 |

    Previous Hint Requests:

    47 |

    Hint responses will show up below automatically once your hint has been answered.

    48 | 49 | {% if hint_list %} 50 | {% for hint in hint_list reversed %} 51 | {% include "hint_row.html" %} 52 | {% endfor %} 53 | {% endif %} 54 |
    55 | {% if not hint_list %} 56 |

    You have not yet submitted any hint requests for this puzzle

    57 | {% endif %} 58 |
    59 |
    60 |
    61 | {% endblock content %} 62 | -------------------------------------------------------------------------------- /docker/proxy_override.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | networks: 6 | local: 7 | 8 | app: 9 | networks: 10 | local: 11 | 12 | huey: 13 | networks: 14 | local: 15 | 16 | redis: 17 | networks: 18 | local: 19 | 20 | web: 21 | labels: 22 | - "traefik.enable=true" 23 | - "traefik.docker.network=proxy-net" 24 | 25 | # HTTPS redirect for any other URL 26 | - "traefik.http.routers.${PROJECT_NAME}_redirect.entrypoints=web" 27 | - "traefik.http.routers.${PROJECT_NAME}_redirect.rule=Host(${REDIRECT_DOMAINS})" 28 | - "traefik.http.middlewares.redirect_https.redirectscheme.scheme=https" 29 | - "traefik.http.routers.${PROJECT_NAME}_redirect.middlewares=redirect_https" 30 | 31 | # Redirects any other URL (https) to proper URL (https) 32 | - "traefik.http.routers.${PROJECT_NAME}_redirect_secure.entrypoints=websecure" 33 | - "traefik.http.routers.${PROJECT_NAME}_redirect_secure.rule=Host(${REDIRECT_DOMAINS})" 34 | - "traefik.http.middlewares.redirect_domain_${PROJECT_NAME}.redirectregex.regex=^https://[0-9a-zA-Z.]+/(.*)" 35 | - "traefik.http.middlewares.redirect_domain_${PROJECT_NAME}.redirectregex.replacement=https://${DOMAIN}/$${1}" 36 | - "traefik.http.routers.${PROJECT_NAME}_redirect_secure.middlewares=redirect_domain_${PROJECT_NAME}" 37 | - "traefik.http.routers.${PROJECT_NAME}_redirect_secure.tls.certresolver=puzzlehunt_resolver" 38 | 39 | # HTTPS redirect for any other URL 40 | - "traefik.http.routers.${PROJECT_NAME}.entrypoints=web" 41 | - "traefik.http.routers.${PROJECT_NAME}.rule=Host(`${DOMAIN}`)" 42 | - "traefik.http.middlewares.base_https.redirectscheme.scheme=https" 43 | - "traefik.http.routers.${PROJECT_NAME}.middlewares=base_https" 44 | 45 | # This just routes to the actual service 46 | - "traefik.http.routers.${PROJECT_NAME}_secure.entrypoints=websecure" 47 | - "traefik.http.routers.${PROJECT_NAME}_secure.rule=Host(`${DOMAIN}`)" 48 | - "traefik.http.routers.${PROJECT_NAME}_secure.service=${PROJECT_NAME}" 49 | - "traefik.http.routers.${PROJECT_NAME}_secure.tls.certresolver=puzzlehunt_resolver" 50 | 51 | - "traefik.http.services.${PROJECT_NAME}.loadbalancer.server.port=80" 52 | 53 | networks: 54 | local: 55 | proxy-net: 56 | 57 | networks: 58 | proxy-net: 59 | external: true 60 | local: 61 | -------------------------------------------------------------------------------- /puzzlehunt_server/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.contrib import admin 3 | from django.contrib.auth import views as base_auth_views 4 | from django.conf import settings 5 | from django.conf.urls.static import static 6 | from django.urls import reverse_lazy 7 | from django.views.generic import RedirectView 8 | 9 | urlpatterns = [ 10 | # Admin redirections/views 11 | url(r'^admin/login/$', RedirectView.as_view(url=reverse_lazy(settings.LOGIN_URL), 12 | query_string=True)), 13 | url(r'^staff/login/$', RedirectView.as_view(url=reverse_lazy(settings.LOGIN_URL), 14 | query_string=True)), 15 | url(r'^admin/$', RedirectView.as_view(url=reverse_lazy('admin:app_list', 16 | args=('huntserver',)))), 17 | url(r'^staff/$', RedirectView.as_view(url=reverse_lazy('admin:app_list', 18 | args=('huntserver',)))), 19 | url(r'^staff/', admin.site.urls), 20 | url(r'^admin/', admin.site.urls), 21 | 22 | # All of the huntserver URLs 23 | url(r'^', include('huntserver.urls', namespace="huntserver")), 24 | 25 | # User auth/password reset 26 | url(r'^accounts/logout/$', base_auth_views.LogoutView.as_view(), 27 | name='logout', kwargs={'next_page': '/'}), 28 | url(r'^accounts/login/$', base_auth_views.LoginView.as_view()), 29 | url(r'^password_reset/$', base_auth_views.PasswordResetView.as_view(), name='password_reset'), 30 | url(r'^password_reset/done/$', base_auth_views.PasswordResetDoneView.as_view(), 31 | name='password_reset_done'), 32 | url(r'^reset/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', 33 | base_auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'), 34 | url(r'^reset/done/$', base_auth_views.PasswordResetCompleteView.as_view(), 35 | name='password_reset_complete'), 36 | ] 37 | 38 | # Use silk if enabled 39 | if 'silk' in settings.INSTALLED_APPS: 40 | urlpatterns.append(url(r'^silk/', include('silk.urls', namespace='silk'))) 41 | 42 | # Hack for using development server 43 | if(settings.DEBUG): 44 | import debug_toolbar 45 | urlpatterns = urlpatterns + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 46 | urlpatterns.append(url(r'^__debug__/', include(debug_toolbar.urls))) 47 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | image: postgres:12-alpine 6 | restart: always 7 | environment: 8 | POSTGRES_DB: ${DB_NAME} 9 | POSTGRES_USER: ${DB_USER} 10 | POSTGRES_PASSWORD: ${DB_PASSWORD} 11 | volumes: 12 | - ./docker/volumes/db_data:/var/lib/postgresql/data 13 | 14 | redis: 15 | image: redis:5-alpine 16 | restart: always 17 | volumes: 18 | - ./docker/volumes/redis_data:/data 19 | 20 | app: 21 | build: 22 | context: . 23 | image: django_puzzlehunt # Name image for use in huey 24 | restart: always 25 | volumes: 26 | - .:/code # Enables live modification of django files 27 | - static:/static 28 | - media:/media 29 | - ./docker/volumes/logs:/var/log/external 30 | environment: 31 | - DOMAIN 32 | - SITE_TITLE 33 | - DJANGO_SECRET_KEY 34 | - DJANGO_ENABLE_DEBUG 35 | - DJANGO_EMAIL_USER 36 | - DJANGO_EMAIL_PASSWORD 37 | - DJANGO_EMAIL_HOST 38 | - DJANGO_EMAIL_PORT 39 | - DJANGO_EMAIL_FROM 40 | - DJANGO_USE_SHIBBOLETH 41 | - PUZZLEHUNT_CHAT_ENABLED 42 | - DJANGO_SETTINGS_MODULE=puzzlehunt_server.settings.env_settings 43 | - DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@db/${DB_NAME} 44 | - ENABLE_DEBUG_TOOLBAR 45 | - SENTRY_DSN 46 | depends_on: 47 | - db 48 | - redis 49 | 50 | huey: 51 | image: django_puzzlehunt # re-use above image 52 | command: bash -c "python /code/manage.py migrate --no-input && python /code/manage.py run_huey --quiet" 53 | restart: always 54 | volumes: 55 | - .:/code 56 | - static:/static 57 | - media:/media 58 | - ./docker/volumes/logs:/var/log/external 59 | environment: 60 | - DJANGO_SECRET_KEY 61 | - DJANGO_SETTINGS_MODULE=puzzlehunt_server.settings.env_settings 62 | - DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@db/${DB_NAME} 63 | - SENTRY_DSN 64 | - DJANGO_EMAIL_USER 65 | - DJANGO_EMAIL_PASSWORD 66 | - DJANGO_EMAIL_HOST 67 | - DJANGO_EMAIL_PORT 68 | - DJANGO_EMAIL_FROM 69 | depends_on: 70 | - app 71 | 72 | web: 73 | restart: always 74 | build: 75 | context: ./docker/ 76 | dockerfile: apacheDockerfile 77 | args: 78 | DOMAIN: ${DOMAIN} 79 | depends_on: 80 | - app 81 | volumes: 82 | - static:/static 83 | - media:/media 84 | environment: 85 | - DOMAIN 86 | - CONTACT_EMAIL 87 | tty: true 88 | 89 | volumes: 90 | static: 91 | media: 92 | -------------------------------------------------------------------------------- /locust/reset_data.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | # sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "puzzlehunt_server.settings.env_settings") 6 | 7 | import django 8 | django.setup() 9 | 10 | # your imports, e.g. Django models 11 | from django.contrib.auth.models import User 12 | from huntserver.models import Hunt, Team, Person 13 | 14 | from django.core.management import call_command 15 | 16 | # Prompt user for confirmation 17 | prompt = ("Are you sure you want to delete all existing database" + 18 | "data and reset to the loadtest default data?") 19 | prompt = '%s [%s]|%s: ' % (prompt, 'n', 'y') 20 | while(True): 21 | ans = input(prompt) 22 | if not ans: 23 | sys.exit() 24 | if ans not in ['y', 'Y', 'n', 'N']: 25 | print('please enter y or n.') 26 | continue 27 | if ans == 'n' or ans == 'N': 28 | sys.exit() 29 | if ans == 'y' or ans == 'Y': 30 | break 31 | 32 | # Wipe existing data and load base data 33 | call_command('flush', verbosity=0, interactive=False) 34 | call_command('loaddata', 'locust', ignorenonexistent=True) 35 | 36 | # Add in the requested number of users/people/teams 37 | curr_hunt = Hunt.objects.get(is_current_hunt=True) 38 | 39 | team_size = curr_hunt.team_size 40 | num_bots = 900 # should be a multiple of 25 and of team size 41 | num_staff = int(num_bots / 25) 42 | num_players = num_bots - num_staff 43 | num_teams = int(num_players / team_size) 44 | 45 | print("Now creating %d players, %d teams, and %d staff members" % (num_players, num_teams, num_staff)) 46 | 47 | for team_index in range(num_teams): 48 | sys.stdout.write(".") 49 | team = Team.objects.create(team_name="team_" + str(team_index), hunt=curr_hunt, 50 | location="test_loc", join_code="JOIN1") 51 | for person_index in range(team_size): 52 | user_number = team_index * team_size + person_index 53 | user = User.objects.create_user('test_user_' + str(user_number), 54 | 'example' + str(user_number) + '@example.com', 55 | 'password' + str(user_number)) 56 | person = Person.objects.create(user=user, is_shib_acct=False) 57 | person.teams.add(team) 58 | person.save() 59 | for person_index in range(num_staff): 60 | user_number = person_index + num_players 61 | user = User.objects.create_superuser('test_user_' + str(user_number), 62 | 'example' + str(user_number) + '@example.com', 63 | 'password' + str(user_number)) 64 | person = Person.objects.create(user=user, is_shib_acct=False) 65 | person.save() 66 | print("\n") 67 | -------------------------------------------------------------------------------- /huntserver/static/huntserver/chat.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | $("#chatcontainer").scrollTop($("#chatcontainer")[0].scrollHeight); 4 | 5 | function is_visible(){ 6 | var stateKey, keys = { 7 | hidden: "visibilitychange", 8 | webkitHidden: "webkitvisibilitychange", 9 | mozHidden: "mozvisibilitychange", 10 | msHidden: "msvisibilitychange" 11 | }; 12 | for (stateKey in keys) { 13 | if (stateKey in document) { 14 | return !document[stateKey]; 15 | } 16 | } 17 | return true; 18 | } 19 | 20 | function send_message(){ 21 | if($("#announce_checkbox:checked").length > 0){ 22 | is_announcement = $("#announce_checkbox:checked")[0].checked 23 | } else { 24 | is_announcement = false 25 | } 26 | data = {team_pk: curr_team, message: $('#messagebox').val(), 27 | is_response: is_response, csrfmiddlewaretoken: csrf_token, 28 | is_announcement: is_announcement}; 29 | $.post(chat_url, data, 'json') 30 | .fail( function(xhr, textStatus, errorThrown) { 31 | console.log(xhr); 32 | }) 33 | .done(function(response) { 34 | response = JSON.parse(response); 35 | receiveMessages(response['message_dict']); 36 | last_pk = response['last_pk']; 37 | }); 38 | $('#messagebox').val(''); 39 | $('#announce_checkbox').prop('checked', false); 40 | } 41 | 42 | var get_posts = function() { 43 | if(is_visible()){ 44 | $.getJSON(chat_url, {last_pk: last_pk}) 45 | .done(function(result){ 46 | receiveMessages(result['message_dict']); 47 | last_pk = result['last_pk']; 48 | }) 49 | .fail( function(xhr, textStatus, errorThrown) { 50 | console.log(xhr); 51 | }); 52 | } 53 | } 54 | 55 | $('#sendbutton').click(function() { 56 | send_message(); 57 | }); 58 | $(document).on("keypress", "#messagebox", function(e) { 59 | if (e.which == 13) { 60 | send_message(); 61 | } 62 | }); 63 | 64 | setInterval(get_posts, 5000); 65 | 66 | function receiveMessages(message_dict) { 67 | $.each(message_dict, function(team_name, team_data) { 68 | if($("#chat_"+ team_data['pk']).length == 0){ 69 | $("#chatcontainer").append("
    "); 70 | var b = ""); 72 | map_buttons(); 73 | } 74 | var message_window = $("#chat_" + team_data['pk']); 75 | message_window.append(team_data['messages']); 76 | if(team_data['pk'] != curr_team && $(team_data['messages']).hasClass('user-message')){ 77 | $("button[data-id=" + team_data['pk'] + "]").css("background-color", "red"); 78 | } 79 | $("#chatcontainer").scrollTop($("#chatcontainer")[0].scrollHeight); 80 | }); 81 | } 82 | }); -------------------------------------------------------------------------------- /huntserver/templates/hunt_info.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | {% load humanize %} 5 |

    {{curr_hunt.hunt_name}}

    6 |

    {{ curr_hunt.display_start_date|date:" m/d/y " }} {{ curr_hunt.display_start_date|time:"h:iA" }} – {{ curr_hunt.display_end_date|date:" m/d/y "}} {{ curr_hunt.display_end_date|time:"h:iA e" }}
    7 | Kickoff location is {{ curr_hunt.location }}

    8 |
    9 |

    What are the puzzles like?

    10 |

    For examples of the kinds of puzzles we'll give you, check out our Past Hunts

    11 |

    Or these similar hunts:

    12 | 16 |

    Sounds fun! How does my team win?

    17 |

    18 | Each puzzle yields a solution (like a word or a phrase). You'll be combining answers to 19 | puzzles with a mechanic that will be explained more on the day of the hunt. 20 | Eventually players reach the endgame, a bonus round for those who 21 | complete the puzzles the fastest. 22 |

    23 |

    How do I register?

    24 |

    Register your team of {{curr_hunt.team_size|apnumber}} (or fewer if you want) here! 25 |

    26 | 27 |

    What should I bring?

    28 |

    29 |

      30 |
    • A team of no more than {{curr_hunt.team_size|apnumber}} people. Humans only, please. Robots are discouraged; they mess up the sensors.
    • 31 |
    • Laptops. All of our puzzles will be online.
    • 32 |
    • Scissors. These will be helpful.
    • 33 | 34 |
    35 |

    36 | 37 |

    How long will it last?

    38 |

    The hunt is expected to run 8 hours. If there isn't a winner by 8 hours in (rare), we'll inform all teams to keep playing and continue until there's a winner.

    39 | 40 |

    Will there be food?

    41 |

    42 | Yes, there will be food, usually pizza. If you have dietary restrictions, please list them in the appropriate box when registering. 43 |

    44 | 45 |

    Is there anything else I should know?

    46 |

    47 |

      48 |
    • There may be some running around campus required.
    • 49 |
    • You CAN register a partial team and we'll do our best to try to match all of the partial teams on the day of.
    • 50 |
    • Nothing on this and the registration page is a puzzle crucial for completing the hunt. No, really, this is not a puzzle.
    • 51 |
    52 |

    53 | 54 |

    Wait, I have more questions!

    55 |

    Email the HALP?! LINE ({% contact_email %}) with "Puzzle Hunt" somewhere in the subject line.

    56 |
    -------------------------------------------------------------------------------- /huntserver/static/huntserver/queue.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | var flashing = false, 3 | focused = true; 4 | var title = document.title; 5 | 6 | $('.sub_form').on('submit', formListener); 7 | 8 | window.addEventListener('focus', function() { 9 | focused = true; 10 | flashing = false; 11 | }); 12 | window.addEventListener('blur', function() { 13 | focused = false; 14 | }); 15 | 16 | /* flash the title if necessary */ 17 | setInterval(function() { 18 | if (flashing && !focused) { 19 | if (document.title[0] == '[') { 20 | document.title = title; 21 | } else { 22 | document.title = '[' + title + '] - New Submissions'; 23 | } 24 | } else { 25 | document.title = title; 26 | } 27 | }, 1000); 28 | 29 | var get_posts = function() { 30 | $.ajax({ 31 | type: 'get', 32 | url: "/staff/queue/", 33 | data: {last_date: last_date, all: true, puzzle_id: puzzle_id, team_id: team_id}, 34 | success: function (response) { 35 | var response = JSON.parse(response); 36 | messages = response.submission_list; 37 | if(messages.length > 0){ 38 | for (var i = 0; i < messages.length; i++) { 39 | receiveMessage(messages[i]); 40 | }; 41 | last_date = response.last_date; 42 | } 43 | }, 44 | error: function (html) { 45 | console.log(html); 46 | } 47 | }); 48 | } 49 | setInterval(get_posts, 3000); 50 | 51 | function formListener(e) { 52 | e.preventDefault(); 53 | old_row = $(this).parent().parent(); 54 | $.ajax({ 55 | url : $(this).attr('action') || window.location.pathname, 56 | type: "POST", 57 | data: $(this).serialize(), 58 | success: function (response) { 59 | response = JSON.parse(response); 60 | old_row.replaceWith($(response.submission_list[0])); 61 | $('.sub_form').on('submit', formListener); 62 | }, 63 | error: function (jXHR, textStatus, errorThrown) { 64 | console.log(jXHR); 65 | } 66 | }); 67 | } 68 | 69 | function receiveMessage(submission) { 70 | submission = $(submission); 71 | pk = submission.data('id'); 72 | if ($('tr[data-id=' + pk + ']').length == 0) { 73 | if(!submission.hasClass('correct')) { 74 | flashing = !focused; 75 | $('audio')[0].play(); 76 | } 77 | submission.prependTo("#sub_table"); 78 | if($('#sub_table tr').length >= 30){ 79 | $('#sub_table tr:last').remove(); 80 | } 81 | } else { 82 | $('tr[data-id=' + pk + ']').replaceWith(submission); 83 | } 84 | $('.sub_form').on('submit', formListener); 85 | } 86 | 87 | /* open a text box for submitting an email */ 88 | $(document).delegate('.needs-response', 'click', function() { 89 | $(this).siblings('form').show(); 90 | return false; 91 | }); 92 | $(document).delegate('.canned-response', 'click', function() { 93 | $(this).siblings('form').submit(); 94 | return false; 95 | }); 96 | }); -------------------------------------------------------------------------------- /docs/basics.rst: -------------------------------------------------------------------------------- 1 | Basics 2 | ****** 3 | 4 | Despite it's size, this project only has one main app named huntserver which 5 | does nearly everything. This page is meant to outline basic low level 6 | operational aspects and design choices of the server. This information is really 7 | only helpful for people looking to help develop or modify the application. If 8 | you're just using the application, you can skip all this. (models and views too) 9 | 10 | Design 11 | ------ 12 | The design of this project is somewhat divided into two parts, the staff 13 | experience and the hunt participant experience. 14 | 15 | Staff is anyone that has the staff attribute set in the admin page. These users 16 | have access to the /staff/ area of the site; however, in order to access all 17 | functions and access the /admin/ area of the site, the user must also be a 18 | superuser as designated by Django. 19 | 20 | Dynamic Content 21 | --------------- 22 | Dynamic content is created by using a combination of the model-view controller 23 | and the default Django templating engine. Both are extensively documented on 24 | Django's website. Both models and views used in this project are documented by 25 | later pages. 26 | 27 | Static Content 28 | -------------- 29 | Puzzles should not be checked into the Github repository. They should exist on 30 | some accessible online file source (we have used Dropboxin the past) 31 | and will be downloaded and converted when the admin choses to do so. 32 | Once downloaded, the puzzle files live in ``{PROJECT FOLDER}/media/puzzles/`` 33 | and are named using the "puzzle id" field of the puzzle which is enforced to 34 | be unique to each puzzle. 35 | 36 | To protect users from being able to just go to 37 | ``/media/puzzles/{Puzzle_id}.pdf`` and get puzzles, the server comes included 38 | with a protected routing path utilizing X-Sendfile. The /protected/ URL will 39 | only allow a user to access puzzle files if they have unlocked the puzzle. 40 | To avoid hard-coding that path, you can use the variable 41 | "settings.PROTECTED_URL" after importing the project settings. 42 | 43 | It is a bit simplistic, but anything in the puzzles directory is permission 44 | guarded by the set of hexadecimal characters before the '-' or '.' of the 45 | filename. If the requesting user has access to the puzzle object with the 46 | corresponding puzzle_id, then they will have access to that file. 47 | You can use this to protect files other than just the puzzle PDFs and PNGs. 48 | 49 | You should protect your /media/puzzles URL by only allowing access to 50 | /media/puzzles/ from internal sources. The Apache configuration for this project 51 | includes protection like this already. 52 | 53 | Database 54 | -------- 55 | As noted in setup, the default database for this project is a Postgres database. 56 | After setup, the database should never need to be modified by hand, 57 | additions or deletions should be done from the online admin GUI or if absolutely 58 | necessary, from the Django interactive shell. 59 | Modifications to the table structure should only be done by modifying models.py 60 | and using the automatically created migration files. 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/dlareau/puzzlehunt_server.svg?branch=master)](https://travis-ci.org/dlareau/puzzlehunt_server) 2 | [![Coverage Status](https://coveralls.io/repos/github/dlareau/puzzlehunt_server/badge.svg)](https://coveralls.io/github/dlareau/puzzlehunt_server) 3 | 4 | # puzzlehunt_server [Archive] 5 | ## Note! This project has been deprecated in favor of [PuzzleSpring](https://github.com/dlareau/puzzlespring). 6 | Please consider using PuzzleSpring instead of this project for new setups. It is more flexible, easier to get started with, and will see regular updates. A migration script for those who have already deployed puzzlehunt_server will be be released alongside PuzzleSpring 1.0. 7 | 8 | 9 | ## Old Description 10 | A server for running puzzle hunts. This project is mainly used by Puzzlehunt CMU to run their puzzlehunt, but is generic enough to be used for nearly any puzzle hunt. Includes basic features such as per-puzzle pages, automatic answer response, teams, customizable unlocking structure, and admin pages to manange submissions, teams, as well as hunt progress. It also includes automatic team creation from registration, privacy settings for hunts, cool charts, a built in chat, and automatic file fetching and hosting. 11 | 12 | Documentation can be found at http://docs.puzzlehunt.club 13 | 14 | If you are interested in getting this running elsewhere, let me (dlareau@cmu.edu) know. I'd be happy to help anyone who wants to get this up and running for their needs, and get help get you over any gaps in setup documentation. I'd also recommend if possible waiting for version 4.1 which will make adapting this software to other hunts easier: https://github.com/dlareau/puzzlehunt_server/issues/121 15 | 16 | Please submit issues for any bugs reports or feature requests. 17 | 18 | ### Setup 19 | This project now uses docker-compose as it's main form of setup. You can use the following steps to get a sample server up and going 20 | 21 | 1. Install [docker/docker-compose.](https://docs.docker.com/compose/install/) 22 | 2. Clone this repository. 23 | 3. Make a copy of ```sample.env``` named ```.env``` (yes, it starts with a dot). 24 | 4. Edit the new ```.env``` file, filling in new values for the first block of uncommented lines. Other lines can be safely ignored as they only provide additional functionality. 25 | 5. Run ```docker-compose up``` (possibly prepending ```sudo``` if needed) 26 | 6. Once up, you'll need to run the following commands to collect all the static files (to be run any time you alter static files) and to load in an initial hunt to pacify some of the display logic (to be run only once) : 27 | ``` 28 | docker-compose exec app python3 /code/manage.py collectstatic --noinput 29 | docker-compose exec app python3 /code/manage.py loaddata initial_hunt 30 | ``` 31 | 7. You should now have the server running on a newly created VM, accessible via [http://localhost](http://localhost). The repository you cloned has been linked into the VM by docker, so any changes made to the repository on the host system should show up automatically. (A ```docker-compose restart``` may be needed for some changes to take effect) 32 | -------------------------------------------------------------------------------- /huntserver/templates/hunt_example.html: -------------------------------------------------------------------------------- 1 | {% extends "hunt_base.html" %} 2 | {% load hunt_tags %} 3 | 4 | 28 | 29 | {% block title %}Puzzles!{% endblock title %} 30 | 31 | {% block content %} 32 |
    33 |
    34 |
    35 |

    Unlocked Puzzles

    36 | 37 |

    Click here for unlocked objects

    38 |
    39 |
    40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {% for puzzle in puzzles %} 50 | 51 | 58 | 63 | 68 | 69 | {% endfor %} 70 | 71 |
    Solved?AnswerPuzzle Name/Link
    52 | {% if puzzle in solved %} 53 | Solved 54 | {% else %} 55 | Solved 56 | {% endif %} 57 | 59 | {% if puzzle in solved %} 60 | {{ puzzle.answer|upper }} 61 | {% endif %} 62 | 64 | 65 |

    P{{puzzle.puzzle_number}} - {{ puzzle.puzzle_name }}

    66 |
    67 |
    72 |
    73 |

    For assistance, email 74 | {% contact_email %} or try our new Chat Feature.

    75 |
    76 |
    77 |
    78 | No plot written yet! 79 |
    80 |
    81 |
    82 |
    83 | {% endblock content %} 84 | -------------------------------------------------------------------------------- /huntserver/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "info_base.html" %} 2 | {% load hunt_tags %} 3 | 4 | {% block includes %} 5 | 6 | {% endblock includes %} 7 | 8 | {% block content %} 9 | 10 |
    11 |
    12 |

    13 | 14 |

    15 |

    16 | {% if curr_hunt.is_locked %} 17 | Our next hunt:
    18 | {% elif curr_hunt.is_public %} 19 | Our previous hunt:
    20 | {% elif curr_hunt.is_day_of_hunt %} 21 | Our current hunt:
    22 | {% else %} 23 | Our next hunt:
    24 | {% endif %} 25 |

    26 |

    27 | {{curr_hunt.hunt_name}}
    28 | {{ curr_hunt.display_start_date|date:" m/d/y " }} {{ curr_hunt.display_start_date|time:"h:iA" }} – {{ curr_hunt.display_end_date|date:" m/d/y "}} {{ curr_hunt.display_end_date|time:"h:iA e" }}
    29 | Kickoff location is {{ curr_hunt.location }} 30 |

    31 | {% if not curr_hunt.is_public %} 32 |

    33 | 34 | 35 | {% if team %} 36 | View Registration 37 | {% else %} 38 | Register Now! 39 | {% endif %} 40 | 41 | 42 |

    43 | {% endif %} 44 |
    45 | 46 |
    47 |

    Who are we?

    48 |

    49 | We are Puzzle Hunt CMU, a group of puzzle enthusiasts from Carnegie Mellon University. We work together to solve Puzzle Hunts such as the MIT mystery hunt, or any of the many tech company puzzle hunts (examples of such companies include Microsoft, APT and Palantir). We also write and host CMU's very own puzzle hunt once a semester, with the goal of providing a fun-filled experience to as many members of the community as possible. 50 |

    51 |

    What is a puzzle hunt?

    52 |

    53 | Very simply put, it's an event where people get together to solve carefully designed puzzles which somehow link together on a larger scale. 54 |
    55 | A puzzle hunt is typically characterized by its structure. In addition to several normal puzzles, each round has a meta-puzzle, which requires teams to have solved most of the normal puzzles to even unlock, and often requires solvers to incorporate answers from that round's normal puzzles to make sense of. 56 |
    57 | Our puzzle hunt's structure changes from semester to semester so as to provide new challenges for solvers. 58 |

    59 |

    Wait that sounds cool, can I participate?

    60 |

    Yeah, check out our current hunt page!

    61 |

    Wait, I have more questions!

    62 |

    Email the HALP?! LINE ({% contact_email %}) with "Puzzle Hunt" somewhere in the subject line.

    63 |
    64 |
    65 | {% endblock content %} 66 | {% block footer %}{% endblock %} 67 | -------------------------------------------------------------------------------- /huntserver/templates/queue.html: -------------------------------------------------------------------------------- 1 | {% extends "staff_base.html" %} 2 | {% block title %}Queue{% endblock title %} 3 | 4 | {% block includes %} 5 | 6 | 11 | 12 | {% endblock includes %} 13 | 14 | {% block content %} 15 |

    Submission Queue

    16 | 17 |
    18 | 26 | 34 | 35 |
    36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {% for submission in submission_list %} 50 | {{ submission }} 51 | {% endfor %} 52 | 53 |
    TeamPuzzleAnswerSubmitted AtResponse
    54 | 55 |
    56 | 57 |
    58 | {% if page_info.has_other_pages %} 59 |
      60 | 61 | {% if page_info.has_previous %} 62 |
    • «
    • 63 | {% else %} 64 |
    • «
    • 65 | {% endif %} 66 | 67 | {% for i in page_info.paginator.page_range %} 68 | {% if page_info.number > 3 and forloop.first %} 69 |
    • 1
    • 70 |
    • 71 | {% endif %} 72 | {% if page_info.number == i %} 73 |
    • {{ i }}
    • 74 | {% elif i > page_info.number|add:'-3' and i < page_info.number|add:'3' %} 75 |
    • {{ i }}
    • 76 | {% endif %} 77 | {% if page_info.paginator.num_pages > page_info.number|add:'3' and forloop.last %} 78 |
    • 79 |
    • {{ page_info.paginator.num_pages }}
    • 80 | {% endif %} 81 | {% endfor %} 82 | 83 | {% if page_info.has_next %} 84 |
    • »
    • 85 | {% else %} 86 |
    • »
    • 87 | {% endif %} 88 | 89 |
    90 | {% endif %} 91 |
    92 | 95 | {% endblock content %} 96 | -------------------------------------------------------------------------------- /huntserver/templatetags/hunt_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | from django.template import Template, Context 4 | from huntserver.models import Hunt 5 | from django.utils import timezone 6 | from huntserver.utils import get_puzzle_answer_regex, get_validation_error 7 | register = template.Library() 8 | 9 | 10 | @register.simple_tag(takes_context=True) 11 | def hunt_static(context): 12 | return settings.MEDIA_URL + "hunt/" + str(context['hunt'].hunt_number) + "/" 13 | 14 | 15 | @register.simple_tag(takes_context=True) 16 | def site_title(context): 17 | return settings.SITE_TITLE 18 | 19 | 20 | @register.simple_tag(takes_context=True) 21 | def contact_email(context): 22 | return settings.CONTACT_EMAIL 23 | 24 | 25 | @register.simple_tag(takes_context=True) 26 | def chat_enabled(context): 27 | return settings.CHAT_ENABLED 28 | 29 | 30 | @register.filter() 31 | def render_with_context(value): 32 | return Template(value).render(Context({'curr_hunt': Hunt.objects.get(is_current_hunt=True)})) 33 | 34 | 35 | @register.tag 36 | def set_curr_hunt(parser, token): 37 | return CurrentHuntEventNode() 38 | 39 | 40 | class CurrentHuntEventNode(template.Node): 41 | def render(self, context): 42 | context['tmpl_curr_hunt'] = Hunt.objects.get(is_current_hunt=True) 43 | return '' 44 | 45 | 46 | @register.tag 47 | def set_hunts(parser, token): 48 | return HuntsEventNode() 49 | 50 | 51 | class HuntsEventNode(template.Node): 52 | def render(self, context): 53 | old_hunts = Hunt.objects.filter(end_date__lt=timezone.now()).exclude(is_current_hunt=True) 54 | context['tmpl_hunts'] = old_hunts.order_by("-hunt_number")[:5] 55 | return '' 56 | 57 | 58 | @register.tag 59 | def set_hunt_from_context(parser, token): 60 | return HuntFromContextEventNode() 61 | 62 | 63 | class HuntFromContextEventNode(template.Node): 64 | def render(self, context): 65 | if("hunt" in context): 66 | context['tmpl_hunt'] = context['hunt'] 67 | return '' 68 | elif("puzzle" in context): 69 | context['tmpl_hunt'] = context['puzzle'].hunt 70 | return '' 71 | else: 72 | context['tmpl_hunt'] = Hunt.objects.get(is_current_hunt=True) 73 | return '' 74 | 75 | 76 | @register.simple_tag() 77 | def hints_open(team, puzzle): 78 | if(team is None or puzzle is None): 79 | return False 80 | return team.hints_open_for_puzzle(puzzle) 81 | 82 | 83 | @register.simple_tag() 84 | def puzzle_answer_regex(puzzle): 85 | return get_puzzle_answer_regex(puzzle.answer_validation_type) 86 | 87 | 88 | @register.simple_tag() 89 | def puzzle_validation_error(puzzle): 90 | return get_validation_error(puzzle.answer_validation_type) 91 | 92 | 93 | @register.simple_tag(takes_context=True) 94 | def shib_login_url(context, entityID, next_path): 95 | if(context['request'].is_secure()): 96 | protocol = "https://" 97 | else: 98 | protocol = "http://" 99 | shib_str = "https://" + settings.SHIB_DOMAIN + "/Shibboleth.sso/Login" 100 | entity_str = "entityID=" + entityID 101 | target_str = "target=" + protocol + context['request'].get_host() + "/shib/login" 102 | next_str = "next=" + next_path 103 | 104 | return shib_str + "?" + entity_str + "&" + target_str + "?" + next_str 105 | -------------------------------------------------------------------------------- /huntserver/templates/previous_hunts.html: -------------------------------------------------------------------------------- 1 | {% extends "info_base.html" %} 2 | 3 | {% block includes %} 4 | 5 | {% endblock includes %} 6 | 7 | {% block content %} 8 | 9 |
    10 |

    11 | Puzzlehunts we have run, listed in reverse chronological order back to 2007. (Playable back to 2015) 12 | 13 |

    14 | {% for hunt in hunts reversed %} 15 |

    16 | 17 | {{hunt.hunt_name}} ({{hunt.season}} '{{ hunt.start_date|date:"y" }}) 18 | 19 |

    20 |
      21 |
    • 22 | {{ hunt.display_start_date|date:" m/d/y " }} 23 | {{ hunt.display_start_date|time:"P" }} - 24 | {% if hunt.display_start_date != hunt.display_end_date %} 25 | {{ hunt.display_end_date|date:" m/d/y " }} 26 | {% endif %} 27 | {{ hunt.display_end_date|time:"P" }} 28 |
    • 29 |
    • {{hunt.puzzle_set.all|length}} Puzzles
    • 30 |
    • {{hunt.real_teams|length}} Teams
    • 31 |
    32 | {% endfor %} 33 |

    Ornithology Hunt (Fall '14)

    34 |
      35 |
    • 10/04/14 Noon - 6pm
    • 36 |
    • 8 Puzzles
    • 37 |
    38 |

    Zombie Apocalypse Hunt (Spring '14)

    39 |
      40 |
    • 03/22/14 Noon - 8pm
    • 41 |
    • 17 Puzzles
    • 42 |
    43 |

    An Infinitely Improbable Puzzle Hunt (Fall '13)

    44 |
      45 |
    • 10/12/13 Noon - 8pm
    • 46 |
    • 22 Puzzles
    • 47 |
    48 |

    Pirate Treasure Hunt (Spring '13)

    49 |
      50 |
    • 02/23/13 Noon - 8pm
    • 51 |
    • 16 Puzzles
    • 52 |
    53 |

    Weather Machine Hunt (Fall '12)

    54 |
      55 |
    • 10/13/12 Noon - 8pm
    • 56 |
    • 22 Puzzles
    • 57 |
    58 |

    The Great Library (Spring '12)

    59 |
      60 |
    • 02/18/12 Noon - 8pm
    • 61 |
    • 22 Puzzles
    • 62 |
    63 |

    Babel Systems Hunt (Fall '11)

    64 |
      65 |
    • 11/12/11 Noon - 8pm
    • 66 |
    • 13 Puzzles
    • 67 |
    68 |

    Jackson Detective Agency (Spring '11)

    69 |
      70 |
    • 04/23/11 Noon - 8pm
    • 71 |
    • 22 Puzzles
    • 72 |
    73 |

    The Wizard's Elemental Tower (Spring '10)

    74 |
      75 |
    • 04/03/10 Noon - 8pm
    • 76 |
    • 16 Puzzles
    • 77 |
    78 |

    Clone Hunt (Fall '09)

    79 |
      80 |
    • 11/07/10 Noon - 8pm
    • 81 |
    • 11 Puzzles
    • 82 |
    83 |

    Time Travel Hunt (Spring '09)

    84 |
      85 |
    • 03/28/09 Noon - 8pm
    • 86 |
    • 12 Puzzles
    • 87 |
    88 |

    Geller Hotel Psychic Convention (Fall '08)

    89 |
      90 |
    • 11/01/09 Noon - 8pm
    • 91 |
    • 15 Puzzles
    • 92 | 93 |
    94 |

    (Pirates?) Hunt (Spring '08)

    95 |
      96 |
    • 03/01/08 Noon - 8pm
    • 97 |
    • 17 Puzzles
    • 98 |
    99 |

    Carmen Sandiego Hunt (Fall '07)

    100 |
      101 |
    • 10/26/07 Noon - 8pm
    • 102 |
    • 12 Puzzles
    • 103 |
    104 |

    Alice in Wonderland Hunt (Spring '07)

    105 |
      106 |
    • 02/23/07 Noon - 8pm
    • 107 |
    • 12 Puzzles
    • 108 |
    109 | 110 |
    111 | {% endblock content %} 112 | -------------------------------------------------------------------------------- /huntserver/templates/staff_hunt_info.html: -------------------------------------------------------------------------------- 1 | {% extends "staff_base.html" %} 2 | {% load admin_urls %} 3 | {% block title %}Hunt Info{% endblock title %} 4 | 5 | {% block content %} 6 | 12 | 13 |

    Hunt Info

    14 |
    15 |
    16 | {{ curr_hunt.real_teams.count }} Teams:
    17 | Needs a room: ({{ need_teams.count }})
    18 | (Ordered first registration to last registration) 19 | 20 | {% for team in need_teams %} 21 | 22 | 23 | 30 | 31 | {% endfor %} 32 |
    {{ team.short_name }} 24 | 29 |
    33 | 34 |
    35 |
    36 | Has a room: ({{ have_teams.count }}) 37 | 38 | {% for team in have_teams %} 39 | 40 | 41 | 48 | 49 | {% endfor %} 50 |
    {{ team.short_name }} 42 | 47 |
    51 | 52 |
    53 |
    54 | Off Campus: ({{ offsite_teams.count }}) 55 | 56 | {% for team in offsite_teams %} 57 | 58 | 59 | 66 | 67 | {% endfor %} 68 |
    {{ team.short_name }} 60 | 65 |
    69 | 70 |
    71 |
    72 |
    73 |
    74 | {{ people|length }} People ({{ new_people|length }} new)

    75 | 76 | Allergies: 77 |
    78 | {% for person in people %} 79 | {% if person.allergies %} 80 | 81 | {{person.allergies}} 82 | 83 |
    84 | {% endif %} 85 | {% endfor %} 86 |
    87 |
    88 | 89 | 102 | {% endblock content %} 103 | -------------------------------------------------------------------------------- /huntserver/static/huntserver/hint.js: -------------------------------------------------------------------------------- 1 | jQuery(document).ready(function($) { 2 | function is_visible(){ 3 | var stateKey, keys = { 4 | hidden: "visibilitychange", 5 | webkitHidden: "webkitvisibilitychange", 6 | mozHidden: "mozvisibilitychange", 7 | msHidden: "msvisibilitychange" 8 | }; 9 | for (stateKey in keys) { 10 | if (stateKey in document) { 11 | return !document[stateKey]; 12 | } 13 | } 14 | return true; 15 | } 16 | 17 | var string_start = "You currently have "; 18 | var string_mid = " available hint"; 19 | var string_end =" for this hunt"; 20 | 21 | // get_posts is set to be called every 3 seconds. 22 | // Each time get_posts does not receive any data, the time till the next call 23 | // gets multiplied by 2.5 until a maximum of 120 seconds, reseting to 3 when 24 | // get_posts receives data. 25 | // TODO: reset to 3 seconds when the user sends an answer 26 | var ajax_delay = 3; 27 | var ajax_timeout; 28 | 29 | var get_posts = function() { 30 | if(is_visible()){ 31 | $.ajax({ 32 | type: 'get', 33 | url: puzzle_url, 34 | data: {last_date: last_date}, 35 | success: function (response) { 36 | var response = JSON.parse(response); 37 | messages = response.hint_list; 38 | if(messages.length > 0){ 39 | ajax_delay = 3; 40 | for (var i = 0; i < messages.length; i++) { 41 | receiveMessage(messages[i]); 42 | }; 43 | last_date = response.last_date; 44 | } 45 | else { 46 | ajax_delay = ajax_delay * 2.5; 47 | if(ajax_delay > 120){ 48 | ajax_delay = 120; 49 | } 50 | } 51 | $("#num_available_hints").html(string_start + response.num_available_hints + 52 | string_mid + ((response.num_available_hints == 1) ? '' : 's') + string_end); 53 | }, 54 | error: function (html) { 55 | console.log(html); 56 | ajax_delay = ajax_delay * 2.5; 57 | if(ajax_delay > 120){ 58 | ajax_delay = 120; 59 | } 60 | } 61 | }); 62 | } 63 | ajax_timeout = setTimeout(get_posts, ajax_delay*1000); 64 | } 65 | 66 | ajax_timeout = setTimeout(get_posts, ajax_delay*1000); 67 | 68 | 69 | $('#sub_form').on('submit', function(e) { 70 | e.preventDefault(); 71 | $.ajax({ 72 | url : $(this).attr('action') || window.location.pathname, 73 | type: "POST", 74 | data: $(this).serialize(), 75 | error: function (jXHR, textStatus, errorThrown) { 76 | console.log(jXHR.responseText); 77 | alert(errorThrown); 78 | }, 79 | success: function (response) { 80 | clearTimeout(ajax_timeout); 81 | ajax_delay = 3; 82 | ajax_timeout = setTimeout(get_posts, ajax_delay*1000); 83 | response = JSON.parse(response); 84 | receiveMessage(response.hint_list[0]); 85 | last_date = response.last_date; 86 | $("#num_available_hints").html(string_start + response.num_available_hints + 87 | string_mid + ((response.num_available_hints == 1) ? '' : 's') + string_end); 88 | } 89 | }); 90 | $('#id_request').val(''); 91 | }); 92 | 93 | // receive a message though the websocket from the server 94 | function receiveMessage(submission) { 95 | $("#no_hint_message").hide(); 96 | submission = $(submission); 97 | pk = submission.data('id'); 98 | if ($('tr[data-id=' + pk + ']').length == 0) { 99 | submission.prependTo("#hint_table"); 100 | } else { 101 | $('tr[data-id=' + pk + ']').replaceWith(submission); 102 | } 103 | } 104 | }); 105 | -------------------------------------------------------------------------------- /huntserver/templates/email.html: -------------------------------------------------------------------------------- 1 | {% extends "staff_base.html" %} 2 | {% load crispy_forms_tags %} 3 | {% block title %}Email{% endblock title %} 4 | 5 | {% block includes %} 6 | 18 | 19 | 72 | {% endblock includes %} 73 | 74 | {% block content %} 75 | 91 | 92 |

    Email

    93 |

    Send email to all hunt competitors:

    94 |
    95 | {% csrf_token %} 96 | {{ email_form|crispy }} 97 |
    98 | 99 |
    100 |
    101 | 102 |
    103 |
    104 | 105 | 106 | 107 |
    108 | {{email_list|safe}} 109 |
    110 | {% endblock content %} 111 | -------------------------------------------------------------------------------- /huntserver/templates/hint_row.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 22 | 23 | 24 | 25 | {% if staff_side %} 26 | {% if hint.response %} 27 | 28 | 34 | {% else %} 35 | 40 | 58 | {% endif %} 59 | {% else %} 60 | {% if hint.response %} 61 | 62 | 63 | {% else %} 64 | 65 | 66 | {% endif %} 67 | {% endif %} 68 | 69 |
    Request{{ hint.request_time|time:"h:i a" }} 8 | 18 |
    19 | {{ hint.request|linebreaksbr }} 20 |
    21 |
    Response{{ hint.response_time|time:"h:i a" }} 29 |
    {{ hint.response|linebreaksbr }}
    30 | 33 |
    36 | {% if hint.responder %} 37 | CLAIMED BY
    {{hint.responder.full_name}}
    38 | {% endif %} 39 |
    41 | {% if not hint.responder %} 42 |
    43 | 46 | {% elif hint.responder.user == request.user %} 47 |
    48 | 51 | {% else %} 52 |
    53 | 56 | {% endif %} 57 |
    {{ hint.response_time|time:"h:i a" }}{{ hint.response|linebreaksbr }}No response yet.
    70 | 71 | -------------------------------------------------------------------------------- /huntserver/static/js.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JavaScript Cookie v2.0.2 3 | * https://github.com/js-cookie/js-cookie 4 | * 5 | * Copyright 2006, 2015 Klaus Hartl 6 | * Released under the MIT license 7 | */ 8 | (function (factory) { 9 | if (typeof define === 'function' && define.amd) { 10 | define(factory); 11 | } else if (typeof exports === 'object') { 12 | module.exports = factory(); 13 | } else { 14 | var _OldCookies = window.Cookies; 15 | var api = window.Cookies = factory(window.jQuery); 16 | api.noConflict = function () { 17 | window.Cookies = _OldCookies; 18 | return api; 19 | }; 20 | } 21 | }(function () { 22 | function extend () { 23 | var i = 0; 24 | var result = {}; 25 | for (; i < arguments.length; i++) { 26 | var attributes = arguments[ i ]; 27 | for (var key in attributes) { 28 | result[key] = attributes[key]; 29 | } 30 | } 31 | return result; 32 | } 33 | 34 | function init (converter) { 35 | function api (key, value, attributes) { 36 | var result; 37 | 38 | // Write 39 | 40 | if (arguments.length > 1) { 41 | attributes = extend({ 42 | path: '/' 43 | }, api.defaults, attributes); 44 | 45 | if (typeof attributes.expires === 'number') { 46 | var expires = new Date(); 47 | expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5); 48 | attributes.expires = expires; 49 | } 50 | 51 | try { 52 | result = JSON.stringify(value); 53 | if (/^[\{\[]/.test(result)) { 54 | value = result; 55 | } 56 | } catch (e) {} 57 | 58 | value = encodeURIComponent(String(value)); 59 | value = value.replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); 60 | 61 | key = encodeURIComponent(String(key)); 62 | key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent); 63 | key = key.replace(/[\(\)]/g, escape); 64 | 65 | return (document.cookie = [ 66 | key, '=', value, 67 | attributes.expires && '; expires=' + attributes.expires.toUTCString(), // use expires attribute, max-age is not supported by IE 68 | attributes.path && '; path=' + attributes.path, 69 | attributes.domain && '; domain=' + attributes.domain, 70 | attributes.secure ? '; secure' : '' 71 | ].join('')); 72 | } 73 | 74 | // Read 75 | 76 | if (!key) { 77 | result = {}; 78 | } 79 | 80 | // To prevent the for loop in the first place assign an empty array 81 | // in case there are no cookies at all. Also prevents odd result when 82 | // calling "get()" 83 | var cookies = document.cookie ? document.cookie.split('; ') : []; 84 | var rdecode = /(%[0-9A-Z]{2})+/g; 85 | var i = 0; 86 | 87 | for (; i < cookies.length; i++) { 88 | var parts = cookies[i].split('='); 89 | var name = parts[0].replace(rdecode, decodeURIComponent); 90 | var cookie = parts.slice(1).join('='); 91 | 92 | if (cookie.charAt(0) === '"') { 93 | cookie = cookie.slice(1, -1); 94 | } 95 | 96 | cookie = converter && converter(cookie, name) || cookie.replace(rdecode, decodeURIComponent); 97 | 98 | if (this.json) { 99 | try { 100 | cookie = JSON.parse(cookie); 101 | } catch (e) {} 102 | } 103 | 104 | if (key === name) { 105 | result = cookie; 106 | break; 107 | } 108 | 109 | if (!key) { 110 | result[name] = cookie; 111 | } 112 | } 113 | 114 | return result; 115 | } 116 | 117 | api.get = api.set = api; 118 | api.getJSON = function () { 119 | return api.apply({ 120 | json: true 121 | }, [].slice.call(arguments)); 122 | }; 123 | api.defaults = {}; 124 | 125 | api.remove = function (key, attributes) { 126 | api(key, '', extend(attributes, { 127 | expires: -1 128 | })); 129 | }; 130 | 131 | api.withConverter = init; 132 | 133 | return api; 134 | } 135 | 136 | return init(); 137 | })); 138 | -------------------------------------------------------------------------------- /huntserver/static/huntserver/admin.css: -------------------------------------------------------------------------------- 1 | /* 2 | This file is for huntserver specific page styling. 3 | Contents do not get applied to included django admin pages 4 | */ 5 | 6 | /* Global */ 7 | 8 | button { 9 | background-color: white; 10 | border: 1px solid black; 11 | margin: 1px; 12 | text-align: center; 13 | text-decoration: none; 14 | border-radius: 4px; 15 | white-space: nowrap; 16 | display: inline-block;} 17 | 18 | 19 | table.table-bordered.team-list { 20 | width:400px; 21 | word-break: break-word; 22 | margin-top: 10px; 23 | margin-bottom: 5px; 24 | } 25 | 26 | table.table-bordered { 27 | border:1px solid black; 28 | margin-top:20px; 29 | } 30 | table.table-bordered > thead > tr > th { 31 | border:1px solid black; 32 | } 33 | table.table-bordered > tbody > tr > td { 34 | border:1px solid black; 35 | } 36 | table.table-bordered > thead > tr > td { 37 | border:1px solid black; 38 | } 39 | 40 | /* Queue */ 41 | #queue { 42 | width: 100%; } 43 | 44 | #queue tbody .success td { 45 | background-color: #c8ffb0; } 46 | 47 | #queue tbody .warning td { 48 | background-color: #f4f5b6; } 49 | 50 | #queue tbody .danger td { 51 | background-color: #ffc5c5; } 52 | 53 | 54 | /* Progress */ 55 | .actions { 56 | text-align: center; 57 | } 58 | 59 | table.table.table-condensed > thead > tr > td { 60 | vertical-align: middle; 61 | } 62 | table.table.table-condensed > tbody > tr > td { 63 | vertical-align: middle; 64 | } 65 | 66 | #table-container { 67 | overflow: scroll; 68 | height: calc(100vh - 180px); 69 | } 70 | 71 | .progress-thead>tr>td { 72 | padding: 3px; 73 | } 74 | 75 | .progress-thead>tr>th { 76 | background-color: whitesmoke; 77 | text-align: center; 78 | position: -webkit-sticky; /* for Safari */ 79 | position: sticky; 80 | top: 0; 81 | box-shadow: black 1px 0px 0 1px 82 | } 83 | 84 | .leftmost { 85 | background-color: whitesmoke; 86 | position: -webkit-sticky; /* for Safari */ 87 | position: sticky; 88 | left: 0; 89 | box-shadow: black 0px 1px 0 1px; 90 | } 91 | 92 | .leftmost.topmost { 93 | z-index: 10; 94 | } 95 | 96 | #progress { 97 | font-size: 80%; } 98 | 99 | #progress .solved { 100 | background: hsla(128, 100%, 75%, 1);; } 101 | 102 | #progress .available { 103 | background: hsla(55, 100%, 75%, 1); } 104 | 105 | .sort_select { 106 | display: inline; 107 | width: 150px; 108 | margin-bottom: 5px; 109 | -webkit-appearance: menulist; 110 | } 111 | 112 | 113 | .sort_label { 114 | margin-left: 20px; 115 | } 116 | 117 | select { 118 | -webkit-appearance: menulist; 119 | } 120 | 121 | /* Management */ 122 | .list-group.list-group-root { 123 | padding: 0; 124 | overflow: hidden; 125 | } 126 | 127 | .list-group.list-group-root .list-group { 128 | margin-bottom: 0; 129 | } 130 | 131 | .list-group.list-group-root .list-group-item { 132 | border-radius: 0; 133 | border-width: 1px 0 0 0; 134 | } 135 | 136 | .list-group.list-group-root > .list-group-item:first-child { 137 | border-top-width: 0; 138 | } 139 | 140 | .list-group.list-group-root > .list-group > .list-group-item { 141 | padding-left: 45px; 142 | } 143 | 144 | .list-group-item .glyphicon { 145 | margin-right: 5px; 146 | } 147 | 148 | .list-group-item button .glyphicon { 149 | margin-right: 0px; 150 | } 151 | 152 | .download-btn { 153 | padding: 3px 12px 1px 12px; 154 | margin-top: -2px; 155 | } 156 | 157 | .vertical-alignment-helper { 158 | display:table; 159 | height: 100%; 160 | width: 100%; 161 | pointer-events:none; 162 | } 163 | .vertical-align-center { 164 | /* To center vertically */ 165 | display: table-cell; 166 | vertical-align: middle; 167 | pointer-events:none; 168 | } 169 | .modal-content { 170 | width:inherit; 171 | max-width:inherit; 172 | height:inherit; 173 | margin: 0 auto; 174 | pointer-events: all; 175 | } 176 | .claimed table.table { 177 | background-color: lightpink; 178 | } 179 | 180 | .answered table.table { 181 | background-color: lightgrey; 182 | } -------------------------------------------------------------------------------- /docs/setup.rst: -------------------------------------------------------------------------------- 1 | Setup 2 | ***** 3 | 4 | Instructions on how to setup a machine to run this project. 5 | 6 | Basic Setup Instructions 7 | ======================== 8 | 9 | This project now uses docker-compose as it's main form of setup. You can use the 10 | following steps to get a sample server up and going 11 | 12 | 1. Install [docker/docker-compose.](https://docs.docker.com/compose/install/) 13 | 2. Clone this repository. 14 | 3. Make a copy of ``sample.env`` named ``.env`` (yes, it starts with a dot). 15 | 4. Edit the new ``.env`` file, filling in new values for the first block of 16 | uncommented lines. Other lines can be safely ignored as they only provide 17 | additional functionality. 18 | 5. Run ``docker-compose up`` (possibly using ``sudo`` if needed) 19 | 6. Once up, you'll need to run the following commands to collect all the static 20 | files (to be run any time after you alter the static files), to load in an 21 | initial hunt to pacify some of the display logic (to be run only once), and 22 | to create a new admin user (follow the prompts). 23 | 24 | .. code-block:: bash 25 | 26 | docker-compose exec app python3 /code/manage.py collectstatic --noinput 27 | docker-compose exec app python3 /code/manage.py loaddata initial_hunt 28 | docker-compose exec app python3 /code/manage.py createsuperuser 29 | 30 | 7. You should now have the server running on a newly created VM, accessible via 31 | (http://localhost). The repository you cloned has been 32 | linked into the VM by docker, so any changes made to the repository on the 33 | host system should show up automatically. (A ``docker-compose restart`` may 34 | be needed for some changes to take effect) 35 | 36 | Setup details 37 | ------------- 38 | 39 | The basic instructions above bring up the following docker containers: 40 | 41 | - db 42 | The postgres database with the settings specified in the .env file. Data 43 | is retained across container restarts in ``docker/volumes/redis_data``. 44 | - redis 45 | A redis server for caching and task management. Data is stored in 46 | ``docker/volumes/redis_data``. 47 | - app 48 | The Django application running using gunicorn on port 8000. 49 | - huey 50 | A Huey consumer for scheduled tasks. 51 | - web 52 | An apache server to proxy web requests to the "app" container and serve 53 | the static files. By default, this container serves web requests using plain 54 | HTTP over port 80. See the "Extra Setup Instructions" for details on 55 | setting up SSL. 56 | 57 | .. Note:: 58 | There are also 2 volumes shared by a number of the containers that hold 59 | static files and media files and will persist across docker restarts. 60 | 61 | Extra Setup Instructions 62 | ======================== 63 | 64 | In addition to the basic instructions above, there are a few additional setup 65 | options available. These additional options are provided via "override files" 66 | that override various parts of the docker compose logic. You can enable which 67 | override files are being used by setting the ``COMPOSE_FILE`` variable in the 68 | ``.env`` file. By default only the ``local_override.yml`` file is enabled. 69 | 70 | local_override 71 | -------------- 72 | 73 | By default, the "web" docker container only "exposes" port 80. The local 74 | override file takes things one step further and maps the host port 80 to the 75 | web container port 80. This is done via an override because docker compose 76 | doesn't support unmapping ports and the proxy_override settings need to map 77 | the reverse proxy to host port 80. 78 | 79 | shib_override 80 | ------------- 81 | 82 | Enabling this override sets up shibboleth authentication on the apache server. 83 | To use pre-existing shibboleth certificates, place sp-cert.pem and sp-key.pem 84 | in ``docker/volumes/shib-certs``. This override file 85 | also uses LetsEncrypt to get a certificate for the site using the DOMAIN 86 | and CONTACT_EMAIL settings from the ``.env`` file. SSL certs are stored in 87 | ``docker/volumes/ssl-certs``. Right now this is the only override that provides 88 | SSL capabilities. In the future there will likely be an SSL_override file that 89 | breaks out the LetsEncrypt functionality. 90 | 91 | proxy_override 92 | -------------- 93 | 94 | Enabling this override file sets up a reverse proxy using Traefik. This 95 | functionality is in development and mostly untested. It currently only works 96 | with shib_override. It also requires an already created docker network named 97 | ``proxy-net`` -------------------------------------------------------------------------------- /huntserver/templates/progress.html: -------------------------------------------------------------------------------- 1 | {% extends "staff_base.html" %} 2 | {% block title %}Puzzle Progress{% endblock title %} 3 | 4 | {% block includes %} 5 | 6 | 11 | 12 | {% endblock includes %} 13 | 14 | {% block content %} 15 |

    Puzzle Progress

    16 | 17 | 18 | 19 | 20 | 26 | 27 | 33 | 34 | 40 | 41 |
    42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {% for puzzle in puzzle_list %} 50 | 60 | {% endfor %} 61 | 62 | 63 | 64 | 65 | 66 | 67 | {% for puzzle in puzzle_list %} 68 | 74 | {% endfor %} 75 | 76 | 77 | 78 | {% for team_dict in sol_list %} 79 | 80 | 83 | 84 | 85 | 86 | {% for puzzle in team_dict.puzzles %} 87 | 98 | {% endfor %} 99 | 100 | {% endfor %} 101 | 102 |
    All teams: 51 |
    52 | {% csrf_token %} 53 | 54 | 55 | 58 |
    59 |
    Team#
    M
    #
    P
    Last Time 72 | {{ puzzle.puzzle_name }} 73 |
    81 | {{ team_dict.team.name|truncatechars:40 }} 82 | 90 | {% elif puzzle.1 == "unlocked" %} 91 | class='available' data-date={{ puzzle.2 |date:"U"}}> 92 | {{ puzzle.3|date:"m/d" }}
    {{ puzzle.3|time:"h:i A" }}
    93 | {% else %} 94 | class='solved' data-date={{ puzzle.2 |date:"U"}}> 95 | {{ puzzle.2|date:"m/d" }}
    {{ puzzle.2|time:"h:i A" }} 96 | {% endif %} 97 |
    103 |
    104 | {% endblock content %} 105 | --------------------------------------------------------------------------------