Skip to content

Commit

Permalink
Merge pull request #72 from clopedion/main
Browse files Browse the repository at this point in the history
Pre-TMPP changes
  • Loading branch information
TheOriginalSoni authored Oct 30, 2024
2 parents 9995556 + 0bc84bf commit 4ef6698
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 34 deletions.
22 changes: 21 additions & 1 deletion myus/myus/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def save(self, commit=True):
return user


class HuntForm(forms.ModelForm):
class NewHuntForm(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")
Expand All @@ -83,6 +83,26 @@ class Meta:
"end_time",
"member_limit",
"guess_limit",
"leaderboard_style",
]


class EditHuntForm(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",
"leaderboard_style",
]


Expand Down
15 changes: 12 additions & 3 deletions myus/myus/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ class Hunt(models.Model):
start_time = models.DateTimeField(
blank=True,
null=True,
help_text="Start time of the hunt. If empty, the hunt will never begin. For indefinitely open hunts, you can just set it to any time in the past.",
help_text="(Not implemented) Start time of the hunt. If empty, the hunt will never begin. For indefinitely open hunts, you can just set it to any time in the past.",
)
end_time = models.DateTimeField(
blank=True,
null=True,
help_text="End date of the hunt. If empty, the hunt will always be open.",
help_text="(Not implemented) End date of the hunt. If empty, the hunt will always be open.",
)
organizers = models.ManyToManyField(User, related_name="organizing_hunts")
invited_organizers = models.ManyToManyField(
Expand All @@ -47,14 +47,23 @@ class Hunt(models.Model):
)
member_limit = models.IntegerField(
default=0,
help_text="The maximum number of members allowed per team; 0 means unlimited",
help_text="(Not implemented) The maximum number of members allowed per team; 0 means unlimited",
validators=[MinValueValidator(0)],
)
guess_limit = models.IntegerField(
default=DEFAULT_GUESS_LIMIT,
help_text="The default number of guesses teams get on each puzzle; 0 means unlimited",
validators=[MinValueValidator(0)],
)

class LeaderboardStyle(models.TextChoices):
DEFAULT = "DEF", "Default (ordered by score, solve count, and last solve time)"
HIDDEN = "HID", "Hidden (not displayed publicly)"
SPEEDRUN = "SPD", "Speedrun (ordered by score and time to solve)"

leaderboard_style = models.CharField(
max_length=3, choices=LeaderboardStyle, default=LeaderboardStyle.DEFAULT
)
slug = models.SlugField(help_text="A short, unique identifier for the hunt.")

def public_puzzles(self):
Expand Down
17 changes: 17 additions & 0 deletions myus/myus/templates/edit_hunt.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block nav %}
» Edit Hunt
{% endblock %}
{% block main %}
<h1>Edit Hunt</h1>
<form method="post">
{% csrf_token %}
{{ form.non_field_errors }}

<table class="classic">
{{ form.as_table }}
</table>
<input type="submit" value="Submit">
</form>

{% endblock %}
28 changes: 28 additions & 0 deletions myus/myus/templates/leaderboard_SPD.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{% extends "base.html" %}
{% load duration %}
{% block nav %}
» <a href="{% url 'view_hunt' hunt.id hunt.slug %}">{{ hunt.name }}</a>
» Leaderboard
{% endblock %}
{% block main %}
<h1>Hunt: {{ hunt.name }} / Leaderboard</h1>

{% if teams %}
<table class="classic">
<tr><th>Team</th><th>Score</th><th>Solves</th><th>Team Creation Time (UTC)</th><th>Last Solve (UTC)</th><th>Total Solve Time</th></tr>
{% for team in teams %}
<tr>
<td>{{ team.name }}</td>
<td>{{ team.score }}</td>
<td>{{ team.solve_count }}</td>
<td>{{ team.creation_time|date:'Y-m-d H:i'}}</td>
<td>{{ team.last_solve|date:'Y-m-d H:i'}}</td>
<td>{{ team.solve_time|duration}}</td>
</tr>
{% endfor %}
</table>
{% else %}
No teams...
{% endif %}

