Fixed #19077, #19079 -- Made USERNAME_FIELD a required field, and modified UserAdmin to match.

This commit is contained in:
Russell Keith-Magee 2012-10-13 11:44:50 +08:00
parent 5fb22329a1
commit c433fcb3fb
14 changed files with 260 additions and 67 deletions

View File

@ -26,7 +26,7 @@
{% if user.is_active and user.is_staff %}
<div id="user-tools">
{% trans 'Welcome,' %}
<strong>{% filter force_escape %}{% firstof user.get_short_name user.username %}{% endfilter %}</strong>.
<strong>{% filter force_escape %}{% firstof user.get_short_name user.get_username %}{% endfilter %}</strong>.
{% block userlinks %}
{% url 'django-admindocs-docroot' as docsroot %}
{% if docsroot %}

View File

@ -29,7 +29,7 @@
{% for action in action_list %}
<tr>
<th scope="row">{{ action.action_time|date:"DATETIME_FORMAT" }}</th>
<td>{{ action.user.username }}{% if action.user.get_full_name %} ({{ action.user.get_full_name }}){% endif %}</td>
<td>{{ action.user.get_username }}{% if action.user.get_full_name %} ({{ action.user.get_full_name }}){% endif %}</td>
<td>{{ action.change_message }}</td>
</tr>
{% endfor %}

View File

@ -5,7 +5,7 @@
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.username }}
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
{% trans "Thanks for using our site!" %}

View File

