"""
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 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,
]