diff --git a/authentication/tests.py b/authentication/tests.py index a7806be..1671e2c 100644 --- a/authentication/tests.py +++ b/authentication/tests.py @@ -318,6 +318,27 @@ class PasswordResetEmailTests(TestCase): self.assertIn("AU", with_club) # initials fallback: no logo set self.assertIn("https://ajax-united.rosterchief.app/accounts/password/reset/key/abc-def/", with_club) + def test_the_initials_badge_text_contrasts_against_the_fallback_colour(self): + # Club has no secondary_color, so the badge falls back to #ec4899 -- + # black text (contrast_color("#ec4899")) reads far better on it than + # the white the template used to hardcode. See + # club/templatetags/club_email.py::contrast_color. + club = Club.objects.create(name="Ajax United", slug="ajax-united") + + rendered = render_to_string("account/email/password_reset_key_message.html", {"club": club, "password_reset_url": "https://ajax-united.rosterchief.app/accounts/password/reset/key/abc-def/"}) + + self.assertIn("background-color:#ec4899", rendered) + self.assertIn("color:#000000", rendered) + + def test_the_reset_button_text_contrasts_against_the_no_club_fallback_colour(self): + # No club at all (the base-domain flow) -- the button falls back to + # #0ea5e9, RosterChief's own sky blue, which also needs black text + # for a passing contrast ratio, not the white previously hardcoded. + rendered = render_to_string("account/email/password_reset_key_message.html", {"club": None, "password_reset_url": "https://rosterchief.app/accounts/password/reset/key/abc-def/", "current_site": None}) + + self.assertIn("background-color:#0ea5e9", rendered) + self.assertIn("color:#000000", rendered) + without_club = render_to_string("account/email/password_reset_key_message.html", {"club": None, "password_reset_url": "https://rosterchief.app/accounts/password/reset/key/abc-def/", "current_site": SimpleNamespace(name="rosterchief.app")}) self.assertIn("Roster", without_club) self.assertIn("Chief", without_club) diff --git a/club/templatetags/club_email.py b/club/templatetags/club_email.py index d9e75f8..b6502c9 100644 --- a/club/templatetags/club_email.py +++ b/club/templatetags/club_email.py @@ -9,9 +9,27 @@ is relative. from django import template +from club.models import Club + register = template.Library() +@register.filter +def contrast_color(hex_color): + """Black or white, whichever reads on ``hex_color`` -- for a literal + fallback background (e.g. ``club.secondary_color|default:"#ec4899"``) + rather than a club's own colour, which already has a matching + ``primary_content_color``/``secondary_content_color`` computed for it. + + Chaining this onto the *same* expression used for the background -- + ``club.secondary_color|default:"#ec4899"|contrast_color`` -- rather than + hardcoding a second, independently-guessed text colour is what keeps the + two from drifting apart: a pale fallback and a dark one both get the + contrast Club._content_color_for would compute for them either way. + """ + return Club._content_color_for(hex_color) + + @register.simple_tag(takes_context=True) def absolute_media_url(context, file_field): """An absolute URL for ``file_field`` (e.g. ``club.logo``), for use in an diff --git a/club/tests.py b/club/tests.py index 806c98a..b4ce02c 100644 --- a/club/tests.py +++ b/club/tests.py @@ -1258,6 +1258,17 @@ class ClubBrandingModelTests(TestCase): def test_no_colour_means_no_contrast_colour(self): self.assertEqual(Club(primary_color="").primary_content_color, "") + def test_contrast_color_filter_matches_the_same_algorithm(self): + # HTML emails apply this to a literal fallback background (e.g. + # club.secondary_color|default:"#ec4899") rather than a club's own + # colour, so it can't be exercised through primary_content_color -- + # but it must still agree with it for a colour a club actually set. + from club.templatetags.club_email import contrast_color + + self.assertEqual(contrast_color("#ec4899"), Club(secondary_color="#ec4899").secondary_content_color) + self.assertEqual(contrast_color("#fef08a"), "#000000") + self.assertEqual(contrast_color("#1e40af"), "#ffffff") + def test_a_colour_must_be_a_hex_value(self): club = Club(name="Ajax United", primary_color="blue") diff --git a/management/tests.py b/management/tests.py index 9355dcf..12982c7 100644 --- a/management/tests.py +++ b/management/tests.py @@ -1516,6 +1516,21 @@ class ParentClaimViewTests(ManagementTestBase): [reset_path] = [line for line in sent.body.splitlines() if "/accounts/password/reset/key/" in line] self.assertIn(reset_path.strip(), html_body) + def test_the_initials_badge_text_contrasts_against_the_fallback_colour(self): + # self.club has no secondary_color set, so the badge falls back to + # #ec4899 -- white text on that (the old hardcoded default) reads + # far worse than black (contrast_color("#ec4899") == "#000000"), see + # club/templatetags/club_email.py::contrast_color. + self.submit() + claim = ParentClaim.objects.get(club=self.club) + self.client.force_login(self.admin_user) + + self.club_post("parent_claim_approve", {"child": str(self.child.pk)}, claim.pk) + + [(html_body, _mimetype)] = mail.outbox[0].alternatives + self.assertIn("background-color:#ec4899", html_body) + self.assertIn("color:#000000", html_body) + def test_the_email_mentions_the_clubs_contact_email_when_set(self): self.club.contact_email = "info@ajax-united.example.com" self.club.save(update_fields=["contact_email"]) diff --git a/members/templates/members/email/claim_approved.html b/members/templates/members/email/claim_approved.html index af13020..634f184 100644 --- a/members/templates/members/email/claim_approved.html +++ b/members/templates/members/email/claim_approved.html @@ -23,7 +23,7 @@ {% else %} - @@ -43,7 +43,7 @@

