rename: Note→Post/Line (dashboard); Recognition→Note (drama); new-post/my-posts to billboard

- dashboard: Note→Post, Item→Line across models, forms, views, API, urls & tests
- new-post (9×3) & my-posts (3×3) applets migrate from dashboard→billboard context; billboard view passes form & recent_posts
- drama: Recognition→Note, related_name notes; billboard URL /recognition/→/my-notes/, set-palette at /note/<slug>/set-palette
- recognition.js→note.js (module Note, data.note key); recognition-page.js→note-page.js; .recog-*→.note-*
- _recognition.scss→_note.scss; BillNotes page header; applet slug billboard-recognition→billboard-notes (My Notes)
- NoteSpec.js replaces RecognitionSpec.js; test_recognition.py→test_applet_my_notes.py
- 4 migrations applied: dashboard 0004, applets 0011+0012, drama 0005; 683 ITs green

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-22 22:32:34 -04:00
parent 6d9d3d4f54
commit 473e6bc45a
54 changed files with 1373 additions and 1283 deletions

View File

@@ -1,30 +1,30 @@
from rest_framework import serializers
from apps.dashboard.models import Item, Note
from apps.dashboard.models import Line, Post
from apps.lyric.models import User
class ItemSerializer(serializers.ModelSerializer):
class LineSerializer(serializers.ModelSerializer):
text = serializers.CharField()
def validate_text(self, value):
note = self.context["note"]
if note.item_set.filter(text=value).exists():
post = self.context["post"]
if post.lines.filter(text=value).exists():
raise serializers.ValidationError("duplicate")
return value
class Meta:
model = Item
model = Line
fields = ["id", "text"]
class NoteSerializer(serializers.ModelSerializer):
class PostSerializer(serializers.ModelSerializer):
name = serializers.ReadOnlyField()
url = serializers.CharField(source="get_absolute_url", read_only=True)
items = ItemSerializer(many=True, read_only=True, source="item_set")
lines = LineSerializer(many=True, read_only=True)
class Meta:
model = Note
fields = ["id", "name", "url", "items"]
model = Post
fields = ["id", "name", "url", "lines"]
class UserSerializer(serializers.ModelSerializer):
class Meta:

View File

@@ -1,7 +1,7 @@
from django.test import TestCase
from rest_framework.test import APIClient
from apps.dashboard.models import Item, Note
from apps.dashboard.models import Line, Post
from apps.lyric.models import User
class BaseAPITest(TestCase):
@@ -11,76 +11,76 @@ class BaseAPITest(TestCase):
self.user = User.objects.create_user("test@example.com")
self.client.force_authenticate(user=self.user)
class NoteDetailAPITest(BaseAPITest):
def test_returns_note_with_items(self):
note = Note.objects.create(owner=self.user)
Item.objects.create(text="item 1", note=note)
Item.objects.create(text="item 2", note=note)
class PostDetailAPITest(BaseAPITest):
def test_returns_post_with_lines(self):
post = Post.objects.create(owner=self.user)
Line.objects.create(text="line 1", post=post)
Line.objects.create(text="line 2", post=post)
response = self.client.get(f"/api/notes/{note.id}/")
response = self.client.get(f"/api/posts/{post.id}/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["id"], str(note.id))
self.assertEqual(len(response.data["items"]), 2)
self.assertEqual(response.data["id"], str(post.id))
self.assertEqual(len(response.data["lines"]), 2)
class NoteItemsAPITest(BaseAPITest):
def test_can_add_item_to_note(self):
note = Note.objects.create(owner=self.user)
class PostLinesAPITest(BaseAPITest):
def test_can_add_line_to_post(self):
post = Post.objects.create(owner=self.user)
response = self.client.post(
f"/api/notes/{note.id}/items/",
{"text": "a new item"},
f"/api/posts/{post.id}/lines/",
{"text": "a new line"},
)
self.assertEqual(response.status_code, 201)
self.assertEqual(Item.objects.count(), 1)
self.assertEqual(Item.objects.first().text, "a new item")
self.assertEqual(Line.objects.count(), 1)
self.assertEqual(Line.objects.first().text, "a new line")
def test_cannot_add_empty_item_to_note(self):
note = Note.objects.create(owner=self.user)
def test_cannot_add_empty_line_to_post(self):
post = Post.objects.create(owner=self.user)
response = self.client.post(
f"/api/notes/{note.id}/items/",
f"/api/posts/{post.id}/lines/",
{"text": ""},
)
self.assertEqual(response.status_code, 400)
self.assertEqual(Item.objects.count(), 0)
self.assertEqual(Line.objects.count(), 0)
def test_cannot_add_duplicate_item_to_note(self):
note = Note.objects.create(owner=self.user)
Item.objects.create(text="note item", note=note)
def test_cannot_add_duplicate_line_to_post(self):
post = Post.objects.create(owner=self.user)
Line.objects.create(text="post line", post=post)
duplicate_response = self.client.post(
f"/api/notes/{note.id}/items/",
{"text": "note item"},
f"/api/posts/{post.id}/lines/",
{"text": "post line"},
)
self.assertEqual(duplicate_response.status_code, 400)
self.assertEqual(Item.objects.count(), 1)
self.assertEqual(Line.objects.count(), 1)
class NotesAPITest(BaseAPITest):
def test_get_returns_only_users_notes(self):
note1 = Note.objects.create(owner=self.user)
Item.objects.create(text="item 1", note=note1)
class PostsAPITest(BaseAPITest):
def test_get_returns_only_users_posts(self):
post1 = Post.objects.create(owner=self.user)
Line.objects.create(text="line 1", post=post1)
other_user = User.objects.create_user("other@example.com")
Note.objects.create(owner=other_user)
Post.objects.create(owner=other_user)
response = self.client.get("/api/notes/")
response = self.client.get("/api/posts/")
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]["id"], str(note1.id))
self.assertEqual(response.data[0]["id"], str(post1.id))
def test_post_creates_note_with_item(self):
def test_post_creates_post_with_line(self):
response = self.client.post(
"/api/notes/",
{"text": "first item"},
"/api/posts/",
{"text": "first line"},
)
self.assertEqual(response.status_code, 201)
self.assertEqual(Note.objects.count(), 1)
self.assertEqual(Note.objects.first().owner, self.user)
self.assertEqual(Item.objects.first().text, "first item")
self.assertEqual(Post.objects.count(), 1)
self.assertEqual(Post.objects.first().owner, self.user)
self.assertEqual(Line.objects.first().text, "first line")
class UserSearchAPITest(BaseAPITest):
def test_returns_users_matching_username(self):
@@ -98,7 +98,7 @@ class UserSearchAPITest(BaseAPITest):
def test_non_searchable_users_are_excluded(self):
alice = User.objects.create_user("alice@example.com")
alice.username = "princessAli"
alice.save() # searchable defaults to False
alice.save() # searchable defaults to False
response = self.client.get("/api/users/?q=prin")

