new _room_gear.html to manage room actions for various gamers (e.g., founders & guests); new _room.scss for gatekeeper styling (still flimsy); added new .btn-abandon Bl-btn palette to _button-pad.scss; new FTs & epic view ITs assert functionality (100 percent coverage, fully passing test suite)
This commit is contained in:
@@ -2,7 +2,7 @@ from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.lyric.models import User
|
||||
from apps.epic.models import Room
|
||||
from apps.epic.models import Room, RoomInvite
|
||||
|
||||
|
||||
class RoomCreationViewTest(TestCase):
|
||||
@@ -74,3 +74,58 @@ class GateStatusViewTest(TestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "gate-modal")
|
||||
|
||||
|
||||
class RoomActionsViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner = User.objects.create(email="owner@test.io")
|
||||
self.gamer = User.objects.create(email="gamer@test.io")
|
||||
self.room = Room.objects.create(name="Test Room", owner=self.owner)
|
||||
self.slot = self.room.gate_slots.get(slot_number=2)
|
||||
self.slot.gamer = self.gamer
|
||||
self.slot.status = "FILLED"
|
||||
self.slot.save()
|
||||
RoomInvite.objects.create(
|
||||
room=self.room, inviter=self.owner,
|
||||
invitee_email=self.gamer.email
|
||||
)
|
||||
|
||||
def test_owner_delete_removes_room(self):
|
||||
self.client.force_login(self.owner)
|
||||
self.client.post(reverse("epic:delete_room", kwargs={"room_id": self.room.id}))
|
||||
self.assertFalse(Room.objects.filter(pk=self.room.pk).exists())
|
||||
|
||||
def test_non_owner_delete_does_not_remove_room(self):
|
||||
self.client.force_login(self.gamer)
|
||||
self.client.post(reverse("epic:delete_room", kwargs={"room_id": self.room.id}))
|
||||
self.assertTrue(Room.objects.filter(pk=self.room.pk).exists())
|
||||
|
||||
def test_delete_redirects_to_gameboard(self):
|
||||
self.client.force_login(self.owner)
|
||||
response = self.client.post(
|
||||
reverse("epic:delete_room", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertRedirects(response, "/gameboard/")
|
||||
|
||||
def test_abandon_clears_slot(self):
|
||||
self.client.force_login(self.gamer)
|
||||
self.client.post(reverse("epic:abandon_room", kwargs={"room_id": self.room.id}))
|
||||
self.slot.refresh_from_db()
|
||||
self.assertEqual(self.slot.status, "EMPTY")
|
||||
self.assertIsNone(self.slot.gamer)
|
||||
|
||||
def test_abandon_deletes_pending_invite(self):
|
||||
self.client.force_login(self.gamer)
|
||||
self.client.post(reverse("epic:abandon_room", kwargs={"room_id": self.room.id}))
|
||||
self.assertFalse(
|
||||
RoomInvite.objects.filter(
|
||||
room=self.room, invitee_email=self.gamer.email
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_abandon_redirects_to_gameboard(self):
|
||||
self.client.force_login(self.gamer)
|
||||
response = self.client.post(
|
||||
reverse("epic:abandon_room", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertRedirects(response, "/gameboard/")
|
||||
|
||||
@@ -10,5 +10,7 @@ urlpatterns = [
|
||||
path('room/<uuid:room_id>/gate/<int:slot_number>/drop_token', views.drop_token, name='drop_token'),
|
||||
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'),
|
||||
path('room/<uuid:room_id>/abandon', views.abandon_room, name='abandon_room'),
|
||||
]
|
||||
|
||||
|
||||
@@ -56,6 +56,27 @@ def invite_gamer(request, room_id):
|
||||
)
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
|
||||
@login_required
|
||||
def delete_room(request, room_id):
|
||||
if request.method == "POST":
|
||||
room = Room.objects.get(id=room_id)
|
||||
if request.user == room.owner:
|
||||
room.delete()
|
||||
return redirect("/gameboard/")
|
||||
|
||||
@login_required
|
||||
def abandon_room(request, room_id):
|
||||
if request.method == "POST":
|
||||
room = Room.objects.get(id=room_id)
|
||||
room.gate_slots.filter(gamer=request.user).update(
|
||||
gamer=None, status="EMPTY", filled_at=None
|
||||
)
|
||||
room.invites.filter(
|
||||
invitee_email=request.user.email,
|
||||
status=RoomInvite.PENDING
|
||||
).delete()
|
||||
return redirect("/gameboard/")
|
||||
|
||||
def gate_status(request, room_id):
|
||||
room = Room.objects.get(id=room_id)
|
||||
if room.gate_status == Room.OPEN:
|
||||
|
||||
@@ -176,3 +176,45 @@ class GatekeeperTest(FunctionalTest):
|
||||
# Restore the following once room built
|
||||
# body = self.browser.find_element(By.TAG_NAME, "body")
|
||||
# self.assertIn("OPEN", body.text)
|
||||
|
||||
def test_owner_can_delete_room_via_gear_menu(self):
|
||||
self.create_pre_authenticated_session("founder@test.io")
|
||||
self.browser.get(self.live_server_url + "/gameboard/")
|
||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_new_game_name"))
|
||||
self.browser.find_element(By.ID, "id_new_game_name").send_keys("Doomed Room")
|
||||
self.browser.find_element(By.ID, "id_create_game_btn").click()
|
||||
self.wait_for(lambda: self.assertIn("/gate/", self.browser.current_url))
|
||||
|
||||
self.browser.find_element(By.CSS_SELECTOR, ".gear-btn").click()
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-danger")
|
||||
).click()
|
||||
|
||||
self.wait_for(lambda: self.assertEqual(
|
||||
self.browser.current_url, self.live_server_url + "/gameboard/"
|
||||
))
|
||||
self.assertFalse(Room.objects.filter(name="Doomed Room").exists())
|
||||
|
||||
def test_gamer_can_abandon_room_via_gear_menu(self):
|
||||
founder = User.objects.create(email="founder@test.io")
|
||||
room = Room.objects.create(name="Dragon's Den", owner=founder)
|
||||
slot = room.gate_slots.get(slot_number=2)
|
||||
self.create_pre_authenticated_session("gamer@test.io")
|
||||
gamer, _ = User.objects.get_or_create(email="gamer@test.io")
|
||||
slot.gamer = gamer
|
||||
slot.status = "FILLED"
|
||||
slot.save()
|
||||
|
||||
self.browser.get(self.live_server_url + f"/gameboard/room/{room.id}/gate/")
|
||||
|
||||
self.browser.find_element(By.CSS_SELECTOR, ".gear-btn").click()
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-abandon")
|
||||
).click()
|
||||
|
||||
self.wait_for(lambda: self.assertEqual(
|
||||
self.browser.current_url, self.live_server_url + "/gameboard/"
|
||||
))
|
||||
slot.refresh_from_db()
|
||||
self.assertEqual(slot.status, "EMPTY")
|
||||
self.assertIsNone(slot.gamer)
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
#id_dash_applet_menu { @extend %applet-menu; }
|
||||
#id_game_applet_menu { @extend %applet-menu; }
|
||||
#id_wallet_applet_menu { @extend %applet-menu; }
|
||||
#id_room_menu { @extend %applet-menu; }
|
||||
|
||||
// ── Applets grid (shared across all boards) ────────────────
|
||||
%applets-grid {
|
||||
|
||||
@@ -90,6 +90,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-abandon {
|
||||
color: rgba(var(--priBl), 1);
|
||||
border-color: rgba(var(--priBl), 1);
|
||||
background-color: rgba(var(--terBl), 1);
|
||||
box-shadow:
|
||||
0.1rem 0.1rem 0.12rem rgba(var(--terBl), 0.25),
|
||||
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||
0.25rem 0.25rem 0.25rem rgba(var(--terBl), 0.12)
|
||||
;
|
||||
|
||||
&:hover {
|
||||
text-shadow:
|
||||
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
|
||||
0 0 1rem rgba(var(--priBl), 1)
|
||||
;
|
||||
box-shadow:
|
||||
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
|
||||
0 0 0.5rem rgba(var(--priBl), 0.12)
|
||||
;
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: 0.18rem solid rgba(var(--priBl), 1);
|
||||
text-shadow:
|
||||
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||
0 0 0.12rem rgba(var(--priBl), 1)
|
||||
;
|
||||
box-shadow:
|
||||
-0.1rem -0.1rem 0.12rem rgba(var(--terBl), 0.25),
|
||||
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
|
||||
0 0 0.5rem rgba(var(--priBl), 0.12)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-cancel {
|
||||
color: rgba(var(--priOr), 1);
|
||||
border-color: rgba(var(--priOr), 1);
|
||||
|
||||
119
src/static_src/scss/_room.scss
Normal file
119
src/static_src/scss/_room.scss
Normal file
@@ -0,0 +1,119 @@
|
||||
$gate-node: 64px;
|
||||
$gate-gap: 36px;
|
||||
$gate-line: 2px;
|
||||
|
||||
.room-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.room-page .gear-btn,
|
||||
#id_room_menu {
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.gate-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.gate-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
padding: 2rem;
|
||||
border-radius: 1rem;
|
||||
background-color: rgba(var(--priUser), 1);
|
||||
|
||||
.gate-header {
|
||||
text-align: center;
|
||||
h1 { margin: 0; }
|
||||
.gate-status {
|
||||
opacity: 0.5;
|
||||
font-size: 0.75em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
}
|
||||
|
||||
.gate-slots {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: $gate-gap;
|
||||
|
||||
.gate-slot {
|
||||
position: relative;
|
||||
width: $gate-node;
|
||||
height: $gate-node;
|
||||
border-radius: 50%;
|
||||
border: $gate-line solid rgba(var(--terUser), 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.filled {
|
||||
background: rgba(var(--terUser), 0.2);
|
||||
}
|
||||
|
||||
.slot-number {
|
||||
font-size: 0.7em;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.slot-gamer { display: none; }
|
||||
|
||||
form {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.drop-token-btn {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--terUser), 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile: 2×3 grid, both rows left-to-right
|
||||
@media (max-width: 550px) {
|
||||
.gate-modal .gate-slots {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, $gate-node);
|
||||
grid-template-rows: repeat(2, $gate-node);
|
||||
gap: $gate-gap;
|
||||
|
||||
.gate-slot {
|
||||
&:nth-child(1) { grid-column: 1; grid-row: 1; }
|
||||
&:nth-child(2) { grid-column: 2; grid-row: 1; }
|
||||
&:nth-child(3) { grid-column: 3; grid-row: 1; }
|
||||
&:nth-child(4) { grid-column: 1; grid-row: 2; }
|
||||
&:nth-child(5) { grid-column: 2; grid-row: 2; }
|
||||
&:nth-child(6) { grid-column: 3; grid-row: 2; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
@import 'dashboard';
|
||||
@import 'gameboard';
|
||||
@import 'palette-picker';
|
||||
@import 'room';
|
||||
@import 'wallet-tokens';
|
||||
|
||||
|
||||
|
||||
16
src/templates/apps/gameboard/_partials/_room_gear.html
Normal file
16
src/templates/apps/gameboard/_partials/_room_gear.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% include "apps/applets/_partials/_gear.html" with menu_id="id_room_menu" %}
|
||||
|
||||
<div id="id_room_menu" style="display:none">
|
||||
<a href="/gameboard/" class="btn btn-cancel">EXIT</a>
|
||||
{% if request.user == room.owner %}
|
||||
<form method="POST" action="{% url 'epic:delete_room' room.id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">DEL</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="POST" action="{% url 'epic:abandon_room' room.id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-abandon">BYE</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -1,5 +1,8 @@
|
||||
{% extends "core/base.html" %}
|
||||
|
||||
{% block title_text %}Gameboard{% endblock title_text %}
|
||||
{% block header_text %}<span>Game</span>room{% endblock header_text %}
|
||||
|
||||
{% block content %}
|
||||
<div class="room-page">
|
||||
<div class="room-shell">
|
||||
@@ -19,5 +22,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include "apps/gameboard/_partials/_room_gear.html" %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
Reference in New Issue
Block a user