Creating a custom user model in Django 1.6 - Part 2

Published byGoogle on

Welcome back for the second part of this tutorial about creating a custom user model in Django.
Last week, we covered the Model and Manager creation as well as schema migration. For those of you who had previously existing users, this week we'll cover data migration and then we will cover (admin) forms.

Before we get started with data migration and after Pavel Zagrebelin's comment, let me just say that this tutorial shows "the hard and complete way" to create a custom user model, if all you want to do is add a field to Django's BaseUser, you can simply extend it (you still have to set AUTH_USER_MODEL in settings.py) in order to achieve that. Maybe I should have made this clearer last week.

Now that this is out of the way and before we get to the migration code, let me just tell you about a handy shortcut method of django's called django.contrib.auth.get_user_model. This method allows you to get the user model which is defined in your settings.py file without ever having to explicitly name it. Before going further, if you have any model in your project which links to the user model, always make sure it uses this method, it should look somewhat like this:

from django.db import models
from django.contrib.auth import get_user_model

class MyModel(models.Model):

  owner = models.ForeignKey(get_user_model())

Now let's move on to the data migration code (for those of you who have no data to migrate, you can directly skip to the next step). First, create an empty data migration by running

$ python manage.py datamigration myauth default_user_migration --freeze auth
Created 0002_default_user_migration.py.

Having chosen to name our model User (which is a very appealing name for a user model) we are going to run into several trouble when trying to migrate our data (we would have run into trouble anyway if we had chosen another name, just one less trouble). The first one (which is directly related to our model name) is that South seems to have some difficulties accessing different models with the same name through its ORM, so we'll have to import django.contrib.auth.models.User directly into our migration instead of using South orm to access the old user model.

