Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion ak/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
from core.templatetags.custom_static import large_static
from libraries.constants import LATEST_RELEASE_URL_PATH_STR
from libraries.mixins import ContributorMixin
from mailing_list.constants import MAILING_LIST_LABELS
from news.models import Entry
from testimonials.models import Testimonial
from core.mock_data import SharedResources


logger = structlog.get_logger()


Expand Down Expand Up @@ -192,6 +192,23 @@ def get_v3_context_data(self, **kwargs):
ctx["hero_image_url"] = SharedResources.hero_image_url
ctx["hero_image_url_light"] = SharedResources.hero_image_url_light
ctx["hero_image_url_dark"] = SharedResources.hero_image_url_dark

user = self.request.user
if user.is_authenticated and self.request.session.pop(
"show_ml_post_auth_modal", False
):
user.data["ml_post_auth_seen"] = True
user.save(update_fields=["data"])
managed = set(settings.MAILMAN_LISTS)
ctx["show_ml_post_auth_modal"] = True
ctx["post_auth_modal_subscribe_url"] = reverse(
"mailing-list-post-auth-subscribe"
)
ctx["post_auth_modal_mailing_lists"] = [
{"id": k, **v} for k, v in MAILING_LIST_LABELS.items() if k in managed
]
ctx["post_auth_modal_user_email"] = user.email

return ctx


Expand Down
6 changes: 6 additions & 0 deletions mailing_list/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from mailing_list.views import ConfirmSubscriptionView
from mailing_list.views import ModalSubscribeView
from mailing_list.views import PostAuthSubscribeView
from mailing_list.views import QuickSubscribeView
from mailing_list.views import SubscribeView

Expand All @@ -17,6 +18,11 @@
ModalSubscribeView.as_view(),
name="mailing-list-modal-subscribe",
),
path(
"post-auth-subscribe/",
PostAuthSubscribeView.as_view(),
name="mailing-list-post-auth-subscribe",
),
path(
"confirm/<str:token>/",
ConfirmSubscriptionView.as_view(),
Expand Down
103 changes: 76 additions & 27 deletions mailing_list/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core import signing
from django.core.cache import cache
from django.db import IntegrityError, transaction
from django.core.mail import send_mail
from django.http import HttpResponseRedirect
from django.db import IntegrityError, transaction
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render
from django.template.loader import render_to_string
from django.urls import reverse
Expand Down Expand Up @@ -78,6 +78,43 @@ def _is_rate_limited(request) -> bool:
return cache.incr(key) > _SUBSCRIBE_RATE_LIMIT


def _subscribe_pending(
request, user, email: str, list_ids: list[str]
) -> tuple[list[str], str | None]:
"""Create PENDING subscription records and send a confirmation email.

Returns (succeeded, error_message). On email failure the records are
rolled back and error_message is set; on partial IntegrityError the
affected list is silently skipped.
"""
succeeded = []
for lid in list_ids:
try:
with transaction.atomic():
UserMailingListSubscription.objects.update_or_create(
user=user,
list_id=lid,
defaults={"email": email, "status": SubscriptionStatus.PENDING},
)
succeeded.append(lid)
except IntegrityError:
pass

if not succeeded:
return [], None

try:
_send_confirmation_email(request, email, user.pk, succeeded)
except Exception as exc:
logger.error("Failed to send confirmation email to %s: %s", email, exc)
UserMailingListSubscription.objects.filter(
user=user, list_id__in=succeeded
).delete()
return [], "Could not send confirmation email. Please try again."

return succeeded, None


def _send_confirmation_email(
request, email: str, user_id: int | None, list_ids: list[str]
) -> None:
Expand Down Expand Up @@ -589,38 +626,20 @@ def _handle_authenticated(self, request, email, list_ids, managed_lists):
manage_url=manage_url,
)

succeeded = []
for lid in to_subscribe:
try:
with transaction.atomic():
UserMailingListSubscription.objects.update_or_create(
user=request.user,
list_id=lid,
defaults={"email": email, "status": SubscriptionStatus.PENDING},
)
succeeded.append(lid)
except IntegrityError:
pass
succeeded, error = _subscribe_pending(
request, request.user, email, to_subscribe
)

if not succeeded:
if error:
return self._card(
request,
state="error",
error_message="Could not subscribe. Please try again.",
user_email=email,
request, state="error", error_message=error, user_email=email
)

try:
_send_confirmation_email(request, email, request.user.pk, succeeded)
except Exception as exc:
logger.error("Failed to send confirmation email to %s: %s", email, exc)
UserMailingListSubscription.objects.filter(
user=request.user, list_id__in=succeeded
).delete()
if not succeeded:
return self._card(
request,
state="error",
error_message="Could not send confirmation email. Please try again.",
error_message="Could not subscribe. Please try again.",
user_email=email,
)

Expand Down Expand Up @@ -652,3 +671,33 @@ def _handle_anonymous(self, request, email, list_ids):
)

return self._card(request, state="pending", user_email=email)


class PostAuthSubscribeView(LoginRequiredMixin, View):
"""Subscribe to one or more lists from the post-login homepage modal.

Only for authenticated users. Returns an empty fragment so HTMX removes
the modal from the DOM. Falls back to a homepage redirect for non-HTMX.
"""

def post(self, request):
email = (request.POST.get("email") or "").strip() or request.user.email
managed_lists = set(settings.MAILMAN_LISTS)
list_ids = [
lid for lid in request.POST.getlist("list_id") if lid in managed_lists
]

