Skip to content

Commit

Permalink
PuzzleTechHub#39: use specific datetime-local widgets (which provides…
Browse files Browse the repository at this point in the history
… a calendar datepicker on modern browsers) for hunt start/end times
  • Loading branch information
madjaqk committed Mar 7, 2024
1 parent e53dee7 commit 675d138
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 121 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,8 @@ staticfiles
# VSCode
\.vscode/

# PyCharm
.idea/

# Direnv (see https://direnv.net/ )
.envrc
Empty file added myus/__init__.py
Empty file.
129 changes: 129 additions & 0 deletions myus/myus/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
from django import forms

from .models import Hunt, Puzzle, Team, User


class MarkdownTextarea(forms.Textarea):
template_name = "widgets/markdown_textarea.html"


# Custom date-time field using a datetime-local input
# Implementation from StackOverflow: https://stackoverflow.com/a/69965027
class DateTimeLocalInput(forms.DateTimeInput):
input_type = "datetime-local"


class DateTimeLocalField(forms.DateTimeField):
input_formats = [
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%dT%H:%M:%S.%f",
"%Y-%m-%dT%H:%M",
]
widget = DateTimeLocalInput(format="%Y-%m-%dT%H:%M")


# based on UserCreationForm from Django source
class RegisterForm(forms.ModelForm):
"""
A form that creates a user, with no privileges, from the given username and
password.
"""

password1 = forms.CharField(label="Password", widget=forms.PasswordInput)
password2 = forms.CharField(
label="Password confirmation",
widget=forms.PasswordInput,
help_text="Enter the same password as above, for verification.",
)
email = forms.EmailField(
label="Email address",
required=False,
help_text="Optional, but you'll get useful email notifications when we implement those.",
)
bio = forms.CharField(
widget=MarkdownTextarea,
required=False,
help_text="(optional) Tell us about yourself. What kinds of puzzle genres or subject matter do you like?",
)

class Meta:
model = User
fields = ("username", "email", "display_name", "discord_username", "bio")

def clean_password2(self):
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(
"The two password fields didn't match.",
code="password_mismatch",
)
return password2

def save(self, commit=True):
user = super(RegisterForm, self).save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user


class HuntForm(forms.ModelForm):
description = forms.CharField(widget=MarkdownTextarea, required=False)
start_time = DateTimeLocalField(required=False, help_text="Date/time must be UTC")
end_time = DateTimeLocalField(required=False, help_text="Date/time must be UTC")

class Meta:
model = Hunt
fields = [
"name",
"slug",
"description",
"start_time",
"end_time",
"member_limit",
"guess_limit",
]


class GuessForm(forms.Form):
guess = forms.CharField()


class TeamForm(forms.ModelForm):
class Meta:
model = Team
fields = ["name"]


class InviteMemberForm(forms.Form):
username = forms.CharField()

def clean(self):
cleaned_data = super().clean()
username = cleaned_data.get("username")

try:
user = User.objects.get(username=username)
except User.DoesNotExist:
raise forms.ValidationError("No such user!")

cleaned_data["user"] = user
return cleaned_data


class PuzzleForm(forms.ModelForm):
content = forms.CharField(widget=MarkdownTextarea, required=False)

class Meta:
model = Puzzle
fields = [
"name",
"slug",
"content",
"answer",
"points",
"order",
"progress_points",
"progress_threshold",
]
75 changes: 73 additions & 2 deletions myus/myus/tests.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
from datetime import datetime, timezone
from http import HTTPStatus

from django.urls import reverse
from django.test import TestCase

from myus.models import Hunt, Puzzle
from myus.forms import HuntForm
from myus.models import Hunt, Puzzle, User

class TestViewHunt(TestCase):
""" Test the view_hunt endpoint
The tests related to the handling of URLs with IDs and slugs should be taken as general tests for the redirect_from_hunt_id_to_hunt_id_and_slug decorator
The tests related to the handling of URLs with IDs and slugs should be taken as
general tests for the redirect_from_hunt_id_to_hunt_id_and_slug decorator
"""

