From 61e0ec86b708a352d69a62f7c2566e7f03748542 Mon Sep 17 00:00:00 2001 From: Jay Qi <2721979+jayqi@users.noreply.github.com> Date: Sat, 20 Jul 2024 15:20:54 -0400 Subject: [PATCH] Add team views, add team demo data (#4) * Add team views, add team demo data * Fix lint * Remove leftover script imports * Fix more lint --------- Co-authored-by: Jay Qi --- .../management/commands/create_demo_data.py | 48 ++++++ huntsite/puzzles/factories.py | 4 +- .../management/commands/create_demo_data.py | 14 -- huntsite/puzzles/signals.py | 0 huntsite/teams/factories.py | 34 ++++- ...members_alter_teamprofile_user_and_more.py | 28 ++++ huntsite/teams/models.py | 18 ++- huntsite/teams/signals.py | 11 -- huntsite/teams/urls.py | 3 +- huntsite/teams/views.py | 30 ++-- huntsite/urls.py | 3 +- huntsite/views.py | 14 ++ templates/base.html | 144 +++++++++--------- templates/partials/puzzle_guess_list.html | 18 +-- .../timestamp_localize_libraries.html | 6 + .../partials/timestamp_localize_script.html | 7 + templates/puzzle_detail.html | 73 +++++---- templates/team_detail.html | 46 ++++++ templates/team_list.html | 28 ++++ 19 files changed, 360 insertions(+), 169 deletions(-) create mode 100644 huntsite/management/commands/create_demo_data.py delete mode 100644 huntsite/puzzles/management/commands/create_demo_data.py delete mode 100644 huntsite/puzzles/signals.py create mode 100644 huntsite/teams/migrations/0002_teamprofile_members_alter_teamprofile_user_and_more.py delete mode 100644 huntsite/teams/signals.py create mode 100644 templates/partials/timestamp_localize_libraries.html create mode 100644 templates/partials/timestamp_localize_script.html create mode 100644 templates/team_detail.html create mode 100644 templates/team_list.html diff --git a/huntsite/management/commands/create_demo_data.py b/huntsite/management/commands/create_demo_data.py new file mode 100644 index 0000000..4cf440d --- /dev/null +++ b/huntsite/management/commands/create_demo_data.py @@ -0,0 +1,48 @@ +import random + +from django.core.management.base import BaseCommand +from django.db import transaction + +from huntsite.puzzles import factories as puzzle_factories +from huntsite.puzzles import services as puzzle_services +from huntsite.teams import factories as team_factories + +PUZZLE_STARTED_PROP = 0.4 +PUZZLE_SOLVED_PROP = 0.75 +INCORRECTED_GUESS_MAX = 10 + + +class Command(BaseCommand): + def handle(self, *args, **options): + with transaction.atomic(): + # Puzzles + puzzles = [] + for i in range(24): + puzzles.append(puzzle_factories.PuzzleFactory(calendar_entry__day=i + 1)) + + # Teams + users = [] + for i in range(10): + users.append( + team_factories.UserFactory( + username=f"user{i}", password="hohohomerrychristmas!" + ) + ) + + # Make guesses + for puzzle in puzzles: + for user in users: + if random.random() < PUZZLE_STARTED_PROP: + # Incorrect guesses + for _ in range(random.randint(0, INCORRECTED_GUESS_MAX)): + puzzle_services.guess_submit( + puzzle=puzzle, + user=user, + guess_text=puzzle_factories.answer_text_factory(), + ) + if random.random() < PUZZLE_SOLVED_PROP: + puzzle_services.guess_submit( + puzzle=puzzle, user=user, guess_text=puzzle.answer + ) + + self.stdout.write(self.style.SUCCESS("create_demo_data complete.")) diff --git a/huntsite/puzzles/factories.py b/huntsite/puzzles/factories.py index 3031c2d..7c1b28b 100644 --- a/huntsite/puzzles/factories.py +++ b/huntsite/puzzles/factories.py @@ -14,12 +14,12 @@ ] -def title_text_factory(instance) -> str: +def title_text_factory(instance=None) -> str: nb = random.randint(1, 3) return " ".join(fake.words(nb=nb)).title() -def answer_text_factory(instance) -> str: +def answer_text_factory(instance=None) -> str: nb = random.randint(1, 2) return " ".join(fake.words(nb=nb)).upper() diff --git a/huntsite/puzzles/management/commands/create_demo_data.py b/huntsite/puzzles/management/commands/create_demo_data.py deleted file mode 100644 index 2bb813b..0000000 --- a/huntsite/puzzles/management/commands/create_demo_data.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.core.management.base import BaseCommand -from django.db import transaction - -from huntsite.puzzles.factories import PuzzleFactory - - -class Command(BaseCommand): - def handle(self, *args, **options): - with transaction.atomic(): - # Puzzles - for i in range(24): - PuzzleFactory(calendar_entry__day=i + 1) - - self.stdout.write(self.style.SUCCESS("create_demo_data complete.")) diff --git a/huntsite/puzzles/signals.py b/huntsite/puzzles/signals.py deleted file mode 100644 index e69de29..0000000 diff --git a/huntsite/teams/factories.py b/huntsite/teams/factories.py index d92b645..fbf1345 100644 --- a/huntsite/teams/factories.py +++ b/huntsite/teams/factories.py @@ -1,20 +1,46 @@ +import random + from django.conf import settings +from django.db.models.signals import post_save import factory +from faker import Faker + +fake = Faker() + + +def team_name_text_factory(instance=None) -> str: + nb = random.randint(1, 3) + return " ".join(fake.words(nb=nb)).title() +def team_members_text_factory(instance=None) -> str: + nb = random.randint(1, 3) + return ", ".join(fake.first_name() for _ in range(nb)) + + +@factory.django.mute_signals(post_save) class UserFactory(factory.django.DjangoModelFactory): class Meta: model = settings.AUTH_USER_MODEL username = factory.Faker("user_name") email = factory.Faker("email") - password = factory.Faker("password") - team_name = factory.Faker("word") + team_name = factory.lazy_attribute(team_name_text_factory) + profile = factory.RelatedFactory("huntsite.teams.factories.TeamProfileFactory", "user") + + @factory.post_generation + def password(obj, create, extracted, **kwargs): + if extracted: + obj.set_password(extracted) + else: + obj.set_unusable_password() +@factory.django.mute_signals(post_save) class TeamProfileFactory(factory.django.DjangoModelFactory): class Meta: - model = "teams.Team" + model = "teams.TeamProfile" - user = factory.SubFactory(UserFactory) + user = factory.SubFactory(UserFactory, profile=None) + members = factory.lazy_attribute(team_members_text_factory) diff --git a/huntsite/teams/migrations/0002_teamprofile_members_alter_teamprofile_user_and_more.py b/huntsite/teams/migrations/0002_teamprofile_members_alter_teamprofile_user_and_more.py new file mode 100644 index 0000000..e795bc8 --- /dev/null +++ b/huntsite/teams/migrations/0002_teamprofile_members_alter_teamprofile_user_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.4 on 2024-07-20 14:23 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='teamprofile', + name='members', + field=models.CharField(blank=True, help_text='List of team members.', max_length=255), + ), + migrations.AlterField( + model_name='teamprofile', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='profile', to=settings.AUTH_USER_MODEL), + ), + migrations.DeleteModel( + name='TeamMember', + ), + ] diff --git a/huntsite/teams/models.py b/huntsite/teams/models.py index 9ff0967..966d6f3 100644 --- a/huntsite/teams/models.py +++ b/huntsite/teams/models.py @@ -1,6 +1,8 @@ from django.conf import settings from django.contrib.auth.models import AbstractUser from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver class User(AbstractUser): @@ -15,16 +17,16 @@ class User(AbstractUser): class TeamProfile(models.Model): - user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) + user = models.OneToOneField( + settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="profile" + ) + members = models.CharField(max_length=255, blank=True, help_text="List of team members.") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) -class TeamMember(models.Model): - """A person on a team.""" - - team = models.ForeignKey(TeamProfile, on_delete=models.CASCADE) - - name = models.CharField(max_length=255) - email = models.EmailField(blank=True) +@receiver(post_save, sender=User) +def create_team_profile(sender, instance, created, **kwargs): + if created: + TeamProfile.objects.create(user=instance) diff --git a/huntsite/teams/signals.py b/huntsite/teams/signals.py deleted file mode 100644 index f91f913..0000000 --- a/huntsite/teams/signals.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.conf import settings -from django.db.models.signals import post_save -from django.dispatch import receiver - -import huntsite.teams.models as models - - -@receiver(post_save, sender=settings.AUTH_USER_MODEL) -def create_team_profile(sender, instance, created, **kwargs): - if created: - models.TeamProfile.objects.create(user=instance) diff --git a/huntsite/teams/urls.py b/huntsite/teams/urls.py index dcb85b1..1ba967e 100644 --- a/huntsite/teams/urls.py +++ b/huntsite/teams/urls.py @@ -3,5 +3,6 @@ import huntsite.teams.views as views urlpatterns = [ - path("", views.account_manage, name="account_manage"), + path("", views.team_list, name="team_list"), + path("/", views.team_detail, name="team_detail"), ] diff --git a/huntsite/teams/views.py b/huntsite/teams/views.py index bf0294d..e81d801 100644 --- a/huntsite/teams/views.py +++ b/huntsite/teams/views.py @@ -1,15 +1,27 @@ -from django.contrib.auth.decorators import login_required from django.template.response import TemplateResponse -from huntsite.teams.forms import AccountManagementForm +from huntsite.puzzles import selectors as puzzle_selectors +from huntsite.teams import models -@login_required -def account_manage(request): - """View to manage the account of the user.""" - user = request.user +def team_detail(request, pk: int): + """View to display the team profile of the user.""" + team = models.User.objects.select_related("profile").get(pk=pk) + solves = puzzle_selectors.solve_list(team) context = { - "user": user, - "form": AccountManagementForm(instance=user), + "user": request.user, + "team": team, + "solves": solves, + "is_self": request.user == team, } - return TemplateResponse(request, "account.html", context) + return TemplateResponse(request, "team_detail.html", context) + + +def team_list(request): + """View to display a list of all teams.""" + teams = models.User.objects.select_related("profile").all() + context = { + "user": request.user, + "teams": teams, + } + return TemplateResponse(request, "team_list.html", context) diff --git a/huntsite/urls.py b/huntsite/urls.py index cf00fc2..ad55276 100644 --- a/huntsite/urls.py +++ b/huntsite/urls.py @@ -6,5 +6,6 @@ path("", views.home_page, name="home"), path("about/", views.about_page, name="about"), path("puzzles/", include("huntsite.puzzles.urls")), - path("accounts/", include("huntsite.teams.urls")), + path("teams/", include("huntsite.teams.urls")), + path("accounts/", views.account_manage, name="account_manage"), ] diff --git a/huntsite/views.py b/huntsite/views.py index aa10c14..cfa2262 100644 --- a/huntsite/views.py +++ b/huntsite/views.py @@ -1,5 +1,8 @@ +from django.contrib.auth.decorators import login_required from django.template.response import TemplateResponse +from huntsite.teams.forms import AccountManagementForm + def home_page(request): return TemplateResponse(request, "home.html", {}) @@ -7,3 +10,14 @@ def home_page(request): def about_page(request): return TemplateResponse(request, "about.html", {}) + + +@login_required +def account_manage(request): + """View to manage the account of the user.""" + user = request.user + context = { + "user": user, + "form": AccountManagementForm(instance=user), + } + return TemplateResponse(request, "account.html", context) diff --git a/templates/base.html b/templates/base.html index d7b5876..f3b913e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,77 +1,81 @@ {% load static %} - - - - - Advent Calendar Puzzle Hunt - - - - - - - - - - - +
+ {% block content %}{% endblock %}
- - -
- {% block content %}{% endblock %} -
-
- {% csrf_token %} - -
- +
+ {% csrf_token %} + +
+ diff --git a/templates/partials/puzzle_guess_list.html b/templates/partials/puzzle_guess_list.html index 79fc5e3..0994335 100644 --- a/templates/partials/puzzle_guess_list.html +++ b/templates/partials/puzzle_guess_list.html @@ -1,6 +1,4 @@ -{% if evaluation_message %} -

{{ evaluation_message }}

-{% endif %} +{% if evaluation_message %}

{{ evaluation_message }}

{% endif %} @@ -13,16 +11,12 @@ {% for guess in guesses %} - - + + {% endfor %}
{{ guess.text }}{{ guess.evaluation}}{{ guess.created_at|date:"c" }}{{ guess.evaluation }} + {{ guess.created_at|date:"c" }} +
- +{% include "partials/timestamp_localize_script.html" %} diff --git a/templates/partials/timestamp_localize_libraries.html b/templates/partials/timestamp_localize_libraries.html new file mode 100644 index 0000000..f4dbcae --- /dev/null +++ b/templates/partials/timestamp_localize_libraries.html @@ -0,0 +1,6 @@ + + + diff --git a/templates/partials/timestamp_localize_script.html b/templates/partials/timestamp_localize_script.html new file mode 100644 index 0000000..48ca8f0 --- /dev/null +++ b/templates/partials/timestamp_localize_script.html @@ -0,0 +1,7 @@ + diff --git a/templates/puzzle_detail.html b/templates/puzzle_detail.html index 9ec5650..40d1ae6 100644 --- a/templates/puzzle_detail.html +++ b/templates/puzzle_detail.html @@ -1,52 +1,51 @@ {% extends "base.html" %} {% load crispy_forms_tags %} - {% block content %} - - - - -
-
-

{{ puzzle.name }}

+ + {% include "partials/timestamp_localize_libraries.html" %} +
+
+

{{ puzzle.name }}

+
-
-
-
-
-
-
-
Check answer
-
- {% csrf_token %} -
- {{ form.guess }} - -
-
+
+
+
+
+
+
Check answer
+
+ {% csrf_token %} +
+ {{ form.guess }} + +
+
- {% if guesses %} - {% include "partials/puzzle_guess_list.html" %} - {% endif %} + {% if guesses %} + {% include "partials/puzzle_guess_list.html" %} + {% endif %} +
+
-
-
-
-
- - -

Something prevented us from displaying the puzzle! Click here to download it instead.

-
+ + +

+ Something prevented us from displaying the puzzle! Click here to download it instead. +

+
+
-
{% endblock content %} diff --git a/templates/team_detail.html b/templates/team_detail.html new file mode 100644 index 0000000..c126a01 --- /dev/null +++ b/templates/team_detail.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} +{% load crispy_forms_tags %} +{% block content %} + {% include "partials/timestamp_localize_libraries.html" %} +
+
+

{{ team.team_name }}

+

+ {% if is_self %} + Manage your account + {% endif %} +

+ {% if team.profile.members %} +

+ Team Members: {{ team.profile.members }} +

+ {% endif %} +

Solves

+

+ Total: {{ solves|length }} +

+ + + + + + + + + {% for solve in solves %} + + + + + {% endfor %} + +
PuzzleSolve Time
{{ solve.puzzle.name }} + {{ solve.created_at|date:"c" }} +
+
+
+
+ {% include "partials/timestamp_localize_script.html" %} +{% endblock content %} diff --git a/templates/team_list.html b/templates/team_list.html new file mode 100644 index 0000000..b728e99 --- /dev/null +++ b/templates/team_list.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% block content %} +
+
+

Teams

+
+
+
+
+ + + + + + + + {% for team in teams %} + + + + {% endfor %} + +
Team Name
+ {{ team.team_name }} +
+
+
+{% endblock content %}