Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #8342 -- Removed code from the admin that assumed that you can'…

…t login with an email address (nixed by r12634). Also refactored login code slightly to be DRY by using more of auth app's forms and views.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14769 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit cc64fb5c4b4315a4ad66e21458e27ece57266847 1 parent 07705ca
Jannis Leidel authored December 02, 2010
43  django/contrib/admin/forms.py
... ...
@@ -0,0 +1,43 @@
  1
+from django import forms
  2
+
  3
+from django.contrib.auth import authenticate
  4
+from django.contrib.auth.forms import AuthenticationForm
  5
+from django.contrib.auth.models import User
  6
+
  7
+from django.utils.translation import ugettext_lazy, ugettext as _
  8
+
  9
+ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. "
  10
+                              "Note that both fields are case-sensitive.")
  11
+
  12
+class AdminAuthenticationForm(AuthenticationForm):
  13
+    """
  14
+    A custom authentication form used in the admin app.
  15
+
  16
+    """
  17
+    this_is_the_login_form = forms.BooleanField(widget=forms.HiddenInput, initial=1,
  18
+        error_messages={'required': ugettext_lazy("Please log in again, because your session has expired.")})
  19
+
  20
+    def clean(self):
  21
+        username = self.cleaned_data.get('username')
  22
+        password = self.cleaned_data.get('password')
  23
+        message = ERROR_MESSAGE
  24
+
  25
+        if username and password:
  26
+            self.user_cache = authenticate(username=username, password=password)
  27
+            if self.user_cache is None:
  28
+                if username is not None and u'@' in username:
  29
+                    # Mistakenly entered e-mail address instead of username? Look it up.
  30
+                    try:
  31
+                        user = User.objects.get(email=username)
  32
+                    except (User.DoesNotExist, User.MultipleObjectsReturned):
  33
+                        # Nothing to do here, moving along.
  34
+                        pass
  35
+                    else:
  36
+                        if user.check_password(password):
  37
+                            message = _("Your e-mail address is not your username."
  38
+                                        " Try '%s' instead.") % user.username
  39
+                raise forms.ValidationError(message)
  40
+            elif not self.user_cache.is_active or not self.user_cache.is_staff:
  41
+                raise forms.ValidationError(message)
  42
+        self.check_for_test_cookie()
  43
+        return self.cleaned_data
109  django/contrib/admin/sites.py
... ...
@@ -1,8 +1,8 @@
1 1
 import re
2 2
 from django import http, template
3  
-from django.contrib.admin import ModelAdmin
4  
-from django.contrib.admin import actions
5  
-from django.contrib.auth import authenticate, login
  3
+from django.contrib.admin import ModelAdmin, actions
  4
+from django.contrib.admin.forms import AdminAuthenticationForm, ERROR_MESSAGE
  5
+from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
6 6
 from django.views.decorators.csrf import csrf_protect
7 7
 from django.db.models.base import ModelBase
8 8
 from django.core.exceptions import ImproperlyConfigured
@@ -15,7 +15,6 @@
15 15
 from django.views.decorators.cache import never_cache
16 16
 from django.conf import settings
17 17
 
18  
-ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
19 18
 LOGIN_FORM_KEY = 'this_is_the_login_form'
20 19
 
21 20
 class AlreadyRegistered(Exception):
@@ -32,7 +31,7 @@ class AdminSite(object):
32 31
     functions that present a full admin interface for the collection of registered
33 32
     models.
34 33
     """
35  
-
  34
+    login_form = None
36 35
     index_template = None
37 36
     app_index_template = None
38 37
     login_template = None
@@ -127,12 +126,12 @@ def get_action(self, name):
127 126
         """
128 127
         return self._global_actions[name]
129 128
 
  129
+    @property
130 130
     def actions(self):
131 131
         """
132 132
         Get all the enabled actions as an iterable of (name, func).
133 133
         """
134 134
         return self._actions.iteritems()
135  
-    actions = property(actions)
136 135
 
137 136
     def has_permission(self, request):
138 137
         """
@@ -240,9 +239,9 @@ def wrapper(*args, **kwargs):
240 239
             )
241 240
         return urlpatterns
242 241
 
  242
+    @property
243 243
     def urls(self):
244 244
         return self.get_urls(), self.app_name, self.name
245  
-    urls = property(urls)
246 245
 
247 246
     def password_change(self, request):
248 247
         """
@@ -254,18 +253,22 @@ def password_change(self, request):
254 253
         else:
255 254
             url = reverse('admin:password_change_done', current_app=self.name)
256 255
         defaults = {
  256
+            'current_app': self.name,
257 257
             'post_change_redirect': url
258 258
         }
259 259
         if self.password_change_template is not None:
260 260
             defaults['template_name'] = self.password_change_template
261 261
         return password_change(request, **defaults)
262 262
 
263  
-    def password_change_done(self, request):
  263
+    def password_change_done(self, request, extra_context=None):
264 264
         """
265 265
         Displays the "success" page after a password change.
266 266
         """
267 267
         from django.contrib.auth.views import password_change_done
268  
-        defaults = {}
  268
+        defaults = {
  269
+            'current_app': self.name,
  270
+            'extra_context': extra_context or {},
  271
+        }
269 272
         if self.password_change_done_template is not None:
