Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2These views wrap Django's `authentication`_ views to provide template output. This also allows ad hoc customization if
3needed.
5.. _authentication: https://docs.djangoproject.com/en/stable/topics/auth/default/#module-django.contrib.auth.views
7"""
8# Imports
10from datetime_machine import DateTime
11from django.conf import settings
12from django.contrib.auth.views import LoginView, LogoutView
13from django.contrib.auth.forms import AuthenticationForm as DefaultAuthenticationForm
14from django.http import Http404
15from django.shortcuts import resolve_url
16from django.urls import reverse, NoReverseMatch
17from django.utils.translation import ugettext_lazy as _
18from superdjango.conf import SUPERDJANGO
19from superdjango.interfaces.hooks import hooks
20from superdjango.views import RedirectView
21from ..utils import get_user_profile
22from .forms import AuthenticationForm
24# Exports
26__all__ = (
27 "Login",
28 "LoginRedirect",
29 "Logout",
30)
32# Constants
34LOGIN_REDIRECT_URL = getattr(settings, "LOGIN_REDIRECT_URL", "/")
36# Views
39class Login(LoginView):
40 """Login form and redirect on successful authentication. Supports "remember me" functionality when
41 ``SUPERDJANGO_REMEMBER_ME_ENABLED`` is ``True``.
42 """
43 history_class = None
44 """A model that may be used to record both successful and unsuccessful login attempts. This class must define three
45 (3) fields:
47 1. ``added_dt``: The date and time the record was created, set to ``auto_now_add``.
48 2. ``success``: A boolean indicating success or failure.
49 3. ``user``: A reference to ``AUTH_USER_MODEL`` that is the subject of the login.
51 .. code-block: python
53 # models.py
54 class LoginHistory(models.Model):
55 added_dt = models.DateTimeField(auto_now_add=True)
56 success = models.BooleanField()
57 user = models.ForeignKey(AUTH_USER_MODEL)
59 # views.py
60 from superdjango.contrib.accounts.auth.views import Login as LoginView
61 from .models import LoginHistory
62 class Login(LoginView):
63 history_class = LoginHistory
65 """
67 max_attempts = SUPERDJANGO.USER_MAX_LOGIN_ATTEMPTS
68 """The maximum number of login attempts (int) before a user account is marked inactive. Requires a
69 ``history_class``.
70 """
72 max_minutes = SUPERDJANGO.USER_MAX_LOGIN_MINUTES
73 """The number of minutes (int) between login attempts."""
75 namespace = None
77 password_reset_text = _("Forgot your password?")
78 """The text to display for the password reset URL, when available."""
80 password_reset_url = None
81 """The URL if any that allows a user to reset his/her password. Tip: Use ``reverse_lazy()``."""
83 pattern_name = "accounts_login"
84 pattern_value = 'login/'
86 # noinspection PyUnresolvedReferences
87 template_name = "accounts/login.html"
89 def get_context_data(self, **kwargs):
90 """Add ``accounts_password_reset`` URL as ``cancel_url`` and ``cancel_text``, if available."""
91 context = super().get_context_data(**kwargs)
93 context['cancel_url'] = self.get_password_reset_url()
94 context['cancel_text'] = self.password_reset_text
96 return context
98 def get_form_class(self):
99 """The default form class is used unless ``SUPERDJANGO_USER_REMEMBER_ME_ENABLED`` is ``True``."""
100 if SUPERDJANGO.USER_REMEMBER_ME_ENABLED:
101 return AuthenticationForm
103 return DefaultAuthenticationForm
105 def from_invalid(self, form):
106 """If ``history_callback`` is callable, the unsuccessful login attempt is recorded."""
107 if callable(self.history_class):
108 user = form.get_user()
110 # noinspection PyUnresolvedReferences
111 self.history_class.objects.create(success=False, user=user)
113 # TODO: Implement management command to unlock user accounts after a specified time has passed. This may
114 # required adding an is_locked field to the user table? Or see accounts.locked.models for a separate locking
115 # model.
117 if self.max_attempts is not None and self.max_minutes is not None:
118 end = DateTime()
119 start = DateTime()
120 start.decrement(minutes=self.max_minutes)
122 # noinspection PyUnresolvedReferences
123 total_attempts = self.history_class.filter(user=user, added_dt__range=[end.dt, start.dt]).count()
124 if total_attempts >= self.max_attempts:
125 user.is_active = False
126 user.save(update_fields=["is_active"])
127 try:
128 return reverse("accounts_locked")
129 except NoReverseMatch:
130 raise Http404("Max login attempts have been reached.")
132 return super().form_invalid(form)
134 def form_valid(self, form):
135 """With valid submit, optionally set session expiration to ``SUPERDJANGO_USER_REMEMBER_ME_SECONDS``. If
136 ``history_callback`` is callable, the successful login is recorded.."""
137 if SUPERDJANGO.USER_REMEMBER_ME_ENABLED:
138 self.request.session.set_expiry(SUPERDJANGO.USER_REMEMBER_ME_SECONDS)
140 if callable(self.history_class):
141 # noinspection PyUnresolvedReferences
142 self.history_class.objects.create(success=True, user=form.get_user())
144 return super().form_valid(form)
146 def get_password_reset_url(self):
147 """Get the URL for password resets.
149 :rtype: str | None
151 """
152 if self.password_reset_url is not None:
153 return self.password_reset_url
155 pattern_name = "accounts_password_reset"
156 if self.namespace is not None:
157 pattern_name = "%s:%s" % (self.namespace, "accounts_password_reset")
159 try:
160 return reverse(pattern_name)
161 except NoReverseMatch:
162 return None
164 def get_redirect_url(self):
165 """Override to return ``accounts_login_redirect`` (if it exists) when no originating URL has been provided.
167 .. note::
168 ``get_success_url()`` behaves as usual.
170 """
171 url = super().get_redirect_url()
173 # An originating has been provided and is safe.
174 if url:
175 return url
177 # Attempt to return accounts_login_redirect, which may or may not be available depending upon the local
178 # configuration.
179 try:
180 return reverse("accounts_login_redirect")
181 except NoReverseMatch:
182 return ""
185class LoginRedirect(RedirectView):
186 """Intelligently redirect the user based on preferences or global settings."""
187 pattern_name = "accounts_login_redirect"
188 pattern_value = 'login/redirect/'
189 permanent = False
191 def get_redirect_url(self, *args, **kwargs):
192 """Get the user's preferred redirect if possible or the `LOGIN_REDIRECT_URL`_ by default.
194 .. _LOGIN_REDIRECT_URL: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LOGIN_REDIRECT_URL
196 """
197 results = hooks.get_hooks("get_login_redirect_url")
198 if results:
199 return results[0](self.request)
201 profile = get_user_profile(self.request.user)
202 if profile is not None:
203 try:
204 return profile.redirect_url
205 except AttributeError:
206 pass
208 return resolve_url(LOGIN_REDIRECT_URL)
211class Logout(LogoutView):
212 """Log the user out."""
213 pattern_name = "accounts_logout"
214 pattern_value = 'logout/'
215 # noinspection PyUnresolvedReferences
216 template_name = "accounts/logout.html"
218 def get_context_data(self, **kwargs):
219 """Add ``login_url``, if available."""
220 context = super(Logout, self).get_context_data(**kwargs)
222 try:
223 context['login_url'] = reverse("accounts_login")
224 except NoReverseMatch:
225 context['login_url'] = None
227 return context