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..8f36bc0 --- /dev/null +++ b/core/migrations/0021_alter_hint_options_qrcode_created_at_teammembership_and_more.py @@ -0,0 +1,75 @@ +# 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.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"), + ] + + 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( + 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( + related_name="teams", + related_query_name="teams", + through="core.TeamMembership", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/core/models.py b/core/models.py index e48c39d..ba79f56 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." ) @@ -138,6 +146,15 @@ 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) @@ -148,10 +165,16 @@ class Team(models.Model): current_qr_i = models.IntegerField(default=0) solo = models.BooleanField(default=False) members = models.ManyToManyField( - related_name="teams", related_query_name="teams", to=User + 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() @@ -169,7 +192,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) @@ -343,6 +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", + # ) hint = models.TextField( max_length=1024, help_text="Hint for the logic puzzle", @@ -387,11 +418,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": @@ -400,6 +426,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() diff --git a/core/views/team.py b/core/views/team.py index 62d94b6..65677f1 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 ) @@ -78,19 +81,6 @@ def make(request): 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"]) @team_required