Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #19077, #19079 -- Made USERNAME_FIELD a required field, and mod…

…ified UserAdmin to match.
  • Loading branch information...
commit c433fcb3fb34fccd69782979f0e7cd5f2d4a4893 1 parent 5fb2232
Russell Keith-Magee authored October 13, 2012
2  django/contrib/admin/templates/admin/base.html
@@ -26,7 +26,7 @@
26 26
         {% if user.is_active and user.is_staff %}
27 27
         <div id="user-tools">
28 28
             {% trans 'Welcome,' %}
29  
-            <strong>{% filter force_escape %}{% firstof user.get_short_name user.username %}{% endfilter %}</strong>.
  29
+            <strong>{% filter force_escape %}{% firstof user.get_short_name user.get_username %}{% endfilter %}</strong>.
30 30
             {% block userlinks %}
31 31
                 {% url 'django-admindocs-docroot' as docsroot %}
32 32
                 {% if docsroot %}
2  django/contrib/admin/templates/admin/object_history.html
@@ -29,7 +29,7 @@
29 29
         {% for action in action_list %}
30 30
         <tr>
31 31
             <th scope="row">{{ action.action_time|date:"DATETIME_FORMAT" }}</th>
32  
-            <td>{{ action.user.username }}{% if action.user.get_full_name %} ({{ action.user.get_full_name }}){% endif %}</td>
  32
+            <td>{{ action.user.get_username }}{% if action.user.get_full_name %} ({{ action.user.get_full_name }}){% endif %}</td>
33 33
             <td>{{ action.change_message }}</td>
34 34
         </tr>
35 35
         {% endfor %}
2  django/contrib/admin/templates/registration/password_reset_email.html
@@ -5,7 +5,7 @@
5 5
 {% block reset_link %}
