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:
@@ -13,7 +13,7 @@ from apps.lyric.models import User
|
||||
def _seed_billboard_applets():
|
||||
for slug, name, cols, rows in [
|
||||
("my-scrolls", "My Scrolls", 4, 3),
|
||||
("my-contacts", "Contacts", 4, 3),
|
||||
("my-buddies", "My Buddies", 4, 3),
|
||||
("most-recent-scroll", "Most Recent Scroll", 8, 6),
|
||||
]:
|
||||
Applet.objects.get_or_create(
|
||||
@@ -37,7 +37,7 @@ class BillboardViewTest(TestCase):
|
||||
self.assertIn("applets", response.context)
|
||||
slugs = [e["applet"].slug for e in response.context["applets"]]
|
||||
self.assertIn("my-scrolls", slugs)
|
||||
self.assertIn("my-contacts", slugs)
|
||||
self.assertIn("my-buddies", slugs)
|
||||
self.assertIn("most-recent-scroll", slugs)
|
||||
|
||||
def test_passes_my_rooms_context(self):
|
||||
@@ -111,7 +111,7 @@ class ToggleBillboardAppletsTest(TestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
from apps.applets.models import UserApplet
|
||||
contacts = Applet.objects.get(slug="my-contacts")
|
||||
contacts = Applet.objects.get(slug="my-buddies")
|
||||
ua = UserApplet.objects.get(user=self.user, applet=contacts)
|
||||
self.assertFalse(ua.visible)
|
||||
|
||||
@@ -136,7 +136,7 @@ class ToggleBillboardAppletsTest(TestCase):
|
||||
reverse("billboard:toggle_applets"),
|
||||
{"applets": [
|
||||
"my-scrolls",
|
||||
"my-contacts",
|
||||
"my-buddies",
|
||||
"most-recent-scroll",
|
||||
]},
|
||||
HTTP_HX_REQUEST="true",
|
||||
@@ -160,7 +160,7 @@ class ToggleBillboardAppletsTest(TestCase):
|
||||
self.assertEqual(body.count('id="id_billboard_applet_menu"'), 1)
|
||||
|
||||
def test_second_toggle_preserves_prior_hidden_state(self):
|
||||
# First toggle: hide Contacts only.
|
||||
# First toggle: hide My Buddies only.
|
||||
self.client.post(
|
||||
reverse("billboard:toggle_applets"),
|
||||
{"applets": [
|
||||
@@ -170,7 +170,7 @@ class ToggleBillboardAppletsTest(TestCase):
|
||||
]},
|
||||
HTTP_HX_REQUEST="true",
|
||||
)
|
||||
# Second toggle: hide Most Recent Scroll additionally — Contacts must stay hidden.
|
||||
# Second toggle: hide Most Recent Scroll additionally — My Buddies must stay hidden.
|
||||
self.client.post(
|
||||
reverse("billboard:toggle_applets"),
|
||||
{"applets": [
|
||||
@@ -180,7 +180,7 @@ class ToggleBillboardAppletsTest(TestCase):
|
||||
HTTP_HX_REQUEST="true",
|
||||
)
|
||||
from apps.applets.models import UserApplet
|
||||
contacts = Applet.objects.get(slug="my-contacts")
|
||||
contacts = Applet.objects.get(slug="my-buddies")
|
||||
most_recent_scroll = Applet.objects.get(slug="most-recent-scroll")
|
||||
self.assertFalse(
|
||||
UserApplet.objects.get(user=self.user, applet=contacts).visible
|
||||
|
||||
Reference in New Issue
Block a user