new migrations in apps.epic app; new models, urls, views handle the founder of a New Game inviting a friend via email to a game gatekeeper; ea. may drop coin in any of up to 6 avail. slots; FTs & ITs passing

This commit is contained in:
Disco DeDisco
2026-03-13 18:37:19 -04:00
parent 6a42b91420
commit e0d1f51bf1
8 changed files with 159 additions and 8 deletions

View File

@@ -0,0 +1,27 @@
# Generated by Django 6.0 on 2026-03-13 22:19
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0002_alter_room_renewal_period'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='RoomInvite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('invitee_email', models.EmailField(max_length=254)),
('status', models.CharField(choices=[('PENDING', 'Pending'), ('ACCEPTED', 'Accepted'), ('DECLINED', 'Declined')], default='PENDING', max_length=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('inviter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_invites', to=settings.AUTH_USER_MODEL)),
('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='epic.room')),
],
),
]

View File

@@ -41,6 +41,7 @@ class Room(models.Model):
board_state = models.JSONField(default=dict)
seed_count = models.IntegerField(default=12)
class GateSlot(models.Model):
EMPTY = "EMPTY"
RESERVED = "RESERVED"
@@ -66,6 +67,25 @@ class GateSlot(models.Model):
filled_at = models.DateTimeField(null=True, blank=True)
class RoomInvite(models.Model):
PENDING = "PENDING"
ACCEPTED = "ACCEPTED"
DECLINED = "DECLINED"
STATUS_CHOICES = [
(PENDING, "Pending"),
(ACCEPTED, "Accepted"),
(DECLINED, "Declined"),
]
room = models.ForeignKey(Room, on_delete=models.CASCADE, related_name="invites")
inviter = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="sent_invites"
)
invitee_email = models.EmailField()
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default=PENDING)
created_at = models.DateTimeField(auto_now_add=True)
@receiver(post_save, sender=Room)
def create_gate_slots(sender, instance, created, **kwargs):
if created:

View File

@@ -1,9 +1,10 @@
from datetime import timedelta
from django.db.models import Q
from django.test import TestCase
from django.urls import reverse
from apps.lyric.models import Token, User
from apps.epic.models import Room, GateSlot, debit_token
from apps.epic.models import GateSlot, Room, RoomInvite, debit_token
class RoomCreationTest(TestCase):
@@ -62,3 +63,31 @@ class CoinTokenInUseTest(TestCase):
html = self.coin.tooltip_room_html()
self.assertIn(f'href="{room_url}"', html)
self.assertIn(self.room.name, html)
class RoomInviteTest(TestCase):
def setUp(self):
self.founder = User.objects.create(email="founder@example.com")
self.room = Room.objects.create(name="Dragon's Den", owner=self.founder)
def test_founder_can_invite_by_email(self):
invite = RoomInvite.objects.create(
room=self.room,
inviter=self.founder,
invitee_email="friend@example.com",
)
self.assertEqual(invite.status, RoomInvite.PENDING)
def test_invited_room_appears_in_my_games_queryset(self):
friend = User.objects.create(email="friend@example.com")
RoomInvite.objects.create(
room=self.room,
inviter=self.founder,
invitee_email=friend.email,
)
rooms = Room.objects.filter(
Q(owner=friend) |
Q(gate_slots__gamer=friend) |
Q(invites__invitee_email=friend.email, invites__status=RoomInvite.PENDING)
).distinct()
self.assertIn(self.room, rooms)

View File

@@ -8,5 +8,6 @@ urlpatterns = [
path('rooms/create_room', views.create_room, name='create_room'),
path('room/<uuid:room_id>/gate/', views.gatekeeper, name='gatekeeper'),
path('room/<uuid:room_id>/gate/<int:slot_number>/drop_token', views.drop_token, name='drop_token'),
path('room/<uuid:room_id>/gate/invite', views.invite_gamer, name='invite_gamer'),
]

View File

@@ -1,7 +1,7 @@
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect, render
from apps.epic.models import Room, debit_token
from apps.epic.models import Room, RoomInvite, debit_token
from apps.lyric.models import Token
@@ -17,9 +17,14 @@ def create_room(request):
def gatekeeper(request, room_id):
room = Room.objects.get(id=room_id)
slots = room.gate_slots.order_by("slot_number")
user_has_slot = (
request.user.is_authenticated
and room.gate_slots.filter(gamer=request.user).exists()
)
return render(request, "apps/gameboard/room.html", {
'room': room,
'slots': slots,
"room": room,
"slots": slots,
"user_has_slot": user_has_slot,
})
@login_required
@@ -35,3 +40,17 @@ def drop_token(request, room_id, slot_number):
if token:
debit_token(request.user, slot, token)
return redirect("epic:gatekeeper", room_id=room_id)
@login_required
def invite_gamer(request, room_id):
if request.method == "POST":
room = Room.objects.get(id=room_id)
email = request.POST.get("invitee_email", "").strip()
if email:
RoomInvite.objects.get_or_create(
room=room,
inviter=request.user,
invitee_email=email,
defaults={"status": RoomInvite.PENDING}
)
return redirect("epic:gatekeeper", room_id=room_id)