|
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 |
@@ -83,6 +83,43 @@ def _is_rate_limited(request) -> bool: |
83 | 83 | return cache.incr(key) > _SUBSCRIBE_RATE_LIMIT |
84 | 84 |
|
85 | 85 |
|
| 86 | +def _subscribe_pending( |
| 87 | + request, user, email: str, list_ids: list[str] |
| 88 | +) -> tuple[list[str], str | None]: |
| 89 | + """Create PENDING subscription records and send a confirmation email. |
| 90 | +
|
| 91 | + Returns (succeeded, error_message). On email failure the records are |
| 92 | + rolled back and error_message is set; on partial IntegrityError the |
| 93 | + affected list is silently skipped. |
| 94 | + """ |
| 95 | + succeeded = [] |
| 96 | + for lid in list_ids: |
| 97 | + try: |
| 98 | + with transaction.atomic(): |
| 99 | + UserMailingListSubscription.objects.update_or_create( |
| 100 | + user=user, |
| 101 | + list_id=lid, |
| 102 | + defaults={"email": email, "status": SubscriptionStatus.PENDING}, |
| 103 | + ) |
| 104 | + succeeded.append(lid) |
| 105 | + except IntegrityError: |
| 106 | + pass |
| 107 | + |
| 108 | + if not succeeded: |
| 109 | + return [], None |
| 110 | + |
| 111 | + try: |
| 112 | + _send_confirmation_email(request, email, user.pk, succeeded) |
| 113 | + except Exception as exc: |
| 114 | + logger.error("Failed to send confirmation email to %s: %s", email, exc) |
| 115 | + UserMailingListSubscription.objects.filter( |
| 116 | + user=user, list_id__in=succeeded |
| 117 | + ).delete() |
| 118 | + return [], "Could not send confirmation email. Please try again." |
| 119 | + |
| 120 | + return succeeded, None |
| 121 | + |
| 122 | + |
86 | 123 | def _send_confirmation_email( |
87 | 124 | request, email: str, user_id: int | None, list_ids: list[str] |
88 | 125 | ) -> None: |
@@ -594,38 +631,20 @@ def _handle_authenticated(self, request, email, list_ids, managed_lists): |
594 | 631 | manage_url=manage_url, |
595 | 632 | ) |
596 | 633 |
|
597 | | - succeeded = [] |
598 | | - for lid in to_subscribe: |
599 | | - try: |
600 | | - with transaction.atomic(): |
601 | | - UserMailingListSubscription.objects.update_or_create( |
602 | | - user=request.user, |
603 | | - list_id=lid, |
604 | | - defaults={"email": email, "status": SubscriptionStatus.PENDING}, |
605 | | - ) |
606 | | - succeeded.append(lid) |
607 | | - except IntegrityError: |
608 | | - pass |
| 634 | + succeeded, error = _subscribe_pending( |
| 635 | + request, request.user, email, to_subscribe |
| 636 | + ) |
609 | 637 |
|
610 | | - if not succeeded: |
| 638 | + if error: |
611 | 639 | return self._card( |
612 | | - request, |
613 | | - state="error", |
614 | | - error_message="Could not subscribe. Please try again.", |
615 | | - user_email=email, |
| 640 | + request, state="error", error_message=error, user_email=email |
616 | 641 | ) |
617 | 642 |
|
618 | | - try: |
619 | | - _send_confirmation_email(request, email, request.user.pk, succeeded) |
620 | | - except Exception as exc: |
621 | | - logger.error("Failed to send confirmation email to %s: %s", email, exc) |
622 | | - UserMailingListSubscription.objects.filter( |
623 | | - user=request.user, list_id__in=succeeded |
624 | | - ).delete() |
| 643 | + if not succeeded: |
625 | 644 | return self._card( |
626 | 645 | request, |
627 | 646 | state="error", |
628 | | - error_message="Could not send confirmation email. Please try again.", |
| 647 | + error_message="Could not subscribe. Please try again.", |
629 | 648 | user_email=email, |
630 | 649 | ) |
631 | 650 |
|
@@ -657,3 +676,33 @@ def _handle_anonymous(self, request, email, list_ids): |
657 | 676 | ) |
658 | 677 |
|
659 | 678 | return self._card(request, state="pending", user_email=email) |
| 679 | + |
| 680 | + |
| 681 | +class PostAuthSubscribeView(LoginRequiredMixin, View): |
| 682 | + """Subscribe to one or more lists from the post-login homepage modal. |
| 683 | +
|
| 684 | + Only for authenticated users. Returns an empty fragment so HTMX removes |
| 685 | + the modal from the DOM. Falls back to a homepage redirect for non-HTMX. |
| 686 | + """ |
| 687 | + |
| 688 | + def post(self, request): |
| 689 | + email = (request.POST.get("email") or "").strip() or request.user.email |
| 690 | + managed_lists = set(constants.MAILMAN_LISTS) |
| 691 | + list_ids = [ |
| 692 | + lid for lid in request.POST.getlist("list_id") if lid in managed_lists |
| 693 | + ] |
| 694 | + |
| 695 | + if list_ids and not _is_rate_limited(request): |
| 696 | + current = { |
| 697 | + sub.list_id |
| 698 | + for sub in UserMailingListSubscription.objects.filter( |
| 699 | + user=request.user, list_id__in=managed_lists |
| 700 | + ) |
| 701 | + } |
| 702 | + to_subscribe = [lid for lid in list_ids if lid not in current] |
| 703 | + |
| 704 | + _subscribe_pending(request, request.user, email, to_subscribe) |
| 705 | + |
| 706 | + if _is_htmx(request): |
| 707 | + return HttpResponse("") |
| 708 | + return _prg_redirect(request) |
0 commit comments