|
7 | 7 | from django.contrib.auth.mixins import LoginRequiredMixin |
8 | 8 | from django.core import signing |
9 | 9 | from django.core.cache import cache |
10 | | -from django.db import IntegrityError, transaction |
11 | 10 | from django.core.mail import send_mail |
12 | | -from django.http import HttpResponseRedirect |
| 11 | +from django.db import IntegrityError, transaction |
| 12 | +from django.http import HttpResponse, HttpResponseRedirect |
13 | 13 | from django.shortcuts import render |
14 | 14 | from django.template.loader import render_to_string |
15 | 15 | from django.urls import reverse |
@@ -78,6 +78,43 @@ def _is_rate_limited(request) -> bool: |
78 | 78 | return cache.incr(key) > _SUBSCRIBE_RATE_LIMIT |
79 | 79 |
|
80 | 80 |
|
| 81 | +def _subscribe_pending( |
| 82 | + request, user, email: str, list_ids: list[str] |
| 83 | +) -> tuple[list[str], str | None]: |
| 84 | + """Create PENDING subscription records and send a confirmation email. |
| 85 | +
|
| 86 | + Returns (succeeded, error_message). On email failure the records are |
| 87 | + rolled back and error_message is set; on partial IntegrityError the |
| 88 | + affected list is silently skipped. |
| 89 | + """ |
| 90 | + succeeded = [] |
| 91 | + for lid in list_ids: |
| 92 | + try: |
| 93 | + with transaction.atomic(): |
| 94 | + UserMailingListSubscription.objects.update_or_create( |
| 95 | + user=user, |
| 96 | + list_id=lid, |
| 97 | + defaults={"email": email, "status": SubscriptionStatus.PENDING}, |
| 98 | + ) |
| 99 | + succeeded.append(lid) |
| 100 | + except IntegrityError: |
| 101 | + pass |
| 102 | + |
| 103 | + if not succeeded: |
| 104 | + return [], None |
| 105 | + |
| 106 | + try: |
| 107 | + _send_confirmation_email(request, email, user.pk, succeeded) |
| 108 | + except Exception as exc: |
| 109 | + logger.error("Failed to send confirmation email to %s: %s", email, exc) |
| 110 | + UserMailingListSubscription.objects.filter( |
| 111 | + user=user, list_id__in=succeeded |
| 112 | + ).delete() |
| 113 | + return [], "Could not send confirmation email. Please try again." |
| 114 | + |
| 115 | + return succeeded, None |
| 116 | + |
| 117 | + |
81 | 118 | def _send_confirmation_email( |
82 | 119 | request, email: str, user_id: int | None, list_ids: list[str] |
83 | 120 | ) -> None: |
@@ -589,38 +626,20 @@ def _handle_authenticated(self, request, email, list_ids, managed_lists): |
589 | 626 | manage_url=manage_url, |
590 | 627 | ) |
591 | 628 |
|
592 | | - succeeded = [] |
593 | | - for lid in to_subscribe: |
594 | | - try: |
595 | | - with transaction.atomic(): |
596 | | - UserMailingListSubscription.objects.update_or_create( |
597 | | - user=request.user, |
598 | | - list_id=lid, |
599 | | - defaults={"email": email, "status": SubscriptionStatus.PENDING}, |
600 | | - ) |
601 | | - succeeded.append(lid) |
602 | | - except IntegrityError: |
603 | | - pass |
| 629 | + succeeded, error = _subscribe_pending( |
| 630 | + request, request.user, email, to_subscribe |
| 631 | + ) |
604 | 632 |
|
605 | | - if not succeeded: |
| 633 | + if error: |
606 | 634 | return self._card( |
607 | | - request, |
608 | | - state="error", |
609 | | - error_message="Could not subscribe. Please try again.", |
610 | | - user_email=email, |
| 635 | + request, state="error", error_message=error, user_email=email |
611 | 636 | ) |
612 | 637 |
|
613 | | - try: |
614 | | - _send_confirmation_email(request, email, request.user.pk, succeeded) |
615 | | - except Exception as exc: |
616 | | - logger.error("Failed to send confirmation email to %s: %s", email, exc) |
617 | | - UserMailingListSubscription.objects.filter( |
618 | | - user=request.user, list_id__in=succeeded |
619 | | - ).delete() |
| 638 | + if not succeeded: |
620 | 639 | return self._card( |
621 | 640 | request, |
622 | 641 | state="error", |
623 | | - error_message="Could not send confirmation email. Please try again.", |
| 642 | + error_message="Could not subscribe. Please try again.", |
624 | 643 | user_email=email, |
625 | 644 | ) |
626 | 645 |
|
@@ -652,3 +671,33 @@ def _handle_anonymous(self, request, email, list_ids): |
652 | 671 | ) |
653 | 672 |
|
654 | 673 | return self._card(request, state="pending", user_email=email) |
| 674 | + |
| 675 | + |
| 676 | +class PostAuthSubscribeView(LoginRequiredMixin, View): |
| 677 | + """Subscribe to one or more lists from the post-login homepage modal. |
| 678 | +
|
| 679 | + Only for authenticated users. Returns an empty fragment so HTMX removes |
| 680 | + the modal from the DOM. Falls back to a homepage redirect for non-HTMX. |
| 681 | + """ |
| 682 | + |
| 683 | + def post(self, request): |
| 684 | + email = (request.POST.get("email") or "").strip() or request.user.email |
| 685 | + managed_lists = set(settings.MAILMAN_LISTS) |
| 686 | + list_ids = [ |
| 687 | + lid for lid in request.POST.getlist("list_id") if lid in managed_lists |
| 688 | + ] |
| 689 | + |
| 690 | + if list_ids and not _is_rate_limited(request): |
| 691 | + current = { |
| 692 | + sub.list_id |
| 693 | + for sub in UserMailingListSubscription.objects.filter( |
| 694 | + user=request.user, list_id__in=managed_lists |
| 695 | + ) |
| 696 | + } |
| 697 | + to_subscribe = [lid for lid in list_ids if lid not in current] |
| 698 | + |
| 699 | + _subscribe_pending(request, request.user, email, to_subscribe) |
| 700 | + |
| 701 | + if _is_htmx(request): |
| 702 | + return HttpResponse("") |
| 703 | + return _prg_redirect(request) |
0 commit comments