My Sign DEL btn: clear-sign affordance on SCAN SIGN landing — Sprint 4b-adjacent of My Sea roadmap — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

Pre-spec'd in [[sprint-my-sea-sign-gate-may19]] as the unblocker for tomorrow's visual verification of 4b's no-sig branch — admin user (@disco) had a saved sig from Sprint 4a testing & there was no in-UI affordance to undo it short of DB surgery. Lands ahead of the deferred 4b visual verify so dev users can toggle between sig/no-sig states on Claudezilla.

- Endpoint: `path("my-sign/clear", views.clear_sign, name="clear_sign")` — POST sets `User.significator = None` + `significator_reversed = False`, redirects to picker; GET is a no-mutation redirect to picker (mirrors save_sign's GET handling). `login_required(login_url="/")`. No trailing slash per [[feedback_url_convention_actions_no_trailing_slash]] (action endpoint, not page).
- Template (my_sign.html): `<form id="id_clear_sign_form" class="my-sign-clear-form">` w. `<button id="id_clear_sign_btn" class="btn btn-danger">DEL</button>`, rendered ONLY when `current_significator` is set; sits inside `.my-sign-landing` as a sibling of `.room-shell` so it's bound to the landing-phase UI alone (picker phase already has its own NVM unlock affordance on focused thumbnails).
- SCSS: anchored bottom-right of `.my-sign-landing` via `position: absolute; bottom: .75rem; right: 1rem` — `.my-sign-landing` gains `position: relative` to scope the absolute. `.btn-danger` carries the destructive treatment; "DEL" mirrors post.html gear menu's DEL convention from [[sprint-post-polish-may13]].
- 3 FTs in new `MySignClearTest` class — covers: btn renders on landing when sig saved (T1, asserts text "DEL" + `.btn-danger` class); btn absent when no sig (T2); click POSTs, reloads, & wipes `User.significator` + `significator_reversed` in DB (T3).
- 6 ITs in new `ClearSignViewTest` + `MySignClearAffordanceTemplateTest` — covers: login_required gate, POST wipes both fields w. redirect-back, GET redirects w.o mutation, POST-w/o-existing-sig is idempotent no-op, template renders btn only when sig set, template's form action targets `clear_sign` reverse.
- 1029 IT/UT green in 47s (+6 from baseline); 20/20 FT green across test_bill_my_sign + test_game_my_sea in 165s.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-19 14:13:20 -04:00
parent a636e940b7
commit c8a603484e
6 changed files with 196 additions and 0 deletions

View File

@@ -868,6 +868,77 @@ class MySignViewTest(TestCase):
self.assertRedirects(response, reverse("billboard:my_sign"))
class ClearSignViewTest(TestCase):
"""Clear-sign endpoint — POST `/billboard/my-sign/clear` wipes the
user's significator FK + reversed flag, then redirects to the picker.
Sprint 4b-adjacent (2026-05-19). See [[project-my-sea-roadmap]]."""
def setUp(self):
self.user = User.objects.create(email="clear@test.io")
self.client.force_login(self.user)
_seed_billboard_applets()
from apps.epic.models import personal_sig_cards
self.target = personal_sig_cards(self.user)[0]
self.user.significator = self.target
self.user.significator_reversed = True
self.user.save(update_fields=["significator", "significator_reversed"])
def test_clear_sign_requires_login(self):
self.client.logout()
response = self.client.post(reverse("billboard:clear_sign"))
self.assertRedirects(
response, "/?next=/billboard/my-sign/clear",
fetch_redirect_response=False,
)
def test_clear_sign_post_wipes_sig_and_reversed_flag(self):
response = self.client.post(reverse("billboard:clear_sign"))
self.assertRedirects(response, reverse("billboard:my_sign"))
self.user.refresh_from_db()
self.assertIsNone(self.user.significator_id)
self.assertFalse(self.user.significator_reversed)
def test_clear_sign_get_redirects_without_clearing(self):
response = self.client.get(reverse("billboard:clear_sign"))
self.assertRedirects(response, reverse("billboard:my_sign"))
self.user.refresh_from_db()
self.assertEqual(self.user.significator_id, self.target.id)
self.assertTrue(self.user.significator_reversed)
def test_clear_sign_post_with_no_existing_sig_is_noop(self):
self.user.significator = None
self.user.significator_reversed = False
self.user.save(update_fields=["significator", "significator_reversed"])
response = self.client.post(reverse("billboard:clear_sign"))
self.assertRedirects(response, reverse("billboard:my_sign"))
self.user.refresh_from_db()
self.assertIsNone(self.user.significator_id)
self.assertFalse(self.user.significator_reversed)
class MySignClearAffordanceTemplateTest(TestCase):
"""Pin the CLEAR SIGN btn template-render contract — visible only when
`user.significator` is set on the picker page."""
def setUp(self):
self.user = User.objects.create(email="ctmpl@test.io")
self.client.force_login(self.user)
_seed_billboard_applets()
def test_clear_btn_absent_when_no_sig_saved(self):
response = self.client.get(reverse("billboard:my_sign"))
self.assertNotContains(response, 'id="id_clear_sign_btn"')
def test_clear_btn_present_and_targets_clear_url_when_sig_saved(self):
from apps.epic.models import personal_sig_cards
target = personal_sig_cards(self.user)[0]
self.user.significator = target
self.user.save(update_fields=["significator"])
response = self.client.get(reverse("billboard:my_sign"))
self.assertContains(response, 'id="id_clear_sign_btn"')
self.assertContains(response, reverse("billboard:clear_sign"))
class BillboardAppletMySignTest(TestCase):
"""My Sign applet rendering on /billboard/."""