various styling & structural changes to unify site themes; token-drop interaction changes across epic urls & views
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.lyric.models import User
|
||||
from apps.epic.models import Room, RoomInvite
|
||||
from apps.lyric.models import Token, User
|
||||
from apps.epic.models import GateSlot, Room, RoomInvite
|
||||
|
||||
|
||||
class RoomCreationViewTest(TestCase):
|
||||
@@ -76,6 +77,158 @@ class GateStatusViewTest(TestCase):
|
||||
self.assertContains(response, "gate-modal")
|
||||
|
||||
|
||||
class DropTokenViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.gamer = User.objects.create(email="gamer@test.io")
|
||||
self.client.force_login(self.gamer)
|
||||
owner = User.objects.create(email="owner@test.io")
|
||||
self.room = Room.objects.create(name="Test Room", owner=owner)
|
||||
|
||||
def test_drop_token_reserves_lowest_empty_slot(self):
|
||||
self.client.post(reverse("epic:drop_token", kwargs={"room_id": self.room.id}))
|
||||
slot = self.room.gate_slots.get(slot_number=1)
|
||||
self.assertEqual(slot.status, GateSlot.RESERVED)
|
||||
self.assertEqual(slot.gamer, self.gamer)
|
||||
|
||||
def test_drop_token_skips_already_filled_slots(self):
|
||||
other = User.objects.create(email="other@test.io")
|
||||
slot1 = self.room.gate_slots.get(slot_number=1)
|
||||
slot1.gamer = other
|
||||
slot1.status = GateSlot.FILLED
|
||||
slot1.save()
|
||||
self.client.post(reverse("epic:drop_token", kwargs={"room_id": self.room.id}))
|
||||
slot2 = self.room.gate_slots.get(slot_number=2)
|
||||
self.assertEqual(slot2.status, GateSlot.RESERVED)
|
||||
self.assertEqual(slot2.gamer, self.gamer)
|
||||
|
||||
def test_drop_token_blocked_when_another_slot_reserved(self):
|
||||
other = User.objects.create(email="other@test.io")
|
||||
slot1 = self.room.gate_slots.get(slot_number=1)
|
||||
slot1.gamer = other
|
||||
slot1.status = GateSlot.RESERVED
|
||||
slot1.reserved_at = timezone.now()
|
||||
slot1.save()
|
||||
self.client.post(reverse("epic:drop_token", kwargs={"room_id": self.room.id}))
|
||||
# Slot 2 should remain EMPTY — lock held by other user
|
||||
slot2 = self.room.gate_slots.get(slot_number=2)
|
||||
self.assertEqual(slot2.status, GateSlot.EMPTY)
|
||||
|
||||
def test_drop_token_blocked_when_user_already_has_filled_slot(self):
|
||||
slot1 = self.room.gate_slots.get(slot_number=1)
|
||||
slot1.gamer = self.gamer
|
||||
slot1.status = GateSlot.FILLED
|
||||
slot1.save()
|
||||
self.client.post(reverse("epic:drop_token", kwargs={"room_id": self.room.id}))
|
||||
slot2 = self.room.gate_slots.get(slot_number=2)
|
||||
self.assertEqual(slot2.status, GateSlot.EMPTY)
|
||||
|
||||
def test_drop_token_sets_reserved_at(self):
|
||||
self.client.post(reverse("epic:drop_token", kwargs={"room_id": self.room.id}))
|
||||
slot = self.room.gate_slots.get(slot_number=1)
|
||||
self.assertIsNotNone(slot.reserved_at)
|
||||
|
||||
def test_drop_token_redirects_to_gatekeeper(self):
|
||||
response = self.client.post(
|
||||
reverse("epic:drop_token", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertRedirects(
|
||||
response, reverse("epic:gatekeeper", args=[self.room.id])
|
||||
)
|
||||
|
||||
|
||||
class ConfirmTokenViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.gamer = User.objects.create(email="gamer@test.io")
|
||||
self.client.force_login(self.gamer)
|
||||
owner = User.objects.create(email="owner@test.io")
|
||||
self.room = Room.objects.create(name="Test Room", owner=owner)
|
||||
self.slot = self.room.gate_slots.get(slot_number=1)
|
||||
self.slot.gamer = self.gamer
|
||||
self.slot.status = GateSlot.RESERVED
|
||||
self.slot.reserved_at = timezone.now()
|
||||
self.slot.save()
|
||||
Token.objects.create(user=self.gamer, token_type=Token.FREE)
|
||||
|
||||
def test_confirm_marks_slot_filled(self):
|
||||
self.client.post(
|
||||
reverse("epic:confirm_token", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.slot.refresh_from_db()
|
||||
self.assertEqual(self.slot.status, GateSlot.FILLED)
|
||||
|
||||
def test_confirm_sets_gate_open_when_all_slots_filled(self):
|
||||
# Fill slots 2–6 via ORM
|
||||
for i in range(2, 7):
|
||||
other = User.objects.create(email=f"g{i}@test.io")
|
||||
s = self.room.gate_slots.get(slot_number=i)
|
||||
s.gamer = other
|
||||
s.status = GateSlot.FILLED
|
||||
s.save()
|
||||
self.client.post(
|
||||
reverse("epic:confirm_token", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.room.refresh_from_db()
|
||||
self.assertEqual(self.room.gate_status, Room.OPEN)
|
||||
|
||||
def test_confirm_redirects_to_gatekeeper(self):
|
||||
response = self.client.post(
|
||||
reverse("epic:confirm_token", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertRedirects(
|
||||
response, reverse("epic:gatekeeper", args=[self.room.id])
|
||||
)
|
||||
|
||||
def test_confirm_does_nothing_without_reserved_slot(self):
|
||||
self.slot.status = GateSlot.EMPTY
|
||||
self.slot.gamer = None
|
||||
self.slot.save()
|
||||
self.client.post(
|
||||
reverse("epic:confirm_token", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.slot.refresh_from_db()
|
||||
self.assertEqual(self.slot.status, GateSlot.EMPTY)
|
||||
|
||||
|
||||
class RejectTokenViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.gamer = User.objects.create(email="gamer@test.io")
|
||||
self.client.force_login(self.gamer)
|
||||
owner = User.objects.create(email="owner@test.io")
|
||||
self.room = Room.objects.create(name="Test Room", owner=owner)
|
||||
self.slot = self.room.gate_slots.get(slot_number=1)
|
||||
self.slot.gamer = self.gamer
|
||||
self.slot.status = GateSlot.RESERVED
|
||||
self.slot.reserved_at = timezone.now()
|
||||
self.slot.save()
|
||||
|
||||
def test_reject_clears_reserved_slot(self):
|
||||
self.client.post(
|
||||
reverse("epic:reject_token", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.slot.refresh_from_db()
|
||||
self.assertEqual(self.slot.status, GateSlot.EMPTY)
|
||||
self.assertIsNone(self.slot.gamer)
|
||||
self.assertIsNone(self.slot.reserved_at)
|
||||
|
||||
def test_reject_after_confirm_clears_filled_slot(self):
|
||||
self.slot.status = GateSlot.FILLED
|
||||
self.slot.save()
|
||||
self.client.post(
|
||||
reverse("epic:reject_token", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.slot.refresh_from_db()
|
||||
self.assertEqual(self.slot.status, GateSlot.EMPTY)
|
||||
self.assertIsNone(self.slot.gamer)
|
||||
|
||||
def test_reject_redirects_to_gatekeeper(self):
|
||||
response = self.client.post(
|
||||
reverse("epic:reject_token", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertRedirects(
|
||||
response, reverse("epic:gatekeeper", args=[self.room.id])
|
||||
)
|
||||
|
||||
|
||||
class RoomActionsViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner = User.objects.create(email="owner@test.io")
|
||||
|
||||
@@ -7,10 +7,11 @@ app_name = 'epic'
|
||||
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/drop_token', views.drop_token, name='drop_token'),
|
||||
path('room/<uuid:room_id>/gate/confirm_token', views.confirm_token, name='confirm_token'),
|
||||
path('room/<uuid:room_id>/gate/reject_token', views.reject_token, name='reject_token'),
|
||||
path('room/<uuid:room_id>/gate/invite', views.invite_gamer, name='invite_gamer'),
|
||||
path('room/<uuid:room_id>/gate/status', views.gate_status, name='gate_status'),
|
||||
path('room/<uuid:room_id>/delete', views.delete_room, name='delete_room'),
|
||||
path('room/<uuid:room_id>/abandon', views.abandon_room, name='abandon_room'),
|
||||
]
|
||||
|
||||
|
||||
@@ -1,11 +1,56 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.epic.models import Room, RoomInvite, debit_token
|
||||
from apps.epic.models import GateSlot, Room, RoomInvite, debit_token
|
||||
from apps.lyric.models import Token
|
||||
|
||||
|
||||
RESERVE_TIMEOUT = timedelta(seconds=60)
|
||||
|
||||
|
||||
def _expire_reserved_slots(room):
|
||||
cutoff = timezone.now() - RESERVE_TIMEOUT
|
||||
room.gate_slots.filter(
|
||||
status=GateSlot.RESERVED,
|
||||
reserved_at__lt=cutoff,
|
||||
).update(status=GateSlot.EMPTY, gamer=None, reserved_at=None)
|
||||
|
||||
|
||||
def _gate_context(room, user):
|
||||
_expire_reserved_slots(room)
|
||||
slots = room.gate_slots.order_by("slot_number")
|
||||
pending_slot = slots.filter(status=GateSlot.RESERVED).first()
|
||||
user_reserved_slot = None
|
||||
user_filled_slot = None
|
||||
if user.is_authenticated:
|
||||
user_reserved_slot = slots.filter(gamer=user, status=GateSlot.RESERVED).first()
|
||||
user_filled_slot = slots.filter(gamer=user, status=GateSlot.FILLED).first()
|
||||
can_drop = (
|
||||
user.is_authenticated
|
||||
and pending_slot is None
|
||||
and user_reserved_slot is None
|
||||
and user_filled_slot is None
|
||||
)
|
||||
is_last_slot = (
|
||||
user_reserved_slot is not None
|
||||
and slots.filter(status=GateSlot.EMPTY).count() == 0
|
||||
)
|
||||
user_can_reject = user_reserved_slot is not None or user_filled_slot is not None
|
||||
return {
|
||||
"slots": slots,
|
||||
"pending_slot": pending_slot,
|
||||
"user_reserved_slot": user_reserved_slot,
|
||||
"user_filled_slot": user_filled_slot,
|
||||
"can_drop": can_drop,
|
||||
"is_last_slot": is_last_slot,
|
||||
"user_can_reject": user_can_reject,
|
||||
}
|
||||
|
||||
|
||||
@login_required
|
||||
def create_room(request):
|
||||
if request.method == "POST":
|
||||
@@ -15,33 +60,68 @@ def create_room(request):
|
||||
return redirect("epic:gatekeeper", room_id=room.id)
|
||||
return redirect("/gameboard/")
|
||||
|
||||
|
||||
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,
|
||||
"user_has_slot": user_has_slot,
|
||||
})
|
||||
ctx = _gate_context(room, request.user)
|
||||
ctx["room"] = room
|
||||
return render(request, "apps/gameboard/room.html", ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
def drop_token(request, room_id, slot_number):
|
||||
def drop_token(request, room_id):
|
||||
if request.method == "POST":
|
||||
room = Room.objects.get(id=room_id)
|
||||
slot = room.gate_slots.get(slot_number=slot_number)
|
||||
token = (
|
||||
request.user.tokens.filter(token_type=Token.COIN).first()
|
||||
or request.user.tokens.filter(token_type=Token.FREE).first()
|
||||
or request.user.tokens.filter(token_type=Token.TITHE).first()
|
||||
)
|
||||
if token:
|
||||
debit_token(request.user, slot, token)
|
||||
if room.gate_slots.filter(status=GateSlot.RESERVED).exists():
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
if room.gate_slots.filter(gamer=request.user, status=GateSlot.FILLED).exists():
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
slot = room.gate_slots.filter(
|
||||
status=GateSlot.EMPTY
|
||||
).order_by("slot_number").first()
|
||||
if slot:
|
||||
slot.gamer = request.user
|
||||
slot.status = GateSlot.RESERVED
|
||||
slot.reserved_at = timezone.now()
|
||||
slot.save()
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
|
||||
|
||||
@login_required
|
||||
def confirm_token(request, room_id):
|
||||
if request.method == "POST":
|
||||
room = Room.objects.get(id=room_id)
|
||||
slot = room.gate_slots.filter(
|
||||
gamer=request.user, status=GateSlot.RESERVED
|
||||
).first()
|
||||
if slot:
|
||||
token = (
|
||||
request.user.tokens.filter(token_type=Token.COIN).first()
|
||||
or request.user.tokens.filter(token_type=Token.FREE).first()
|
||||
or request.user.tokens.filter(token_type=Token.TITHE).first()
|
||||
)
|
||||
if token:
|
||||
debit_token(request.user, slot, token)
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
|
||||
|
||||
@login_required
|
||||
def reject_token(request, room_id):
|
||||
if request.method == "POST":
|
||||
room = Room.objects.get(id=room_id)
|
||||
slot = room.gate_slots.filter(
|
||||
gamer=request.user,
|
||||
status__in=[GateSlot.RESERVED, GateSlot.FILLED],
|
||||
).first()
|
||||
if slot:
|
||||
slot.gamer = None
|
||||
slot.status = GateSlot.EMPTY
|
||||
slot.reserved_at = None
|
||||
slot.filled_at = None
|
||||
slot.save()
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
|
||||
|
||||
@login_required
|
||||
def invite_gamer(request, room_id):
|
||||
if request.method == "POST":
|
||||
@@ -56,6 +136,7 @@ def invite_gamer(request, room_id):
|
||||
)
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
|
||||
|
||||
@login_required
|
||||
def delete_room(request, room_id):
|
||||
if request.method == "POST":
|
||||
@@ -64,6 +145,7 @@ def delete_room(request, room_id):
|
||||
room.delete()
|
||||
return redirect("/gameboard/")
|
||||
|
||||
|
||||
@login_required
|
||||
def abandon_room(request, room_id):
|
||||
if request.method == "POST":
|
||||
@@ -77,17 +159,11 @@ def abandon_room(request, room_id):
|
||||
).delete()
|
||||
return redirect("/gameboard/")
|
||||
|
||||
|
||||
def gate_status(request, room_id):
|
||||
room = Room.objects.get(id=room_id)
|
||||
if room.gate_status == Room.OPEN:
|
||||
return HttpResponse("")
|
||||
slots = room.gate_slots.order_by("slot_number")
|
||||
user_has_slot = (
|
||||
request.user.is_authenticated
|
||||
and slots.filter(gamer=request.user).exists()
|
||||
)
|
||||
return render(request, "apps/gameboard/_partials/_gatekeeper.html", {
|
||||
"room": room,
|
||||
"slots": slots,
|
||||
"user_has_slot": user_has_slot,
|
||||
})
|
||||
ctx = _gate_context(room, request.user)
|
||||
ctx["room"] = room
|
||||
return render(request, "apps/gameboard/_partials/_gatekeeper.html", ctx)
|
||||
|
||||
Reference in New Issue
Block a user