Django Channels role-select sprint: turn_changed, roles_revealed, role_select_start consumer handlers; WS URL changed from room_slug to room_id UUID; TableSeat model - room, gamer, slot_number, role, role_revealed, seat_position fields; Room.table_status field with ROLE_SELECT, SIG_SELECT, IN_GAME choices; migration 0006_table_status_and_table_seat; pick_roles and select_role views; _role_select_context helper; _notify_turn_changed, _notify_roles_revealed, _notify_role_select_start notifiers; all gate-mutation views now call _notify_gate_update; ChannelsFunctionalTest base class with serve_static, screenshot, dump helpers; SQLite TEST NAME set to file path for ChannelsLiveServerTestCase; InMemoryChannelLayer added to test CHANNEL_LAYERS settings; FT 5 and FT 6 now passing - active seat arc and turn advance via WS, no page refresh; room.js, gatekeeper.js, role-select.js added to apps/epic/static; applets.js, game-kit.js, dashboard.js, wallet.js relocated to app-scoped static dirs; room.html: hex table, table-seat arcs, card-stack, inventory panel, role-card hand, WS scripts; _room.scss: room-shell flex layout, .table-hex polygon clip-path, .table-seat and .seat-card-arc, .card-stack eligible/ineligible states, .card flip animation, .inv-role-card stacked hand, .role-select-backdrop; gear btn and room menu always position: fixed; 375 tests, 0 skipped
This commit is contained in:
@@ -3,8 +3,8 @@ from channels.generic.websocket import AsyncJsonWebsocketConsumer
|
||||
|
||||
class RoomConsumer(AsyncJsonWebsocketConsumer):
|
||||
async def connect(self):
|
||||
self.room_slug = self.scope["url_route"]["kwargs"]["room_slug"]
|
||||
self.group_name = f"room_{self.room_slug}"
|
||||
self.room_id = self.scope["url_route"]["kwargs"]["room_id"]
|
||||
self.group_name = f"room_{self.room_id}"
|
||||
await self.channel_layer.group_add(self.group_name, self.channel_name)
|
||||
await self.accept()
|
||||
|
||||
@@ -16,3 +16,12 @@ class RoomConsumer(AsyncJsonWebsocketConsumer):
|
||||
|
||||
async def gate_update(self, event):
|
||||
await self.send_json(event)
|
||||
|
||||
async def role_select_start(self, event):
|
||||
await self.send_json(event)
|
||||
|
||||
async def turn_changed(self, event):
|
||||
await self.send_json(event)
|
||||
|
||||
async def roles_revealed(self, event):
|
||||
await self.send_json(event)
|
||||
|
||||
33
src/apps/epic/migrations/0006_table_status_and_table_seat.py
Normal file
33
src/apps/epic/migrations/0006_table_status_and_table_seat.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 6.0 on 2026-03-17 00:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('epic', '0005_gateslot_debited_token_fields'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='room',
|
||||
name='table_status',
|
||||
field=models.CharField(blank=True, choices=[('ROLE_SELECT', 'Role Select'), ('SIG_SELECT', 'Significator Select'), ('IN_GAME', 'In Game')], max_length=20, null=True),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TableSeat',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slot_number', models.IntegerField()),
|
||||
('role', models.CharField(blank=True, choices=[('PC', 'Player'), ('BC', 'Builder'), ('SC', 'Shepherd'), ('AC', 'Alchemist'), ('NC', 'Narrator'), ('EC', 'Economist')], max_length=2, null=True)),
|
||||
('role_revealed', models.BooleanField(default=False)),
|
||||
('seat_position', models.IntegerField(blank=True, null=True)),
|
||||
('gamer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='table_seats', to=settings.AUTH_USER_MODEL)),
|
||||
('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='table_seats', to='epic.room')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -29,6 +29,15 @@ class Room(models.Model):
|
||||
(INVITE_ONLY, "Invite Only"),
|
||||
]
|
||||
|
||||
ROLE_SELECT = "ROLE_SELECT"
|
||||
SIG_SELECT = "SIG_SELECT"
|
||||
IN_GAME = "IN_GAME"
|
||||
TABLE_STATUS_CHOICES = [
|
||||
(ROLE_SELECT, "Role Select"),
|
||||
(SIG_SELECT, "Significator Select"),
|
||||
(IN_GAME, "In Game"),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=200)
|
||||
owner = models.ForeignKey(
|
||||
@@ -36,6 +45,9 @@ class Room(models.Model):
|
||||
)
|
||||
visibility = models.CharField(max_length=20, choices=VISIBILITY_CHOICES, default=PRIVATE)
|
||||
gate_status = models.CharField(max_length=20, choices=GATE_STATUS_CHOICES, default=GATHERING)
|
||||
table_status = models.CharField(
|
||||
max_length=20, choices=TABLE_STATUS_CHOICES, null=True, blank=True
|
||||
)
|
||||
renewal_period = models.DurationField(null=True, blank=True, default=timedelta(days=7))
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
board_state = models.JSONField(default=dict)
|
||||
@@ -133,3 +145,31 @@ def debit_token(user, slot, token):
|
||||
if not room.gate_slots.filter(status=GateSlot.EMPTY).exists():
|
||||
room.gate_status = Room.OPEN
|
||||
room.save()
|
||||
|
||||
|
||||
class TableSeat(models.Model):
|
||||
PC = "PC"
|
||||
BC = "BC"
|
||||
SC = "SC"
|
||||
AC = "AC"
|
||||
NC = "NC"
|
||||
EC = "EC"
|
||||
ROLE_CHOICES = [
|
||||
(PC, "Player"),
|
||||
(BC, "Builder"),
|
||||
(SC, "Shepherd"),
|
||||
(AC, "Alchemist"),
|
||||
(NC, "Narrator"),
|
||||
(EC, "Economist"),
|
||||
]
|
||||
PARTNER_MAP = {PC: BC, BC: PC, SC: AC, AC: SC, NC: EC, EC: NC}
|
||||
|
||||
room = models.ForeignKey(Room, on_delete=models.CASCADE, related_name="table_seats")
|
||||
gamer = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, null=True, blank=True,
|
||||
on_delete=models.SET_NULL, related_name="table_seats"
|
||||
)
|
||||
slot_number = models.IntegerField()
|
||||
role = models.CharField(max_length=2, choices=ROLE_CHOICES, null=True, blank=True)
|
||||
role_revealed = models.BooleanField(default=False)
|
||||
seat_position = models.IntegerField(null=True, blank=True)
|
||||
|
||||
@@ -4,5 +4,5 @@ from . import consumers
|
||||
|
||||
|
||||
websocket_urlpatterns = [
|
||||
path('ws/room/<slug:room_slug>/', consumers.RoomConsumer.as_asgi()),
|
||||
path('ws/room/<uuid:room_id>/', consumers.RoomConsumer.as_asgi()),
|
||||
]
|
||||
|
||||
12
src/apps/epic/static/apps/epic/gatekeeper.js
Normal file
12
src/apps/epic/static/apps/epic/gatekeeper.js
Normal file
@@ -0,0 +1,12 @@
|
||||
(function () {
|
||||
window.addEventListener('room:gate_update', function () {
|
||||
const wrapper = document.getElementById('id_gate_wrapper');
|
||||
if (!wrapper) return;
|
||||
|
||||
fetch(wrapper.dataset.gateStatusUrl)
|
||||
.then(function (r) { return r.text(); })
|
||||
.then(function (html) {
|
||||
wrapper.outerHTML = html;
|
||||
});
|
||||
});
|
||||
}());
|
||||
163
src/apps/epic/static/apps/epic/role-select.js
Normal file
163
src/apps/epic/static/apps/epic/role-select.js
Normal file
@@ -0,0 +1,163 @@
|
||||
var RoleSelect = (function () {
|
||||
var ROLES = [
|
||||
{ code: "PC", name: "Player", element: "Fire" },
|
||||
{ code: "BC", name: "Builder", element: "Stone" },
|
||||
{ code: "SC", name: "Shepherd", element: "Air" },
|
||||
{ code: "AC", name: "Alchemist", element: "Water" },
|
||||
{ code: "NC", name: "Narrator", element: "Time" },
|
||||
{ code: "EC", name: "Economist", element: "Space" },
|
||||
];
|
||||
|
||||
function getSelectRoleUrl() {
|
||||
var el = document.querySelector("[data-select-role-url]");
|
||||
return el ? el.dataset.selectRoleUrl : null;
|
||||
}
|
||||
|
||||
function getCsrf() {
|
||||
var m = document.cookie.match(/csrftoken=([^;]+)/);
|
||||
return m ? m[1] : "";
|
||||
}
|
||||
|
||||
function closeFan() {
|
||||
var backdrop = document.querySelector(".role-select-backdrop");
|
||||
if (backdrop) backdrop.remove();
|
||||
}
|
||||
|
||||
function selectRole(roleCode, cardEl) {
|
||||
var invCard = cardEl.cloneNode(true);
|
||||
invCard.classList.add("flipped");
|
||||
// strip old event listeners from the clone by replacing with a clean copy
|
||||
var clean = invCard.cloneNode(true);
|
||||
|
||||
closeFan();
|
||||
|
||||
var invSlot = document.getElementById("id_inv_role_card");
|
||||
if (invSlot) invSlot.appendChild(clean);
|
||||
|
||||
// Update the stack's taken-roles so the next openFan() filters correctly
|
||||
var stack = document.querySelector(".card-stack[data-taken-roles]");
|
||||
if (stack) {
|
||||
var current = stack.dataset.takenRoles;
|
||||
stack.dataset.takenRoles = current ? current + "," + roleCode : roleCode;
|
||||
}
|
||||
|
||||
var url = getSelectRoleUrl();
|
||||
if (!url) return;
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-CSRFToken": getCsrf(),
|
||||
},
|
||||
body: "role=" + encodeURIComponent(roleCode),
|
||||
});
|
||||
}
|
||||
|
||||
function getTakenRoles() {
|
||||
var stack = document.querySelector(".card-stack[data-taken-roles]");
|
||||
if (!stack) return [];
|
||||
var raw = stack.dataset.takenRoles;
|
||||
return raw ? raw.split(",").map(function (s) { return s.trim(); }) : [];
|
||||
}
|
||||
|
||||
function openFan() {
|
||||
if (document.querySelector(".role-select-backdrop")) return;
|
||||
|
||||
var taken = getTakenRoles();
|
||||
var available = ROLES.filter(function (r) { return taken.indexOf(r.code) === -1; });
|
||||
|
||||
var backdrop = document.createElement("div");
|
||||
backdrop.className = "role-select-backdrop";
|
||||
|
||||
var modal = document.createElement("div");
|
||||
modal.id = "id_role_select";
|
||||
|
||||
available.forEach(function (role) {
|
||||
var card = document.createElement("div");
|
||||
card.className = "card";
|
||||
card.dataset.role = role.code;
|
||||
|
||||
var back = document.createElement("div");
|
||||
back.className = "card-back";
|
||||
back.textContent = "?";
|
||||
|
||||
var front = document.createElement("div");
|
||||
front.className = "card-front";
|
||||
front.innerHTML = '<div class="card-role-name">' + role.name + "</div>";
|
||||
|
||||
card.appendChild(back);
|
||||
card.appendChild(front);
|
||||
|
||||
card.addEventListener("mouseenter", function () {
|
||||
card.classList.add("flipped");
|
||||
});
|
||||
card.addEventListener("mouseleave", function () {
|
||||
card.classList.remove("flipped");
|
||||
});
|
||||
card.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
selectRole(role.code, card);
|
||||
});
|
||||
|
||||
modal.appendChild(card);
|
||||
});
|
||||
|
||||
backdrop.appendChild(modal);
|
||||
backdrop.addEventListener("click", closeFan);
|
||||
document.body.appendChild(backdrop);
|
||||
}
|
||||
|
||||
function init() {
|
||||
var stack = document.querySelector(".card-stack[data-state='eligible']");
|
||||
if (!stack) return;
|
||||
stack.addEventListener("click", openFan);
|
||||
}
|
||||
|
||||
var _reload = function () { window.location.reload(); };
|
||||
|
||||
function handleRolesRevealed() {
|
||||
_reload();
|
||||
}
|
||||
|
||||
function handleTurnChanged(event) {
|
||||
var active = String(event.detail.active_slot);
|
||||
|
||||
// Update card-stack eligibility
|
||||
var stack = document.querySelector(".card-stack[data-user-slots]");
|
||||
if (stack) {
|
||||
var userSlots = stack.dataset.userSlots
|
||||
? stack.dataset.userSlots.split(",") : [];
|
||||
if (userSlots.indexOf(active) !== -1) {
|
||||
stack.dataset.state = "eligible";
|
||||
stack.removeEventListener("click", openFan);
|
||||
stack.addEventListener("click", openFan);
|
||||
} else {
|
||||
stack.dataset.state = "ineligible";
|
||||
stack.removeEventListener("click", openFan);
|
||||
}
|
||||
}
|
||||
|
||||
// Move .active to the newly active seat
|
||||
document.querySelectorAll(".table-seat.active").forEach(function (s) {
|
||||
s.classList.remove("active");
|
||||
});
|
||||
var activeSeat = document.querySelector(".table-seat[data-slot='" + active + "']");
|
||||
if (activeSeat) activeSeat.classList.add("active");
|
||||
}
|
||||
|
||||
window.addEventListener("room:role_select_start", init);
|
||||
window.addEventListener("room:turn_changed", handleTurnChanged);
|
||||
window.addEventListener("room:roles_revealed", handleRolesRevealed);
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
return {
|
||||
openFan: openFan,
|
||||
closeFan: closeFan,
|
||||
setReload: function (fn) { _reload = fn; },
|
||||
};
|
||||
}());
|
||||
17
src/apps/epic/static/apps/epic/room.js
Normal file
17
src/apps/epic/static/apps/epic/room.js
Normal file
@@ -0,0 +1,17 @@
|
||||
(function () {
|
||||
const roomPage = document.querySelector('.room-page');
|
||||
if (!roomPage) return;
|
||||
|
||||
const roomId = roomPage.dataset.roomId;
|
||||
const wsScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const ws = new WebSocket(`${wsScheme}://${window.location.host}/ws/room/${roomId}/`);
|
||||
|
||||
ws.onmessage = function (event) {
|
||||
const data = JSON.parse(event.data);
|
||||
window.dispatchEvent(new CustomEvent('room:' + data.type, { detail: data }));
|
||||
};
|
||||
|
||||
ws.onclose = function () {
|
||||
console.warn('Room WebSocket closed');
|
||||
};
|
||||
}());
|
||||
@@ -15,18 +15,66 @@ TEST_CHANNEL_LAYERS = {
|
||||
@override_settings(CHANNEL_LAYERS=TEST_CHANNEL_LAYERS)
|
||||
class RoomConsumerTest(SimpleTestCase):
|
||||
async def test_can_connect_and_disconnect(self):
|
||||
communicator = WebsocketCommunicator(application, "/ws/room/test-room/")
|
||||
communicator = WebsocketCommunicator(application, "/ws/room/00000000-0000-0000-0000-000000000001/")
|
||||
connected, _ = await communicator.connect()
|
||||
self.assertTrue(connected)
|
||||
await communicator.disconnect()
|
||||
|
||||
async def test_receives_gate_update_broadcast(self):
|
||||
communicator = WebsocketCommunicator(application, "/ws/room/test-room/")
|
||||
async def test_receives_role_select_start_broadcast(self):
|
||||
communicator = WebsocketCommunicator(application, "/ws/room/00000000-0000-0000-0000-000000000001/")
|
||||
await communicator.connect()
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
await channel_layer.group_send(
|
||||
"room_test-room",
|
||||
"room_00000000-0000-0000-0000-000000000001",
|
||||
{"type": "role_select_start", "slot_order": [1, 2, 3, 4, 5, 6]},
|
||||
)
|
||||
|
||||
response = await communicator.receive_json_from()
|
||||
self.assertEqual(response["type"], "role_select_start")
|
||||
self.assertEqual(response["slot_order"], [1, 2, 3, 4, 5, 6])
|
||||
|
||||
await communicator.disconnect()
|
||||
|
||||
async def test_receives_turn_changed_broadcast(self):
|
||||
communicator = WebsocketCommunicator(application, "/ws/room/00000000-0000-0000-0000-000000000001/")
|
||||
await communicator.connect()
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
await channel_layer.group_send(
|
||||
"room_00000000-0000-0000-0000-000000000001",
|
||||
{"type": "turn_changed", "active_slot": 2},
|
||||
)
|
||||
|
||||
response = await communicator.receive_json_from()
|
||||
self.assertEqual(response["type"], "turn_changed")
|
||||
self.assertEqual(response["active_slot"], 2)
|
||||
|
||||
await communicator.disconnect()
|
||||
|
||||
async def test_receives_roles_revealed_broadcast(self):
|
||||
communicator = WebsocketCommunicator(application, "/ws/room/00000000-0000-0000-0000-000000000001/")
|
||||
await communicator.connect()
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
await channel_layer.group_send(
|
||||
"room_00000000-0000-0000-0000-000000000001",
|
||||
{"type": "roles_revealed", "assignments": {"1": "PC", "2": "BC"}},
|
||||
)
|
||||
|
||||
response = await communicator.receive_json_from()
|
||||
self.assertEqual(response["type"], "roles_revealed")
|
||||
self.assertIn("assignments", response)
|
||||
|
||||
await communicator.disconnect()
|
||||
|
||||
async def test_receives_gate_update_broadcast(self):
|
||||
communicator = WebsocketCommunicator(application, "/ws/room/00000000-0000-0000-0000-000000000001/")
|
||||
await communicator.connect()
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
await channel_layer.group_send(
|
||||
"room_00000000-0000-0000-0000-000000000001",
|
||||
{"type": "gate_update", "gate_state": "some_state"},
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ 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, select_token
|
||||
from apps.epic.models import GateSlot, Room, RoomInvite, TableSeat, debit_token, select_token
|
||||
|
||||
|
||||
class RoomCreationTest(TestCase):
|
||||
@@ -132,6 +132,62 @@ class SelectTokenTest(TestCase):
|
||||
self.assertEqual(token.token_type, Token.PASS)
|
||||
|
||||
|
||||
class RoomTableStatusTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner = User.objects.create(email="founder@test.io")
|
||||
self.room = Room.objects.create(name="Test Room", owner=self.owner)
|
||||
|
||||
def test_table_status_defaults_to_blank(self):
|
||||
self.room.refresh_from_db()
|
||||
self.assertFalse(self.room.table_status)
|
||||
|
||||
def test_room_has_role_select_constant(self):
|
||||
self.assertEqual(Room.ROLE_SELECT, "ROLE_SELECT")
|
||||
|
||||
def test_room_has_sig_select_constant(self):
|
||||
self.assertEqual(Room.SIG_SELECT, "SIG_SELECT")
|
||||
|
||||
def test_room_has_in_game_constant(self):
|
||||
self.assertEqual(Room.IN_GAME, "IN_GAME")
|
||||
|
||||
def test_table_status_accepts_role_select(self):
|
||||
self.room.table_status = Room.ROLE_SELECT
|
||||
self.room.save()
|
||||
self.room.refresh_from_db()
|
||||
self.assertEqual(self.room.table_status, Room.ROLE_SELECT)
|
||||
|
||||
|
||||
class TableSeatModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner = User.objects.create(email="founder@test.io")
|
||||
self.room = Room.objects.create(name="Test Room", owner=self.owner)
|
||||
|
||||
def test_table_seat_can_be_created(self):
|
||||
seat = TableSeat.objects.create(
|
||||
room=self.room,
|
||||
gamer=self.owner,
|
||||
slot_number=1,
|
||||
)
|
||||
self.assertEqual(seat.slot_number, 1)
|
||||
self.assertIsNone(seat.role)
|
||||
self.assertFalse(seat.role_revealed)
|
||||
self.assertIsNone(seat.seat_position)
|
||||
|
||||
def test_table_seat_role_choices_cover_all_six(self):
|
||||
role_codes = [c[0] for c in TableSeat.ROLE_CHOICES]
|
||||
for code in ["PC", "BC", "SC", "AC", "NC", "EC"]:
|
||||
self.assertIn(code, role_codes)
|
||||
|
||||
def test_partner_map_pairs_are_mutual(self):
|
||||
for a, b in [(TableSeat.PC, TableSeat.BC), (TableSeat.SC, TableSeat.AC), (TableSeat.NC, TableSeat.EC)]:
|
||||
self.assertEqual(TableSeat.PARTNER_MAP[a], b)
|
||||
self.assertEqual(TableSeat.PARTNER_MAP[b], a)
|
||||
|
||||
def test_room_table_seats_reverse_relation(self):
|
||||
TableSeat.objects.create(room=self.room, gamer=self.owner, slot_number=1)
|
||||
self.assertEqual(self.room.table_seats.count(), 1)
|
||||
|
||||
|
||||
class RoomInviteTest(TestCase):
|
||||
def setUp(self):
|
||||
self.founder = User.objects.create(email="founder@example.com")
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
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
|
||||
from apps.epic.models import GateSlot, Room, RoomInvite, TableSeat
|
||||
|
||||
|
||||
class RoomCreationViewTest(TestCase):
|
||||
@@ -346,6 +348,298 @@ class ConfirmTokenPriorityViewTest(TestCase):
|
||||
self.assertIsNone(self.coin.current_room)
|
||||
|
||||
|
||||
class RoleSelectRenderingTest(TestCase):
|
||||
def setUp(self):
|
||||
self.founder = User.objects.create(email="founder@test.io")
|
||||
self.room = Room.objects.create(name="Test Room", owner=self.founder)
|
||||
self.gamers = [self.founder]
|
||||
for i in range(2, 7):
|
||||
self.gamers.append(User.objects.create(email=f"g{i}@test.io"))
|
||||
for i, gamer in enumerate(self.gamers, start=1):
|
||||
slot = self.room.gate_slots.get(slot_number=i)
|
||||
slot.gamer = gamer
|
||||
slot.status = GateSlot.FILLED
|
||||
slot.save()
|
||||
self.room.gate_status = Room.OPEN
|
||||
self.room.table_status = Room.ROLE_SELECT
|
||||
self.room.save()
|
||||
for i, gamer in enumerate(self.gamers, start=1):
|
||||
TableSeat.objects.create(room=self.room, gamer=gamer, slot_number=i)
|
||||
|
||||
def test_room_view_includes_card_stack_when_role_select(self):
|
||||
self.client.force_login(self.founder)
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, "card-stack")
|
||||
|
||||
def test_card_stack_eligible_for_slot1_gamer(self):
|
||||
self.client.force_login(self.founder)
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, 'data-state="eligible"')
|
||||
|
||||
def test_card_stack_ineligible_for_slot2_gamer(self):
|
||||
self.client.force_login(self.gamers[1])
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, 'data-state="ineligible"')
|
||||
|
||||
def test_card_stack_ineligible_shows_fa_ban(self):
|
||||
self.client.force_login(self.gamers[1])
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, "fa-ban")
|
||||
|
||||
def test_card_stack_eligible_omits_fa_ban(self):
|
||||
self.client.force_login(self.founder)
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertNotContains(response, "fa-ban")
|
||||
|
||||
def test_gatekeeper_overlay_absent_when_role_select(self):
|
||||
self.client.force_login(self.founder)
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertNotContains(response, "gate-overlay")
|
||||
|
||||
def test_six_table_seats_rendered(self):
|
||||
self.client.force_login(self.founder)
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, "table-seat", count=6)
|
||||
|
||||
def test_active_table_seat_has_active_class(self):
|
||||
self.client.force_login(self.founder) # slot 1 is active
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, 'class="table-seat active"')
|
||||
|
||||
def test_inactive_table_seat_lacks_active_class(self):
|
||||
self.client.force_login(self.founder)
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
# Slots 2–6 are not active, so at least one plain table-seat exists
|
||||
self.assertContains(response, 'class="table-seat"')
|
||||
|
||||
def test_card_stack_has_data_user_slots_for_eligible_gamer(self):
|
||||
self.client.force_login(self.founder) # founder is slot 1 only
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, 'data-user-slots="1"')
|
||||
|
||||
def test_card_stack_has_data_user_slots_for_ineligible_gamer(self):
|
||||
self.client.force_login(self.gamers[1]) # slot 2 gamer
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, 'data-user-slots="2"')
|
||||
|
||||
|
||||
class PickRolesViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.founder = User.objects.create(email="founder@test.io")
|
||||
self.client.force_login(self.founder)
|
||||
self.room = Room.objects.create(name="Test Room", owner=self.founder)
|
||||
for i in range(1, 7):
|
||||
gamer = self.founder if i == 1 else User.objects.create(email=f"g{i}@test.io")
|
||||
slot = self.room.gate_slots.get(slot_number=i)
|
||||
slot.gamer = gamer
|
||||
slot.status = GateSlot.FILLED
|
||||
slot.save()
|
||||
self.room.gate_status = Room.OPEN
|
||||
self.room.save()
|
||||
|
||||
def test_pick_roles_transitions_room_to_role_select(self):
|
||||
self.client.post(reverse("epic:pick_roles", kwargs={"room_id": self.room.id}))
|
||||
self.room.refresh_from_db()
|
||||
self.assertEqual(self.room.table_status, Room.ROLE_SELECT)
|
||||
|
||||
def test_pick_roles_creates_one_table_seat_per_filled_slot(self):
|
||||
self.client.post(reverse("epic:pick_roles", kwargs={"room_id": self.room.id}))
|
||||
self.assertEqual(TableSeat.objects.filter(room=self.room).count(), 6)
|
||||
|
||||
def test_pick_roles_table_seats_carry_gamer_and_slot_number(self):
|
||||
self.client.post(reverse("epic:pick_roles", kwargs={"room_id": self.room.id}))
|
||||
seat = TableSeat.objects.get(room=self.room, slot_number=1)
|
||||
self.assertEqual(seat.gamer, self.founder)
|
||||
|
||||
def test_only_open_room_can_start_role_select(self):
|
||||
self.room.gate_status = Room.GATHERING
|
||||
self.room.save()
|
||||
self.client.post(reverse("epic:pick_roles", kwargs={"room_id": self.room.id}))
|
||||
self.room.refresh_from_db()
|
||||
self.assertIsNone(self.room.table_status)
|
||||
|
||||
def test_pick_roles_requires_login(self):
|
||||
self.client.logout()
|
||||
response = self.client.post(
|
||||
reverse("epic:pick_roles", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn("/accounts/login/", response.url)
|
||||
|
||||
def test_pick_roles_redirects_to_room(self):
|
||||
response = self.client.post(
|
||||
reverse("epic:pick_roles", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertRedirects(
|
||||
response, reverse("epic:gatekeeper", args=[self.room.id])
|
||||
)
|
||||
|
||||
def test_pick_roles_notifies_channel_layer(self):
|
||||
with patch("apps.epic.views._notify_role_select_start") as mock_notify:
|
||||
self.client.post(reverse("epic:pick_roles", kwargs={"room_id": self.room.id}))
|
||||
mock_notify.assert_called_once_with(self.room.id)
|
||||
|
||||
|
||||
class SelectRoleViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.founder = User.objects.create(email="founder@test.io")
|
||||
self.room = Room.objects.create(name="Test Room", owner=self.founder)
|
||||
self.gamers = [self.founder]
|
||||
for i in range(2, 7):
|
||||
self.gamers.append(User.objects.create(email=f"g{i}@test.io"))
|
||||
for i, gamer in enumerate(self.gamers, start=1):
|
||||
slot = self.room.gate_slots.get(slot_number=i)
|
||||
slot.gamer = gamer
|
||||
slot.status = GateSlot.FILLED
|
||||
slot.save()
|
||||
self.room.gate_status = Room.OPEN
|
||||
self.room.table_status = Room.ROLE_SELECT
|
||||
self.room.save()
|
||||
for i, gamer in enumerate(self.gamers, start=1):
|
||||
TableSeat.objects.create(room=self.room, gamer=gamer, slot_number=i)
|
||||
self.client.force_login(self.founder)
|
||||
|
||||
def test_select_role_records_choice(self):
|
||||
self.client.post(
|
||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||
data={"role": "PC"},
|
||||
)
|
||||
seat = TableSeat.objects.get(room=self.room, slot_number=1)
|
||||
self.assertEqual(seat.role, "PC")
|
||||
|
||||
def test_select_role_wrong_turn_makes_no_change(self):
|
||||
self.client.force_login(self.gamers[1]) # slot 2 — not their turn
|
||||
self.client.post(
|
||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||
data={"role": "BC"},
|
||||
)
|
||||
seat = TableSeat.objects.get(room=self.room, slot_number=2)
|
||||
self.assertIsNone(seat.role)
|
||||
|
||||
def test_turn_advances_after_selection(self):
|
||||
self.client.post(
|
||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||
data={"role": "PC"},
|
||||
)
|
||||
next_active = TableSeat.objects.filter(
|
||||
room=self.room, role__isnull=True
|
||||
).order_by("slot_number").first()
|
||||
self.assertEqual(next_active.slot_number, 2)
|
||||
|
||||
def test_all_selected_sets_sig_select(self):
|
||||
roles = ["PC", "BC", "SC", "AC", "NC"]
|
||||
for i, role in enumerate(roles):
|
||||
seat = TableSeat.objects.get(room=self.room, slot_number=i + 1)
|
||||
seat.role = role
|
||||
seat.save()
|
||||
self.client.force_login(self.gamers[5]) # slot 6 — last
|
||||
self.client.post(
|
||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||
data={"role": "EC"},
|
||||
)
|
||||
self.room.refresh_from_db()
|
||||
self.assertEqual(self.room.table_status, Room.SIG_SELECT)
|
||||
|
||||
def test_select_role_notifies_turn_changed(self):
|
||||
with patch("apps.epic.views._notify_turn_changed") as mock_notify:
|
||||
self.client.post(
|
||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||
data={"role": "PC"},
|
||||
)
|
||||
mock_notify.assert_called_once_with(self.room.id)
|
||||
|
||||
def test_select_role_notifies_roles_revealed_when_last(self):
|
||||
roles = ["PC", "BC", "SC", "AC", "NC"]
|
||||
for i, role in enumerate(roles):
|
||||
seat = TableSeat.objects.get(room=self.room, slot_number=i + 1)
|
||||
seat.role = role
|
||||
seat.save()
|
||||
self.client.force_login(self.gamers[5])
|
||||
with patch("apps.epic.views._notify_roles_revealed") as mock_notify:
|
||||
self.client.post(
|
||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||
data={"role": "EC"},
|
||||
)
|
||||
mock_notify.assert_called_once_with(self.room.id)
|
||||
|
||||
def test_select_role_requires_login(self):
|
||||
self.client.logout()
|
||||
response = self.client.post(
|
||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||
data={"role": "PC"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn("/accounts/login/", response.url)
|
||||
|
||||
def test_select_role_redirects_to_room(self):
|
||||
response = self.client.post(
|
||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||
data={"role": "PC"},
|
||||
)
|
||||
self.assertRedirects(
|
||||
response, reverse("epic:gatekeeper", args=[self.room.id])
|
||||
)
|
||||
|
||||
|
||||
class RevealPhaseRenderingTest(TestCase):
|
||||
def setUp(self):
|
||||
self.founder = User.objects.create(email="founder@test.io")
|
||||
self.room = Room.objects.create(name="Test Room", owner=self.founder)
|
||||
gamers = [self.founder]
|
||||
for i in range(2, 7):
|
||||
gamers.append(User.objects.create(email=f"g{i}@test.io"))
|
||||
roles = ["PC", "BC", "SC", "AC", "NC", "EC"]
|
||||
for i, (gamer, role) in enumerate(zip(gamers, roles), start=1):
|
||||
TableSeat.objects.create(
|
||||
room=self.room, gamer=gamer, slot_number=i,
|
||||
role=role, role_revealed=True,
|
||||
)
|
||||
self.room.gate_status = Room.OPEN
|
||||
self.room.table_status = Room.SIG_SELECT
|
||||
self.room.save()
|
||||
self.client.force_login(self.founder)
|
||||
|
||||
def test_face_up_role_cards_rendered_when_sig_select(self):
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, "face-up")
|
||||
|
||||
def test_inv_role_card_slot_present(self):
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, "id_inv_role_card")
|
||||
|
||||
def test_partner_indicator_present_when_sig_select(self):
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, "partner-indicator")
|
||||
|
||||
|
||||
class RoomActionsViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner = User.objects.create(email="owner@test.io")
|
||||
|
||||
@@ -11,6 +11,8 @@ urlpatterns = [
|
||||
path('room/<uuid:room_id>/gate/confirm_token', views.confirm_token, name='confirm_token'),
|
||||
path('room/<uuid:room_id>/gate/return_token', views.return_token, name='return_token'),
|
||||
path('room/<uuid:room_id>/gate/release_slot', views.release_slot, name='release_slot'),
|
||||
path('room/<uuid:room_id>/pick-roles', views.pick_roles, name='pick_roles'),
|
||||
path('room/<uuid:room_id>/select-role', views.select_role, name='select_role'),
|
||||
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'),
|
||||
|
||||
@@ -1,17 +1,60 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
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 GateSlot, Room, RoomInvite, debit_token, select_token
|
||||
from apps.epic.models import GateSlot, Room, RoomInvite, TableSeat, debit_token, select_token
|
||||
from apps.lyric.models import Token
|
||||
|
||||
|
||||
RESERVE_TIMEOUT = timedelta(seconds=60)
|
||||
|
||||
|
||||
def _notify_gate_update(room_id):
|
||||
async_to_sync(get_channel_layer().group_send)(
|
||||
f'room_{room_id}',
|
||||
{'type': 'gate_update'},
|
||||
)
|
||||
|
||||
|
||||
def _notify_turn_changed(room_id):
|
||||
active_seat = TableSeat.objects.filter(
|
||||
room_id=room_id, role__isnull=True
|
||||
).order_by("slot_number").first()
|
||||
active_slot = active_seat.slot_number if active_seat else None
|
||||
async_to_sync(get_channel_layer().group_send)(
|
||||
f'room_{room_id}',
|
||||
{'type': 'turn_changed', 'active_slot': active_slot},
|
||||
)
|
||||
|
||||
|
||||
def _notify_roles_revealed(room_id):
|
||||
assignments = {
|
||||
str(seat.slot_number): seat.role
|
||||
for seat in TableSeat.objects.filter(room_id=room_id).order_by("slot_number")
|
||||
}
|
||||
async_to_sync(get_channel_layer().group_send)(
|
||||
f'room_{room_id}',
|
||||
{'type': 'roles_revealed', 'assignments': assignments},
|
||||
)
|
||||
|
||||
|
||||
def _notify_role_select_start(room_id):
|
||||
slot_order = list(
|
||||
GateSlot.objects.filter(room_id=room_id, status=GateSlot.FILLED)
|
||||
.order_by("slot_number")
|
||||
.values_list("slot_number", flat=True)
|
||||
)
|
||||
async_to_sync(get_channel_layer().group_send)(
|
||||
f'room_{room_id}',
|
||||
{'type': 'role_select_start', 'slot_order': slot_order},
|
||||
)
|
||||
|
||||
|
||||
def _expire_reserved_slots(room):
|
||||
cutoff = timezone.now() - RESERVE_TIMEOUT
|
||||
room.gate_slots.filter(
|
||||
@@ -79,6 +122,65 @@ def _gate_context(room, user):
|
||||
}
|
||||
|
||||
|
||||
def _role_select_context(room, user):
|
||||
user_seat = None
|
||||
active_seat = None
|
||||
unassigned = room.table_seats.filter(role__isnull=True).order_by("slot_number")
|
||||
if unassigned.exists():
|
||||
# Normal path — TableSeats present
|
||||
active_seat = unassigned.first()
|
||||
user_seat = None
|
||||
if user.is_authenticated:
|
||||
user_seat = room.table_seats.filter(gamer=user, role__isnull=True).order_by("slot_number").first()
|
||||
if user_seat and user_seat.slot_number == active_seat.slot_number:
|
||||
card_stack_state = "eligible"
|
||||
else:
|
||||
card_stack_state = "ineligible"
|
||||
else:
|
||||
# Fallback — no TableSeats yet; use GateSlot drop order
|
||||
active_slot = room.gate_slots.filter(
|
||||
status=GateSlot.FILLED
|
||||
).order_by("slot_number").first()
|
||||
if active_slot is None:
|
||||
card_stack_state = None
|
||||
elif user.is_authenticated and active_slot.gamer == user:
|
||||
card_stack_state = "eligible"
|
||||
else:
|
||||
card_stack_state = "ineligible"
|
||||
taken_roles = list(
|
||||
room.table_seats.exclude(role__isnull=True).values_list("role", flat=True)
|
||||
)
|
||||
_action_order = {r: i for i, r in enumerate(["PC", "NC", "EC", "SC", "AC", "BC"])}
|
||||
assigned_seats = (
|
||||
sorted(
|
||||
room.table_seats.filter(gamer=user, role__isnull=False),
|
||||
key=lambda s: _action_order.get(s.role, 99),
|
||||
)
|
||||
if user.is_authenticated else []
|
||||
)
|
||||
active_slot = active_seat.slot_number if active_seat else None
|
||||
ctx = {
|
||||
"card_stack_state": card_stack_state,
|
||||
"taken_roles": taken_roles,
|
||||
"assigned_seats": assigned_seats,
|
||||
"user_seat": user_seat,
|
||||
"user_slots": list(
|
||||
room.table_seats.filter(gamer=user, role__isnull=True)
|
||||
.order_by("slot_number")
|
||||
.values_list("slot_number", flat=True)
|
||||
) if user.is_authenticated else [],
|
||||
"active_slot": active_slot,
|
||||
}
|
||||
if room.table_status == Room.SIG_SELECT:
|
||||
user_seat = room.table_seats.filter(gamer=user).first() if user.is_authenticated else None
|
||||
partner_role = TableSeat.PARTNER_MAP.get(user_seat.role) if user_seat and user_seat.role else None
|
||||
partner_seat = room.table_seats.filter(role=partner_role).first() if partner_role else None
|
||||
ctx["user_seat"] = user_seat
|
||||
ctx["partner_seat"] = partner_seat
|
||||
ctx["revealed_seats"] = room.table_seats.filter(role_revealed=True).order_by("slot_number")
|
||||
return ctx
|
||||
|
||||
|
||||
@login_required
|
||||
def create_room(request):
|
||||
if request.method == "POST":
|
||||
@@ -91,7 +193,10 @@ def create_room(request):
|
||||
|
||||
def gatekeeper(request, room_id):
|
||||
room = Room.objects.get(id=room_id)
|
||||
ctx = _gate_context(room, request.user)
|
||||
if room.table_status:
|
||||
ctx = _role_select_context(room, request.user)
|
||||
else:
|
||||
ctx = _gate_context(room, request.user)
|
||||
ctx["room"] = room
|
||||
return render(request, "apps/gameboard/room.html", ctx)
|
||||
|
||||
@@ -113,6 +218,7 @@ def drop_token(request, room_id):
|
||||
token.current_room = room
|
||||
token.save()
|
||||
request.session["kit_token_id"] = str(token.id)
|
||||
_notify_gate_update(room_id)
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
if room.gate_slots.filter(status=GateSlot.RESERVED).exists():
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
@@ -127,6 +233,7 @@ def drop_token(request, room_id):
|
||||
slot.reserved_at = timezone.now()
|
||||
slot.save()
|
||||
request.session["kit_token_id"] = str(token.id)
|
||||
_notify_gate_update(room_id)
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
|
||||
|
||||
@@ -150,6 +257,7 @@ def confirm_token(request, room_id):
|
||||
if int(slot_number) > carte.slots_claimed:
|
||||
carte.slots_claimed = int(slot_number)
|
||||
carte.save()
|
||||
_notify_gate_update(room_id)
|
||||
else:
|
||||
slot = room.gate_slots.filter(
|
||||
gamer=request.user, status=GateSlot.RESERVED
|
||||
@@ -163,6 +271,7 @@ def confirm_token(request, room_id):
|
||||
token = select_token(request.user)
|
||||
if token:
|
||||
debit_token(request.user, slot, token)
|
||||
_notify_gate_update(room_id)
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
|
||||
|
||||
@@ -185,6 +294,7 @@ def return_token(request, room_id):
|
||||
carte.slots_claimed = 0
|
||||
carte.save()
|
||||
request.session.pop("kit_token_id", None)
|
||||
_notify_gate_update(room_id)
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
slot = room.gate_slots.filter(
|
||||
gamer=request.user,
|
||||
@@ -214,6 +324,7 @@ def return_token(request, room_id):
|
||||
slot.debited_token_type = None
|
||||
slot.debited_token_expires_at = None
|
||||
slot.save()
|
||||
_notify_gate_update(room_id)
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
|
||||
|
||||
@@ -240,6 +351,52 @@ def release_slot(request, room_id):
|
||||
if room.gate_status == Room.OPEN:
|
||||
room.gate_status = Room.GATHERING
|
||||
room.save()
|
||||
_notify_gate_update(room_id)
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
|
||||
|
||||
@login_required
|
||||
def select_role(request, room_id):
|
||||
if request.method == "POST":
|
||||
room = Room.objects.get(id=room_id)
|
||||
if room.table_status != Room.ROLE_SELECT:
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
active_seat = room.table_seats.filter(
|
||||
role__isnull=True
|
||||
).order_by("slot_number").first()
|
||||
if not active_seat or active_seat.gamer != request.user:
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
role = request.POST.get("role")
|
||||
valid_roles = [code for code, _ in TableSeat.ROLE_CHOICES]
|
||||
if not role or role not in valid_roles:
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
if room.table_seats.filter(role=role).exists():
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
active_seat.role = role
|
||||
active_seat.save()
|
||||
if room.table_seats.filter(role__isnull=True).exists():
|
||||
_notify_turn_changed(room_id)
|
||||
else:
|
||||
room.table_status = Room.SIG_SELECT
|
||||
room.save()
|
||||
_notify_roles_revealed(room_id)
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
|
||||
|
||||
@login_required
|
||||
def pick_roles(request, room_id):
|
||||
if request.method == "POST":
|
||||
room = Room.objects.get(id=room_id)
|
||||
if room.gate_status == Room.OPEN:
|
||||
room.table_status = Room.ROLE_SELECT
|
||||
room.save()
|
||||
for slot in room.gate_slots.filter(status=GateSlot.FILLED).order_by("slot_number"):
|
||||
TableSeat.objects.create(
|
||||
room=room,
|
||||
gamer=slot.gamer,
|
||||
slot_number=slot.slot_number,
|
||||
)
|
||||
_notify_role_select_start(room_id)
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user