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:
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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"},
|
||||
)
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def rename_note_applets_to_post(apps, schema_editor):
|
||||
Applet = apps.get_model('applets', 'Applet')
|
||||
Applet.objects.filter(slug='new-note').update(slug='new-post', name='New Post', context='billboard')
|
||||
Applet.objects.filter(slug='my-notes').update(slug='my-posts', name='My Posts', context='billboard')
|
||||
|
||||
|
||||
def reverse_rename_note_applets_to_post(apps, schema_editor):
|
||||
Applet = apps.get_model('applets', 'Applet')
|
||||
Applet.objects.filter(slug='new-post').update(slug='new-note', name='New Note', context='dashboard')
|
||||
Applet.objects.filter(slug='my-posts').update(slug='my-notes', name='My Notes', context='dashboard')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applets', '0010_recognition_applet'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(rename_note_applets_to_post, reverse_rename_note_applets_to_post),
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def rename_recognition_applet_to_notes(apps, schema_editor):
|
||||
Applet = apps.get_model('applets', 'Applet')
|
||||
Applet.objects.filter(slug='billboard-recognition').update(
|
||||
slug='billboard-notes',
|
||||
name='My Notes',
|
||||
)
|
||||
|
||||
|
||||
def reverse_rename_recognition_applet_to_notes(apps, schema_editor):
|
||||
Applet = apps.get_model('applets', 'Applet')
|
||||
Applet.objects.filter(slug='billboard-notes').update(
|
||||
slug='billboard-recognition',
|
||||
name='Recognition',
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applets', '0011_rename_note_applets_to_post'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
rename_recognition_applet_to_notes,
|
||||
reverse_rename_recognition_applet_to_notes,
|
||||
),
|
||||
]
|
||||
@@ -8,7 +8,7 @@
|
||||
// ── helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
function _activeModal() {
|
||||
return _activeItem && _activeItem.querySelector('.recog-palette-modal');
|
||||
return _activeItem && _activeItem.querySelector('.note-palette-modal');
|
||||
}
|
||||
|
||||
function _paletteClass(el) {
|
||||
@@ -26,14 +26,14 @@
|
||||
// Clone from <template> if the modal is not in the DOM yet.
|
||||
var existing = _activeModal();
|
||||
if (!existing) {
|
||||
var tpl = _activeItem.querySelector('.recog-palette-modal-tpl');
|
||||
var tpl = _activeItem.querySelector('.note-palette-modal-tpl');
|
||||
if (!tpl) return;
|
||||
var clone = tpl.content.firstElementChild.cloneNode(true);
|
||||
_activeItem.appendChild(clone);
|
||||
_wireModal();
|
||||
}
|
||||
_state = 'open';
|
||||
var confirmEl = _activeModal().querySelector('.recog-palette-confirm');
|
||||
var confirmEl = _activeModal().querySelector('.note-palette-confirm');
|
||||
if (confirmEl) confirmEl.hidden = true;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
if (!modal) return;
|
||||
|
||||
// Swatch body → preview (remove modal from DOM)
|
||||
modal.querySelectorAll('.recog-swatch-body').forEach(function (body) {
|
||||
modal.querySelectorAll('.note-swatch-body').forEach(function (body) {
|
||||
body.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
_selectedPalette = _paletteClass(body.parentElement);
|
||||
@@ -60,17 +60,17 @@
|
||||
|
||||
// OK button (not inside confirm submenu) → show confirm submenu
|
||||
modal.querySelectorAll('.btn.btn-confirm').forEach(function (btn) {
|
||||
if (btn.closest('.recog-palette-confirm')) return;
|
||||
if (btn.closest('.note-palette-confirm')) return;
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
_selectedPalette = _paletteClass(btn.parentElement);
|
||||
var confirmEl = modal.querySelector('.recog-palette-confirm');
|
||||
var confirmEl = modal.querySelector('.note-palette-confirm');
|
||||
if (confirmEl) confirmEl.hidden = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Confirm YES → POST and update DOM
|
||||
modal.querySelectorAll('.recog-palette-confirm .btn.btn-confirm').forEach(function (btn) {
|
||||
modal.querySelectorAll('.note-palette-confirm .btn.btn-confirm').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
_doSetPalette();
|
||||
@@ -78,10 +78,10 @@
|
||||
});
|
||||
|
||||
// Confirm NVM → hide confirm submenu
|
||||
modal.querySelectorAll('.recog-palette-confirm .btn.btn-cancel').forEach(function (btn) {
|
||||
modal.querySelectorAll('.note-palette-confirm .btn.btn-cancel').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
btn.closest('.recog-palette-confirm').hidden = true;
|
||||
btn.closest('.note-palette-confirm').hidden = true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,10 +106,10 @@
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function () {
|
||||
_closeModal();
|
||||
var imageBox = _activeItem.querySelector('.recog-item__image-box');
|
||||
var imageBox = _activeItem.querySelector('.note-item__image-box');
|
||||
if (imageBox) {
|
||||
var swatch = document.createElement('div');
|
||||
swatch.className = 'recog-item__palette ' + palette;
|
||||
swatch.className = 'note-item__palette ' + palette;
|
||||
imageBox.parentNode.replaceChild(swatch, imageBox);
|
||||
}
|
||||
});
|
||||
@@ -119,10 +119,10 @@
|
||||
|
||||
function _init() {
|
||||
// Image-box click → open modal
|
||||
document.querySelectorAll('.recog-item__image-box').forEach(function (box) {
|
||||
document.querySelectorAll('.note-item__image-box').forEach(function (box) {
|
||||
box.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
_activeItem = box.closest('.recog-item');
|
||||
_activeItem = box.closest('.note-item');
|
||||
_openModal();
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.applets.models import Applet
|
||||
from apps.drama.models import GameEvent, Recognition, ScrollPosition, record
|
||||
from apps.drama.models import GameEvent, Note, ScrollPosition, record
|
||||
from apps.epic.models import Room
|
||||
from apps.lyric.models import User
|
||||
|
||||
@@ -164,65 +164,65 @@ class BillscrollViewTest(TestCase):
|
||||
self.assertContains(response, 'class="drama-event-time"')
|
||||
|
||||
|
||||
class RecognitionPageViewTest(TestCase):
|
||||
class NotePageViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="recog@test.io")
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_requires_login(self):
|
||||
self.client.logout()
|
||||
response = self.client.get("/billboard/recognition/")
|
||||
response = self.client.get("/billboard/my-notes/")
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_returns_200(self):
|
||||
response = self.client.get("/billboard/recognition/")
|
||||
response = self.client.get("/billboard/my-notes/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_uses_recognition_template(self):
|
||||
response = self.client.get("/billboard/recognition/")
|
||||
self.assertTemplateUsed(response, "apps/billboard/recognition.html")
|
||||
def test_uses_note_page_template(self):
|
||||
response = self.client.get("/billboard/my-notes/")
|
||||
self.assertTemplateUsed(response, "apps/billboard/my_notes.html")
|
||||
|
||||
def test_passes_recognitions_in_context(self):
|
||||
recog = Recognition.objects.create(
|
||||
def test_passes_notes_in_context(self):
|
||||
recog = Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=timezone.now()
|
||||
)
|
||||
response = self.client.get("/billboard/recognition/")
|
||||
self.assertIn(recog, response.context["recognitions"])
|
||||
response = self.client.get("/billboard/my-notes/")
|
||||
self.assertIn(recog, response.context["notes"])
|
||||
|
||||
def test_excludes_other_users_recognitions(self):
|
||||
def test_excludes_other_users_notes(self):
|
||||
other = User.objects.create(email="other@test.io")
|
||||
Recognition.objects.create(
|
||||
Note.objects.create(
|
||||
user=other, slug="stargazer", earned_at=timezone.now()
|
||||
)
|
||||
response = self.client.get("/billboard/recognition/")
|
||||
self.assertEqual(list(response.context["recognitions"]), [])
|
||||
response = self.client.get("/billboard/my-notes/")
|
||||
self.assertEqual(list(response.context["notes"]), [])
|
||||
|
||||
def test_renders_recog_list_and_items(self):
|
||||
Recognition.objects.create(
|
||||
Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=timezone.now()
|
||||
)
|
||||
response = self.client.get("/billboard/recognition/")
|
||||
self.assertContains(response, 'class="recog-list"')
|
||||
self.assertContains(response, 'class="recog-item"')
|
||||
response = self.client.get("/billboard/my-notes/")
|
||||
self.assertContains(response, 'class="note-list"')
|
||||
self.assertContains(response, 'class="note-item"')
|
||||
|
||||
def test_renders_recog_item_title_description_image_box(self):
|
||||
Recognition.objects.create(
|
||||
Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=timezone.now()
|
||||
)
|
||||
response = self.client.get("/billboard/recognition/")
|
||||
self.assertContains(response, 'class="recog-item__title"')
|
||||
self.assertContains(response, 'class="recog-item__description"')
|
||||
self.assertContains(response, 'class="recog-item__image-box"')
|
||||
response = self.client.get("/billboard/my-notes/")
|
||||
self.assertContains(response, 'class="note-item__title"')
|
||||
self.assertContains(response, 'class="note-item__description"')
|
||||
self.assertContains(response, 'class="note-item__image-box"')
|
||||
|
||||
|
||||
class RecognitionSetPaletteViewTest(TestCase):
|
||||
class NoteSetPaletteViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="setpal@test.io")
|
||||
self.client.force_login(self.user)
|
||||
self.recognition = Recognition.objects.create(
|
||||
self.note = Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
||||
)
|
||||
self.url = "/billboard/recognition/stargazer/set-palette"
|
||||
self.url = "/billboard/note/stargazer/set-palette"
|
||||
|
||||
def test_requires_login(self):
|
||||
self.client.logout()
|
||||
@@ -233,14 +233,14 @@ class RecognitionSetPaletteViewTest(TestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_sets_palette_on_recognition(self):
|
||||
def test_sets_palette_on_note(self):
|
||||
self.client.post(
|
||||
self.url,
|
||||
data=_json.dumps({"palette": "palette-bardo"}),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.recognition.refresh_from_db()
|
||||
self.assertEqual(self.recognition.palette, "palette-bardo")
|
||||
self.note.refresh_from_db()
|
||||
self.assertEqual(self.note.palette, "palette-bardo")
|
||||
|
||||
def test_returns_200_with_ok(self):
|
||||
response = self.client.post(
|
||||
@@ -253,7 +253,7 @@ class RecognitionSetPaletteViewTest(TestCase):
|
||||
|
||||
def test_returns_404_for_slug_user_does_not_own(self):
|
||||
response = self.client.post(
|
||||
"/billboard/recognition/schizo/set-palette",
|
||||
"/billboard/note/schizo/set-palette",
|
||||
data=_json.dumps({"palette": "palette-bardo"}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ app_name = "billboard"
|
||||
urlpatterns = [
|
||||
path("", views.billboard, name="billboard"),
|
||||
path("toggle-applets", views.toggle_billboard_applets, name="toggle_applets"),
|
||||
path("recognition/", views.recognition_page, name="recognition"),
|
||||
path("recognition/<slug:slug>/set-palette", views.recognition_set_palette, name="recognition_set_palette"),
|
||||
path("my-notes/", views.my_notes, name="my_notes"),
|
||||
path("note/<slug:slug>/set-palette", views.note_set_palette, name="note_set_palette"),
|
||||
path("room/<uuid:room_id>/scroll/", views.room_scroll, name="scroll"),
|
||||
path("room/<uuid:room_id>/scroll-position/", views.save_scroll_position, name="save_scroll_position"),
|
||||
]
|
||||
|
||||
@@ -6,11 +6,24 @@ from django.http import JsonResponse
|
||||
from django.shortcuts import redirect, render
|
||||
|
||||
from apps.applets.utils import applet_context, apply_applet_toggle
|
||||
from apps.drama.models import GameEvent, Recognition, ScrollPosition
|
||||
from apps.dashboard.forms import LineForm
|
||||
from apps.dashboard.models import Post
|
||||
from apps.drama.models import GameEvent, Note, ScrollPosition
|
||||
from apps.epic.models import Room
|
||||
from apps.epic.utils import rooms_for_user
|
||||
|
||||
|
||||
def _recent_posts(user, limit=3):
|
||||
return (
|
||||
Post
|
||||
.objects
|
||||
.filter(Q(owner=user) | Q(shared_with=user))
|
||||
.annotate(last_line=Max('lines__id'))
|
||||
.order_by('-last_line')
|
||||
.distinct()[:limit]
|
||||
)
|
||||
|
||||
|
||||
@login_required(login_url="/")
|
||||
def billboard(request):
|
||||
my_rooms = rooms_for_user(request.user).order_by("-created_at")
|
||||
@@ -42,6 +55,8 @@ def billboard(request):
|
||||
"recent_events": recent_events,
|
||||
"viewer": request.user,
|
||||
"applets": applet_context(request.user, "billboard"),
|
||||
"form": LineForm(),
|
||||
"recent_posts": _recent_posts(request.user),
|
||||
"page_class": "page-billboard",
|
||||
})
|
||||
|
||||
@@ -53,6 +68,8 @@ def toggle_billboard_applets(request):
|
||||
if request.headers.get("HX-Request"):
|
||||
return render(request, "apps/billboard/_partials/_applets.html", {
|
||||
"applets": applet_context(request.user, "billboard"),
|
||||
"form": LineForm(),
|
||||
"recent_posts": _recent_posts(request.user),
|
||||
})
|
||||
return redirect("billboard:billboard")
|
||||
|
||||
@@ -71,7 +88,7 @@ def room_scroll(request, room_id):
|
||||
})
|
||||
|
||||
|
||||
_RECOGNITION_META = {
|
||||
_NOTE_META = {
|
||||
"stargazer": {
|
||||
"title": "Stargazer",
|
||||
"description": "You saved your first personal sky chart.",
|
||||
@@ -91,35 +108,35 @@ _RECOGNITION_META = {
|
||||
|
||||
|
||||
@login_required(login_url="/")
|
||||
def recognition_set_palette(request, slug):
|
||||
def note_set_palette(request, slug):
|
||||
from django.http import Http404
|
||||
try:
|
||||
recognition = Recognition.objects.get(user=request.user, slug=slug)
|
||||
except Recognition.DoesNotExist:
|
||||
note = Note.objects.get(user=request.user, slug=slug)
|
||||
except Note.DoesNotExist:
|
||||
raise Http404
|
||||
if request.method == "POST":
|
||||
body = json.loads(request.body)
|
||||
recognition.palette = body.get("palette", "")
|
||||
recognition.save(update_fields=["palette"])
|
||||
note.palette = body.get("palette", "")
|
||||
note.save(update_fields=["palette"])
|
||||
return JsonResponse({"ok": True})
|
||||
|
||||
|
||||
@login_required(login_url="/")
|
||||
def recognition_page(request):
|
||||
qs = Recognition.objects.filter(user=request.user)
|
||||
recognitions = [
|
||||
def my_notes(request):
|
||||
qs = Note.objects.filter(user=request.user)
|
||||
note_items = [
|
||||
{
|
||||
"obj": r,
|
||||
"title": _RECOGNITION_META.get(r.slug, {}).get("title", r.slug),
|
||||
"description": _RECOGNITION_META.get(r.slug, {}).get("description", ""),
|
||||
"palette_options": _RECOGNITION_META.get(r.slug, {}).get("palette_options", []),
|
||||
"obj": n,
|
||||
"title": _NOTE_META.get(n.slug, {}).get("title", n.slug),
|
||||
"description": _NOTE_META.get(n.slug, {}).get("description", ""),
|
||||
"palette_options": _NOTE_META.get(n.slug, {}).get("palette_options", []),
|
||||
}
|
||||
for r in qs
|
||||
for n in qs
|
||||
]
|
||||
return render(request, "apps/billboard/recognition.html", {
|
||||
"recognitions": qs,
|
||||
"recognition_items": recognitions,
|
||||
"page_class": "page-recognition",
|
||||
return render(request, "apps/billboard/my_notes.html", {
|
||||
"notes": qs,
|
||||
"note_items": note_items,
|
||||
"page_class": "page-notes",
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from .models import Item
|
||||
from .models import Line
|
||||
|
||||
DUPLICATE_ITEM_ERROR = "You've already logged this to your note"
|
||||
EMPTY_ITEM_ERROR = "You can't have an empty note item"
|
||||
DUPLICATE_LINE_ERROR = "You've already logged this to your post"
|
||||
EMPTY_LINE_ERROR = "You can't have an empty post line"
|
||||
|
||||
class ItemForm(forms.Form):
|
||||
class LineForm(forms.Form):
|
||||
text = forms.CharField(
|
||||
error_messages = {"required": EMPTY_ITEM_ERROR},
|
||||
error_messages = {"required": EMPTY_LINE_ERROR},
|
||||
required=True,
|
||||
)
|
||||
|
||||
def save(self, for_note):
|
||||
return Item.objects.create(
|
||||
note=for_note,
|
||||
def save(self, for_post):
|
||||
return Line.objects.create(
|
||||
post=for_post,
|
||||
text=self.cleaned_data["text"],
|
||||
)
|
||||
|
||||
class ExistingNoteItemForm(ItemForm):
|
||||
def __init__(self, for_note, *args, **kwargs):
|
||||
class ExistingPostLineForm(LineForm):
|
||||
def __init__(self, for_post, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._for_note = for_note
|
||||
self._for_post = for_post
|
||||
|
||||
def clean_text(self):
|
||||
text = self.cleaned_data["text"]
|
||||
if self._for_note.item_set.filter(text=text).exists():
|
||||
raise forms.ValidationError(DUPLICATE_ITEM_ERROR)
|
||||
if self._for_post.lines.filter(text=text).exists():
|
||||
raise forms.ValidationError(DUPLICATE_LINE_ERROR)
|
||||
return text
|
||||
|
||||
def save(self):
|
||||
return super().save(for_note=self._for_note)
|
||||
return super().save(for_post=self._for_post)
|
||||
|
||||
18
src/apps/dashboard/migrations/0004_rename_note_to_post.py
Normal file
18
src/apps/dashboard/migrations/0004_rename_note_to_post.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dashboard', '0003_alter_note_owner_alter_note_shared_with'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel('Note', 'Post'),
|
||||
migrations.RenameModel('Item', 'Line'),
|
||||
migrations.RenameField('Line', 'note', 'post'),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='line',
|
||||
unique_together={('post', 'text')},
|
||||
),
|
||||
]
|
||||
@@ -4,11 +4,11 @@ from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class Note(models.Model):
|
||||
class Post(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
owner = models.ForeignKey(
|
||||
"lyric.User",
|
||||
related_name="notes",
|
||||
related_name="posts",
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.CASCADE,
|
||||
@@ -16,24 +16,24 @@ class Note(models.Model):
|
||||
|
||||
shared_with = models.ManyToManyField(
|
||||
"lyric.User",
|
||||
related_name="shared_notes",
|
||||
related_name="shared_posts",
|
||||
blank=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.item_set.first().text
|
||||
return self.lines.first().text
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("view_note", args=[self.id])
|
||||
return reverse("view_post", args=[self.id])
|
||||
|
||||
class Item(models.Model):
|
||||
class Line(models.Model):
|
||||
text = models.TextField(default="")
|
||||
note = models.ForeignKey(Note, default=None, on_delete=models.CASCADE)
|
||||
post = models.ForeignKey(Post, default=None, on_delete=models.CASCADE, related_name="lines")
|
||||
|
||||
class Meta:
|
||||
ordering = ("id",)
|
||||
unique_together = ("note", "text")
|
||||
unique_together = ("post", "text")
|
||||
|
||||
def __str__(self):
|
||||
return self.text
|
||||
|
||||
49
src/apps/dashboard/static/apps/dashboard/note.js
Normal file
49
src/apps/dashboard/static/apps/dashboard/note.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const Note = (() => {
|
||||
'use strict';
|
||||
|
||||
function showBanner(note) {
|
||||
if (!note) return;
|
||||
|
||||
const earned = new Date(note.earned_at);
|
||||
const dateStr = earned.toLocaleDateString(undefined, {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
});
|
||||
|
||||
const banner = document.createElement('div');
|
||||
banner.className = 'note-banner';
|
||||
banner.innerHTML =
|
||||
'<div class="note-banner__body">' +
|
||||
'<p class="note-banner__title">' + _esc(note.title) + '</p>' +
|
||||
'<p class="note-banner__description">' + _esc(note.description) + '</p>' +
|
||||
'<time class="note-banner__timestamp" datetime="' + _esc(note.earned_at) + '">' +
|
||||
dateStr +
|
||||
'</time>' +
|
||||
'</div>' +
|
||||
'<div class="note-banner__image"></div>' +
|
||||
'<button type="button" class="btn btn-danger note-banner__nvm">NVM</button>' +
|
||||
'<a href="/billboard/my-notes/" class="btn btn-caution note-banner__fyi">FYI</a>';
|
||||
|
||||
banner.querySelector('.note-banner__nvm').addEventListener('click', function () {
|
||||
banner.remove();
|
||||
});
|
||||
|
||||
var h2 = document.querySelector('h2');
|
||||
if (h2 && h2.parentNode) {
|
||||
h2.parentNode.insertBefore(banner, h2.nextSibling);
|
||||
} else {
|
||||
document.body.insertBefore(banner, document.body.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSaveResponse(data) {
|
||||
showBanner(data && data.note);
|
||||
}
|
||||
|
||||
function _esc(str) {
|
||||
var d = document.createElement('div');
|
||||
d.textContent = str || '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
return { showBanner: showBanner, handleSaveResponse: handleSaveResponse };
|
||||
})();
|
||||
@@ -1,49 +0,0 @@
|
||||
const Recognition = (() => {
|
||||
'use strict';
|
||||
|
||||
function showBanner(recognition) {
|
||||
if (!recognition) return;
|
||||
|
||||
const earned = new Date(recognition.earned_at);
|
||||
const dateStr = earned.toLocaleDateString(undefined, {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
});
|
||||
|
||||
const banner = document.createElement('div');
|
||||
banner.className = 'recog-banner';
|
||||
banner.innerHTML =
|
||||
'<div class="recog-banner__body">' +
|
||||
'<p class="recog-banner__title">' + _esc(recognition.title) + '</p>' +
|
||||
'<p class="recog-banner__description">' + _esc(recognition.description) + '</p>' +
|
||||
'<time class="recog-banner__timestamp" datetime="' + _esc(recognition.earned_at) + '">' +
|
||||
dateStr +
|
||||
'</time>' +
|
||||
'</div>' +
|
||||
'<div class="recog-banner__image"></div>' +
|
||||
'<button type="button" class="btn btn-danger recog-banner__nvm">NVM</button>' +
|
||||
'<a href="/billboard/recognition/" class="btn btn-caution recog-banner__fyi">FYI</a>';
|
||||
|
||||
banner.querySelector('.recog-banner__nvm').addEventListener('click', function () {
|
||||
banner.remove();
|
||||
});
|
||||
|
||||
var h2 = document.querySelector('h2');
|
||||
if (h2 && h2.parentNode) {
|
||||
h2.parentNode.insertBefore(banner, h2.nextSibling);
|
||||
} else {
|
||||
document.body.insertBefore(banner, document.body.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSaveResponse(data) {
|
||||
showBanner(data && data.recognition);
|
||||
}
|
||||
|
||||
function _esc(str) {
|
||||
var d = document.createElement('div');
|
||||
d.textContent = str || '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
return { showBanner: showBanner, handleSaveResponse: handleSaveResponse };
|
||||
})();
|
||||
@@ -1,41 +1,41 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.dashboard.forms import (
|
||||
DUPLICATE_ITEM_ERROR,
|
||||
EMPTY_ITEM_ERROR,
|
||||
ExistingNoteItemForm,
|
||||
ItemForm,
|
||||
DUPLICATE_LINE_ERROR,
|
||||
EMPTY_LINE_ERROR,
|
||||
ExistingPostLineForm,
|
||||
LineForm,
|
||||
)
|
||||
from apps.dashboard.models import Item, Note
|
||||
from apps.dashboard.models import Line, Post
|
||||
|
||||
|
||||
class ItemFormTest(TestCase):
|
||||
def test_form_save_handles_saving_to_a_note(self):
|
||||
mynote = Note.objects.create()
|
||||
form = ItemForm(data={"text": "do re mi"})
|
||||
class LineFormTest(TestCase):
|
||||
def test_form_save_handles_saving_to_a_post(self):
|
||||
mypost = Post.objects.create()
|
||||
form = LineForm(data={"text": "do re mi"})
|
||||
self.assertTrue(form.is_valid())
|
||||
new_item = form.save(for_note=mynote)
|
||||
self.assertEqual(new_item, Item.objects.get())
|
||||
self.assertEqual(new_item.text, "do re mi")
|
||||
self.assertEqual(new_item.note, mynote)
|
||||
new_line = form.save(for_post=mypost)
|
||||
self.assertEqual(new_line, Line.objects.get())
|
||||
self.assertEqual(new_line.text, "do re mi")
|
||||
self.assertEqual(new_line.post, mypost)
|
||||
|
||||
class ExistingNoteItemFormTest(TestCase):
|
||||
def test_form_validation_for_blank_items(self):
|
||||
note = Note.objects.create()
|
||||
form = ExistingNoteItemForm(for_note=note, data={"text": ""})
|
||||
class ExistingPostLineFormTest(TestCase):
|
||||
def test_form_validation_for_blank_lines(self):
|
||||
post = Post.objects.create()
|
||||
form = ExistingPostLineForm(for_post=post, data={"text": ""})
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertEqual(form.errors["text"], [EMPTY_ITEM_ERROR])
|
||||
self.assertEqual(form.errors["text"], [EMPTY_LINE_ERROR])
|
||||
|
||||
def test_form_validation_for_duplicate_items(self):
|
||||
note = Note.objects.create()
|
||||
Item.objects.create(note=note, text="twins, basil")
|
||||
form = ExistingNoteItemForm(for_note=note, data={"text": "twins, basil"})
|
||||
def test_form_validation_for_duplicate_lines(self):
|
||||
post = Post.objects.create()
|
||||
Line.objects.create(post=post, text="twins, basil")
|
||||
form = ExistingPostLineForm(for_post=post, data={"text": "twins, basil"})
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertEqual(form.errors["text"], [DUPLICATE_ITEM_ERROR])
|
||||
self.assertEqual(form.errors["text"], [DUPLICATE_LINE_ERROR])
|
||||
|
||||
def test_form_save(self):
|
||||
mynote = Note.objects.create()
|
||||
form = ExistingNoteItemForm(for_note=mynote, data={"text": "howdy"})
|
||||
mypost = Post.objects.create()
|
||||
form = ExistingPostLineForm(for_post=mypost, data={"text": "howdy"})
|
||||
self.assertTrue(form.is_valid())
|
||||
new_item = form.save()
|
||||
self.assertEqual(new_item, Item.objects.get())
|
||||
new_line = form.save()
|
||||
self.assertEqual(new_line, Line.objects.get())
|
||||
|
||||
@@ -2,69 +2,69 @@ from django.core.exceptions import ValidationError
|
||||
from django.db.utils import IntegrityError
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.dashboard.models import Item, Note
|
||||
from apps.dashboard.models import Line, Post
|
||||
from apps.lyric.models import User
|
||||
|
||||
|
||||
class ItemModelTest(TestCase):
|
||||
def test_item_is_related_to_note(self):
|
||||
mynote = Note.objects.create()
|
||||
item = Item()
|
||||
item.note = mynote
|
||||
item.save()
|
||||
self.assertIn(item, mynote.item_set.all())
|
||||
class LineModelTest(TestCase):
|
||||
def test_line_is_related_to_post(self):
|
||||
mypost = Post.objects.create()
|
||||
line = Line()
|
||||
line.post = mypost
|
||||
line.save()
|
||||
self.assertIn(line, mypost.lines.all())
|
||||
|
||||
def test_cannot_save_null_note_items(self):
|
||||
mynote = Note.objects.create()
|
||||
item = Item(note=mynote, text=None)
|
||||
def test_cannot_save_null_post_lines(self):
|
||||
mypost = Post.objects.create()
|
||||
line = Line(post=mypost, text=None)
|
||||
with self.assertRaises(IntegrityError):
|
||||
item.save()
|
||||
line.save()
|
||||
|
||||
def test_cannot_save_empty_note_items(self):
|
||||
mynote = Note.objects.create()
|
||||
item = Item(note=mynote, text="")
|
||||
def test_cannot_save_empty_post_lines(self):
|
||||
mypost = Post.objects.create()
|
||||
line = Line(post=mypost, text="")
|
||||
with self.assertRaises(ValidationError):
|
||||
item.full_clean()
|
||||
line.full_clean()
|
||||
|
||||
def test_duplicate_items_are_invalid(self):
|
||||
mynote = Note.objects.create()
|
||||
Item.objects.create(note=mynote, text="jklol")
|
||||
def test_duplicate_lines_are_invalid(self):
|
||||
mypost = Post.objects.create()
|
||||
Line.objects.create(post=mypost, text="jklol")
|
||||
with self.assertRaises(ValidationError):
|
||||
item = Item(note=mynote, text="jklol")
|
||||
item.full_clean()
|
||||
line = Line(post=mypost, text="jklol")
|
||||
line.full_clean()
|
||||
|
||||
def test_still_can_save_same_item_to_different_notes(self):
|
||||
note1 = Note.objects.create()
|
||||
note2 = Note.objects.create()
|
||||
Item.objects.create(note=note1, text="nojk")
|
||||
item = Item(note=note2, text="nojk")
|
||||
item.full_clean() # should not raise
|
||||
def test_still_can_save_same_line_to_different_posts(self):
|
||||
post1 = Post.objects.create()
|
||||
post2 = Post.objects.create()
|
||||
Line.objects.create(post=post1, text="nojk")
|
||||
line = Line(post=post2, text="nojk")
|
||||
line.full_clean() # should not raise
|
||||
|
||||
class NoteModelTest(TestCase):
|
||||
class PostModelTest(TestCase):
|
||||
def test_get_absolute_url(self):
|
||||
mynote = Note.objects.create()
|
||||
self.assertEqual(mynote.get_absolute_url(), f"/dashboard/note/{mynote.id}/")
|
||||
mypost = Post.objects.create()
|
||||
self.assertEqual(mypost.get_absolute_url(), f"/dashboard/post/{mypost.id}/")
|
||||
|
||||
def test_note_items_order(self):
|
||||
note1 = Note.objects.create()
|
||||
item1 = Item.objects.create(note=note1, text="i1")
|
||||
item2 = Item.objects.create(note=note1, text="item 2")
|
||||
item3 = Item.objects.create(note=note1, text="3")
|
||||
def test_post_lines_order(self):
|
||||
post1 = Post.objects.create()
|
||||
line1 = Line.objects.create(post=post1, text="i1")
|
||||
line2 = Line.objects.create(post=post1, text="line 2")
|
||||
line3 = Line.objects.create(post=post1, text="3")
|
||||
self.assertEqual(
|
||||
list(note1.item_set.all()),
|
||||
[item1, item2, item3],
|
||||
list(post1.lines.all()),
|
||||
[line1, line2, line3],
|
||||
)
|
||||
|
||||
def test_notes_can_have_owners(self):
|
||||
def test_posts_can_have_owners(self):
|
||||
user = User.objects.create(email="a@b.cde")
|
||||
mynote = Note.objects.create(owner=user)
|
||||
self.assertIn(mynote, user.notes.all())
|
||||
mypost = Post.objects.create(owner=user)
|
||||
self.assertIn(mypost, user.posts.all())
|
||||
|
||||
def test_note_owner_is_optional(self):
|
||||
Note.objects.create()
|
||||
def test_post_owner_is_optional(self):
|
||||
Post.objects.create()
|
||||
|
||||
def test_note_name_is_first_item_text(self):
|
||||
note = Note.objects.create()
|
||||
Item.objects.create(note=note, text="first item")
|
||||
Item.objects.create(note=note, text="second item")
|
||||
self.assertEqual(note.name, "first item")
|
||||
def test_post_name_is_first_line_text(self):
|
||||
post = Post.objects.create()
|
||||
Line.objects.create(post=post, text="first line")
|
||||
Line.objects.create(post=post, text="second line")
|
||||
self.assertEqual(post.name, "first line")
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
sky_view — GET /dashboard/sky/ → renders sky template
|
||||
sky_preview — GET /dashboard/sky/preview → proxies to PySwiss (no DB write)
|
||||
sky_save — POST /dashboard/sky/save → saves natal data to User model;
|
||||
grants Stargazer Recognition on first save with real chart_data
|
||||
grants Stargazer Note on first save with real chart_data
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -12,7 +12,7 @@ from unittest.mock import patch, MagicMock
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.drama.models import Recognition
|
||||
from apps.drama.models import Note
|
||||
from apps.lyric.models import User
|
||||
|
||||
|
||||
@@ -233,8 +233,8 @@ _REAL_CHART = {
|
||||
}
|
||||
|
||||
|
||||
class SkySaveRecognitionTest(TestCase):
|
||||
"""sky_save grants the Stargazer Recognition on the first save with real chart_data."""
|
||||
class SkySaveNoteTest(TestCase):
|
||||
"""sky_save grants the Stargazer Note on the first save with real chart_data."""
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="star@test.io")
|
||||
@@ -256,35 +256,35 @@ class SkySaveRecognitionTest(TestCase):
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
def test_first_save_with_chart_data_returns_stargazer_recognition(self):
|
||||
def test_first_save_with_chart_data_returns_stargazer_note(self):
|
||||
data = self._post().json()
|
||||
self.assertIn("recognition", data)
|
||||
recog = data["recognition"]
|
||||
self.assertIn("note", data)
|
||||
recog = data["note"]
|
||||
self.assertEqual(recog["slug"], "stargazer")
|
||||
self.assertIn("title", recog)
|
||||
self.assertIn("description", recog)
|
||||
self.assertIn("earned_at", recog)
|
||||
|
||||
def test_first_save_creates_recognition_in_db(self):
|
||||
def test_first_save_creates_note_in_db(self):
|
||||
self._post()
|
||||
self.assertEqual(Recognition.objects.filter(user=self.user, slug="stargazer").count(), 1)
|
||||
self.assertEqual(Note.objects.filter(user=self.user, slug="stargazer").count(), 1)
|
||||
|
||||
def test_second_save_returns_null_recognition(self):
|
||||
def test_second_save_returns_null_note(self):
|
||||
self._post()
|
||||
data = self._post().json()
|
||||
self.assertIsNone(data["recognition"])
|
||||
self.assertIsNone(data["note"])
|
||||
|
||||
def test_second_save_does_not_create_duplicate_recognition(self):
|
||||
def test_second_save_does_not_create_duplicate_note(self):
|
||||
self._post()
|
||||
self._post()
|
||||
self.assertEqual(Recognition.objects.filter(user=self.user, slug="stargazer").count(), 1)
|
||||
self.assertEqual(Note.objects.filter(user=self.user, slug="stargazer").count(), 1)
|
||||
|
||||
def test_save_with_empty_chart_data_does_not_grant_recognition(self):
|
||||
def test_save_with_empty_chart_data_does_not_grant_note(self):
|
||||
data = self._post(chart_data={}).json()
|
||||
self.assertIsNone(data["recognition"])
|
||||
self.assertEqual(Recognition.objects.filter(user=self.user, slug="stargazer").count(), 0)
|
||||
self.assertIsNone(data["note"])
|
||||
self.assertEqual(Note.objects.filter(user=self.user, slug="stargazer").count(), 0)
|
||||
|
||||
def test_save_with_null_chart_data_does_not_grant_recognition(self):
|
||||
def test_save_with_null_chart_data_does_not_grant_note(self):
|
||||
data = self._post(chart_data=None).json()
|
||||
self.assertIsNone(data["recognition"])
|
||||
self.assertEqual(Recognition.objects.filter(user=self.user, slug="stargazer").count(), 0)
|
||||
self.assertIsNone(data["note"])
|
||||
self.assertEqual(Note.objects.filter(user=self.user, slug="stargazer").count(), 0)
|
||||
|
||||
@@ -9,11 +9,11 @@ from django.utils import html, timezone
|
||||
|
||||
from apps.applets.models import Applet, UserApplet
|
||||
from apps.dashboard.forms import (
|
||||
DUPLICATE_ITEM_ERROR,
|
||||
EMPTY_ITEM_ERROR,
|
||||
DUPLICATE_LINE_ERROR,
|
||||
EMPTY_LINE_ERROR,
|
||||
)
|
||||
from apps.dashboard.models import Item, Note
|
||||
from apps.drama.models import Recognition
|
||||
from apps.dashboard.models import Line, Post
|
||||
from apps.drama.models import Note
|
||||
from apps.lyric.models import User
|
||||
|
||||
|
||||
@@ -21,64 +21,59 @@ class HomePageTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="disco@test.io")
|
||||
self.client.force_login(self.user)
|
||||
Applet.objects.get_or_create(slug="new-note", defaults={"name": "New Note"})
|
||||
|
||||
def test_uses_home_template(self):
|
||||
response = self.client.get('/')
|
||||
self.assertTemplateUsed(response, 'apps/dashboard/home.html')
|
||||
|
||||
def test_renders_input_form(self):
|
||||
response = self.client.get('/')
|
||||
parsed = lxml.html.fromstring(response.content)
|
||||
forms = parsed.cssselect('form[method=POST]')
|
||||
self.assertIn("/dashboard/new_note", [form.get("action") for form in forms])
|
||||
[form] = [form for form in forms if form.get("action") == "/dashboard/new_note"]
|
||||
inputs = form.cssselect("input")
|
||||
self.assertIn("text", [input.get("name") for input in inputs])
|
||||
|
||||
class NewNoteTest(TestCase):
|
||||
@override_settings(COMPRESS_ENABLED=False)
|
||||
class NewPostTest(TestCase):
|
||||
def setUp(self):
|
||||
user = User.objects.create(email="disco@test.io")
|
||||
self.client.force_login(user)
|
||||
self.user = User.objects.create(email="disco@test.io")
|
||||
self.client.force_login(self.user)
|
||||
Applet.objects.get_or_create(
|
||||
slug="new-post",
|
||||
defaults={"name": "New Post", "context": "billboard", "grid_cols": 9, "grid_rows": 3},
|
||||
)
|
||||
|
||||
def test_can_save_a_POST_request(self):
|
||||
self.client.post("/dashboard/new_note", data={"text": "A new note item"})
|
||||
self.assertEqual(Item.objects.count(), 1)
|
||||
new_item = Item.objects.get()
|
||||
self.assertEqual(new_item.text, "A new note item")
|
||||
self.client.post("/dashboard/new_post", data={"text": "A new post line"})
|
||||
self.assertEqual(Line.objects.count(), 1)
|
||||
new_line = Line.objects.get()
|
||||
self.assertEqual(new_line.text, "A new post line")
|
||||
|
||||
def test_redirects_after_POST(self):
|
||||
response = self.client.post("/dashboard/new_note", data={"text": "A new note item"})
|
||||
new_note = Note.objects.get()
|
||||
self.assertRedirects(response, f"/dashboard/note/{new_note.id}/")
|
||||
response = self.client.post("/dashboard/new_post", data={"text": "A new post line"})
|
||||
new_post = Post.objects.get()
|
||||
self.assertRedirects(response, f"/dashboard/post/{new_post.id}/")
|
||||
|
||||
# Post invalid input helper
|
||||
def post_invalid_input(self):
|
||||
return self.client.post("/dashboard/new_note", data={"text": ""})
|
||||
return self.client.post("/dashboard/new_post", data={"text": ""})
|
||||
|
||||
def test_for_invalid_input_nothing_saved_to_db(self):
|
||||
self.post_invalid_input()
|
||||
self.assertEqual(Item.objects.count(), 0)
|
||||
self.assertEqual(Line.objects.count(), 0)
|
||||
|
||||
def test_for_invalid_input_renders_home_template(self):
|
||||
def test_for_invalid_input_renders_billboard_template(self):
|
||||
response = self.post_invalid_input()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, "apps/dashboard/home.html")
|
||||
self.assertTemplateUsed(response, "apps/billboard/billboard.html")
|
||||
|
||||
def test_for_invalid_input_shows_error_on_page(self):
|
||||
response = self.post_invalid_input()
|
||||
self.assertContains(response, html.escape(EMPTY_ITEM_ERROR))
|
||||
self.assertContains(response, html.escape(EMPTY_LINE_ERROR))
|
||||
|
||||
@override_settings(COMPRESS_ENABLED=False)
|
||||
class NoteViewTest(TestCase):
|
||||
def test_uses_note_template(self):
|
||||
mynote = Note.objects.create()
|
||||
response = self.client.get(f"/dashboard/note/{mynote.id}/")
|
||||
self.assertTemplateUsed(response, "apps/dashboard/note.html")
|
||||
class PostViewTest(TestCase):
|
||||
def test_uses_post_template(self):
|
||||
mypost = Post.objects.create()
|
||||
response = self.client.get(f"/dashboard/post/{mypost.id}/")
|
||||
self.assertTemplateUsed(response, "apps/dashboard/post.html")
|
||||
|
||||
def test_renders_input_form(self):
|
||||
mynote = Note.objects.create()
|
||||
url = f"/dashboard/note/{mynote.id}/"
|
||||
mypost = Post.objects.create()
|
||||
url = f"/dashboard/post/{mypost.id}/"
|
||||
response = self.client.get(url)
|
||||
parsed = lxml.html.fromstring(response.content)
|
||||
forms = parsed.cssselect("form[method=POST]")
|
||||
@@ -87,62 +82,62 @@ class NoteViewTest(TestCase):
|
||||
inputs = form.cssselect("input")
|
||||
self.assertIn("text", [input.get("name") for input in inputs])
|
||||
|
||||
def test_displays_only_items_for_that_note(self):
|
||||
def test_displays_only_lines_for_that_post(self):
|
||||
# Given/Arrange
|
||||
correct_note = Note.objects.create()
|
||||
Item.objects.create(text="itemey 1", note=correct_note)
|
||||
Item.objects.create(text="itemey 2", note=correct_note)
|
||||
other_note = Note.objects.create()
|
||||
Item.objects.create(text="other note item", note=other_note)
|
||||
correct_post = Post.objects.create()
|
||||
Line.objects.create(text="itemey 1", post=correct_post)
|
||||
Line.objects.create(text="itemey 2", post=correct_post)
|
||||
other_post = Post.objects.create()
|
||||
Line.objects.create(text="other post line", post=other_post)
|
||||
# When/Act
|
||||
response = self.client.get(f"/dashboard/note/{correct_note.id}/")
|
||||
response = self.client.get(f"/dashboard/post/{correct_post.id}/")
|
||||
# Then/Assert
|
||||
self.assertContains(response, "itemey 1")
|
||||
self.assertContains(response, "itemey 2")
|
||||
self.assertNotContains(response, "other note item")
|
||||
self.assertNotContains(response, "other post line")
|
||||
|
||||
def test_can_save_a_POST_request_to_an_existing_note(self):
|
||||
other_note = Note.objects.create()
|
||||
correct_note = Note.objects.create()
|
||||
def test_can_save_a_POST_request_to_an_existing_post(self):
|
||||
other_post = Post.objects.create()
|
||||
correct_post = Post.objects.create()
|
||||
|
||||
self.client.post(
|
||||
f"/dashboard/note/{correct_note.id}/",
|
||||
data={"text": "A new item for an existing note"},
|
||||
f"/dashboard/post/{correct_post.id}/",
|
||||
data={"text": "A new line for an existing post"},
|
||||
)
|
||||
|
||||
self.assertEqual(Item.objects.count(), 1)
|
||||
new_item = Item.objects.get()
|
||||
self.assertEqual(new_item.text, "A new item for an existing note")
|
||||
self.assertEqual(new_item.note, correct_note)
|
||||
self.assertEqual(Line.objects.count(), 1)
|
||||
new_line = Line.objects.get()
|
||||
self.assertEqual(new_line.text, "A new line for an existing post")
|
||||
self.assertEqual(new_line.post, correct_post)
|
||||
|
||||
def test_POST_redirects_to_note_view(self):
|
||||
other_note = Note.objects.create()
|
||||
correct_note = Note.objects.create()
|
||||
def test_POST_redirects_to_post_view(self):
|
||||
other_post = Post.objects.create()
|
||||
correct_post = Post.objects.create()
|
||||
|
||||
response = self.client.post(
|
||||
f"/dashboard/note/{correct_note.id}/",
|
||||
data={"text": "A new item for an existing note"},
|
||||
f"/dashboard/post/{correct_post.id}/",
|
||||
data={"text": "A new line for an existing post"},
|
||||
)
|
||||
|
||||
self.assertRedirects(response, f"/dashboard/note/{correct_note.id}/")
|
||||
self.assertRedirects(response, f"/dashboard/post/{correct_post.id}/")
|
||||
|
||||
# Post invalid input helper
|
||||
def post_invalid_input(self):
|
||||
mynote = Note.objects.create()
|
||||
return self.client.post(f"/dashboard/note/{mynote.id}/", data={"text": ""})
|
||||
mypost = Post.objects.create()
|
||||
return self.client.post(f"/dashboard/post/{mypost.id}/", data={"text": ""})
|
||||
|
||||
def test_for_invalid_input_nothing_saved_to_db(self):
|
||||
self.post_invalid_input()
|
||||
self.assertEqual(Item.objects.count(), 0)
|
||||
self.assertEqual(Line.objects.count(), 0)
|
||||
|
||||
def test_for_invalid_input_renders_note_template(self):
|
||||
def test_for_invalid_input_renders_post_template(self):
|
||||
response = self.post_invalid_input()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, "apps/dashboard/note.html")
|
||||
self.assertTemplateUsed(response, "apps/dashboard/post.html")
|
||||
|
||||
def test_for_invalid_input_shows_error_on_page(self):
|
||||
response = self.post_invalid_input()
|
||||
self.assertContains(response, html.escape(EMPTY_ITEM_ERROR))
|
||||
self.assertContains(response, html.escape(EMPTY_LINE_ERROR))
|
||||
|
||||
def test_for_invalid_input_sets_is_invalid_class(self):
|
||||
response = self.post_invalid_input()
|
||||
@@ -150,26 +145,26 @@ class NoteViewTest(TestCase):
|
||||
[input] = parsed.cssselect("input[name=text]")
|
||||
self.assertIn("is-invalid", set(input.classes))
|
||||
|
||||
def test_duplicate_item_validation_errors_end_up_on_note_page(self):
|
||||
note1 = Note.objects.create()
|
||||
Item.objects.create(note=note1, text="lorem ipsum")
|
||||
def test_duplicate_line_validation_errors_end_up_on_post_page(self):
|
||||
post1 = Post.objects.create()
|
||||
Line.objects.create(post=post1, text="lorem ipsum")
|
||||
|
||||
response = self.client.post(
|
||||
f"/dashboard/note/{note1.id}/",
|
||||
f"/dashboard/post/{post1.id}/",
|
||||
data={"text": "lorem ipsum"},
|
||||
)
|
||||
|
||||
expected_error = html.escape(DUPLICATE_ITEM_ERROR)
|
||||
expected_error = html.escape(DUPLICATE_LINE_ERROR)
|
||||
self.assertContains(response, expected_error)
|
||||
self.assertTemplateUsed(response, "apps/dashboard/note.html")
|
||||
self.assertEqual(Item.objects.all().count(), 1)
|
||||
self.assertTemplateUsed(response, "apps/dashboard/post.html")
|
||||
self.assertEqual(Line.objects.all().count(), 1)
|
||||
|
||||
class MyNotesTest(TestCase):
|
||||
def test_my_notes_url_renders_my_notes_template(self):
|
||||
class MyPostsTest(TestCase):
|
||||
def test_my_posts_url_renders_my_posts_template(self):
|
||||
user = User.objects.create(email="a@b.cde")
|
||||
self.client.force_login(user)
|
||||
response = self.client.get(f"/dashboard/users/{user.id}/")
|
||||
self.assertTemplateUsed(response, "apps/dashboard/my_notes.html")
|
||||
self.assertTemplateUsed(response, "apps/dashboard/my_posts.html")
|
||||
|
||||
def test_passes_correct_owner_to_template(self):
|
||||
User.objects.create(email="wrongowner@example.com")
|
||||
@@ -178,71 +173,69 @@ class MyNotesTest(TestCase):
|
||||
response = self.client.get(f"/dashboard/users/{correct_user.id}/")
|
||||
self.assertEqual(response.context["owner"], correct_user)
|
||||
|
||||
def test_note_owner_is_saved_if_user_is_authenticated(self):
|
||||
def test_post_owner_is_saved_if_user_is_authenticated(self):
|
||||
user = User.objects.create(email="a@b.cde")
|
||||
self.client.force_login(user)
|
||||
self.client.post("/dashboard/new_note", data={"text": "new item"})
|
||||
new_note = Note.objects.get()
|
||||
self.assertEqual(new_note.owner, user)
|
||||
self.client.post("/dashboard/new_post", data={"text": "new line"})
|
||||
new_post = Post.objects.get()
|
||||
self.assertEqual(new_post.owner, user)
|
||||
|
||||
def test_my_notes_redirects_if_not_logged_in(self):
|
||||
user = User.objects.create(email="a@b.cde")
|
||||
def test_my_posts_redirects_if_not_logged_in(self):
|
||||
user = User.objects.create(email="a@b.cde")
|
||||
response = self.client.get(f"/dashboard/users/{user.id}/")
|
||||
self.assertRedirects(response, "/")
|
||||
|
||||
def test_my_notes_returns_403_for_wrong_user(self):
|
||||
# create two users, login as user_a, request user_b's my_notes url
|
||||
def test_my_posts_returns_403_for_wrong_user(self):
|
||||
user1 = User.objects.create(email="a@b.cde")
|
||||
user2 = User.objects.create(email="wrongowner@example.com")
|
||||
self.client.force_login(user2)
|
||||
response = self.client.get(f"/dashboard/users/{user1.id}/")
|
||||
# assert 403
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
class ShareNoteTest(TestCase):
|
||||
def test_post_to_share_note_url_redirects_to_note(self):
|
||||
our_note = Note.objects.create()
|
||||
class SharePostTest(TestCase):
|
||||
def test_post_to_share_post_url_redirects_to_post(self):
|
||||
our_post = Post.objects.create()
|
||||
alice = User.objects.create(email="alice@example.com")
|
||||
response = self.client.post(
|
||||
f"/dashboard/note/{our_note.id}/share_note",
|
||||
f"/dashboard/post/{our_post.id}/share_post",
|
||||
data={"recipient": "alice@example.com"},
|
||||
)
|
||||
self.assertRedirects(response, f"/dashboard/note/{our_note.id}/")
|
||||
self.assertRedirects(response, f"/dashboard/post/{our_post.id}/")
|
||||
|
||||
def test_post_with_email_adds_user_to_shared_with(self):
|
||||
our_note = Note.objects.create()
|
||||
our_post = Post.objects.create()
|
||||
alice = User.objects.create(email="alice@example.com")
|
||||
self.client.post(
|
||||
f"/dashboard/note/{our_note.id}/share_note",
|
||||
f"/dashboard/post/{our_post.id}/share_post",
|
||||
data={"recipient": "alice@example.com"},
|
||||
)
|
||||
self.assertIn(alice, our_note.shared_with.all())
|
||||
self.assertIn(alice, our_post.shared_with.all())
|
||||
|
||||
def test_post_with_nonexistent_email_redirects_to_note(self):
|
||||
our_note = Note.objects.create()
|
||||
def test_post_with_nonexistent_email_redirects_to_post(self):
|
||||
our_post = Post.objects.create()
|
||||
response = self.client.post(
|
||||
f"/dashboard/note/{our_note.id}/share_note",
|
||||
f"/dashboard/post/{our_post.id}/share_post",
|
||||
data={"recipient": "nobody@example.com"},
|
||||
)
|
||||
self.assertRedirects(
|
||||
response,
|
||||
f"/dashboard/note/{our_note.id}/",
|
||||
f"/dashboard/post/{our_post.id}/",
|
||||
fetch_redirect_response=False,
|
||||
)
|
||||
|
||||
def test_share_note_does_not_add_owner_as_recipient(self):
|
||||
def test_share_post_does_not_add_owner_as_recipient(self):
|
||||
owner = User.objects.create(email="owner@example.com")
|
||||
our_note = Note.objects.create(owner=owner)
|
||||
our_post = Post.objects.create(owner=owner)
|
||||
self.client.force_login(owner)
|
||||
self.client.post(reverse("share_note", args=[our_note.id]),
|
||||
self.client.post(reverse("share_post", args=[our_post.id]),
|
||||
data={"recipient": "owner@example.com"})
|
||||
self.assertNotIn(owner, our_note.shared_with.all())
|
||||
self.assertNotIn(owner, our_post.shared_with.all())
|
||||
|
||||
@override_settings(MESSAGE_STORAGE='django.contrib.messages.storage.session.SessionStorage')
|
||||
def test_share_note_shows_privacy_safe_message(self):
|
||||
our_note = Note.objects.create()
|
||||
def test_share_post_shows_privacy_safe_message(self):
|
||||
our_post = Post.objects.create()
|
||||
response = self.client.post(
|
||||
f"/dashboard/note/{our_note.id}/share_note",
|
||||
f"/dashboard/post/{our_post.id}/share_post",
|
||||
data={"recipient": "nobody@example.com"},
|
||||
follow=True,
|
||||
)
|
||||
@@ -252,26 +245,26 @@ class ShareNoteTest(TestCase):
|
||||
"An invite has been sent if that address is registered.",
|
||||
)
|
||||
|
||||
class ViewAuthNoteTest(TestCase):
|
||||
class ViewAuthPostTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner = User.objects.create(email="disco@example.com")
|
||||
self.our_note = Note.objects.create(owner=self.owner)
|
||||
self.our_post = Post.objects.create(owner=self.owner)
|
||||
|
||||
def test_anonymous_user_is_redirected(self):
|
||||
response = self.client.get(reverse("view_note", args=[self.our_note.id]))
|
||||
response = self.client.get(reverse("view_post", args=[self.our_post.id]))
|
||||
self.assertRedirects(response, "/", fetch_redirect_response=False)
|
||||
|
||||
def test_non_owner_non_shared_user_gets_403(self):
|
||||
stranger = User.objects.create(email="stranger@example.com")
|
||||
self.client.force_login(stranger)
|
||||
response = self.client.get(reverse("view_note", args=[self.our_note.id]))
|
||||
response = self.client.get(reverse("view_post", args=[self.our_post.id]))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_shared_with_user_can_access_note(self):
|
||||
def test_shared_with_user_can_access_post(self):
|
||||
guest = User.objects.create(email="guest@example.com")
|
||||
self.our_note.shared_with.add(guest)
|
||||
self.our_post.shared_with.add(guest)
|
||||
self.client.force_login(guest)
|
||||
response = self.client.get(reverse("view_note", args=[self.our_note.id]))
|
||||
response = self.client.get(reverse("view_post", args=[self.our_post.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@override_settings(COMPRESS_ENABLED=False)
|
||||
@@ -349,14 +342,14 @@ class SetPaletteTest(TestCase):
|
||||
swatches = parsed.cssselect(".swatch")
|
||||
self.assertEqual(len(swatches), len(response.context["palettes"]))
|
||||
|
||||
class RecognitionPaletteContextTest(TestCase):
|
||||
class NotePaletteContextTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="recog_palette@test.io")
|
||||
self.client.force_login(self.user)
|
||||
Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"})
|
||||
|
||||
def test_recognition_palette_unlocks_swatch_in_context(self):
|
||||
Recognition.objects.create(
|
||||
def test_note_palette_unlocks_swatch_in_context(self):
|
||||
Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
||||
palette="palette-bardo",
|
||||
)
|
||||
@@ -365,8 +358,8 @@ class RecognitionPaletteContextTest(TestCase):
|
||||
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
|
||||
self.assertFalse(bardo["locked"])
|
||||
|
||||
def test_recognition_palette_shoptalk_contains_recognition_title(self):
|
||||
Recognition.objects.create(
|
||||
def test_note_palette_shoptalk_contains_note_title(self):
|
||||
Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
||||
palette="palette-bardo",
|
||||
)
|
||||
@@ -375,8 +368,8 @@ class RecognitionPaletteContextTest(TestCase):
|
||||
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
|
||||
self.assertIn("Stargazer", bardo["shoptalk"])
|
||||
|
||||
def test_recognition_without_palette_field_keeps_swatch_locked(self):
|
||||
Recognition.objects.create(
|
||||
def test_note_without_palette_field_keeps_swatch_locked(self):
|
||||
Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
||||
palette=None,
|
||||
)
|
||||
@@ -385,8 +378,8 @@ class RecognitionPaletteContextTest(TestCase):
|
||||
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
|
||||
self.assertTrue(bardo["locked"])
|
||||
|
||||
def test_recognition_palette_allows_set_palette_via_view(self):
|
||||
Recognition.objects.create(
|
||||
def test_note_palette_allows_set_palette_via_view(self):
|
||||
Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
||||
palette="palette-bardo",
|
||||
)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from apps.dashboard.forms import (
|
||||
EMPTY_ITEM_ERROR,
|
||||
ItemForm,
|
||||
EMPTY_LINE_ERROR,
|
||||
LineForm,
|
||||
)
|
||||
|
||||
|
||||
class SimpleItemFormTest(SimpleTestCase):
|
||||
def test_form_validation_for_blank_items(self):
|
||||
form = ItemForm(data={"text": ""})
|
||||
class SimpleLineFormTest(SimpleTestCase):
|
||||
def test_form_validation_for_blank_lines(self):
|
||||
form = LineForm(data={"text": ""})
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertEqual(form.errors["text"], [EMPTY_ITEM_ERROR])
|
||||
self.assertEqual(form.errors["text"], [EMPTY_LINE_ERROR])
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from apps.dashboard.models import Item
|
||||
from apps.dashboard.models import Line
|
||||
|
||||
|
||||
class SimpleItemModelTest(SimpleTestCase):
|
||||
class SimpleLineModelTest(SimpleTestCase):
|
||||
def test_default_text(self):
|
||||
item = Item()
|
||||
self.assertEqual(item.text, "")
|
||||
line = Line()
|
||||
self.assertEqual(line.text, "")
|
||||
|
||||
def test_string_representation(self):
|
||||
item = Item(text="sample text")
|
||||
self.assertEqual(str(item), "sample text")
|
||||
line = Line(text="sample text")
|
||||
self.assertEqual(str(line), "sample text")
|
||||
|
||||
@@ -2,12 +2,12 @@ from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('new_note', views.new_note, name='new_note'),
|
||||
path('note/<uuid:note_id>/', views.view_note, name='view_note'),
|
||||
path('note/<uuid:note_id>/share_note', views.share_note, name="share_note"),
|
||||
path('new_post', views.new_post, name='new_post'),
|
||||
path('post/<uuid:post_id>/', views.view_post, name='view_post'),
|
||||
path('post/<uuid:post_id>/share_post', views.share_post, name="share_post"),
|
||||
path('set_palette', views.set_palette, name='set_palette'),
|
||||
path('set_profile', views.set_profile, name='set_profile'),
|
||||
path('users/<uuid:user_id>/', views.my_notes, name='my_notes'),
|
||||
path('users/<uuid:user_id>/', views.my_posts, name='my_posts'),
|
||||
path('toggle_applets', views.toggle_applets, name="toggle_applets"),
|
||||
path('wallet/', views.wallet, name='wallet'),
|
||||
path('wallet/toggle-applets', views.toggle_wallet_applets, name='toggle_wallet_applets'),
|
||||
|
||||
@@ -15,14 +15,14 @@ from django.utils import timezone
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
|
||||
from apps.applets.utils import applet_context, apply_applet_toggle
|
||||
from apps.dashboard.forms import ExistingNoteItemForm, ItemForm
|
||||
from apps.dashboard.models import Item, Note
|
||||
from apps.drama.models import Recognition
|
||||
from apps.dashboard.forms import ExistingPostLineForm, LineForm
|
||||
from apps.dashboard.models import Line, Post
|
||||
from apps.drama.models import Note
|
||||
from apps.epic.utils import _compute_distinctions
|
||||
from apps.lyric.models import PaymentMethod, Token, User, Wallet
|
||||
|
||||
|
||||
APPLET_ORDER = ["wallet", "new-note", "my-notes", "username", "palette"]
|
||||
APPLET_ORDER = ["wallet", "username", "palette"]
|
||||
_BASE_UNLOCKED = frozenset([
|
||||
"palette-default",
|
||||
"palette-cedar",
|
||||
@@ -40,7 +40,7 @@ _PALETTE_DEFS = [
|
||||
{"name": "palette-terrestre", "label": "Terrestre", "locked": True},
|
||||
{"name": "palette-celestia", "label": "Celestia", "locked": True},
|
||||
]
|
||||
_RECOGNITION_TITLES = {
|
||||
_NOTE_TITLES = {
|
||||
"stargazer": "Stargazer",
|
||||
"schizo": "Schizo",
|
||||
"nomad": "Nomad",
|
||||
@@ -54,7 +54,7 @@ def _palettes_for_user(user):
|
||||
return [dict(p, shoptalk="Placeholder") for p in _PALETTE_DEFS]
|
||||
granted = {
|
||||
r.palette: r
|
||||
for r in Recognition.objects.filter(user=user, palette__isnull=False).exclude(palette="")
|
||||
for r in Note.objects.filter(user=user, palette__isnull=False).exclude(palette="")
|
||||
}
|
||||
result = []
|
||||
for p in _PALETTE_DEFS:
|
||||
@@ -62,7 +62,7 @@ def _palettes_for_user(user):
|
||||
r = granted.get(p["name"])
|
||||
if r and p["locked"]:
|
||||
entry["locked"] = False
|
||||
title = _RECOGNITION_TITLES.get(r.slug, r.slug.capitalize())
|
||||
title = _NOTE_TITLES.get(r.slug, r.slug.capitalize())
|
||||
entry["shoptalk"] = f"{title} · {r.earned_at.strftime('%b %d, %Y').replace(' 0', ' ')}"
|
||||
else:
|
||||
entry["shoptalk"] = "Placeholder"
|
||||
@@ -73,89 +73,86 @@ def _palettes_for_user(user):
|
||||
def _unlocked_palettes_for_user(user):
|
||||
base = set(_BASE_UNLOCKED)
|
||||
if user and user.is_authenticated:
|
||||
for r in Recognition.objects.filter(user=user, palette__isnull=False).exclude(palette=""):
|
||||
for r in Note.objects.filter(user=user, palette__isnull=False).exclude(palette=""):
|
||||
base.add(r.palette)
|
||||
return base
|
||||
|
||||
|
||||
def _recent_notes(user, limit=3):
|
||||
def _recent_posts(user, limit=3):
|
||||
return (
|
||||
Note
|
||||
Post
|
||||
.objects
|
||||
.filter(Q(owner=user) | Q(shared_with=user))
|
||||
.annotate(last_item=Max('item__id'))
|
||||
.order_by('-last_item')
|
||||
.annotate(last_line=Max('lines__id'))
|
||||
.order_by('-last_line')
|
||||
.distinct()[:limit]
|
||||
)
|
||||
|
||||
def home_page(request):
|
||||
context = {
|
||||
"form": ItemForm(),
|
||||
"palettes": _palettes_for_user(request.user),
|
||||
"page_class": "page-dashboard",
|
||||
}
|
||||
if request.user.is_authenticated:
|
||||
context["applets"] = applet_context(request.user, "dashboard")
|
||||
context["recent_notes"] = _recent_notes(request.user)
|
||||
return render(request, "apps/dashboard/home.html", context)
|
||||
|
||||
def new_note(request):
|
||||
form = ItemForm(data=request.POST)
|
||||
def new_post(request):
|
||||
form = LineForm(data=request.POST)
|
||||
if form.is_valid():
|
||||
nunote = Note.objects.create()
|
||||
nupost = Post.objects.create()
|
||||
if request.user.is_authenticated:
|
||||
nunote.owner = request.user
|
||||
nunote.save()
|
||||
form.save(for_note=nunote)
|
||||
return redirect(nunote)
|
||||
nupost.owner = request.user
|
||||
nupost.save()
|
||||
form.save(for_post=nupost)
|
||||
return redirect(nupost)
|
||||
else:
|
||||
context = {
|
||||
"form": form,
|
||||
"palettes": _palettes_for_user(request.user),
|
||||
"page_class": "page-dashboard",
|
||||
"page_class": "page-billboard",
|
||||
}
|
||||
if request.user.is_authenticated:
|
||||
context["applets"] = applet_context(request.user, "dashboard")
|
||||
context["recent_notes"] = _recent_notes(request.user)
|
||||
return render(request, "apps/dashboard/home.html", context)
|
||||
context["applets"] = applet_context(request.user, "billboard")
|
||||
context["recent_posts"] = _recent_posts(request.user)
|
||||
return render(request, "apps/billboard/billboard.html", context)
|
||||
|
||||
def view_note(request, note_id):
|
||||
our_note = Note.objects.get(id=note_id)
|
||||
def view_post(request, post_id):
|
||||
our_post = Post.objects.get(id=post_id)
|
||||
|
||||
if our_note.owner:
|
||||
if our_post.owner:
|
||||
if not request.user.is_authenticated:
|
||||
return redirect("/")
|
||||
if request.user != our_note.owner and request.user not in our_note.shared_with.all():
|
||||
if request.user != our_post.owner and request.user not in our_post.shared_with.all():
|
||||
return HttpResponseForbidden()
|
||||
|
||||
form = ExistingNoteItemForm(for_note=our_note)
|
||||
form = ExistingPostLineForm(for_post=our_post)
|
||||
|
||||
if request.method == "POST":
|
||||
form = ExistingNoteItemForm(for_note=our_note, data=request.POST)
|
||||
form = ExistingPostLineForm(for_post=our_post, data=request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect(our_note)
|
||||
return render(request, "apps/dashboard/note.html", {"note": our_note, "form": form})
|
||||
return redirect(our_post)
|
||||
return render(request, "apps/dashboard/post.html", {"post": our_post, "form": form})
|
||||
|
||||
def my_notes(request, user_id):
|
||||
def my_posts(request, user_id):
|
||||
owner = User.objects.get(id=user_id)
|
||||
if not request.user.is_authenticated:
|
||||
return redirect("/")
|
||||
if request.user.id != owner.id:
|
||||
return HttpResponseForbidden()
|
||||
return render(request, "apps/dashboard/my_notes.html", {"owner": owner})
|
||||
return render(request, "apps/dashboard/my_posts.html", {"owner": owner})
|
||||
|
||||
def share_note(request, note_id):
|
||||
our_note = Note.objects.get(id=note_id)
|
||||
def share_post(request, post_id):
|
||||
our_post = Post.objects.get(id=post_id)
|
||||
try:
|
||||
recipient = User.objects.get(email=request.POST["recipient"])
|
||||
if recipient == request.user:
|
||||
return redirect(our_note)
|
||||
our_note.shared_with.add(recipient)
|
||||
return redirect(our_post)
|
||||
our_post.shared_with.add(recipient)
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
messages.success(request, "An invite has been sent if that address is registered.")
|
||||
return redirect(our_note)
|
||||
return redirect(our_post)
|
||||
|
||||
@login_required(login_url="/")
|
||||
def set_palette(request):
|
||||
@@ -184,8 +181,6 @@ def toggle_applets(request):
|
||||
return render(request, "apps/dashboard/_partials/_applets.html", {
|
||||
"applets": applet_context(request.user, "dashboard"),
|
||||
"palettes": _palettes_for_user(request.user),
|
||||
"form": ItemForm(),
|
||||
"recent_notes": _recent_notes(request.user),
|
||||
})
|
||||
return redirect("home")
|
||||
|
||||
@@ -410,18 +405,18 @@ def sky_save(request):
|
||||
'sky_birth_place', 'sky_birth_tz', 'sky_house_system', 'sky_chart_data',
|
||||
])
|
||||
|
||||
recognition_payload = None
|
||||
note_payload = None
|
||||
if user.sky_chart_data:
|
||||
recog, created = Recognition.grant_if_new(user, "stargazer")
|
||||
note, created = Note.grant_if_new(user, "stargazer")
|
||||
if created:
|
||||
recognition_payload = {
|
||||
"slug": recog.slug,
|
||||
note_payload = {
|
||||
"slug": note.slug,
|
||||
"title": "Stargazer",
|
||||
"description": "You saved your first personal sky chart.",
|
||||
"earned_at": recog.earned_at.isoformat(),
|
||||
"earned_at": note.earned_at.isoformat(),
|
||||
}
|
||||
|
||||
return JsonResponse({"saved": True, "recognition": recognition_payload})
|
||||
return JsonResponse({"saved": True, "note": note_payload})
|
||||
|
||||
|
||||
@login_required(login_url="/")
|
||||
|
||||
24
src/apps/drama/migrations/0005_rename_recognition_to_note.py
Normal file
24
src/apps/drama/migrations/0005_rename_recognition_to_note.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import django.conf
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('drama', '0004_recognition'),
|
||||
migrations.swappable_dependency(django.conf.settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel('Recognition', 'Note'),
|
||||
migrations.AlterField(
|
||||
model_name='note',
|
||||
name='user',
|
||||
field=models.ForeignKey(
|
||||
django.conf.settings.AUTH_USER_MODEL,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='notes',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -170,10 +170,10 @@ def record(room, verb, actor=None, **data):
|
||||
return GameEvent.objects.create(room=room, actor=actor, verb=verb, data=data)
|
||||
|
||||
|
||||
class Recognition(models.Model):
|
||||
class Note(models.Model):
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
|
||||
related_name="recognitions",
|
||||
related_name="notes",
|
||||
)
|
||||
slug = models.SlugField(max_length=60)
|
||||
earned_at = models.DateTimeField()
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.test import TestCase
|
||||
from django.db import IntegrityError
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.drama.models import GameEvent, Recognition, ScrollPosition, record
|
||||
from apps.drama.models import GameEvent, Note, ScrollPosition, record
|
||||
from apps.epic.models import Room
|
||||
from apps.lyric.models import User
|
||||
|
||||
@@ -176,44 +176,44 @@ class ScrollPositionModelTest(TestCase):
|
||||
self.assertEqual(ScrollPosition.objects.get(user=self.user, room=self.room).position, 200)
|
||||
|
||||
|
||||
class RecognitionModelTest(TestCase):
|
||||
class NoteModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="earner@test.io")
|
||||
|
||||
def test_can_create_recognition(self):
|
||||
recog = Recognition.objects.create(
|
||||
recog = Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
||||
)
|
||||
self.assertEqual(Recognition.objects.count(), 1)
|
||||
self.assertEqual(Note.objects.count(), 1)
|
||||
self.assertEqual(recog.slug, "stargazer")
|
||||
self.assertEqual(recog.user, self.user)
|
||||
|
||||
def test_palette_is_null_by_default(self):
|
||||
recog = Recognition.objects.create(
|
||||
recog = Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
||||
)
|
||||
self.assertIsNone(recog.palette)
|
||||
|
||||
def test_palette_can_be_set(self):
|
||||
recog = Recognition.objects.create(
|
||||
recog = Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
||||
palette="palette-bardo",
|
||||
)
|
||||
self.assertEqual(recog.palette, "palette-bardo")
|
||||
|
||||
def test_unique_per_user_and_slug(self):
|
||||
Recognition.objects.create(user=self.user, slug="stargazer", earned_at=timezone.now())
|
||||
Note.objects.create(user=self.user, slug="stargazer", earned_at=timezone.now())
|
||||
with self.assertRaises(IntegrityError):
|
||||
Recognition.objects.create(user=self.user, slug="stargazer", earned_at=timezone.now())
|
||||
Note.objects.create(user=self.user, slug="stargazer", earned_at=timezone.now())
|
||||
|
||||
def test_different_users_can_share_slug(self):
|
||||
other = User.objects.create(email="other@test.io")
|
||||
Recognition.objects.create(user=self.user, slug="stargazer", earned_at=timezone.now())
|
||||
Recognition.objects.create(user=other, slug="stargazer", earned_at=timezone.now())
|
||||
self.assertEqual(Recognition.objects.count(), 2)
|
||||
Note.objects.create(user=self.user, slug="stargazer", earned_at=timezone.now())
|
||||
Note.objects.create(user=other, slug="stargazer", earned_at=timezone.now())
|
||||
self.assertEqual(Note.objects.count(), 2)
|
||||
|
||||
def test_str_includes_slug_and_email(self):
|
||||
recog = Recognition.objects.create(
|
||||
recog = Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
||||
)
|
||||
s = str(recog)
|
||||
@@ -221,22 +221,22 @@ class RecognitionModelTest(TestCase):
|
||||
self.assertIn("earner@test.io", s)
|
||||
|
||||
def test_grant_if_new_creates_on_first_call(self):
|
||||
recog, created = Recognition.grant_if_new(self.user, "stargazer")
|
||||
recog, created = Note.grant_if_new(self.user, "stargazer")
|
||||
self.assertTrue(created)
|
||||
self.assertEqual(recog.slug, "stargazer")
|
||||
self.assertIsNotNone(recog.earned_at)
|
||||
|
||||
def test_grant_if_new_is_idempotent(self):
|
||||
Recognition.grant_if_new(self.user, "stargazer")
|
||||
recog, created = Recognition.grant_if_new(self.user, "stargazer")
|
||||
Note.grant_if_new(self.user, "stargazer")
|
||||
recog, created = Note.grant_if_new(self.user, "stargazer")
|
||||
self.assertFalse(created)
|
||||
self.assertEqual(Recognition.objects.count(), 1)
|
||||
self.assertEqual(Note.objects.count(), 1)
|
||||
|
||||
def test_grant_if_new_does_not_overwrite_palette(self):
|
||||
Recognition.objects.create(
|
||||
Note.objects.create(
|
||||
user=self.user, slug="stargazer",
|
||||
earned_at=timezone.now(), palette="palette-bardo",
|
||||
)
|
||||
recog, created = Recognition.grant_if_new(self.user, "stargazer")
|
||||
recog, created = Note.grant_if_new(self.user, "stargazer")
|
||||
self.assertFalse(created)
|
||||
self.assertEqual(recog.palette, "palette-bardo")
|
||||
|
||||
Reference in New Issue
Block a user