Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/tokens/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Init for application token."""
23 changes: 17 additions & 6 deletions src/tokens/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
"""All Django admin views for application token."""

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import ClassVar

from django.contrib import admin

from .models import Token
Expand All @@ -8,13 +15,17 @@

@admin.register(Token)
class TokenAdmin(admin.ModelAdmin):
list_filter = ["camp", "category", "active"]
list_display = ["token", "description", "camp", "category", "active", "valid_when"]
search_fields = ["token", "description", "category"]
"""Django admin for tokens."""

list_filter: ClassVar[list[str]] = ["camp", "category", "active"]
list_display: ClassVar[list[str]] = ["token", "description", "camp", "category", "active", "valid_when"]
search_fields: ClassVar[list[str]] = ["token", "description", "category"]


@admin.register(TokenFind)
class TokenFindAdmin(admin.ModelAdmin):
list_filter = ["token__camp", "user"]
list_display = ["token", "user", "created"]
search_fields = ["user", "token"]
"""Django admin for token finds."""

list_filter: ClassVar[list[str]] = ["token__camp", "user"]
list_display: ClassVar[list[str]] = ["token", "user", "created"]
search_fields: ClassVar[list[str]] = ["user", "token"]
4 changes: 4 additions & 0 deletions src/tokens/apps.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
"""All apps for token application."""

from __future__ import annotations

from django.apps import AppConfig


class TokensConfig(AppConfig):
"""TokensConfig."""

name = "tokens"
45 changes: 35 additions & 10 deletions src/tokens/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
"""All models for the token application."""

from __future__ import annotations

from typing import TYPE_CHECKING

from django.contrib.postgres.fields import DateTimeRangeField
from django.db import models
from django.urls import reverse
Expand All @@ -8,8 +12,15 @@

from utils.models import CampRelatedModel

if TYPE_CHECKING:
from typing import ClassVar

from camps.models import Camp


class Token(ExportModelOperationsMixin("token"), CampRelatedModel):
"""Token model."""

camp = models.ForeignKey("camps.Camp", on_delete=models.PROTECT)

token = models.CharField(max_length=32, help_text="The secret token")
Expand All @@ -22,7 +33,9 @@ class Token(ExportModelOperationsMixin("token"), CampRelatedModel):

active = models.BooleanField(
default=False,
help_text="An active token is listed and can be found by players, an inactive token is unlisted and returns an error saying 'valid but inactive token found' to players who find it.",
help_text="An active token is listed and can be found by players, "
"an inactive token is unlisted and returns an error saying 'valid "
"but inactive token found' to players who find it.",
)

valid_when = DateTimeRangeField(
Expand All @@ -35,33 +48,43 @@ class Token(ExportModelOperationsMixin("token"), CampRelatedModel):
camp_filter = "camp"

def __str__(self) -> str:
"""String formatter."""
return f"{self.description} ({self.camp})"

class Meta:
ordering = ["camp"]
"""Meta."""

ordering: ClassVar[list[str]] = ["camp"]

def get_absolute_url(self):
def get_absolute_url(self) -> str:
"""Get absolute url."""
return reverse(
"backoffice:token_detail",
kwargs={"camp_slug": self.camp.slug, "pk": self.pk},
)

@property
def valid_now(self) -> bool:
"""Returns if the token is valid 'now'."""
valid = False
if not self.valid_when:
# no time limit
return True
if self.valid_when.lower and self.valid_when.lower > timezone.now():
valid = True
elif self.valid_when.lower and self.valid_when.lower > timezone.now():
# not valid yet
return False
if self.valid_when.upper and self.valid_when.upper < timezone.now():
valid = False
elif self.valid_when.upper and self.valid_when.upper < timezone.now():
# not valid anymore
return False
return True
valid = False
return valid


class TokenFind(ExportModelOperationsMixin("token_find"), CampRelatedModel):
"""Model for submitting the found token."""

class Meta:
"""Meta."""

unique_together = ("user", "token")

token = models.ForeignKey("tokens.Token", on_delete=models.PROTECT)
Expand All @@ -75,8 +98,10 @@ class Meta:
camp_filter = "token__camp"

def __str__(self) -> str:
"""String formatter."""
return f"{self.token} found by {self.user}"

@property
def camp(self):
def camp(self) -> Camp:
"""Property camp linked to this token find via Token."""
return self.token.camp
1 change: 1 addition & 0 deletions src/tokens/templatetags/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Template tags for the Token application."""
12 changes: 10 additions & 2 deletions src/tokens/templatetags/token_tags.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
"""Token template tags for the Token application."""
from __future__ import annotations

