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
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Disco DeDisco
2026-03-18 23:14:53 -04:00
parent 4f076165ef
commit 8c2a5d24ec
5 changed files with 51 additions and 18 deletions

View File

@@ -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);
}
}
}

View File

@@ -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):

View File

@@ -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)

View File

@@ -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);
});

View File

@@ -13,7 +13,7 @@
<div class="table-center">
{% if room.table_status == "ROLE_SELECT" and card_stack_state %}
<div class="card-stack" data-state="{{ card_stack_state }}"
data-taken-roles="{{ taken_roles|join:',' }}"
data-starter-roles="{{ starter_roles|join:',' }}"
data-user-slots="{{ user_slots|join:',' }}">
{% if card_stack_state == "ineligible" %}
<i class="fa-solid fa-ban"></i>