270 273
             defaults['template_name'] = self.password_change_done_template
271 274
         return password_change_done(request, **defaults)
@@ -283,69 +286,44 @@ def i18n_javascript(self, request):
283 286
             from django.views.i18n import null_javascript_catalog as javascript_catalog
284 287
         return javascript_catalog(request, packages='django.conf')
285 288
 
286  
-    def logout(self, request):
  289
+    @never_cache
  290
+    def logout(self, request, extra_context=None):
287 291
         """
288 292
         Logs out the user for the given HttpRequest.
289 293
 
290 294
         This should *not* assume the user is already logged in.
291 295
         """
292 296
         from django.contrib.auth.views import logout
293  
-        defaults = {}
  297
+        defaults = {
  298
+            'current_app': self.name,
  299
+            'extra_context': extra_context or {},
  300
+        }
294 301
         if self.logout_template is not None:
295 302
             defaults['template_name'] = self.logout_template
296 303
         return logout(request, **defaults)
297  
-    logout = never_cache(logout)
298 304
 
299  
-    def login(self, request):
  305
+    @never_cache
  306
+    def login(self, request, extra_context=None):
300 307
         """
301 308
         Displays the login form for the given HttpRequest.
302 309
         """
303  
-        from django.contrib.auth.models import User
304  
-
305  
-        # If this isn't already the login page, display it.
306  
-        if LOGIN_FORM_KEY not in request.POST:
307  
-            if request.POST:
308  
-                message = _("Please log in again, because your session has expired.")
309  
-            else:
310  
-                message = ""
311  
-            return self.display_login_form(request, message)
312  
-
313  
-        # Check that the user accepts cookies.
314  
-        if not request.session.test_cookie_worked():
315  
-            message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
316  
-            return self.display_login_form(request, message)
317  
-        else:
318  
-            request.session.delete_test_cookie()
319  
-
320  
-        # Check the password.
321  
-        username = request.POST.get('username', None)
322  
-        password = request.POST.get('password', None)
323  
-        user = authenticate(username=username, password=password)
324  
-        if user is None:
325  
-            message = ERROR_MESSAGE
326  
-            if username is not None and u'@' in username:
327  
-                # Mistakenly entered e-mail address instead of username? Look it up.
328  
-                try:
329  
-                    user = User.objects.get(email=username)
330  
-                except (User.DoesNotExist, User.MultipleObjectsReturned):
331  
-                    message = _("Usernames cannot contain the '@' character.")
332  
-                else:
333  
-                    if user.check_password(password):
334  
-                        message = _("Your e-mail address is not your username."
335  
-                                    " Try '%s' instead.") % user.username
336  
-                    else:
337  
-                        message = _("Usernames cannot contain the '@' character.")
338  
-            return self.display_login_form(request, message)
339  
-
340  
-        # The user data is correct; log in the user in and continue.
341  
-        else:
342  
-            if user.is_active and user.is_staff:
343  
-                login(request, user)
344  
-                return http.HttpResponseRedirect(request.get_full_path())
345  
-            else:
346  
-                return self.display_login_form(request, ERROR_MESSAGE)
347  
-    login = never_cache(login)
  310
+        from django.contrib.auth.views import login
  311
+        context = {
  312
+            'title': _('Log in'),
  313
+            'root_path': self.root_path,
  314
+            'app_path': request.get_full_path(),
  315
+            REDIRECT_FIELD_NAME: request.get_full_path(),
  316
+        }
  317
+        context.update(extra_context or {})
  318
+        defaults = {
  319
+            'extra_context': context,
  320
+            'current_app': self.name,
  321
+            'authentication_form': self.login_form or AdminAuthenticationForm,
  322
+            'template_name': self.login_template or 'admin/login.html',
  323
+        }
  324
+        return login(request, **defaults)
348 325
 
  326
+    @never_cache
349 327
     def index(self, request, extra_context=None):
