new model fields & migrations for apps.epic & apps.lyric; new FTs, ITs & UTs passing
; some styling changes effected primarily to _gatekeetper.html modal
This commit is contained in:
18
src/apps/epic/migrations/0004_alter_room_gate_status.py
Normal file
18
src/apps/epic/migrations/0004_alter_room_gate_status.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0 on 2026-03-15 00:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('epic', '0003_roominvite'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='room',
|
||||
name='gate_status',
|
||||
field=models.CharField(choices=[('GATHERING', 'GATHERING GAMERS'), ('OPEN', 'Open'), ('RENEWAL_DUE', 'Renewal Due')], default='GATHERING', max_length=20),
|
||||
),
|
||||
]
|
||||
@@ -93,13 +93,30 @@ def create_gate_slots(sender, instance, created, **kwargs):
|
||||
GateSlot.objects.create(room=instance, slot_number=i)
|
||||
|
||||
|
||||
def select_token(user):
|
||||
if user.is_staff:
|
||||
pass_token = user.tokens.filter(token_type=Token.PASS).first()
|
||||
if pass_token:
|
||||
return pass_token
|
||||
coin = user.tokens.filter(token_type=Token.COIN, current_room__isnull=True).first()
|
||||
if coin:
|
||||
return coin
|
||||
free = user.tokens.filter(
|
||||
token_type=Token.FREE,
|
||||
expires_at__gt=timezone.now(),
|
||||
).order_by("expires_at").first()
|
||||
if free:
|
||||
return free
|
||||
return user.tokens.filter(token_type=Token.TITHE).first()
|
||||
|
||||
|
||||
def debit_token(user, slot, token):
|
||||
if token.token_type == Token.COIN:
|
||||
token.current_room = slot.room
|
||||
period = slot.room.renewal_period or timedelta(days=7)
|
||||
token.next_ready_at = timezone.now() + period
|
||||
token.save()
|
||||
else:
|
||||
elif token.token_type != Token.PASS:
|
||||
token.delete()
|
||||
slot.gamer = user
|
||||
slot.status = GateSlot.FILLED
|
||||
|
||||
@@ -2,9 +2,10 @@ from datetime import timedelta
|
||||
from django.db.models import Q
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.lyric.models import Token, User
|
||||
from apps.epic.models import GateSlot, Room, RoomInvite, debit_token
|
||||
from apps.epic.models import GateSlot, Room, RoomInvite, debit_token, select_token
|
||||
|
||||
|
||||
class RoomCreationTest(TestCase):
|
||||
@@ -77,6 +78,60 @@ class CoinTokenInUseTest(TestCase):
|
||||
self.assertIn(self.room.name, html)
|
||||
|
||||
|
||||
class SelectTokenTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="gamer@test.io")
|
||||
self.other_room = Room.objects.create(name="Other Room", owner=self.user)
|
||||
self.coin = Token.objects.get(user=self.user, token_type=Token.COIN)
|
||||
|
||||
def test_returns_coin_when_available(self):
|
||||
token = select_token(self.user)
|
||||
self.assertEqual(token.token_type, Token.COIN)
|
||||
|
||||
def test_returns_free_token_when_coin_in_use(self):
|
||||
self.coin.current_room = self.other_room
|
||||
self.coin.save()
|
||||
token = select_token(self.user)
|
||||
self.assertEqual(token.token_type, Token.FREE)
|
||||
|
||||
def test_free_token_selection_is_fefo(self):
|
||||
self.coin.current_room = self.other_room
|
||||
self.coin.save()
|
||||
Token.objects.filter(user=self.user, token_type=Token.FREE).delete()
|
||||
soon = Token.objects.create(
|
||||
user=self.user, token_type=Token.FREE,
|
||||
expires_at=timezone.now() + timedelta(days=2),
|
||||
)
|
||||
Token.objects.create(
|
||||
user=self.user, token_type=Token.FREE,
|
||||
expires_at=timezone.now() + timedelta(days=6),
|
||||
)
|
||||
token = select_token(self.user)
|
||||
self.assertEqual(token.pk, soon.pk)
|
||||
|
||||
def test_returns_tithe_when_coin_in_use_and_no_free_tokens(self):
|
||||
self.coin.current_room = self.other_room
|
||||
self.coin.save()
|
||||
Token.objects.filter(user=self.user, token_type=Token.FREE).delete()
|
||||
tithe = Token.objects.create(user=self.user, token_type=Token.TITHE)
|
||||
token = select_token(self.user)
|
||||
self.assertEqual(token.pk, tithe.pk)
|
||||
|
||||
def test_returns_none_when_all_depleted(self):
|
||||
self.coin.current_room = self.other_room
|
||||
self.coin.save()
|
||||
Token.objects.filter(user=self.user, token_type=Token.FREE).delete()
|
||||
token = select_token(self.user)
|
||||
self.assertIsNone(token)
|
||||
|
||||
def test_returns_pass_for_staff(self):
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
pass_token = Token.objects.create(user=self.user, token_type=Token.PASS)
|
||||
token = select_token(self.user)
|
||||
self.assertEqual(token.token_type, Token.PASS)
|
||||
|
||||
|
||||
class RoomInviteTest(TestCase):
|
||||
def setUp(self):
|
||||
self.founder = User.objects.create(email="founder@example.com")
|
||||
|
||||
@@ -229,6 +229,81 @@ class RejectTokenViewTest(TestCase):
|
||||
)
|
||||
|
||||
|
||||
class DropTokenAvailabilityViewTest(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.other_room = Room.objects.create(name="Other Room", owner=owner)
|
||||
self.coin = Token.objects.get(user=self.gamer, token_type=Token.COIN)
|
||||
|
||||
def test_drop_reserves_slot_when_tokens_available(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)
|
||||
# token not debited yet — that happens at confirm
|
||||
self.coin.refresh_from_db()
|
||||
self.assertIsNone(self.coin.current_room)
|
||||
|
||||
def test_drop_returns_402_when_all_tokens_depleted(self):
|
||||
self.coin.current_room = self.other_room
|
||||
self.coin.save()
|
||||
Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete()
|
||||
response = self.client.post(
|
||||
reverse("epic:drop_token", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertEqual(response.status_code, 402)
|
||||
|
||||
|
||||
class ConfirmTokenPriorityViewTest(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.other_room = Room.objects.create(name="Other 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()
|
||||
self.coin = Token.objects.get(user=self.gamer, token_type=Token.COIN)
|
||||
|
||||
def test_confirm_leases_coin_to_room(self):
|
||||
self.client.post(reverse("epic:confirm_token", kwargs={"room_id": self.room.id}))
|
||||
self.coin.refresh_from_db()
|
||||
self.assertEqual(self.coin.current_room, self.room)
|
||||
self.assertTrue(Token.objects.filter(pk=self.coin.pk).exists())
|
||||
|
||||
def test_confirm_uses_free_token_when_coin_in_use(self):
|
||||
self.coin.current_room = self.other_room
|
||||
self.coin.save()
|
||||
self.client.post(reverse("epic:confirm_token", kwargs={"room_id": self.room.id}))
|
||||
self.assertEqual(
|
||||
Token.objects.filter(user=self.gamer, token_type=Token.FREE).count(), 0
|
||||
)
|
||||
self.coin.refresh_from_db()
|
||||
self.assertEqual(self.coin.current_room, self.other_room)
|
||||
|
||||
def test_confirm_uses_tithe_when_free_tokens_exhausted(self):
|
||||
self.coin.current_room = self.other_room
|
||||
self.coin.save()
|
||||
Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete()
|
||||
tithe = Token.objects.create(user=self.gamer, token_type=Token.TITHE)
|
||||
self.client.post(reverse("epic:confirm_token", kwargs={"room_id": self.room.id}))
|
||||
self.assertFalse(Token.objects.filter(pk=tithe.pk).exists())
|
||||
|
||||
def test_pass_not_consumed_and_coin_not_leased(self):
|
||||
self.gamer.is_staff = True
|
||||
self.gamer.save()
|
||||
pass_token = Token.objects.create(user=self.gamer, token_type=Token.PASS)
|
||||
self.client.post(reverse("epic:confirm_token", kwargs={"room_id": self.room.id}))
|
||||
self.assertTrue(Token.objects.filter(pk=pass_token.pk).exists())
|
||||
self.coin.refresh_from_db()
|
||||
self.assertIsNone(self.coin.current_room)
|
||||
|
||||
|
||||
class RoomActionsViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner = User.objects.create(email="owner@test.io")
|
||||
|
||||
@@ -5,8 +5,7 @@ from django.http import HttpResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.epic.models import GateSlot, Room, RoomInvite, debit_token
|
||||
from apps.lyric.models import Token
|
||||
from apps.epic.models import GateSlot, Room, RoomInvite, debit_token, select_token
|
||||
|
||||
|
||||
RESERVE_TIMEOUT = timedelta(seconds=60)
|
||||
@@ -29,12 +28,14 @@ def _gate_context(room, user):
|
||||
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 = (
|
||||
eligible = (
|
||||
user.is_authenticated
|
||||
and pending_slot is None
|
||||
and user_reserved_slot is None
|
||||
and user_filled_slot is None
|
||||
)
|
||||
token_depleted = eligible and select_token(user) is None
|
||||
can_drop = eligible and not token_depleted
|
||||
is_last_slot = (
|
||||
user_reserved_slot is not None
|
||||
and slots.filter(status=GateSlot.EMPTY).count() == 0
|
||||
@@ -46,6 +47,7 @@ def _gate_context(room, user):
|
||||
"user_reserved_slot": user_reserved_slot,
|
||||
"user_filled_slot": user_filled_slot,
|
||||
"can_drop": can_drop,
|
||||
"token_depleted": token_depleted,
|
||||
"is_last_slot": is_last_slot,
|
||||
"user_can_reject": user_can_reject,
|
||||
}
|
||||
@@ -76,6 +78,8 @@ def drop_token(request, room_id):
|
||||
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)
|
||||
if select_token(request.user) is None:
|
||||
return HttpResponse(status=402)
|
||||
slot = room.gate_slots.filter(
|
||||
status=GateSlot.EMPTY
|
||||
).order_by("slot_number").first()
|
||||
@@ -95,11 +99,7 @@ def confirm_token(request, room_id):
|
||||
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()
|
||||
)
|
||||
token = select_token(request.user)
|
||||
if token:
|
||||
debit_token(request.user, slot, token)
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
|
||||
Reference in New Issue
Block a user