"""
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.views import LoginView, LogoutView
from django.contrib.auth.forms import AuthenticationForm as DefaultAuthenticationForm
from django.http import Http404
from django.shortcuts import resolve_url
from django.urls import reverse, NoReverseMatch
from django.utils.translation import ugettext_lazy as _
from superdjango.conf import SUPERDJANGO
from superdjango.interfaces.hooks import hooks
from superdjango.views import RedirectView
from ..utils import get_user_profile
from .forms import AuthenticationForm
# Exports
__all__ = (
"Login",
"LoginRedirect",
"Logout",
)
# Constants
LOGIN_REDIRECT_URL = getattr(settings, "LOGIN_REDIRECT_URL", "/")
# Views
[docs]class Login(LoginView):
"""Login form and redirect on successful authentication. Supports "remember me" functionality when
``SUPERDJANGO_REMEMBER_ME_ENABLED`` is ``True``.
"""
history_class = None
"""A model that may be used to record both successful and unsuccessful login attempts. This class must define three
(3) fields:
1. ``added_dt``: The date and time the record was created, set to ``auto_now_add``.
2. ``success``: A boolean indicating success or failure.
3. ``user``: A reference to ``AUTH_USER_MODEL`` that is the subject of the login.
.. code-block: python
# models.py
class LoginHistory(models.Model):
added_dt = models.DateTimeField(auto_now_add=True)
success = models.BooleanField()
user = models.ForeignKey(AUTH_USER_MODEL)
# views.py
from superdjango.contrib.accounts.auth.views import Login as LoginView
from .models import LoginHistory
class Login(LoginView):
history_class = LoginHistory
"""
max_attempts = SUPERDJANGO.USER_MAX_LOGIN_ATTEMPTS
"""The maximum number of login attempts (int) before a user account is marked inactive. Requires a
``history_class``.
"""
max_minutes = SUPERDJANGO.USER_MAX_LOGIN_MINUTES
"""The number of minutes (int) between login attempts."""
namespace = None
password_reset_text = _("Forgot your password?")
"""The text to display for the password reset URL, when available."""
password_reset_url = None
"""The URL if any that allows a user to reset his/her password. Tip: Use ``reverse_lazy()``."""
pattern_name = "accounts_login"
pattern_value = 'login/'
# noinspection PyUnresolvedReferences
template_name = "accounts/login.html"
[docs] def get_context_data(self, **kwargs):
"""Add ``accounts_password_reset`` URL as ``cancel_url`` and ``cancel_text``, if available."""
context = super().get_context_data(**kwargs)
context['cancel_url'] = self.get_password_reset_url()
context['cancel_text'] = self.password_reset_text
return context
[docs] def from_invalid(self, form):
"""If ``history_callback`` is callable, the unsuccessful login attempt is recorded."""
if self.history_class is not None:
user = form.get_user()
# noinspection PyUnresolvedReferences
self.history_class.objects.create(success=False, user=user)
# TODO: Implement management command to unlock user accounts after a specified time has passed. This may
# require adding an is_locked field to the user table? Or see accounts.locked.models for a separate locking
# model.
if self.max_attempts is not None and self.max_minutes is not None:
end = DateTime()
start = DateTime()
start.decrement(minutes=self.max_minutes)
# 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 login attempts have been reached.")
return super().form_invalid(form)
[docs] def get_password_reset_url(self):
"""Get the URL for password resets.
:rtype: str | None
"""
if self.password_reset_url is not None:
return self.password_reset_url
pattern_name = "accounts_password_reset"
if self.namespace is not None:
pattern_name = "%s:%s" % (self.namespace, "accounts_password_reset")
try:
return reverse(pattern_name)
except NoReverseMatch:
return None
[docs] def get_redirect_url(self):
"""Override to return ``accounts_login_redirect`` (if it exists) when no originating URL has been provided.
.. note::
``get_success_url()`` behaves as usual.
"""
url = super().get_redirect_url()
# An originating has been provided and is safe.
if url:
return url
# Attempt to return accounts_login_redirect, which may or may not be available depending upon the local
# configuration.
try:
return reverse("accounts_login_redirect")
except NoReverseMatch:
return ""
[docs]class LoginRedirect(RedirectView):
"""Intelligently redirect the user based on preferences or global settings."""
pattern_name = "accounts_login_redirect"
pattern_value = 'login/redirect/'
permanent = False
[docs] def get_redirect_url(self, *args, **kwargs):
"""Get the user's preferred redirect if possible or the `LOGIN_REDIRECT_URL`_ by default.
.. _LOGIN_REDIRECT_URL: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LOGIN_REDIRECT_URL
"""
results = hooks.get_hooks("get_login_redirect_url")
if results:
return results[0](self.request)
profile = get_user_profile(self.request.user)
if profile is not None:
try:
return profile.redirect_url
except AttributeError:
pass
return resolve_url(LOGIN_REDIRECT_URL)
[docs]class Logout(LogoutView):
"""Log the user out."""
pattern_name = "accounts_logout"
pattern_value = 'logout/'
# noinspection PyUnresolvedReferences
template_name = "accounts/logout.html"
[docs] def get_context_data(self, **kwargs):
"""Add ``login_url``, if available."""
context = super(Logout, self).get_context_data(**kwargs)
try:
context['login_url'] = reverse("accounts_login")
except NoReverseMatch:
context['login_url'] = None
return context