{% endblock %}
4 changes: 3 additions & 1 deletion myus/myus/templates/my_team.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
{% load user_display %}
{% block main %}
<h1>Hunt: {{ hunt.name }}</h1>
<a href="{% url 'leaderboard' hunt.id hunt.slug %}">leaderboard</a>
{% if hunt.leaderboard_style != "HID" %}
<a href="{% url 'leaderboard' hunt.id hunt.slug %}">leaderboard</a>
{% endif %}

{% if error %}
Error: {{ error }}
Expand Down
14 changes: 9 additions & 5 deletions myus/myus/templates/view_hunt.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
{% block main %}
<h1>Hunt: {{ hunt.name }}</h1>
<nav class="nav-hunt">
<p>
<a href="{% url 'leaderboard' hunt.id hunt.slug %}">Leaderboard</a>
</p>
{% if hunt.leaderboard_style != "HID" or is_organizer %}
<p>
<a href="{% url 'leaderboard' hunt.id hunt.slug %}">Leaderboard</a>
</p>
{% endif %}

{% if is_organizer %}
<p>You are an organizer of this hunt. <a href="{% url 'new_puzzle' hunt.id hunt.slug %}">add puzzle</a></p>
<p>You are an organizer of this hunt:
<ul><li><a href="{% url 'new_puzzle' hunt.id hunt.slug %}">add puzzle</a></li>
<li><a href="{% url 'edit_hunt' hunt.id hunt.slug %}">edit hunt settings</a></li></ul> </p>
{% elif team %}
<p>You are <a href="{% url 'my_team' hunt.id hunt.slug %}">on Team {{ team.name }}</a>.</p>
{% else %}
Expand All @@ -31,7 +35,7 @@ <h2>Puzzles</h2>
<tr>
<td><a href="{% url 'view_puzzle' hunt.id hunt.slug puzzle.id puzzle.slug %}">{{ puzzle.name }}</a></td>
<td>{% if puzzle.correct_guess %}✅{% endif %}</td>
<td>{% if puzzle.correct_guess %}<samp>{{ puzzle.correct_guess }}</samp>{% endif %}</td>
<td>{% if puzzle.correct_guess %}<samp>{{ puzzle.answer | upper }}</samp>{% endif %}</td>
<td>{{ puzzle.solve_count }}</td>
<td>{{ puzzle.guess_count }}</td>
</tr>
Expand Down
2 changes: 1 addition & 1 deletion myus/myus/templates/view_puzzle.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ <h1>Puzzle: {{ puzzle.name }}</h1>
<tr>
<td>
{% if guess.correct %}
<strong>{{ guess.guess }}</strong>
<strong>{{ puzzle.answer | upper }}</strong>
{% else %}
{{ guess.guess }}
{% endif %}
Expand Down
12 changes: 12 additions & 0 deletions myus/myus/templatetags/duration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django import template

register = template.Library()


@register.filter
def duration(td):
seconds = int(td.total_seconds())
(days, seconds) = divmod(seconds, 3600 * 24)
(hours, seconds) = divmod(seconds, 3600)
(minutes, seconds) = divmod(seconds, 60)
return "{} days {:02}:{:02}:{:02}".format(days, hours, minutes, seconds)
30 changes: 15 additions & 15 deletions myus/myus/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.urls import reverse
from django.test import TestCase

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


Expand Down Expand Up @@ -108,8 +108,8 @@ def test_view_puzzle_with_ids_and_wrong_slugs_redirects_to_ids_and_correct_slugs
self.assertRedirects(res, self.correct_url)


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

