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(
|
gamer = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='sig_reservations'
|
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(
|
card = models.ForeignKey(
|
||||||
'TarotCard', on_delete=models.CASCADE, related_name='sig_reservations'
|
'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 django.utils import timezone
|
||||||
|
|
||||||
from apps.drama.models import GameEvent, record
|
from apps.drama.models import GameEvent, record
|
||||||
|
from django.db.models import Case, IntegerField, Value, When
|
||||||
|
|
||||||
from apps.epic.models import (
|
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,
|
active_sig_seat, debit_token, levity_sig_cards, gravity_sig_cards,
|
||||||
select_token, sig_deck_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"}
|
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 = {
|
_ROLE_SCRAWL_NAMES = {
|
||||||
"PC": "Player", "NC": "Narrator", "EC": "Economist",
|
"PC": "Player", "NC": "Narrator", "EC": "Economist",
|
||||||
"SC": "Shepherd", "AC": "Alchemist", "BC": "Builder",
|
"SC": "Shepherd", "AC": "Alchemist", "BC": "Builder",
|
||||||
@@ -242,7 +261,7 @@ def _role_select_context(room, user):
|
|||||||
"slots": room.gate_slots.order_by("slot_number"),
|
"slots": room.gate_slots.order_by("slot_number"),
|
||||||
}
|
}
|
||||||
if room.table_status == Room.SIG_SELECT:
|
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_role = user_seat.role if user_seat else None
|
||||||
user_polarity = None
|
user_polarity = None
|
||||||
if user_role in _LEVITY_ROLES:
|
if user_role in _LEVITY_ROLES:
|
||||||
@@ -583,7 +602,7 @@ def sig_reserve(request, room_id):
|
|||||||
if room.table_status != Room.SIG_SELECT:
|
if room.table_status != Room.SIG_SELECT:
|
||||||
return HttpResponse(status=400)
|
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:
|
if not user_seat or not user_seat.role:
|
||||||
return HttpResponse(status=403)
|
return HttpResponse(status=403)
|
||||||
|
|
||||||
@@ -622,7 +641,7 @@ def sig_reserve(request, room_id):
|
|||||||
|
|
||||||
SigReservation.objects.create(
|
SigReservation.objects.create(
|
||||||
room=room, gamer=request.user, card=card,
|
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)
|
_notify_sig_reserved(room_id, card.pk, user_seat.role, reserved=True)
|
||||||
return HttpResponse(status=200)
|
return HttpResponse(status=200)
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ INSTALLED_APPS = [
|
|||||||
# Board apps
|
# Board apps
|
||||||
'apps.dashboard',
|
'apps.dashboard',
|
||||||
'apps.gameboard',
|
'apps.gameboard',
|
||||||
|
'apps.billboard',
|
||||||
# Gamer apps
|
# Gamer apps
|
||||||
'apps.lyric',
|
'apps.lyric',
|
||||||
'apps.epic',
|
'apps.epic',
|
||||||
'apps.drama',
|
'apps.drama',
|
||||||
'apps.billboard',
|
|
||||||
'apps.ap',
|
|
||||||
# Custom apps
|
# Custom apps
|
||||||
|
'apps.ap',
|
||||||
'apps.api',
|
'apps.api',
|
||||||
'apps.applets',
|
'apps.applets',
|
||||||
'functional_tests',
|
'functional_tests',
|
||||||
|
|||||||
@@ -34,14 +34,15 @@ def _assign_all_roles(room, role_order=None):
|
|||||||
slug="earthman",
|
slug="earthman",
|
||||||
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
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"}
|
_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):
|
for number in (11, 12, 13, 14):
|
||||||
TarotCard.objects.get_or_create(
|
TarotCard.objects.get_or_create(
|
||||||
deck_variant=earthman,
|
deck_variant=earthman,
|
||||||
slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em",
|
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()}"},
|
"name": f"{_NAME[number]} of {suit.capitalize()}"},
|
||||||
)
|
)
|
||||||
for number, name, slug in [
|
for number, name, slug in [
|
||||||
@@ -257,13 +258,13 @@ class SigSelectChannelsTest(ChannelsFunctionalTest):
|
|||||||
)
|
)
|
||||||
|
|
||||||
reserved_card = self.browser.find_element(By.CSS_SELECTOR, reserved_card_sel)
|
reserved_card = self.browser.find_element(By.CSS_SELECTOR, reserved_card_sel)
|
||||||
border_color = self.browser.execute_script(
|
box_shadow = self.browser.execute_script(
|
||||||
"return window.getComputedStyle(arguments[0]).borderTopColor",
|
"return window.getComputedStyle(arguments[0]).boxShadow",
|
||||||
reserved_card,
|
reserved_card,
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertIn(
|
||||||
border_color, "rgb(255, 207, 52)",
|
"255, 207, 52", box_shadow,
|
||||||
f"Expected --priYl border for NC reservation, got {border_color}",
|
f"Expected --priYl (255,207,52) in box-shadow for NC reservation, got {box_shadow}",
|
||||||
)
|
)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
@@ -311,7 +312,7 @@ class SigSelectThemeTest(FunctionalTest):
|
|||||||
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
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.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(
|
above = self.wait_for(
|
||||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
|
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.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.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(
|
above = self.wait_for(
|
||||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
|
||||||
|
|||||||
@@ -507,7 +507,7 @@ html:has(.sig-backdrop) {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 9999;
|
z-index: 200; // above sig-overlay (120), below tray (310)
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user