Hide keyboard shortcuts

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. 

4 

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

6 

7""" 

8# Imports 

9 

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 

23 

24# Exports 

25 

26__all__ = ( 

27 "PasswordChange", 

28 "PasswordChangeComplete", 

29 "PasswordReset", 

30 "PasswordResetComplete", 

31 "PasswordResetConfirm", 

32 "PasswordResetSubmit", 

33 "PasswordsViewSet", 

34) 

35 

36# Constants 

37 

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

39 

40# Views 

41 

42 

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()``.""" 

47 

48 pattern_name = "accounts_password_change" 

49 pattern_value = 'password/change/' 

50 

51 # noinspection PyUnresolvedReferences 

52 template_name = "accounts/password_change.html" 

53 

54 def get_cancel_url(self): 

55 """Get the ``cancel_url``, or if one has not been specified, attempt to return ``accounts_profile``. 

56 

57 :rtype: str | None 

58 

59 """ 

60 if self.cancel_url is not None: 

61 return self.cancel_url 

62 

63 try: 

64 return reverse("accounts_profile") 

65 except NoReverseMatch: 

66 return None 

67 

68 def get_context_data(self, **kwargs): 

69 """Attempt to add ``cancel_url``.""" 

70 context = super().get_context_data(**kwargs) 

71 

72 context['cancel_url'] = self.get_cancel_url() 

73 

74 return context 

75 

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") 

84 

85 

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/' 

90 

91 # noinspection PyUnresolvedReferences 

92 template_name = "accounts/password_change_complete.html" 

93 

94 

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" 

98 

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: 

102 

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. 

109 

110 .. code-block: python 

111 

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) 

120 

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 

126 

127 """ 

128 

129 max_attempts = SUPERDJANGO.USER_MAX_PASSWORD_RESET_ATTEMPTS 

130 """The maximum number of password reset attempts (int) before a user account is locked.""" 

131 

132 max_minutes = SUPERDJANGO.USER_MAX_PASSWORD_RESET_MINUTES 

133 """The number of minutes (int) between max password reset attempts.""" 

134 

135 pattern_name = "accounts_password_reset" 

136 pattern_value = 'password/reset/' 

137 subject_template_name = 'accounts/password_reset_subject.txt' 

138 

139 # noinspection PyUnresolvedReferences 

140 template_name = "accounts/password_reset.html" 

141 

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.""" 

144 

145 def form_invalid(self, form): 

146 """Log unsuccessful password reset request when ``history_class`` is callable.""" 

147 if callable(self.history_class): 

148 

149 if self.max_attempts is not None and self.max_minutes is not None: 

150 users = form.get_users(form.cleaned_data['email']) 

151 

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 ) 

161 

162 end = DateTime() 

163 start = DateTime() 

164 start.decrement(minutes=self.max_minutes) 

165 

166 # TODO: See todo for Login view and unlocking accounts. 

167 

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.") 

177 

178 return super().form_invalid(form) 

179 

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 ) 

193 

194 return super().form_valid(form) 

195 

196 def get_form_class(self): 

197 """Use the username form when ``username_enabled`` is ``True``.""" 

198 if self.username_enabled: 

199 return PasswordResetForm 

200 

201 return DefaultPasswordResetForm 

202 

203 def get_success_url(self): 

204 """Get the password reset submitted URL.""" 

205 return reverse("accounts_password_reset_submit") 

206 

207 

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" 

213 

214 

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 

222 

223 # noinspection PyUnresolvedReferences 

224 template_name = "accounts/password_reset_confirm.html" 

225 

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) 

233 

234 return reverse("accounts_password_reset_complete") 

235 

236 

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/' 

243 

244 # noinspection PyUnresolvedReferences 

245 template_name = "accounts/password_reset_complete.html" 

246 

247# BaseView Sets 

248 

249 

250class PasswordsViewSet(ViewSet): 

251 

252 views = [ 

253 PasswordChange, 

254 PasswordChangeComplete, 

255 PasswordReset, 

256 PasswordResetSubmit, 

257 PasswordResetConfirm, 

258 PasswordResetComplete, 

259 ]