0

I am wrote a serializer for the User model in Django with DRF:

the model:

from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import BaseUserManager
from django.db import models
from django.utils.translation import ugettext


class BaseModel(models.Model):
    # all models should be inheritted from this model
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


class User(AbstractBaseUser, BaseModel):
    username = models.CharField(
        ugettext('Username'), max_length=255,
        db_index=True, unique=True
    )
    email = models.EmailField(
        ugettext('Email'), max_length=255, db_index=True,
        blank=True, null=True, unique=True
    )

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ('email', 'password',)

    class Meta:
        app_label = 'users'

the serializer:

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.User
        fields = ['email', 'username', 'password']
        extra_kwargs = {'password': {'write_only': True}}

    def create(self, validated_data):
        user = super().create(validated_data)
        user.set_password(validated_data['password'])
        user.save()
        return user

    def update(self, user, validated_data):
        user = super().update(user, validated_data)
        user.set_password(validated_data['password'])
        user.save()
        return user

It works. But I probably do two calls instead of one on every create/update and the code looks a little bit weird(not DRY). Is there an idiomatic way to do that?

$python -V
Python 3.7.3

Django==2.2.3
djangorestframework==3.10.1
kharandziuk
  • 12,020
  • 17
  • 63
  • 121

2 Answers2

1

You can create your own user manager by overriding BaseUserManager and use set_password() method there. There is a full example in django's documentation. So your models.py will be:

# models.py
from django.db import models
from django.contrib.auth.models import (
    BaseUserManager, AbstractBaseUser
)


class MyUserManager(BaseUserManager):
    def create_user(self, email, username, password=None):

        if not email:
            raise ValueError('Users must have an email address')

        user = self.model(
            email=self.normalize_email(email),
            username=username,
        )

        user.set_password(password)
        user.save(using=self._db)
        return user


class BaseModel(models.Model):
    # all models should be inheritted from this model
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


class User(AbstractBaseUser, BaseModel):
    username = models.CharField(
        ugettext('Username'), max_length=255,
        db_index=True, unique=True
    )
    email = models.EmailField(
        ugettext('Email'), max_length=255, db_index=True,
        blank=True, null=True, unique=True
    )

    # don't forget to set your custom manager
    objects = MyUserManager()

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ('email', 'password',)

    class Meta:
        app_label = 'users'

Then, you can directly call create_user() in your serializer's create() method. You can also add a custom update method in your custom manager.

# serializers.py
class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = models.User
        fields = ['email', 'username', 'password']
        extra_kwargs = {'password': {'write_only': True}}

    def create(self, validated_data):
        user = models.User.objects.create_user(
            username=validated_data['username'],
            email=validated_data['email'],
            password=validated_data['password']
        )
        return user
cagrias
  • 1,847
  • 3
  • 13
  • 24
  • I need to call `set_password` and `save` of the model. or I need to call `create_user`, but then I can't call super on `create` in serializer – kharandziuk Jul 18 '19 at 13:17
  • https://github.com/kharandziuk/django-jwt-authentication - here you can find the code with some tests. Try what you are proposing. And try to run the tests – kharandziuk Jul 18 '19 at 13:38
1

I hope this will solve the issue,

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.User
        fields = ['email', 'username', 'password']
        extra_kwargs = {'password': {'write_only': True}}

    def create(self, validated_data):
        return models.User.objects.create_user(**validated_data)

    def update(self, user, validated_data):
        password = validated_data.pop('password', None)
        if password is not None:
            user.set_password(password)
        for field, value in validated_data.items():
            setattr(user, field, value)
        user.save()
        return user

The create_user() method uses the set_password() method to set the hashable password.

JPG
  • 82,442
  • 19
  • 127
  • 206
  • yep, it should work. The only problem: we don't call super of the serializer in this case. and it contains some code. what is your opinion, Is it a problem? it has some code related to the validation – kharandziuk Jul 18 '19 at 13:53
  • It won't be a problem unless you are trying to save some ***relational data*** (`FK` or `M2M` fields) – JPG Jul 18 '19 at 13:58