Compare commits
4 Commits
4da8750c60
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3800c5bdad | ||
|
|
12d575a84b | ||
|
|
c14b6d7062 | ||
|
|
a7c5468cbc |
23
src/apps/epic/migrations/0030_sigreservation_seat_fk.py
Normal file
23
src/apps/epic/migrations/0030_sigreservation_seat_fk.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('epic', '0029_fix_schizo_cautions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sigreservation',
|
||||
name='seat',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='sig_reservation',
|
||||
to='epic.tableseat',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -359,6 +359,10 @@ class SigReservation(models.Model):
|
||||
gamer = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='sig_reservations'
|
||||
)
|
||||
seat = models.ForeignKey(
|
||||
'TableSeat', null=True, blank=True,
|
||||
on_delete=models.SET_NULL, related_name='sig_reservation',
|
||||
)
|
||||
card = models.ForeignKey(
|
||||
'TarotCard', on_delete=models.CASCADE, related_name='sig_reservations'
|
||||
)
|
||||
|
||||
@@ -10,8 +10,11 @@ from django.shortcuts import redirect, render
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.drama.models import GameEvent, record
|
||||
from django.db.models import Case, IntegerField, Value, When
|
||||
|
||||
from apps.epic.models import (
|
||||
GateSlot, Room, RoomInvite, SigReservation, TableSeat, TarotCard, TarotDeck,
|
||||
GateSlot, Room, RoomInvite, SIG_SEAT_ORDER, SigReservation, TableSeat,
|
||||
TarotCard, TarotDeck,
|
||||
active_sig_seat, debit_token, levity_sig_cards, gravity_sig_cards,
|
||||
select_token, sig_deck_cards,
|
||||
)
|
||||
@@ -92,6 +95,22 @@ def _notify_sig_reserved(room_id, card_id, role, reserved):
|
||||
|
||||
SLOT_ROLE_LABELS = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
|
||||
|
||||
_SIG_SEAT_ORDERING = Case(
|
||||
*[When(role=r, then=Value(i)) for i, r in enumerate(SIG_SEAT_ORDER)],
|
||||
default=Value(99),
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
|
||||
|
||||
def _canonical_user_seat(room, user):
|
||||
"""Return the user's seat whose role comes first in PC→NC→EC→SC→AC→BC order.
|
||||
|
||||
In normal play (one user = one seat) this is equivalent to .first().
|
||||
For Carte Blanche (one user = all seats) it returns the PC seat, ensuring
|
||||
sig-select cursor placement is seat-based, not position/slot-based.
|
||||
"""
|
||||
return room.table_seats.filter(gamer=user).order_by(_SIG_SEAT_ORDERING).first()
|
||||
|
||||
_ROLE_SCRAWL_NAMES = {
|
||||
"PC": "Player", "NC": "Narrator", "EC": "Economist",
|
||||
"SC": "Shepherd", "AC": "Alchemist", "BC": "Builder",
|
||||
@@ -242,7 +261,7 @@ def _role_select_context(room, user):
|
||||
"slots": room.gate_slots.order_by("slot_number"),
|
||||
}
|
||||
if room.table_status == Room.SIG_SELECT:
|
||||
user_seat = room.table_seats.filter(gamer=user).first() if user.is_authenticated else None
|
||||
user_seat = _canonical_user_seat(room, user) if user.is_authenticated else None
|
||||
user_role = user_seat.role if user_seat else None
|
||||
user_polarity = None
|
||||
if user_role in _LEVITY_ROLES:
|
||||
@@ -583,7 +602,7 @@ def sig_reserve(request, room_id):
|
||||
if room.table_status != Room.SIG_SELECT:
|
||||
return HttpResponse(status=400)
|
||||
|
||||
user_seat = room.table_seats.filter(gamer=request.user).first()
|
||||
user_seat = _canonical_user_seat(room, request.user)
|
||||
if not user_seat or not user_seat.role:
|
||||
return HttpResponse(status=403)
|
||||
|
||||
@@ -622,7 +641,7 @@ def sig_reserve(request, room_id):
|
||||
|
||||
SigReservation.objects.create(
|
||||
room=room, gamer=request.user, card=card,
|
||||
role=user_seat.role, polarity=polarity,
|
||||
seat=user_seat, role=user_seat.role, polarity=polarity,
|
||||
)
|
||||
_notify_sig_reserved(room_id, card.pk, user_seat.role, reserved=True)
|
||||
return HttpResponse(status=200)
|
||||
|
||||
@@ -57,13 +57,13 @@ INSTALLED_APPS = [
|
||||
# Board apps
|
||||
'apps.dashboard',
|
||||
'apps.gameboard',
|
||||
'apps.billboard',
|
||||
# Gamer apps
|
||||
'apps.lyric',
|
||||
'apps.epic',
|
||||
'apps.drama',
|
||||
'apps.billboard',
|
||||
'apps.ap',
|
||||
# Custom apps
|
||||
'apps.ap',
|
||||
'apps.api',
|
||||
'apps.applets',
|
||||
'functional_tests',
|
||||
|
||||
@@ -34,14 +34,15 @@ def _assign_all_roles(room, role_order=None):
|
||||
slug="earthman",
|
||||
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
||||
)
|
||||
# Seed the 18 sig deck cards (migration data is flushed in TransactionTestCase FTs)
|
||||
# Seed the 18 sig deck cards (migration data is flushed in TransactionTestCase FTs).
|
||||
# _sig_unique_cards() filters arcana=MIDDLE, suits BRANDS/CROWNS/BLADES/GRAILS (Earthman).
|
||||
_NAME = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"}
|
||||
for suit in ("WANDS", "PENTACLES", "SWORDS", "CUPS"):
|
||||
for suit in ("BRANDS", "CROWNS", "BLADES", "GRAILS"):
|
||||
for number in (11, 12, 13, 14):
|
||||
TarotCard.objects.get_or_create(
|
||||
deck_variant=earthman,
|
||||
slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em",
|
||||
defaults={"arcana": "MINOR", "suit": suit, "number": number,
|
||||
defaults={"arcana": "MIDDLE", "suit": suit, "number": number,
|
||||
"name": f"{_NAME[number]} of {suit.capitalize()}"},
|
||||
)
|
||||
for number, name, slug in [
|
||||
@@ -257,13 +258,13 @@ class SigSelectChannelsTest(ChannelsFunctionalTest):
|
||||
)
|
||||
|
||||
reserved_card = self.browser.find_element(By.CSS_SELECTOR, reserved_card_sel)
|
||||
border_color = self.browser.execute_script(
|
||||
"return window.getComputedStyle(arguments[0]).borderTopColor",
|
||||
box_shadow = self.browser.execute_script(
|
||||
"return window.getComputedStyle(arguments[0]).boxShadow",
|
||||
reserved_card,
|
||||
)
|
||||
self.assertEqual(
|
||||
border_color, "rgb(255, 207, 52)",
|
||||
f"Expected --priYl border for NC reservation, got {border_color}",
|
||||
self.assertIn(
|
||||
"255, 207, 52", box_shadow,
|
||||
f"Expected --priYl (255,207,52) in box-shadow for NC reservation, got {box_shadow}",
|
||||
)
|
||||
|
||||
finally:
|
||||
@@ -311,7 +312,7 @@ class SigSelectThemeTest(FunctionalTest):
|
||||
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
||||
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
|
||||
|
||||
self._hover_card('.sig-card[data-arcana="Minor Arcana"]')
|
||||
self._hover_card('.sig-card[data-arcana="Middle Arcana"]')
|
||||
|
||||
above = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
|
||||
@@ -346,7 +347,7 @@ class SigSelectThemeTest(FunctionalTest):
|
||||
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
||||
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
|
||||
|
||||
self._hover_card('.sig-card[data-arcana="Minor Arcana"]')
|
||||
self._hover_card('.sig-card[data-arcana="Middle Arcana"]')
|
||||
|
||||
above = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
|
||||
|
||||
@@ -507,7 +507,7 @@ html:has(.sig-backdrop) {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
z-index: 200; // above sig-overlay (120), below tray (310)
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user