Files
python-tdd/src/apps/drama/tests/integrated/test_models.py

330 lines
15 KiB
Python
Raw Normal View History

from django.test import TestCase
from django.db import IntegrityError
from django.utils import timezone
from apps.drama.models import GameEvent, Note, ScrollPosition, record
from apps.epic.models import Room
from apps.lyric.models import User
class GameEventModelTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="actor@test.io")
self.room = Room.objects.create(name="Test Room", owner=self.user)
def test_record_creates_game_event(self):
event = record(self.room, GameEvent.SLOT_FILLED, actor=self.user, slot_number=1, token_type="tithe")
self.assertEqual(GameEvent.objects.count(), 1)
self.assertEqual(event.room, self.room)
self.assertEqual(event.actor, self.user)
self.assertEqual(event.verb, GameEvent.SLOT_FILLED)
self.assertEqual(event.data, {"slot_number": 1, "token_type": "tithe"})
def test_record_without_actor(self):
event = record(self.room, GameEvent.ROOM_CREATED)
self.assertIsNone(event.actor)
self.assertEqual(event.verb, GameEvent.ROOM_CREATED)
def test_events_ordered_by_timestamp(self):
record(self.room, GameEvent.ROOM_CREATED)
record(self.room, GameEvent.SLOT_RESERVED, actor=self.user)
record(self.room, GameEvent.SLOT_FILLED, actor=self.user)
verbs = list(GameEvent.objects.values_list("verb", flat=True))
self.assertEqual(verbs, [
GameEvent.ROOM_CREATED,
GameEvent.SLOT_RESERVED,
GameEvent.SLOT_FILLED,
])
def test_str_includes_actor_and_verb(self):
event = record(self.room, GameEvent.ROLE_SELECTED, actor=self.user, role="PC")
self.assertIn("actor@test.io", str(event))
self.assertIn(GameEvent.ROLE_SELECTED, str(event))
# ── to_prose — ROLE_SELECTED ──────────────────────────────────────────
def test_role_selected_prose_uses_ordinal_chair(self):
for role, ordinal in [("PC", "1st"), ("NC", "2nd"), ("EC", "3rd"),
("SC", "4th"), ("AC", "5th"), ("BC", "6th")]:
with self.subTest(role=role):
event = record(self.room, GameEvent.ROLE_SELECTED, actor=self.user,
role=role, role_display="")
self.assertIn(f"assumes {ordinal} Chair", event.to_prose())
def test_role_selected_prose_includes_role_name(self):
event = record(self.room, GameEvent.ROLE_SELECTED, actor=self.user,
role="PC", role_display="Player")
prose = event.to_prose()
self.assertIn("Player", prose)
pronouns: per-user pronouns ideology + Pronouns applet on Game Kit; provenance prose uses actor.pronouns at render time — TDD - User.pronouns CharField w. choices=[pluralism (default), bawlmorese, misogyny, misandry, misanthropy] + pronoun_subj/obj/poss properties; PRONOUN_TABLE single source of truth in apps.lyric.models; mig 0002_user_pronouns - drama.GameEvent.to_prose() drops module-level PRONOUN_* constants; SIG_READY/SIG_UNREADY/ROLE_SELECTED now resolve poss/subj from self.actor.pronouns at render time, so flipping a user's preference rewrites all their existing scroll prose; default actor → "their" - SIG_READY prose strips a leading "The " from card_name so "the The Wanderer" reads "the Wanderer" and "the Engraven The Nomad" reads "the Engraven Nomad"; minor arcana ("Maid of Brands") untouched - new applets/0005 seeds 'pronouns' applet (3x3, game-kit, default visible); _game_kit_sections.html grows a #id_gk_pronouns block w. 5 .gk-pronoun-card items labeled by ideology slug (italic) and tagged data-pronoun + data-trio - card click → window.showGuard(card, "Set pronoun preference?<span class='guard-pronoun-trio'>{trio}</span>", commitCb); on OK fetches POST /dashboard/set-pronouns w. CSRF cookie + reloads so .active class moves and provenance prose re-renders; NVM dismisses - dashboard.set_pronouns view (POST-only, login_required, 204/400/405) at /dashboard/set-pronouns; rejects choices not in PRONOUN_TABLE - _game-kit.scss extends shared card rule to .gk-pronoun-card w. .active fill state + italic ideology label; #id_guard_portal .guard-pronoun-trio styled small/dim/centered under the question - billscroll aperture: padding-right 0.75rem on #id_drama_scroll inside .applet-scroll so the timestamp column no longer sits beneath the scrollbar Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:11:40 -04:00
# Default user pronouns = pluralism → "they".
self.assertIn("they will start the game", prose)
# ── to_prose — SIG_READY ─────────────────────────────────────────────
def test_sig_ready_prose_embodies_card_with_rank_and_icon(self):
event = record(self.room, GameEvent.SIG_READY, actor=self.user,
card_name="Maid of Brands", corner_rank="M",
suit_icon="fa-wand-sparkles")
prose = event.to_prose()
pronouns: per-user pronouns ideology + Pronouns applet on Game Kit; provenance prose uses actor.pronouns at render time — TDD - User.pronouns CharField w. choices=[pluralism (default), bawlmorese, misogyny, misandry, misanthropy] + pronoun_subj/obj/poss properties; PRONOUN_TABLE single source of truth in apps.lyric.models; mig 0002_user_pronouns - drama.GameEvent.to_prose() drops module-level PRONOUN_* constants; SIG_READY/SIG_UNREADY/ROLE_SELECTED now resolve poss/subj from self.actor.pronouns at render time, so flipping a user's preference rewrites all their existing scroll prose; default actor → "their" - SIG_READY prose strips a leading "The " from card_name so "the The Wanderer" reads "the Wanderer" and "the Engraven The Nomad" reads "the Engraven Nomad"; minor arcana ("Maid of Brands") untouched - new applets/0005 seeds 'pronouns' applet (3x3, game-kit, default visible); _game_kit_sections.html grows a #id_gk_pronouns block w. 5 .gk-pronoun-card items labeled by ideology slug (italic) and tagged data-pronoun + data-trio - card click → window.showGuard(card, "Set pronoun preference?<span class='guard-pronoun-trio'>{trio}</span>", commitCb); on OK fetches POST /dashboard/set-pronouns w. CSRF cookie + reloads so .active class moves and provenance prose re-renders; NVM dismisses - dashboard.set_pronouns view (POST-only, login_required, 204/400/405) at /dashboard/set-pronouns; rejects choices not in PRONOUN_TABLE - _game-kit.scss extends shared card rule to .gk-pronoun-card w. .active fill state + italic ideology label; #id_guard_portal .guard-pronoun-trio styled small/dim/centered under the question - billscroll aperture: padding-right 0.75rem on #id_drama_scroll inside .applet-scroll so the timestamp column no longer sits beneath the scrollbar Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:11:40 -04:00
# Default user pronouns = pluralism → "their".
self.assertIn("embodies as their Significator the Maid of Brands", prose)
self.assertIn("(M", prose)
self.assertIn("fa-wand-sparkles", prose)
def test_sig_ready_prose_omits_icon_when_none(self):
event = record(self.room, GameEvent.SIG_READY, actor=self.user,
card_name="The Wanderer", corner_rank="0", suit_icon="")
prose = event.to_prose()
pronouns: per-user pronouns ideology + Pronouns applet on Game Kit; provenance prose uses actor.pronouns at render time — TDD - User.pronouns CharField w. choices=[pluralism (default), bawlmorese, misogyny, misandry, misanthropy] + pronoun_subj/obj/poss properties; PRONOUN_TABLE single source of truth in apps.lyric.models; mig 0002_user_pronouns - drama.GameEvent.to_prose() drops module-level PRONOUN_* constants; SIG_READY/SIG_UNREADY/ROLE_SELECTED now resolve poss/subj from self.actor.pronouns at render time, so flipping a user's preference rewrites all their existing scroll prose; default actor → "their" - SIG_READY prose strips a leading "The " from card_name so "the The Wanderer" reads "the Wanderer" and "the Engraven The Nomad" reads "the Engraven Nomad"; minor arcana ("Maid of Brands") untouched - new applets/0005 seeds 'pronouns' applet (3x3, game-kit, default visible); _game_kit_sections.html grows a #id_gk_pronouns block w. 5 .gk-pronoun-card items labeled by ideology slug (italic) and tagged data-pronoun + data-trio - card click → window.showGuard(card, "Set pronoun preference?<span class='guard-pronoun-trio'>{trio}</span>", commitCb); on OK fetches POST /dashboard/set-pronouns w. CSRF cookie + reloads so .active class moves and provenance prose re-renders; NVM dismisses - dashboard.set_pronouns view (POST-only, login_required, 204/400/405) at /dashboard/set-pronouns; rejects choices not in PRONOUN_TABLE - _game-kit.scss extends shared card rule to .gk-pronoun-card w. .active fill state + italic ideology label; #id_guard_portal .guard-pronoun-trio styled small/dim/centered under the question - billscroll aperture: padding-right 0.75rem on #id_drama_scroll inside .applet-scroll so the timestamp column no longer sits beneath the scrollbar Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:11:40 -04:00
# Trump card: leading "The " is stripped so qualifier (if any) butts up
# against the proper name and "the The Wanderer" never reads doubled.
self.assertIn("embodies as their Significator the Wanderer (0)", prose)
self.assertNotIn("the The Wanderer", prose)
self.assertNotIn("fa-", prose)
pronouns: per-user pronouns ideology + Pronouns applet on Game Kit; provenance prose uses actor.pronouns at render time — TDD - User.pronouns CharField w. choices=[pluralism (default), bawlmorese, misogyny, misandry, misanthropy] + pronoun_subj/obj/poss properties; PRONOUN_TABLE single source of truth in apps.lyric.models; mig 0002_user_pronouns - drama.GameEvent.to_prose() drops module-level PRONOUN_* constants; SIG_READY/SIG_UNREADY/ROLE_SELECTED now resolve poss/subj from self.actor.pronouns at render time, so flipping a user's preference rewrites all their existing scroll prose; default actor → "their" - SIG_READY prose strips a leading "The " from card_name so "the The Wanderer" reads "the Wanderer" and "the Engraven The Nomad" reads "the Engraven Nomad"; minor arcana ("Maid of Brands") untouched - new applets/0005 seeds 'pronouns' applet (3x3, game-kit, default visible); _game_kit_sections.html grows a #id_gk_pronouns block w. 5 .gk-pronoun-card items labeled by ideology slug (italic) and tagged data-pronoun + data-trio - card click → window.showGuard(card, "Set pronoun preference?<span class='guard-pronoun-trio'>{trio}</span>", commitCb); on OK fetches POST /dashboard/set-pronouns w. CSRF cookie + reloads so .active class moves and provenance prose re-renders; NVM dismisses - dashboard.set_pronouns view (POST-only, login_required, 204/400/405) at /dashboard/set-pronouns; rejects choices not in PRONOUN_TABLE - _game-kit.scss extends shared card rule to .gk-pronoun-card w. .active fill state + italic ideology label; #id_guard_portal .guard-pronoun-trio styled small/dim/centered under the question - billscroll aperture: padding-right 0.75rem on #id_drama_scroll inside .applet-scroll so the timestamp column no longer sits beneath the scrollbar Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:11:40 -04:00
def test_sig_ready_prose_strips_leading_the_on_qualified_trump(self):
# Trump card with a levity/gravity qualifier already pre-pended in
# card_name: must read "the Engraven Nomad", not "the Engraven The Nomad".
# Default actor pronouns = pluralism → "their".
event = record(self.room, GameEvent.SIG_READY, actor=self.user,
card_name="Engraven The Nomad", corner_rank="0", suit_icon="")
prose = event.to_prose()
self.assertIn("embodies as their Significator the Engraven Nomad (0)", prose)
self.assertNotIn("Engraven The Nomad", prose)
def test_sig_ready_prose_uses_actor_pronouns_at_render_time(self):
# Bawlmorese actor → "yos"; default actor → "their"; switching
# the actor's pronouns updates ALL their existing prose on next render.
self.user.pronouns = "bawlmorese"
self.user.save(update_fields=["pronouns"])
event = record(self.room, GameEvent.SIG_READY, actor=self.user,
card_name="Maid of Brands", corner_rank="M",
suit_icon="fa-wand-sparkles")
self.assertIn("embodies as yos Significator the Maid of Brands", event.to_prose())
self.user.pronouns = "misogyny"
self.user.save(update_fields=["pronouns"])
# Re-fetch — the related object cache may hold the stale pronouns.
event.refresh_from_db()
self.assertIn("embodies as his Significator the Maid of Brands", event.to_prose())
def test_sig_unready_prose_uses_actor_pronouns(self):
self.user.pronouns = "misandry"
self.user.save(update_fields=["pronouns"])
event = record(self.room, GameEvent.SIG_UNREADY, actor=self.user)
self.assertIn("disembodies hers Significator", event.to_prose())
def test_role_selected_prose_uses_actor_pronouns(self):
self.user.pronouns = "misanthropy"
self.user.save(update_fields=["pronouns"])
event = record(self.room, GameEvent.ROLE_SELECTED, actor=self.user,
role="PC", role_display="Player")
self.assertIn("it will start the game", event.to_prose())
SAVE SKY provenance + sky→hex (not sky→sea) transition — TDD - drama.GameEvent.SKY_SAVED verb + to_prose branch: "X beholds the skyscape of {poss} birth, which yields {obj} a unique {Cap} capacity."; tied highest scores switch "a unique" → "equal", join w. "and" (2-way) or Oxford comma (3+), and pluralize "capacity" → "capacities"; pronouns resolved from actor.pronouns at render time, same machinery as SIG_READY/ROLE_SELECTED - epic.utils.ELEMENT_CAPACITOR_NAMES + ELEMENT_ORDER + top_capacitors(elements) helper: maps Fire→Ardor Stone→Ossum Time→Tempo Space→Nexus Air→Pneuma Water→Humor; tolerates both flat-int and enriched-dict (`{count, contributors}`) chart_data shapes; returns capacitor names tied for highest count, ordered by canonical wheel ring - epic.natus_save: on action=confirm, records GameEvent.SKY_SAVED w. top_capacitors=[…] before _notify_sky_confirmed; per-room billscroll AND billboard Most Recent Scroll pick up the new prose - _natus_overlay.html _onSkyConfirmed: removed sea-partial fetch+inject; now calls closeNatus() + window.location.reload() so the gamer lands on the table hex w. the PICK SKY → PICK SEA btn swap (server-side, driven by sky_confirmed=True), then opts into the sea overlay manually. The auto-launch via 39e12d6 was buried by FTs that were pinning the wrong contract — gamer never had a chance to witness PICK SEA on the hex - test_room_sea_select.py: three FTs renamed/rewired from auto-launch assertions (sea_overlay_appears_without_page_refresh, natus_overlay_not_visible_after_sky_confirm, sea_open_class_on_html_after_confirm) to (pick_sea_btn_visible_after_sky_confirm, natus_overlay_closed_after_sky_confirm, clicking_pick_sea_btn_opens_sea_overlay) — sea overlay now requires explicit PICK SEA click Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:57:35 -04:00
# ── to_prose — SKY_SAVED ──────────────────────────────────────────────
def test_sky_saved_prose_single_capacitor(self):
# Default user pronouns = pluralism → "their" + "them".
event = record(self.room, GameEvent.SKY_SAVED, actor=self.user,
top_capacitors=["Ardor"])
prose = event.to_prose()
self.assertIn(
"beholds the skyscape of their birth, which yields them a unique Ardor capacity.",
prose,
)
def test_sky_saved_prose_two_way_tie(self):
# Tied highest scores: pluralize "a unique" → "equal", join w. "and",
# pluralize "capacity" → "capacities".
event = record(self.room, GameEvent.SKY_SAVED, actor=self.user,
top_capacitors=["Ardor", "Ossum"])
prose = event.to_prose()
self.assertIn(
"beholds the skyscape of their birth, which yields them equal Ardor and Ossum capacities.",
prose,
)
def test_sky_saved_prose_three_way_tie_uses_oxford_comma(self):
event = record(self.room, GameEvent.SKY_SAVED, actor=self.user,
top_capacitors=["Ardor", "Ossum", "Pneuma"])
prose = event.to_prose()
self.assertIn(
"beholds the skyscape of their birth, which yields them equal Ardor, Ossum, and Pneuma capacities.",
prose,
)
def test_sky_saved_prose_uses_actor_pronouns(self):
self.user.pronouns = "bawlmorese"
self.user.save(update_fields=["pronouns"])
event = record(self.room, GameEvent.SKY_SAVED, actor=self.user,
top_capacitors=["Tempo"])
# Bawlmorese → poss "yos", obj "yo".
self.assertIn(
"beholds the skyscape of yos birth, which yields yo a unique Tempo capacity.",
event.to_prose(),
)
def test_sig_ready_prose_degrades_without_corner_rank(self):
# Old events recorded before this change have no corner_rank key
event = record(self.room, GameEvent.SIG_READY, actor=self.user,
card_name="Maid of Brands")
prose = event.to_prose()
pronouns: per-user pronouns ideology + Pronouns applet on Game Kit; provenance prose uses actor.pronouns at render time — TDD - User.pronouns CharField w. choices=[pluralism (default), bawlmorese, misogyny, misandry, misanthropy] + pronoun_subj/obj/poss properties; PRONOUN_TABLE single source of truth in apps.lyric.models; mig 0002_user_pronouns - drama.GameEvent.to_prose() drops module-level PRONOUN_* constants; SIG_READY/SIG_UNREADY/ROLE_SELECTED now resolve poss/subj from self.actor.pronouns at render time, so flipping a user's preference rewrites all their existing scroll prose; default actor → "their" - SIG_READY prose strips a leading "The " from card_name so "the The Wanderer" reads "the Wanderer" and "the Engraven The Nomad" reads "the Engraven Nomad"; minor arcana ("Maid of Brands") untouched - new applets/0005 seeds 'pronouns' applet (3x3, game-kit, default visible); _game_kit_sections.html grows a #id_gk_pronouns block w. 5 .gk-pronoun-card items labeled by ideology slug (italic) and tagged data-pronoun + data-trio - card click → window.showGuard(card, "Set pronoun preference?<span class='guard-pronoun-trio'>{trio}</span>", commitCb); on OK fetches POST /dashboard/set-pronouns w. CSRF cookie + reloads so .active class moves and provenance prose re-renders; NVM dismisses - dashboard.set_pronouns view (POST-only, login_required, 204/400/405) at /dashboard/set-pronouns; rejects choices not in PRONOUN_TABLE - _game-kit.scss extends shared card rule to .gk-pronoun-card w. .active fill state + italic ideology label; #id_guard_portal .guard-pronoun-trio styled small/dim/centered under the question - billscroll aperture: padding-right 0.75rem on #id_drama_scroll inside .applet-scroll so the timestamp column no longer sits beneath the scrollbar Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:11:40 -04:00
# Default user pronouns = pluralism → "their".
self.assertIn("embodies as their Significator the Maid of Brands", prose)
self.assertNotIn("(", prose)
def test_str_without_actor_shows_system(self):
event = record(self.room, GameEvent.ROLES_REVEALED)
self.assertIn("system", str(event))
# ── to_prose — remaining verb branches ───────────────────────────────
def test_slot_reserved_prose(self):
event = record(self.room, GameEvent.SLOT_RESERVED, actor=self.user)
self.assertEqual(event.to_prose(), "reserves a seat")
def test_slot_returned_prose(self):
event = record(self.room, GameEvent.SLOT_RETURNED, actor=self.user)
self.assertEqual(event.to_prose(), "withdraws from the gate")
def test_slot_released_prose_includes_slot_number(self):
event = record(self.room, GameEvent.SLOT_RELEASED, actor=self.user, slot_number=3)
self.assertIn("slot 3", event.to_prose())
def test_invite_sent_prose(self):
event = record(self.room, GameEvent.INVITE_SENT, actor=self.user)
self.assertEqual(event.to_prose(), "sends an invitation")
def test_role_select_started_prose(self):
event = record(self.room, GameEvent.ROLE_SELECT_STARTED)
self.assertEqual(event.to_prose(), "Role selection begins")
def test_roles_revealed_prose(self):
event = record(self.room, GameEvent.ROLES_REVEALED)
self.assertEqual(event.to_prose(), "All roles assigned")
def test_role_selected_prose_unknown_role_code_uses_question_mark_ordinal(self):
event = record(self.room, GameEvent.ROLE_SELECTED, actor=self.user,
role="XX", role_display="Unknown")
self.assertIn("?", event.to_prose())
def test_sig_unready_prose(self):
event = record(self.room, GameEvent.SIG_UNREADY, actor=self.user)
self.assertIn("disembodies", event.to_prose())
self.assertIn("Significator", event.to_prose())
def test_unknown_verb_falls_back_to_verb_string(self):
event = record(self.room, "custom_event", actor=self.user)
self.assertEqual(event.to_prose(), "custom_event")
def test_to_activity_returns_none_when_actor_has_no_username(self):
actor = User.objects.create(email="noname@test.io")
event = record(self.room, GameEvent.SLOT_FILLED, actor=actor, slot_number=1)
self.assertIsNone(event.to_activity("https://example.com"))
class ScrollPositionStrTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="reader@test.io")
self.room = Room.objects.create(name="Test Room", owner=self.user)
def test_str_includes_email_room_and_position(self):
from apps.drama.models import ScrollPosition
sp = ScrollPosition.objects.create(user=self.user, room=self.room, position=42)
s = str(sp)
self.assertIn("reader@test.io", s)
self.assertIn("Test Room", s)
self.assertIn("42", s)
class ScrollPositionModelTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="reader@test.io")
self.room = Room.objects.create(name="Test Room", owner=self.user)
def test_can_save_scroll_position(self):
sp = ScrollPosition.objects.create(user=self.user, room=self.room, position=150)
self.assertEqual(ScrollPosition.objects.count(), 1)
self.assertEqual(sp.position, 150)
def test_default_position_is_zero(self):
sp = ScrollPosition.objects.create(user=self.user, room=self.room)
self.assertEqual(sp.position, 0)
def test_unique_per_user_and_room(self):
ScrollPosition.objects.create(user=self.user, room=self.room, position=50)
with self.assertRaises(IntegrityError):
ScrollPosition.objects.create(user=self.user, room=self.room, position=100)
def test_upsert_updates_existing_position(self):
ScrollPosition.objects.create(user=self.user, room=self.room, position=50)
ScrollPosition.objects.update_or_create(
user=self.user, room=self.room,
defaults={"position": 200},
)
self.assertEqual(ScrollPosition.objects.get(user=self.user, room=self.room).position, 200)
class NoteModelTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="earner@test.io")
def test_can_create_recognition(self):
recog = Note.objects.create(
user=self.user, slug="stargazer", earned_at=timezone.now(),
)
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 = Note.objects.create(
user=self.user, slug="stargazer", earned_at=timezone.now(),
)
self.assertIsNone(recog.palette)
def test_palette_can_be_set(self):
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):
Note.objects.create(user=self.user, slug="stargazer", earned_at=timezone.now())
with self.assertRaises(IntegrityError):
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")
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 = Note.objects.create(
user=self.user, slug="stargazer", earned_at=timezone.now(),
)
s = str(recog)
self.assertIn("stargazer", s)
self.assertIn("earner@test.io", s)
def test_grant_if_new_creates_on_first_call(self):
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):
Note.grant_if_new(self.user, "stargazer")
recog, created = Note.grant_if_new(self.user, "stargazer")
self.assertFalse(created)
self.assertEqual(Note.objects.count(), 1)
def test_grant_if_new_does_not_overwrite_palette(self):
Note.objects.create(
user=self.user, slug="stargazer",
earned_at=timezone.now(), palette="palette-bardo",
)
recog, created = Note.grant_if_new(self.user, "stargazer")
self.assertFalse(created)
self.assertEqual(recog.palette, "palette-bardo")