Coverage for baibu/users/views.py: 97%
94 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
1import logging
3from django.conf import settings
4from django.contrib import messages
5from django.contrib.auth import logout
6from django.contrib.auth.decorators import login_required
7from django.contrib.auth.mixins import LoginRequiredMixin
8from django.contrib.messages.views import SuccessMessageMixin
9from django.core.cache import cache
10from django.http import HttpRequest
11from django.http import HttpResponse
12from django.shortcuts import redirect
13from django.shortcuts import render
14from django.urls import reverse
15from django.utils.crypto import salted_hmac
16from django.utils.http import url_has_allowed_host_and_scheme
17from django.utils.translation import gettext_lazy as _
18from django.views.decorators.cache import cache_control
19from django.views.decorators.http import require_http_methods
20from django.views.generic import UpdateView
22from .account_deletion_requests import capture_account_deletion_request
23from .consent import consent_state
24from .consent import record_consent
25from .forms import AccountDeletionRequestForm
26from .forms import ConsentForm
27from .forms import UserAccountForm
28from .middleware import missing_profile_fields
29from .models import AccountDeletionRequest
30from .models import User
32logger = logging.getLogger(__name__)
35class UserAccountView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
36 model = User
37 form_class = UserAccountForm
38 template_name = "users/account.html"
39 success_message = _("Your profile was updated.")
41 def get_object(self, queryset=None) -> User:
42 return self.request.user
44 def get_success_url(self) -> str:
45 return reverse("users:account")
47 def get_context_data(self, **kwargs):
48 context = super().get_context_data(**kwargs)
49 context["consent_state"] = consent_state(user=self.request.user)
50 return context
53account_view = cache_control(private=True, no_store=True)(UserAccountView.as_view())
56class CompleteProfileView(UserAccountView):
57 template_name = "users/complete_profile.html"
58 success_message = _("Thanks, your profile is complete.")
60 def get_form(self, form_class=None):
61 form = super().get_form(form_class)
62 for field in missing_profile_fields(self.request.user):
63 form.fields[field].required = True
64 return form
66 def get_success_url(self) -> str:
67 next_url = self.request.GET.get("next", "")
68 if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts={self.request.get_host()}):
69 return next_url
70 return reverse("users:account")
73complete_profile_view = cache_control(private=True, no_store=True)(CompleteProfileView.as_view())
76@login_required
77@cache_control(private=True, no_store=True)
78@require_http_methods(["GET", "POST"])
79def consent_view(request: HttpRequest) -> HttpResponse:
80 state = consent_state(user=request.user)
81 form = ConsentForm(request.POST or None, initial={"tier": state.tier if state.latest_record else None})
82 if request.method == "POST" and form.is_valid():
83 new_tier = form.cleaned_data["tier"]
84 if state.latest_record is None or new_tier != state.tier:
85 record_consent(
86 user=request.user,
87 tier=new_tier,
88 source="account_settings",
89 metadata={
90 "locale": getattr(request, "LANGUAGE_CODE", settings.LANGUAGE_CODE),
91 "consent_text_version": settings.CONSENT_TEXT_VERSION,
92 },
93 )
94 messages.success(request, _("Your choice was saved."))
95 return redirect("users:consent")
96 history = request.user.consent_records.filter(scope=settings.CONSENT_DEFAULT_SCOPE)[:20]
97 return render(request, "users/consent.html", {"form": form, "consent_state": state, "history": history})
100@login_required
101@cache_control(private=True, no_store=True)
102@require_http_methods(["GET", "POST"])
103def account_deletion_view(request: HttpRequest) -> HttpResponse:
104 form = AccountDeletionRequestForm(request.POST or None)
105 if request.method == "POST" and form.is_valid():
106 if _deletion_rate_limited(request):
107 form.add_error(None, _("Too many requests. Please wait before trying again."))
108 else:
109 deletion_request, _created = capture_account_deletion_request(
110 source=AccountDeletionRequest.Source.IN_APP,
111 user=request.user,
112 requester_name=request.user.name,
113 requester_email=request.user.email or "",
114 request_details=form.cleaned_data["request_details"],
115 metadata={"locale": getattr(request, "LANGUAGE_CODE", settings.LANGUAGE_CODE)},
116 )
117 # The account is now inactive, so end the session.
118 logout(request)
119 return render(request, "users/account_deletion_done.html", {"receipt_id": deletion_request.pk})
120 return render(request, "users/account_deletion.html", {"form": form})
123def _deletion_rate_limited(request: HttpRequest) -> bool:
124 digest = salted_hmac("account-deletion-rate", f"user:{request.user.pk}").hexdigest()
125 key = f"account-deletion-rate:{digest}"
126 try:
127 if cache.add(key, 1, timeout=settings.ACCOUNT_DELETION_REQUEST_RATE_WINDOW_SECONDS):
128 return False
129 count = cache.incr(key)
130 except Exception as exc: # noqa: BLE001
131 logger.warning("Account deletion rate limiter unavailable; error_type=%s", type(exc).__name__)
132 return False
133 return count > settings.ACCOUNT_DELETION_REQUEST_RATE_LIMIT