6 6
 {{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %}
7 7
 {% endblock %}
8  
-{% trans "Your username, in case you've forgotten:" %} {{ user.username }}
  8
+{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
9 9
 
10 10
 {% trans "Thanks for using our site!" %}
11 11
 
7  django/contrib/auth/admin.py
@@ -11,14 +11,13 @@
11 11
 from django.template.response import TemplateResponse
12 12
 from django.utils.html import escape
13 13
 from django.utils.decorators import method_decorator
14  
-from django.utils.safestring import mark_safe
15  
-from django.utils import six
16 14
 from django.utils.translation import ugettext, ugettext_lazy as _
17 15
 from django.views.decorators.csrf import csrf_protect
18 16
 from django.views.decorators.debug import sensitive_post_parameters
19 17
 
20 18
 csrf_protect_m = method_decorator(csrf_protect)
21 19
 
  20
+
22 21
 class GroupAdmin(admin.ModelAdmin):
23 22
     search_fields = ('name',)
24 23
     ordering = ('name',)
@@ -106,9 +105,10 @@ def add_view(self, request, form_url='', extra_context=None):
106 105
             raise PermissionDenied
107 106
         if extra_context is None:
108 107
             extra_context = {}
  108
+        username_field = self.model._meta.get_field(self.model.USERNAME_FIELD)
109 109
         defaults = {
110 110
             'auto_populated_fields': (),
111  
-            'username_help_text': self.model._meta.get_field('username').help_text,
  111
+            'username_help_text': username_field.help_text,
112 112
         }
113 113
         extra_context.update(defaults)
114 114
         return super(UserAdmin, self).add_view(request, form_url,
@@ -171,4 +171,3 @@ def response_add(self, request, obj, post_url_continue='../%s/'):
171 171
 
172 172
 admin.site.register(Group, GroupAdmin)
173 173
 admin.site.register(User, UserAdmin)
174  
-
2  django/contrib/auth/backends.py
@@ -105,7 +105,7 @@ def authenticate(self, remote_user):
105 105
         # built-in safeguards for multiple threads.
106 106
         if self.create_unknown_user:
107 107
             user, created = UserModel.objects.get_or_create(**{
108  
-                getattr(UserModel, 'USERNAME_FIELD', 'username'): username
  108
+                UserModel.USERNAME_FIELD: username
109 109
             })
110 110
             if created:
111 111
                 user = self.configure_user(user)
8  django/contrib/auth/forms.py
@@ -52,6 +52,9 @@ def __init__(self, *args, **kwargs):
52 52
         kwargs.setdefault("required", False)
53 53
         super(ReadOnlyPasswordHashField, self).__init__(*args, **kwargs)
54 54
 
  55
+    def clean_password(self):
  56
+        return self.initial
  57
+
55 58
 
56 59
 class UserCreationForm(forms.ModelForm):
57 60
     """
@@ -118,9 +121,6 @@ class UserChangeForm(forms.ModelForm):
118 121
                     "this user's password, but you can change the password "
119 122
                     "using <a href=\"password/\">this form</a>."))
120 123
 
121  
-    def clean_password(self):
122  
-        return self.initial["password"]
123  
-
124 124
     class Meta:
125 125
         model = User
126 126
 
@@ -160,7 +160,7 @@ def __init__(self, request=None, *args, **kwargs):
160 160
 
161 161
         # Set the label for the "username" field.
162 162
         UserModel = get_user_model()
163  
-        username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username'))
  163
+        username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
164 164
         self.fields['username'].label = capfirst(username_field.verbose_name)
165 165
 
166 166
     def clean(self):
2  django/contrib/auth/management/commands/changepassword.py
@@ -34,7 +34,7 @@ def handle(self, *args, **options):
34 34
 
35 35
         try:
36 36
             u = UserModel.objects.using(options.get('database')).get(**{
37  
-                    getattr(UserModel, 'USERNAME_FIELD', 'username'): username
  37
+                    UserModel.USERNAME_FIELD: username
38 38
                 })
39 39
         except UserModel.DoesNotExist:
40 40
             raise CommandError("user '%s' does not exist" % username)
6  django/contrib/auth/management/commands/createsuperuser.py
@@ -42,7 +42,7 @@ def handle(self, *args, **options):
42 42
 
43 43
         UserModel = get_user_model()
44 44
 
45  
-        username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username'))
  45
+        username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
46 46
         other_fields = UserModel.REQUIRED_FIELDS
47 47
 
48 48
         # If not provided, create the user with an unusable password
@@ -74,7 +74,7 @@ def handle(self, *args, **options):
74 74
 
75 75
                 # Get a username
76 76
                 while username is None:
77  
-                    username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username'))
  77
+                    username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
78 78
                     if not username:
79 79
                         input_msg = capfirst(username_field.verbose_name)
80 80
                         if default_username:
@@ -91,7 +91,7 @@ def handle(self, *args, **options):
91 91
                         continue
92 92
                     try:
93 93
                         UserModel.objects.using(database).get(**{
94  
-                                getattr(UserModel, 'USERNAME_FIELD', 'username'): username
  94
+                                UserModel.USERNAME_FIELD: username
95 95
                             })
96 96
                     except UserModel.DoesNotExist:
97 97
                         pass
4  django/contrib/auth/middleware.py
@@ -55,7 +55,7 @@ def process_request(self, request):
55 55
         # getting passed in the headers, then the correct user is already
56 56
         # persisted in the session and we don't need to continue.
57 57
         if request.user.is_authenticated():
58  
-            if request.user.username == self.clean_username(username, request):
  58
+            if request.user.get_username() == self.clean_username(username, request):
59 59
                 return
60 60
         # We are seeing this user for the first time in this session, attempt
61 61
         # to authenticate the user.
@@ -75,6 +75,6 @@ def clean_username(self, username, request):
75 75
         backend = auth.load_backend(backend_str)
76 76
         try:
77 77
             username = backend.clean_username(username)
78  
-        except AttributeError: # Backend has no clean_username method.
  78
+        except AttributeError:  # Backend has no clean_username method.
79 79
             pass
80 80
         return username
21  django/contrib/auth/models.py
@@ -165,7 +165,7 @@ def make_random_password(self, length=10,
165 165
         return get_random_string(length, allowed_chars)
166 166
 
167 167
     def get_by_natural_key(self, username):
168  
-        return self.get(**{getattr(self.model, 'USERNAME_FIELD', 'username'): username})
  168
+        return self.get(**{self.model.USERNAME_FIELD: username})
169 169
 
170 170
 
171 171
 class UserManager(BaseUserManager):
@@ -227,6 +227,7 @@ def _user_has_module_perms(user, app_label):
227 227
     return False
228 228
 
229 229
 
  230
+@python_2_unicode_compatible
230 231
 class AbstractBaseUser(models.Model):
231 232
     password = models.CharField(_('password'), max_length=128)
232 233
     last_login = models.DateTimeField(_('last login'), default=timezone.now)
@@ -236,6 +237,16 @@ class AbstractBaseUser(models.Model):
236 237
     class Meta:
237 238
         abstract = True
238 239
 
  240
+    def get_username(self):
  241
+        "Return the identifying username for this User"
  242
+        return getattr(self, self.USERNAME_FIELD)
  243
+
  244
+    def __str__(self):
  245
+        return self.get_username()
  246
+
  247
+    def natural_key(self):
  248
+        return (self.get_username(),)
  249
+
239 250
     def is_anonymous(self):
240 251
         """
241 252
         Always returns False. This is a way of comparing User objects to
@@ -277,7 +288,6 @@ def get_short_name(self):
277 288
         raise NotImplementedError()
278 289
 
279 290
 
280  
-@python_2_unicode_compatible
281 291
 class AbstractUser(AbstractBaseUser):
282 292
     """
283 293
     An abstract base class implementing a fully featured User model with
@@ -314,6 +324,7 @@ class AbstractUser(AbstractBaseUser):
314 324
 
315 325
     objects = UserManager()
316 326
 
  327
+    USERNAME_FIELD = 'username'
317 328
     REQUIRED_FIELDS = ['email']
318 329
 
319 330
     class Meta:
@@ -321,12 +332,6 @@ class Meta:
321 332
         verbose_name_plural = _('users')
322 333
         abstract = True
323 334
 
324  
-    def __str__(self):
325  
-        return self.username
326  
-
327  
-    def natural_key(self):
328  
-        return (self.username,)
329  
-
330 335
     def get_absolute_url(self):
331 336
         return "/users/%s/" % urlquote(self.username)
332 337
 
13  django/contrib/comments/admin.py
... ...
@@ -1,11 +1,22 @@
1 1
 from __future__ import unicode_literals
2 2
 
3 3
 from django.contrib import admin
  4
+from django.contrib.auth import get_user_model
4 5
 from django.contrib.comments.models import Comment
5 6
 from django.utils.translation import ugettext_lazy as _, ungettext
6 7
 from django.contrib.comments import get_model
7 8
 from django.contrib.comments.views.moderation import perform_flag, perform_approve, perform_delete
8 9
 
  10
+
  11
+class UsernameSearch(object):
  12
+    """The User object may not be auth.User, so we need to provide
  13
+    a mechanism for issuing the equivalent of a .filter(user__username=...)
  14
+    search in CommentAdmin.
  15
+    """
  16
+    def __str__(self):
  17
+        return 'user__%s' % get_user_model().USERNAME_FIELD
  18
+
  19
+
9 20
 class CommentsAdmin(admin.ModelAdmin):
10 21
     fieldsets = (
11 22
         (None,
@@ -24,7 +35,7 @@ class CommentsAdmin(admin.ModelAdmin):
24 35
     date_hierarchy = 'submit_date'
25 36
     ordering = ('-submit_date',)
26 37
     raw_id_fields = ('user',)
27  
-    search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address')
  38
+    search_fields = ('comment', UsernameSearch(), 'user_name', 'user_email', 'user_url', 'ip_address')
28 39
     actions = ["flag_comments", "approve_comments", "remove_comments"]
29 40
 
30 41
     def get_actions(self, request):
36  django/contrib/comments/models.py
@@ -19,14 +19,14 @@ class BaseCommentAbstractModel(models.Model):
19 19
     """
20 20
 
21 21
     # Content-object field
22  
-    content_type   = models.ForeignKey(ContentType,
  22
+    content_type = models.ForeignKey(ContentType,
23 23
             verbose_name=_('content type'),
24 24
             related_name="content_type_set_for_%(class)s")
25  
-    object_pk      = models.TextField(_('object ID'))
  25
+    object_pk = models.TextField(_('object ID'))
26 26
     content_object = generic.GenericForeignKey(ct_field="content_type", fk_field="object_pk")
27 27
 
28 28
     # Metadata about the comment
29  
-    site        = models.ForeignKey(Site)
  29
+    site = models.ForeignKey(Site)
30 30
 
31 31
     class Meta:
32 32
         abstract = True
@@ -50,21 +50,21 @@ class Comment(BaseCommentAbstractModel):
50 50
     # Who posted this comment? If ``user`` is set then it was an authenticated
51 51
     # user; otherwise at least user_name should have been set and the comment
52 52
     # was posted by a non-authenticated user.
53  
-    user        = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'),
  53
+    user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'),
54 54
                     blank=True, null=True, related_name="%(class)s_comments")
55  
-    user_name   = models.CharField(_("user's name"), max_length=50, blank=True)
56  
-    user_email  = models.EmailField(_("user's email address"), blank=True)
57  
-    user_url    = models.URLField(_("user's URL"), blank=True)
  55
+    user_name = models.CharField(_("user's name"), max_length=50, blank=True)
  56
+    user_email = models.EmailField(_("user's email address"), blank=True)
  57
+    user_url = models.URLField(_("user's URL"), blank=True)
58 58
 
59 59
     comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH)
60 60
 
61 61
     # Metadata about the comment
62 62
     submit_date = models.DateTimeField(_('date/time submitted'), default=None)
63  
-    ip_address  = models.IPAddressField(_('IP address'), blank=True, null=True)
64  
-    is_public   = models.BooleanField(_('is public'), default=True,
  63
+    ip_address = models.IPAddressField(_('IP address'), blank=True, null=True)
  64
+    is_public = models.BooleanField(_('is public'), default=True,
65 65
                     help_text=_('Uncheck this box to make the comment effectively ' \
66 66
                                 'disappear from the site.'))
67  
-    is_removed  = models.BooleanField(_('is removed'), default=False,
  67
+    is_removed = models.BooleanField(_('is removed'), default=False,
68 68
                     help_text=_('Check this box if the comment is inappropriate. ' \
69 69
                                 'A "This comment has been removed" message will ' \
70 70
                                 'be displayed instead.'))
@@ -96,9 +96,9 @@ def _get_userinfo(self):
96 96
         """
97 97
         if not hasattr(self, "_userinfo"):
98 98
             userinfo = {
99  
-                "name"  : self.user_name,
100  
-                "email" : self.user_email,
101  
-                "url"   : self.user_url
  99
+                "name": self.user_name,
  100
+                "email": self.user_email,
  101
+                "url": self.user_url
102 102
             }
103 103
             if self.user_id:
104 104
                 u = self.user
@@ -111,7 +111,7 @@ def _get_userinfo(self):
111 111
                 if u.get_full_name():
112 112
                     userinfo["name"] = self.user.get_full_name()
113 113
                 elif not self.user_name:
114  
-                    userinfo["name"] = u.username
  114
+                    userinfo["name"] = u.get_username()
115 115
             self._userinfo = userinfo
116 116
         return self._userinfo
117 117
     userinfo = property(_get_userinfo, doc=_get_userinfo.__doc__)
@@ -174,9 +174,9 @@ class CommentFlag(models.Model):
174 174
     design users are only allowed to flag a comment with a given flag once;
175 175
     if you want rating look elsewhere.
176 176
     """
177  
-    user      = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), related_name="comment_flags")
178  
-    comment   = models.ForeignKey(Comment, verbose_name=_('comment'), related_name="flags")
179  
-    flag      = models.CharField(_('flag'), max_length=30, db_index=True)
  177
+    user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), related_name="comment_flags")
  178