from typing import TYPE_CHECKING

from django import template

from tokens.models import TokenFind

if TYPE_CHECKING:
from django.contrib.auth.models import User

register = template.Library()


@register.filter
def found_by_user(token, user):
def found_by_user(token: str, user: User) -> bool:
"""Template tag to show if the token is found."""
try:
tokenfind = TokenFind.objects.get(token=token, user=user)
return tokenfind.created
except TokenFind.DoesNotExist:
return False
else:
return tokenfind.created
1 change: 1 addition & 0 deletions src/tokens/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Init for tests of application tokens."""
163 changes: 163 additions & 0 deletions src/tokens/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""Tests for token views."""

from __future__ import annotations

from datetime import datetime

import pytz
from bs4 import BeautifulSoup
from django.urls import reverse

from tokens.models import Token
from utils.tests import BornhackTestBase


class TestTokenViews(BornhackTestBase):
"""Test Token view."""

token: Token
token_inactive: Token
token_timed: Token
token_timed_old: Token
token_timed_current: Token
token_timed_new: Token

@classmethod
def setUpTestData(cls) -> None:
"""Add test data."""
# first add users and other basics
super().setUpTestData()

tz = pytz.timezone("Europe/Copenhagen")
now = datetime.now(tz)
year = now.year

# then create some tokens
cls.token = Token(
camp=cls.camp,
token="DEADBEAF1234",
description="Test token",
category="Test",
active=True,
)
cls.token.save()

cls.token_inactive = Token(
camp=cls.camp,
token="1234DEADBEAF",
description="Test Inactive token",
category="Test",
active=False,
)
cls.token_inactive.save()

cls.token_timed_old = Token(
camp=cls.camp,
token="12341337F01F",
description="Test timed token",
category="Test",
active=True,
valid_when=["2000-01-01 00:00:00", "2000-10-10 10:10:10"],
)
cls.token_timed_old.save()

cls.token_timed_current = Token(
camp=cls.camp,
token="12341337F02F",
description="Test timed token",
category="Test",
active=True,
valid_when=[now, datetime(year + 1, 1, 1, 1, 1, tzinfo=tz)],
)
cls.token_timed_current.save()

cls.token_timed_new = Token(
camp=cls.camp,
token="12341337F03F",
description="Test timed token",
category="Test",
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."""
self.client.login(username="user0", password="user0")
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)")

def test_token_find_view(self) -> None:
"""Test the basics of the token find view."""
self.client.login(username="user0", password="user0")

url = reverse("tokens:details", kwargs={"token": self.token.token})

# Test finding the test token
response = self.client.get(path=url, follow=True)
assert response.status_code == 200

# Test if the page returned a success
content = response.content.decode()
soup = BeautifulSoup(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)]
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")

# Test finding a inactive token
url = reverse("tokens:details", kwargs={"token": self.token_inactive.token})
response = self.client.get(path=url, follow=True)
assert response.status_code == 200

# Test if the page returned a warning
content = response.content.decode()
soup = BeautifulSoup(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)
assert response.status_code == 200

# Test if the page returned a warning
content = response.content.decode()
soup = BeautifulSoup(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)
assert response.status_code == 200

# Test if the page returned a success
content = response.content.decode()
soup = BeautifulSoup(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)]
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)
assert response.status_code == 200

# Test if the page returned a warning
content = response.content.decode()
soup = BeautifulSoup(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.")
2 changes: 2 additions & 0 deletions src/tokens/urls.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""All URLs for the Token application."""

from __future__ import annotations

from django.urls import path
Expand Down
Loading
Loading