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 django.urls import reverse
|
||||||
|
|
||||||
from apps.lyric.models import User
|
from apps.lyric.models import User
|
||||||
from apps.epic.models import Room
|
from apps.epic.models import Room, RoomInvite
|
||||||
|
|
||||||
|
|
||||||
class RoomCreationViewTest(TestCase):
|
class RoomCreationViewTest(TestCase):
|
||||||
@@ -74,3 +74,58 @@ class GateStatusViewTest(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, "gate-modal")
|
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/<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/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>/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)
|
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):
|
def gate_status(request, room_id):
|
||||||
room = Room.objects.get(id=room_id)
|
room = Room.objects.get(id=room_id)
|
||||||
if room.gate_status == Room.OPEN:
|
if room.gate_status == Room.OPEN:
|
||||||
|
|||||||
@@ -176,3 +176,45 @@ class GatekeeperTest(FunctionalTest):
|
|||||||
# Restore the following once room built
|
# Restore the following once room built
|
||||||
# body = self.browser.find_element(By.TAG_NAME, "body")
|
# body = self.browser.find_element(By.TAG_NAME, "body")
|
||||||
# self.assertIn("OPEN", body.text)
|
# 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_dash_applet_menu { @extend %applet-menu; }
|
||||||
#id_game_applet_menu { @extend %applet-menu; }
|
#id_game_applet_menu { @extend %applet-menu; }
|
||||||
#id_wallet_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 (shared across all boards) ────────────────
|
||||||
%applets-grid {
|
%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 {
|
&.btn-cancel {
|
||||||
color: rgba(var(--priOr), 1);
|
color: rgba(var(--priOr), 1);
|
||||||
border-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 'dashboard';
|
||||||
@import 'gameboard';
|
@import 'gameboard';
|
||||||
@import 'palette-picker';
|
@import 'palette-picker';
|
||||||
|
@import 'room';
|
||||||
@import 'wallet-tokens';
|
@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" %}
|
{% extends "core/base.html" %}
|
||||||
|
|
||||||
|
{% block title_text %}Gameboard{% endblock title_text %}
|
||||||
|
{% block header_text %}<span>Game</span>room{% endblock header_text %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="room-page">
|
<div class="room-page">
|
||||||
<div class="room-shell">
|
<div class="room-shell">
|
||||||
@@ -19,5 +22,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% include "apps/gameboard/_partials/_room_gear.html" %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
Reference in New Issue
Block a user