buddies sprint phase 1: User.buddies M2M(self,symm=False) + my_buddies aperture page + add_buddy JSON endpoint + buddy btn slide-out — TDD; My Contacts applet renamed → My Buddies (slug + name + partial)
- lyric/0004 adds User.buddies = ManyToManyField('self', symmetrical=False, blank=True, related_name='added_as_buddy'). Asymmetric one-way add: A.buddies.add(B) doesn't reciprocate. Reverse via B.added_as_buddy.all() — load-bearing for the future "buddy changed username" snapshot-accept flow noted in design.
- applets/0006 renames slug my-contacts → my-buddies + name 'Contacts' → 'My Buddies'. Existing migrations 0003/0004 untouched (historical artifacts).
- billboard.views.my_buddies + add_buddy:
• my_buddies: GET /billboard/my-buddies/ → renders the aperture page with request.user.buddies.all().
• add_buddy: POST /billboard/buddies/add → JSON {buddy: {id, username, email}|null}. Privacy: returns null when email isn't a registered User OR is the requester's own; never leaks membership. Idempotent on re-add (M2M dedup).
- templates:
• _applet-my-contacts.html → _applet-my-buddies.html (heading + link to /billboard/my-buddies/).
• my_buddies.html — bottom-anchored aperture list of buddies w. {% empty %} fallback "No buddies yet."
• _buddy_add_panel.html — bottom-left handshake btn + slide-out, mirrors _buddy_panel.html (post share) but POSTs to add_buddy and appends to #id_buddies_list. Skips append if data-buddy-id already in DOM (race-safe). Drops the .buddy-entry--empty row on first add.
- SCSS: page-billbuddies joins the body-class aperture trio; .buddies-page extends %billboard-page-base + flex-column + bottom-anchor for #id_buddies_list. id_applet_my_contacts → id_applet_my_buddies (test references + grid placement).
- tests: new test_buddies.py — 14 ITs covering UserBuddiesM2MTest (asymmetric, idempotent), MyBuddiesViewTest (lists own buddies only, anon redirect), AddBuddyViewTest (registered/unregistered/self/idempotent/email-fallback/405). Existing test_views/test_billboard/test_game_kit references swapped to my-buddies. New test_my_buddies.py FT — 4 tests: pre-existing buddies render, empty state, add via panel appends entry w. username, unregistered silent no-op.
- 841 ITs (+14) + 4 my_buddies FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,13 +35,15 @@
|
||||
|
||||
html:has(body.page-billboard),
|
||||
html:has(body.page-billscroll),
|
||||
html:has(body.page-billpost) {
|
||||
html:has(body.page-billpost),
|
||||
html:has(body.page-billbuddies) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.page-billboard,
|
||||
body.page-billscroll,
|
||||
body.page-billpost {
|
||||
body.page-billpost,
|
||||
body.page-billbuddies {
|
||||
overflow: hidden;
|
||||
|
||||
.container {
|
||||
@@ -215,6 +217,58 @@ body.page-billpost {
|
||||
}
|
||||
}
|
||||
|
||||
// ── My Buddies page (aperture list + add-buddy panel) ────────────────────
|
||||
// Mirrors .post-page's flex-column / overflow / bottom-anchor pattern;
|
||||
// the add-buddy panel is included from the same _buddy*.scss styling.
|
||||
|
||||
.buddies-page {
|
||||
@extend %billboard-page-base;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.75rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
.buddies-header {
|
||||
flex-shrink: 0;
|
||||
|
||||
.buddies-title {
|
||||
margin: 0 0 0.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
#id_buddies_list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 0.75rem 0 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
|
||||
.buddy-entry {
|
||||
padding: 0.4rem 0;
|
||||
|
||||
.buddy-name {
|
||||
font-weight: bold;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&--empty {
|
||||
opacity: 0.6;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.buddy-entry-buffer {
|
||||
flex-shrink: 0;
|
||||
height: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Billboard applet placement ─────────────────────────────────────────────
|
||||
// Left column (4-wide): My Scrolls → Contacts → Notes stacked.
|
||||
// Right column (8-wide): Most Recent Scroll spans full height.
|
||||
@@ -222,13 +276,13 @@ body.page-billpost {
|
||||
|
||||
#id_billboard_applets_container {
|
||||
#id_applet_my_scrolls { grid-column: 1 / span 4; grid-row: 1 / span 3; }
|
||||
#id_applet_my_contacts { grid-column: 1 / span 4; grid-row: 4 / span 3; }
|
||||
#id_applet_my_buddies { grid-column: 1 / span 4; grid-row: 4 / span 3; }
|
||||
#id_applet_notes { grid-column: 1 / span 4; grid-row: 7 / span 4; }
|
||||
#id_applet_most_recent_scroll { grid-column: 5 / span 8; grid-row: 1 / span 10; }
|
||||
|
||||
@container (max-width: 550px) {
|
||||
#id_applet_my_scrolls,
|
||||
#id_applet_my_contacts,
|
||||
#id_applet_my_buddies,
|
||||
#id_applet_notes,
|
||||
#id_applet_most_recent_scroll {
|
||||
grid-column: 1 / span 12;
|
||||
|
||||
Reference in New Issue
Block a user