Members page: show a live total member count

Count every membership account (free and paid together) on each page
load and show it under the headline; no per-tier numbers. Hidden when
there are no members.

Also correct the reference tiers.eom.json and service-groups.eom.json
to the real LDAP DNs (ou=Memberships; ou=<Service>,ou=Services), which
now match the ansible-role-www defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-06 19:49:18 -05:00
co-authored by Claude Opus 5.5
parent c7dbcdd574
commit 5954ec1061
6 changed files with 50 additions and 36 deletions
+2
View File
@@ -5,6 +5,8 @@
.member-main { padding-bottom: 4rem; }
.member-hero { padding-block: clamp(3rem, 8vw, 6rem); }
.member-hero h1 { font-size: clamp(2.8rem, 6vw, 5rem); max-width: 900px; }
.member-count { color: var(--muted); font-size: 1.1rem; }
.member-count strong { color: var(--accent-strong); font-size: 1.6rem; font-variant-numeric: tabular-nums; }
.member-intro { max-width: 700px; color: var(--muted); font-size: 1.2rem; }
.member-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; }
.member-actions form { margin: 0; }
@@ -51,6 +51,15 @@ class RegistrationTests(TestCase):
self.assertEqual(response.headers["Cache-Control"], "no-store")
self.assertContains(self.client.get("/members/register/"), "Send verification email")
def test_home_shows_live_total_member_count(self):
self.assertNotContains(self.client.get("/members/"), "member-count")
Account.objects.create(uid="alice", email="alice@example.org")
self.assertContains(self.client.get("/members/"), "<strong>1</strong> member and counting", html=False)
Account.objects.create(uid="bob", email="bob@example.org", membership_level=3)
response = self.client.get("/members/")
# One total across free and paid levels; no per-tier numbers.
self.assertContains(response, "<strong>2</strong> members and counting", html=False)
def start_signup(self):
self.client.post("/members/register/", {"uid": "alice", "email": "ALICE@example.org"})
match = re.search(r"/verify/([^/]+)/", str(mail.outbox[-1].body))
+3 -1
View File
@@ -79,7 +79,9 @@ def home(request):
# Presentation copies only: never mutate authorization/payment settings.
tiers = settings.MEMBERSHIP_TIERS
services = service_access_catalog(tiers, settings.MEMBERSHIP_SERVICE_GROUPS)
return page(request, account=account, tiers=tiers, services=services, subscriptions=subscriptions, crypto_invoices=invoices, shkeeper_enabled=settings.SHKEEPER_ENABLED, now=timezone.now())
# Public total across every level (free and paid); never a per-tier breakdown.
member_count = Account.objects.count()
return page(request, account=account, tiers=tiers, services=services, subscriptions=subscriptions, crypto_invoices=invoices, shkeeper_enabled=settings.SHKEEPER_ENABLED, now=timezone.now(), member_count=member_count)
@require_http_methods(["GET", "POST"])
+30 -30
View File
@@ -1,44 +1,44 @@
{
"0": [
"cn=Dex Users,ou=Services,dc=eom,dc=dev",
"cn=Discourse Users,ou=Services,dc=eom,dc=dev",
"cn=Gitea Users,ou=Services,dc=eom,dc=dev",
"cn=Matrix Users,ou=Services,dc=eom,dc=dev"
"cn=Dex Users,ou=Dex,ou=Services,dc=eom,dc=dev",
"cn=Discourse Users,ou=Discourse,ou=Services,dc=eom,dc=dev",
"cn=Gitea Users,ou=Gitea,ou=Services,dc=eom,dc=dev",
"cn=Matrix Users,ou=Matrix,ou=Services,dc=eom,dc=dev"
],
"1": [
"cn=Dex Users,ou=Services,dc=eom,dc=dev",
"cn=Discourse Users,ou=Services,dc=eom,dc=dev",
"cn=Gitea Users,ou=Services,dc=eom,dc=dev",
"cn=Matrix Users,ou=Services,dc=eom,dc=dev"
"cn=Dex Users,ou=Dex,ou=Services,dc=eom,dc=dev",
"cn=Discourse Users,ou=Discourse,ou=Services,dc=eom,dc=dev",
"cn=Gitea Users,ou=Gitea,ou=Services,dc=eom,dc=dev",
"cn=Matrix Users,ou=Matrix,ou=Services,dc=eom,dc=dev"
],
"2": [
"cn=Dex Users,ou=Services,dc=eom,dc=dev",
"cn=Discourse Users,ou=Services,dc=eom,dc=dev",
"cn=Gitea Users,ou=Services,dc=eom,dc=dev",
"cn=Matrix Users,ou=Services,dc=eom,dc=dev"
"cn=Dex Users,ou=Dex,ou=Services,dc=eom,dc=dev",
"cn=Discourse Users,ou=Discourse,ou=Services,dc=eom,dc=dev",
"cn=Gitea Users,ou=Gitea,ou=Services,dc=eom,dc=dev",
"cn=Matrix Users,ou=Matrix,ou=Services,dc=eom,dc=dev"
],
"3": [
"cn=Dex Users,ou=Services,dc=eom,dc=dev",
"cn=Discourse Users,ou=Services,dc=eom,dc=dev",
"cn=Gitea Users,ou=Services,dc=eom,dc=dev",
"cn=Matrix Users,ou=Services,dc=eom,dc=dev",
"cn=Mastodon Users,ou=Services,dc=eom,dc=dev",
"cn=PeerTube Users,ou=Services,dc=eom,dc=dev"
"cn=Dex Users,ou=Dex,ou=Services,dc=eom,dc=dev",
"cn=Discourse Users,ou=Discourse,ou=Services,dc=eom,dc=dev",
"cn=Gitea Users,ou=Gitea,ou=Services,dc=eom,dc=dev",
"cn=Matrix Users,ou=Matrix,ou=Services,dc=eom,dc=dev",
"cn=Mastodon Users,ou=Mastodon,ou=Services,dc=eom,dc=dev",
"cn=PeerTube Users,ou=PeerTube,ou=Services,dc=eom,dc=dev"
],
"4": [
"cn=Dex Users,ou=Services,dc=eom,dc=dev",
"cn=Discourse Users,ou=Services,dc=eom,dc=dev",
"cn=Gitea Users,ou=Services,dc=eom,dc=dev",
"cn=Matrix Users,ou=Services,dc=eom,dc=dev",
"cn=Mastodon Users,ou=Services,dc=eom,dc=dev",
"cn=PeerTube Users,ou=Services,dc=eom,dc=dev"
"cn=Dex Users,ou=Dex,ou=Services,dc=eom,dc=dev",
"cn=Discourse Users,ou=Discourse,ou=Services,dc=eom,dc=dev",
"cn=Gitea Users,ou=Gitea,ou=Services,dc=eom,dc=dev",
"cn=Matrix Users,ou=Matrix,ou=Services,dc=eom,dc=dev",
"cn=Mastodon Users,ou=Mastodon,ou=Services,dc=eom,dc=dev",
"cn=PeerTube Users,ou=PeerTube,ou=Services,dc=eom,dc=dev"
],
"5": [
"cn=Dex Users,ou=Services,dc=eom,dc=dev",
"cn=Discourse Users,ou=Services,dc=eom,dc=dev",
"cn=Gitea Users,ou=Services,dc=eom,dc=dev",
"cn=Matrix Users,ou=Services,dc=eom,dc=dev",
"cn=Mastodon Users,ou=Services,dc=eom,dc=dev",
"cn=PeerTube Users,ou=Services,dc=eom,dc=dev"
"cn=Dex Users,ou=Dex,ou=Services,dc=eom,dc=dev",
"cn=Discourse Users,ou=Discourse,ou=Services,dc=eom,dc=dev",
"cn=Gitea Users,ou=Gitea,ou=Services,dc=eom,dc=dev",
"cn=Matrix Users,ou=Matrix,ou=Services,dc=eom,dc=dev",
"cn=Mastodon Users,ou=Mastodon,ou=Services,dc=eom,dc=dev",
"cn=PeerTube Users,ou=PeerTube,ou=Services,dc=eom,dc=dev"
]
}
+1
View File
@@ -43,6 +43,7 @@
<section class="member-hero">
<p class="eyebrow">Music, films, projects &amp; shared worlds</p>
<h1>Support the work.<br>Get closer to what’s next.</h1>
{% if member_count %}<p class="member-count"><strong>{{ member_count }}</strong> member{{ member_count|pluralize }} and counting</p>{% endif %}
<p class="member-intro">Membership supports my music, films, writing, open-source projects, and game servers. Join the community for free, or choose a paid tier to support the work and receive the benefits listed below.</p>
{% if account %}
<div class="member-panel account-panel">
+5 -5
View File
@@ -5,7 +5,7 @@
"price_id": "price_1Rgv4xC5KPXqTvVLfMd6z7wn",
"price_label": "$4 USD / month",
"usd_amount": "4.00",
"group_dn": "cn=Level 1,ou=Members,dc=eom,dc=dev",
"group_dn": "cn=Level 1,ou=Memberships,dc=eom,dc=dev",
"description": "Go beyond the public feed.",
"benefits": [
"Exclusive Discourse and Matrix content, including work-in-progress updates",
@@ -19,7 +19,7 @@
"price_id": "price_1Rgv5QC5KPXqTvVLv1PaHWb5",
"price_label": "$8 USD / month",
"usd_amount": "8.00",
"group_dn": "cn=Level 2,ou=Members,dc=eom,dc=dev",
"group_dn": "cn=Level 2,ou=Memberships,dc=eom,dc=dev",
"description": "Try new work before release.",
"benefits": [
"Silver-tier giveaways",
@@ -32,7 +32,7 @@
"price_id": "price_1Rgv5rC5KPXqTvVL8Ol8sMUf",
"price_label": "$16 USD / month",
"usd_amount": "16.00",
"group_dn": "cn=Level 3,ou=Members,dc=eom,dc=dev",
"group_dn": "cn=Level 3,ou=Memberships,dc=eom,dc=dev",
"description": "Help shape the next experiment.",
"benefits": [
"Gold-tier giveaways",
@@ -46,7 +46,7 @@
"price_id": "price_1Rgv6FC5KPXqTvVL39mAxYjm",
"price_label": "$32 USD / month",
"usd_amount": "32.00",
"group_dn": "cn=Level 4,ou=Members,dc=eom,dc=dev",
"group_dn": "cn=Level 4,ou=Memberships,dc=eom,dc=dev",
"description": "Have a voice in the platform's direction.",
"benefits": [
"Platinum-tier giveaways",
@@ -59,7 +59,7 @@
"price_id": "price_1Rgv6mC5KPXqTvVLkcmznWZa",
"price_label": "$64 USD / month",
"usd_amount": "64.00",
"group_dn": "cn=Level 5,ou=Members,dc=eom,dc=dev",
"group_dn": "cn=Level 5,ou=Memberships,dc=eom,dc=dev",
"description": "Give the independent work its biggest boost.",
"benefits": [
"Diamond-tier giveaways",