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
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -34,11 +34,11 @@ var RoleSelect = (function () {
|
|||||||
var invSlot = document.getElementById("id_inv_role_card");
|
var invSlot = document.getElementById("id_inv_role_card");
|
||||||
if (invSlot) invSlot.appendChild(clean);
|
if (invSlot) invSlot.appendChild(clean);
|
||||||
|
|
||||||
// Update the stack's taken-roles so the next openFan() filters correctly
|
// Optimistically mark role as taken so re-opened fan filters it
|
||||||
var stack = document.querySelector(".card-stack[data-taken-roles]");
|
var stack = document.querySelector(".card-stack[data-starter-roles]");
|
||||||
if (stack) {
|
if (stack) {
|
||||||
var current = stack.dataset.takenRoles;
|
var current = stack.dataset.starterRoles;
|
||||||
stack.dataset.takenRoles = current ? current + "," + roleCode : roleCode;
|
stack.dataset.starterRoles = current ? current + "," + roleCode : roleCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = getSelectRoleUrl();
|
var url = getSelectRoleUrl();
|
||||||
@@ -50,20 +50,30 @@ var RoleSelect = (function () {
|
|||||||
"X-CSRFToken": getCsrf(),
|
"X-CSRFToken": getCsrf(),
|
||||||
},
|
},
|
||||||
body: "role=" + encodeURIComponent(roleCode),
|
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() {
|
function getStarterRoles() {
|
||||||
var stack = document.querySelector(".card-stack[data-taken-roles]");
|
var stack = document.querySelector(".card-stack[data-starter-roles]");
|
||||||
if (!stack) return [];
|
if (!stack) return [];
|
||||||
var raw = stack.dataset.takenRoles;
|
var raw = stack.dataset.starterRoles;
|
||||||
return raw ? raw.split(",").map(function (s) { return s.trim(); }) : [];
|
return raw ? raw.split(",").map(function (s) { return s.trim(); }) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function openFan() {
|
function openFan() {
|
||||||
if (document.querySelector(".role-select-backdrop")) return;
|
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 available = ROLES.filter(function (r) { return taken.indexOf(r.code) === -1; });
|
||||||
|
|
||||||
var backdrop = document.createElement("div");
|
var backdrop = document.createElement("div");
|
||||||
@@ -124,18 +134,30 @@ var RoleSelect = (function () {
|
|||||||
var invSlot = document.getElementById("id_inv_role_card");
|
var invSlot = document.getElementById("id_inv_role_card");
|
||||||
if (invSlot) invSlot.innerHTML = "";
|
if (invSlot) invSlot.innerHTML = "";
|
||||||
|
|
||||||
// Update card-stack eligibility
|
|
||||||
var stack = document.querySelector(".card-stack[data-user-slots]");
|
var stack = document.querySelector(".card-stack[data-user-slots]");
|
||||||
if (stack) {
|
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
|
var userSlots = stack.dataset.userSlots
|
||||||
? stack.dataset.userSlots.split(",") : [];
|
? stack.dataset.userSlots.split(",") : [];
|
||||||
if (userSlots.indexOf(active) !== -1) {
|
if (userSlots.indexOf(active) !== -1) {
|
||||||
stack.dataset.state = "eligible";
|
stack.dataset.state = "eligible";
|
||||||
|
var ban = stack.querySelector(".fa-ban");
|
||||||
|
if (ban) ban.remove();
|
||||||
stack.removeEventListener("click", openFan);
|
stack.removeEventListener("click", openFan);
|
||||||
stack.addEventListener("click", openFan);
|
stack.addEventListener("click", openFan);
|
||||||
} else {
|
} else {
|
||||||
stack.dataset.state = "ineligible";
|
stack.dataset.state = "ineligible";
|
||||||
stack.removeEventListener("click", openFan);
|
stack.removeEventListener("click", openFan);
|
||||||
|
if (!stack.querySelector(".fa-ban")) {
|
||||||
|
var icon = document.createElement("i");
|
||||||
|
icon.className = "fa-solid fa-ban";
|
||||||
|
stack.appendChild(icon);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -593,14 +593,20 @@ class SelectRoleViewTest(TestCase):
|
|||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertIn("/accounts/login/", response.url)
|
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(
|
response = self.client.post(
|
||||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||||
data={"role": "PC"},
|
data={"role": "PC"},
|
||||||
)
|
)
|
||||||
self.assertRedirects(
|
self.assertEqual(response.status_code, 200)
|
||||||
response, reverse("epic:gatekeeper", args=[self.room.id])
|
|
||||||
|
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):
|
class RevealPhaseRenderingTest(TestCase):
|
||||||
|
|||||||
@@ -26,9 +26,13 @@ def _notify_turn_changed(room_id):
|
|||||||
room_id=room_id, role__isnull=True
|
room_id=room_id, role__isnull=True
|
||||||
).order_by("slot_number").first()
|
).order_by("slot_number").first()
|
||||||
active_slot = active_seat.slot_number if active_seat else None
|
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)(
|
async_to_sync(get_channel_layer().group_send)(
|
||||||
f'room_{room_id}',
|
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"
|
card_stack_state = "eligible"
|
||||||
else:
|
else:
|
||||||
card_stack_state = "ineligible"
|
card_stack_state = "ineligible"
|
||||||
taken_roles = list(
|
starter_roles = list(
|
||||||
room.table_seats.exclude(role__isnull=True).values_list("role", flat=True)
|
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"])}
|
_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
|
active_slot = active_seat.slot_number if active_seat else None
|
||||||
ctx = {
|
ctx = {
|
||||||
"card_stack_state": card_stack_state,
|
"card_stack_state": card_stack_state,
|
||||||
"taken_roles": taken_roles,
|
"starter_roles": starter_roles,
|
||||||
"assigned_seats": assigned_seats,
|
"assigned_seats": assigned_seats,
|
||||||
"user_seat": user_seat,
|
"user_seat": user_seat,
|
||||||
"user_slots": list(
|
"user_slots": list(
|
||||||
@@ -371,7 +375,7 @@ def select_role(request, room_id):
|
|||||||
if not role or role not in valid_roles:
|
if not role or role not in valid_roles:
|
||||||
return redirect("epic:gatekeeper", room_id=room_id)
|
return redirect("epic:gatekeeper", room_id=room_id)
|
||||||
if room.table_seats.filter(role=role).exists():
|
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.role = role
|
||||||
active_seat.save()
|
active_seat.save()
|
||||||
if room.table_seats.filter(role__isnull=True).exists():
|
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.table_status = Room.SIG_SELECT
|
||||||
room.save()
|
room.save()
|
||||||
_notify_roles_revealed(room_id)
|
_notify_roles_revealed(room_id)
|
||||||
|
return HttpResponse(status=200)
|
||||||
return redirect("epic:gatekeeper", room_id=room_id)
|
return redirect("epic:gatekeeper", room_id=room_id)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ describe("RoleSelect", () => {
|
|||||||
stack.className = "card-stack";
|
stack.className = "card-stack";
|
||||||
stack.dataset.state = "ineligible";
|
stack.dataset.state = "ineligible";
|
||||||
stack.dataset.userSlots = "1";
|
stack.dataset.userSlots = "1";
|
||||||
stack.dataset.takenRoles = "";
|
stack.dataset.starterRoles = "";
|
||||||
testDiv.appendChild(stack);
|
testDiv.appendChild(stack);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<div class="table-center">
|
<div class="table-center">
|
||||||
{% if room.table_status == "ROLE_SELECT" and card_stack_state %}
|
{% if room.table_status == "ROLE_SELECT" and card_stack_state %}
|
||||||
<div class="card-stack" data-state="{{ 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:',' }}">
|
data-user-slots="{{ user_slots|join:',' }}">
|
||||||
{% if card_stack_state == "ineligible" %}
|
{% if card_stack_state == "ineligible" %}
|
||||||
<i class="fa-solid fa-ban"></i>
|
<i class="fa-solid fa-ban"></i>
|
||||||
|
|||||||
Reference in New Issue
Block a user