350 328
         """
351 329
         Displays the main admin index page, which lists all of the installed
@@ -396,21 +374,6 @@ def index(self, request, extra_context=None):
396 374
         return render_to_response(self.index_template or 'admin/index.html', context,
397 375
             context_instance=context_instance
398 376
         )
399  
-    index = never_cache(index)
400  
-
401  
-    def display_login_form(self, request, error_message='', extra_context=None):
402  
-        request.session.set_test_cookie()
403  
-        context = {
404  
-            'title': _('Log in'),
405  
-            'app_path': request.get_full_path(),
406  
-            'error_message': error_message,
407  
-            'root_path': self.root_path,
408  
-        }
409  
-        context.update(extra_context or {})
410  
-        context_instance = template.RequestContext(request, current_app=self.name)
411  
-        return render_to_response(self.login_template or 'admin/login.html', context,
412  
-            context_instance=context_instance
413  
-        )
414 377
 
415 378
     def app_index(self, request, app_label, extra_context=None):
416 379
         user = request.user
22  django/contrib/admin/templates/admin/login.html
@@ -12,17 +12,31 @@
12 12
 {% block breadcrumbs %}{% endblock %}
13 13
 
14 14
 {% block content %}
15  
-{% if error_message %}
16  
-<p class="errornote">{{ error_message }}</p>
  15
+{% if form.errors and not form.non_field_errors and not form.this_is_the_login_form.errors %}
  16
+<p class="errornote">
  17
+{% blocktrans count form.errors.items|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
  18
+</p>
17 19
 {% endif %}
  20
+
  21
+{% if form.non_field_errors or form.this_is_the_login_form.errors %}
  22
+{% for error in form.non_field_errors|add:form.this_is_the_login_form.errors %}
  23
+<p class="errornote">
  24
+    {{ error }}
  25
+</p>
  26
+{% endfor %}
  27
+{% endif %}
  28
+
18 29
 <div id="content-main">
19 30
 <form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
20 31
   <div class="form-row">
21  
-    <label for="id_username">{% trans 'Username:' %}</label> <input type="text" name="username" id="id_username" />
  32
+    {% if not form.this_is_the_login_form.errors %}{{ form.username.errors }}{% endif %}
  33
+    <label for="id_username" class="required">{% trans 'Username:' %}</label> {{ form.username }}
22 34
   </div>
23 35
   <div class="form-row">
24  
-    <label for="id_password">{% trans 'Password:' %}</label> <input type="password" name="password" id="id_password" />
  36
+    {% if not form.this_is_the_login_form.errors %}{{ form.password.errors }}{% endif %}
  37
+    <label for="id_password" class="required">{% trans 'Password:' %}</label> {{ form.password }}
25 38
     <input type="hidden" name="this_is_the_login_form" value="1" />
  39
+    <input type="hidden" name="next" value="{{ next }}" />
26 40
   </div>
27 41
   <div class="submit-row">
28 42
     <label>&nbsp;</label><input type="submit" value="{% trans 'Log in' %}" />
70  django/contrib/admin/views/decorators.py
@@ -3,22 +3,13 @@
3 3
 except ImportError:
4 4
     from django.utils.functional import wraps  # Python 2.4 fallback.
5 5
 
6  
-from django import http, template
7  
-from django.contrib.auth.models import User
8  
-from django.contrib.auth import authenticate, login
  6
+from django import template
9 7
 from django.shortcuts import render_to_response
10  
-from django.utils.translation import ugettext_lazy, ugettext as _
  8
+from django.utils.translation import ugettext as _
11 9
 
12  
-ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
13  
-LOGIN_FORM_KEY = 'this_is_the_login_form'
14  
-
15  
-def _display_login_form(request, error_message=''):
16  
-    request.session.set_test_cookie()
17  
-    return render_to_response('admin/login.html', {
18  
-        'title': _('Log in'),
19  
-        'app_path': request.get_full_path(),
20  
-        'error_message': error_message
21  
-    }, context_instance=template.RequestContext(request))
  10
+from django.contrib.admin.forms import AdminAuthenticationForm
  11
+from django.contrib.auth.views import login
  12
+from django.contrib.auth import REDIRECT_FIELD_NAME
22 13
 
23 14
 def staff_member_required(view_func):
24 15
     """
@@ -31,45 +22,14 @@ def _checklogin(request, *args, **kwargs):
31 22
             return view_func(request, *args, **kwargs)
32 23
 
33 24
         assert hasattr(request, 'session'), "The Django admin requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'."
34  
-
35  
-        # If this isn't already the login page, display it.
36  
-        if LOGIN_FORM_KEY not in request.POST:
37  
-            if request.POST:
38  
-                message = _("Please log in again, because your session has expired.")
39  
-            else:
40  
-                message = ""
41  
-            return _display_login_form(request, message)
42  
-
43  
-        # Check that the user accepts cookies.
44  
-        if not request.session.test_cookie_worked():
45  
-            message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
46  
-            return _display_login_form(request, message)
47  
-        else:
48  
-            request.session.delete_test_cookie()
49  
-
50  
-        # Check the password.
51  
-        username = request.POST.get('username', None)
52  
-        password = request.POST.get('password', None)
53  
-        user = authenticate(username=username, password=password)
54  
-        if user is None:
55  
-            message = ERROR_MESSAGE
56  
-            if '@' in username:
57  
-                # Mistakenly entered e-mail address instead of username? Look it up.
58  
-                users = list(User.objects.filter(email=username))
59  
-                if len(users) == 1 and users[0].check_password(password):
60  
-                    message = _("Your e-mail address is not your username. Try '%s' instead.") % users[0].username
61  
-                else:
62  
-                    # Either we cannot find the user, or if more than 1
63  
-                    # we cannot guess which user is the correct one.
64  
-                    message = _("Usernames cannot contain the '@' character.")
65  
-            return _display_login_form(request, message)
66  
-
67  
-        # The user data is correct; log in the user in and continue.
68  
-        else:
69  
-            if user.is_active and user.is_staff:
70  
-                login(request, user)
71  
-                return http.HttpResponseRedirect(request.get_full_path())
72  
-            else:
73  
-                return _display_login_form(request, ERROR_MESSAGE)
74  
-
  25
+        defaults = {
  26
+            'template_name': 'admin/login.html',
  27
+            'authentication_form': AdminAuthenticationForm,
  28
+            'extra_context': {
  29
+                'title': _('Log in'),
  30
+                'app_path': request.get_full_path(),
  31
+                REDIRECT_FIELD_NAME: request.get_full_path(),
  32
+            },
  33
+        }
  34