def setUp(self):
self.shared_test_data = {
Expand All @@ -120,59 +120,59 @@ def setUp(self):
}

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)"""
"""The NewHuntForm 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)
form = NewHuntForm(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 NewHuntForm 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)
form = NewHuntForm(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)
"""The NewHuntForm uses a datetime-local input for the start_time field"""
form = NewHuntForm(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)"""
"""The NewHuntForm 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)
form = NewHuntForm(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 NewHuntForm 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)
form = NewHuntForm(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)
"""The NewHuntForm uses a datetime-local input for the end_time field"""
form = NewHuntForm(data=self.shared_test_data)
end_time_field = form.fields["end_time"]
self.assertEqual(end_time_field.widget.input_type, "datetime-local")
2 changes: 2 additions & 0 deletions myus/myus/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
),
path("register", views.register, name="register"),
path("new", views.new_hunt, name="new_hunt"),
path("hunt/<int:hunt_id>/edit", views.edit_hunt, name="edit_hunt"),
path("hunt/<int:hunt_id>-<slug:slug>/edit", views.edit_hunt, name="edit_hunt"),
path("hunt/<int:hunt_id>", views.view_hunt, name="view_hunt"),
path("hunt/<int:hunt_id>-<slug:slug>", views.view_hunt, name="view_hunt"),
path("hunt/<int:hunt_id>/team", views.my_team, name="my_team"),
Expand Down
56 changes: 48 additions & 8 deletions myus/myus/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@

from django import urls
from django.contrib.auth.decorators import login_required
from django.db.models import OuterRef, Sum, Subquery, Count, Q
from django.db.models.functions import Coalesce
from django.db.models import OuterRef, Sum, Subquery, Count, Q, F
from django.db.models.functions import Coalesce, Greatest
from django.http import Http404, JsonResponse
from django.http import HttpResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.template.loader import render_to_string
from django.views.decorators.csrf import csrf_exempt
from django.core.exceptions import PermissionDenied

import django.urls as urls
import django.forms as forms

from .forms import (
GuessForm,
HuntForm,
NewHuntForm,
EditHuntForm,
InviteMemberForm,
PuzzleForm,
RegisterForm,
Expand Down Expand Up @@ -54,7 +56,7 @@ def register(request):
@login_required
def new_hunt(request):
if request.method == "POST":
form = HuntForm(request.POST)
form = NewHuntForm(request.POST)
if form.is_valid():
hunt = form.save()

Expand All @@ -63,7 +65,7 @@ def new_hunt(request):

return redirect(urls.reverse("view_hunt", args=[hunt.pk, hunt.slug]))
else:
form = HuntForm()
form = NewHuntForm()

return render(
request,
Expand Down Expand Up @@ -207,13 +209,22 @@ def leaderboard(request, hunt_id: int, slug: Optional[str] = None):
.order_by("-time")[:1]
.values("time")
),
).order_by("-score", "-solve_count", "last_solve")
created_or_start=Greatest(F("creation_time"), hunt.start_time),
solve_time=Coalesce(F("last_solve"), F("created_or_start"))
- F("created_or_start"),
)

print(teams.query)
# print(teams.query)
if hunt.leaderboard_style == Hunt.LeaderboardStyle.SPEEDRUN:
teams = teams.order_by("-score", "-solve_count", "solve_time")
template = "leaderboard_SPD.html"
else:
teams = teams.order_by("-score", "-solve_count", "last_solve")
template = "leaderboard.html"

return render(
request,
"leaderboard.html",
template,
{
"hunt": hunt,
"team": team,
Expand Down Expand Up @@ -551,6 +562,35 @@ def edit_puzzle(
)


@login_required
@redirect_from_hunt_id_to_hunt_id_and_slug
def edit_hunt(
request,
hunt_id: int,
slug: Optional[str] = None,
):
user = request.user
hunt = get_object_or_404(Hunt, id=hunt_id)
if not hunt.organizers.filter(id=user.id).exists():
raise PermissionDenied

if request.method == "POST":
form = EditHuntForm(request.POST, instance=hunt)
if form.is_valid():
hunt = form.save()
return redirect(urls.reverse("view_hunt", args=[hunt.pk, hunt.slug]))
else:
form = EditHuntForm(instance=hunt)

return render(
request,
"edit_hunt.html",
{
"form": form,
},
)


@csrf_exempt
def preview_markdown(request):
if request.method == "POST":
Expand Down

0 comments on commit 4ef6698

Please sign in to comment.