brief sprint C2: introduce billboard.Brief notification model + view_post marks-read on GET — TDD
Brief is the slide-down-banner record that connects an event (a Line freshly appended to a Post) to a user who needs to see it. It's the C3 attachment point for note-unlock + share-invite + future event sources; the banner JS (C3) reads the Brief shape to render kind-specific affordances. C2 lays the schema + the FYI-read contract; C3 hooks the senders. Schema (billboard.Brief): - owner FK→lyric.User (related_name='briefs') — required; whose attention this is for - post FK→billboard.Post (related_name='briefs') — required; where FYI navigates - line FK→billboard.Line (related_name='briefs', null=True) — the appended Line that triggered the Brief; nullable for share-invite-style flows where the Line write races behind the Brief - is_unread BooleanField default=True — flips on view_post GET - kind CharField (note_unlock | user_post | share_invite, default=user_post) — drives banner-side affordances - title CharField (blank=True) — banner display title - created_at DateTimeField (default=timezone.now) — Meta.ordering='-created_at' view_post (the post-detail GET) now bulk-updates is_unread=False on every Brief where owner == request.user AND post == our_post AND is_unread=True. POST (the compose-a-new-Line path) intentionally does NOT mark read — the user is authoring, not reviewing. Tests: BriefModelTest (7) covers defaults, kind choices include all three values, line nullability, owner+post requiredness, title field, __str__ shape. ViewPostMarksReadTest (5) covers the GET flips owner's unread Brief to read; doesn't flip other users' Briefs on the same post; doesn't flip Briefs on unrelated posts; idempotent for already-read; POST request does NOT mark read. Auto migration billboard/0002_brief creates the table. 801-test IT regression green (789 + 12 new). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class Post(models.Model):
|
||||
@@ -38,3 +39,64 @@ class Line(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return self.text
|
||||
|
||||
|
||||
class Brief(models.Model):
|
||||
"""A slide-down notification record. Owner = whose attention; post = where
|
||||
FYI navigates (and where mark-read happens on GET); line = the specific
|
||||
appended Line that triggered it (so the banner can surface its text).
|
||||
|
||||
`kind` discriminates the affordances the banner renders. NOTE_UNLOCK
|
||||
Briefs get a clickable square that jumps direct to my_notes.html;
|
||||
SHARE_INVITE Briefs render the invitation copy; USER_POST is the legacy
|
||||
user-authored compose flow.
|
||||
|
||||
Magic-link confirmation + invalid-link banners use the same Gaussian-glass
|
||||
visual styling but ride no Brief row (transient one-shot).
|
||||
"""
|
||||
KIND_NOTE_UNLOCK = "note_unlock"
|
||||
KIND_USER_POST = "user_post"
|
||||
KIND_SHARE_INVITE = "share_invite"
|
||||
KIND_CHOICES = [
|
||||
(KIND_NOTE_UNLOCK, "Note unlock"),
|
||||
(KIND_USER_POST, "User post"),
|
||||
(KIND_SHARE_INVITE, "Share invite"),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
owner = models.ForeignKey(
|
||||
"lyric.User",
|
||||
related_name="briefs",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
post = models.ForeignKey(
|
||||
Post,
|
||||
related_name="briefs",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
# Line is nullable because a share_invite-style Brief can race ahead of its
|
||||
# async-appended Line write; the post FK alone is enough to navigate.
|
||||
line = models.ForeignKey(
|
||||
Line,
|
||||
related_name="briefs",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
is_unread = models.BooleanField(default=True)
|
||||
kind = models.CharField(
|
||||
max_length=32,
|
||||
choices=KIND_CHOICES,
|
||||
default=KIND_USER_POST,
|
||||
)
|
||||
title = models.CharField(max_length=255, blank=True)
|
||||
created_at = models.DateTimeField(default=timezone.now)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"Brief({self.kind}, {self.owner.email}, "
|
||||
f"unread={self.is_unread})"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user