[0-9a-zA-Z\.@]{12,32})/$",
TokenSubmitFormView.as_view(),
- name="details",
+ name="submit",
),
path("submit", TokenSubmitFormView.as_view(), name="submit"),
]
diff --git a/src/tokens/views.py b/src/tokens/views.py
index 9ae0c5e00..3cf2687de 100644
--- a/src/tokens/views.py
+++ b/src/tokens/views.py
@@ -98,15 +98,19 @@ def get_player_stats_metrics(self, player_finds: QuerySet) -> dict:
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()
+ )
return {
"count": camp_finds.distinct("user").count(),
- "last_join": (
- 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().latest_find
+ "last_join_time": (
+ last_joined_player.latest_find if last_joined_player else None
)
}
@@ -114,10 +118,11 @@ def get_tokens_found_metrics(self, camp_finds: QuerySet) -> dict:
"""Return metrics for the 'tokens found' widget"""
token_finds_count = camp_finds.distinct("token").count()
token_count = self.object_list.count()
+ last_token_find = camp_finds.order_by("created").last()
return {
"count": camp_finds.count(),
- "last_found": camp_finds.order_by("created").last().created,
+ "last_found": last_token_find.created if last_token_find else None,
"chart": {
"series": [
token_finds_count, (token_count - token_finds_count)
From fea12f32337ffe4fedd7d745750d17a82c68699d Mon Sep 17 00:00:00 2001
From: 0xUnicorn <17219001+0xUnicorn@users.noreply.github.com>
Date: Fri, 27 Jun 2025 13:24:15 +0200
Subject: [PATCH 34/51] Consolidate widget data parsing from template to JS
file in 'extra_head' block. Implement light/dark theme support for apexcharts
---
src/static_src/js/tokens/token_charts.js | 184 +++++++++++-----------
src/tokens/templates/token_dashboard.html | 4 +-
2 files changed, 90 insertions(+), 98 deletions(-)
diff --git a/src/static_src/js/tokens/token_charts.js b/src/static_src/js/tokens/token_charts.js
index 00bafbd59..6444d0706 100644
--- a/src/static_src/js/tokens/token_charts.js
+++ b/src/static_src/js/tokens/token_charts.js
@@ -1,108 +1,102 @@
-const defaultOptions = {
- chart: {
- background: 'undefined',
- toolbar: {
- show: false,
+$(document).ready(function () {
+ const widgets = JSON.parse(document.getElementById('widgets').textContent);
+ const theme = getComputedStyle(document.body).getPropertyValue('color-scheme');
+
+ const defaultOptions = {
+ chart: {
+ background: 'undefined',
+ toolbar: {
+ show: false,
+ },
},
- },
- //colors: ['#198754', '#dc3545'],
- theme: {
- mode: 'dark',
- },
-}
+ //colors: ['#198754', '#dc3545'],
+ theme: {
+ mode: theme === 'light' ? 'light' : 'dark'
+ },
+ }
-var totalPlayersOptions = {
- ...defaultOptions,
- chart: {
- ...defaultOptions.chart,
- type: 'pie'
- },
- series: [5, 45],
- labels: ['Players', 'Non-players'],
- legend: {
- show: true,
- position: 'bottom',
- horizontalAlign: 'left',
- formatter: function(seriesName, opts) {
- return [seriesName, '(' + opts.w.globals.series[opts.seriesIndex] + ')']
+ var totalPlayersOptions = {
+ ...defaultOptions,
+ chart: {
+ ...defaultOptions.chart,
+ type: 'pie'
+ },
+ series: [5, 45],
+ labels: ['Players', 'Non-players'],
+ legend: {
+ show: true,
+ position: 'bottom',
+ horizontalAlign: 'left',
+ formatter: function(seriesName, opts) {
+ return [seriesName, '(' + opts.w.globals.series[opts.seriesIndex] + ')']
+ },
},
- },
-};
+ };
-const tokensFoundData = JSON.parse(
- document.getElementById('tokensFoundChart').textContent
-);
-var tokensFoundOptions = {
- ...defaultOptions,
- chart: {
- ...defaultOptions.chart,
- type: 'pie'
- },
- series: tokensFoundData.series,
- labels: tokensFoundData.labels,
- legend: {
- show: true,
- position: 'bottom',
- horizontalAlign: 'left',
- formatter: function(seriesName, opts) {
- return [seriesName, '(' + opts.w.globals.series[opts.seriesIndex] + ')']
+ var tokensFoundOptions = {
+ ...defaultOptions,
+ chart: {
+ ...defaultOptions.chart,
+ type: 'pie'
+ },
+ series: widgets.tokens_found.chart.series,
+ labels: widgets.tokens_found.chart.labels,
+ legend: {
+ show: true,
+ position: 'bottom',
+ horizontalAlign: 'left',
+ formatter: function(seriesName, opts) {
+ return [seriesName, '(' + opts.w.globals.series[opts.seriesIndex] + ')']
+ },
},
- },
-};
+ };
-const tokenCategoryData = JSON.parse(
- document.getElementById('tokenCategoryChart').textContent
-);
-var tokenCategoryOptions = {
- ...defaultOptions,
- chart: {
- ...defaultOptions.chart,
- type: 'radar',
- height: '100%',
- },
- series: [
- {
- name: 'Found',
- data: tokenCategoryData.series
- }
- ],
- labels: tokenCategoryData.labels,
- tooltip: {
- y: {
- formatter: function (value) {
- return value + '%';
+ var tokenCategoryOptions = {
+ ...defaultOptions,
+ chart: {
+ ...defaultOptions.chart,
+ type: 'radar',
+ height: '100%',
+ },
+ series: [
+ {
+ name: 'Found',
+ data: widgets.token_categories.chart.series
+ }
+ ],
+ labels: widgets.token_categories.chart.labels,
+ tooltip: {
+ y: {
+ formatter: function (value) {
+ return value + '%';
+ },
},
},
- },
- yaxis: {
- show: false,
- },
-};
+ yaxis: {
+ show: false,
+ },
+ };
-const tokenActivityData = JSON.parse(
- document.getElementById('tokenActivityChart').textContent
-);
-var tokenActivityOptions = {
- ...defaultOptions,
- chart: {
- ...defaultOptions.chart,
- type: 'bar'
- },
- series: [{
- name: 'Tokens found',
- data: tokenActivityData.series,
- }],
- dataLabels: {
- enabled: false,
- },
- xaxis: {
- categories: tokenActivityData.labels,
- tickAmount: 12,
- tickPlacement: 'on',
- },
-};
+ var tokenActivityOptions = {
+ ...defaultOptions,
+ chart: {
+ ...defaultOptions.chart,
+ type: 'bar'
+ },
+ series: [{
+ name: 'Tokens found',
+ data: widgets.token_activity.chart.series,
+ }],
+ dataLabels: {
+ enabled: false,
+ },
+ xaxis: {
+ categories: widgets.token_activity.chart.labels,
+ tickAmount: 12,
+ tickPlacement: 'on',
+ },
+ };
-$(document).ready(function () {
var totalPlayers = new ApexCharts(
document.querySelector('#total_players_chart'),
totalPlayersOptions
diff --git a/src/tokens/templates/token_dashboard.html b/src/tokens/templates/token_dashboard.html
index 09ed486a7..95c5d7892 100644
--- a/src/tokens/templates/token_dashboard.html
+++ b/src/tokens/templates/token_dashboard.html
@@ -10,9 +10,7 @@
{% block extra_head %}
-{{ widgets.tokens_found.chart|json_script:"tokensFoundChart" }}
-{{ widgets.token_activity.chart|json_script:"tokenActivityChart" }}
-{{ widgets.token_categories.chart|json_script:"tokenCategoryChart" }}
+{{ widgets|json_script:"widgets" }}
{% endblock extra_head %}
From 22f15c14ded41c9b6857177ccf43ac5da721bb76 Mon Sep 17 00:00:00 2001
From: 0xUnicorn <17219001+0xUnicorn@users.noreply.github.com>
Date: Fri, 27 Jun 2025 13:29:04 +0200
Subject: [PATCH 35/51] Fix wrong 'theme' value used for ternary operator logic
---
src/static_src/js/tokens/token_charts.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/static_src/js/tokens/token_charts.js b/src/static_src/js/tokens/token_charts.js
index 6444d0706..c016dbccd 100644
--- a/src/static_src/js/tokens/token_charts.js
+++ b/src/static_src/js/tokens/token_charts.js
@@ -11,7 +11,7 @@ $(document).ready(function () {
},
//colors: ['#198754', '#dc3545'],
theme: {
- mode: theme === 'light' ? 'light' : 'dark'
+ mode: theme === 'normal' ? 'light' : 'dark'
},
}
From ff8f7d3f57928607d9ebacfe45a92cd92949d952 Mon Sep 17 00:00:00 2001
From: 0xUnicorn <17219001+0xUnicorn@users.noreply.github.com>
Date: Fri, 27 Jun 2025 13:37:28 +0200
Subject: [PATCH 36/51] Fix 'typo' for document ready, is now EventListener for
'DOMContentLoaded'
---
src/static_src/js/tokens/token_charts.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/static_src/js/tokens/token_charts.js b/src/static_src/js/tokens/token_charts.js
index c016dbccd..6d107a1a5 100644
--- a/src/static_src/js/tokens/token_charts.js
+++ b/src/static_src/js/tokens/token_charts.js
@@ -1,4 +1,4 @@
-$(document).ready(function () {
+document.addEventListener('DOMContentLoaded', function () {
const widgets = JSON.parse(document.getElementById('widgets').textContent);
const theme = getComputedStyle(document.body).getPropertyValue('color-scheme');
@@ -117,5 +117,5 @@ $(document).ready(function () {
tokenCategoryOptions
);
tokenCategory.render();
-});
+});
From 00734762d21d8fd077ef63053142b47808c29e9a Mon Sep 17 00:00:00 2001
From: 0xUnicorn <17219001+0xUnicorn@users.noreply.github.com>
Date: Fri, 27 Jun 2025 18:46:18 +0200
Subject: [PATCH 37/51] Implemented fully no-js compatible icons and widgets
---
src/static_src/css/bornhack.css | 8 ++
src/static_src/js/tokens/token_charts.js | 13 ++-
src/tokens/templates/token_dashboard.html | 119 ++++++++++++++++++++--
src/tokens/views.py | 27 ++++-
4 files changed, 151 insertions(+), 16 deletions(-)
diff --git a/src/static_src/css/bornhack.css b/src/static_src/css/bornhack.css
index ed1571084..6bcb53c5f 100644
--- a/src/static_src/css/bornhack.css
+++ b/src/static_src/css/bornhack.css
@@ -486,3 +486,11 @@ img {
margin-top: -94px; /*same height as header*/
visibility: hidden;
}
+
+/* Token styling */
+
+.token-widgets .widget-content-no-js {
+ max-height: 20vh;
+ overflow-y: auto;
+ display: block;
+}
diff --git a/src/static_src/js/tokens/token_charts.js b/src/static_src/js/tokens/token_charts.js
index 6d107a1a5..41dc156d0 100644
--- a/src/static_src/js/tokens/token_charts.js
+++ b/src/static_src/js/tokens/token_charts.js
@@ -102,20 +102,23 @@ document.addEventListener('DOMContentLoaded', function () {
totalPlayersOptions
);
totalPlayers.render();
+
var tokensFound = new ApexCharts(
document.querySelector('#tokens_found_chart'),
tokensFoundOptions
);
tokensFound.render();
- var tokenActivity = new ApexCharts(
- document.querySelector('#token_activity_chart'),
- tokenActivityOptions
- );
- tokenActivity.render();
+
var tokenCategory = new ApexCharts(
document.querySelector('#token_category_chart'),
tokenCategoryOptions
);
tokenCategory.render();
+ var tokenActivity = new ApexCharts(
+ document.querySelector('#token_activity_chart'),
+ tokenActivityOptions
+ );
+ tokenActivity.render();
+
});
diff --git a/src/tokens/templates/token_dashboard.html b/src/tokens/templates/token_dashboard.html
index 95c5d7892..d82bba691 100644
--- a/src/tokens/templates/token_dashboard.html
+++ b/src/tokens/templates/token_dashboard.html
@@ -25,7 +25,8 @@ Token game dashboard
-
+ 🏆
+
{{ player_stats.tokens_found }} found
@@ -35,7 +36,8 @@
{{ player_stats.tokens_found }} found
-
+ 🔍
+
{{ player_stats.tokens_missing }} missing
@@ -45,7 +47,8 @@
{{ player_stats.tokens_missing }} missing
-
+ 📄
+
{{ player_stats.tokens_count }} total
@@ -81,7 +84,7 @@ How to play?
-
@@ -107,7 +133,30 @@
Tokens found
{{ widgets.tokens_found.count }}
-
+
+
+
+
+
+
+ | Status |
+ Count |
+
+
+
+ {% for key, value in widgets.tokens_found.no_js.items %}
+
+ | {{ key }} |
+ {{ value.value }}/{{ widgets.tokens_found.count}} ({{ value.pct|floatformat:'0' }}%) |
+
+ {% endfor %}
+
+
+
+ no-js widget
+
+
+
@@ -120,7 +169,32 @@
Token categories
{{ widgets.token_categories.count }}
-
+
+
+
+
+
+
+ | Category |
+ Found |
+
+
+
+ {% for key, value in widgets.token_categories.no_js.items %}
+
+ | {{ key }} |
+
+ {{ value|floatformat:'0' }}%
+ |
+
+ {% endfor %}
+
+
+
+
+ no-js widget
+
+
@@ -134,7 +208,30 @@ Token activity
{{ widgets.token_activity.last_60m_count }}
-
+
+
+
+
+
+
+ | Hour |
+ Tokens found |
+
+
+
+ {% for key, value in widgets.token_activity.no_js.items %}
+
+ | {{ key }} |
+ {{ value }} |
+
+ {% endfor %}
+
+
+
+
+ no-js widget
+
+
@@ -165,9 +262,11 @@ Your submitted tokens
{{ token.category.name }} |
{% if token.valid_now %}
-
+ ✔
+
{% else %}
-
+ ❌
+
{% endif %}
|
diff --git a/src/tokens/views.py b/src/tokens/views.py
index 3cf2687de..8d5be27e6 100644
--- a/src/tokens/views.py
+++ b/src/tokens/views.py
@@ -107,8 +107,19 @@ def get_total_players_metrics(self, camp_finds: QuerySet) -> dict:
.order_by("latest_find")
.last()
)
+ unique_player_count = camp_finds.distinct("user").count()
return {
- "count": camp_finds.distinct("user").count(),
+ "count": unique_player_count,
+ "no_js": {
+ "Players": {
+ "value": unique_player_count,
+ "pct": unique_player_count / (unique_player_count + 200) * 100
+ },
+ "Non-Players": {
+ "value": 200,
+ "pct": 200 / (unique_player_count + 200) * 100
+ },
+ },
"last_join_time": (
last_joined_player.latest_find if last_joined_player else None
)
@@ -123,6 +134,16 @@ def get_tokens_found_metrics(self, camp_finds: QuerySet) -> dict:
return {
"count": camp_finds.count(),
"last_found": last_token_find.created if last_token_find else None,
+ "no_js": {
+ "Found": {
+ "value": token_finds_count,
+ "pct": (token_finds_count / token_count) * 100,
+ },
+ "Not found": {
+ "value": (token_count - token_finds_count),
+ "pct": (token_count - token_finds_count) / token_count * 100,
+ }
+ },
"chart": {
"series": [
token_finds_count, (token_count - token_finds_count)
@@ -158,6 +179,7 @@ def get_token_categories_metrics(self) -> dict:
return {
"count": len(labels),
+ "no_js": dict(zip(labels, series)),
"chart": {
"labels": labels,
"series": series
@@ -188,8 +210,11 @@ def get_token_activity_metrics(self, camp_finds: QuerySet) -> dict:
last_60m_qs = camp_finds.filter(created__gte=(now - timedelta(minutes=60)))
+ series.reverse()
+ labels.reverse()
return {
"last_60m_count": last_60m_qs.count(),
+ "no_js": dict(zip(labels, series)),
"chart": {
"series": series,
"labels": labels,
From 1abc494b743d0d8a44135de2cb3d0523b8316dc6 Mon Sep 17 00:00:00 2001
From: 0xUnicorn <17219001+0xUnicorn@users.noreply.github.com>
Date: Sun, 29 Jun 2025 12:54:50 +0200
Subject: [PATCH 38/51] Add regex validator to 'Token' model, with tests
---
...n_description_alter_token_hint_and_more.py | 42 ++++++++++++
src/tokens/models.py | 22 +++++-
src/tokens/tests/test_models.py | 67 +++++++++++++++++++
3 files changed, 128 insertions(+), 3 deletions(-)
create mode 100644 src/tokens/migrations/0015_alter_token_description_alter_token_hint_and_more.py
create mode 100644 src/tokens/tests/test_models.py
diff --git a/src/tokens/migrations/0015_alter_token_description_alter_token_hint_and_more.py b/src/tokens/migrations/0015_alter_token_description_alter_token_hint_and_more.py
new file mode 100644
index 000000000..3e61f4df0
--- /dev/null
+++ b/src/tokens/migrations/0015_alter_token_description_alter_token_hint_and_more.py
@@ -0,0 +1,42 @@
+# Generated by Django 4.2.21 on 2025-06-29 10:47
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("tokens", "0014_tokencategory_created_tokencategory_updated"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="token",
+ name="description",
+ field=models.TextField(
+ help_text="The description of the token (not visible to players until they find the token)"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="token",
+ name="hint",
+ field=models.TextField(
+ help_text="The hint for this token (always visible to players)"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="token",
+ name="token",
+ field=models.CharField(
+ help_text="The secret token (^[0-9a-zA-Z\\.@]{12,32}$)",
+ max_length=32,
+ validators=[
+ django.core.validators.RegexValidator(
+ "^[0-9a-zA-Z\\.@]{12,32}$",
+ "The token did not match the regex of 12-32 characters, with (letters, numbers, dot(s) and @)",
+ )
+ ],
+ ),
+ ),
+ ]
diff --git a/src/tokens/models.py b/src/tokens/models.py
index 5a1d66d1e..c6858c91c 100644
--- a/src/tokens/models.py
+++ b/src/tokens/models.py
@@ -5,6 +5,7 @@
from typing import TYPE_CHECKING
from django.contrib.postgres.fields import DateTimeRangeField
+from django.core.validators import RegexValidator
from django.db import models
from django.urls import reverse
from django.utils import timezone
@@ -63,11 +64,26 @@ class Meta:
camp = models.ForeignKey("camps.Camp", on_delete=models.PROTECT)
- token = models.CharField(max_length=32, help_text="The secret token")
+ token = models.CharField(
+ max_length=32,
+ validators=[
+ RegexValidator(
+ r"^[0-9a-zA-Z\.@]{12,32}$",
+ ("The token did not match the regex of 12-32 characters, "
+ "with (letters, numbers, dot(s) and @)")
+ )
+ ],
+ help_text="The secret token (^[0-9a-zA-Z\.@]{12,32}$)"
+ )
- hint = models.TextField(help_text="The hint for this token")
+ hint = models.TextField(
+ help_text="The hint for this token (always visible to players)"
+ )
- description = models.TextField(help_text="The description of the token")
+ description = models.TextField(
+ help_text="The description of the token "
+ "(not visible to players until they find the token)"
+ )
active = models.BooleanField(
default=False,
diff --git a/src/tokens/tests/test_models.py b/src/tokens/tests/test_models.py
new file mode 100644
index 000000000..c4c18bcc5
--- /dev/null
+++ b/src/tokens/tests/test_models.py
@@ -0,0 +1,67 @@
+from django.core.exceptions import ValidationError
+
+from utils.tests import BornhackTestBase
+
+from tokens.models import Token
+from tokens.models import TokenCategory
+
+
+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
+ )
+
From d2df54e46689cbc17a3ec8fa74b686be8bded9e1 Mon Sep 17 00:00:00 2001
From: 0xUnicorn <17219001+0xUnicorn@users.noreply.github.com>
Date: Sun, 29 Jun 2025 13:10:27 +0200
Subject: [PATCH 39/51] Remove the regex from url path and substitute with a
'str:token', so validation doesn't happen in the URL parsing, but in the POST
using a form and we render a nice alert on the dashboard instead of returning
a 404
---
src/tokens/urls.py | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/src/tokens/urls.py b/src/tokens/urls.py
index 46c3af937..6d1f8287e 100644
--- a/src/tokens/urls.py
+++ b/src/tokens/urls.py
@@ -12,10 +12,6 @@
urlpatterns = [
path("", TokenDashboardListView.as_view(), name="dashboard"),
- re_path(
- r"(?P[0-9a-zA-Z\.@]{12,32})/$",
- TokenSubmitFormView.as_view(),
- name="submit",
- ),
+ path("", TokenSubmitFormView.as_view()), # Allow token in URL
path("submit", TokenSubmitFormView.as_view(), name="submit"),
]
From 49e44fbdb9d13fceebfa222818fa21626985149e Mon Sep 17 00:00:00 2001
From: 0xUnicorn <17219001+0xUnicorn@users.noreply.github.com>
Date: Sun, 29 Jun 2025 13:11:14 +0200
Subject: [PATCH 40/51] Fix broken test by adding css class, it broke after
adding 'no-js' widgets with tables
---
src/tokens/templates/token_dashboard.html | 2 +-
src/tokens/tests/test_views.py | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/tokens/templates/token_dashboard.html b/src/tokens/templates/token_dashboard.html
index d82bba691..2a5787236 100644
--- a/src/tokens/templates/token_dashboard.html
+++ b/src/tokens/templates/token_dashboard.html
@@ -244,7 +244,7 @@ Your submitted tokens
-
+
| Category |
diff --git a/src/tokens/tests/test_views.py b/src/tokens/tests/test_views.py
index 3d35992ec..b5bb1cc3c 100644
--- a/src/tokens/tests/test_views.py
+++ b/src/tokens/tests/test_views.py
@@ -121,11 +121,11 @@ def test_dashboard_table_shows_active_tokens(self) -> None:
response = self.client.get(self.url_dashboard)
decoded_content = response.content.decode()
soup = BeautifulSoup(decoded_content, "html.parser")
- rows = soup.select("div#main table tbody tr")
+ 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_find_view(self) -> None:
- """Test the basics of the token find view."""
+ 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
From 3ddd70908409f67422faa1c04fe8be51d4976331 Mon Sep 17 00:00:00 2001
From: 0xUnicorn <17219001+0xUnicorn@users.noreply.github.com>
Date: Tue, 1 Jul 2025 13:15:22 +0200
Subject: [PATCH 41/51] Add monochrome coloring, using the 'camp_colour'
attribute.
---
src/static_src/js/tokens/token_charts.js | 6 +++++-
src/tokens/views.py | 2 +-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/static_src/js/tokens/token_charts.js b/src/static_src/js/tokens/token_charts.js
index 41dc156d0..74394e627 100644
--- a/src/static_src/js/tokens/token_charts.js
+++ b/src/static_src/js/tokens/token_charts.js
@@ -11,7 +11,11 @@ document.addEventListener('DOMContentLoaded', function () {
},
//colors: ['#198754', '#dc3545'],
theme: {
- mode: theme === 'normal' ? 'light' : 'dark'
+ mode: theme === 'normal' ? 'light' : 'dark',
+ monochrome: {
+ enabled: true,
+ color: widgets.options.camp_colour,
+ },
},
}
diff --git a/src/tokens/views.py b/src/tokens/views.py
index 8d5be27e6..6cba7e024 100644
--- a/src/tokens/views.py
+++ b/src/tokens/views.py
@@ -34,7 +34,6 @@
from .models import Token
from .models import TokenFind
-from .models import TokenCategory
logger = logging.getLogger(f"bornhack.{__name__}")
@@ -78,6 +77,7 @@ def get_context_data(self, **kwargs):
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),
"tokens_found": self.get_tokens_found_metrics(camp_finds),
"token_activity": self.get_token_activity_metrics(camp_finds),
From c4f41593288bbcc32ff1d61d9aefd44e99a11a44 Mon Sep 17 00:00:00 2001
From: 0xUnicorn <17219001+0xUnicorn@users.noreply.github.com>
Date: Fri, 4 Jul 2025 17:45:36 +0200
Subject: [PATCH 42/51] Added negative space around the page for better mobile
view
---
src/tokens/templates/token_dashboard.html | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/tokens/templates/token_dashboard.html b/src/tokens/templates/token_dashboard.html
index 2a5787236..85afb97a0 100644
--- a/src/tokens/templates/token_dashboard.html
+++ b/src/tokens/templates/token_dashboard.html
@@ -16,13 +16,13 @@
{% block content %}
-
+
- Token game dashboard
+ Token game dashboard
-
+
🏆
@@ -33,7 +33,7 @@ {{ player_stats.tokens_found }} found
-
+
🔍
@@ -44,7 +44,7 @@ {{ player_stats.tokens_missing }} missing
-
+
📄
@@ -56,10 +56,10 @@ {{ player_stats.tokens_count }} total
-
+
- How to play?
+ How to play?
The token game lasts the whole event and is about finding little text strings matching the regular expression:
[0-9a-zA-Z\.@]{12,32}
Token examples:
@@ -71,9 +71,9 @@ How to play?
Tokens are hidden or in plain sight physically or virtually on the BornHack venue, online and offline.
- Submit the tokens you find in the field below.
+ Submit the tokens you find in the field below. You can start with this one: HelloTokenHunters{{ camp.year }}
- |