+        return login(request, **defaults)
75 35
     return wraps(view_func)(_checklogin)
13  django/contrib/auth/forms.py
@@ -87,14 +87,15 @@ def clean(self):
87 87
                 raise forms.ValidationError(_("Please enter a correct username and password. Note that both fields are case-sensitive."))
88 88
             elif not self.user_cache.is_active:
89 89
                 raise forms.ValidationError(_("This account is inactive."))
90  
-
91  
-        # TODO: determine whether this should move to its own method.
92  
-        if self.request:
93  
-            if not self.request.session.test_cookie_worked():
94  
-                raise forms.ValidationError(_("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in."))
95  
-
  90
+        self.check_for_test_cookie()
96 91
         return self.cleaned_data
97 92
 
  93
+    def check_for_test_cookie(self):
  94
+        if self.request and not self.request.session.test_cookie_worked():
  95
+            raise forms.ValidationError(
  96
+                _("Your Web browser doesn't appear to have cookies enabled. "
  97
+                  "Cookies are required for logging in."))
  98
+
98 99
     def get_user_id(self):
99 100
         if self.user_cache:
100 101
             return self.user_cache.id
35  django/contrib/auth/views.py
@@ -24,7 +24,7 @@
24 24
 def login(request, template_name='registration/login.html',
25 25
           redirect_field_name=REDIRECT_FIELD_NAME,
26 26
           authentication_form=AuthenticationForm,
27  
-          extra_context=None):
  27
+          current_app=None, extra_context=None):
28 28
     """Displays the login form and handles the login action."""
29 29
 
30 30
     redirect_to = request.REQUEST.get(redirect_field_name, '')
@@ -65,12 +65,12 @@ def login(request, template_name='registration/login.html',
65 65
     }
66 66
     context.update(extra_context or {})
67 67
     return render_to_response(template_name, context,
68  
-                              context_instance=RequestContext(request))
  68
+                              context_instance=RequestContext(request, current_app=current_app))
69 69
 
70 70
 def logout(request, next_page=None,
71 71
            template_name='registration/logged_out.html',
72 72
            redirect_field_name=REDIRECT_FIELD_NAME,
73  
-           extra_context=None):
  73
+           current_app=None, extra_context=None):
74 74
     "Logs out the user and displays 'You are logged out' message."
75 75
     from django.contrib.auth import logout
76 76
     logout(request)
@@ -87,16 +87,16 @@ def logout(request, next_page=None,
87 87
             }
88 88
             context.update(extra_context or {})
89 89
             return render_to_response(template_name, context,
90  
-                                      context_instance=RequestContext(request))
  90
+                                      context_instance=RequestContext(request, current_app=current_app))
91 91
     else:
92 92
         # Redirect to this page until the session has been cleared.
93 93
         return HttpResponseRedirect(next_page or request.path)
94 94
 
95  
-def logout_then_login(request, login_url=None, extra_context=None):
  95
+def logout_then_login(request, login_url=None, current_app=None, extra_context=None):
96 96
     "Logs out the user if he is logged in. Then redirects to the log-in page."
97 97
     if not login_url:
98 98
         login_url = settings.LOGIN_URL
99  
-    return logout(request, login_url, extra_context=extra_context)
  99
+    return logout(request, login_url, current_app=current_app, extra_context=extra_context)
100 100
 
101 101
 def redirect_to_login(next, login_url=None,
102 102
                       redirect_field_name=REDIRECT_FIELD_NAME):
@@ -127,6 +127,7 @@ def password_reset(request, is_admin_site=False,
127 127
                    token_generator=default_token_generator,
128 128
                    post_reset_redirect=None,
129 129
                    from_email=None,
  130
+                   current_app=None,
130 131
                    extra_context=None):
131 132
     if post_reset_redirect is None:
132 133
         post_reset_redirect = reverse('django.contrib.auth.views.password_reset_done')
@@ -151,15 +152,15 @@ def password_reset(request, is_admin_site=False,
151 152
     }
152 153
     context.update(extra_context or {})
153 154
     return render_to_response(template_name, context,
154  
-                              context_instance=RequestContext(request))
  155
+                              context_instance=RequestContext(request, current_app=current_app))
155 156
 
156 157
 def password_reset_done(request,
157 158
                         template_name='registration/password_reset_done.html',
158  
-                        extra_context=None):
  159
+                        current_app=None, extra_context=None):
159 160
     context = {}
160 161
     context.update(extra_context or {})
161 162
     return render_to_response(template_name, context,
162  
-                              context_instance=RequestContext(request))
  163
+                              context_instance=RequestContext(request, current_app=current_app))
163 164
 
164 165
 # Doesn't need csrf_protect since no-one can guess the URL
