Dev Journal #7 - Extending the User Model
djangoWrapper Model
Currently, I am using the Django’s built-in user model from the django.contrib.auth library. As I discussed earlier in my Dev Journal #4, this library provides a lot of tools for handling common authentication practices.

The default underlying auth_user
table for the User
model can already handle storing basic information about the user account. However, I will eventually need to save additional user information that is specific to my application as I continue developing. I can technically achieve this by extending the same auth_user
table to carry the extra columns for each additional user fields. However, this approach could further complicate the default behaviour and clutter the model. Instead, I will create a wrapper model called UserProfile
that will have an One-to-One relationship with the User
model. In addition to having access to User
model fields, UserProfile
will have its own fields to handle any additional custom attributes. Below are the two entities from the previously proposed ERD diagram that I will be referring to in this post.

UserProfile
The declaration of this new model class would look something like this:
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(max_length=150, null=True, blank=True)
birth_date = models.DateField(null=True, blank=True)
gender = models.CharField(max_length=25, choices=GenderType.tuple(), null=True, blank=True)
pronoun = models.CharField(max_length=20, choices=PronounType.tuple(), null=True, blank=True)
image = models.ImageField(upload_to=upload_profile_image, null=True, blank=True)
unit_type = models.CharField(max_length=8, choices=UnitType.tuple(), default=UnitType.get_default())
is_private = models.BooleanField(default=False)
@property
def owner(self):
return self.user
The user
attribute is defined as OneToOneField
with the User
model, which means that for every UserProfile
model, there will exist a link to the User
model that holds the account information. The rest of the attributes are additional custom fields I wish to capture about the user. Notice how the attributes gender
, pronoun
and unit_type
are CharField
but it behaves like an enum when provided with the list of tuples as the choices parameter. Each tuple has two items where the first element is the actual value and the second is the display name. If the choices are given, the field will be enforced with the built-in model validation which raises errors if the user tries to input an undefined choice value. Lastly, the @property
decorator is used to mark the owner of the object which is helpful to know during delete requests.
Signal Dispatchers
Finally, I want the UserProfile
to be instantiated every time a new User
instance is created. A signal receiver can be used to get notified every time a user account is created. Then the receiver runs a function that creates a corresponding UserProfile
which links back to that particular user account. Another receiver listening for delete events from the UserProfile
can be used to cascade the deletion down to the associated User
model.
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.userprofile.save()
@receiver(post_delete, sender=UserProfile)
def post_delete_user(sender, instance, *args, **kwargs):
if instance.user:
instance.user.delete()
Serializer
Serializers allow querysets and model instances to be rendered into a JSON data format. It can also deserialize parsed data to be converted back to model instances. They are also used for validating all incoming data. Below is my example of UserProfileSerializer
, which contains fields combined from both the User
model and the UserProfile
model.
class UserProfileSerializer(serializers.ModelSerializer):
username = serializers.CharField(source='user.username', read_only=True)
first_name = serializers.CharField(source='user.first_name', required=False)
last_name = serializers.CharField(source='user.last_name', required=False)
last_login = serializers.DateTimeField(source='user.last_login', read_only=True)
date_joined = serializers.DateTimeField(source='user.date_joined', read_only=True)
class Meta:
model = UserProfile
fields = [
'username',
'first_name',
'last_name',
'bio',
'pronoun',
'gender',
'birth_date',
'unit_type',
'last_login',
'date_joined',
'is_private'
]
I only allow updates on first_name
and last_name
for the fields from the User
model. All fields that belong to UserProfile
are allowed to be updated as well. Django provides basic validation based on the data type defined in its model class. However, it’s also important to provide some additional semantic validations on top of the basic validations to provide a better user experience. I am also overriding the first_name
and last_name
fields from the nested model serializer. This is because nested serializer validations are skipped if the parent serializer throws an exception. However, I want the user to receive all validation messages at once and this can be done if I override the default validation in the parent serializer.
class UserProfileSerializer(serializers.ModelSerializer):
...
# Custom validations
@staticmethod
def validate_birth_date(value):
if value > datetime.date.today():
raise serializers.ValidationError('Date of birth cannot be in the future.')
return value
# Overriding default User model validation
@staticmethod
def validate_first_name(value):
if len(value) > 30:
raise serializers.ValidationError('First name cannot exceed more than 30 characters.')
return value
@staticmethod
def validate_last_name(value):
if len(value) > 30:
raise serializers.ValidationError('Last name cannot exceed more than 30 characters.')
return value
...
Since I am updating two different models through a single serializer, I need to override the update function of the serializers.ModelSerializer
to change its default behaviour. The first_name
and last_name
fields are provided as the user
object’s attributes, and the user
object is nested under the original instance. In the new update function, I unstitch the given instance into two separate instances for each model and save the validated data accordingly. I also call the UserDetailSerializer
with the partial=True
parameter because I only want to update the first_name
and the last_name
, but the serializer will be expecting values from all fields belonging to UserDetailSerializer
by default.
from django.contrib.auth import get_user_model
User = get_user_model()
class UserProfileSerializer(serializers.ModelSerializer):
...
def update(self, instance, validated_data):
# Update Uer instances that are in the request
user_data = validated_data.pop('user', {})
user_serializer = UserSerializer(instance.user, data=user_data, partial=True)
user_serializer.is_valid(raise_exception=True)
user_serializer.update(instance.user, user_data)
# Update UserProfile instance
super(UserProfileSerializer, self).update(instance, validated_data)
return instance
...
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
'id',
'username',
'email',
'date_joined',
'last_login',
'first_name',
'last_name'
]
Here is a sample error response body returned from the server when the data failed validations during the PUT
requests
Input
{
"first_name": "Adolph Blaine Charles David Earl Frederick Gerald Hubert Irvin John Kenneth Lloyd Martin Nero Oliver Paul Quincy Randolph Sherman Thomas Uncas Victor William Xerxes Yancy Zeus",
"last_name": "Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepflegeundsorgfaltigkeitbeschutzenvorangreifendurchihrraubgierigfeindewelchevoralternzwolfhunderttausendjahresvorandieerscheinenvonderersteerdemenschderraumschiffgenachtmittungsteinundsiebeniridiumelektrischmotorsgebrauchlichtalsseinursprungvonkraftgestartseinlangefahrthinzwischensternartigraumaufdersuchennachbarschaftdersternwelchegehabtbewohnbarplanetenkreisedrehensichundwohinderneuerassevonverstandigmenschlichkeitkonntefortpflanzenundsicherfreuenanlebenslanglichfreudeundruhemitnichteinfurchtvorangreifenvorandererintelligentgeschopfsvonhinzwischensternartigraum",
"bio": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
"birth_date": "2029-01-01",
"unit_type": "FFF",
"gender": "GENDERQUEER",
"pronoun": "HE",
"is_private": "depends"
}
Output
{
"first_name": [
"First name cannot exceed more than 30 characters."
],
"last_name": [
"Last name cannot exceed more than 30 characters."
],
"bio": [
"Ensure this field has no more than 150 characters."
],
"birth_date": [
"Date of birth cannot be in the future."
],
"unit_type": [
"\"FFF\" is not a valid choice."
],
"is_private": [
"Must be a valid boolean."
]
}
Lastly, I run a conditional check on the is_private
field to see if I need to hide any sensitive user data from the public. Later on, the same field will be used to determine whether to hide the user’s followers and following lists, user’s achievements as well as the user’s exercise logs. For now, I will hide the email
, first_name
, last_name
, last_login
, date_joined
, birth_date
, weight
, height
and unit_type
fields if the account is set to private mode. To hide the data, I simply return a different representation of the serialized field by overriding the default to_representation
method. In my code below, I return a dictionary comprehension that loops through the sensitive field list and excludes them from the original serialization. However, I shouldn’t be hiding this information if the API request is made by the owner of the profile, so I added an additional check to exclude those cases.
class UserProfileSerializer(serializers.ModelSerializer):
...
# Exclude sensitive data to other users if is_private=True
def to_representation(self, instance):
ret = super().to_representation(instance)
if self.context['request'].user != self.instance.user and ret.get('is_private'):
sensitive_fields = ['first_name', 'last_name', 'gender', 'pronoun', 'birth_date', 'last_login',
'unit_type', 'date_joined']
return {key: ret[key] for key in ret if key not in sensitive_fields}
return ret
Create an API View
The last step is to provide an access point for users to communicate to the backend. POST
API is not needed because the UserProfile
should only be instantiated when a new user account is created via the signal dispatcher. The GET
API is required to allow users to view user profiles. But I don’t want to give everyone access to view everyone else’s profile! Only those who have been authenticated through the login system should be able to view other users. And not all other users but only the ones who have been successfully activated through the email confirmation and are recognized as active users. The PUT
API is also required to allow users to update their own profiles. Similarly, I should only let the owner of the profile be able to update their own profile and nobody else’s. These permission rules can be enforced by the IsOwnerOrReadOnly
permission which is defined as below. Finally, the DELETE
API is required to delete the profile and close the user accounts completely. Since deleting an account is a very serious action, I will handle this in a separate API that requires email confirmation.
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
# Instance must have an attribute named `owner`.
return obj.owner == request.user
In the urls.py
under the user
module, I included the path to the API as path('<str:user__username>/', UserProfileAPIView.as_view(), name='profile-detail')
. I use the username as my lookup_field
in the url instead of the user’s id because I prefer working with human readable APIs.
class UserProfileAPIView(generics.RetrieveUpdateAPIView):
permission_classes = [permissions.IsAuthenticated, IsOwnerOrReadOnly]
serializer_class = UserProfileSerializer
lookup_field = 'user__username'
def get_queryset(self):
return UserProfile.objects.filter(user__is_active=True)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
There is still some additional work that needs to be done. For example, I am planning on building an email change API that sends email confirmation before updating the email address. I think user delete requests should be handled in the same way as well.
Django has been surprisingly fun to work with despite the initial learning curve. I just wish I had more time to code… yaaaaaawn
Good night 🌙