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

23 

24# Exports 

25 

26__all__ = ( 

27 "Login", 

28 "LoginRedirect", 

29 "Logout", 

30) 

31 

32# Constants 

33 

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

35 

36# Views 

37 

38 

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: 

46  

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. 

50  

51 .. code-block: python 

52 

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) 

58 

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 

64  

65 """ 

66 

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

71 

72 max_minutes = SUPERDJANGO.USER_MAX_LOGIN_MINUTES 

73 """The number of minutes (int) between login attempts.""" 

74 

75 namespace = None 

76 

77 password_reset_text = _("Forgot your password?") 

78 """The text to display for the password reset URL, when available.""" 

79 

80 password_reset_url = None 

81 """The URL if any that allows a user to reset his/her password. Tip: Use ``reverse_lazy()``.""" 

82 

83 pattern_name = "accounts_login" 

84 pattern_value = 'login/' 

85 

86 # noinspection PyUnresolvedReferences 

87 template_name = "accounts/login.html" 

88 

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) 

92 

93 context['cancel_url'] = self.get_password_reset_url() 

94 context['cancel_text'] = self.password_reset_text 

95 

96 return context 

97 

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 

102 

103 return DefaultAuthenticationForm 

104 

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

109 

110 # noinspection PyUnresolvedReferences 

111 self.history_class.objects.create(success=False, user=user) 

112 

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. 

116 

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) 

121 

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

131 

132 return super().form_invalid(form) 

133 

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) 

139 

140 if callable(self.history_class): 

141 # noinspection PyUnresolvedReferences 

142 self.history_class.objects.create(success=True, user=form.get_user()) 

143 

144 return super().form_valid(form) 

145 

146 def get_password_reset_url(self): 

147 """Get the URL for password resets. 

148 

149 :rtype: str | None 

150 

151 """ 

152 if self.password_reset_url is not None: 

153 return self.password_reset_url 

154 

155 pattern_name = "accounts_password_reset" 

156 if self.namespace is not None: 

157 pattern_name = "%s:%s" % (self.namespace, "accounts_password_reset") 

158 

159 try: 

160 return reverse(pattern_name) 

161 except NoReverseMatch: 

162 return None 

163 

164 def get_redirect_url(self): 

165 """Override to return ``accounts_login_redirect`` (if it exists) when no originating URL has been provided. 

166 

167 .. note:: 

168 ``get_success_url()`` behaves as usual. 

169 

170 """ 

171 url = super().get_redirect_url() 

172 

173 # An originating has been provided and is safe. 

174 if url: 

175 return url 

176 

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

183 

184 

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 

190 

191 def get_redirect_url(self, *args, **kwargs): 

192 """Get the user's preferred redirect if possible or the `LOGIN_REDIRECT_URL`_ by default. 

193 

194 .. _LOGIN_REDIRECT_URL: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LOGIN_REDIRECT_URL 

195 

196 """ 

197 results = hooks.get_hooks("get_login_redirect_url") 

198 if results: 

199 return results[0](self.request) 

200 

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 

207 

208 return resolve_url(LOGIN_REDIRECT_URL) 

209 

210 

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" 

217 

218 def get_context_data(self, **kwargs): 

219 """Add ``login_url``, if available.""" 

220 context = super(Logout, self).get_context_data(**kwargs) 

221 

222 try: 

223 context['login_url'] = reverse("accounts_login") 

224 except NoReverseMatch: 

225 context['login_url'] = None 

226 

227 return context