Source code for superdjango.contrib.accounts.passwords.views

"""
These views wrap Django's `authentication`_ views to provide template output. This also allows ad hoc customization if
needed.

.. _authentication: https://docs.djangoproject.com/en/stable/topics/auth/default/#module-django.contrib.auth.views

"""
# Imports

from datetime_machine import DateTime
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView, PasswordResetConfirmView, \
    PasswordResetView
from django.contrib.auth.forms import PasswordResetForm as DefaultPasswordResetForm
from django.http import Http404
from django.shortcuts import resolve_url
from django.urls import reverse, NoReverseMatch
from superdjango.conf import SUPERDJANGO
from superdjango.patterns import REGEX_PASSWORD_RESET
from superdjango.views import TemplateView, ViewSet
from .forms import PasswordResetForm

# Exports

__all__ = (
    "PasswordChange",
    "PasswordChangeComplete",
    "PasswordReset",
    "PasswordResetComplete",
    "PasswordResetConfirm",
    "PasswordResetSubmit",
    "PasswordsViewSet",
)

# Constants

LOGIN_REDIRECT_URL = getattr(settings, "LOGIN_REDIRECT_URL", "/")

# Views


[docs]class PasswordChange(PasswordChangeView): """Present and validate the password change view.""" cancel_url = None """The URL to which the user is sent when cancelling the password change. Tip: use ``reverse_lazy()``.""" pattern_name = "accounts_password_change" pattern_value = 'password/change/' # noinspection PyUnresolvedReferences template_name = "accounts/password_change.html"
[docs] def get_cancel_url(self): """Get the ``cancel_url``, or if one has not been specified, attempt to return ``accounts_profile``. :rtype: str | None """ if self.cancel_url is not None: return self.cancel_url try: return reverse("accounts_profile") except NoReverseMatch: return None
[docs] def get_context_data(self, **kwargs): """Attempt to add ``cancel_url``.""" context = super().get_context_data(**kwargs) context['cancel_url'] = self.get_cancel_url() return context
[docs] def get_success_url(self): """Get the user profile URL (``accounts_profile``) if possible or the URL for password change complete (``accounts_password_change_complete``). """ try: return reverse("accounts_profile") except NoReverseMatch: return reverse("accounts_password_change_complete")
[docs]class PasswordChangeComplete(PasswordChangeDoneView): """The default page to which users are sent after a password change has been completed.""" pattern_name = "accounts_password_change_complete" pattern_value = 'password/change/complete/' # noinspection PyUnresolvedReferences template_name = "accounts/password_change_complete.html"
[docs]class PasswordReset(PasswordResetView): """Enable password resets from the login form. This is step 1 of the process.""" email_template_name = "accounts/password_reset_email.txt" history_class = None """A model that may be used to record both successful and unsuccessful login attempts. This class must define six (6) fields: 1. ``added_dt``: The date and time the record was created, set to ``auto_now_add``. 2. ``email``: The email address submitted with the request. 3. ``ip_address``: A ``GenericIPAddressField``. Not required. 4. ``success``: A boolean indicating success or failure. 5. ``user_agent``: A ``CharField``. Not required. 6. ``username``: A ``CharField``. Not required. .. code-block: python # models.py class PasswordResetHistory(models.Model): added_dt = models.DateTimeField(auto_now_add=True) email = models.EmailField() ip_address = models.GenericIPAddressField(blank=True, null=True) success = models.BooleanField() user_agent = models.CharField(blank=True, max_length=256) username = models.CharField(blank=True, max_length=255) # views.py from superdjango.contrib.accounts.auth.views import Login as LoginView from .models import PasswordResetHistory class Login(LoginView): history_class = PasswordResetHistory """ max_attempts = SUPERDJANGO.USER_MAX_PASSWORD_RESET_ATTEMPTS """The maximum number of password reset attempts (int) before a user account is locked.""" max_minutes = SUPERDJANGO.USER_MAX_PASSWORD_RESET_MINUTES """The number of minutes (int) between max password reset attempts.""" pattern_name = "accounts_password_reset" pattern_value = 'password/reset/' subject_template_name = 'accounts/password_reset_subject.txt' # noinspection PyUnresolvedReferences template_name = "accounts/password_reset.html" username_enabled = SUPERDJANGO.USER_PASSWORD_RESET_USERNAME_ENABLED """When ``True``, the reset lookup will also attempt to find a given user name if one has been given."""
[docs] def form_invalid(self, form): """Log unsuccessful password reset request when ``history_class`` is callable.""" if callable(self.history_class): if self.max_attempts is not None and self.max_minutes is not None: users = form.get_users(form.cleaned_data['email']) for user in users: # noinspection PyUnresolvedReferences self.history_class.objects.create( email=user.email, ip_address=self.request.META.get('REMOTE_ADDR'), success=True, user_agent=self.request.META.get('HTTP_USER_AGENT'), username=getattr(user, AbstractUser.USERNAME_FIELD), ) end = DateTime() start = DateTime() start.decrement(minutes=self.max_minutes) # TODO: See todo for Login view and unlocking accounts. # noinspection PyUnresolvedReferences total_attempts = self.history_class.filter(user=user, added_dt__range=[end.dt, start.dt]).count() if total_attempts >= self.max_attempts: user.is_active = False user.save(update_fields=["is_active"]) try: return reverse("accounts_locked") except NoReverseMatch: raise Http404("Max password reset attempts have been reached.") return super().form_invalid(form)
[docs] def form_valid(self, form): """Log a successful password reset request when ``history_class`` is callable.""" if callable(self.history_class): users = form.get_users(form.cleaned_data['email']) for user in users: # noinspection PyUnresolvedReferences self.history_class.objects.create( email=user.email, ip_address=self.request.META.get('REMOTE_ADDR'), success=True, user_agent=self.request.META.get('HTTP_USER_AGENT'), username=getattr(user, AbstractUser.USERNAME_FIELD), ) return super().form_valid(form)
[docs] def get_form_class(self): """Use the username form when ``username_enabled`` is ``True``.""" if self.username_enabled: return PasswordResetForm return DefaultPasswordResetForm
[docs] def get_success_url(self): """Get the password reset submitted URL.""" return reverse("accounts_password_reset_submit")
[docs]class PasswordResetSubmit(TemplateView): """Presents a page after a password reset request has been submitted. This is step 2 of the process.""" pattern_name = "accounts_password_reset_submit" pattern_value = 'password/reset/submit/' template_name = "accounts/password_reset_submit.html"
[docs]class PasswordResetConfirm(PasswordResetConfirmView): """A user follows the password reset link to this page in order to reset his or her password. This is step 3 of the process. """ pattern_name = "accounts_password_reset_confirm" pattern_regex = 'password/reset/confirm/%s/' % REGEX_PASSWORD_RESET post_reset_login = SUPERDJANGO.USER_PASSWORD_RESET_LOGIN # noinspection PyUnresolvedReferences template_name = "accounts/password_reset_confirm.html"
[docs] def get_success_url(self): """Get the complete (log in again) page or intelligent redirect depending upon ``post_reset_login``.""" if self.post_reset_login: try: return reverse("accounts_login_redirect") except NoReverseMatch: return resolve_url(LOGIN_REDIRECT_URL) return reverse("accounts_password_reset_complete")
[docs]class PasswordResetComplete(TemplateView): """Display a page after the password reset has been completed. This is step 4 of the process if post reset login is not enabled. """ pattern_name = "accounts_password_reset_complete" pattern_value = 'password/reset/complete/' # noinspection PyUnresolvedReferences template_name = "accounts/password_reset_complete.html"
# BaseView Sets
[docs]class PasswordsViewSet(ViewSet): views = [ PasswordChange, PasswordChangeComplete, PasswordReset, PasswordResetSubmit, PasswordResetConfirm, PasswordResetComplete, ]