Edit myauth/migrations/0002_default_user_migration.py to make it look like this (don't change anything after the forward method):

from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
from django.contrib.auth.models import User

class Migration(DataMigration):

    def forwards(self, orm):
        "Write your forwards methods here."
        for old_u in User.objects.all():
            new_u = orm.User.objects.create(
                        date_joined=old_u.date_joined,
                        email=old_u.email,
                        first_name=old_u.first_name,
                        id=old_u.id,
                        username=old_u.username,
                        is_active=old_u.is_active,
                        is_staff=old_u.is_staff,
                        is_superuser=old_u.is_superuser,
                        last_login=old_u.last_login,
                        last_name=old_u.last_name,
                        password=old_u.password)
            new_u.user_permissions = old_u.user_permissions.all()
            new_u.groups = old_u.groups.all()

Note that we make sure the new user records have the same id as the old ones in order to keep foreign keys to the user valid

Now the second problem that we are going to have to circumvent is that, in order to run our data migration, we need both "User" models up at the same time and that both models have a relationship to Group and Permission which have the same related_name (because we are extending PermissionMixin).
To get rid of this issue we are temporarily going to edit our User class so that it looks like this:

#Edit this import at the top of the file to add Group and Permission
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager, Group, Permission

#Change the User class inheritance:
class User(AbstractBaseUser):
  .
  .
  .

#Add those two fields (notice the "tmp_" in the related_name property):
  groups = models.ManyToManyField(Group, verbose_name=_('groups'),
    blank=True, help_text=_('The groups this user belongs to. A user will '
      'get all permissions granted to each of '
      'his/her group.'),
    related_name="tmp_user_set", related_query_name="user")
  user_permissions = models.ManyToManyField(Permission,
    verbose_name=_('user permissions'), blank=True,
    help_text=_('Specific permissions for this user.'),
    related_name="tmp_user_set", related_query_name="user")

And finally, in the latest 1.6 versions of Django, the django.contrib.auth.models.User model isn't accessible when AUTH_USER_MODEL points to another model, so edit your settings.py file and comment the line where it is defined.
Now we can run our data migration:

$ python manage.py migrate
Running migrations for babbler:
- Nothing to migrate.
 - Loading initial data for babbler.
Installed 0 object(s) from 0 fixture(s)
Running migrations for theme:
- Nothing to migrate.
 - Loading initial data for theme.
Installed 0 object(s) from 0 fixture(s)
Running migrations for myauth:
 - Migrating forwards to 0002_default_user_migration.
 > myauth:0002_default_user_migration
 - Migration 'myauth:0002_default_user_migration' is marked for no-dry-run.
 - Loading initial data for myauth.
Installed 0 object(s) from 0 fixture(s)

Make sure you save your changes into your versionning system, run the migration on your testing/staging/production servers and then use your versionning sytem to revert the latest changes made to the custom User model and to settings.py, in my case:

$ hg revert -r 29 mybaseproject/settings.py 
$ hg revert -r 29 myauth/models.py

Now, in order to cleanup the old auth.models.User from the database, you should run manage.py syncdb. Django will then ask you if you want to remove the extra table, answer "Yes".

$ python manage.py syncdb
Syncing...
Creating tables ...
The following content types are stale and need to be deleted:

    auth | user

Any objects related to these content types by a foreign key will also
be deleted. Are you sure you want to delete these content types?
If you're unsure, answer 'no'.

    Type 'yes' to continue, or 'no' to cancel: yes
Installing custom SQL ...
Installing indexes ...
Installed 0 object(s) from 0 fixture(s)

Synced:
 > django.contrib.admin
 > django.contrib.auth
 > django.contrib.contenttypes
 > django.contrib.sessions
 > django.contrib.messages
 > django.contrib.staticfiles
 > south

Not synced (use migrations):
 - babbler
 - theme
 - myauth
(use ./manage.py migrate to migrate these)

We are now ready to create our forms. We need two types of forms:

  1. admin forms (Django by default uses 2 different forms for adding and editing users, we will do the same)
  2. the login form (which we will cover next week)

In order to be able to manage models in Django admin, most of the time we only need to define a ModelAdmin. User model is a bit special since it requires special fetures for the password field (it should not display the password and should show a password confirmation field upon creation) and the setting of permissions, we thus need special forms to handle that. Some very similar forms already exist in django.contrib.auth.forms so we are going to extend them in myauth/forms.py.

from django.contrib.auth.forms import UserCreationForm as AuthUserCreationForm, UserChangeForm as AuthUserChangeForm
from django import forms

from myauth.models import User

class UserCreationForm(AuthUserCreationForm):

  receive_newsletter = forms.BooleanField(required=False)

  class Meta:
    model = User

  ## This method is defined in django.contrib.auth.form.UserCreationForm and explicitly links to auth.models.User so we need to override it
  def clean_username(self):
    username = self.cleaned_data["username"]
    try:
      User._default_manager.get(username=username)
    except User.DoesNotExist:
      return username
    raise forms.ValidationError(
      self.error_messages['duplicate_username'],
      code='duplicate_username',
    )


class UserChangeForm(AuthUserChangeForm):

  receive_newsletter = forms.BooleanField(required=False)

  class Meta:
    model = User

Now to admin.py's turn. As far as the user model is concerned, the admin is a little more complex than the regular admin but everything should be rather explicit all the same. Here is what myauth/admin.py should look like.

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as AuthUserAdmin

from myauth.models import User
from myauth.forms import UserCreationForm, UserChangeForm

class UserAdmin(AuthUserAdmin):
  fieldsets = (
    (None, {'fields': ('username', 'password', 'receive_newsletter')}),
    ('Personal info', {'fields': ('first_name', 'last_name', 'email')}),
    ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser',
                    'groups', 'user_permissions')}),
    ('Important dates', {'fields': ('last_login', 'date_joined')}),
  )
  add_fieldsets = (
    (None, {'fields': ('username', 'password1', 'password2', 'receive_newsletter')}),
    ('Personal info', {'fields': ('first_name', 'last_name', 'email')}),
    ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser')}),
  )
  form = UserChangeForm
  add_form = UserCreationForm
  list_display = ('username', 'email', 'first_name', 'last_name', 'is_active', 'is_staff', 'receive_newsletter')
  list_editable = ('is_active','receive_newsletter')
  list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups', 'receive_newsletter')
  search_fields = ('username', 'email', 'first_name', 'last_name')
  ordering = ('last_name','first_name',)