165 166
 def password_reset_confirm(request, uidb36=None, token=None,
@@ -167,7 +168,7 @@ def password_reset_confirm(request, uidb36=None, token=None,
167 168
                            token_generator=default_token_generator,
168 169
                            set_password_form=SetPasswordForm,
169 170
                            post_reset_redirect=None,
170  
-                           extra_context=None):
  171
+                           current_app=None, extra_context=None):
171 172
     """
172 173
     View that checks the hash in a password reset link and presents a
173 174
     form for entering a new password.
@@ -199,17 +200,17 @@ def password_reset_confirm(request, uidb36=None, token=None,
199 200
     }
200 201
     context.update(extra_context or {})
201 202
     return render_to_response(template_name, context,
202  
-                              context_instance=RequestContext(request))
  203
+                              context_instance=RequestContext(request, current_app=current_app))
203 204
 
204 205
 def password_reset_complete(request,
205 206
                             template_name='registration/password_reset_complete.html',
206  
-                            extra_context=None):
  207
+                            current_app=None, extra_context=None):
207 208
     context = {
208 209
         'login_url': settings.LOGIN_URL
209 210
     }
210 211
     context.update(extra_context or {})
211 212
     return render_to_response(template_name, context,
212  
-                              context_instance=RequestContext(request))
  213
+                              context_instance=RequestContext(request, current_app=current_app))
213 214
 
214 215
 @csrf_protect
215 216
 @login_required
@@ -217,7 +218,7 @@ def password_change(request,
217 218
                     template_name='registration/password_change_form.html',
218 219
                     post_change_redirect=None,
219 220
                     password_change_form=PasswordChangeForm,
220  
-                    extra_context=None):
  221
+                    current_app=None, extra_context=None):
221 222
     if post_change_redirect is None:
222 223
         post_change_redirect = reverse('django.contrib.auth.views.password_change_done')
223 224
     if request.method == "POST":
@@ -232,12 +233,12 @@ def password_change(request,
232 233
     }
233 234
     context.update(extra_context or {})
234 235
     return render_to_response(template_name, context,
235  
-                              context_instance=RequestContext(request))
  236
+                              context_instance=RequestContext(request, current_app=current_app))
236 237
 
237 238
 def password_change_done(request,
238 239
                          template_name='registration/password_change_done.html',
239  
-                         extra_context=None):
  240
+                         current_app=None, extra_context=None):
240 241
     context = {}
241 242
     context.update(extra_context or {})
242 243
     return render_to_response(template_name, context,
243  
-                              context_instance=RequestContext(request))
  244
+                              context_instance=RequestContext(request, current_app=current_app))
7  docs/ref/contrib/admin/index.txt
@@ -1459,6 +1459,13 @@ Path to a custom template that will be used by the admin site main index view.
1459 1459
 
1460 1460
 Path to a custom template that will be used by the admin site login view.
1461 1461
 
  1462
+.. versionadded:: 1.3
  1463
+
  1464
+.. attribute:: AdminSite.login_form
  1465
+
  1466
+Subclass of :class:`~django.contrib.auth.forms.AuthenticationForm` that will
  1467
+be used by the admin site login view.
  1468
+
1462 1469
 .. attribute:: AdminSite.logout_template
1463 1470
 
1464 1471
 .. versionadded:: 1.2
17  docs/releases/1.3-alpha-2.txt
@@ -95,6 +95,23 @@ As a result, we took the following steps to rectify the issue:
95 95
     **if the value is not None**, and falls back to the previously used
96 96
     :setting:`MEDIA_URL` setting otherwise.
97 97
 
  98
+Changes to the login methods of the admin
  99
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  100
+
  101
+In previous version the admin app defined login methods in multiple locations
  102
+and ignored the almost identical implementation in the already used auth app.
  103
+A side effect of this duplication was the missing adoption of the changes made
  104
+in r12634_ to support a broader set of characters for usernames.
  105
+
  106
+This release refactores the admin's login mechanism to use a subclass of the
  107
+:class:`~django.contrib.auth.forms.AuthenticationForm` instead of a manual
  108
+form validation. The previously undocumented method
  109
+``'django.contrib.admin.sites.AdminSite.display_login_form'`` has been removed
  110
+in favor of a new :attr:`~django.contrib.admin.AdminSite.login_form`
  111
+attribute.
  112
+
  113
+.. _r12634: http://code.djangoproject.com/changeset/12634
  114
+
98 115
 The Django 1.3 roadmap
99 116
 ======================
100 117
 
17  docs/releases/1.3.txt
@@ -424,3 +424,20 @@ Django 1.5, the old behavior will be replaced with the new behavior.
424 424
 To ensure compatibility with future versions of Django, existing
425 425
 templates should be modified to use the new ``future`` libraries and
426 426
 syntax.
  427
+
  428
+Changes to the login methods of the admin
  429
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  430
+
  431
+In previous version the admin app defined login methods in multiple locations
  432
+and ignored the almost identical implementation in the already used auth app.
  433
+A side effect of this duplication was the missing adoption of the changes made
  434
+in r12634_ to support a broader set of characters for usernames.
  435
+
  436
+This release refactores the admin's login mechanism to use a subclass of the
  437
+:class:`~django.contrib.auth.forms.AuthenticationForm` instead of a manual
  438
+form validation. The previously undocumented method
  439
+``'django.contrib.admin.sites.AdminSite.display_login_form'`` has been removed
  440
+in favor of a new :attr:`~django.contrib.admin.AdminSite.login_form`
  441
+attribute.
  442
+
  443
+.. _r12634: http://code.djangoproject.com/changeset/12634
3  tests/regressiontests/admin_views/customadmin.py
@@ -5,9 +5,10 @@
5 5
 from django.contrib import admin
6 6
 from django.http import HttpResponse
7 7
 
8  
-import models
  8
+import models, forms
9 9
 
10 10
 class Admin2(admin.AdminSite):
  11
+    login_form = forms.CustomAdminAuthenticationForm
11 12
     login_template = 'custom_admin/login.html'
12 13
     logout_template = 'custom_admin/logout.html'
13 14
     index_template = 'custom_admin/index.html'
10  tests/regressiontests/admin_views/forms.py
... ...
@@ -0,0 +1,10 @@
  1
+from django import forms
  2
+from django.contrib.admin.forms import AdminAuthenticationForm
  3
+
  4
+class CustomAdminAuthenticationForm(AdminAuthenticationForm):
  5
+
  6
+    def clean_username(self):
  7
+        username = self.cleaned_data.get('username')
  8
+        if username == 'customform':
  9
+            raise forms.ValidationError('custom form error')
  10
+        return username
166  tests/regressiontests/admin_views/tests.py
@@ -5,7 +5,7 @@
5 5
 from django.conf import settings
6 6
 from django.core import mail
7 7
 from django.core.files import temp as tempfile
8  
-from django.contrib.auth import admin # Register auth models with the admin.
  8
+from django.contrib.auth import REDIRECT_FIELD_NAME, admin # Register auth models with the admin.
9 9
 from django.contrib.auth.models import User, Permission, UNUSABLE_PASSWORD
10 10
 from django.contrib.contenttypes.models import ContentType
11 11
 from django.contrib.admin.models import LogEntry, DELETION
@@ -377,6 +377,19 @@ def test_save_as_display(self):
377 377
 class CustomModelAdminTest(AdminViewBasicTest):
378 378
     urlbit = "admin2"
379 379
 
  380
+    def testCustomAdminSiteLoginForm(self):
  381
+        self.client.logout()
  382
+        request = self.client.get('/test_admin/admin2/')
  383
+        self.failUnlessEqual(request.status_code, 200)
  384
+        login = self.client.post('/test_admin/admin2/', {
  385
+            REDIRECT_FIELD_NAME: '/test_admin/admin2/',
  386
+            LOGIN_FORM_KEY: 1,
  387
+            'username': 'customform',
  388
+            'password': 'secret',
  389
+        })
  390
+        self.failUnlessEqual(login.status_code, 200)
  391
+        self.assertContains(login, 'custom form error')
  392
+
380 393
     def testCustomAdminSiteLoginTemplate(self):
381 394
         self.client.logout()
382 395
         request = self.client.get('/test_admin/admin2/')
@@ -446,36 +459,52 @@ def setUp(self):
446 459
 
447 460
         # login POST dicts
448 461
         self.super_login = {
449  
-                     LOGIN_FORM_KEY: 1,
450  
-                     'username': 'super',
451  
-                     'password': 'secret'}
  462
+            REDIRECT_FIELD_NAME: '/test_admin/admin/',
  463
+            LOGIN_FORM_KEY: 1,
  464
+            'username': 'super',
  465
+            'password': 'secret',
  466
+        }
452 467
         self.super_email_login = {
453  
-                     LOGIN_FORM_KEY: 1,
454  
-                     'username': 'super@example.com',
455  
-                     'password': 'secret'}
  468
+            REDIRECT_FIELD_NAME: '/test_admin/admin/',
  469
+            LOGIN_FORM_KEY: 1,
  470
+            'username': 'super@example.com',
  471
+            'password': 'secret',
  472
+        }
456 473
         self.super_email_bad_login = {
457  
-                      LOGIN_FORM_KEY: 1,
458  
-                      'username': 'super@example.com',
459  
-                      'password': 'notsecret'}
  474
+            REDIRECT_FIELD_NAME: '/test_admin/admin/',
  475
+            LOGIN_FORM_KEY: 1,
  476
+            'username': 'super@example.com',
  477
+            'password': 'notsecret',
  478
+        }
460 479
         self.adduser_login = {
461  
-                     LOGIN_FORM_KEY: 1,
462  
-                     'username': 'adduser',
463  
-                     'password': 'secret'}
  480
+            REDIRECT_FIELD_NAME: '/test_admin/admin/',
  481
+            LOGIN_FORM_KEY: 1,
  482
+            'username': 'adduser',
  483
+            'password': 'secret',
  484
+        }
464 485
         self.changeuser_login = {
465  
-                     LOGIN_FORM_KEY: 1,
466  
-                     'username': 'changeuser',
467  
-                     'password': 'secret'}
  486
+            REDIRECT_FIELD_NAME: '/test_admin/admin/',
  487
+            LOGIN_FORM_KEY: 1,
  488
+            'username': 'changeuser',
  489
+            'password': 'secret',
  490
+        }
468 491
         self.deleteuser_login = {
469  
-                     LOGIN_FORM_KEY: 1,
470  
-                     'username': 'deleteuser',
471  
-                     'password': 'secret'}
  492
+            REDIRECT_FIELD_NAME: '/test_admin/admin/',
  493
+            LOGIN_FORM_KEY: 1,
  494
+            'username': 'deleteuser',
  495
+            'password': 'secret',
  496
+        }
472 497
         self.joepublic_login = {
473  
-                     LOGIN_FORM_KEY: 1,
474  
-                     'username': 'joepublic',
475  
-                     'password': 'secret'}
  498
+            REDIRECT_FIELD_NAME: '/test_admin/admin/',
  499
+            LOGIN_FORM_KEY: 1,
  500
+            'username': 'joepublic',
  501
+            'password': 'secret',
  502
+        }
476 503
         self.no_username_login = {
477  
-                     LOGIN_FORM_KEY: 1,
478  
-                     'password': 'secret'}
  504
+            REDIRECT_FIELD_NAME: '/test_admin/admin/',
  505
+            LOGIN_FORM_KEY: 1,
  506
+            'password': 'secret',
  507
+        }
479 508
 
480 509
     def testLogin(self):
481 510
         """
