How to Add a Feature to a Django User
I'm writing this for myself in the future, so that next time I need to update a Django app with a new feature for the user I won't be fumbling as much to remember how to not destroy everything.
Updating User Model
We're going to already assume you're familiar with the importance of using a custom user model. We'll assume you've defined the model in a users
app in models.py
.
Let's add a new class for our new options.
Define new options model
class UserSignatureOption(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
button_url = models.URLField(max_length=200, null=True, blank=True)
website_url = models.URLField(max_length=200, null=True, blank=True)
def __str__(self) -> str:
return f"UserSignatureOption( user={self.user}, button_url={self.button_url}, website_url={self.website_url} )"
Then we're going to create migrations for this file.
Do this locally in a development environment where you have dummy dev data plugged in.
# This will crease the UserSignatureOption table
./src/manage.py makemigrations
# This will give you a space to update existing Users
./src/manage.py makemigrations --empty users
Remember, these commands create migration files which are instructions to modify the database. They will not migrate until we tell them to.
That's really the sensitive aspect of it all is that we were going to be modifying the users in our database to add these fields. Because we are big fat pussy, we've elected not to do that, and instead create a sibling model that attaches to the user with a ForeignKey
.
Update existing users with new options
Problem is when this change is done, all the existing users say, "Hey, that's great that those options are possible, but they were never created when we made our account."
No problem. We'll write the second migration from the --empty
one to tell it to create them.
You would have seen something like this when you ran the commands above:
Migrations for 'users':
src/users/migrations/0010_auto_20250716_0342.py
Open that file.
from django.db import migrations
def create_user_signature_options(apps, schema_editor):
User = apps.get_model('users', 'User')
UserSignatureOption = apps.get_model('users', 'UserSignatureOption')
for user in User.objects.all():
UserSignatureOption.objects.create(
user=user,
button_url=None,
website_url=None
)
class Migration(migrations.Migration):
dependencies = [
# Make Sure this Matches the Previous Migration
('users', '0009_usersignatureoption'),
]
operations = [
migrations.RunPython(create_user_signature_options),
]
Now. Remember we are in a dev environment with local dummy dev data.
./src/manage.py migrate
Provide new options to new users when created
This is where signals come in. You probably have some already in signals.py
.
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model
from users.models import UserSignatureOption
# How you access the auth user mode varies by context.
# In the migration above we used different calls.
# Here, this is the best way
User = get_user_model()
@receiver(post_save, sender=User)
def create_signature_options_for_user(sender, instance, created, **kwargs):
if created:
UserSignatureOption.objects.create(
user=instance,
button_url=None,
website_url=None
)
We're going to assume you know that the signals have to be registered. If you haven't done that look it up.
Write a test to prove it works
You weren't going to skip this were you? In tests.py
:
from django.test import TestCase
from django.contrib.auth import get_user_model
from users.models import UserSignatureOption
User = get_user_model()
class UserSignalsTest(TestCase):
def test_create_user_creates_signature_options(self):
user = User.objects.create_user(
email='test@example.com',
name='somebody',
password='securepassword123'
)
signature_option = UserSignatureOption.objects.get(user=user)
self.assertIsNone(signature_option.button_url, "button_url should be None")
self.assertIsNone(signature_option.website_url, "website_url should be None")
Now run it.
./src/manage.py test users.tests.UserSignalsTest
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.322s
OK
Destroying test database for alias 'default'...
When we push this to production everything should migrate and tests should pass.
Was dis de weh?
Maybe this is paranoia and unnecessary fear to migrate onto the production users? We did say it's assumed that you have a proper custom user model set up, in which ease of modifying it was one of the reasons we did that as I recall. Could we not have just done it? I wonder what you think.
In any case, commit here, do not push to production; no reason to migrate anything to real users until the whole feature is done. And for that, there is no shortcut. Just get back to reading the code.