Membership: newsletter opt-in at signup and on the profile
An unticked checkbox on registration subscribes the verified address to the listmonk list (preconfirmed) once the account exists. The profile page gets a subscribe/unsubscribe toggle, and verified email changes follow through to listmonk. Newsletter outages never block accounts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.views.decorators.http import require_GET, require_POST, require_http_methods
|
||||
|
||||
from . import billing, directory, profiles, shkeeper
|
||||
from . import billing, directory, newsletter, profiles, shkeeper
|
||||
from .forms import PasswordForm, RegistrationForm
|
||||
from .service_groups import configured_groups, service_access_catalog
|
||||
from .profile_forms import CurrentPasswordForm, ProfileEmailForm, ProfileForm, ProfilePasswordForm
|
||||
@@ -93,7 +93,7 @@ def register(request):
|
||||
return HttpResponse("Please try again later.", status=429)
|
||||
token = secrets.token_urlsafe(32)
|
||||
digest = hashlib.sha256(token.encode()).hexdigest()
|
||||
Registration.objects.create(token_hash=digest, uid=form.cleaned_data["uid"], email=email, expires_at=timezone.now() + timedelta(hours=1))
|
||||
Registration.objects.create(token_hash=digest, uid=form.cleaned_data["uid"], email=email, newsletter=form.cleaned_data.get("newsletter", False), expires_at=timezone.now() + timedelta(hours=1))
|
||||
url = settings.PUBLIC_URL + reverse("members:verify", args=[token])
|
||||
try:
|
||||
send_mail("Verify your eom.dev account", f"Finish creating your account (expires in one hour):\n\n{url}\n\nIf you did not request this, ignore this message. Existing accounts are never changed by this link.", settings.DEFAULT_FROM_EMAIL, [email])
|
||||
@@ -139,10 +139,16 @@ def finish_registration(request):
|
||||
remember_groups(account)
|
||||
Registration.objects.filter(uid=registration.uid).delete()
|
||||
request.session.pop("registration_hash", None)
|
||||
subscribed = registration.newsletter and _subscribe_newsletter(account)
|
||||
# Server-side, short-lived proof permits first binding of ONLY the
|
||||
# account just provisioned in this browser, never an email match.
|
||||
request.session["registration_identity"] = {"account_id": str(account.pk), "expires_at": (timezone.now() + timedelta(hours=1)).timestamp()}
|
||||
return page(request, title="Account created", message="Your free eom.dev account is ready. Sign in to explore the community and its services, or choose an optional paid membership for additional benefits.")
|
||||
message = "Your free eom.dev account is ready. Sign in to explore the community and its services, or choose an optional paid membership for additional benefits."
|
||||
if subscribed:
|
||||
message += " You're subscribed to premiere emails; you can change that from your profile."
|
||||
elif registration.newsletter:
|
||||
message += " We couldn't sign you up for premiere emails just now; you can try again from your profile."
|
||||
return page(request, title="Account created", message=message)
|
||||
except ValueError:
|
||||
form.add_error(None, "An account with those details may already exist. Try signing in or contact us for help.")
|
||||
except Exception as exc:
|
||||
@@ -151,6 +157,26 @@ def finish_registration(request):
|
||||
return page(request, title="Choose your password", form=form, submit="Create free account")
|
||||
|
||||
|
||||
def _subscribe_newsletter(account):
|
||||
# The address was verified by the registration link, so it is preconfirmed.
|
||||
# Best effort: the account already exists whatever happens here.
|
||||
try:
|
||||
return newsletter.subscribe(account.email, uid=account.uid) == newsletter.SUBSCRIBED
|
||||
except Exception as exc:
|
||||
logger.error("Newsletter subscribe failed account=%s kind=%s", account.pk, type(exc).__name__)
|
||||
return False
|
||||
|
||||
|
||||
def _newsletter_status(account):
|
||||
if not newsletter.enabled():
|
||||
return None
|
||||
try:
|
||||
return newsletter.status(account.email)
|
||||
except Exception as exc:
|
||||
logger.error("Newsletter status failed account=%s kind=%s", account.pk, type(exc).__name__)
|
||||
return "unavailable"
|
||||
|
||||
|
||||
def dex():
|
||||
oauth = OAuth()
|
||||
remote = oauth.register("dex", client_id=settings.DEX_CLIENT_ID, client_secret=settings.DEX_CLIENT_SECRET, server_metadata_url=settings.DEX_ISSUER.rstrip("/") + "/.well-known/openid-configuration", client_kwargs={"scope": "openid email profile groups federated:id", "code_challenge_method": "S256", "timeout": 10})
|
||||
@@ -267,7 +293,7 @@ def profile(request):
|
||||
logger.error("Profile read failed account=%s kind=%s", member.pk, type(exc).__name__)
|
||||
return render(request, "profile.html", {"account": member, "profile_loaded": False}, status=503)
|
||||
context = _profile_context(member, values)
|
||||
context.update(saved=request.GET.get("saved") == "1", email_sent=request.GET.get("email_sent") == "1")
|
||||
context.update(saved=request.GET.get("saved") == "1", email_sent=request.GET.get("email_sent") == "1", newsletter_status=_newsletter_status(member))
|
||||
if request.method == "POST":
|
||||
if limited("profile-write", str(member.pk), 20) or limited("profile-write-ip", client_ip(request), 40):
|
||||
return HttpResponse("Please try again later.", status=429)
|
||||
@@ -369,10 +395,16 @@ def confirm_profile_email(request):
|
||||
if member.ldap_uuid != pending.ldap_uuid:
|
||||
raise ValueError("Directory identity changed.")
|
||||
profiles.change_email(member, form.cleaned_data["current_password"], pending.email)
|
||||
old_email = member.email
|
||||
member.email = pending.email
|
||||
member.save(update_fields=["email"])
|
||||
ProfileEmailChange.objects.filter(account=member).delete()
|
||||
request.session.pop("profile_email_hash", None)
|
||||
if newsletter.enabled():
|
||||
try:
|
||||
newsletter.change_email(old_email, member.email)
|
||||
except Exception as exc:
|
||||
logger.error("Newsletter email change failed account=%s kind=%s", member.pk, type(exc).__name__)
|
||||
request.session.cycle_key()
|
||||
return redirect(reverse("members:profile") + "?saved=1")
|
||||
except Exception as exc:
|
||||
@@ -381,6 +413,27 @@ def confirm_profile_email(request):
|
||||
return render(request, "profile.html", {"account": request.member, "confirm_email": True, "confirmation_email": pending.email, "confirmation_form": form})
|
||||
|
||||
|
||||
@require_POST
|
||||
@account_required
|
||||
def newsletter_preference(request):
|
||||
member = request.member
|
||||
action = request.POST.get("action")
|
||||
if not newsletter.enabled() or action not in ("subscribe", "unsubscribe"):
|
||||
return HttpResponse(status=400)
|
||||
if limited("newsletter", str(member.pk), 20) or limited("newsletter-ip", client_ip(request), 40):
|
||||
return HttpResponse("Please try again later.", status=429)
|
||||
try:
|
||||
if action == "subscribe":
|
||||
result = newsletter.subscribe(member.email, uid=member.uid)
|
||||
else:
|
||||
result = newsletter.unsubscribe(member.email)
|
||||
logger.info("Newsletter preference changed account=%s action=%s result=%s", member.pk, action, result)
|
||||
except Exception as exc:
|
||||
logger.error("Newsletter preference failed account=%s kind=%s", member.pk, type(exc).__name__)
|
||||
return redirect(reverse("members:profile") + "?newsletter=error#newsletter")
|
||||
return redirect(reverse("members:profile") + "?newsletter=saved#newsletter")
|
||||
|
||||
|
||||
def stripe_redirect(url, host):
|
||||
parsed = urlsplit(url or "")
|
||||
if parsed.scheme != "https" or parsed.hostname != host or parsed.username:
|
||||
|
||||
Reference in New Issue
Block a user