@@ -500,12 +529,12 @@ def testLogin(self):
500 529
         self.assertContains(login, "Your e-mail address is not your username")
501 530
         # only correct passwords get a username hint
502 531
         login = self.client.post('/test_admin/admin/', self.super_email_bad_login)
503  
-        self.assertContains(login, "Usernames cannot contain the &#39;@&#39; character")
  532
+        self.assertContains(login, "Please enter a correct username and password.")
504 533
         new_user = User(username='jondoe', password='secret', email='super@example.com')
505 534
         new_user.save()
506 535
         # check to ensure if there are multiple e-mail addresses a user doesn't get a 500
507 536
         login = self.client.post('/test_admin/admin/', self.super_email_login)
508  
-        self.assertContains(login, "Usernames cannot contain the &#39;@&#39; character")
  537
+        self.assertContains(login, "Please enter a correct username and password.")
509 538
 
510 539
         # Add User
511 540
         request = self.client.get('/test_admin/admin/')
@@ -536,23 +565,24 @@ def testLogin(self):
536 565
         self.failUnlessEqual(request.status_code, 200)
537 566
         login = self.client.post('/test_admin/admin/', self.joepublic_login)
538 567
         self.failUnlessEqual(login.status_code, 200)
539  
-        # Login.context is a list of context dicts we just need to check the first one.
540  
-        self.assert_(login.context[0].get('error_message'))
  568
