applet feed unification — My Buds + My Notes drop the [Feature forthcoming] / empty placeholders for live top-3 feeds, mirroring the long-standing My Posts pattern; all five in-grid list applets (My Posts / My Buds / My Notes / My Scrolls / My Games) now route their <ul> through a single shared partial _applet-grid-list.html (newly extracted) so item rendering + empty-state row + scroll-buffer all live in one place — _applet-list-shell.html (the dedicated billbuds/billposts page shell) now internally includes the same grid-list partial for its inner <ul>, so the dedicated-page and in-grid lists share the same skeleton; new per-applet item partials _my_buds_applet_item.html (mirrors _my_buds_item.html w. data-bud-id + display_name), _my_notes_item.html (links to billboard:my_notes; uses display_name), _my_posts_applet_item.html (Post link + title), _my_scrolls_item.html (Room link to billboard:scroll), _my_games_item.html (Room link to epic:gatekeeper); view-side _billboard_context gains _recent_buds(user) — sorts the User.buds auto-through table by -id so newest-added-first w.o. an explicit through model w. timestamps (manage [r.to_user for r in rows]) — + _recent_notes(user) (user.notes.order_by('-earned_at')[:limit]); same two helpers threaded into new_post's GET-with-form-errors branch (line 270-274) so the rerender keeps the new applet content visible; 7 ITs added to BillboardViewTest covering recent_buds ordering / cap / empty + recent_notes ordering / cap / cross-user isolation / empty; SCSS — .applet-list / .applet-list-entry / .applet-list-buffer lifted from .applet-list-page .applet-scroll scope to top level so they apply in both surfaces; in-grid applets get display: flex; flex-direction: column; .applet-list { flex: 1 } so the list scrolls within the applet box; #id_applet_my_games ul-centring + .scroll-list + #id_applet_notes h2 { writing-mode: vertical-rl ... } overrides removed (centring was an empty-state-only behaviour, scroll-list + vertical-rl redundant w. the new shared rule + the %applet-box > h2 rule); My Games items now left-aligned by default; empty-state row recovers the centred-italic-dim treatment via .applet-list-entry--empty { flex: 1; display: flex; align-items: center; justify-content: center; opacity: 0.6; font-style: italic } + .applet-list:has(> .applet-list-entry--empty) { display: flex; flex-direction: column } — so "No buds yet" / "No notes yet" / "No games yet" / "No scrolls yet" / "No posts yet" all centre in their applet aperture, reverting to the left-aligned stack the moment a real item lands; Most Recent Scroll's outer empty <p><small>No recent activity.</small></p> adopts the same .applet-list-entry .applet-list-entry--empty classes (section is already flex-column from existing rule) so it picks up the unified centred-italic-dim treatment
pipeline fix — `_post_gear.html` (commit 6a7464e) gated the NVM target on `{% url 'billboard:my_posts' user_id=request.user.id %}`, which exploded w. NoReverseMatch when an anonymous user (Percival ch.18 anonymous-post lab — ownerless `Post.objects.create()`) hit view_post (which has no @login_required); whole gear-include now wrapped in `{% if request.user.is_authenticated %}` since anonymous viewers can't DEL/BYE/back-to-my-posts anyway; AnonymousPostViewerTest pins the 200-render + gear-absence contract so future ownerless-post regressions surface in ITs (pipeline run #298 fixed) — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,73 @@ class BillboardViewTest(TestCase):
|
||||
self.assertIsNone(response.context["recent_room"])
|
||||
self.assertEqual(list(response.context["recent_events"]), [])
|
||||
|
||||
# ── recent_buds + recent_notes (applet feed) ──────────────────────
|
||||
# Mirrors the recent_posts pattern: each in-grid applet now lists
|
||||
# its own most-recent items (capped at 3) plus an empty-state row.
|
||||
|
||||
def test_passes_recent_buds_context(self):
|
||||
first = User.objects.create(email="first-bud@test.io")
|
||||
second = User.objects.create(email="second-bud@test.io")
|
||||
self.user.buds.add(first)
|
||||
self.user.buds.add(second)
|
||||
response = self.client.get("/billboard/")
|
||||
# Most-recently-added bud is first
|
||||
self.assertEqual(list(response.context["recent_buds"]), [second, first])
|
||||
|
||||
def test_recent_buds_capped_at_3(self):
|
||||
added = [
|
||||
User.objects.create(email=f"bud{i}@test.io") for i in range(5)
|
||||
]
|
||||
for u in added:
|
||||
self.user.buds.add(u)
|
||||
response = self.client.get("/billboard/")
|
||||
recent = list(response.context["recent_buds"])
|
||||
self.assertEqual(len(recent), 3)
|
||||
# Newest three in newest-first order
|
||||
self.assertEqual(recent, list(reversed(added))[:3])
|
||||
|
||||
def test_recent_buds_empty_when_no_buds(self):
|
||||
response = self.client.get("/billboard/")
|
||||
self.assertEqual(list(response.context["recent_buds"]), [])
|
||||
|
||||
def test_passes_recent_notes_context(self):
|
||||
Note.objects.create(
|
||||
user=self.user, slug="stargazer",
|
||||
earned_at=timezone.now() - timezone.timedelta(days=2),
|
||||
)
|
||||
Note.objects.create(
|
||||
user=self.user, slug="super-schizo",
|
||||
earned_at=timezone.now(),
|
||||
)
|
||||
response = self.client.get("/billboard/")
|
||||
slugs = [n.slug for n in response.context["recent_notes"]]
|
||||
# Most-recently-earned first
|
||||
self.assertEqual(slugs, ["super-schizo", "stargazer"])
|
||||
|
||||
def test_recent_notes_capped_at_3(self):
|
||||
slugs = ["stargazer", "super-schizo", "super-nomad", "ladidah", "doodah"]
|
||||
base = timezone.now()
|
||||
for i, slug in enumerate(slugs):
|
||||
Note.objects.create(
|
||||
user=self.user, slug=slug,
|
||||
earned_at=base - timezone.timedelta(hours=i),
|
||||
)
|
||||
response = self.client.get("/billboard/")
|
||||
names = [n.slug for n in response.context["recent_notes"]]
|
||||
self.assertEqual(len(names), 3)
|
||||
# Newest-first; oldest two trimmed
|
||||
self.assertEqual(names, slugs[:3])
|
||||
|
||||
def test_recent_notes_excludes_other_users(self):
|
||||
other = User.objects.create(email="other-note@test.io")
|
||||
Note.objects.create(user=other, slug="stargazer", earned_at=timezone.now())
|
||||
response = self.client.get("/billboard/")
|
||||
self.assertEqual(list(response.context["recent_notes"]), [])
|
||||
|
||||
def test_recent_notes_empty_when_no_notes(self):
|
||||
response = self.client.get("/billboard/")
|
||||
self.assertEqual(list(response.context["recent_notes"]), [])
|
||||
|
||||
|
||||
class SaveScrollPositionViewTest(TestCase):
|
||||
def setUp(self):
|
||||
@@ -602,3 +669,31 @@ class AbandonPostViewTest(TestCase):
|
||||
self.client.post(reverse("billboard:abandon_post", args=[admin_post.id]))
|
||||
admin_post.refresh_from_db()
|
||||
self.assertIn(self.invitee, admin_post.shared_with.all())
|
||||
|
||||
|
||||
class AnonymousPostViewerTest(TestCase):
|
||||
"""view_post has no @login_required (Percival ch.18 anonymous-post lab —
|
||||
ownerless Posts are viewable by anyone). The gear menu's NVM target
|
||||
reverses `my_posts user_id=request.user.id`, which previously exploded
|
||||
w. NoReverseMatch when request.user.id was None (anonymous user).
|
||||
Gear menu must be gated on is_authenticated; the post body still renders
|
||||
for anonymous viewers of ownerless posts."""
|
||||
|
||||
def setUp(self):
|
||||
from apps.billboard.models import Post
|
||||
# Ownerless Post — matches the Percival anonymous-share contract
|
||||
# exercised by apps.dashboard.tests.integrated.test_views.SharePostTest.
|
||||
# No Line needed; the empty post still exercises the template render.
|
||||
self.post = Post.objects.create(title="Public-ish")
|
||||
|
||||
def test_anonymous_can_view_ownerless_post_without_500(self):
|
||||
response = self.client.get(
|
||||
reverse("billboard:view_post", args=[self.post.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_anonymous_gets_no_gear_menu(self):
|
||||
response = self.client.get(
|
||||
reverse("billboard:view_post", args=[self.post.id])
|
||||
)
|
||||
self.assertNotIn(b"id_post_menu", response.content)
|
||||
|
||||
@@ -31,6 +31,23 @@ def _recent_posts(user, limit=3):
|
||||
)
|
||||
|
||||
|
||||
def _recent_buds(user, limit=3):
|
||||
"""Most-recently-added buds, newest first. M2M has no explicit through
|
||||
model, so we sort the auto-through table by its monotonic `id`."""
|
||||
through = User.buds.through # type: ignore[attr-defined]
|
||||
rows = (
|
||||
through.objects
|
||||
.filter(from_user=user)
|
||||
.select_related("to_user")
|
||||
.order_by("-id")[:limit]
|
||||
)
|
||||
return [r.to_user for r in rows]
|
||||
|
||||
|
||||
def _recent_notes(user, limit=3):
|
||||
return user.notes.order_by("-earned_at")[:limit]
|
||||
|
||||
|
||||
def _billboard_context(user):
|
||||
my_rooms = rooms_for_user(user).order_by("-created_at")
|
||||
|
||||
@@ -73,6 +90,8 @@ def _billboard_context(user):
|
||||
"applets": applet_context(user, "billboard"),
|
||||
"form": LineForm(),
|
||||
"recent_posts": _recent_posts(user),
|
||||
"recent_buds": _recent_buds(user),
|
||||
"recent_notes": _recent_notes(user),
|
||||
}
|
||||
|
||||
|
||||
@@ -251,6 +270,8 @@ def new_post(request):
|
||||
if request.user.is_authenticated:
|
||||
context["applets"] = applet_context(request.user, "billboard")
|
||||
context["recent_posts"] = _recent_posts(request.user)
|
||||
context["recent_buds"] = _recent_buds(request.user)
|
||||
context["recent_notes"] = _recent_notes(request.user)
|
||||
return render(request, "apps/billboard/billboard.html", context)
|
||||
|
||||
|
||||
|
||||
@@ -1,25 +1,80 @@
|
||||
// ── My Posts applet ────────────────────────────────────────────────────────
|
||||
// ── Shared scrollable applet list (.applet-list) ───────────────────────────
|
||||
// In-grid applet partials (My Posts, My Buds, My Notes, My Scrolls, My Games)
|
||||
// + the dedicated applet-list pages (billbuds, billposts) all wrap items in a
|
||||
// `<ul class="applet-list">`. The list rules live at top level so the same
|
||||
// item-entry styling applies in both surfaces; per-context wrappers below
|
||||
// handle flex sizing so the list scrolls inside its parent's aperture.
|
||||
|
||||
#id_applet_my_posts {
|
||||
.applet-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 0.75rem 0 0;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
|
||||
// Flex-column lets the empty-state entry fill the aperture so it can
|
||||
// centre vertically. Fires only when the list is entirely empty —
|
||||
// as soon as a real .applet-list-entry lands, layout reverts to the
|
||||
// default left-aligned vertical stack.
|
||||
&:has(> .applet-list-entry--empty) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
// Empty-state filler (`No <X> yet.` rows). Centres in any flex-column
|
||||
// parent — the .applet-list above OR a `display: flex; flex-direction:
|
||||
// column` applet section directly (e.g. #id_applet_most_recent_scroll
|
||||
// when no Room has events yet).
|
||||
.applet-list-entry--empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
opacity: 0.6;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.applet-list-entry {
|
||||
padding: 0.4rem 0;
|
||||
|
||||
.bud-name { font-weight: bold; opacity: 0.85; }
|
||||
|
||||
a {
|
||||
color: rgba(var(--terUser), 1);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
transition: text-shadow 0.15s ease;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
color: rgba(var(--ninUser), 1);
|
||||
text-shadow: 0 0 0.55rem rgba(var(--terUser), 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.applet-list-buffer {
|
||||
flex-shrink: 0;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
// In-grid applet sections: flex-column so the .applet-list can flex:1
|
||||
// and scroll within the applet box. Left-aligned items across the
|
||||
// board (My Games used to centre — symmetrised w. the rest 2026-05-12).
|
||||
|
||||
#id_applet_my_posts,
|
||||
#id_applet_my_buds,
|
||||
#id_applet_my_scrolls,
|
||||
#id_applet_notes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.my-posts-container {
|
||||
.applet-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
mask-origin: padding-box;
|
||||
mask-clip: padding-box;
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
black 5%,
|
||||
black 85%,
|
||||
transparent 100%
|
||||
);
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,40 +290,9 @@ body.page-billposts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.applet-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 0.75rem 0 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.applet-list-entry {
|
||||
padding: 0.4rem 0;
|
||||
|
||||
.bud-name { font-weight: bold; opacity: 0.85; }
|
||||
|
||||
&--empty { opacity: 0.6; font-style: italic; }
|
||||
|
||||
a {
|
||||
color: rgba(var(--terUser), 1);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
transition: text-shadow 0.15s ease;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
color: rgba(var(--ninUser), 1);
|
||||
text-shadow: 0 0 0.55rem rgba(var(--terUser), 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.applet-list-buffer {
|
||||
flex-shrink: 0;
|
||||
height: 0.5rem;
|
||||
}
|
||||
// .applet-list / .applet-list-entry / .applet-list-buffer rules
|
||||
// live at top level (above) so they apply to in-grid applets too.
|
||||
.applet-list { flex: 1; }
|
||||
}
|
||||
|
||||
// Side-by-side in landscape; stacked in portrait (default).
|
||||
@@ -302,20 +326,6 @@ body.page-billposts {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Notes applet — vertical title ─────────────────────────────────────────
|
||||
|
||||
#id_applet_notes {
|
||||
h2 {
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Most Recent Scroll applet — scrollable drama feed ─────────────────────
|
||||
|
||||
#id_applet_most_recent_scroll {
|
||||
@@ -365,22 +375,5 @@ body.page-billposts {
|
||||
}
|
||||
}
|
||||
|
||||
// ── My Scrolls list ────────────────────────────────────────────────────────
|
||||
|
||||
#id_applet_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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
// My Scrolls now rides the shared `.applet-list` rule above (lifted out of
|
||||
// `.applet-list-page .applet-scroll`). Old `.scroll-list` styling removed.
|
||||
|
||||
@@ -83,33 +83,18 @@ body.page-gameboard {
|
||||
}
|
||||
}
|
||||
|
||||
#id_applet_new_game,
|
||||
#id_applet_new_game {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#id_applet_my_games {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
ul {
|
||||
.applet-list {
|
||||
flex: 1;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
small {
|
||||
opacity: 0.5;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#id_applet_my_games {
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
17
src/templates/apps/applets/_partials/_applet-grid-list.html
Normal file
17
src/templates/apps/applets/_partials/_applet-grid-list.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{# Scrollable <ul> shared by in-grid applet partials (My Buds / My Notes / #}
|
||||
{# My Posts / My Scrolls / My Games) AND by the dedicated billposts / #}
|
||||
{# billbuds pages that wrap it in _applet-list-shell.html. #}
|
||||
{# #}
|
||||
{# Parameters: #}
|
||||
{# list_items — iterable rendered into the list #}
|
||||
{# list_item_template — partial rendering each <li>; receives `item` #}
|
||||
{# list_empty — fallback string for the empty-state row #}
|
||||
{# list_id — optional `id=` for the <ul> (e.g. "id_buds_list") #}
|
||||
<ul {% if list_id %}id="{{ list_id }}"{% endif %} class="applet-list">
|
||||
{% for item in list_items %}
|
||||
{% include list_item_template %}
|
||||
{% empty %}
|
||||
<li class="applet-list-entry applet-list-entry--empty">{{ list_empty|default:"Nothing here yet." }}</li>
|
||||
{% endfor %}
|
||||
<li class="applet-list-buffer" aria-hidden="true"></li>
|
||||
</ul>
|
||||
@@ -1,7 +1,9 @@
|
||||
{# Shared applet-scroll-style list section — vertical-title <h2> on the #}
|
||||
{# left + scrollable <ul> aperture. Inclusion shell (NOT a base template) #}
|
||||
{# so a single page can invoke it more than once (e.g. my_posts.html #}
|
||||
{# stacks "My Posts" + "Posts shared with me"). #}
|
||||
{# stacks "My Posts" + "Posts shared with me"). The inner <ul> is in the #}
|
||||
{# shared `_applet-grid-list.html` partial so the in-grid applet #}
|
||||
{# partials can reuse it without the .applet-scroll <section> wrapper. #}
|
||||
{# #}
|
||||
{# Parameters: #}
|
||||
{# shell_title — vertical-rotated heading text (string) #}
|
||||
@@ -12,12 +14,5 @@
|
||||
{# shell_empty — text for the {% empty %} fallback row #}
|
||||
<section class="applet-scroll">
|
||||
<h2>{{ shell_title }}</h2>
|
||||
<ul {% if shell_list_id %}id="{{ shell_list_id }}"{% endif %} class="applet-list">
|
||||
{% for item in shell_items %}
|
||||
{% include shell_item_template %}
|
||||
{% empty %}
|
||||
<li class="applet-list-entry applet-list-entry--empty">{{ shell_empty|default:"Nothing here yet." }}</li>
|
||||
{% endfor %}
|
||||
<li class="applet-list-buffer" aria-hidden="true"></li>
|
||||
</ul>
|
||||
{% include "apps/applets/_partials/_applet-grid-list.html" with list_items=shell_items list_item_template=shell_item_template list_empty=shell_empty list_id=shell_list_id %}
|
||||
</section>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% else %}
|
||||
<p><small>No recent activity.</small></p>
|
||||
<p class="applet-list-entry applet-list-entry--empty">No recent activity.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
<script>
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<h2><a href="{% url 'billboard:my_buds' %}" class="my-buds-main">My Buds</a></h2>
|
||||
{% include "core/_partials/_forthcoming.html" %}
|
||||
{% include "apps/applets/_partials/_applet-grid-list.html" with list_items=recent_buds list_item_template="apps/billboard/_partials/_my_buds_applet_item.html" list_empty="No buds yet." %}
|
||||
</section>
|
||||
|
||||
@@ -3,15 +3,5 @@
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<h2><a href="{% url 'billboard:my_posts' user.id %}" class="my-posts-main">My Posts</a></h2>
|
||||
<div class="my-posts-container">
|
||||
<ul>
|
||||
{% for post in recent_posts %}
|
||||
<li>
|
||||
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li>No posts yet.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% include "apps/applets/_partials/_applet-grid-list.html" with list_items=recent_posts list_item_template="apps/billboard/_partials/_my_posts_applet_item.html" list_empty="No posts yet." %}
|
||||
</section>
|
||||
|
||||
@@ -3,13 +3,5 @@
|
||||
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>
|
||||
{% include "apps/applets/_partials/_applet-grid-list.html" with list_items=my_rooms list_item_template="apps/billboard/_partials/_my_scrolls_item.html" list_empty="No scrolls yet." %}
|
||||
</section>
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
id="id_applet_notes"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<h2><a href="/billboard/my-notes/">My Notes</a></h2>
|
||||
<h2><a href="{% url 'billboard:my_notes' %}">My Notes</a></h2>
|
||||
{% include "apps/applets/_partials/_applet-grid-list.html" with list_items=recent_notes list_item_template="apps/billboard/_partials/_my_notes_item.html" list_empty="No notes yet." %}
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{% load lyric_extras %}
|
||||
{# Recent-bud row for the My Buds in-grid applet. `item` is a User. #}
|
||||
{# Mirrors the dedicated billbuds page item: just a name chip, no link #}
|
||||
{# (per-bud surface doesn't exist yet — coming in the 3-col sprint). #}
|
||||
<li class="applet-list-entry bud-entry" data-bud-id="{{ item.id }}">
|
||||
<span class="bud-name">{{ item|display_name }}</span>
|
||||
</li>
|
||||
@@ -0,0 +1,4 @@
|
||||
{# Recent-note row for the My Notes in-grid applet. `item` is a Note. #}
|
||||
<li class="applet-list-entry">
|
||||
<a href="{% url 'billboard:my_notes' %}">{{ item.display_name }}</a>
|
||||
</li>
|
||||
@@ -0,0 +1,4 @@
|
||||
{# Recent-posts row for the My Posts in-grid applet. `item` is a Post. #}
|
||||
<li class="applet-list-entry">
|
||||
<a href="{{ item.get_absolute_url }}">{{ item.title }}</a>
|
||||
</li>
|
||||
@@ -0,0 +1,4 @@
|
||||
{# Recent-scroll row for the My Scrolls in-grid applet. `item` is a Room. #}
|
||||
<li class="applet-list-entry">
|
||||
<a href="{% url 'billboard:scroll' item.id %}">{{ item.name }}</a>
|
||||
</li>
|
||||
@@ -3,6 +3,9 @@
|
||||
{# User-Posts add DEL (owner) or BYE (invitee). #}
|
||||
{# Admin-Posts (kind=NOTE_UNLOCK) intentionally have NVM only — DEL + BYE #}
|
||||
{# don't apply to system-authored threads. #}
|
||||
{# Anonymous viewers (Percival ch.18 anonymous-post lab) get no gear at all #}
|
||||
{# — they can't DEL/BYE and the my_posts NVM target needs request.user.id. #}
|
||||
{% if request.user.is_authenticated %}
|
||||
<div id="id_post_menu" style="display:none">
|
||||
<a href="{% url 'billboard:my_posts' user_id=request.user.id %}" class="btn btn-cancel">NVM</a>
|
||||
{% if post.kind != 'note_unlock' %}
|
||||
@@ -20,3 +23,4 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include "apps/applets/_partials/_gear.html" with menu_id="id_post_menu" %}
|
||||
{% endif %}
|
||||
|
||||
@@ -3,11 +3,5 @@
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<h2>My Games</h2>
|
||||
<ul class="game-list">
|
||||
{% for room in my_games %}
|
||||
<li><a href="{% url 'epic:gatekeeper' room.id %}">{{ room.name }}</a></li>
|
||||
{% empty %}
|
||||
<li class="game-list-item"><small>No games yet</small></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% include "apps/applets/_partials/_applet-grid-list.html" with list_items=my_games list_item_template="apps/gameboard/_partials/_my_games_item.html" list_empty="No games yet." %}
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{# Recent-game row for the My Games in-grid applet. `item` is a Room. #}
|
||||
<li class="applet-list-entry">
|
||||
<a href="{% url 'epic:gatekeeper' item.id %}">{{ item.name }}</a>
|
||||
</li>
|
||||
Reference in New Issue
Block a user