- _palettes_for_user: shoptalk→description; "available by default" / "explore to unlock" / "recognized via {Title}"; unlocked_date key carries earned_at ISO for Note-unlocked
- template: data-shoptalk→data-description; data-unlocked-date now holds ISO datetime (was literal "Default")
- dashboard.js: lockText drops "— Default"; dateLine renders "Apr 22, 2026 · 3:05 AM" after Unlocked line
- _palette-picker.scss: tt-shoptalk→tt-description in display:block list; tt-date added
- _tooltips.scss: .tt-date mirrors .tt-expiry w. --priGn colour
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
654 lines
27 KiB
Python
654 lines
27 KiB
Python
import json
|
|
import lxml.html
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
from django.contrib.messages import get_messages
|
|
from django.test import override_settings, TestCase
|
|
from django.urls import reverse
|
|
from django.utils import html, timezone
|
|
|
|
from apps.applets.models import Applet, UserApplet
|
|
from apps.dashboard.forms import (
|
|
DUPLICATE_LINE_ERROR,
|
|
EMPTY_LINE_ERROR,
|
|
)
|
|
from apps.dashboard.models import Line, Post
|
|
from apps.drama.models import Note
|
|
from apps.lyric.models import User
|
|
|
|
|
|
class HomePageTest(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create(email="disco@test.io")
|
|
self.client.force_login(self.user)
|
|
|
|
def test_uses_home_template(self):
|
|
response = self.client.get('/')
|
|
self.assertTemplateUsed(response, 'apps/dashboard/home.html')
|
|
|
|
@override_settings(COMPRESS_ENABLED=False)
|
|
class NewPostTest(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-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_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_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_post", data={"text": ""})
|
|
|
|
def test_for_invalid_input_nothing_saved_to_db(self):
|
|
self.post_invalid_input()
|
|
self.assertEqual(Line.objects.count(), 0)
|
|
|
|
def test_for_invalid_input_renders_billboard_template(self):
|
|
response = self.post_invalid_input()
|
|
self.assertEqual(response.status_code, 200)
|
|
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_LINE_ERROR))
|
|
|
|
@override_settings(COMPRESS_ENABLED=False)
|
|
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):
|
|
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]")
|
|
self.assertIn(url, [form.get("action") for form in forms])
|
|
[form] = [form for form in forms if form.get("action") == url]
|
|
inputs = form.cssselect("input")
|
|
self.assertIn("text", [input.get("name") for input in inputs])
|
|
|
|
def test_displays_only_lines_for_that_post(self):
|
|
# Given/Arrange
|
|
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/post/{correct_post.id}/")
|
|
# Then/Assert
|
|
self.assertContains(response, "itemey 1")
|
|
self.assertContains(response, "itemey 2")
|
|
self.assertNotContains(response, "other post line")
|
|
|
|
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/post/{correct_post.id}/",
|
|
data={"text": "A new line for an existing post"},
|
|
)
|
|
|
|
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_post_view(self):
|
|
other_post = Post.objects.create()
|
|
correct_post = Post.objects.create()
|
|
|
|
response = self.client.post(
|
|
f"/dashboard/post/{correct_post.id}/",
|
|
data={"text": "A new line for an existing post"},
|
|
)
|
|
|
|
self.assertRedirects(response, f"/dashboard/post/{correct_post.id}/")
|
|
|
|
# Post invalid input helper
|
|
def post_invalid_input(self):
|
|
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(Line.objects.count(), 0)
|
|
|
|
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/post.html")
|
|
|
|
def test_for_invalid_input_shows_error_on_page(self):
|
|
response = self.post_invalid_input()
|
|
self.assertContains(response, html.escape(EMPTY_LINE_ERROR))
|
|
|
|
def test_for_invalid_input_sets_is_invalid_class(self):
|
|
response = self.post_invalid_input()
|
|
parsed = lxml.html.fromstring(response.content)
|
|
[input] = parsed.cssselect("input[name=text]")
|
|
self.assertIn("is-invalid", set(input.classes))
|
|
|
|
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/post/{post1.id}/",
|
|
data={"text": "lorem ipsum"},
|
|
)
|
|
|
|
expected_error = html.escape(DUPLICATE_LINE_ERROR)
|
|
self.assertContains(response, expected_error)
|
|
self.assertTemplateUsed(response, "apps/dashboard/post.html")
|
|
self.assertEqual(Line.objects.all().count(), 1)
|
|
|
|
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_posts.html")
|
|
|
|
def test_passes_correct_owner_to_template(self):
|
|
User.objects.create(email="wrongowner@example.com")
|
|
correct_user = User.objects.create(email="a@b.cde")
|
|
self.client.force_login(correct_user)
|
|
response = self.client.get(f"/dashboard/users/{correct_user.id}/")
|
|
self.assertEqual(response.context["owner"], correct_user)
|
|
|
|
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_post", data={"text": "new line"})
|
|
new_post = Post.objects.get()
|
|
self.assertEqual(new_post.owner, user)
|
|
|
|
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_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}/")
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
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/post/{our_post.id}/share_post",
|
|
data={"recipient": "alice@example.com"},
|
|
)
|
|
self.assertRedirects(response, f"/dashboard/post/{our_post.id}/")
|
|
|
|
def test_post_with_email_adds_user_to_shared_with(self):
|
|
our_post = Post.objects.create()
|
|
alice = User.objects.create(email="alice@example.com")
|
|
self.client.post(
|
|
f"/dashboard/post/{our_post.id}/share_post",
|
|
data={"recipient": "alice@example.com"},
|
|
)
|
|
self.assertIn(alice, our_post.shared_with.all())
|
|
|
|
def test_post_with_nonexistent_email_redirects_to_post(self):
|
|
our_post = Post.objects.create()
|
|
response = self.client.post(
|
|
f"/dashboard/post/{our_post.id}/share_post",
|
|
data={"recipient": "nobody@example.com"},
|
|
)
|
|
self.assertRedirects(
|
|
response,
|
|
f"/dashboard/post/{our_post.id}/",
|
|
fetch_redirect_response=False,
|
|
)
|
|
|
|
def test_share_post_does_not_add_owner_as_recipient(self):
|
|
owner = User.objects.create(email="owner@example.com")
|
|
our_post = Post.objects.create(owner=owner)
|
|
self.client.force_login(owner)
|
|
self.client.post(reverse("share_post", args=[our_post.id]),
|
|
data={"recipient": "owner@example.com"})
|
|
self.assertNotIn(owner, our_post.shared_with.all())
|
|
|
|
@override_settings(MESSAGE_STORAGE='django.contrib.messages.storage.session.SessionStorage')
|
|
def test_share_post_shows_privacy_safe_message(self):
|
|
our_post = Post.objects.create()
|
|
response = self.client.post(
|
|
f"/dashboard/post/{our_post.id}/share_post",
|
|
data={"recipient": "nobody@example.com"},
|
|
follow=True,
|
|
)
|
|
messages = list(get_messages(response.wsgi_request))
|
|
self.assertEqual(
|
|
str(messages[0]),
|
|
"An invite has been sent if that address is registered.",
|
|
)
|
|
|
|
class ViewAuthPostTest(TestCase):
|
|
def setUp(self):
|
|
self.owner = User.objects.create(email="disco@example.com")
|
|
self.our_post = Post.objects.create(owner=self.owner)
|
|
|
|
def test_anonymous_user_is_redirected(self):
|
|
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_post", args=[self.our_post.id]))
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
def test_shared_with_user_can_access_post(self):
|
|
guest = User.objects.create(email="guest@example.com")
|
|
self.our_post.shared_with.add(guest)
|
|
self.client.force_login(guest)
|
|
response = self.client.get(reverse("view_post", args=[self.our_post.id]))
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
@override_settings(COMPRESS_ENABLED=False)
|
|
class SetPaletteTest(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create(email="a@b.cde")
|
|
self.client.force_login(self.user)
|
|
self.url = reverse("home")
|
|
Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"})
|
|
|
|
def test_anonymous_user_is_redirected_home(self):
|
|
response = self.client.post("/dashboard/set_palette")
|
|
self.assertRedirects(response, "/", fetch_redirect_response=False)
|
|
|
|
def test_set_palette_updates_user_palette(self):
|
|
User.objects.filter(pk=self.user.pk).update(palette="palette-sheol")
|
|
self.client.post("/dashboard/set_palette", data={"palette": "palette-default"})
|
|
self.user.refresh_from_db()
|
|
self.assertEqual(self.user.palette, "palette-default")
|
|
|
|
def test_locked_palette_is_rejected(self):
|
|
response = self.client.post("/dashboard/set_palette", data={"palette": "palette-bardo"})
|
|
self.user.refresh_from_db()
|
|
self.assertEqual(self.user.palette, "palette-default")
|
|
self.assertRedirects(response, "/", fetch_redirect_response=False)
|
|
|
|
def test_set_palette_redirects_home(self):
|
|
response = self.client.post("/dashboard/set_palette", data={"palette": "palette-default"})
|
|
self.assertRedirects(response, "/", fetch_redirect_response=False)
|
|
|
|
def test_set_palette_returns_json_when_requested(self):
|
|
response = self.client.post(
|
|
"/dashboard/set_palette",
|
|
data={"palette": "palette-cedar"},
|
|
headers={"Accept": "application/json"},
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response.json(), {"palette": "palette-cedar"})
|
|
|
|
def test_locked_palette_returns_unchanged_json(self):
|
|
response = self.client.post(
|
|
"/dashboard/set_palette",
|
|
data={"palette": "palette-bardo"},
|
|
headers={"Accept": "application/json"},
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response.json(), {"palette": "palette-default"})
|
|
|
|
def test_unlocked_swatches_count_matches_context(self):
|
|
response = self.client.get(self.url)
|
|
parsed = lxml.html.fromstring(response.content)
|
|
swatches = parsed.cssselect(".swatch:not(.locked)")
|
|
unlocked = [p for p in response.context["palettes"] if not p["locked"]]
|
|
self.assertEqual(len(swatches), len(unlocked))
|
|
|
|
def test_active_palette_swatch_has_active_class(self):
|
|
response = self.client.get(self.url)
|
|
parsed = lxml.html.fromstring(response.content)
|
|
[active] = parsed.cssselect(".swatch.active")
|
|
self.assertIn("palette-default", active.classes)
|
|
|
|
def test_locked_palettes_are_not_forms(self):
|
|
response = self.client.get(self.url)
|
|
parsed = lxml.html.fromstring(response.content)
|
|
locked = parsed.cssselect(".swatch.locked")
|
|
expected_locked = [p for p in response.context["palettes"] if p["locked"]]
|
|
self.assertEqual(len(locked), len(expected_locked))
|
|
# they mustn't be button els
|
|
for swatch in locked:
|
|
self.assertNotEqual(swatch.tag, "button")
|
|
|
|
def test_palette_picker_count_matches_context(self):
|
|
response = self.client.get(self.url)
|
|
parsed = lxml.html.fromstring(response.content)
|
|
swatches = parsed.cssselect(".swatch")
|
|
self.assertEqual(len(swatches), len(response.context["palettes"]))
|
|
|
|
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_note_palette_unlocks_swatch_in_context(self):
|
|
Note.objects.create(
|
|
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
|
palette="palette-bardo",
|
|
)
|
|
response = self.client.get("/")
|
|
palettes = response.context["palettes"]
|
|
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
|
|
self.assertFalse(bardo["locked"])
|
|
|
|
def test_note_palette_description_contains_note_title(self):
|
|
Note.objects.create(
|
|
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
|
palette="palette-bardo",
|
|
)
|
|
response = self.client.get("/")
|
|
palettes = response.context["palettes"]
|
|
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
|
|
self.assertIn("Stargazer", bardo["description"])
|
|
|
|
def test_default_palette_description_is_available_by_default(self):
|
|
response = self.client.get("/")
|
|
palettes = response.context["palettes"]
|
|
default = next(p for p in palettes if p["name"] == "palette-default")
|
|
self.assertEqual(default["description"], "available by default")
|
|
|
|
def test_locked_palette_description_is_explore_to_unlock(self):
|
|
response = self.client.get("/")
|
|
palettes = response.context["palettes"]
|
|
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
|
|
self.assertEqual(bardo["description"], "explore to unlock")
|
|
|
|
def test_note_palette_description_is_recognized_via(self):
|
|
Note.objects.create(
|
|
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
|
palette="palette-bardo",
|
|
)
|
|
response = self.client.get("/")
|
|
palettes = response.context["palettes"]
|
|
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
|
|
self.assertEqual(bardo["description"], "recognized via Stargazer")
|
|
|
|
def test_note_palette_entry_includes_unlocked_date_iso(self):
|
|
earned = timezone.now()
|
|
Note.objects.create(
|
|
user=self.user, slug="stargazer", earned_at=earned,
|
|
palette="palette-bardo",
|
|
)
|
|
response = self.client.get("/")
|
|
palettes = response.context["palettes"]
|
|
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
|
|
self.assertIn("unlocked_date", bardo)
|
|
self.assertEqual(bardo["unlocked_date"], earned.isoformat())
|
|
|
|
def test_note_without_palette_field_keeps_swatch_locked(self):
|
|
Note.objects.create(
|
|
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
|
palette=None,
|
|
)
|
|
response = self.client.get("/")
|
|
palettes = response.context["palettes"]
|
|
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
|
|
self.assertTrue(bardo["locked"])
|
|
|
|
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",
|
|
)
|
|
self.client.post("/dashboard/set_palette", data={"palette": "palette-bardo"})
|
|
self.user.refresh_from_db()
|
|
self.assertEqual(self.user.palette, "palette-bardo")
|
|
|
|
|
|
@override_settings(COMPRESS_ENABLED=False)
|
|
class ProfileViewTest(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create(email="discoman@example.com")
|
|
self.client.force_login(self.user)
|
|
|
|
def test_post_username_saves_to_user(self):
|
|
self.client.post("/dashboard/set_profile", data={"username": "discoman"})
|
|
self.user.refresh_from_db()
|
|
self.assertEqual(self.user.username, "discoman")
|
|
|
|
def test_post_username_requires_login(self):
|
|
self.client.logout()
|
|
response = self.client.post("/dashboard/set_profile", data={"username": "somnambulist"})
|
|
self.assertRedirects(response, "/?next=/dashboard/set_profile", fetch_redirect_response=False)
|
|
|
|
def test_dash_renders_username_applet(self):
|
|
response = self.client.get("/")
|
|
parsed = lxml.html.fromstring(response.content)
|
|
[applet] = parsed.cssselect("#id_applet_username")
|
|
self.assertIn("@", applet.text_content())
|
|
[input_el] = parsed.cssselect("#id_new_username")
|
|
self.assertEqual("", input_el.get("value"))
|
|
|
|
def test_dash_shows_display_name_in_applet(self):
|
|
self.user.username = "discoman"
|
|
self.user.save()
|
|
response = self.client.get("/")
|
|
parsed = lxml.html.fromstring(response.content)
|
|
[username_input] = parsed.cssselect("#id_new_username")
|
|
self.assertEqual("discoman", username_input.get("value"))
|
|
|
|
class ToggleDashAppletsViewTest(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create(email="disco@test.io")
|
|
self.client.force_login(self.user)
|
|
self.username_applet, _ = Applet.objects.get_or_create(slug="username", defaults={"name": "Username"})
|
|
self.palette_applet, _ = Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"})
|
|
self.url = reverse("toggle_applets")
|
|
|
|
def test_unauthenticated_user_is_redirected(self):
|
|
self.client.logout()
|
|
response = self.client.post(self.url)
|
|
self.assertRedirects(
|
|
response, f"/?next={self.url}", fetch_redirect_response=False
|
|
)
|
|
|
|
def test_unchecked_applet_gets_user_applet_with_visible_false(self):
|
|
self.client.post(self.url, {"applets": ["username"]})
|
|
ua = UserApplet.objects.get(user=self.user, applet=self.palette_applet)
|
|
self.assertFalse(ua.visible)
|
|
|
|
def test_redirects_on_normal_post(self):
|
|
response = self.client.post(
|
|
self.url, {"applets": ["username", "palette"]}
|
|
)
|
|
self.assertRedirects(response, reverse("home"), fetch_redirect_response=False)
|
|
|
|
def test_returns_200_on_htmx_post(self):
|
|
response = self.client.post(
|
|
self.url,
|
|
{"applets": ["username", "palette"]},
|
|
HTTP_HX_REQUEST="true",
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_htmx_post_renders_visible_applets_only(self):
|
|
response = self.client.post(
|
|
self.url,
|
|
{"applets": ["username"]},
|
|
HTTP_HX_REQUEST="true",
|
|
)
|
|
parsed = lxml.html.fromstring(response.content)
|
|
self.assertEqual(len(parsed.cssselect("#id_applet_username")), 1)
|
|
self.assertEqual(len(parsed.cssselect("#id_applet_palette")), 0)
|
|
|
|
def test_toggle_applets_does_not_affect_gameboard_applets(self):
|
|
game_applet, _ = Applet.objects.get_or_create(
|
|
slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
|
|
)
|
|
self.client.post(self.url, {"applets": ["username", "palette"]})
|
|
self.assertFalse(UserApplet.objects.filter(user=self.user, applet=game_applet). exists())
|
|
|
|
class AppletVisibilityContextTest(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create(email="disco@test.io")
|
|
self.client.force_login(self.user)
|
|
self.username_applet, _ = Applet.objects.get_or_create(slug="username", defaults={"name": "Username"})
|
|
self.palette_applet, _ = Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"})
|
|
UserApplet.objects.create(user=self.user, applet=self.palette_applet, visible=False)
|
|
|
|
def test_dash_reflects_user_applet_visibility(self):
|
|
response = self.client.get("/")
|
|
applet_map = {entry["applet"].slug: entry["visible"] for entry in response.context["applets"]}
|
|
self.assertFalse(applet_map["palette"])
|
|
self.assertTrue(applet_map["username"])
|
|
|
|
class FooterNavTest(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create(email="disco@test.io")
|
|
self.client.force_login(self.user)
|
|
|
|
def test_footer_nav_present_on_dashboard(self):
|
|
response = self.client.get("/")
|
|
parsed = lxml.html.fromstring(response.content)
|
|
[nav] = parsed.cssselect("#id_footer_nav")
|
|
self.assertIsNotNone(nav)
|
|
|
|
def test_footer_nav_has_dashboard_link(self):
|
|
response = self.client.get("/")
|
|
parsed = lxml.html.fromstring(response.content)
|
|
[nav] = parsed.cssselect("#id_footer_nav")
|
|
links = [a.get("href") for a in nav.cssselect("a")]
|
|
self.assertIn("/", links)
|
|
|
|
def test_footer_nav_has_gameboard_link(self):
|
|
response = self.client.get("/")
|
|
parsed = lxml.html.fromstring(response.content)
|
|
[nav] = parsed.cssselect("#id_footer_nav")
|
|
links = [a.get("href") for a in nav.cssselect("a")]
|
|
self.assertIn("/gameboard/", links)
|
|
|
|
class WalletAppletTest(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="wallet", defaults={"name": "Wallet"})
|
|
response = self.client.get("/")
|
|
self.parsed = lxml.html.fromstring(response.content)
|
|
|
|
def test_wallet_applet_present_on_dash(self):
|
|
[_] = self.parsed.cssselect("#id_applet_wallet")
|
|
|
|
def test_wallet_applet_has_manage_link(self):
|
|
[link] = self.parsed.cssselect("#id_applet_wallet a.wallet-manage-link")
|
|
self.assertEqual(link.get("href"), "/dashboard/wallet/")
|
|
|
|
|
|
ENRICHED_CHART = {
|
|
"planets": {"Sun": {"lon": 10.0, "sign": "Aries", "house": 1, "degree": 10.0}},
|
|
"houses": {"cusps": [float(i * 30) for i in range(12)]},
|
|
"elements": {
|
|
"Fire": {"count": 3, "contributors": ["Sun", "Mars", "Jupiter"]},
|
|
"Stone": {"count": 1, "contributors": ["Venus"]},
|
|
"Air": {"count": 2, "contributors": ["Mercury", "Uranus"]},
|
|
"Water": {"count": 0, "contributors": []},
|
|
"Time": {"count": 1, "stellia": ["Saturn"]},
|
|
"Space": {"count": 1, "parades": ["Neptune"]},
|
|
},
|
|
"distinctions": [],
|
|
"timezone": "UTC",
|
|
}
|
|
|
|
BIRTH_PAYLOAD = {
|
|
"birth_dt": "1990-06-15T12:00:00Z",
|
|
"birth_lat": 51.5,
|
|
"birth_lon": -0.1,
|
|
"birth_place": "London",
|
|
"house_system": "O",
|
|
"chart_data": {"stale": True},
|
|
}
|
|
|
|
|
|
@override_settings(PYSWISS_URL="http://pyswiss-test")
|
|
class SkySaveViewTest(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create(
|
|
email="disco@test.io",
|
|
sky_birth_lat=51.5,
|
|
sky_birth_lon=-0.1,
|
|
sky_birth_place="London",
|
|
)
|
|
self.client.force_login(self.user)
|
|
|
|
def _post(self, payload=None):
|
|
return self.client.post(
|
|
"/dashboard/sky/save",
|
|
data=json.dumps(payload or BIRTH_PAYLOAD),
|
|
content_type="application/json",
|
|
)
|
|
|
|
def test_save_stores_client_chart_data(self):
|
|
"""sky_save stores the chart_data from the client (already enriched by sky_preview)."""
|
|
client_chart = {
|
|
"planets": {},
|
|
"houses": {"cusps": [float(i * 30) for i in range(12)], "asc": 123.4, "mc": 45.6},
|
|
"elements": {"Fire": {"count": 1, "contributors": []}},
|
|
}
|
|
payload = dict(BIRTH_PAYLOAD, chart_data=client_chart)
|
|
|
|
response = self._post(payload)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.user.refresh_from_db()
|
|
self.assertAlmostEqual(self.user.sky_chart_data["houses"]["asc"], 123.4)
|
|
|
|
|
|
class SkyNatusDataViewTest(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create(email="disco@test.io")
|
|
self.client.force_login(self.user)
|
|
|
|
def test_returns_stored_chart_with_asc_preserved(self):
|
|
"""sky_natus_data returns sky_chart_data — asc must match what was saved."""
|
|
stored = {
|
|
"planets": {},
|
|
"houses": {"cusps": [float(i * 30) for i in range(12)], "asc": 236.1, "mc": 159.1},
|
|
"elements": {"Fire": {"count": 1, "contributors": []}},
|
|
"aspects": [],
|
|
"house_system": "O",
|
|
}
|
|
self.user.sky_chart_data = stored
|
|
self.user.save(update_fields=["sky_chart_data"])
|
|
|
|
response = self.client.get("/dashboard/sky/data")
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
data = json.loads(response.content)
|
|
self.assertAlmostEqual(data["houses"]["asc"], 236.1)
|
|
self.assertIn("distinctions", data)
|
|
|
|
def test_returns_404_if_no_chart_data_saved(self):
|
|
response = self.client.get("/dashboard/sky/data")
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
def test_requires_login(self):
|
|
self.client.logout()
|
|
response = self.client.get("/dashboard/sky/data")
|
|
self.assertRedirects(response, "/?next=/dashboard/sky/data", fetch_redirect_response=False)
|