-
-
-
-
The Secret Token game lasts the whole event and is about finding little text strings matching the regular expression:
- [0-9a-zA-Z\.@]{12,32}
- Tokens are hidden or in plain sight physically or virtually on the BornHack venue, online and offline.
-
If you think you found a secret token you can register it by visiting https://bornhack.dk/token/TOKEN where TOKEN is replaced by the token you found.
-
This page shows an overview of the tokens in this years game, a hint for each token, and how many of them you have found. Here is your first token to start the hunt: "HelloTokenHunters2024"
-
-
-
- {% for token in object_list %}
- {% ifchanged token.camp %}
-
- {{ token.camp.title }} |
-
-
- | Category |
- Time Limits |
- Token |
- Description |
- Found |
-
- {% endifchanged %}
-
- | {{ token.category }} |
-
- {% if token.valid_now %}
-
- {% else %}
-
- {% endif %}
-
- {% if token.valid_when.lower %}
- Not valid before {{ token.valid_when.lower }}
- {% endif %}
- {% if token.valid_when.upper %}
- Not valid after {{ token.valid_when.upper }}
- {% endif %}
- {% if not token.valid_when %}
- No time limit
- {% endif %}
- |
-
- {% with token|found_by_user:user as user_has_found_token %}
- {% if user_has_found_token %}
- {{ token.token }} |
- {{ token.description }} |
- {{ user_has_found_token }} |
- {% else %}
- - |
- - |
- - |
- {% endif %}
- {% endwith %}
-
-
- {% endfor %}
-
-
-
-{% endblock %}
diff --git a/src/tokens/tests/test_models.py b/src/tokens/tests/test_models.py
new file mode 100644
index 000000000..3f73bd43a
--- /dev/null
+++ b/src/tokens/tests/test_models.py
@@ -0,0 +1,61 @@
+from __future__ import annotations
+
+from django.core.exceptions import ValidationError
+
+from tokens.models import Token
+from tokens.models import TokenCategory
+from utils.tests import BornhackTestBase
+
+
+class TestTokenModel(BornhackTestBase):
+ """Test Token model."""
+
+ @classmethod
+ def setUpTestData(cls) -> None:
+ """Add test data."""
+ # first add users and other basics
+ super().setUpTestData()
+
+ cls.category = TokenCategory.objects.create(
+ name="Test",
+ description="Test category",
+ )
+
+ def create_token(self, token_str: str) -> Token:
+ """Helper method for creating tokens"""
+ return Token.objects.create(
+ token=token_str,
+ camp=self.camp,
+ category=self.category,
+ hint="Test token hint",
+ description="Test token description",
+ active=True,
+ )
+
+ def test_token_regex_validator_min_length(self) -> None:
+ """Test minimum length of token, with regex validator raising exception."""
+ with self.assertRaises(ValidationError):
+ self.create_token("minlength")
+
+ def test_token_regex_validator_max_length(self) -> None:
+ """Test maximum length of token, with regex validator raising exception."""
+ with self.assertRaises(ValidationError):
+ self.create_token("maxLengthOf32CharactersTokenTests")
+
+ def test_token_regex_validator_invalid_char(self) -> None:
+ """Test invalid characters in token, with regex validator raising exception."""
+ with self.assertRaises(ValidationError):
+ self.create_token("useOfInvalidCharacter+")
+
+ with self.assertRaises(ValidationError):
+ self.create_token("-useOfInvalidCharacter")
+
+ def test_creating_valid_tokens(self) -> None:
+ """Test valid tokens, with regex validator."""
+ self.assertIsInstance(self.create_token("validTokenWith@"), Token)
+ self.assertIsInstance(self.create_token("validTokenWith."), Token)
+ self.assertIsInstance(self.create_token("validToken12"), Token)
+ self.assertIsInstance(
+ self.create_token("maxLengthOf32CharactersTokenTest"),
+ Token,
+ )
diff --git a/src/tokens/tests/test_views.py b/src/tokens/tests/test_views.py
index 508b31091..0dc48f900 100644
--- a/src/tokens/tests/test_views.py
+++ b/src/tokens/tests/test_views.py
@@ -9,6 +9,7 @@
from django.urls import reverse
from tokens.models import Token
+from tokens.models import TokenCategory
from utils.tests import BornhackTestBase
@@ -32,12 +33,27 @@ def setUpTestData(cls) -> None:
now = datetime.now(tz)
year = now.year
+ kwargs = {"camp_slug": cls.camp.slug}
+ cls.url_dashboard = reverse(
+ "tokens:dashboard",
+ kwargs=kwargs,
+ )
+ cls.url_submit = reverse(
+ "tokens:submit",
+ kwargs=kwargs,
+ )
+
+ # create test category
+ cls.category = TokenCategory(name="Test", description="Test category")
+ cls.category.save()
+
# then create some tokens
cls.token = Token(
camp=cls.camp,
token="DEADBEAF1234",
description="Test token",
- category="Test",
+ hint="Token1",
+ category=cls.category,
active=True,
)
cls.token.save()
@@ -46,7 +62,8 @@ def setUpTestData(cls) -> None:
camp=cls.camp,
token="1234DEADBEAF",
description="Test Inactive token",
- category="Test",
+ hint="Token2",
+ category=cls.category,
active=False,
)
cls.token_inactive.save()
@@ -55,7 +72,8 @@ def setUpTestData(cls) -> None:
camp=cls.camp,
token="12341337F01F",
description="Test timed token",
- category="Test",
+ hint="Token3",
+ category=cls.category,
active=True,
valid_when=["2000-01-01 00:00:00", "2000-10-10 10:10:10"],
)
@@ -65,7 +83,8 @@ def setUpTestData(cls) -> None:
camp=cls.camp,
token="12341337F02F",
description="Test timed token",
- category="Test",
+ hint="Token4",
+ category=cls.category,
active=True,
valid_when=[now, datetime(year + 1, 1, 1, 1, 1, tzinfo=tz)],
)
@@ -75,89 +94,97 @@ def setUpTestData(cls) -> None:
camp=cls.camp,
token="12341337F03F",
description="Test timed token",
- category="Test",
+ hint="Token5",
+ category=cls.category,
active=True,
valid_when=[datetime(year + 1, 1, 1, 1, 1, tzinfo=tz), datetime(year + 1, 12, 1, 1, 1, tzinfo=tz)],
)
cls.token_timed_new.save()
- def test_token_list_view(self) -> None:
- """Test the basics of the token list view."""
+ def test_player_stats_in_context(self) -> None:
+ """Test `player stats` values in context (only active tokens gets counted)"""
self.client.force_login(self.users[0])
- url = reverse("tokens:tokenfind_list")
- response = self.client.get(url)
- content = response.content.decode()
- soup = BeautifulSoup(content, "html.parser")
- rows = soup.select("div#main > div > div > div > div > table > tbody > tr")
- self.assertEqual(len(rows), 6, "token list does not return 6 entries (camp name, header and tokens)")
+ response = self.client.get(self.url_dashboard)
+ result = response.context.get("player_stats")
- def test_token_find_view(self) -> None:
- """Test the basics of the token find view."""
+ assert result.get("tokens_found") == 0
+ assert result.get("tokens_missing") == 4
+ assert result.get("tokens_count") == 4
+
+ def test_dashboard_table_shows_active_tokens(self) -> None:
+ """Test if all of the active tokens shows up in the dashboard table"""
self.client.force_login(self.users[0])
- url = reverse("tokens:details", kwargs={"token": self.token.token})
+ response = self.client.get(self.url_dashboard)
+ decoded_content = response.content.decode()
+ soup = BeautifulSoup(decoded_content, "html.parser")
+ rows = soup.select("div#main table.submitted-tokens tbody tr")
+ self.assertEqual(len(rows), 4, "The dashboard table does not show 4 rows")
+
+ def test_token_submit_form_view(self) -> None:
+ """Test the basics of the token submit form view."""
+ self.client.force_login(self.users[0])
# Test finding the test token
- response = self.client.get(path=url, follow=True)
+ response = self.client.post(self.url_submit, {"token": self.token.token}, follow=True)
assert response.status_code == 200
# Test if the page returned a success
- content = response.content.decode()
- soup = BeautifulSoup(content, "html.parser")
+ decoded_content = response.content.decode()
+ soup = BeautifulSoup(decoded_content, "html.parser")
rows = soup.select("div.alert.alert-success")
- matches = [s for s in rows if "You found a secret token:" in str(s)]
+ matches = [s for s in rows if "You found a token:" in str(s)]
self.assertEqual(len(matches), 1, "token find does not return the found msg.")
# Test finding a false token
- url = reverse("tokens:details", kwargs={"token": "F00000001234"})
- response = self.client.get(path=url, follow=True)
- self.assertEqual(response.status_code, 404, "Did not get 404 for a non-existing token")
+ response = self.client.post(self.url_submit, {"token": "F00000001234"}, follow=True)
+ decoded_content = response.content.decode()
+ soup = BeautifulSoup(decoded_content, "html.parser")
+ rows = soup.select("div.alert.alert-danger")
+ matches = [s for s in rows if "We did not recognize the token" in str(s)]
+ self.assertEqual(len(matches), 1, "token find does not return the found msg.")
# Test finding a inactive token
- url = reverse("tokens:details", kwargs={"token": self.token_inactive.token})
- response = self.client.get(path=url, follow=True)
+ response = self.client.post(self.url_submit, {"token": self.token_inactive.token}, follow=True)
assert response.status_code == 200
# Test if the page returned a warning
- content = response.content.decode()
- soup = BeautifulSoup(content, "html.parser")
+ decoded_content = response.content.decode()
+ soup = BeautifulSoup(decoded_content, "html.parser")
rows = soup.select("div.alert.alert-warning")
matches = [s for s in rows if "Patience!" in str(s)]
self.assertEqual(len(matches), 1, "inactive token find does not return the Patience msg.")
# Test finding a time expired token
- url = reverse("tokens:details", kwargs={"token": self.token_timed_old.token})
- response = self.client.get(path=url, follow=True)
+ response = self.client.post(self.url_submit, {"token": self.token_timed_old.token}, follow=True)
assert response.status_code == 200
# Test if the page returned a warning
- content = response.content.decode()
- soup = BeautifulSoup(content, "html.parser")
+ decoded_content = response.content.decode()
+ soup = BeautifulSoup(decoded_content, "html.parser")
rows = soup.select("div.alert.alert-warning")
matches = [s for s in rows if "This token is not valid after" in str(s)]
self.assertEqual(len(matches), 1, "inactive token find does not return the not valid after msg.")
# Test finding a timed token
- url = reverse("tokens:details", kwargs={"token": self.token_timed_current.token})
- response = self.client.get(path=url, follow=True)
+ response = self.client.post(self.url_submit, {"token": self.token_timed_current.token}, follow=True)
assert response.status_code == 200
# Test if the page returned a success
- content = response.content.decode()
- soup = BeautifulSoup(content, "html.parser")
+ decoded_content = response.content.decode()
+ soup = BeautifulSoup(decoded_content, "html.parser")
rows = soup.select("div.alert.alert-success")
- matches = [s for s in rows if "You found a secret token:" in str(s)]
+ matches = [s for s in rows if "Congratulations! You found a token:" in str(s)]
self.assertEqual(len(matches), 1, "token find does not return the found msg.")
# Test finding a timed not active yet token
- url = reverse("tokens:details", kwargs={"token": self.token_timed_new.token})
- response = self.client.get(path=url, follow=True)
+ response = self.client.post(self.url_submit, {"token": self.token_timed_new.token}, follow=True)
assert response.status_code == 200
# Test if the page returned a warning
- content = response.content.decode()
- soup = BeautifulSoup(content, "html.parser")
+ decoded_content = response.content.decode()
+ soup = BeautifulSoup(decoded_content, "html.parser")
rows = soup.select("div.alert.alert-warning")
matches = [s for s in rows if "This token is not valid yet" in str(s)]
self.assertEqual(len(matches), 1, "inactive token find does not return the not valid yet.")
diff --git a/src/tokens/urls.py b/src/tokens/urls.py
index 495bdfb50..ba5db6508 100644
--- a/src/tokens/urls.py
+++ b/src/tokens/urls.py
@@ -3,18 +3,14 @@
from __future__ import annotations
from django.urls import path
-from django.urls import re_path
-from .views import TokenFindListView
-from .views import TokenFindView
+from .views import TokenDashboardListView
+from .views import TokenSubmitFormView
app_name = "tokens"
urlpatterns = [
- path("", TokenFindListView.as_view(), name="tokenfind_list"),
- re_path(
- r"(?P
[0-9a-zA-Z\.@]{12,32})/$",
- TokenFindView.as_view(),
- name="details",
- ),
+ path("", TokenDashboardListView.as_view(), name="dashboard"),
+ path("", TokenSubmitFormView.as_view()), # Allow token in URL
+ path("submit", TokenSubmitFormView.as_view(), name="submit"),
]
diff --git a/src/tokens/views.py b/src/tokens/views.py
index 4fa02b858..bc86abfdf 100644
--- a/src/tokens/views.py
+++ b/src/tokens/views.py
@@ -2,28 +2,40 @@
from __future__ import annotations
+import logging
+from datetime import timedelta
from typing import TYPE_CHECKING
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
+from django.contrib.auth.models import User
+from django.db.models import Count
+from django.db.models import Exists
+from django.db.models import F
+from django.db.models import Min
+from django.db.models import OuterRef
+from django.db.models import Q
+from django.db.models.functions import TruncHour
from django.http import Http404
from django.shortcuts import redirect
from django.urls import reverse
from django.utils import timezone
-from django.views.generic import DetailView
+from django.views.generic import FormView
from django.views.generic import ListView
from prometheus_client import Gauge
if TYPE_CHECKING:
from django.db.models import QuerySet
- from django.http import HttpRequest
- from django.http import HttpResponseRedirect
+from tokens.forms import TokenFindSubmitForm
from utils.models import CampReadOnlyModeError
from .models import Token
from .models import TokenFind
+logger = logging.getLogger(f"bornhack.{__name__}")
+
+
TOKEN_FINDS = Gauge(
"bornhack_tokengame_token_finds_total",
"The total number of token finds by camp, user and token",
@@ -36,64 +48,256 @@
)
-class TokenFindView(LoginRequiredMixin, DetailView):
- """View for submitting the token found."""
+class TokenDashboardListView(LoginRequiredMixin, ListView):
+ """A View with a list of active tokens one can find."""
model = Token
- slug_field = "token"
- slug_url_kwarg = "token"
+ template_name = "token_dashboard.html"
+
+ def get_queryset(self):
+ """Get active tokens filtered by camp slug"""
+ return self.model.objects.filter(active=True).filter(camp=self.request.camp).prefetch_related("category")
+
+ def get_context_data(self, **kwargs):
+ """Return context containing form, player-statistics, and widgets metrics"""
+ context = super().get_context_data(**kwargs)
+ context["form"] = TokenFindSubmitForm()
+
+ camp_finds = TokenFind.objects.filter(token__camp=self.request.camp.pk)
+ player_finds = camp_finds.filter(user=self.request.user)
+
+ context["player_stats"] = self.get_player_stats_metrics(player_finds)
+ context["widgets"] = {
+ "options": {"camp_colour": self.request.camp.colour},
+ "total_players": self.get_total_players_metrics(camp_finds),
+ "total_finds": self.get_total_finds_metrics(camp_finds),
+ "token_activity": self.get_token_activity_metrics(camp_finds),
+ "token_categories": self.get_token_categories_metrics(),
+ }
+
+ return context
+
+ def get_player_stats_metrics(self, player_finds: QuerySet) -> dict:
+ """Return all of the metrics for player statistics"""
+ player_finds_count = player_finds.count()
+ tokens_count = self.object_list.count()
+ return {
+ "tokens_found": player_finds_count,
+ "tokens_missing": tokens_count - player_finds_count,
+ "tokens_count": tokens_count,
+ }
+
+ def get_total_players_metrics(self, camp_finds: QuerySet) -> dict:
+ """ "Return metrics for the 'total players' widget"""
+ last_joined_player = (
+ User.objects.filter(
+ token_finds__isnull=False,
+ token_finds__token__camp=self.request.camp.pk,
+ )
+ .annotate(latest_find=Min("token_finds__created"))
+ .order_by("latest_find")
+ .last()
+ )
+ unique_player_count = camp_finds.distinct("user").count()
+ non_player_count = self.request.camp.participant_count
+
+ if non_player_count: # Avoid ZeroDivisionError
+ players_pct = unique_player_count / (unique_player_count + non_player_count) * 100
+ non_players_pct = non_player_count / (unique_player_count + non_player_count) * 100
+ else:
+ players_pct = 0
+ non_players_pct = 0
+
+ return {
+ "count": unique_player_count,
+ "no_js": {
+ "Players": {
+ "value": unique_player_count,
+ "pct": players_pct,
+ },
+ "Non-Players": {
+ "value": non_player_count,
+ "pct": non_players_pct,
+ },
+ },
+ "chart": {
+ "series": [unique_player_count, non_player_count],
+ "labels": ["Players", "Non-players"],
+ },
+ "last_join_time": (last_joined_player.latest_find if last_joined_player else None),
+ }
+
+ def get_total_finds_metrics(self, camp_finds: QuerySet) -> dict:
+ """Return metrics for the 'total finds' widget"""
+ total_finds_count = camp_finds.distinct("token").count()
+ token_count = self.object_list.count()
+ latest_find = camp_finds.order_by("created").last()
+
+ return {
+ "count": camp_finds.count(),
+ "latest_find": latest_find.created if latest_find else None,
+ "no_js": {
+ "Unique finds": {
+ "value": total_finds_count,
+ "pct": (total_finds_count / token_count) * 100,
+ },
+ "Not found": {
+ "value": (token_count - total_finds_count),
+ "pct": (token_count - total_finds_count) / token_count * 100,
+ },
+ },
+ "chart": {
+ "series": [
+ total_finds_count,
+ (token_count - total_finds_count),
+ ],
+ "labels": ["Unique finds", "Not found"],
+ },
+ }
+
+ def get_token_categories_metrics(self) -> dict:
+ """Return metrics for the 'token categories' widget
+
+ Calculate the percentage of tokens found in each category by all players.
+ """
+ token_finds_qs = TokenFind.objects.filter(token=OuterRef("id"))
+ found_tokens_qs = Token.objects.filter(camp=self.request.camp).annotate(
+ was_found=Exists(token_finds_qs),
+ )
+ category_data = found_tokens_qs.values(category_name=F("category__name")).annotate(
+ total_tokens=Count("id", distinct=True),
+ found_tokens=Count("id", filter=Q(was_found=True), distinct=True),
+ )
+
+ labels, series = [], []
+ for category in category_data:
+ labels.append(category["category_name"])
+ series.append(
+ (category["found_tokens"] / category["total_tokens"]) * 100,
+ )
+
+ return {
+ "count": len(labels),
+ "no_js": dict(zip(labels, series, strict=False)),
+ "chart": {
+ "labels": labels,
+ "series": series,
+ },
+ }
- def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponseRedirect:
- """Method for submitting the token found."""
- if not self.get_object().active:
+ def get_token_activity_metrics(self, camp_finds: QuerySet) -> dict:
+ """Return metrics for the 'token activity' widget"""
+ now = timezone.localtime()
+ start = now - timezone.timedelta(hours=23)
+ last_24h_qs = (
+ camp_finds.filter(created__gte=start, created__lte=now)
+ .annotate(hour=TruncHour("created"))
+ .values("hour")
+ .annotate(count=Count("id"))
+ .order_by("hour")
+ )
+ count_by_hours = {entry["hour"]: entry["count"] for entry in last_24h_qs}
+
+ labels, series = [], []
+ HOURS = 24
+ for i in range(HOURS):
+ delta = start + timedelta(hours=i)
+ labels.append(delta.strftime("%H"))
+
+ trunc_hour = delta.replace(minute=0, second=0, microsecond=0)
+ series.append(count_by_hours.get(trunc_hour, 0))
+
+ last_60m_qs = camp_finds.filter(created__gte=(now - timedelta(minutes=60)))
+ no_js_series = series.copy()
+ no_js_series.reverse()
+ no_js_labels = labels.copy()
+ no_js_labels.reverse()
+
+ return {
+ "last_60m_count": last_60m_qs.count(),
+ "no_js": dict(zip(no_js_labels, no_js_series, strict=False)),
+ "chart": {
+ "series": series,
+ "labels": labels,
+ },
+ }
+
+
+class TokenSubmitFormView(LoginRequiredMixin, FormView):
+ """View for submitting a token form"""
+
+ form_class = TokenFindSubmitForm
+ template_name = "token_submit.html"
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ form_data = {"token": self.kwargs.get("token")}
+ context["form"] = TokenFindSubmitForm(initial=form_data)
+ return context
+
+ def form_invalid(self, form):
+ return super().form_invalid(form)
+
+ def form_valid(self, form):
+ """Return success url when form is valid"""
+ cleaned_token = form.cleaned_data.get("token")
+ try:
+ token = Token.objects.get(token=cleaned_token)
+ except Token.DoesNotExist:
+ messages.error(
+ self.request,
+ "We did not recognize the token you submitted. Please try something else.",
+ )
+ return redirect(reverse("tokens:dashboard", kwargs={"camp_slug": self.request.camp.slug}))
+
+ if not token.active:
messages.warning(
self.request,
"Patience! You found a valid token, but it is not active. Try again later!",
)
- return redirect(reverse("tokens:tokenfind_list"))
+ return redirect(reverse("tokens:dashboard", kwargs={"camp_slug": self.request.camp.slug}))
- if self.get_object().valid_when:
- if self.get_object().valid_when.lower and self.get_object().valid_when.lower > timezone.now():
+ if token.valid_when:
+ if token.valid_when.lower and token.valid_when.lower > timezone.now():
messages.warning(
self.request,
- f"This token is not valid yet! Try again after {self.get_object().valid_when.lower}",
+ f"This token is not valid yet! Try again after {token.valid_when.lower}",
)
- return redirect(reverse("tokens:tokenfind_list"))
+ return redirect(reverse("tokens:dashboard", kwargs={"camp_slug": self.request.camp.slug}))
- if self.get_object().valid_when.upper and self.get_object().valid_when.upper < timezone.now():
+ if token.valid_when.upper and token.valid_when.upper < timezone.now():
messages.warning(
self.request,
- f"This token is not valid after {self.get_object().valid_when.upper}! Maybe find a flux capacitor?",
+ f"This token is not valid after {token.valid_when.upper}! Maybe find a flux capacitor?",
)
- return redirect(reverse("tokens:tokenfind_list"))
+ return redirect(reverse("tokens:dashboard", kwargs={"camp_slug": self.request.camp.slug}))
- # register this token find if it isn't already
try:
- token, created = TokenFind.objects.get_or_create(
- token=self.get_object(),
- user=request.user,
+ token_find, created = TokenFind.objects.get_or_create(
+ token=token,
+ user=self.request.user,
)
except CampReadOnlyModeError as e:
raise Http404 from e
if created:
# user found a new token
- username = request.user.profile.get_public_credit_name
+ username = self.request.user.profile.get_public_credit_name
if username == "Unnamed":
username = "anonymous_player_{request.user.id}"
# register metrics
- if TokenFind.objects.filter(token=self.get_object()).count() == 1:
+ if TokenFind.objects.filter(token=token).count() == 1:
# this is the first time this token has been found, count it as such
FIRST_TOKEN_FINDS.labels(
token.camp.title,
- request.user.id,
+ self.request.user.id,
username,
token.id,
).inc()
TOKEN_FINDS.labels(
token.camp.title,
- request.user.id,
+ self.request.user.id,
username,
token.id,
).inc()
@@ -101,19 +305,18 @@ def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponseRedirect:
# message for the user
messages.success(
self.request,
- f"Congratulations! You found a secret token: '{self.get_object().description}' "
- "- Your visit has been registered! Keep hunting, there might be more tokens out there.",
+ f"Congratulations! You found a token: '{token.description}' "
+ "- Keep hunting, there might be more tokens out there.",
)
- return redirect(reverse("tokens:tokenfind_list"))
-
-class TokenFindListView(LoginRequiredMixin, ListView):
- """A View with a list of active tokens one can find."""
+ else:
+ messages.info(
+ self.request,
+ f"You already got this token. You submitted it at: {token_find.created}",
+ )
- model = Token
- template_name = "tokenfind_list.html"
+ return redirect(self.get_success_url())
- def get_queryset(self) -> QuerySet:
- """Get QuerySet of active tokens."""
- qs = super().get_queryset()
- return qs.filter(active=True)
+ def get_success_url(self):
+ """Redirect back to dashboard on success"""
+ return reverse("tokens:dashboard", kwargs={"camp_slug": self.request.camp.slug})
diff --git a/src/utils/bootstrap/base.py b/src/utils/bootstrap/base.py
index 2cb76c6e4..47da1f1c8 100644
--- a/src/utils/bootstrap/base.py
+++ b/src/utils/bootstrap/base.py
@@ -92,6 +92,7 @@
from tickets.models import SponsorTicket
from tickets.models import TicketType
from tokens.models import Token
+from tokens.models import TokenCategory
from tokens.models import TokenFind
from utils.slugs import unique_slugify
from villages.models import Village
@@ -1452,6 +1453,11 @@ def create_camp_teams(self, camp: Camp) -> dict:
description="The NOC team is in charge of establishing and running a network onsite.",
camp=camp,
)
+ teams["game"] = Team.objects.create(
+ name="Game",
+ description="The Game team is in charge of token game.",
+ camp=camp,
+ )
teams["gis"] = Team.objects.create(
name="GIS",
description="The GIS team is in charge of managing the gis data.",
@@ -1988,7 +1994,34 @@ def create_camp_sponsor_tickets(
ticket_type=ticket_types["adult_full_week"],
)
- def create_camp_tokens(self, camp: Camp) -> dict[Token]:
+ def create_token_categories(self, camp: Camp) -> dict[str, TokenCategory]:
+ """Create the camp tokens."""
+ year = camp.camp.lower.year
+ self.output(f"Creating token categories for {year}...")
+ categories = {}
+ categories["physical"], _ = TokenCategory.objects.get_or_create(
+ name="Physical",
+ description="Tokens exist in the physical space",
+ )
+ categories["phone"], _ = TokenCategory.objects.get_or_create(
+ name="Phone",
+ description="Tokens exist in a phoney space",
+ )
+ categories["electrical"], _ = TokenCategory.objects.get_or_create(
+ name="Electrical",
+ description="Tokens with power",
+ )
+ categories["internet"], _ = TokenCategory.objects.get_or_create(
+ name="Internet",
+ description="Tokens exist in the virtual space",
+ )
+ categories["website"], _ = TokenCategory.objects.get_or_create(
+ name="Website",
+ description="Tokens exist on the bornhack website",
+ )
+ return categories
+
+ def create_camp_tokens(self, camp: Camp, categories: dict) -> dict[Token]:
"""Create the camp tokens."""
tokens = {}
year = camp.camp.lower.year
@@ -1996,42 +2029,48 @@ def create_camp_tokens(self, camp: Camp) -> dict[Token]:
tokens[0] = Token.objects.create(
camp=camp,
token=get_random_string(length=32),
- category="Physical",
+ category=categories["physical"],
+ hint="Token in a tent",
description="Token in the back of the speakers tent (in binary)",
active=True,
)
tokens[1] = Token.objects.create(
camp=camp,
token=get_random_string(length=32),
- category="Internet",
- description="Twitter",
+ category=categories["internet"],
+ hint="Social media",
+ description="Mastodon",
active=True,
)
tokens[2] = Token.objects.create(
camp=camp,
token=get_random_string(length=32),
- category="Website",
+ category=categories["website"],
+ hint="Web server",
description="Token hidden in the X-Secret-Token HTTP header on the BornHack website",
active=True,
)
tokens[3] = Token.objects.create(
camp=camp,
token=get_random_string(length=32),
- category="Physical",
+ category=categories["physical"],
+ hint="QR Code",
description="Token in infodesk (QR code)",
active=True,
)
tokens[4] = Token.objects.create(
camp=camp,
token=get_random_string(length=32),
- category="Physical",
+ category=categories["physical"],
+ hint="Gadget",
description=f"Token on the back of the BornHack {year} badge",
active=True,
)
tokens[5] = Token.objects.create(
camp=camp,
token=get_random_string(length=32),
- category="Website",
+ category=categories["website"],
+ hint="EXIF",
description="Token hidden in EXIF data in the logo posted on the website sunday",
active=True,
)
@@ -2461,7 +2500,9 @@ def bootstrap_camp(self, options: dict) -> None:
camp_sponsors = self.create_camp_sponsors(camp, sponsor_tiers)
- tokens = self.create_camp_tokens(camp)
+ categories = self.create_token_categories(camp)
+
+ tokens = self.create_camp_tokens(camp, categories)
self.create_camp_token_finds(camp, tokens, self.users)
diff --git a/src/utils/management/commands/bootstrap_devsite.py b/src/utils/management/commands/bootstrap_devsite.py
index 2bb8a79b0..cc6722c50 100644
--- a/src/utils/management/commands/bootstrap_devsite.py
+++ b/src/utils/management/commands/bootstrap_devsite.py
@@ -10,6 +10,7 @@
logger = logging.getLogger(f"bornhack.{__name__}")
+
class Command(BaseCommand):
args = "none"
help = "Create mock data for development instances"
diff --git a/src/utils/middleware.py b/src/utils/middleware.py
index 8f6cb36d7..15f9b2807 100644
--- a/src/utils/middleware.py
+++ b/src/utils/middleware.py
@@ -45,5 +45,5 @@ def __call__(self, request):
response = self.get_response(request)
if "Vary" in response:
if request.path.startswith("/maps/kfproxy/"):
- del(response["Vary"])
+ del response["Vary"]
return response
diff --git a/src/utils/mixins.py b/src/utils/mixins.py
index 7eabe716d..9608bd039 100644
--- a/src/utils/mixins.py
+++ b/src/utils/mixins.py
@@ -4,7 +4,6 @@
from typing import TYPE_CHECKING
from django.conf import settings
-
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.mixins import UserPassesTestMixin
@@ -39,8 +38,10 @@ class RaisePermissionRequiredMixin(PermissionRequiredMixin):
raise_exception = True
+
class IsTeamPermContextMixin:
"""Mixing for adding is_team_{perm} to context"""
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
perms = self.request.user.get_all_permissions()
@@ -57,6 +58,7 @@ def get_context_data(self, **kwargs):
context[f"is_team_{perm}"] = False
return context
+
class BaseTeamPermRequiredMixin:
"""Base class for TeamRequiredMixins."""