+        self.assertContains(login, "Please enter a correct username and password.")
541 569
 
542 570
         # Requests without username should not return 500 errors.
543 571
         request = self.client.get('/test_admin/admin/')
544 572
         self.failUnlessEqual(request.status_code, 200)
545 573
         login = self.client.post('/test_admin/admin/', self.no_username_login)
546 574
         self.failUnlessEqual(login.status_code, 200)
547  
-        # Login.context is a list of context dicts we just need to check the first one.
548  
-        self.assert_(login.context[0].get('error_message'))
  575
+        form = login.context[0].get('form')
  576
+        self.failUnlessEqual(form.errors['username'][0], 'This field is required.')
549 577
 
550 578
     def testLoginSuccessfullyRedirectsToOriginalUrl(self):
551 579
         request = self.client.get('/test_admin/admin/')
552 580
         self.failUnlessEqual(request.status_code, 200)
553  
-        query_string = "the-answer=42"
554  
-        login = self.client.post('/test_admin/admin/', self.super_login, QUERY_STRING = query_string )
555  
-        self.assertRedirects(login, '/test_admin/admin/?%s' % query_string)
  581
+        query_string = 'the-answer=42'
  582
+        redirect_url = '/test_admin/admin/?%s' % query_string
  583
+        new_next = {REDIRECT_FIELD_NAME: redirect_url}
  584
+        login = self.client.post('/test_admin/admin/', dict(self.super_login, **new_next), QUERY_STRING=query_string)
  585
+        self.assertRedirects(login, redirect_url)
556 586
 
557 587
     def testAddView(self):
558 588
         """Test add view restricts access and actually adds items."""
@@ -967,33 +997,47 @@ class SecureViewTest(TestCase):
967 997
     def setUp(self):
968 998
         # login POST dicts
969 999
         self.super_login = {
970  
-                     LOGIN_FORM_KEY: 1,
971  
-                     'username': 'super',
972  
-                     'password': 'secret'}
  1000
+            LOGIN_FORM_KEY: 1,
  1001
+            REDIRECT_FIELD_NAME: '/test_admin/admin/secure-view/',
  1002
+            'username': 'super',
  1003
+            'password': 'secret',
  1004
+        }
973 1005
         self.super_email_login = {
974  
-                     LOGIN_FORM_KEY: 1,
975  
-                     'username': 'super@example.com',
976  
-                     'password': 'secret'}
  1006
+            LOGIN_FORM_KEY: 1,
  1007
+            REDIRECT_FIELD_NAME: '/test_admin/admin/secure-view/',
  1008
+            'username': 'super@example.com',
  1009
+            'password': 'secret',
  1010
+        }
977 1011
         self.super_email_bad_login = {
978  
-                      LOGIN_FORM_KEY: 1,
979  
-                      'username': 'super@example.com',
980  
-                      'password': 'notsecret'}
  1012