+    comment = models.ForeignKey(Comment, verbose_name=_('comment'), related_name="flags")
  179
+    flag = models.CharField(_('flag'), max_length=30, db_index=True)
180 180
     flag_date = models.DateTimeField(_('date'), default=None)
181 181
 
182 182
     # Constants for flag types
@@ -192,7 +192,7 @@ class Meta:
192 192
 
193 193
     def __str__(self):
194 194
         return "%s flag of comment ID %s by %s" % \
195  
-            (self.flag, self.comment_id, self.user.username)
  195
+            (self.flag, self.comment_id, self.user.get_username())
196 196
 
197 197
     def save(self, *args, **kwargs):
198 198
         if self.flag_date is None:
25  django/contrib/comments/views/comments.py
@@ -15,7 +15,6 @@
15 15
 from django.views.decorators.http import require_POST
16 16
 
17 17
 
18  
-
19 18
 class CommentPostBadRequest(http.HttpResponseBadRequest):
20 19
     """
21 20
     Response returned when a comment post is invalid. If ``DEBUG`` is on a
@@ -27,6 +26,7 @@ def __init__(self, why):
27 26
         if settings.DEBUG:
28 27
             self.content = render_to_string("comments/400-debug.html", {"why": why})
29 28
 
  29
+
30 30
 @csrf_protect
31 31
 @require_POST
32 32
 def post_comment(request, next=None, using=None):
@@ -40,7 +40,7 @@ def post_comment(request, next=None, using=None):
40 40
     data = request.POST.copy()
41 41
     if request.user.is_authenticated():
42 42
         if not data.get('name', ''):
43  
-            data["name"] = request.user.get_full_name() or request.user.username
  43
+            data["name"] = request.user.get_full_name() or request.user.get_username()
44 44
         if not data.get('email', ''):
45 45
             data["email"] = request.user.email
46 46
 
@@ -98,8 +98,8 @@ def post_comment(request, next=None, using=None):
98 98
         ]
99 99
         return render_to_response(
100 100
             template_list, {
101  
-                "comment" : form.data.get("comment", ""),
102  
-                "form" : form,
  101
+                "comment": form.data.get("comment", ""),
  102
+                "form": form,
103 103
                 "next": next,
104 104
             },
105 105
             RequestContext(request, {})
@@ -113,9 +113,9 @@ def post_comment(request, next=None, using=None):
113 113
 
114 114
     # Signal that the comment is about to be saved
115 115
     responses = signals.comment_will_be_posted.send(
116  
-        sender  = comment.__class__,
117  
-        comment = comment,
118  
-        request = request
  116
+        sender=comment.__class__,
  117
+        comment=comment,
  118
+        request=request
119 119
     )
120 120
 
121 121
     for (receiver, response) in responses:
@@ -126,15 +126,14 @@ def post_comment(request, next=None, using=None):
126 126
     # Save the comment and signal that it was saved
127 127
     comment.save()
128 128
     signals.comment_was_posted.send(
129  
-        sender  = comment.__class__,
130  
-        comment = comment,
131  
-        request = request
  129
+        sender=comment.__class__,
  130
+        comment=comment,
  131
+        request=request
132 132
     )
133 133
 
134 134
     return next_redirect(data, next, comment_done, c=comment._get_pk_val())
135 135
 
136 136
 comment_done = confirmation_view(
137  
-    template = "comments/posted.html",
138  
-    doc = """Display a "comment was posted" success page."""
  137
+    template="comments/posted.html",
  138
+    doc="""Display a "comment was posted" success page."""
139 139
 )
140  
-
197  docs/topics/auth.txt
@@ -149,6 +149,12 @@ Methods
149 149
     :class:`~django.contrib.auth.models.User` objects have the following custom
150 150
     methods:
151 151
 
  152
+    .. method:: models.User.get_username()
  153
+
  154
+        Returns the username for the user. Since the User model can be swapped
  155
+        out, you should use  this method instead of referencing the username
  156
+        attribute directly.
  157
+
152 158
     .. method:: models.User.is_anonymous()
153 159
 
154 160
         Always returns ``False``. This is a way of differentiating
@@ -1826,11 +1832,12 @@ different User model.
1826 1832
 Instead of referring to :class:`~django.contrib.auth.models.User` directly,
1827 1833
 you should reference the user model using
1828 1834
 :func:`django.contrib.auth.get_user_model()`. This method will return the
1829  
-currently  active User model -- the custom User model if one is specified, or
  1835
+currently active User model -- the custom User model if one is specified, or
1830 1836
 :class:`~django.contrib.auth.User` otherwise.
1831 1837
 
1832  
-In relations to the User model, you should specify the custom model using
1833  
-the :setting:`AUTH_USER_MODEL` setting. For example::
  1838
+When you define a foreign key or many-to-many relations to the User model,
  1839
+you should specify the custom model using the :setting:`AUTH_USER_MODEL`
  1840
+setting. For example::
1834 1841
 
1835 1842
     from django.conf import settings
1836 1843
     from django.db import models
@@ -1910,6 +1917,60 @@ password resets. You must then provide some key implementation details:
1910 1917
     identifies the user in an informal way. It may also return the same
1911 1918
     value as :meth:`django.contrib.auth.User.get_full_name()`.
1912 1919
 
  1920
+The following methods are available on any subclass of
  1921
+:class:`~django.contrib.auth.models.AbstractBaseUser`::
  1922
+
  1923
+.. class:: models.AbstractBaseUser
  1924
+
  1925
+    .. method:: models.AbstractBaseUser.get_username()
  1926
+
  1927
+        Returns the value of the field nominated by ``USERNAME_FIELD``.
  1928
+
  1929
+    .. method:: models.AbstractBaseUser.is_anonymous()
  1930
+
  1931
+        Always returns ``False``. This is a way of differentiating
  1932
+        from  :class:`~django.contrib.auth.models.AnonymousUser` objects.
  1933
+        Generally, you should prefer using
  1934
+        :meth:`~django.contrib.auth.models.AbstractBaseUser.is_authenticated()` to this
  1935
+        method.
  1936
+
  1937
+    .. method:: models.AbstractBaseUser.is_authenticated()
  1938
+
  1939
+        Always returns ``True``. This is a way to tell if the user has been
  1940
+        authenticated. This does not imply any permissions, and doesn't check
  1941
+        if the user is active - it only indicates that the user has provided a
  1942
+        valid username and password.
  1943
+
  1944
+    .. method:: models.AbstractBaseUser.set_password(raw_password)
  1945
+
  1946
+        Sets the user's password to the given raw string, taking care of the
  1947
+        password hashing. Doesn't save the
  1948
+        :class:`~django.contrib.auth.models.AbstractBaseUser` object.
  1949
+
  1950
+    .. method:: models.AbstractBaseUser.check_password(raw_password)
  1951
+
  1952
+        Returns ``True`` if the given raw string is the correct password for
  1953
+        the user. (This takes care of the password hashing in making the
  1954
+        comparison.)
  1955
+
  1956
+    .. method:: models.AbstractBaseUser.set_unusable_password()
  1957
+
  1958
+        Marks the user as having no password set.  This isn't the same as
  1959
+        having a blank string for a password.
  1960
+        :meth:`~django.contrib.auth.models.AbstractBaseUser.check_password()` for this user
  1961
+        will never return ``True``. Doesn't save the
  1962
+        :class:`~django.contrib.auth.models.AbstractBaseUser` object.
  1963
+
  1964
+        You may need this if authentication for your application takes place
  1965
+        against an existing external source such as an LDAP directory.
  1966
+
  1967
+    .. method:: models.AbstractBaseUser.has_usable_password()
  1968
+
  1969
+        Returns ``False`` if
  1970
+        :meth:`~django.contrib.auth.models.AbstractBaseUser.set_unusable_password()` has
  1971
+        been called for this user.
  1972
+
  1973
+
1913 1974
 You should also define a custom manager for your User model. If your User
1914 1975
 model defines `username` and `email` fields the same as Django's default User,
1915 1976
 you can just install Django's
@@ -1941,6 +2002,31 @@ additional methods:
1941 2002
     Unlike `create_user()`, `create_superuser()` *must* require the caller
1942 2003
     to provider a password.
1943 2004
 
  2005
+:class:`~django.contrib.auth.models.BaseUserManager` provides the following
  2006
+utility methods:
  2007
+
  2008
+.. class:: models.BaseUserManager
  2009
+    .. method:: models.BaseUserManager.normalize_email(email)
  2010
+
  2011
+        A classmethod that normalizes email addresses by lowercasing
  2012
+        the domain portion of the email address.
  2013
+
  2014
+    .. method:: models.BaseUserManager.get_by_natural_key(username)
  2015
+
  2016
+        Retrieves a user instance using the contents of the field
  2017
+        nominated by ``USERNAME_FIELD``.
  2018
+
  2019
+    .. method:: models.BaseUserManager.make_random_password(length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789')
  2020
+
  2021
+        Returns a random password with the given length and given string of
  2022
+        allowed characters. (Note that the default value of ``allowed_chars``
  2023
+        doesn't contain letters that can cause user confusion, including:
  2024
+
  2025
+        * ``i``, ``l``, ``I``, and ``1`` (lowercase letter i, lowercase
  2026
+          letter L, uppercase letter i, and the number one)
  2027
+        * ``o``, ``O``, and ``0`` (uppercase letter o, lowercase letter o,
  2028
+          and zero)
  2029
+
1944 2030
 Extending Django's default User
1945 2031
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1946 2032
 
@@ -2020,6 +2106,16 @@ control access of the User to admin content:
2020 2106
     Returns True if the user has permission to access models in
2021 2107
     the given app.
2022 2108
 
  2109
+You will also need to register your custom User model with the admin. If
  2110
+your custom User model extends :class:`~django.contrib.auth.models.AbstractUser`,
  2111
+you can use Django's existing :class:`~django.contrib.auth.admin.UserAdmin`
  2112
+class. However, if your User model extends
  2113
+:class:`~django.contrib.auth.models.AbstractBaseUser`, you'll need to define
  2114
+a custom ModelAdmin class. It may be possible to subclass the default
  2115
+:class:`~django.contrib.auth.admin.UserAdmin`; however, you'll need to
  2116
+override any of the definitions that refer to fields on
  2117
+:class:`~django.contrib.auth.models.AbstractUser` that aren't on your
  2118
+custom User class.
2023 2119
 
2024 2120
 Custom users and Proxy models
2025 2121
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2036,11 +2132,11 @@ behavior into your User subclass.
2036 2132
 A full example
2037 2133
 --------------
2038 2134
 
2039  
-Here is an example of a full models.py for an admin-compliant custom
2040  
-user app. This user model uses an email address as the username, and has a
2041  
-required date of birth; it provides no permission checking, beyond a simple
2042  
-`admin` flag on the user account. This model would be compatible with all
2043  
-the built-in auth forms and views, except for the User creation forms.
  2135
+Here is an example of an admin-compliant custom user app. This user model uses
  2136
+an email address as the username, and has a required date of birth; it
  2137
+provides no permission checking, beyond a simple `admin` flag on the user
  2138
+account. This model would be compatible with all the built-in auth forms and
  2139
+views, except for the User creation forms.
2044 2140
 
2045 2141
 This code would all live in a ``models.py`` file for a custom
2046 2142
 authentication app::
@@ -2086,7 +2182,9 @@ authentication app::
2086 2182
     class MyUser(AbstractBaseUser):
2087 2183
         email = models.EmailField(
2088 2184
             verbose_name='email address',
2089  
-            max_length=255
  2185
+            max_length=255,
  2186
+            unique=True,
  2187
+            db_index=True,
2090 2188
         )
2091 2189
         date_of_birth = models.DateField()
2092 2190
         is_active = models.BooleanField(default=True)
@@ -2124,6 +2222,87 @@ authentication app::
2124 2222
             # Simplest possible answer: All admins are staff
2125 2223
             return self.is_admin
2126 2224
 
  2225
+Then, to register this custom User model with Django's admin, the following
  2226
+code would be required in ``admin.py``::
  2227
+
  2228
+    from django import forms
  2229
+    from django.contrib import admin
  2230
+    from django.contrib.auth.models import Group
  2231
+    from django.contrib.auth.admin import UserAdmin
  2232
+    from django.contrib.auth.forms import ReadOnlyPasswordHashField
  2233
+
  2234
+    from customauth.models import MyUser
  2235
+
  2236
+
  2237
+    class UserCreationForm(forms.ModelForm):
  2238
+        """A form for creating new users. Includes all the required
  2239
