From 8c2a5d24eced68ff33661eb6d7ada9b4837374a5 Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Wed, 18 Mar 2026 23:14:53 -0400 Subject: [PATCH] updated .fa-ban icon to update via js & ws; changed taken_roles (or its cognates) everywhere to starter_roles, as 'taken' will be used in respect to roles thru-out entire game, not just this seat-determining phase of Role Select; patched up chosen cards not disappearing upon previous gamer choice, & a try,except that catches attempts to select one anyway w. a 409 & optimistic card rollback; new IT confirms this 409 --- src/apps/epic/static/apps/epic/role-select.js | 40 ++++++++++++++----- src/apps/epic/tests/integrated/test_views.py | 12 ++++-- src/apps/epic/views.py | 13 ++++-- src/static_src/tests/RoleSelectSpec.js | 2 +- src/templates/apps/gameboard/room.html | 2 +- 5 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/apps/epic/static/apps/epic/role-select.js b/src/apps/epic/static/apps/epic/role-select.js index 846a7b5..cf753d8 100644 --- a/src/apps/epic/static/apps/epic/role-select.js +++ b/src/apps/epic/static/apps/epic/role-select.js @@ -34,11 +34,11 @@ var RoleSelect = (function () { 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]"); + // Optimistically mark role as taken so re-opened fan filters it + var stack = document.querySelector(".card-stack[data-starter-roles]"); if (stack) { - var current = stack.dataset.takenRoles; - stack.dataset.takenRoles = current ? current + "," + roleCode : roleCode; + var current = stack.dataset.starterRoles; + stack.dataset.starterRoles = current ? current + "," + roleCode : roleCode; } var url = getSelectRoleUrl(); @@ -50,20 +50,30 @@ var RoleSelect = (function () { "X-CSRFToken": getCsrf(), }, body: "role=" + encodeURIComponent(roleCode), + }).then(function (response) { + if (!response.ok) { + // Server rejected (role already taken) — undo optimistic update + if (invSlot && invSlot.contains(clean)) invSlot.removeChild(clean); + if (stack) { + stack.dataset.starterRoles = stack.dataset.starterRoles + .split(",").filter(function (r) { return r.trim() !== roleCode; }).join(","); + } + openFan(); + } }); } - function getTakenRoles() { - var stack = document.querySelector(".card-stack[data-taken-roles]"); + function getStarterRoles() { + var stack = document.querySelector(".card-stack[data-starter-roles]"); if (!stack) return []; - var raw = stack.dataset.takenRoles; + var raw = stack.dataset.starterRoles; return raw ? raw.split(",").map(function (s) { return s.trim(); }) : []; } function openFan() { if (document.querySelector(".role-select-backdrop")) return; - var taken = getTakenRoles(); + var taken = getStarterRoles(); var available = ROLES.filter(function (r) { return taken.indexOf(r.code) === -1; }); var backdrop = document.createElement("div"); @@ -124,18 +134,30 @@ var RoleSelect = (function () { var invSlot = document.getElementById("id_inv_role_card"); if (invSlot) invSlot.innerHTML = ""; - // Update card-stack eligibility var stack = document.querySelector(".card-stack[data-user-slots]"); if (stack) { + // Sync starter-roles from server so the fan reflects actual DB state + if (event.detail.starter_roles) { + stack.dataset.starterRoles = event.detail.starter_roles.join(","); + } + + // Update eligibility and ban icon together var userSlots = stack.dataset.userSlots ? stack.dataset.userSlots.split(",") : []; if (userSlots.indexOf(active) !== -1) { stack.dataset.state = "eligible"; + var ban = stack.querySelector(".fa-ban"); + if (ban) ban.remove(); stack.removeEventListener("click", openFan); stack.addEventListener("click", openFan); } else { stack.dataset.state = "ineligible"; stack.removeEventListener("click", openFan); + if (!stack.querySelector(".fa-ban")) { + var icon = document.createElement("i"); + icon.className = "fa-solid fa-ban"; + stack.appendChild(icon); + } } } diff --git a/src/apps/epic/tests/integrated/test_views.py b/src/apps/epic/tests/integrated/test_views.py index 2ba5ace..f25be86 100644 --- a/src/apps/epic/tests/integrated/test_views.py +++ b/src/apps/epic/tests/integrated/test_views.py @@ -593,14 +593,20 @@ class SelectRoleViewTest(TestCase): self.assertEqual(response.status_code, 302) self.assertIn("/accounts/login/", response.url) - def test_select_role_redirects_to_room(self): + def test_select_role_returns_ok(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]) + self.assertEqual(response.status_code, 200) + + def test_select_role_returns_409_for_duplicate_role(self): + TableSeat.objects.filter(room=self.room, slot_number=2).update(role="BC") + response = self.client.post( + reverse("epic:select_role", kwargs={"room_id": self.room.id}), + data={"role": "BC"}, ) + self.assertEqual(response.status_code, 409) class RevealPhaseRenderingTest(TestCase): diff --git a/src/apps/epic/views.py b/src/apps/epic/views.py index 02ce379..4458114 100644 --- a/src/apps/epic/views.py +++ b/src/apps/epic/views.py @@ -26,9 +26,13 @@ def _notify_turn_changed(room_id): room_id=room_id, role__isnull=True ).order_by("slot_number").first() active_slot = active_seat.slot_number if active_seat else None + starter_roles = list( + TableSeat.objects.filter(room_id=room_id, role__isnull=False) + .values_list("role", flat=True) + ) async_to_sync(get_channel_layer().group_send)( f'room_{room_id}', - {'type': 'turn_changed', 'active_slot': active_slot}, + {'type': 'turn_changed', 'active_slot': active_slot, 'starter_roles': starter_roles}, ) @@ -147,7 +151,7 @@ def _role_select_context(room, user): card_stack_state = "eligible" else: card_stack_state = "ineligible" - taken_roles = list( + starter_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"])} @@ -161,7 +165,7 @@ def _role_select_context(room, user): active_slot = active_seat.slot_number if active_seat else None ctx = { "card_stack_state": card_stack_state, - "taken_roles": taken_roles, + "starter_roles": starter_roles, "assigned_seats": assigned_seats, "user_seat": user_seat, "user_slots": list( @@ -371,7 +375,7 @@ def select_role(request, room_id): 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) + return HttpResponse(status=409) active_seat.role = role active_seat.save() if room.table_seats.filter(role__isnull=True).exists(): @@ -380,6 +384,7 @@ def select_role(request, room_id): room.table_status = Room.SIG_SELECT room.save() _notify_roles_revealed(room_id) + return HttpResponse(status=200) return redirect("epic:gatekeeper", room_id=room_id) diff --git a/src/static_src/tests/RoleSelectSpec.js b/src/static_src/tests/RoleSelectSpec.js index 66e2453..5fb407a 100644 --- a/src/static_src/tests/RoleSelectSpec.js +++ b/src/static_src/tests/RoleSelectSpec.js @@ -180,7 +180,7 @@ describe("RoleSelect", () => { stack.className = "card-stack"; stack.dataset.state = "ineligible"; stack.dataset.userSlots = "1"; - stack.dataset.takenRoles = ""; + stack.dataset.starterRoles = ""; testDiv.appendChild(stack); }); diff --git a/src/templates/apps/gameboard/room.html b/src/templates/apps/gameboard/room.html index 5e8935f..bdc3078 100644 --- a/src/templates/apps/gameboard/room.html +++ b/src/templates/apps/gameboard/room.html @@ -13,7 +13,7 @@
{% if room.table_status == "ROLE_SELECT" and card_stack_state %}
{% if card_stack_state == "ineligible" %}