How to extend the behaviour of the User class in Django ( < 1.5 ) by markon
There are basically four ways to do it:
- use get_profile() and an external class
- use a Proxy model
- subclass User
- monkey-patching
Let's see their pros/cons.
1. use get_profile()
This is what everybody should use. Before all, we have to define a class containing new methods/attributes. Let's say:
class UserProfile(models.Model):
user = models.OneToOneField(User, unique=True)
url = models.URLField()
home_address = models.TextField()
phone_number = models.PhoneNumberField()
Being a one-to-one relationship, we have to define a OneToOneField.
A new table, called user_profile, will be created when we sync the database, but we still have to catch the post_save signal in order to create a new UserProfile instance, associated to a single user.
def create_profile(sender, instance, created, **kwargs):
if created:
profile, created = UserProfile.\
objects.get_or_create(user=instance)
post_save.connect(create_profile, sender=User)
Eventually, we have to set the AUTH_PROFILE_MODULE constant inside settings.py (e.g, AUTH_PROFILE_MODULE=‘accounts.UserProfile’).
Pros:
- Easy to implement.
- Low coupling: User does not need UserProfile to exist.
- Strong cohesion: we are not adding new responsabilities to User.
Cons
- A new table is needed
- An extra query each time we have to access to a UserProfile field:
user = User.objects.get(pk=1)
profile = user.get_profile()
print profile.home_address
2. Use a Proxy Model
Another general way to add new functionalities to our User class is by defining a Proxy Model. We are not going to extend User, just like in the previous method, because we are going to use the Proxy Pattern. Let's write a sample:
class ProxyUser(User):
class Meta:
proxy = True
def get_username_and_email_as_tuple(self):
return self.username, self.email
Since they are sharing the same interface, our ProxyUser can access the User attributes/methods.
Do you want to sort the users by username?
class OrderedUser(User):
class Meta:
ordering = ["username"]
proxy = True
However, we can still add a new manager:
class ProxyUserManager(models.Manager):
... # your methods here
class ProxyUser(User):
objects = ProxyUserManager()
class Meta:
proxy = True
Pros:
- Easy to implement
- Low coupling
- Using proxies, your application is more extendable with existing modules
- You don't need 2 queries to get a User instance
Cons:
- You cannot add new fields to User
- You are tied to Proxies in order to use new functionalities
3. Subclass User
This is the most intuitive OOP method. However, as James Bennet explains here:
I’d wager that probably 90% or more of the things people say they want to do with subclasses could be better accomplished by instead defining a related model and linking it back with a unique foreign key.
He's basically suggesting to use get_profile(). Then he says:
I’ve seen a lot of people say they want to subclass User not because they want to change the types of auth-related information, but because they want to add a field for the user’s website URL, or a short “bio” field, or lots of other useful information related to the user.
Did you spot the key word in that last phrase? Other useful information related to the user. That should be a dead giveaway that what we want in the database is a separate table where each row relates back to a row in the auth table. And in OO terms, the user’s website, bio and other information aren’t really part of their authentication and access controls and really should be encapsulated in their own object. So in OO terms what we want is a separate class where each instance has an attribute pointing to an instance of User.
So the question: why do you want to add new responsibilities to User, when it was born to manage auth-related data, like username, email, password? If you want to change the authentication backend, well that's a good reason. Here I found some code we can use to understand how to do that:
from django.contrib.auth.models import User, UserManager
class CustomUser(User):
timezone = models.CharField(max_length=50,
default='Europe/London')
objects = UserManager()
This subclass does not do anything useful: it simply adds a new field.
However, useless or not, we have to specify a new auth-backend.
from django.conf import settings
from django.contrib.auth.backends import ModelBackend
from django.core.exceptions import ImproperlyConfigured
from django.db.models import get_model
class CustomUserModelBackend(ModelBackend):
def authenticate(self, username=None, password=None):
try:
user = self.\
user_class.objects.get(username=username)
if user.check_password(password):
return user
except self.user_class.DoesNotExist:
return None
def get_user(self, user_id):
try:
return self.user_class.objects.get(pk=user_id)
except self.user_class.DoesNotExist:
return None
@property
def user_class(self):
if not hasattr(self, '_user_class'):
self._user_class = get_model(
*settings.CUSTOM_USER_MODEL.split('.', 2))
if not self._user_class:
raise ImproperlyConfigured(
'Could not get custom user model')
return self._user_class
Then, go open settings.py and tell Django that you want to use CustomUser as your new User model and CustomUserModelBackend as your auth-backend:
AUTHENTICATION_BACKENDS = (
'myproject.auth_backends.CustomUserModelBackend',
)
CUSTOM_USER_MODEL = 'accounts.CustomUser'
Pros:
- The best way to add new auth-related fields.
- Just one table for each entity.
- Just one query to get a User instance.
Cons:
- It may give problems: read some comments here.
- If you are not an OOP guru, you will begin to add new not-auth-related fields, like "books read", "your pet's name" and so forth.
4. Monkey patching
Let's start by saying that you don't have to use this method. I am still astonished that I saw its usage inside a larger project, in particular here: askbot/models/__init__.py. You define a method (or an attribute), user_get_absolute_url(instance), and then it will be added to User at runtime by using the add_to_class method, defined by ModelBase
User.add_to_class('get_absolute_url', user_get_absolute_url)
User.add_to_class('get_profile_url', get_profile_url)
# altro codice
def user_get_absolute_url(self):
return self.get_profile_url()
def get_profile_url(self):
"""Returns the URL for this User's profile."""
return reverse('user_profile',
kwargs={
'id' : self.id,
'slug' : slugify(self.username)
}
)
Pros:
- Easy to implement.
- No extra queries.
- No extra tables.
Cons:
- What will it happen if in the next version of Django they add get_profile_url? It breakes. PLEASE READ THE LINK.
- Please remember the Single Responsibility Principle.
Some words about Django 1.5
As you may see in the official documentation, the constant AUTH_PROFILE_MODULE is no longer supported. This means that the first method described in this post is not anymore a good way to add new functionalities/attributes. You should prefer an external class, e.g., UserProfile containing a OneToOneField to User:
from django.contrib.auth.models import User
class UserProfile(models.Model):
user = models.OneToOneField(User)
department = models.CharField(max_length=100)
>>> u = User.objects.get(username='fsmith')
>>> freds_department = u.employee.department
Of course, you still have to catch the post_save signal to associate a UserProfile instance to a User instance.
However, another way of implementing a new User authentication model has been implemented in Django 1.5. You first create a custom MyUser class, then set the constant AUTH_USER_MODEL = accounts.MyUser. Don't forget that throughout the code, you have to replace User with django.contrib.auth.get_user_model(), because now you have to reference to a different User model.
Useful links
- Extending User object in Django: User model inheritance or use UserProfile?
- About Model Subclassing
- Extending the Django User Model
- Extending the Django User Model with inheritance
- Django tips: extending the user model
- Django Proxy Models (official doc)
- How to extend django’s user class and change authentication middleware.
Comments
makaroni4 commented 3 months ago
@markon could you please post it to #django reddit?
markon commented 3 months ago
Yeah, of course :)
makaroni4 commented 3 months ago
@markon according to http://gistflow.com/tags/cloud there is no big #django community here, but I am pretty sure that it could get bigger with posts like this!
markon commented 3 months ago
I hope so :)
pydanny commented 3 months ago
Monkey patching? Monkey patching?!? Why are you listing this as an option without much sterner warnings?
markon commented 3 months ago
Mmm,.. they're listed as "cons" :)
pydanny commented 3 months ago
Monkey patching is a 'con'? Having had to fix this in a production site, 'con' is a very weak statement in regards to how bad this solution is for real projects.
This is why I asked why you listed it as an option without much sterner warnings.
markon commented 3 months ago
I think that if you know that your solution may break, then it's not a good one. So, if after you've read this you still consider it a good method, well, use it. Then don't bother if your code breaks :)
pydanny commented 3 months ago
I know it's bad, you know it's bad, but what about beginners?
The problem is that this gist will be read by people who may not understand how much of a dangerous hack the monkey patch method is to projects.
markon commented 3 months ago
You are right. I added a small note about it.