admin.site.register(User, UserAdmin)

As you can see, the main difference here with a "regular" admin is the use of add_fieldsets and add_form.

You can now run your development server and go to your admin and see the changes. Notice that Users has been moved from Auth to Myauth as well as the new field in both forms.

Well, this is it for this week, next week we will cover the admin site and front-end login using either a username or an e-mail address.
In the mean time have a great week everyone and happy coding Smile

Tutorial South Django Custom user model

Comments

This was a great tutorial. Thanks!

By Jeff C - 2 months ago Reply 

You are welcome :-)

By Emmanuelle Delescolle - 2 months ago Reply 

Thank you for these tutorials!

However, I'm having a spot of bother following this method, combined with using django-cms...

According to the django-cms documentation's section on using a custom User model [ http://docs.django-cms.org/en/latest/basic_reference/configuration.html#custom-user-requirements ] , the following requirements need to be met;

<quote>
DjangoCMS expects a user model with at minimum the following fields: email, password, first_name, last_name, is_active, is_staff, and is_superuser. Additionally, it should inherit from AbstractBaseUser and PermissionsMixin (or AbstractUser), and must define one field as the USERNAME_FIELD (see Django documentation for more details).

Additionally, the application in which the model is defined must be loaded before cms in INSTALLED_APPS.
</quote>

So as well as implementing your tutorial's User model, I've placed 'myauth' above 'cms' in my settings.py

Unfortunately, after performing the steps in Part 1 & 2, I keep getting hit by the following error when I try browsing to localhost:8000 ...

<quote>
ImproperlyConfigured: 'PageUserAdmin.list_editable[0]' refers to 'is_active' which is not defined in 'list_display'.
</quote>

This happens no matter if I'm trying this in a virtualenv configured with python 2.7, 3.3, or 3.4 (I tested all 3 python versions to see if it was down to the version of python).

It looks like this may be being caused by django-cms, but they do claim to support custom User models.

The question is - what is going wrong? :)

Regards.

By KC - 1 month, 2 weeks ago Reply 

Hi,

Thank you :-)

The problem comes from the list_display property of the UserAdmin (either yours or Django CMS). It is missing 'is_active' which is expected by PageUserAdmin.

Hope this helps

By Emmanuelle Delescolle - 1 month, 2 weeks ago Reply 

Thanks for the response! That's cleared a mental block I've been having, as I've been struggling with that error for all of last week!

The problem is indeed being caused by the file <b>useradmin.py</b> in the cms/admin folder, if you take a look at it : https://github.com/divio/django-cms/blob/master/cms/admin/useradmin.py

As you can see, the way it is written (in the master branch), if a custom User model is used, then problems such as the one in my original post occur.

Now I need to figure out if I need to rewrite the CMS useradmin.py, or somehow extend it in an app of my own.

Regards

By Kevin Cave - 1 month, 2 weeks ago Reply 

You are welcome.

Well, unless you do need to have is_active editable in the admin change_list, I would simply remove it from your custom user's admin list_editable, I guess that should fix the problem

Cheers

By Emmanuelle Delescolle - 1 month, 2 weeks ago Reply 

After adding groups and user_permissions field i am getting this error
Local field 'groups' in class 'UserProfile' clashes with field of similar name from base class 'PermissionsMixin'

I am using django1.7 , and using its built in migration
Any idea about this problem ?

By Abhishek Mehta - 1 week, 2 days ago Reply 

My mistake, i found out, i forgot to remove PermissionsMixin

By Abhishek Mehta - 1 week, 2 days ago Reply 

No problem. I'm glad you found the problem :-)

By Emmanuelle Delescolle - 1 week, 2 days ago Reply 

Post a comment