From b4d0765b3c008e62733382522b9f1a9a0f0301a9 Mon Sep 17 00:00:00 2001 From: Jason Cameron Date: Sat, 2 Dec 2023 15:56:34 -0500 Subject: [PATCH] Added Hunt model to have continuity throughout hunts. Style: Format --- .idea/misc.xml | 4 +- .idea/workspace.xml | 58 ++++--- core/admin.py | 22 ++- core/migrations/0001_initial.py | 49 ++++-- .../0004_qrcode_short_alter_invite_code.py | 4 +- ...e_code_alter_qrcode_key_alter_team_name.py | 4 +- core/migrations/0009_team_solo.py | 5 +- ...0013_logicpuzzlehint_alter_qrcode_notes.py | 13 +- ...unt_logicpuzzlehint_belongs_to_and_more.py | 153 ++++++++++++++++++ core/models.py | 75 +++++++-- core/templates/core/credits.html | 12 +- core/templates/core/qr.html | 3 +- core/templatetags/qr.py | 14 ++ core/views/auth.py | 6 +- core/views/index.py | 4 +- core/views/qr.py | 3 +- core/views/team.py | 3 +- 17 files changed, 365 insertions(+), 67 deletions(-) create mode 100644 core/migrations/0019_alter_qrcode_key_hunt_logicpuzzlehint_belongs_to_and_more.py diff --git a/.idea/misc.xml b/.idea/misc.xml index 995cec4..b1ea2f1 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - - \ No newline at end of file + + diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 92f74f9..8a03b4c 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,10 +4,10 @@ - + - + - { + "keyToString": { + "RunOnceActivity.OpenProjectViewOnStart": "true", + "RunOnceActivity.ShowReadmeOnStart": "true", + "SHARE_PROJECT_CONFIGURATION_FILES": "true", + "WebServerToolWindowFactoryState": "true", + "WebServerToolWindowPanel.toolwindow.highlight.mappings": "true", + "WebServerToolWindowPanel.toolwindow.highlight.symlinks": "true", + "WebServerToolWindowPanel.toolwindow.show.date": "false", + "WebServerToolWindowPanel.toolwindow.show.permissions": "false", + "WebServerToolWindowPanel.toolwindow.show.size": "false", + "git-widget-placeholder": "main", + "ignore.virus.scanning.warn.message": "true", + "last_opened_file_path": "C:/Users/jason/projects/metro/venv", + "node.js.detected.package.eslint": "true", + "node.js.detected.package.tslint": "true", + "node.js.selected.package.eslint": "(autodetect)", + "node.js.selected.package.tslint": "(autodetect)", + "nodejs_package_manager_path": "npm", + "settings.editor.selected.configurable": "settings.sync", + "vue.rearranger.settings.migration": "true" } -}]]> +} @@ -153,6 +160,10 @@ + + + + - @@ -300,7 +319,8 @@ - diff --git a/core/admin.py b/core/admin.py index ba09d98..1656957 100644 --- a/core/admin.py +++ b/core/admin.py @@ -8,6 +8,7 @@ from .forms import * + @admin.action( permissions=["change"], description=_("Set selected users as a Location Setter"), @@ -77,12 +78,22 @@ class TeamAdmin(admin.ModelAdmin): @admin.display(description="Path") def path(self, team): return "\n".join( - map(lambda pk: str(QrCode.objects.get(id=pk)), QrCode.code_pks(team)) + map( + lambda pk: str(QrCode.objects.get(id=pk)), + QrCode.code_pks(team), + ) ) class QrCodeAdmin(admin.ModelAdmin): - fields = ["short", "location", "notes", "key", "image_tag", "image_url"] + fields = [ + "short", + "location", + "notes", + "key", + "image_tag", + "image_url", + ] readonly_fields = ["url", "key", "image_tag"] list_display = ["location", "url"] inlines = [HintsInLine] @@ -115,7 +126,10 @@ class UserAdmin(UserAdmin_): fieldsets = tuple( admin_field + [ - ("Metropolis Integration (OAuth)", dict(fields=["metropolis_id"])), + ( + "Metropolis Integration (OAuth)", + dict(fields=["metropolis_id"]), + ), ("Game", dict(fields=["team"])), ] ) @@ -125,3 +139,5 @@ class UserAdmin(UserAdmin_): admin.site.register(Team, TeamAdmin) admin.site.register(QrCode, QrCodeAdmin) admin.site.register(LogicPuzzleHint, LogicPuzzleAdmin) +admin.site.register(Hunt) + diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py index b3d4039..44cd690 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -28,11 +28,16 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "password", + models.CharField(max_length=128, verbose_name="password"), + ), ( "last_login", models.DateTimeField( - blank=True, null=True, verbose_name="last login" + blank=True, + null=True, + verbose_name="last login", ), ), ( @@ -61,19 +66,25 @@ class Migration(migrations.Migration): ( "first_name", models.CharField( - blank=True, max_length=150, verbose_name="first name" + blank=True, + max_length=150, + verbose_name="first name", ), ), ( "last_name", models.CharField( - blank=True, max_length=150, verbose_name="last name" + blank=True, + max_length=150, + verbose_name="last name", ), ), ( "email", models.EmailField( - blank=True, max_length=254, verbose_name="email address" + blank=True, + max_length=254, + verbose_name="email address", ), ), ( @@ -95,7 +106,8 @@ class Migration(migrations.Migration): ( "date_joined", models.DateTimeField( - default=django.utils.timezone.now, verbose_name="date joined" + default=django.utils.timezone.now, + verbose_name="date joined", ), ), ("metropolis_id", models.IntegerField()), @@ -135,7 +147,10 @@ class Migration(migrations.Migration): migrations.CreateModel( name="QrCode", fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), + ( + "id", + models.AutoField(primary_key=True, serialize=False), + ), ( "location", models.CharField( @@ -152,21 +167,30 @@ class Migration(migrations.Migration): migrations.CreateModel( name="Team", fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), + ( + "id", + models.AutoField(primary_key=True, serialize=False), + ), ("name", models.CharField(max_length=64, unique=True)), ("is_active", models.BooleanField(default=True)), ("is_open", models.BooleanField(default=False)), - ("current_qr_code", models.IntegerField(blank=True, null=True)), + ( + "current_qr_code", + models.IntegerField(blank=True, null=True), + ), ( "completed_qr_codes", models.ManyToManyField( - blank=True, related_name="completed_qr_codes", to="core.qrcode" + blank=True, + related_name="completed_qr_codes", + to="core.qrcode", ), ), ( "members", models.ManyToManyField( - related_name="team", to=settings.AUTH_USER_MODEL + related_name="team", + to=settings.AUTH_USER_MODEL, ), ), ], @@ -188,7 +212,8 @@ class Migration(migrations.Migration): ( "team", models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="core.team" + on_delete=django.db.models.deletion.CASCADE, + to="core.team", ), ), ], diff --git a/core/migrations/0004_qrcode_short_alter_invite_code.py b/core/migrations/0004_qrcode_short_alter_invite_code.py index 9160175..2b0db47 100644 --- a/core/migrations/0004_qrcode_short_alter_invite_code.py +++ b/core/migrations/0004_qrcode_short_alter_invite_code.py @@ -24,7 +24,9 @@ class Migration(migrations.Migration): model_name="invite", name="code", field=models.CharField( - default=core.models.generate_invite_code, max_length=32, unique=True + default=core.models.generate_invite_code, + max_length=32, + unique=True, ), ), ] diff --git a/core/migrations/0008_alter_invite_code_alter_qrcode_key_alter_team_name.py b/core/migrations/0008_alter_invite_code_alter_qrcode_key_alter_team_name.py index 07094ea..f62d793 100644 --- a/core/migrations/0008_alter_invite_code_alter_qrcode_key_alter_team_name.py +++ b/core/migrations/0008_alter_invite_code_alter_qrcode_key_alter_team_name.py @@ -19,7 +19,9 @@ class Migration(migrations.Migration): model_name="qrcode", name="key", field=models.CharField( - default=core.models.generate_hint_key, max_length=64, unique=True + default=core.models.generate_hint_key, + max_length=64, + unique=True, ), ), migrations.AlterField( diff --git a/core/migrations/0009_team_solo.py b/core/migrations/0009_team_solo.py index aeaaece..1f947d2 100644 --- a/core/migrations/0009_team_solo.py +++ b/core/migrations/0009_team_solo.py @@ -5,7 +5,10 @@ class Migration(migrations.Migration): dependencies = [ - ("core", "0008_alter_invite_code_alter_qrcode_key_alter_team_name"), + ( + "core", + "0008_alter_invite_code_alter_qrcode_key_alter_team_name", + ), ] operations = [ diff --git a/core/migrations/0013_logicpuzzlehint_alter_qrcode_notes.py b/core/migrations/0013_logicpuzzlehint_alter_qrcode_notes.py index 7ca9dc2..95f8b20 100644 --- a/core/migrations/0013_logicpuzzlehint_alter_qrcode_notes.py +++ b/core/migrations/0013_logicpuzzlehint_alter_qrcode_notes.py @@ -12,14 +12,21 @@ class Migration(migrations.Migration): migrations.CreateModel( name="LogicPuzzleHint", fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), + ( + "id", + models.AutoField(primary_key=True, serialize=False), + ), ( "hint", models.TextField( - help_text="Hint for the logic puzzle", max_length=1024 + help_text="Hint for the logic puzzle", + max_length=1024, ), ), - ("notes", models.TextField(blank=True, help_text="Internal notes")), + ( + "notes", + models.TextField(blank=True, help_text="Internal notes"), + ), ( "qr_index", models.IntegerField( diff --git a/core/migrations/0019_alter_qrcode_key_hunt_logicpuzzlehint_belongs_to_and_more.py b/core/migrations/0019_alter_qrcode_key_hunt_logicpuzzlehint_belongs_to_and_more.py new file mode 100644 index 0000000..769f895 --- /dev/null +++ b/core/migrations/0019_alter_qrcode_key_hunt_logicpuzzlehint_belongs_to_and_more.py @@ -0,0 +1,153 @@ +# Generated by Django 4.1.13 on 2023-12-02 20:46 +from django.utils import timezone + +import core.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + +def create_hunt(apps, schema_editor): + # Create a Hunt object if needed + from core.models import Hunt, QrCode + + if QrCode.objects.count() == 0: + return + # COULD ERROR IF ONLY ONE QR OBJ EXISTS + Hunt.objects.get_or_create(name="First Hunt", start=timezone.now() - timezone.timedelta(days=2), end=timezone.now(), + starting_location=QrCode.objects.first(), ending_location=QrCode.objects.last(), + form_url="https://forms.gle/eAghxHxWRiXWgeKF9", + ending_text="You found Derek! Now, solve the entirety of the logic puzzle to find which of the 5 SAC members in question this locker belongs to {{here}}! Oh, and close the locker, a dead Derek smells bad.") + +def undo_create_hunt(apps, schema_editor): + # Create a Hunt object if needed + from core.models import Hunt + try: + Hunt.objects.get(name="First Hunt").delete() + except Hunt.DoesNotExist: + pass + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0018_qrcode_image_url"), + ] + + operations = [ + migrations.AlterField( + model_name="qrcode", + name="key", + field=models.CharField( + default=core.models.generate_hint_key, + help_text="Key to access the hint, used in the QR code ", + max_length=64, + unique=True, + ), + ), + migrations.CreateModel( + name="Hunt", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("name", models.CharField(max_length=64)), + ("start", models.DateTimeField()), + ("end", models.DateTimeField()), + ( + "team_size", + models.PositiveSmallIntegerField( + default=4, help_text="Max Team size" + ), + ), + ( + "path_length", + models.PositiveSmallIntegerField( + default=15, + help_text="Length of the path: The amount of codes each time will have to find before the end.", + ), + ), + ( + "form_url", + models.URLField( + blank=True, + help_text="Google form to fill out after the hunt", + null=True, + ), + ), + ( + "ending_text", + models.TextField( + help_text="Text to display after the hunt is over. If you want to include a url (e.g. a google form) at the end use text inside of double curly brackets {{ }} to show where the form will go. The text inside the brackets is what will be shown to the user. e.g. {{this form}}, users will only see 'this form' but can click it to get to the form specified above", + max_length=250, + ), + ), + ( + "early_access_users", + models.ManyToManyField( + help_text="Users that can access this hunt before it starts", + related_name="early_access_users", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "ending_location", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="ending_location", + to="core.qrcode", + ), + ), + ( + "starting_location", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="starting_location", + to="core.qrcode", + ), + ), + ], + ), + migrations.RunPython(create_hunt, reverse_code=undo_create_hunt), + migrations.AddField( + model_name="logicpuzzlehint", + name="belongs_to", + field=models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.CASCADE, + related_name="logic_puzzle", + to="core.hunt", + ), + preserve_default=False, + ), + migrations.AddField( + model_name="team", + name="hunt", + field=models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.CASCADE, + related_name="teams", + to="core.hunt", + ), + preserve_default=False, + ), + migrations.AddConstraint( + model_name="hunt", + constraint=models.CheckConstraint( + check=models.Q(("start__lt", models.F("end"))), name="start_before_end" + ), + ), + migrations.AddConstraint( + model_name="hunt", + constraint=models.CheckConstraint( + check=models.Q( + ("starting_location", models.F("ending_location")), _negated=True + ), + name="start_not_equal_end", + ), + ), + migrations.AddConstraint( + model_name="hunt", + constraint=models.CheckConstraint( + check=models.Q( + ("ending_text__contains", "{{"), ("ending_text__contains", "}}") + ), + name="form_in_ending_text", + ), + ), + ] diff --git a/core/models.py b/core/models.py index b64e45b..25aa13a 100644 --- a/core/models.py +++ b/core/models.py @@ -17,7 +17,11 @@ class User(AbstractUser): metropolis_id = models.IntegerField() refresh_token = models.CharField(max_length=128) team = models.ForeignKey( - "Team", related_name="members", on_delete=models.SET_NULL, blank=True, null=True + "Team", + related_name="members", + on_delete=models.SET_NULL, + blank=True, + null=True, ) @property @@ -125,6 +129,7 @@ class Team(models.Model): ) # todo use this field to have a club-like page so you can join an open team (future feature) current_qr_i = models.IntegerField(default=0) solo = models.BooleanField(default=False) + hunt = models.ForeignKey("Hunt", on_delete=models.CASCADE, related_name="teams") def update_current_qr_i(self, i: int): self.current_qr_i = max(self.current_qr_i, i) @@ -170,17 +175,59 @@ class Invite(models.Model): code = models.CharField(max_length=32, unique=True) -# class Hunt(models.Model): -# id = models.AutoField(primary_key=True) -# name = models.CharField(max_length=64) -# start = models.DateTimeField() -# end = models.DateTimeField() -# is_active = models.BooleanField(default=False) -# team_size = models.IntegerField(default=4, help_text="Max Team size") -# final_qr_id = models.IntegerField(null=True, blank=True) -# -# def __str__(self): -# return self.name +class Hunt(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=64) + start = models.DateTimeField() + end = models.DateTimeField() + team_size = models.PositiveSmallIntegerField(default=4, help_text="Max Team size") + path_length = models.PositiveSmallIntegerField(default=15, help_text="Length of the path: The amount of codes each time will have to find before the end.") + starting_location = models.ForeignKey( + QrCode, on_delete=models.PROTECT, related_name="starting_location" + ) + ending_location = models.ForeignKey( + QrCode, on_delete=models.PROTECT, related_name="ending_location" + ) + early_access_users = models.ManyToManyField( + User, + related_name="early_access_users", + help_text="Users that can access this hunt before it starts", + ) + form_url = models.URLField(help_text="Google form to fill out after the hunt", null=True, blank=True) + ending_text = models.TextField( + help_text="Text to display after the hunt is over. If you want to include a url (e.g. a google form) at the end use text inside of double curly brackets {{ }} to show where the form will go. " + "The text inside the brackets is what will be shown to the user. " + "e.g. {{this form}}, users will only see 'this form' but can click it to get to the form specified above", + max_length=250, + ) + + def __str__(self): + return self.name + + class Meta: + constraints = [ + models.CheckConstraint( + check=models.Q(start__lt=models.F("end")), + name="start_before_end", + ), + # starting location cannot be the same as ending + models.CheckConstraint( + check=~models.Q(starting_location=models.F("ending_location")), + name="start_not_equal_end", + ), + # make sure ending_text contains {{ }} for the form + models.CheckConstraint( + check=models.Q(ending_text__contains="{{") & models.Q( + ending_text__contains="}}" + ), + name="form_in_ending_text", + ), + # Ensure there isn't a different hunt running in that timespan + models.CheckConstraint( + check=models.Q(start__gt=models.F("end")), + name="no_overlapping_hunts", # todo check if this works + ), + ] class LogicPuzzleHint(models.Model): @@ -195,7 +242,9 @@ class LogicPuzzleHint(models.Model): unique=True, ) - # belongs_to = models.ForeignKey(Hunt, related_name="logic_puzzle_hunt", on_delete=models.CASCADE) + belongs_to = models.ForeignKey( + Hunt, related_name="logic_puzzle", on_delete=models.CASCADE + ) def __str__(self): return str(self.hint) diff --git a/core/templates/core/credits.html b/core/templates/core/credits.html index 4167623..2e9c08b 100644 --- a/core/templates/core/credits.html +++ b/core/templates/core/credits.html @@ -18,12 +18,12 @@

{% translate 'project manager' %}

{% translate 'programming' %}

    -
  • nyiyui
  • -
  • Jason Cameron
  • -
  • Jimmy Liu
  • -
  • Glen Lin
  • -
  • Joshua Wang
  • -
  • Chelsea Wong
  • +
  • Jason Cameron
  • +
  • nyiyui
  • +
  • Jimmy Liu
  • +
  • Glen Lin
  • +
  • Joshua Wang
  • +
  • Chelsea Wong

{% translate 'content' %}

diff --git a/core/templates/core/qr.html b/core/templates/core/qr.html index e85c1d9..55d4dac 100644 --- a/core/templates/core/qr.html +++ b/core/templates/core/qr.html @@ -52,7 +52,7 @@ {% if nextqr %} {% hint nextqr request.user.team as hint %} - {{ hint |mistune }} + {{ hint|mistune }} {% if request.user.is_superuser %}
({{ nextqr }} change) {% endif %} @@ -65,6 +65,7 @@ {% endif %} {% else %} + {% ending_block qr.hunt %} You found Derek! Now, solve the entirety of the logic puzzle to find which of the 5 SAC members in question this locker belongs to here! Oh, and close the locker, a dead Derek smells bad. {% translate 'Oh ho? What lieth there? Tis the light at the end of the tunnel!
Congratulations valiant scavenger and thank you for playing!' %} {% endif %} diff --git a/core/templatetags/qr.py b/core/templatetags/qr.py index 24cb00c..ee5bdb8 100644 --- a/core/templatetags/qr.py +++ b/core/templatetags/qr.py @@ -1,3 +1,5 @@ +import re + from django import template from django.urls import reverse from django.utils.html import format_html @@ -13,3 +15,15 @@ def hint(qr, team): @register.simple_tag def join_url(code): return format_html("%s%s" % (reverse("join"), f"?code={code}")) + + +@register.simple_tag +def ending_block(hunt): + pattern = r"{{(.*?)}}" + match = re.search(pattern, hunt.ending_text) + + if match: + ending_text = match.group(1).strip() + return format_html( + hunt.ending_text.replace(match.group(0), '{}'.format(hunt.ending_form, ending_text))) + return hunt.ending_text diff --git a/core/views/auth.py b/core/views/auth.py index ac3f64b..a607051 100644 --- a/core/views/auth.py +++ b/core/views/auth.py @@ -31,7 +31,8 @@ def pkce1(q): code_challenge = code_challenge.rstrip("=") code_challenge_method = "S256" return dict( - code_challenge=code_challenge, code_challenge_method=code_challenge_method + code_challenge=code_challenge, + code_challenge_method=code_challenge_method, ) @@ -94,7 +95,8 @@ def oauth_auth(q): access_token = s2d["access_token"] refresh_token = s2d["refresh_token"] q3 = requests.get( - settings.YASOI["me_url"], headers={"Authorization": f"Bearer {access_token}"} + settings.YASOI["me_url"], + headers={"Authorization": f"Bearer {access_token}"}, ) q3.raise_for_status() s3d = q3.json() diff --git a/core/views/index.py b/core/views/index.py index 065b8bd..db5a8f2 100644 --- a/core/views/index.py +++ b/core/views/index.py @@ -9,7 +9,9 @@ @require_http_methods(["GET"]) def index(q): return render( - q, "core/index.html" if q.user.is_authenticated else "core/gate.html", {} + q, + "core/index.html" if q.user.is_authenticated else "core/gate.html", + {}, ) diff --git a/core/views/qr.py b/core/views/qr.py index 5f500cb..88a0546 100644 --- a/core/views/qr.py +++ b/core/views/qr.py @@ -38,7 +38,7 @@ def after_start(f): def wrapped(*args, **kwargs): request = args[0] if ( - not request.user.has_perm("core.view_before_start") + not request.user.has_perm("core.view_before_start")# or current_hunt. todo impl and settings.START > datetime.datetime.now() ): messages.error( @@ -58,6 +58,7 @@ def wrapped(*args, **kwargs): def qr(request, key): context = dict(first=False) context["qr"] = qr = get_object_or_404(QrCode, key=key) + context["hunt"] = qr.hunt codes = QrCode.code_pks(request.user.team) if qr.id not in codes: context["offpath"] = True diff --git a/core/views/team.py b/core/views/team.py index 9b45d66..62dfc9c 100644 --- a/core/views/team.py +++ b/core/views/team.py @@ -37,7 +37,8 @@ def join(request): invite.invites += 1 invite.save() messages.success( - request, _("Joined team %(team_name)s") % dict(team_name=team.name) + request, + _("Joined team %(team_name)s") % dict(team_name=team.name), ) return redirect("/") else: