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.models import AbstractUser
13from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView, PasswordResetConfirmView, \
14 PasswordResetView
15from django.contrib.auth.forms import PasswordResetForm as DefaultPasswordResetForm
16from django.http import Http404
17from django.shortcuts import resolve_url
18from django.urls import reverse, NoReverseMatch
19from superdjango.conf import SUPERDJANGO
20from superdjango.patterns import REGEX_PASSWORD_RESET
21from superdjango.views import TemplateView, ViewSet
22from .forms import PasswordResetForm
24# Exports
26__all__ = (
27 "PasswordChange",
28 "PasswordChangeComplete",
29 "PasswordReset",
30 "PasswordResetComplete",
31 "PasswordResetConfirm",
32 "PasswordResetSubmit",
33 "PasswordsViewSet",
34)
36# Constants
38LOGIN_REDIRECT_URL = getattr(settings, "LOGIN_REDIRECT_URL", "/")
40# Views
43class PasswordChange(PasswordChangeView):
44 """Present and validate the password change view."""
45 cancel_url = None
46 """The URL to which the user is sent when cancelling the password change. Tip: use ``reverse_lazy()``."""
48 pattern_name = "accounts_password_change"
49 pattern_value = 'password/change/'
51 # noinspection PyUnresolvedReferences
52 template_name = "accounts/password_change.html"
54 def get_cancel_url(self):
55 """Get the ``cancel_url``, or if one has not been specified, attempt to return ``accounts_profile``.
57 :rtype: str | None
59 """
60 if self.cancel_url is not None:
61 return self.cancel_url
63 try:
64 return reverse("accounts_profile")
65 except NoReverseMatch:
66 return None
68 def get_context_data(self, **kwargs):
69 """Attempt to add ``cancel_url``."""
70 context = super().get_context_data(**kwargs)
72 context['cancel_url'] = self.get_cancel_url()
74 return context
76 def get_success_url(self):
77 """Get the user profile URL (``accounts_profile``) if possible or the URL for password change complete
78 (``accounts_password_change_complete``).
79 """
80 try:
81 return reverse("accounts_profile")
82 except NoReverseMatch:
83 return reverse("accounts_password_change_complete")
86class PasswordChangeComplete(PasswordChangeDoneView):
87 """The default page to which users are sent after a password change has been completed."""
88 pattern_name = "accounts_password_change_complete"
89 pattern_value = 'password/change/complete/'
91 # noinspection PyUnresolvedReferences
92 template_name = "accounts/password_change_complete.html"
95class PasswordReset(PasswordResetView):
96 """Enable password resets from the login form. This is step 1 of the process."""
97 email_template_name = "accounts/password_reset_email.txt"
99 history_class = None
100 """A model that may be used to record both successful and unsuccessful login attempts. This class must define six
101 (6) fields:
103 1. ``added_dt``: The date and time the record was created, set to ``auto_now_add``.
104 2. ``email``: The email address submitted with the request.
105 3. ``ip_address``: A ``GenericIPAddressField``. Not required.
106 4. ``success``: A boolean indicating success or failure.
107 5. ``user_agent``: A ``CharField``. Not required.
108 6. ``username``: A ``CharField``. Not required.
110 .. code-block: python
112 # models.py
113 class PasswordResetHistory(models.Model):
114 added_dt = models.DateTimeField(auto_now_add=True)
115 email = models.EmailField()
116 ip_address = models.GenericIPAddressField(blank=True, null=True)
117 success = models.BooleanField()
118 user_agent = models.CharField(blank=True, max_length=256)
119 username = models.CharField(blank=True, max_length=255)
121 # views.py
122 from superdjango.contrib.accounts.auth.views import Login as LoginView
123 from .models import PasswordResetHistory
124 class Login(LoginView):
125 history_class = PasswordResetHistory
127 """
129 max_attempts = SUPERDJANGO.USER_MAX_PASSWORD_RESET_ATTEMPTS
130 """The maximum number of password reset attempts (int) before a user account is locked."""
132 max_minutes = SUPERDJANGO.USER_MAX_PASSWORD_RESET_MINUTES
133 """The number of minutes (int) between max password reset attempts."""
135 pattern_name = "accounts_password_reset"
136 pattern_value = 'password/reset/'
137 subject_template_name = 'accounts/password_reset_subject.txt'
139 # noinspection PyUnresolvedReferences
140 template_name = "accounts/password_reset.html"
142 username_enabled = SUPERDJANGO.USER_PASSWORD_RESET_USERNAME_ENABLED
143 """When ``True``, the reset lookup will also attempt to find a given user name if one has been given."""
145 def form_invalid(self, form):
146 """Log unsuccessful password reset request when ``history_class`` is callable."""
147 if callable(self.history_class):
149 if self.max_attempts is not None and self.max_minutes is not None:
150 users = form.get_users(form.cleaned_data['email'])
152 for user in users:
153 # noinspection PyUnresolvedReferences
154 self.history_class.objects.create(
155 email=user.email,
156 ip_address=self.request.META.get('REMOTE_ADDR'),
157 success=True,
158 user_agent=self.request.META.get('HTTP_USER_AGENT'),
159 username=getattr(user, AbstractUser.USERNAME_FIELD),
160 )
162 end = DateTime()
163 start = DateTime()
164 start.decrement(minutes=self.max_minutes)
166 # TODO: See todo for Login view and unlocking accounts.
168 # noinspection PyUnresolvedReferences
169 total_attempts = self.history_class.filter(user=user, added_dt__range=[end.dt, start.dt]).count()
170 if total_attempts >= self.max_attempts:
171 user.is_active = False
172 user.save(update_fields=["is_active"])
173 try:
174 return reverse("accounts_locked")
175 except NoReverseMatch:
176 raise Http404("Max password reset attempts have been reached.")
178 return super().form_invalid(form)
180 def form_valid(self, form):
181 """Log a successful password reset request when ``history_class`` is callable."""
182 if callable(self.history_class):
183 users = form.get_users(form.cleaned_data['email'])
184 for user in users:
185 # noinspection PyUnresolvedReferences
186 self.history_class.objects.create(
187 email=user.email,
188 ip_address=self.request.META.get('REMOTE_ADDR'),
189 success=True,
190 user_agent=self.request.META.get('HTTP_USER_AGENT'),
191 username=getattr(user, AbstractUser.USERNAME_FIELD),
192 )
194 return super().form_valid(form)
196 def get_form_class(self):
197 """Use the username form when ``username_enabled`` is ``True``."""
198 if self.username_enabled:
199 return PasswordResetForm
201 return DefaultPasswordResetForm
203 def get_success_url(self):
204 """Get the password reset submitted URL."""
205 return reverse("accounts_password_reset_submit")
208class PasswordResetSubmit(TemplateView):
209 """Presents a page after a password reset request has been submitted. This is step 2 of the process."""
210 pattern_name = "accounts_password_reset_submit"
211 pattern_value = 'password/reset/submit/'
212 template_name = "accounts/password_reset_submit.html"
215class PasswordResetConfirm(PasswordResetConfirmView):
216 """A user follows the password reset link to this page in order to reset his or her password. This is step 3 of the
217 process.
218 """
219 pattern_name = "accounts_password_reset_confirm"
220 pattern_regex = 'password/reset/confirm/%s/' % REGEX_PASSWORD_RESET
221 post_reset_login = SUPERDJANGO.USER_PASSWORD_RESET_LOGIN
223 # noinspection PyUnresolvedReferences
224 template_name = "accounts/password_reset_confirm.html"
226 def get_success_url(self):
227 """Get the complete (log in again) page or intelligent redirect depending upon ``post_reset_login``."""
228 if self.post_reset_login:
229 try:
230 return reverse("accounts_login_redirect")
231 except NoReverseMatch:
232 return resolve_url(LOGIN_REDIRECT_URL)
234 return reverse("accounts_password_reset_complete")
237class PasswordResetComplete(TemplateView):
238 """Display a page after the password reset has been completed. This is step 4 of the process if post reset login is
239 not enabled.
240 """
241 pattern_name = "accounts_password_reset_complete"
242 pattern_value = 'password/reset/complete/'
244 # noinspection PyUnresolvedReferences
245 template_name = "accounts/password_reset_complete.html"
247# BaseView Sets
250class PasswordsViewSet(ViewSet):
252 views = [
253 PasswordChange,
254 PasswordChangeComplete,
255 PasswordReset,
256 PasswordResetSubmit,
257 PasswordResetConfirm,
258 PasswordResetComplete,
259 ]