new applet structure for apps.billboard, incl. My Scrolls, Contacts & Most Recent applets; completely revamped _billboard.scss, tho some styling inconsistencies persist; ensured #id_billboard_applets_container inherited base styles found in _applets.scss; a pair of new migrations in apps.applets to support new applet models & fields; billboard gets its first ITs, new urls & views; pair of new FT classes in FTs.test_billboard
This commit is contained in:
48
src/apps/applets/migrations/0006_billboard_applets.py
Normal file
48
src/apps/applets/migrations/0006_billboard_applets.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def seed_billboard_applets(apps, schema_editor):
|
||||||
|
Applet = apps.get_model("applets", "Applet")
|
||||||
|
for slug, name, cols, rows in [
|
||||||
|
("billboard-my-scrolls", "My Scrolls", 4, 3),
|
||||||
|
("billboard-my-contacts", "Contacts", 4, 3),
|
||||||
|
("billboard-most-recent", "Most Recent", 8, 6),
|
||||||
|
]:
|
||||||
|
Applet.objects.get_or_create(
|
||||||
|
slug=slug,
|
||||||
|
defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "billboard"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_billboard_applets(apps, schema_editor):
|
||||||
|
Applet = apps.get_model("applets", "Applet")
|
||||||
|
Applet.objects.filter(slug__in=[
|
||||||
|
"billboard-my-scrolls",
|
||||||
|
"billboard-my-contacts",
|
||||||
|
"billboard-most-recent",
|
||||||
|
]).delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("applets", "0005_gameboard_applet_heights"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="applet",
|
||||||
|
name="context",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("dashboard", "Dashboard"),
|
||||||
|
("gameboard", "Gameboard"),
|
||||||
|
("wallet", "Wallet"),
|
||||||
|
("billboard", "Billboard"),
|
||||||
|
],
|
||||||
|
default="dashboard",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(seed_billboard_applets, remove_billboard_applets),
|
||||||
|
]
|
||||||
29
src/apps/applets/migrations/0007_fix_billboard_applets.py
Normal file
29
src/apps/applets/migrations/0007_fix_billboard_applets.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def fix_billboard_applets(apps, schema_editor):
|
||||||
|
Applet = apps.get_model("applets", "Applet")
|
||||||
|
# billboard-scroll belongs only to the billscroll page template, not the grid
|
||||||
|
Applet.objects.filter(slug="billboard-scroll").delete()
|
||||||
|
# Rename "My Contacts" → "Contacts"
|
||||||
|
Applet.objects.filter(slug="billboard-my-contacts").update(name="Contacts")
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_fix_billboard_applets(apps, schema_editor):
|
||||||
|
Applet = apps.get_model("applets", "Applet")
|
||||||
|
Applet.objects.get_or_create(
|
||||||
|
slug="billboard-scroll",
|
||||||
|
defaults={"name": "Billscroll", "grid_cols": 12, "grid_rows": 6, "context": "billboard"},
|
||||||
|
)
|
||||||
|
Applet.objects.filter(slug="billboard-my-contacts").update(name="My Contacts")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("applets", "0006_billboard_applets"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(fix_billboard_applets, reverse_fix_billboard_applets),
|
||||||
|
]
|
||||||
@@ -4,10 +4,12 @@ class Applet(models.Model):
|
|||||||
DASHBOARD = "dashboard"
|
DASHBOARD = "dashboard"
|
||||||
GAMEBOARD = "gameboard"
|
GAMEBOARD = "gameboard"
|
||||||
WALLET = "wallet"
|
WALLET = "wallet"
|
||||||
|
BILLBOARD = "billboard"
|
||||||
CONTEXT_CHOICES = [
|
CONTEXT_CHOICES = [
|
||||||
(DASHBOARD, "Dashboard"),
|
(DASHBOARD, "Dashboard"),
|
||||||
(GAMEBOARD, "Gameboard"),
|
(GAMEBOARD, "Gameboard"),
|
||||||
(WALLET, "Wallet"),
|
(WALLET, "Wallet"),
|
||||||
|
(BILLBOARD, "Billboard"),
|
||||||
]
|
]
|
||||||
|
|
||||||
slug = models.SlugField(unique=True)
|
slug = models.SlugField(unique=True)
|
||||||
|
|||||||
0
src/apps/billboard/tests/__init__.py
Normal file
0
src/apps/billboard/tests/__init__.py
Normal file
0
src/apps/billboard/tests/integrated/__init__.py
Normal file
0
src/apps/billboard/tests/integrated/__init__.py
Normal file
111
src/apps/billboard/tests/integrated/test_billboard_views.py
Normal file
111
src/apps/billboard/tests/integrated/test_billboard_views.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from apps.applets.models import Applet
|
||||||
|
from apps.drama.models import GameEvent, record
|
||||||
|
from apps.epic.models import Room
|
||||||
|
from apps.lyric.models import User
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_billboard_applets():
|
||||||
|
for slug, name, cols, rows in [
|
||||||
|
("billboard-my-scrolls", "My Scrolls", 4, 3),
|
||||||
|
("billboard-my-contacts", "Contacts", 4, 3),
|
||||||
|
("billboard-most-recent", "Most Recent", 8, 6),
|
||||||
|
]:
|
||||||
|
Applet.objects.get_or_create(
|
||||||
|
slug=slug,
|
||||||
|
defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "billboard"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BillboardViewTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create(email="test@billboard.io")
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
_seed_billboard_applets()
|
||||||
|
|
||||||
|
def test_uses_billboard_template(self):
|
||||||
|
response = self.client.get("/billboard/")
|
||||||
|
self.assertTemplateUsed(response, "apps/billboard/billboard.html")
|
||||||
|
|
||||||
|
def test_passes_applets_context(self):
|
||||||
|
response = self.client.get("/billboard/")
|
||||||
|
self.assertIn("applets", response.context)
|
||||||
|
slugs = [e["applet"].slug for e in response.context["applets"]]
|
||||||
|
self.assertIn("billboard-my-scrolls", slugs)
|
||||||
|
self.assertIn("billboard-my-contacts", slugs)
|
||||||
|
self.assertIn("billboard-most-recent", slugs)
|
||||||
|
|
||||||
|
def test_passes_my_rooms_context(self):
|
||||||
|
room = Room.objects.create(name="Test Room", owner=self.user)
|
||||||
|
response = self.client.get("/billboard/")
|
||||||
|
self.assertIn(room, response.context["my_rooms"])
|
||||||
|
|
||||||
|
def test_passes_recent_room_and_events(self):
|
||||||
|
room = Room.objects.create(name="Test Room", owner=self.user)
|
||||||
|
record(
|
||||||
|
room, GameEvent.SLOT_FILLED, actor=self.user,
|
||||||
|
slot_number=1, token_type="coin",
|
||||||
|
token_display="Coin-on-a-String", renewal_days=7,
|
||||||
|
)
|
||||||
|
response = self.client.get("/billboard/")
|
||||||
|
self.assertEqual(response.context["recent_room"], room)
|
||||||
|
self.assertEqual(len(response.context["recent_events"]), 1)
|
||||||
|
|
||||||
|
def test_recent_room_is_none_when_no_events(self):
|
||||||
|
response = self.client.get("/billboard/")
|
||||||
|
self.assertIsNone(response.context["recent_room"])
|
||||||
|
self.assertEqual(list(response.context["recent_events"]), [])
|
||||||
|
|
||||||
|
|
||||||
|
class ToggleBillboardAppletsTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create(email="test@toggle.io")
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
_seed_billboard_applets()
|
||||||
|
|
||||||
|
def test_toggle_hides_unchecked_applets(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("billboard:toggle_applets"),
|
||||||
|
{"applets": ["billboard-my-scrolls"]},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
from apps.applets.models import UserApplet
|
||||||
|
contacts = Applet.objects.get(slug="billboard-my-contacts")
|
||||||
|
ua = UserApplet.objects.get(user=self.user, applet=contacts)
|
||||||
|
self.assertFalse(ua.visible)
|
||||||
|
|
||||||
|
def test_toggle_returns_partial_on_htmx(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("billboard:toggle_applets"),
|
||||||
|
{"applets": ["billboard-my-scrolls"]},
|
||||||
|
HTTP_HX_REQUEST="true",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, "apps/billboard/_partials/_applets.html")
|
||||||
|
|
||||||
|
|
||||||
|
class BillscrollViewTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create(email="test@billscroll.io")
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
self.room = Room.objects.create(name="Test Room", owner=self.user)
|
||||||
|
record(
|
||||||
|
self.room, GameEvent.SLOT_FILLED, actor=self.user,
|
||||||
|
slot_number=1, token_type="coin",
|
||||||
|
token_display="Coin-on-a-String", renewal_days=7,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_uses_room_scroll_template(self):
|
||||||
|
response = self.client.get(f"/billboard/room/{self.room.id}/scroll/")
|
||||||
|
self.assertTemplateUsed(response, "apps/billboard/room_scroll.html")
|
||||||
|
|
||||||
|
def test_passes_events_context(self):
|
||||||
|
response = self.client.get(f"/billboard/room/{self.room.id}/scroll/")
|
||||||
|
self.assertIn("events", response.context)
|
||||||
|
self.assertEqual(response.context["events"].count(), 1)
|
||||||
|
|
||||||
|
def test_passes_page_class_billscroll(self):
|
||||||
|
response = self.client.get(f"/billboard/room/{self.room.id}/scroll/")
|
||||||
|
self.assertEqual(response.context["page_class"], "page-billscroll")
|
||||||
@@ -6,5 +6,6 @@ app_name = "billboard"
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.billboard, name="billboard"),
|
path("", views.billboard, name="billboard"),
|
||||||
|
path("toggle-applets", views.toggle_billboard_applets, name="toggle_applets"),
|
||||||
path("room/<uuid:room_id>/scroll/", views.room_scroll, name="scroll"),
|
path("room/<uuid:room_id>/scroll/", views.room_scroll, name="scroll"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.db.models import Q
|
from django.db.models import Max, Q
|
||||||
from django.shortcuts import render
|
from django.shortcuts import redirect, render
|
||||||
|
|
||||||
|
from apps.applets.models import Applet, UserApplet
|
||||||
|
from apps.applets.utils import applet_context
|
||||||
from apps.drama.models import GameEvent
|
from apps.drama.models import GameEvent
|
||||||
from apps.epic.models import GateSlot, Room, RoomInvite
|
from apps.epic.models import GateSlot, Room, RoomInvite
|
||||||
|
|
||||||
@@ -13,12 +15,48 @@ def billboard(request):
|
|||||||
Q(gate_slots__gamer=request.user) |
|
Q(gate_slots__gamer=request.user) |
|
||||||
Q(invites__invitee_email=request.user.email, invites__status=RoomInvite.PENDING)
|
Q(invites__invitee_email=request.user.email, invites__status=RoomInvite.PENDING)
|
||||||
).distinct().order_by("-created_at")
|
).distinct().order_by("-created_at")
|
||||||
|
|
||||||
|
recent_room = (
|
||||||
|
Room.objects.filter(
|
||||||
|
Q(owner=request.user) | Q(gate_slots__gamer=request.user)
|
||||||
|
)
|
||||||
|
.annotate(last_event=Max("events__timestamp"))
|
||||||
|
.filter(last_event__isnull=False)
|
||||||
|
.order_by("-last_event")
|
||||||
|
.distinct()
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
recent_events = (
|
||||||
|
recent_room.events.select_related("actor").order_by("-timestamp")[:10]
|
||||||
|
if recent_room else []
|
||||||
|
)
|
||||||
|
|
||||||
return render(request, "apps/billboard/billboard.html", {
|
return render(request, "apps/billboard/billboard.html", {
|
||||||
"my_rooms": my_rooms,
|
"my_rooms": my_rooms,
|
||||||
|
"recent_room": recent_room,
|
||||||
|
"recent_events": recent_events,
|
||||||
|
"viewer": request.user,
|
||||||
|
"applets": applet_context(request.user, "billboard"),
|
||||||
"page_class": "page-billboard",
|
"page_class": "page-billboard",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required(login_url="/")
|
||||||
|
def toggle_billboard_applets(request):
|
||||||
|
checked = request.POST.getlist("applets")
|
||||||
|
for applet in Applet.objects.filter(context="billboard"):
|
||||||
|
UserApplet.objects.update_or_create(
|
||||||
|
user=request.user,
|
||||||
|
applet=applet,
|
||||||
|
defaults={"visible": applet.slug in checked},
|
||||||
|
)
|
||||||
|
if request.headers.get("HX-Request"):
|
||||||
|
return render(request, "apps/billboard/_partials/_applets.html", {
|
||||||
|
"applets": applet_context(request.user, "billboard"),
|
||||||
|
})
|
||||||
|
return redirect("billboard:billboard")
|
||||||
|
|
||||||
|
|
||||||
@login_required(login_url="/")
|
@login_required(login_url="/")
|
||||||
def room_scroll(request, room_id):
|
def room_scroll(request, room_id):
|
||||||
room = Room.objects.get(id=room_id)
|
room = Room.objects.get(id=room_id)
|
||||||
@@ -27,5 +65,5 @@ def room_scroll(request, room_id):
|
|||||||
"room": room,
|
"room": room,
|
||||||
"events": events,
|
"events": events,
|
||||||
"viewer": request.user,
|
"viewer": request.user,
|
||||||
"page_class": "page-billboard",
|
"page_class": "page-billscroll",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
|
|
||||||
from .base import FunctionalTest
|
from .base import FunctionalTest
|
||||||
|
from apps.applets.models import Applet
|
||||||
from apps.drama.models import GameEvent, record
|
from apps.drama.models import GameEvent, record
|
||||||
from apps.epic.models import Room, GateSlot
|
from apps.epic.models import Room, GateSlot
|
||||||
from apps.lyric.models import User
|
from apps.lyric.models import User
|
||||||
@@ -18,8 +19,17 @@ class BillboardScrollTest(FunctionalTest):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.founder = User.objects.create(email="founder@scroll.io")
|
for slug, name, cols, rows in [
|
||||||
self.other = User.objects.create(email="other@scroll.io")
|
("billboard-my-scrolls", "My Scrolls", 4, 3),
|
||||||
|
("billboard-my-contacts", "Contacts", 4, 3),
|
||||||
|
("billboard-most-recent", "Most Recent", 8, 6),
|
||||||
|
]:
|
||||||
|
Applet.objects.get_or_create(
|
||||||
|
slug=slug,
|
||||||
|
defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "billboard"},
|
||||||
|
)
|
||||||
|
self.founder = User.objects.create(email="founder@test.io")
|
||||||
|
self.other = User.objects.create(email="other@test.io")
|
||||||
self.room = Room.objects.create(name="Blissful Ignorance", owner=self.founder)
|
self.room = Room.objects.create(name="Blissful Ignorance", owner=self.founder)
|
||||||
# Simulate two gate fills — one by founder, one by the other gamer
|
# Simulate two gate fills — one by founder, one by the other gamer
|
||||||
record(
|
record(
|
||||||
@@ -44,7 +54,7 @@ class BillboardScrollTest(FunctionalTest):
|
|||||||
|
|
||||||
def test_footer_scroll_icon_leads_to_billboard_with_rooms(self):
|
def test_footer_scroll_icon_leads_to_billboard_with_rooms(self):
|
||||||
# Founder logs in and lands on the dashboard
|
# Founder logs in and lands on the dashboard
|
||||||
self.create_pre_authenticated_session("founder@scroll.io")
|
self.create_pre_authenticated_session("founder@test.io")
|
||||||
self.browser.get(self.live_server_url + "/")
|
self.browser.get(self.live_server_url + "/")
|
||||||
|
|
||||||
# Footer contains a scroll icon link pointing to /billboard/
|
# Footer contains a scroll icon link pointing to /billboard/
|
||||||
@@ -65,7 +75,7 @@ class BillboardScrollTest(FunctionalTest):
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
def test_scroll_shows_human_readable_event_log(self):
|
def test_scroll_shows_human_readable_event_log(self):
|
||||||
self.create_pre_authenticated_session("founder@scroll.io")
|
self.create_pre_authenticated_session("founder@test.io")
|
||||||
self.browser.get(self.live_server_url + "/billboard/")
|
self.browser.get(self.live_server_url + "/billboard/")
|
||||||
|
|
||||||
# Click the room link to reach the scroll
|
# Click the room link to reach the scroll
|
||||||
@@ -93,7 +103,7 @@ class BillboardScrollTest(FunctionalTest):
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
def test_scroll_aligns_own_events_right_and_others_left(self):
|
def test_scroll_aligns_own_events_right_and_others_left(self):
|
||||||
self.create_pre_authenticated_session("founder@scroll.io")
|
self.create_pre_authenticated_session("founder@test.io")
|
||||||
self.browser.get(self.live_server_url + "/billboard/")
|
self.browser.get(self.live_server_url + "/billboard/")
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.browser.find_element(By.LINK_TEXT, "Blissful Ignorance")
|
lambda: self.browser.find_element(By.LINK_TEXT, "Blissful Ignorance")
|
||||||
@@ -111,3 +121,102 @@ class BillboardScrollTest(FunctionalTest):
|
|||||||
|
|
||||||
# The other gamer's event mentions their display name
|
# The other gamer's event mentions their display name
|
||||||
self.assertIn("other", theirs_events[0].text)
|
self.assertIn("other", theirs_events[0].text)
|
||||||
|
|
||||||
|
|
||||||
|
class BillboardAppletsTest(FunctionalTest):
|
||||||
|
"""
|
||||||
|
FT: billboard page renders three applets in the grid — My Scrolls,
|
||||||
|
My Contacts, and Most Recent — with a functioning gear menu.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.founder = User.objects.create(email="founder@test.io")
|
||||||
|
self.room = Room.objects.create(name="Arcane Assembly", owner=self.founder)
|
||||||
|
for slug, name, cols, rows in [
|
||||||
|
("billboard-my-scrolls", "My Scrolls", 4, 3),
|
||||||
|
("billboard-my-contacts", "Contacts", 4, 3),
|
||||||
|
("billboard-most-recent", "Most Recent", 8, 6),
|
||||||
|
]:
|
||||||
|
Applet.objects.get_or_create(
|
||||||
|
slug=slug,
|
||||||
|
defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "billboard"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_billboard_shows_three_applets(self):
|
||||||
|
# 1. Log in, navigate to billboard
|
||||||
|
self.create_pre_authenticated_session("founder@test.io")
|
||||||
|
self.browser.get(self.live_server_url + "/billboard/")
|
||||||
|
# 2. Assert all three applet sections present
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.ID, "id_applet_billboard_my_scrolls")
|
||||||
|
)
|
||||||
|
self.browser.find_element(By.ID, "id_applet_billboard_my_contacts")
|
||||||
|
self.browser.find_element(By.ID, "id_applet_billboard_most_recent")
|
||||||
|
|
||||||
|
def test_billboard_my_scrolls_lists_rooms(self):
|
||||||
|
# 1. Log in, navigate to billboard
|
||||||
|
self.create_pre_authenticated_session("founder@test.io")
|
||||||
|
self.browser.get(self.live_server_url + "/billboard/")
|
||||||
|
# 2. My Scrolls applet contains a link to the room's scroll
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertIn(
|
||||||
|
"Arcane Assembly",
|
||||||
|
self.browser.find_element(By.ID, "id_applet_billboard_my_scrolls").text,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_billboard_gear_btn_opens_applet_menu(self):
|
||||||
|
# 1. Log in, navigate to billboard
|
||||||
|
self.create_pre_authenticated_session("founder@test.io")
|
||||||
|
self.browser.get(self.live_server_url + "/billboard/")
|
||||||
|
# 2. Gear button is visible
|
||||||
|
gear_btn = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".billboard-page .gear-btn")
|
||||||
|
)
|
||||||
|
# 3. Menu is hidden before click
|
||||||
|
menu = self.browser.find_element(By.ID, "id_billboard_applet_menu")
|
||||||
|
self.assertFalse(menu.is_displayed())
|
||||||
|
# 4. Clicking gear opens the menu (JS click bypasses kit-bag overlap in headless)
|
||||||
|
self.browser.execute_script("arguments[0].click()", gear_btn)
|
||||||
|
self.wait_for_slow(lambda: self.assertTrue(menu.is_displayed()))
|
||||||
|
|
||||||
|
|
||||||
|
class BillscrollAppletsTest(FunctionalTest):
|
||||||
|
"""
|
||||||
|
FT: billscroll page renders as a single full-width applet that fills
|
||||||
|
the viewport aperture and contains the room's drama events.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.founder = User.objects.create(email="founder@billtest.io")
|
||||||
|
self.room = Room.objects.create(name="Spectral Council", owner=self.founder)
|
||||||
|
record(
|
||||||
|
self.room, GameEvent.SLOT_FILLED, actor=self.founder,
|
||||||
|
slot_number=1, token_type="coin",
|
||||||
|
token_display="Coin-on-a-String", renewal_days=7,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_billscroll_shows_full_width_applet(self):
|
||||||
|
# 1. Log in, navigate to the room's scroll
|
||||||
|
self.create_pre_authenticated_session("founder@billtest.io")
|
||||||
|
self.browser.get(
|
||||||
|
self.live_server_url + f"/billboard/room/{self.room.id}/scroll/"
|
||||||
|
)
|
||||||
|
# 2. The full-width applet section is present
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.ID, "id_applet_billboard_scroll")
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_billscroll_applet_contains_drama_events(self):
|
||||||
|
# 1. Log in, navigate to the room's scroll
|
||||||
|
self.create_pre_authenticated_session("founder@billtest.io")
|
||||||
|
self.browser.get(
|
||||||
|
self.live_server_url + f"/billboard/room/{self.room.id}/scroll/"
|
||||||
|
)
|
||||||
|
# 2. Drama scroll is inside the applet and shows the event
|
||||||
|
scroll = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.ID, "id_drama_scroll")
|
||||||
|
)
|
||||||
|
self.assertIn("Coin-on-a-String", scroll.text)
|
||||||
|
|||||||
@@ -208,3 +208,4 @@
|
|||||||
#id_applets_container { @extend %applets-grid; }
|
#id_applets_container { @extend %applets-grid; }
|
||||||
#id_game_applets_container { @extend %applets-grid; }
|
#id_game_applets_container { @extend %applets-grid; }
|
||||||
#id_wallet_applets_container { @extend %applets-grid; }
|
#id_wallet_applets_container { @extend %applets-grid; }
|
||||||
|
#id_billboard_applets_container { @extend %applets-grid; }
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
html:has(body.page-billboard) {
|
// ── Shared aperture fill for both billboard pages ──────────────────────────
|
||||||
|
|
||||||
|
%billboard-page-base {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
html:has(body.page-billboard),
|
||||||
|
html:has(body.page-billscroll) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.page-billboard {
|
body.page-billboard,
|
||||||
|
body.page-billscroll {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@@ -19,10 +32,61 @@ body.page-billboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Billboard page (three-applet grid) ─────────────────────────────────────
|
||||||
|
|
||||||
.billboard-page {
|
.billboard-page {
|
||||||
flex: 1;
|
@extend %billboard-page-base;
|
||||||
min-width: 0;
|
|
||||||
overflow-y: auto;
|
// Gear btn positioning mirrors dashboard/wallet pattern
|
||||||
position: relative;
|
> .gear-btn {
|
||||||
padding: 0.75rem;
|
position: fixed;
|
||||||
|
bottom: calc(var(--footer-w, 4rem) + 0.75rem);
|
||||||
|
right: calc(var(--footer-w, 4rem) + 0.75rem);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Billscroll page (single full-aperture applet) ──────────────────────────
|
||||||
|
|
||||||
|
.billscroll-page {
|
||||||
|
@extend %billboard-page-base;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.75rem;
|
||||||
|
|
||||||
|
// The single scroll applet stretches to fill the remaining aperture
|
||||||
|
.applet-billboard-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
// Override grid-row span — this applet fills height via flex, not grid rows
|
||||||
|
grid-row: unset;
|
||||||
|
|
||||||
|
#id_drama_scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── My Scrolls list ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#id_applet_billboard_my_scrolls {
|
||||||
|
.scroll-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
border-bottom: 1px solid rgba(var(--priUser), 0.15);
|
||||||
|
|
||||||
|
&:last-child { border-bottom: none; }
|
||||||
|
|
||||||
|
a { text-decoration: none; }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<section
|
||||||
|
id="id_applet_billboard_most_recent"
|
||||||
|
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||||
|
>
|
||||||
|
<h2>Most Recent</h2>
|
||||||
|
{% if recent_room %}
|
||||||
|
<a href="{% url 'billboard:scroll' recent_room.id %}" class="most-recent-room-link">{{ recent_room.name }}</a>
|
||||||
|
{% with events=recent_events %}
|
||||||
|
{% include "core/_partials/_scroll.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
{% else %}
|
||||||
|
<p><small>No recent activity.</small></p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<section
|
||||||
|
id="id_applet_billboard_my_contacts"
|
||||||
|
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||||
|
>
|
||||||
|
<h2>Contacts</h2>
|
||||||
|
<p><small>Coming soon.</small></p>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<section
|
||||||
|
id="id_applet_billboard_my_scrolls"
|
||||||
|
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||||
|
>
|
||||||
|
<h2>My Scrolls</h2>
|
||||||
|
<ul class="scroll-list">
|
||||||
|
{% for room in my_rooms %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'billboard:scroll' room.id %}">{{ room.name }}</a>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<li><small>No scrolls yet.</small></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<section id="id_applet_billboard_scroll" class="applet-billboard-scroll">
|
||||||
|
<h2>{{ room.name }}</h2>
|
||||||
|
{% include "core/_partials/_scroll.html" %}
|
||||||
|
</section>
|
||||||
27
src/templates/apps/billboard/_partials/_applets.html
Normal file
27
src/templates/apps/billboard/_partials/_applets.html
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<div id="id_billboard_applets_container">
|
||||||
|
<div id="id_billboard_applet_menu" style="display:none;">
|
||||||
|
<form
|
||||||
|
hx-post="{% url "billboard:toggle_applets" %}"
|
||||||
|
hx-target="#id_billboard_applets_container"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for entry in applets %}
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="applets"
|
||||||
|
value="{{ entry.applet.slug }}"
|
||||||
|
{% if entry.visible %}checked{% endif %}
|
||||||
|
>
|
||||||
|
{{ entry.applet.name }}
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="menu-btns">
|
||||||
|
<button type="submit" class="btn btn-confirm">OK</button>
|
||||||
|
<button type="button" class="btn btn-cancel applet-menu-cancel">NVM</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% include "apps/applets/_partials/_applets.html" %}
|
||||||
|
</div>
|
||||||
@@ -5,15 +5,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="billboard-page">
|
<div class="billboard-page">
|
||||||
<h2>My Scrolls</h2>
|
{% include "apps/applets/_partials/_gear.html" with menu_id="id_billboard_applet_menu" %}
|
||||||
<ul class="game-list">
|
{% include "apps/billboard/_partials/_applets.html" %}
|
||||||
{% for room in my_rooms %}
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'billboard:scroll' room.id %}">{{ room.name }}</a>
|
|
||||||
</li>
|
|
||||||
{% empty %}
|
|
||||||
<li><small>No scrolls yet.</small></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
{% block header_text %}<span>Bill</span>scroll{% endblock header_text %}
|
{% block header_text %}<span>Bill</span>scroll{% endblock header_text %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="billboard-page">
|
<div class="billscroll-page">
|
||||||
<h2>{{ room.name }}</h2>
|
{% include "apps/billboard/_partials/_applet-billboard-scroll.html" %}
|
||||||
{% include "core/_partials/_scroll.html" %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user