{% blocktrans with club=club.name child=child %}{{ club }} has confirmed that you're {{ child }}'s parent or guardian, and your account is ready.{% endblocktrans %}

{% trans "Set your password" as set_password_label %} - {% include "email/_button.html" with href=set_password_url label=set_password_label bg=club.primary_color|default:"#4f46e5" fg=club.primary_content_color|default:"#ffffff" %} + {% include "email/_button.html" with href=set_password_url label=set_password_label bg=club.primary_color|default:"#4f46e5" fg=club.primary_color|default:"#4f46e5"|contrast_color %}

{% trans "That link is for you alone — please don't forward it." %}

diff --git a/static/css/app.css b/static/css/app.css index 463ad66..1730a58 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -4294,6 +4294,9 @@ --tw-tracking: var(--tracking-wider); letter-spacing: var(--tracking-wider); } + .text-wrap { + text-wrap: wrap; + } .whitespace-nowrap { white-space: nowrap; } diff --git a/templates/account/email/password_reset_key_message.html b/templates/account/email/password_reset_key_message.html index 866a186..0d5fd8c 100644 --- a/templates/account/email/password_reset_key_message.html +++ b/templates/account/email/password_reset_key_message.html @@ -39,7 +39,7 @@ {% else %}
+ {{ club.initials }}
- @@ -60,7 +60,7 @@

{% trans "You're receiving this email because you or someone else has requested a password reset for your user account. It can be safely ignored if you did not request one." %}

{% trans "Reset your password" as reset_label %} - {% include "email/_button.html" with href=password_reset_url label=reset_label bg=club.primary_color|default:"#0ea5e9" fg=club.primary_content_color|default:"#ffffff" %} + {% include "email/_button.html" with href=password_reset_url label=reset_label bg=club.primary_color|default:"#0ea5e9" fg=club.primary_color|default:"#0ea5e9"|contrast_color %} {% if username %}

{% blocktrans %}In case you forgot, your username is {{ username }}.{% endblocktrans %}

+ {{ club.initials }}