From f43cbcbf8422178f31a3f20026b5e820d9c51cd5 Mon Sep 17 00:00:00 2001 From: Jason Cameron Date: Thu, 7 Dec 2023 19:27:55 -0500 Subject: [PATCH 1/7] fix: constraint for user being on a single team not being enforced --- core/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/models.py b/core/models.py index e48c39d..a9b7807 100644 --- a/core/models.py +++ b/core/models.py @@ -191,6 +191,7 @@ def save(self, *args, **kwargs): class Meta: # team.name must be insensitive unique per hunt + unique_together = ['hunt', 'members'] constraints = [ models.UniqueConstraint( fields=["name", "hunt"], From 1689be41b2e6db0efd4904f483291eb6f0feb2ac Mon Sep 17 00:00:00 2001 From: Jason Cameron Date: Thu, 7 Dec 2023 19:30:01 -0500 Subject: [PATCH 2/7] fleshed out owner + added created at field --- core/models.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/core/models.py b/core/models.py index a9b7807..b57742a 100644 --- a/core/models.py +++ b/core/models.py @@ -49,7 +49,15 @@ def generate_hint_key(): class QrCode(models.Model): id = models.AutoField(primary_key=True) - # code = models.CharField(max_length=32, default=secrets.token_urlsafe(32), unique=True) +# creator = models.ForeignKey( +# User, +# on_delete=models.SET_NULL, +# null=True, +# blank=False, +# related_name="qr_codes", +# help_text="User that created the QR code", +# ) + created_at = models.DateTimeField(auto_now_add=True) short = models.CharField( max_length=64, help_text="Short string to remember the place." ) @@ -344,6 +352,14 @@ class Meta: class LogicPuzzleHint(models.Model): id = models.AutoField(primary_key=True) +# creator = models.ForeignKey( +# User, +# on_delete=models.SET_NULL, +# null=True, +# blank=False, +# related_name="logic_hints", +# help_text="User that created the QR code", +# ) hint = models.TextField( max_length=1024, help_text="Hint for the logic puzzle", @@ -388,11 +404,6 @@ class Meta: ] -from django.db.models.signals import m2m_changed -from django.dispatch import receiver -from django.core.exceptions import ValidationError - - @receiver(m2m_changed, sender=Team.members.through) def team_m2m_clean(sender, instance, action, **kwargs): if action == "post_clear": @@ -401,6 +412,6 @@ def team_m2m_clean(sender, instance, action, **kwargs): except: pass elif action == "post_remove": - if instance.is_empty(): + if instance.is_empty: print("Deleting empty team: ", instance.name) instance.delete() From 6297e8a2993dd324655b76968afc40c18459ce90 Mon Sep 17 00:00:00 2001 From: Jason Cameron Date: Sat, 9 Dec 2023 21:43:48 -0500 Subject: [PATCH 3/7] saved incase I want to retain --- ...code_created_at_teammembership_and_more.py | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py diff --git a/core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py b/core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py new file mode 100644 index 0000000..1f9b6c5 --- /dev/null +++ b/core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py @@ -0,0 +1,86 @@ +# Generated by Django 4.1.13 on 2023-12-08 17:43 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +teams = [] + +def migrate_teams_p1(apps, schema_editor): + # Deletes all Team objects and write them + Team = apps.get_model("core", "qrcode") + for team in Team.objects.all(): + teams.append(team.__dict__) + team.delete() + print(teams) + + +def migrate_teams_p2(apps, schema_editor): + # Creates + Team = apps.get_model("core", "Team") + Team.bulk_create(teams) + print(f"added {len(teams)}") + + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0020_alter_logicpuzzlehint_qr_index_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="hint", + options={}, + ), + migrations.AddField( + model_name="qrcode", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.CreateModel( + name="TeamMembership", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "team", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.team" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "unique_together": {("user", "team")}, + }, + ), + migrations.RunPython(migrate_teams_p1, migrate_teams_p2), + migrations.AlterField( + model_name="team", + name="members", + field=models.ManyToManyField( + related_name="teams", + related_query_name="teams", + through="core.TeamMembership", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.RunPython(migrate_teams_p2, migrate_teams_p1), + ] From 74654f5b594054a92f7ba43f90a7ba3af8ceac7b Mon Sep 17 00:00:00 2001 From: Jason Cameron Date: Sat, 9 Dec 2023 21:48:15 -0500 Subject: [PATCH 4/7] added migration to allow for limiting users to one team per hunt note: **WILL REMOVE ALL TEAMS** feat: added created_at for qr codes --- ...code_created_at_teammembership_and_more.py | 24 +++---------------- core/models.py | 14 +++++++---- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py b/core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py index 1f9b6c5..9f1a2aa 100644 --- a/core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py +++ b/core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py @@ -1,26 +1,9 @@ -# Generated by Django 4.1.13 on 2023-12-08 17:43 +# Generated by Django 4.1.13 on 2023-12-10 02:45 from django.conf import settings from django.db import migrations, models import django.db.models.deletion import django.utils.timezone -teams = [] - -def migrate_teams_p1(apps, schema_editor): - # Deletes all Team objects and write them - Team = apps.get_model("core", "qrcode") - for team in Team.objects.all(): - teams.append(team.__dict__) - team.delete() - print(teams) - - -def migrate_teams_p2(apps, schema_editor): - # Creates - Team = apps.get_model("core", "Team") - Team.bulk_create(teams) - print(f"added {len(teams)}") - class Migration(migrations.Migration): @@ -71,8 +54,8 @@ class Migration(migrations.Migration): "unique_together": {("user", "team")}, }, ), - migrations.RunPython(migrate_teams_p1, migrate_teams_p2), - migrations.AlterField( + migrations.RemoveField(model_name="team", name="members"), # added manually + migrations.AddField( # changed from alter to add model_name="team", name="members", field=models.ManyToManyField( @@ -82,5 +65,4 @@ class Migration(migrations.Migration): to=settings.AUTH_USER_MODEL, ), ), - migrations.RunPython(migrate_teams_p2, migrate_teams_p1), ] diff --git a/core/models.py b/core/models.py index b57742a..fc0161f 100644 --- a/core/models.py +++ b/core/models.py @@ -145,7 +145,14 @@ class Hint(models.Model): def __str__(self): return self.hint - +class TeamMembership(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + team = models.ForeignKey("Team", on_delete=models.CASCADE) + + class Meta: + # Create a unique constraint to enforce one team per user per hunt + unique_together = ('user', 'team') + class Team(models.Model): # owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name="teams_ownership") potentially add this later id = models.AutoField(primary_key=True) @@ -155,8 +162,8 @@ class Team(models.Model): ) # todo use this field to have a club-like page so you can join an open team (future feature) current_qr_i = models.IntegerField(default=0) solo = models.BooleanField(default=False) - members = models.ManyToManyField( - related_name="teams", related_query_name="teams", to=User + members = models.ManyToManyField(User, + related_name="teams", related_query_name="teams", through=TeamMembership ) hunt = models.ForeignKey("Hunt", on_delete=models.CASCADE, related_name="teams") @@ -199,7 +206,6 @@ def save(self, *args, **kwargs): class Meta: # team.name must be insensitive unique per hunt - unique_together = ['hunt', 'members'] constraints = [ models.UniqueConstraint( fields=["name", "hunt"], From 55b08f471c586e544c15290500eca0aca7cb0969 Mon Sep 17 00:00:00 2001 From: Jason Cameron Date: Sat, 9 Dec 2023 21:56:20 -0500 Subject: [PATCH 5/7] updated the migration to delete all teams. --- ...int_options_qrcode_created_at_teammembership_and_more.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py b/core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py index 9f1a2aa..4cccdba 100644 --- a/core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py +++ b/core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py @@ -2,10 +2,13 @@ from django.conf import settings from django.db import migrations, models -import django.db.models.deletion import django.utils.timezone +def clear_teams(apps, schema_editor): + Team = apps.get_model("core", "team") + Team.objects.all().delete() # delete all teams IRREVERSIBLE + class Migration(migrations.Migration): dependencies = [ ("core", "0020_alter_logicpuzzlehint_qr_index_and_more"), @@ -54,6 +57,7 @@ class Migration(migrations.Migration): "unique_together": {("user", "team")}, }, ), + migrations.RunPython(clear_teams, reverse_code=None), # if you want to "reverse" change this to migrations.RunPython.noop though it will not bring back the teams migrations.RemoveField(model_name="team", name="members"), # added manually migrations.AddField( # changed from alter to add model_name="team", From f2d522fc95a1ddb01cdd54d03a2f64fa3e61f3d8 Mon Sep 17 00:00:00 2001 From: Jason Cameron Date: Sat, 9 Dec 2023 22:12:51 -0500 Subject: [PATCH 6/7] finalizes fixing team logic (next up: remove solo) --- core/models.py | 13 ++++++++++--- core/views/team.py | 20 +++++--------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/core/models.py b/core/models.py index fc0161f..a013a94 100644 --- a/core/models.py +++ b/core/models.py @@ -42,7 +42,8 @@ def in_team(self) -> bool: """Returns True if the user is in a team for the current or upcoming hunt.""" return self.current_team is not None - + + def generate_hint_key(): return secrets.token_urlsafe(48) @@ -166,7 +167,13 @@ class Team(models.Model): related_name="teams", related_query_name="teams", through=TeamMembership ) hunt = models.ForeignKey("Hunt", on_delete=models.CASCADE, related_name="teams") - + + def leave(self, member: User): + if self.members.filter(id=member.id).first() is None: + raise IndexError("User is not in team") + # remove the member + self.members.through.remove(member) + def update_current_qr_i(self, i: int): self.current_qr_i = max(self.current_qr_i, i) self.save() @@ -184,7 +191,7 @@ def join(self, user: User): return if self.is_full: raise IndexError("Team is full") - self.members.add(user) + self.members.through.add(user) def invites(self): return Invite.objects.filter(team=self) diff --git a/core/views/team.py b/core/views/team.py index 62d94b6..bb65650 100644 --- a/core/views/team.py +++ b/core/views/team.py @@ -63,7 +63,10 @@ def make(request): raw.hunt = Hunt.current_hunt() or Hunt.next_hunt() raw: Team = form.save() - raw.members.add(request.user) + if request.user.in_team: + team = Team.objects.get(id=request.user.current_team.id) + team.leave(request.user) + raw.join(request.user) Invite.objects.get_or_create( team=raw, code=generate_invite_code(), invites=0 ) @@ -76,20 +79,7 @@ def make(request): else: form = TeamMakeForm() return render(request, "core/team_new.html", dict(form=form)) - - -@login_required -@upcoming_hunt_required -@block_if_current_hunt -def solo(q: HttpRequest): - hunt_ = Hunt.current_hunt() or Hunt.next_hunt() - team_ = Team.objects.create( - solo=True, hunt=hunt_, name=f"{q.user.username}'s Solo Team" - ) - team_.members.add(q.user) - - return redirect(reverse("index")) - + @login_required @require_http_methods(["GET"]) From befd9442d6d066649b05c4a2e94a05ec250638a8 Mon Sep 17 00:00:00 2001 From: Jason Cameron Date: Sat, 9 Dec 2023 22:13:37 -0500 Subject: [PATCH 7/7] fmt --- ...code_created_at_teammembership_and_more.py | 13 +++-- core/models.py | 51 ++++++++++--------- core/views/team.py | 2 +- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py b/core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py index 4cccdba..8f36bc0 100644 --- a/core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py +++ b/core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py @@ -7,8 +7,9 @@ def clear_teams(apps, schema_editor): Team = apps.get_model("core", "team") - Team.objects.all().delete() # delete all teams IRREVERSIBLE - + Team.objects.all().delete() # delete all teams IRREVERSIBLE + + class Migration(migrations.Migration): dependencies = [ ("core", "0020_alter_logicpuzzlehint_qr_index_and_more"), @@ -57,9 +58,11 @@ class Migration(migrations.Migration): "unique_together": {("user", "team")}, }, ), - migrations.RunPython(clear_teams, reverse_code=None), # if you want to "reverse" change this to migrations.RunPython.noop though it will not bring back the teams - migrations.RemoveField(model_name="team", name="members"), # added manually - migrations.AddField( # changed from alter to add + migrations.RunPython( + clear_teams, reverse_code=None + ), # if you want to "reverse" change this to migrations.RunPython.noop though it will not bring back the teams + migrations.RemoveField(model_name="team", name="members"), # added manually + migrations.AddField( # changed from alter to add model_name="team", name="members", field=models.ManyToManyField( diff --git a/core/models.py b/core/models.py index a013a94..ba79f56 100644 --- a/core/models.py +++ b/core/models.py @@ -42,22 +42,21 @@ def in_team(self) -> bool: """Returns True if the user is in a team for the current or upcoming hunt.""" return self.current_team is not None - - + def generate_hint_key(): return secrets.token_urlsafe(48) class QrCode(models.Model): id = models.AutoField(primary_key=True) -# creator = models.ForeignKey( -# User, -# on_delete=models.SET_NULL, -# null=True, -# blank=False, -# related_name="qr_codes", -# help_text="User that created the QR code", -# ) + # creator = models.ForeignKey( + # User, + # on_delete=models.SET_NULL, + # null=True, + # blank=False, + # related_name="qr_codes", + # help_text="User that created the QR code", + # ) created_at = models.DateTimeField(auto_now_add=True) short = models.CharField( max_length=64, help_text="Short string to remember the place." @@ -146,14 +145,16 @@ class Hint(models.Model): def __str__(self): return self.hint + class TeamMembership(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) team = models.ForeignKey("Team", on_delete=models.CASCADE) - + class Meta: # Create a unique constraint to enforce one team per user per hunt - unique_together = ('user', 'team') - + unique_together = ("user", "team") + + class Team(models.Model): # owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name="teams_ownership") potentially add this later id = models.AutoField(primary_key=True) @@ -163,17 +164,17 @@ class Team(models.Model): ) # todo use this field to have a club-like page so you can join an open team (future feature) current_qr_i = models.IntegerField(default=0) solo = models.BooleanField(default=False) - members = models.ManyToManyField(User, - related_name="teams", related_query_name="teams", through=TeamMembership + members = models.ManyToManyField( + User, related_name="teams", related_query_name="teams", through=TeamMembership ) hunt = models.ForeignKey("Hunt", on_delete=models.CASCADE, related_name="teams") - + def leave(self, member: User): if self.members.filter(id=member.id).first() is None: raise IndexError("User is not in team") # remove the member self.members.through.remove(member) - + def update_current_qr_i(self, i: int): self.current_qr_i = max(self.current_qr_i, i) self.save() @@ -365,14 +366,14 @@ class Meta: class LogicPuzzleHint(models.Model): id = models.AutoField(primary_key=True) -# creator = models.ForeignKey( -# User, -# on_delete=models.SET_NULL, -# null=True, -# blank=False, -# related_name="logic_hints", -# help_text="User that created the QR code", -# ) + # creator = models.ForeignKey( + # User, + # on_delete=models.SET_NULL, + # null=True, + # blank=False, + # related_name="logic_hints", + # help_text="User that created the QR code", + # ) hint = models.TextField( max_length=1024, help_text="Hint for the logic puzzle", diff --git a/core/views/team.py b/core/views/team.py index bb65650..65677f1 100644 --- a/core/views/team.py +++ b/core/views/team.py @@ -79,7 +79,7 @@ def make(request): else: form = TeamMakeForm() return render(request, "core/team_new.html", dict(form=form)) - + @login_required @require_http_methods(["GET"])