+            LOGIN_FORM_KEY: 1,
  1013
+            REDIRECT_FIELD_NAME: '/test_admin/admin/secure-view/',
  1014
+            'username': 'super@example.com',
  1015
+            'password': 'notsecret',
  1016
+        }
981 1017
         self.adduser_login = {
982  
-                     LOGIN_FORM_KEY: 1,
983  
-                     'username': 'adduser',
984  
-                     'password': 'secret'}
  1018
+            LOGIN_FORM_KEY: 1,
  1019
+            REDIRECT_FIELD_NAME: '/test_admin/admin/secure-view/',
  1020
+            'username': 'adduser',
  1021
+            'password': 'secret',
  1022
+        }
985 1023
         self.changeuser_login = {
986  
-                     LOGIN_FORM_KEY: 1,
987  
-                     'username': 'changeuser',
988  
-                     'password': 'secret'}
  1024
+            LOGIN_FORM_KEY: 1,
  1025
+            REDIRECT_FIELD_NAME: '/test_admin/admin/secure-view/',
  1026
+            'username': 'changeuser',
  1027
+            'password': 'secret',
  1028
+        }
989 1029
         self.deleteuser_login = {
990  
-                     LOGIN_FORM_KEY: 1,
991  
-                     'username': 'deleteuser',
992  
-                     'password': 'secret'}
  1030
+            LOGIN_FORM_KEY: 1,
  1031
+            REDIRECT_FIELD_NAME: '/test_admin/admin/secure-view/',
  1032
+            'username': 'deleteuser',
  1033
+            'password': 'secret',
  1034
+        }
993 1035
         self.joepublic_login = {
994  
-                     LOGIN_FORM_KEY: 1,
995  
-                     'username': 'joepublic',
996  
-                     'password': 'secret'}
  1036
+            LOGIN_FORM_KEY: 1,
  1037
+            REDIRECT_FIELD_NAME: '/test_admin/admin/secure-view/',
  1038
+            'username': 'joepublic',
  1039
+            'password': 'secret',
  1040
+        }
997 1041
 
998 1042
     def tearDown(self):
999 1043
         self.client.logout()
@@ -1006,9 +1050,11 @@ def test_secure_view_shows_login_if_not_logged_in(self):
1006 1050
     def test_secure_view_login_successfully_redirects_to_original_url(self):
1007 1051
         request = self.client.get('/test_admin/admin/secure-view/')
1008 1052
         self.failUnlessEqual(request.status_code, 200)
1009  
-        query_string = "the-answer=42"
1010  
-        login = self.client.post('/test_admin/admin/secure-view/', self.super_login, QUERY_STRING = query_string )
1011  
-        self.assertRedirects(login, '/test_admin/admin/secure-view/?%s' % query_string)
  1053
+        query_string = 'the-answer=42'
  1054
+        redirect_url = '/test_admin/admin/secure-view/?%s' % query_string
  1055
+        new_next = {REDIRECT_FIELD_NAME: redirect_url}
  1056
+        login = self.client.post('/test_admin/admin/secure-view/', dict(self.super_login, **new_next), QUERY_STRING=query_string)
  1057
+        self.assertRedirects(login, redirect_url)
1012 1058
 
1013 1059
     def test_staff_member_required_decorator_works_as_per_admin_login(self):
1014 1060
         """
@@ -1035,12 +1081,12 @@ def test_staff_member_required_decorator_works_as_per_admin_login(self):
1035 1081
         self.assertContains(login, "Your e-mail address is not your username")
1036 1082
         # only correct passwords get a username hint
1037 1083
         login = self.client.post('/test_admin/admin/secure-view/', self.super_email_bad_login)
1038  
-        self.assertContains(login, "Usernames cannot contain the &#39;@&#39; character")
  1084
+        self.assertContains(login, "Please enter a correct username and password.")
1039 1085
         new_user = User(username='jondoe', password='secret', email='super@example.com')
1040 1086
         new_user.save()
1041 1087
         # check to ensure if there are multiple e-mail addresses a user doesn't get a 500
1042 1088
         login = self.client.post('/test_admin/admin/secure-view/', self.super_email_login)
1043  
-        self.assertContains(login, "Usernames cannot contain the &#39;@&#39; character")
  1089
+        self.assertContains(login, "Please enter a correct username and password.")
1044 1090
 
1045 1091
         # Add User
1046 1092
         request = self.client.get('/test_admin/admin/secure-view/')
@@ -1072,7 +1118,7 @@ def test_staff_member_required_decorator_works_as_per_admin_login(self):
1072 1118
         login = self.client.post('/test_admin/admin/secure-view/', self.joepublic_login)
1073 1119
         self.failUnlessEqual(login.status_code, 200)
1074 1120
         # Login.context is a list of context dicts we just need to check the first one.
1075  
-        self.assert_(login.context[0].get('error_message'))
  1121
+        self.assertContains(login, "Please enter a correct username and password.")
1076 1122
 
1077 1123
         # 8509 - if a normal user is already logged in, it is possible
1078 1124
         # to change user into the superuser without error

0 notes on commit cc64fb5

Please sign in to comment.
Something went wrong with that request. Please try again.