Skip to content

Commit

Permalink
Merge pull request #45 from madjaqk/39_date_time_widgets
Browse files Browse the repository at this point in the history
#39: Use datetime-local inputs for hunt start/end times
  • Loading branch information
TheOriginalSoni authored Apr 24, 2024
2 parents f5126e6 + c5b7f3f commit 5038a97
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 142 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",
]
154 changes: 131 additions & 23 deletions myus/myus/tests.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,178 @@
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
"""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):
self.hunt = Hunt.objects.create(name="Test Hunt", slug="test-hunt")
self.view_name = "view_hunt"

def test_view_hunt_with_id_and_slug_success(self):
""" Visiting the view_hunt endpoint with both ID and slug in the URL displays the requested page """
res = self.client.get(reverse(self.view_name, args=[self.hunt.id, self.hunt.slug]))
"""Visiting the view_hunt endpoint with both ID and slug in the URL displays the requested page"""
res = self.client.get(
reverse(self.view_name, args=[self.hunt.id, self.hunt.slug])
)
self.assertEqual(res.status_code, HTTPStatus.OK)
self.assertTemplateUsed(res, "view_hunt.html")

def test_view_hunt_with_id_only_redirects_to_id_and_slug(self):
""" Visiting the view_hunt endpoint with only ID in the URL redirects to the URL with ID and slug """
"""Visiting the view_hunt endpoint with only ID in the URL redirects to the URL with ID and slug"""
res = self.client.get(reverse(self.view_name, args=[self.hunt.id]))
self.assertRedirects(res, reverse(self.view_name, args=[self.hunt.id, self.hunt.slug]))
self.assertRedirects(
res, reverse(self.view_name, args=[self.hunt.id, self.hunt.slug])
)

def test_view_hunt_with_id_and_wrong_slug_redirects_to_id_and_correct_slug(self):
""" Visiting the view_hunt endpoint with ID and the wrong slug in URL redirects to URL with ID and correct slug """
res = self.client.get(reverse(self.view_name, args=[self.hunt.id, "the-wrong-slug"]))
self.assertRedirects(res, reverse(self.view_name, args=[self.hunt.id, self.hunt.slug]))
"""Visiting the view_hunt endpoint with ID and the wrong slug in URL redirects to URL with ID and correct slug"""
res = self.client.get(
reverse(self.view_name, args=[self.hunt.id, "the-wrong-slug"])
)
self.assertRedirects(
res, reverse(self.view_name, args=[self.hunt.id, self.hunt.slug])
)


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

def setUp(self):
self.hunt = Hunt.objects.create(name="Test Hunt", slug="test-hunt")
self.puzzle = Puzzle.objects.create(name="Test Puzzle", slug="test-puzzle", hunt=self.hunt)
self.puzzle = Puzzle.objects.create(
name="Test Puzzle", slug="test-puzzle", hunt=self.hunt
)
self.view_name = "view_puzzle"
self.correct_url = reverse(self.view_name, args=[self.hunt.id, self.hunt.slug, self.puzzle.id, self.puzzle.slug])
self.correct_url = reverse(
self.view_name,
args=[self.hunt.id, self.hunt.slug, self.puzzle.id, self.puzzle.slug],
)

def test_view_puzzle_with_ids_and_slugs_success(self):
""" Visiting the view_puzzle endpoint with hunt and puzzle IDs and slug in the URL displays the requested page """
"""Visiting the view_puzzle endpoint with hunt and puzzle IDs and slug in the URL displays the requested page"""
res = self.client.get(self.correct_url)
self.assertEqual(res.status_code, HTTPStatus.OK)
self.assertTemplateUsed(res, "view_puzzle.html")

def test_view_puzzle_with_no_hunt_slug_redirects_to_full_url(self):
""" Visiting the view_puzzle endpoint with no hunt_slug in the URL redirects to the full URL """
res = self.client.get(reverse(self.view_name, args=[self.hunt.id, self.puzzle.id, self.puzzle.slug]))
"""Visiting the view_puzzle endpoint with no hunt_slug in the URL redirects to the full URL"""
res = self.client.get(
reverse(
self.view_name, args=[self.hunt.id, self.puzzle.id, self.puzzle.slug]
)
)
self.assertRedirects(res, self.correct_url)

def test_view_puzzle_with_no_puzzle_slug_redirects_to_full_url(self):
""" Visiting the view_puzzle endpoint with no puzzle_slug in the URL redirects to the full URL """
res = self.client.get(reverse(self.view_name, args=[self.hunt.id, self.hunt.slug, self.puzzle.id]))
"""Visiting the view_puzzle endpoint with no puzzle_slug in the URL redirects to the full URL"""
res = self.client.get(
reverse(self.view_name, args=[self.hunt.id, self.hunt.slug, self.puzzle.id])
)
self.assertRedirects(res, self.correct_url)

def test_view_puzzle_with_no_hunt_or_puzzle_slug_redirects_to_full_url(self):
""" Visiting the view_puzzle endpoint with no hunt_slug or puzzle_slug in the URL redirects to the full URL """
res = self.client.get(reverse(self.view_name, args=[self.hunt.id, self.puzzle.id]))
"""Visiting the view_puzzle endpoint with no hunt_slug or puzzle_slug in the URL redirects to the full URL"""
res = self.client.get(
reverse(self.view_name, args=[self.hunt.id, self.puzzle.id])
)
self.assertRedirects(res, self.correct_url)

def test_view_puzzle_with_ids_and_wrong_slugs_redirects_to_ids_and_correct_slugs(self):
""" 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"]))
def test_view_puzzle_with_ids_and_wrong_slugs_redirects_to_ids_and_correct_slugs(
self,
):
"""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.shared_test_data = {
"name": "Test Hunt",
"slug": "test",
"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 5038a97

Please sign in to comment.