View File

@@ -1,19 +1,19 @@
from django.test import SimpleTestCase
from apps.api.serializers import ItemSerializer, NoteSerializer
from apps.api.serializers import LineSerializer, PostSerializer
class ItemSerializerTest(SimpleTestCase):
class LineSerializerTest(SimpleTestCase):
def test_fields(self):
serializer = ItemSerializer()
serializer = LineSerializer()
self.assertEqual(
set(serializer.fields.keys()),
{"id", "text"},
)
class NoteSerializerTest(SimpleTestCase):
class PostSerializerTest(SimpleTestCase):
def test_fields(self):
serializer = NoteSerializer()
serializer = PostSerializer()
self.assertEqual(
set(serializer.fields.keys()),
{"id", "name", "url", "items"},
{"id", "name", "url", "lines"},
)

View File

@@ -4,8 +4,8 @@ from . import views
urlpatterns = [
path('notes/', views.NotesAPI.as_view(), name='api_notes'),
path('notes/<uuid:note_id>/', views.NoteDetailAPI.as_view(), name='api_note_detail'),
path('notes/<uuid:note_id>/items/', views.NoteItemsAPI.as_view(), name='api_note_items'),
path('posts/', views.PostsAPI.as_view(), name='api_posts'),
path('posts/<uuid:post_id>/', views.PostDetailAPI.as_view(), name='api_post_detail'),
path('posts/<uuid:post_id>/lines/', views.PostLinesAPI.as_view(), name='api_post_lines'),
path('users/', views.UserSearchAPI.as_view(), name='api_users'),
]

View File

@@ -2,36 +2,36 @@ from django.shortcuts import get_object_or_404
from rest_framework.views import APIView
from rest_framework.response import Response
from apps.api.serializers import ItemSerializer, NoteSerializer, UserSerializer
from apps.dashboard.models import Item, Note
from apps.api.serializers import LineSerializer, PostSerializer, UserSerializer
from apps.dashboard.models import Line, Post
from apps.lyric.models import User
class NoteDetailAPI(APIView):
def get(self, request, note_id):
note = get_object_or_404(Note, id=note_id)
serializer = NoteSerializer(note)
class PostDetailAPI(APIView):
def get(self, request, post_id):
post = get_object_or_404(Post, id=post_id)
serializer = PostSerializer(post)
return Response(serializer.data)
class NoteItemsAPI(APIView):
def post(self, request, note_id):
note = get_object_or_404(Note, id=note_id)
serializer = ItemSerializer(data=request.data, context={"note": note})
class PostLinesAPI(APIView):
def post(self, request, post_id):
post = get_object_or_404(Post, id=post_id)
serializer = LineSerializer(data=request.data, context={"post": post})
if serializer.is_valid():
serializer.save(note=note)
serializer.save(post=post)
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)
class NotesAPI(APIView):
class PostsAPI(APIView):
def get(self, request):
notes = Note.objects.filter(owner=request.user)
serializer = NoteSerializer(notes, many=True)
posts = Post.objects.filter(owner=request.user)
serializer = PostSerializer(posts, many=True)
return Response(serializer.data)
def post(self, request):
note = Note.objects.create(owner=request.user)
item = Item.objects.create(text=request.data.get("text", ""), note=note)
serializer = NoteSerializer(note)
post = Post.objects.create(owner=request.user)
line = Line.objects.create(text=request.data.get("text", ""), post=post)
serializer = PostSerializer(post)
return Response(serializer.data, status=201)
class UserSearchAPI(APIView):