if list_ids and not _is_rate_limited(request):
current = {
sub.list_id
for sub in UserMailingListSubscription.objects.filter(
user=request.user, list_id__in=managed_lists
)
}
to_subscribe = [lid for lid in list_ids if lid not in current]

_subscribe_pending(request, request.user, email, to_subscribe)

if _is_htmx(request):
return HttpResponse("")
return _prg_redirect(request)
111 changes: 111 additions & 0 deletions static/css/v3/mailing-list-card.css
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,114 @@ input.mailing-list-modal__checkbox:focus-visible + .mailing-list-modal__checkbox
line-height: var(--line-height-default);
color: var(--color-text-secondary);
}

/* ============================================
POST-AUTH MODAL — first-login subscription prompt
============================================ */

/* Header contains only the title (no close X per design) */
.ml-post-auth-modal__header {
padding: var(--space-large) var(--space-large) 0 var(--space-large);
width: 100%;
box-sizing: border-box;
}

.ml-post-auth-modal__header .dialog-modal__title {
margin: 0;
}

/* Email input row */
.ml-post-auth-modal__email-row {
padding: 0 var(--space-large);
width: 100%;
box-sizing: border-box;
}

.ml-post-auth-modal__email-field {
display: block;
width: 100%;
height: 40px;
padding: 0 var(--space-large);
box-sizing: border-box;
background-color: var(--color-surface-weak) !important;
border: 1px solid var(--color-stroke-weak) !important;
border-radius: var(--border-radius-xl) !important;
font-family: var(--font-sans);
font-size: var(--font-size-base);
font-weight: var(--font-weight-regular);
line-height: var(--line-height-default);
letter-spacing: var(--letter-spacing-tight);
color: var(--color-text-secondary);
outline: none;
box-shadow: none;
-webkit-appearance: none;
appearance: none;
}

.ml-post-auth-modal__email-field::placeholder {
color: var(--color-text-secondary);
}

.ml-post-auth-modal__email-field:focus-visible {
background-color: var(--color-surface-mid);
border-color: var(--color-stroke-strong);
}

.ml-post-auth-modal__container .dialog-modal__buttons {
padding-top: var(--space-large);
}

/* Description + list card section */
.ml-post-auth-modal__body {
display: flex;
flex-direction: column;
gap: var(--space-default);
width: 100%;
box-sizing: border-box;
}

.ml-post-auth-modal__description {
padding: 0 var(--space-large);
margin: 0;
font-family: var(--font-sans);
font-size: var(--font-size-base);
font-weight: var(--font-weight-regular);
line-height: var(--line-height-default);
letter-spacing: var(--letter-spacing-tight);
color: var(--color-text-secondary);
}

.ml-post-auth-modal__list-section {
display: flex;
flex-direction: column;
gap: var(--space-large);
padding: 0 var(--space-large) var(--space-large);
width: 100%;
box-sizing: border-box;
}

/* Override the bottom margin that .mailing-list-modal__list-card applies
since margin is handled by the parent flex gap here */
.ml-post-auth-modal__list-card {
margin: 0;
}

/* Unselect All link + "X of Y selected" counter */
.ml-post-auth-modal__footer-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
box-sizing: border-box;
font-size: var(--font-size-small);
line-height: var(--line-height-relaxed);
letter-spacing: var(--letter-spacing-tight);
white-space: nowrap;
}

.ml-post-auth-modal__count {
font-family: var(--font-sans);
font-weight: var(--font-weight-regular);
color: var(--color-text-secondary);
}
4 changes: 4 additions & 0 deletions templates/v3/homepage.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,8 @@
</div>
</div>
</div>

{% if show_ml_post_auth_modal %}
{% include "v3/includes/_mailing_list_post_auth_modal.html" with post_auth_modal_subscribe_url=post_auth_modal_subscribe_url post_auth_modal_mailing_lists=post_auth_modal_mailing_lists post_auth_modal_user_email=post_auth_modal_user_email %}
{% endif %}
{% endblock %}
2 changes: 1 addition & 1 deletion templates/v3/includes/_content_modal.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
Trigger: <a href="#{{ modal_id }}">…</a>
{% endcomment %}
<div class="dialog-modal content-modal" id="{{ modal_id }}">
<a class="dialog-modal__backdrop" href="{{ close_url|default:'#_' }}" aria-hidden="true" tabindex="-1"></a>
<a class="dialog-modal__backdrop" href="{{ close_url|default:'#_' }}" tabindex="-1"></a>
<div class="content-modal__container"
role="dialog"
aria-modal="true"
Expand Down
2 changes: 1 addition & 1 deletion templates/v3/includes/_dialog.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
{% endcomment %}

<div class="dialog-modal" id="{{ dialog_id }}">
<a class="dialog-modal__backdrop" href="#_" aria-hidden="true" tabindex="-1"></a>
<a class="dialog-modal__backdrop" href="#_" tabindex="-1"></a>
<div class="dialog-modal__container"
role="dialog"
aria-modal="true"
Expand Down
2 changes: 1 addition & 1 deletion templates/v3/includes/_mailing_list_card.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@

{% if modal_subscribe_url and mailing_lists %}
<div class="dialog-modal" id="mailing-list-modal">
<a class="dialog-modal__backdrop" href="#_" tabindex="-1" aria-hidden="true"></a>
<a class="dialog-modal__backdrop" href="#_" tabindex="-1"></a>
<div class="dialog-modal__container"
role="dialog"
aria-modal="true"
Expand Down
Loading
Loading