+        fields, plus a repeated password."""
  2240
+        password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
  2241
+        password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
  2242
+
  2243
+        class Meta:
  2244
+            model = MyUser
  2245
+            fields = ('email', 'date_of_birth')
  2246
+
  2247
+        def clean_password2(self):
  2248
+            # Check that the two password entries match
  2249
+            password1 = self.cleaned_data.get("password1")
  2250
+            password2 = self.cleaned_data.get("password2")
  2251
+            if password1 and password2 and password1 != password2:
  2252
+                raise forms.ValidationError('Passwords don't match')
  2253
+            return password2
  2254
+
  2255
+        def save(self, commit=True):
  2256
+            # Save the provided password in hashed format
  2257
+            user = super(UserCreationForm, self).save(commit=False)
  2258
+            user.set_password(self.cleaned_data["password1"])
  2259
+            if commit:
  2260
+                user.save()
  2261
+            return user
  2262
+
  2263
+
  2264
+    class UserChangeForm(forms.ModelForm):
  2265
+        """A form for updateing users. Includes all the fields on
  2266
+        the user, but replaces the password field with admin's
  2267
+        pasword hash display field.
  2268
+        """
  2269
+        password = ReadOnlyPasswordHashField()
  2270
+
  2271
+        class Meta:
  2272
+            model = MyUser
  2273
+
  2274
+
  2275
+    class MyUserAdmin(UserAdmin):
  2276
+        # The forms to add and change user instances
  2277
+        form = UserChangeForm
  2278
+        add_form = UserCreationForm
  2279
+
  2280
+        # The fields to be used in displaying the User model.
  2281
+        # These override the definitions on the base UserAdmin
  2282
+        # that reference specific fields on auth.User.
  2283
+        list_display = ('email', 'date_of_birth', 'is_admin')
  2284
+        list_filter = ('is_admin',)
  2285
+        fieldsets = (
  2286
+            (None, {'fields': ('email', 'password')}),
  2287
+            ('Personal info', {'fields': ('date_of_birth',)}),
  2288
+            ('Permissions', {'fields': ('is_admin',)}),
  2289
+            ('Important dates', {'fields': ('last_login',)}),
  2290
+        )
  2291
+        add_fieldsets = (
  2292
+            (None, {
  2293
+                'classes': ('wide',),
  2294
+                'fields': ('email', 'date_of_birth', 'password1', 'password2')}
  2295
+            ),
  2296
+        )
  2297
+        search_fields = ('email',)
  2298
+        ordering = ('email',)
  2299
+        filter_horizontal = ()
  2300
+
  2301
+    # Now register the new UserAdmin...
  2302
+    admin.site.register(MyUser, MyUserAdmin)
  2303
+    # ... and, since we're not using Django's builtin permissions,
  2304
+    # unregister the Group model from admin.
  2305
+    admin.site.unregister(Group)
2127 2306
 
2128 2307
 .. _authentication-backends:
2129 2308
 

0 notes on commit c433fcb

Preston Holmes

see: https://code.djangoproject.com/ticket/19133

There are a couple things overlooked here - first the method should be def clean(self, value) - but even with that, the value that is passed from the readonly widget is None - so we need to get the password hash from the UserChangeForm's initial data as was being done before.

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