@ -11,14 +11,13 @@ from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from django.utils.html import escape
from django.utils.decorators import method_decorator
from django.utils.safestring import mark_safe
from django.utils import six
from django.utils.translation import ugettext, ugettext_lazy as _
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
csrf_protect_m = method_decorator(csrf_protect)
class GroupAdmin(admin.ModelAdmin):
search_fields = ('name',)
ordering = ('name',)
@ -106,9 +105,10 @@ class UserAdmin(admin.ModelAdmin):
raise PermissionDenied
if extra_context is None:
extra_context = {}
username_field = self.model._meta.get_field(self.model.USERNAME_FIELD)
defaults = {
'auto_populated_fields': (),
'username_help_text': self.model._meta.get_field('username').help_text,
'username_help_text': username_field.help_text,
}
extra_context.update(defaults)
return super(UserAdmin, self).add_view(request, form_url,
@ -171,4 +171,3 @@ class UserAdmin(admin.ModelAdmin):
admin.site.register(Group, GroupAdmin)
admin.site.register(User, UserAdmin)

View File

@ -105,7 +105,7 @@ class RemoteUserBackend(ModelBackend):
# built-in safeguards for multiple threads.
if self.create_unknown_user:
user, created = UserModel.objects.get_or_create(**{
getattr(UserModel, 'USERNAME_FIELD', 'username'): username
UserModel.USERNAME_FIELD: username
})
if created:
user = self.configure_user(user)

View File

@ -52,6 +52,9 @@ class ReadOnlyPasswordHashField(forms.Field):
kwargs.setdefault("required", False)
super(ReadOnlyPasswordHashField, self).__init__(*args, **kwargs)
def clean_password(self):
return self.initial
class UserCreationForm(forms.ModelForm):
"""
@ -118,9 +121,6 @@ class UserChangeForm(forms.ModelForm):
"this user's password, but you can change the password "
"using <a href=\"password/\">this form</a>."))
def clean_password(self):
return self.initial["password"]
class Meta:
model = User
@ -160,7 +160,7 @@ class AuthenticationForm(forms.Form):
# Set the label for the "username" field.
UserModel = get_user_model()
username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username'))
username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
self.fields['username'].label = capfirst(username_field.verbose_name)
def clean(self):

View File

@ -34,7 +34,7 @@ class Command(BaseCommand):
try:
u = UserModel.objects.using(options.get('database')).get(**{
getattr(UserModel, 'USERNAME_FIELD', 'username'): username
UserModel.USERNAME_FIELD: username
})
except UserModel.DoesNotExist:
raise CommandError("user '%s' does not exist" % username)

View File

@ -42,7 +42,7 @@ class Command(BaseCommand):
UserModel = get_user_model()
username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username'))
username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
other_fields = UserModel.REQUIRED_FIELDS
# If not provided, create the user with an unusable password
@ -74,7 +74,7 @@ class Command(BaseCommand):
# Get a username
while username is None:
username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username'))
username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
if not username:
input_msg = capfirst(username_field.verbose_name)
if default_username:
@ -91,7 +91,7 @@ class Command(BaseCommand):
continue
try:
UserModel.objects.using(database).get(**{
getattr(UserModel, 'USERNAME_FIELD', 'username'): username
UserModel.USERNAME_FIELD: username
})
except UserModel.DoesNotExist:
pass

View File

@ -55,7 +55,7 @@ class RemoteUserMiddleware(object):
# getting passed in the headers, then the correct user is already
# persisted in the session and we don't need to continue.
if request.user.is_authenticated():
if request.user.username == self.clean_username(username, request):
if request.user.get_username() == self.clean_username(username, request):
return
# We are seeing this user for the first time in this session, attempt
# to authenticate the user.
@ -75,6 +75,6 @@ class RemoteUserMiddleware(object):
backend = auth.load_backend(backend_str)
try:
username = backend.clean_username(username)
except AttributeError: # Backend has no clean_username method.
except AttributeError: # Backend has no clean_username method.
pass
return username

View File

@ -165,7 +165,7 @@ class BaseUserManager(models.Manager):
return get_random_string(length, allowed_chars)
def get_by_natural_key(self, username):
return self.get(**{getattr(self.model, 'USERNAME_FIELD', 'username'): username})
return self.get(**{self.model.USERNAME_FIELD: username})
class UserManager(BaseUserManager):
@ -227,6 +227,7 @@ def _user_has_module_perms(user, app_label):
return False
@python_2_unicode_compatible
class AbstractBaseUser(models.Model):
password = models.CharField(_('password'), max_length=128)
last_login = models.DateTimeField(_('last login'), default=timezone.now)
@ -236,6 +237,16 @@ class AbstractBaseUser(models.Model):
class Meta:
abstract = True
def get_username(self):
"Return the identifying username for this User"
return getattr(self, self.USERNAME_FIELD)
def __str__(self):
return self.get_username()
def natural_key(self):
return (self.get_username(),)
def is_anonymous(self):
"""
Always returns False. This is a way of comparing User objects to
@ -277,7 +288,6 @@ class AbstractBaseUser(models.Model):
raise NotImplementedError()
@python_2_unicode_compatible
class AbstractUser(AbstractBaseUser):
"""
An abstract base class implementing a fully featured User model with
@ -314,6 +324,7 @@ class AbstractUser(AbstractBaseUser):
objects = UserManager()
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email']
class Meta:
@ -321,12 +332,6 @@ class AbstractUser(AbstractBaseUser):
verbose_name_plural = _('users')
abstract = True
def __str__(self):
return self.username
def natural_key(self):
return (self.username,)
def get_absolute_url(self):
return "/users/%s/" % urlquote(self.username)

View File

@ -1,11 +1,22 @@
from __future__ import unicode_literals
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.comments.models import Comment
from django.utils.translation import ugettext_lazy as _, ungettext
from django.contrib.comments import get_model
from django.contrib.comments.views.moderation import perform_flag, perform_approve, perform_delete
class UsernameSearch(object):
"""The User object may not be auth.User, so we need to provide
a mechanism for issuing the equivalent of a .filter(user__username=...)
search in CommentAdmin.
"""
def __str__(self):
return 'user__%s' % get_user_model().USERNAME_FIELD
class CommentsAdmin(admin.ModelAdmin):
fieldsets = (
(None,
@ -24,7 +35,7 @@ class CommentsAdmin(admin.ModelAdmin):
date_hierarchy = 'submit_date'
ordering = ('-submit_date',)
raw_id_fields = ('user',)
search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address')
search_fields = ('comment', UsernameSearch(), 'user_name', 'user_email', 'user_url', 'ip_address')
actions = ["flag_comments", "approve_comments", "remove_comments"]
def get_actions(self, request):

View File

@ -19,14 +19,14 @@ class BaseCommentAbstractModel(models.Model):
"""
# Content-object field
content_type = models.ForeignKey(ContentType,
content_type = models.ForeignKey(ContentType,
verbose_name=_('content type'),
related_name="content_type_set_for_%(class)s")
object_pk = models.TextField(_('object ID'))
object_pk = models.TextField(_('object ID'))
content_object = generic.GenericForeignKey(ct_field="content_type", fk_field="object_pk")
# Metadata about the comment
site = models.ForeignKey(Site)
site = models.ForeignKey(Site)
class Meta:
abstract = True
@ -50,21 +50,21 @@ class Comment(BaseCommentAbstractModel):
# Who posted this comment? If ``user`` is set then it was an authenticated
# user; otherwise at least user_name should have been set and the comment
# was posted by a non-authenticated user.
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'),
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'),
blank=True, null=True, related_name="%(class)s_comments")
user_name = models.CharField(_("user's name"), max_length=50, blank=True)
user_email = models.EmailField(_("user's email address"), blank=True)
user_url = models.URLField(_("user's URL"), blank=True)
user_name = models.CharField(_("user's name"), max_length=50, blank=True)
user_email = models.EmailField(_("user's email address"), blank=True)
user_url = models.URLField(_("user's URL"), blank=True)
comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH)
# Metadata about the comment
submit_date = models.DateTimeField(_('date/time submitted'), default=None)
ip_address = models.IPAddressField(_('IP address'), blank=True, null=True)
is_public = models.BooleanField(_('is public'), default=True,
ip_address = models.IPAddressField(_('IP address'), blank=True, null=True)
is_public = models.BooleanField(_('is public'), default=True,
help_text=_('Uncheck this box to make the comment effectively ' \
'disappear from the site.'))
is_removed = models.BooleanField(_('is removed'), default=False,
is_removed = models.BooleanField(_('is removed'), default=False,
help_text=_('Check this box if the comment is inappropriate. ' \
'A "This comment has been removed" message will ' \
'be displayed instead.'))
@ -96,9 +96,9 @@ class Comment(BaseCommentAbstractModel):
"""
if not hasattr(self, "_userinfo"):
userinfo = {
"name" : self.user_name,
"email" : self.user_email,
"url" : self.user_url
"name": self.user_name,
"email": self.user_email,
"url": self.user_url
}
if self.user_id:
u = self.user
@ -111,7 +111,7 @@ class Comment(BaseCommentAbstractModel):
if u.get_full_name():
userinfo["name"] = self.user.get_full_name()
elif not self.user_name:
userinfo["name"] = u.username
userinfo["name"] = u.get_username()
self._userinfo = userinfo
return self._userinfo
userinfo = property(_get_userinfo, doc=_get_userinfo.__doc__)
@ -174,9 +174,9 @@ class CommentFlag(models.Model):
design users are only allowed to flag a comment with a given flag once;
if you want rating look elsewhere.
"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), related_name="comment_flags")
comment = models.ForeignKey(Comment, verbose_name=_('comment'), related_name="flags")
flag = models.CharField(_('flag'), max_length=30, db_index=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), related_name="comment_flags")
comment = models.ForeignKey(Comment, verbose_name=_('comment'), related_name="flags")
flag = models.CharField(_('flag'), max_length=30, db_index=True)
flag_date = models.DateTimeField(_('date'), default=None)
# Constants for flag types
@ -192,7 +192,7 @@ class CommentFlag(models.Model):
def __str__(self):
return "%s flag of comment ID %s by %s" % \
(self.flag, self.comment_id, self.user.username)
(self.flag, self.comment_id, self.user.get_username())
def save(self, *args, **kwargs):
if self.flag_date is None:

View File

@ -15,7 +15,6 @@ from django.views.decorators.csrf import csrf_protect
from django.views.decorators.http import require_POST
class CommentPostBadRequest(http.HttpResponseBadRequest):
"""
Response returned when a comment post is invalid. If ``DEBUG`` is on a
@ -27,6 +26,7 @@ class CommentPostBadRequest(http.HttpResponseBadRequest):
if settings.DEBUG:
self.content = render_to_string("comments/400-debug.html", {"why": why})
@csrf_protect
@require_POST
def post_comment(request, next=None, using=None):
@ -40,7 +40,7 @@ def post_comment(request, next=None, using=None):
data = request.POST.copy()
if request.user.is_authenticated():
if not data.get('name', ''):
data["name"] = request.user.get_full_name() or request.user.username
data["name"] = request.user.get_full_name() or request.user.get_username()
if not data.get('email', ''):
data["email"] = request.user.email
@ -98,8 +98,8 @@ def post_comment(request, next=None, using=None):
]
return render_to_response(
template_list, {
"comment" : form.data.get("comment", ""),
"form" : form,
"comment": form.data.get("comment", ""),
"form": form,
"next": next,
},
RequestContext(request, {})
@ -113,9 +113,9 @@ def post_comment(request, next=None, using=None):
# Signal that the comment is about to be saved
responses = signals.comment_will_be_posted.send(
sender = comment.__class__,
comment = comment,
request = request
sender=comment.__class__,
comment=comment,
request=request
)
for (receiver, response) in responses:
@ -126,15 +126,14 @@ def post_comment(request, next=None, using=None):
# Save the comment and signal that it was saved
comment.save()
signals.comment_was_posted.send(
sender = comment.__class__,
comment = comment,
request = request
sender=comment.__class__,
comment=comment,
request=request
)
return next_redirect(data, next, comment_done, c=comment._get_pk_val())
comment_done = confirmation_view(
template = "comments/posted.html",
doc = """Display a "comment was posted" success page."""
template="comments/posted.html",
doc="""Display a "comment was posted" success page."""
)

View File

@ -149,6 +149,12 @@ Methods
:class:`~django.contrib.auth.models.User` objects have the following custom
methods:
.. method:: models.User.get_username()
Returns the username for the user. Since the User model can be swapped
out, you should use this method instead of referencing the username
attribute directly.
.. method:: models.User.is_anonymous()
Always returns ``False``. This is a way of differentiating
@ -1826,11 +1832,12 @@ different User model.
Instead of referring to :class:`~django.contrib.auth.models.User` directly,
you should reference the user model using
:func:`django.contrib.auth.get_user_model()`. This method will return the
currently active User model -- the custom User model if one is specified, or
currently active User model -- the custom User model if one is specified, or
:class:`~django.contrib.auth.User` otherwise.
In relations to the User model, you should specify the custom model using
the :setting:`AUTH_USER_MODEL` setting. For example::
When you define a foreign key or many-to-many relations to the User model,
you should specify the custom model using the :setting:`AUTH_USER_MODEL`
setting. For example::
from django.conf import settings
from django.db import models
@ -1910,6 +1917,60 @@ password resets. You must then provide some key implementation details:
identifies the user in an informal way. It may also return the same
value as :meth:`django.contrib.auth.User.get_full_name()`.
The following methods are available on any subclass of
:class:`~django.contrib.auth.models.AbstractBaseUser`::
.. class:: models.AbstractBaseUser
.. method:: models.AbstractBaseUser.get_username()
Returns the value of the field nominated by ``USERNAME_FIELD``.
.. method:: models.AbstractBaseUser.is_anonymous()
Always returns ``False``. This is a way of differentiating
from :class:`~django.contrib.auth.models.AnonymousUser` objects.
Generally, you should prefer using
:meth:`~django.contrib.auth.models.AbstractBaseUser.is_authenticated()` to this
method.
.. method:: models.AbstractBaseUser.is_authenticated()
Always returns ``True``. This is a way to tell if the user has been
authenticated. This does not imply any permissions, and doesn't check
if the user is active - it only indicates that the user has provided a
valid username and password.
.. method:: models.AbstractBaseUser.set_password(raw_password)
Sets the user's password to the given raw string, taking care of the
password hashing. Doesn't save the
:class:`~django.contrib.auth.models.AbstractBaseUser` object.
.. method:: models.AbstractBaseUser.check_password(raw_password)
Returns ``True`` if the given raw string is the correct password for
the user. (This takes care of the password hashing in making the
comparison.)
.. method:: models.AbstractBaseUser.set_unusable_password()
Marks the user as having no password set. This isn't the same as
having a blank string for a password.
:meth:`~django.contrib.auth.models.AbstractBaseUser.check_password()` for this user
will never return ``True``. Doesn't save the
:class:`~django.contrib.auth.models.AbstractBaseUser` object.
You may need this if authentication for your application takes place
against an existing external source such as an LDAP directory.
.. method:: models.AbstractBaseUser.has_usable_password()
Returns ``False`` if
:meth:`~django.contrib.auth.models.AbstractBaseUser.set_unusable_password()` has
been called for this user.
You should also define a custom manager for your User model. If your User
model defines `username` and `email` fields the same as Django's default User,
you can just install Django's
@ -1941,6 +2002,31 @@ additional methods:
Unlike `create_user()`, `create_superuser()` *must* require the caller
to provider a password.
:class:`~django.contrib.auth.models.BaseUserManager` provides the following
utility methods:
.. class:: models.BaseUserManager
.. method:: models.BaseUserManager.normalize_email(email)
A classmethod that normalizes email addresses by lowercasing
the domain portion of the email address.
.. method:: models.BaseUserManager.get_by_natural_key(username)
Retrieves a user instance using the contents of the field
nominated by ``USERNAME_FIELD``.
.. method:: models.BaseUserManager.make_random_password(length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789')
Returns a random password with the given length and given string of
allowed characters. (Note that the default value of ``allowed_chars``
doesn't contain letters that can cause user confusion, including:
* ``i``, ``l``, ``I``, and ``1`` (lowercase letter i, lowercase
letter L, uppercase letter i, and the number one)
* ``o``, ``O``, and ``0`` (uppercase letter o, lowercase letter o,
and zero)
Extending Django's default User
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -2020,6 +2106,16 @@ control access of the User to admin content:
Returns True if the user has permission to access models in
the given app.
You will also need to register your custom User model with the admin. If
your custom User model extends :class:`~django.contrib.auth.models.AbstractUser`,
you can use Django's existing :class:`~django.contrib.auth.admin.UserAdmin`
class. However, if your User model extends
:class:`~django.contrib.auth.models.AbstractBaseUser`, you'll need to define
a custom ModelAdmin class. It may be possible to subclass the default
:class:`~django.contrib.auth.admin.UserAdmin`; however, you'll need to
override any of the definitions that refer to fields on
:class:`~django.contrib.auth.models.AbstractUser` that aren't on your
custom User class.
Custom users and Proxy models
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -2036,11 +2132,11 @@ behavior into your User subclass.
A full example
--------------
Here is an example of a full models.py for an admin-compliant custom
user app. This user model uses an email address as the username, and has a
required date of birth; it provides no permission checking, beyond a simple
`admin` flag on the user account. This model would be compatible with all
the built-in auth forms and views, except for the User creation forms.
Here is an example of an admin-compliant custom user app. This user model uses
an email address as the username, and has a required date of birth; it
provides no permission checking, beyond a simple `admin` flag on the user
account. This model would be compatible with all the built-in auth forms and
views, except for the User creation forms.
This code would all live in a ``models.py`` file for a custom
authentication app::
@ -2086,7 +2182,9 @@ authentication app::
class MyUser(AbstractBaseUser):
email = models.EmailField(
verbose_name='email address',
max_length=255
max_length=255,
unique=True,
db_index=True,
)
date_of_birth = models.DateField()
is_active = models.BooleanField(default=True)
@ -2124,6 +2222,87 @@ authentication app::
# Simplest possible answer: All admins are staff
return self.is_admin
Then, to register this custom User model with Django's admin, the following
code would be required in ``admin.py``::
from django import forms
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from customauth.models import MyUser
class UserCreationForm(forms.ModelForm):
"""A form for creating new users. Includes all the required
fields, plus a repeated password."""
password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
class Meta:
model = MyUser
fields = ('email', 'date_of_birth')
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError('Passwords don't match')
return password2
def save(self, commit=True):
# Save the provided password in hashed format
user = super(UserCreationForm, self).save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
class UserChangeForm(forms.ModelForm):
"""A form for updateing users. Includes all the fields on
the user, but replaces the password field with admin's
pasword hash display field.
"""
password = ReadOnlyPasswordHashField()
class Meta:
model = MyUser
class MyUserAdmin(UserAdmin):
# The forms to add and change user instances
form = UserChangeForm
add_form = UserCreationForm
# The fields to be used in displaying the User model.
# These override the definitions on the base UserAdmin
# that reference specific fields on auth.User.
list_display = ('email', 'date_of_birth', 'is_admin')
list_filter = ('is_admin',)
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Personal info', {'fields': ('date_of_birth',)}),
('Permissions', {'fields': ('is_admin',)}),
('Important dates', {'fields': ('last_login',)}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'date_of_birth', 'password1', 'password2')}
),
)
search_fields = ('email',)
ordering = ('email',)
filter_horizontal = ()
# Now register the new UserAdmin...
admin.site.register(MyUser, MyUserAdmin)
# ... and, since we're not using Django's builtin permissions,
# unregister the Group model from admin.
admin.site.unregister(Group)
.. _authentication-backends: