Hi all,
Following the BDFL pronouncement of a preferred option for
customisable User models in contrib.auth [1], I've just pushed a
branch to Github that contains a draft implementation [2].
It's not ready for trunk quite yet, but you can use this code to set
up a custom User model, and then log into admin with that User model,
and the effort required to do this is fairly minimal.
There isn't a whole lot by way of documentation -- all I've done is
add the documentation for the new AUTH_USER_MODEL setting, and a stub
for where the documentation about swappable User models will finally
go.
Testing is also an interesting area. I've got some tests in place for
the validation code, and for parts of the management. Widespread
testing that apps (e.g., admin) will work with a different User model
will be a bit difficult because the app cache is populated at the
start of the test run, which prohibits switching the User model later
in the suite. Any suggestions on areas where more testing is necessary
and possible are welcome.
So - how does it work? Here's a minimal example in which MyUser has a
unique, required EmailField acting as the identifier for users.
There's also a required "date_of_birth" field for every user. To
switch to using this User model:
* Define a new "myauth" app, and put the following into your models.py file
===
from django.db import models
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
class MyUserManager(BaseUserManager):
def create_user(self, email, date_of_birth, password=None):
if not email:
raise ValueError('Users must have an email address')
user = self.model(
email=MyUserManager.normalize_email(email),
date_of_birth=date_of_birth,
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, username, password, date_of_birth):
u = self.create_user(username, password=password,
date_of_birth=date_of_birth)
u.is_admin = True
u.save(using=self._db)
return u
class MyUser(AbstractBaseUser):
email = models.EmailField(verbose_name='email address',
max_length=255, unique=True)
date_of_birth = models.DateField()
is_admin = models.BooleanField(default=False)
objects = MyUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['date_of_birth']
def get_full_name(self):
return self.email
def get_short_name(self):
return self.email
def __unicode__(self):
return self.email
# Admin required fields
def has_perm(self, perm, obj=None):
return True
def has_module_perms(self, app_label):
return True
@property
def is_staff(self):
return self.is_admin
@property
def is_active(self):
return True
===
* Set AUTH_USER_MODEL = 'myauth.MyUser'
* Run syncdb.
It's important that you do all this on a clean project; once you've
run syncdb, the foreign keys are already set up, and changing
AUTH_USER_MODEL will lead to hilarity.
Some implementation notes:
* As the branch currently stands, you can log into admin with a
custom User model by doing nothing more than defining an
'admin-compatible' User model and setting AUTH_USER_MODEL.
* The fields on MyUser are the minimum subset that I've been able to
establish in order to be "admin-compatible". The implementation of
MyUser is intentionally simple -- every user is effectively a
superuser. If you want fine-grained permissions, you'll need to
implement them.
* The createsuperuser and changepassword management commands both
work with the designated User model.
* The custom manager is required to make sure that the
createsuperuser management command works. Strictly, only
create_superuser is required, but create_user is an obvious utility to
have, so I've included it here.
* I've made get_full_name() and get_short_name() part of the required
API; this follows the most practical and applicable of the suggestions
from the W3C guidelines on personal names [3].
* USERNAME_FIELD and REQUIRED_FIELDS are constants used for
metaprogramming. For example, createsuperuser needs to ask for data
for all the required fields; these constants let you define what
createsuperuser should ask for. They're also used whenever the auth
backend needs to retrieve the User based on credentials (the backend
can't say User.objects.get(username=…), because the field may not be
called username). I'm not completely happy about the way these
constants are defined, but I also can't think of a better option,
other than top-level settings (e.g., AUTH_USER_USERNAME_FIELD).
Suggestions and opinions welcome.
* The swapping mechanic is set up using a new Meta option on models
called 'swappable' that defines the setting where an alternate model
can be defined. Technically, the swappable option *could* be used for
any model; however, I'm not proposing that we actually document this.
I've implemented it as a generic approach purely to make the internals
cleaner -- mostly to avoid needing to make references to auth.User in
the model syncdb code, and to make testing possible (i.e., we can do
validation tests on a dummy 'swappable' model, rather than trying to
munge a validation test for auth.User specifically).
Notable limitations:
* The auth backend requires that the User has an integer primary key.
If your User model uses something else as the primary key (e.g., the
username), you'll still need to write a custom auth backend.
* The auth forms are bound to User, and don't adapt to MyUser.
* You need to roll your own admin registration for MyUser
* The admin template still asks for "username", even if the email
address is the identity field.
What still needs to be done:
* Documentation
* A full audit of admin to formally establish the "admin User" interface.
* Possibly factor out the fields on User that relate to Permissions,
so it's easy to add the full set of admin permission bits to a custom
User model. I know Alex Ogier's branch has done some work on these
last two points, so that's probably where I'll start (or if someone
wants to beat me to it…)
* Some consideration of how we could make the interface with
Forms/ModelAdmin easier. As it stands, it falls into the category of
"Possible, but non-trivial", because you have to write all your own
forms, and your own ModelAdmin. However, most of the password handling
will be fairly boilerplate, as will a lot of the "unique identifier"
handling, once you abstract for the name of the identifier field
(e.g., username vs email). The fact that it is boilerplate seems to
suggest that there is room for some refactoring.
* Protection against changing AUTH_USER_MODEL after the initial
syncdb. This would be a nice-to-have, since it will protect against
the most obvious mistake that will get made.
At this point, feedback and offers of assistance are most welcome.
[1]
https://code.djangoproject.com/wiki/ContribAuthImprovements#Recommendations
[2]
https://github.com/freakboy3742/django/tree/t3011
[3]
http://www.w3.org/International/questions/qa-personal-names
Yours,
Russ Magee %-)