Video coming soon

The recording for this lesson has not been published yet.

What we cover

  • AbstractUser versus AbstractBaseUser
  • AUTH_USER_MODEL and get_user_model()
  • Migrating an existing project carefully
  • Registering the user model in the admin

Lesson notes

1. Why you swap the user model on day one

In the tutorial your Post model points at Django's built-in user:

# blog/models.py (from the tutorial)
from django.conf import settings
from django.db import models


class Post(models.Model):
    author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    text = models.TextField()

That works, and it will keep working - until the day you need one extra field on the user. A phone number, a "has accepted the terms" flag, a preferred language, or the very common wish to log in with an email address instead of a username. The built-in django.contrib.auth.models.User is a normal concrete model living in an app you do not own, so you cannot add a field to it.

The official recommendation is therefore blunt: start every project with your own user model, even if it is empty. It costs you five minutes now and saves a painful migration later.

2. AbstractUser versus AbstractBaseUser

Django gives you two starting points. They differ in how much you inherit and how much you have to write yourself.

AbstractUserAbstractBaseUser
What you get username, first_name, last_name, email, is_staff, is_active, date_joined, groups, permissions password, last_login and the password hashing machinery - nothing else
What you must write only your extra fields every field, USERNAME_FIELD, REQUIRED_FIELDS, a manager, and usually PermissionsMixin
Admin and auth forms work out of the box need custom forms
Use it when you are happy with a username plus extras (almost always) you truly need a different identity model, for example email-only with no username
AbstractBaseUser password, last_login PermissionsMixin groups, user_permissions AbstractUser username, email, flags accounts.User (yours) the long road: subclass the two directly

3. Adding the model to the blog project

The user belongs in its own app. Create accounts and put a single, almost empty model in it:

python manage.py startapp accounts
# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models


class User(AbstractUser):
    bio = models.TextField(blank=True)
    website = models.URLField(blank=True)

    def __str__(self):
        return self.get_username()

Register the app and point the setting at the model using the app_label.ModelName form - never an import:

# mysite/settings.py
INSTALLED_APPS = [
    ...
    "accounts.apps.AccountsConfig",
    "blog.apps.BlogConfig",
]

AUTH_USER_MODEL = "accounts.User"

From now on, never import User directly in your own code. There are two correct ways to refer to it, and which one you use depends on when the code runs.

WhereUseWhy
Model fields settings.AUTH_USER_MODEL (a string) Models are imported at load time; a string avoids a circular import
Views, forms, tests, management commands get_user_model() Resolved at call time, when the app registry is ready
# blog/models.py
from django.conf import settings
from django.db import models


class Post(models.Model):
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="posts",
    )
    ...
# blog/views.py
from django.contrib.auth import get_user_model

User = get_user_model()          # fine at module level in a view module


def author_list(request):
    authors = User.objects.filter(posts__isnull=False).distinct()
    ...

4. Doing it on a project that already migrated

If your blog is fresh, delete db.sqlite3 and the migration files of your own apps, then run makemigrations and migrate once. Done.

If the blog is on PythonAnywhere with posts you care about, you have three honest options.

  1. Start over locally, keep the data as a fixture. Works if the data is small:
    python manage.py dumpdata blog --indent 2 > posts.json
    # rebuild the database with the new user model, recreate the users, then
    python manage.py loaddata posts.json
    You will have to fix the author ids in the JSON by hand, because the new user table starts numbering from 1 again.
  2. Do not swap the model at all. Keep auth.User and put your extra fields in a Profile model with a OneToOneField. That is lesson 4.4, and it is a perfectly respectable answer for an existing project.
  3. The surgical route. Create the new model with db_table = "auth_user", then use migrations.SeparateDatabaseAndState so Django's state changes while the table stays. You also have to repoint django_admin_log, auth_user_groups and auth_user_user_permissions. It is documented in tickets and blog posts rather than in the official docs, and it is easy to get wrong. Only attempt it on a branch, against a copy of the production database.

5. Registering the user model in the admin

Swapping the model unregisters the nice admin page you used in the tutorial. Reuse Django's UserAdmin and only extend the fieldsets:

# accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin

from .models import User


@admin.register(User)
class UserAdmin(BaseUserAdmin):
    list_display = ("username", "email", "is_staff", "date_joined")
    fieldsets = BaseUserAdmin.fieldsets + (
        ("Blog profile", {"fields": ("bio", "website")}),
    )
    add_fieldsets = BaseUserAdmin.add_fieldsets + (
        ("Blog profile", {"fields": ("bio", "website")}),
    )

If you write your own forms, subclass the auth ones so password hashing keeps working:

# accounts/forms.py
from django.contrib.auth.forms import UserChangeForm, UserCreationForm

from .models import User


class UserCreateForm(UserCreationForm):
    class Meta(UserCreationForm.Meta):
        model = User
        fields = ("username", "email")


class UserEditForm(UserChangeForm):
    class Meta(UserChangeForm.Meta):
        model = User

Finally, prove it works:

python manage.py makemigrations accounts
python manage.py migrate
python manage.py createsuperuser
python manage.py shell -c "from django.contrib.auth import get_user_model; print(get_user_model())"
<class 'accounts.models.User'>

6. Exercise

  1. Create an accounts app with a User model based on AbstractUser and set AUTH_USER_MODEL before your first migrate.
  2. Change Post.author to use settings.AUTH_USER_MODEL and add related_name="posts". Check the generated migration with sqlmigrate.
  3. Search your project for from django.contrib.auth.models import User and replace every hit with get_user_model().
  4. Register the model in the admin with an extra fieldset and confirm that creating a user there still hashes the password (the stored value must start with pbkdf2_sha256$).
  5. Write a test that asserts get_user_model()._meta.label == "accounts.User", so a future settings mistake fails loudly.

Further reading

After this lesson you will

  • Explain why you swap the user model on day one
  • Introduce a custom user model in the blog project