def setUp(self):
Expand Down Expand Up @@ -68,3 +71,71 @@ def test_view_puzzle_with_ids_and_wrong_slugs_redirects_to_ids_and_correct_slugs
""" Visiting the view_puzzle endpoint with IDs and the wrong slugs in URL redirects to URL with IDs and correct slugs """
res = self.client.get(reverse(self.view_name, args=[self.hunt.id, "wrong-hunt-slug", self.puzzle.id, "wrong-puzzle-slug"]))
self.assertRedirects(res, self.correct_url)


class TestHuntForm(TestCase):
""" Test the HuntForm """

def setUp(self):
# self.user = User.objects.create_user(username="example", password="password")
# self.client.force_login(self.user)
self.shared_test_data = {"name": "Test Hunt", "slug": "test", "member_limit": 0, "guess_limit": 20}
# Maybe also "member_limit": 0, "guess_limit": 20

def test_hunt_form_accepts_start_time_in_iso_format(self):
""" The HuntForm accepts the start_time field in ISO format (YYYY-MM-DDTHH:MM:SS) """
test_data = self.shared_test_data.copy()
start_time = datetime(2024, 3, 15, 1, 2, tzinfo=timezone.utc)
test_data["start_time"] = start_time.isoformat()
form = HuntForm(data=test_data)
self.assertTrue(form.is_valid(), msg=form.errors)
hunt = form.save()
self.assertEqual(hunt.start_time, start_time)

def test_hunt_form_accepts_start_time_without_seconds(self):
""" The HuntForm accepts the start_time field without seconds specified
The out-of-the-box datetime-local input appears to provide data in this format
"""
test_data = self.shared_test_data.copy()
start_time = datetime(2024, 3, 15, 1, 2, tzinfo=timezone.utc)
test_data["start_time"] = start_time.strftime("%Y-%m-%dT%H:%M")
form = HuntForm(data=test_data)
self.assertTrue(form.is_valid(), msg=form.errors)
hunt = form.save()
self.assertEqual(hunt.start_time, start_time)

def test_hunt_form_start_time_uses_datetime_local_input(self):
""" The HuntForm uses a datetime-local input for the start_time field """
form = HuntForm(data=self.shared_test_data)
start_time_field = form.fields["start_time"]
self.assertEqual(start_time_field.widget.input_type, "datetime-local")

def test_hunt_form_accepts_end_time_in_iso_format(self):
""" The HuntForm accepts the end_time field in ISO format (YYYY-MM-DDTHH:MM:SS) """
test_data = self.shared_test_data.copy()
end_time = datetime(2024, 3, 15, 1, 2, tzinfo=timezone.utc)
test_data["end_time"] = end_time.isoformat()
form = HuntForm(data=test_data)
self.assertTrue(form.is_valid(), msg=form.errors)
hunt = form.save()
self.assertEqual(hunt.end_time, end_time)

def test_hunt_form_accepts_end_time_without_seconds(self):
""" The HuntForm accepts the end_time field without seconds specified
The out-of-the-box datetime-local input appears to provide data in this format
"""
test_data = self.shared_test_data.copy()
end_time = datetime(2024, 3, 15, 1, 2, tzinfo=timezone.utc)
test_data["end_time"] = end_time.strftime("%Y-%m-%dT%H:%M")
form = HuntForm(data=test_data)
self.assertTrue(form.is_valid(), msg=form.errors)
hunt = form.save()
self.assertEqual(hunt.end_time, end_time)

def test_hunt_form_end_time_displays_datetime_local_widget(self):
""" The HuntForm uses a datetime-local input for the end_time field """
form = HuntForm(data=self.shared_test_data)
end_time_field = form.fields["end_time"]
self.assertEqual(end_time_field.widget.input_type, "datetime-local")
Loading

0 comments on commit 675d138

Please sign in to comment.