How to combine fields of two different model and create ModelSerializer - python

I have two models CustomUser and UserProfile.
class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(_('email address'), max_length=254, unique=True, blank=True)
first_name = models.CharField(_('first name'), max_length=30, blank=True)
last_name = models.CharField(_('last name'), max_length=30, blank=True)
class UserProfile(models.Model):
user = models.OneToOneField(CustomUser, on_delete=models.CASCADE)
owner_of = models.CharField(max_length=100, blank=True)
total_years= models.CharField(_('In Current Profession Since'),max_length=100,blank=True, null=True)
serializers.py:
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = ('id', 'email','first_name', 'last_name',)
class ProfileSerializer(serializers.ModelSerializer):
user = AccountSerializer(read_only=True,required=False)
class Meta:
model = UserProfile
fields = ('user','owner_of','total_years','first_name','last_name','email')
Error I get
"Field name first_name is not valid for model UserProfile."
How to combine two model fields and make create and update function

Your profileserializer should be like this,
class ProfileSerializer(serializers.ModelSerializer):
user = AccountSerializer(required=True)
class Meta:
model = UserProfile
fields=('user','owner_of','total_years')
which results like this,
{
{
"user":
{
"id: ...,
"first_name": ...,
"last_name": ...,
"email": ...
},
"owner_of": ...,
"total_years" ...
},
...
}
More detailed explanation is here.
By default nested serializers are read-only. If you want to support write-operations to a nested serializer field you'll need to create create() and/or update() methods in order to explicitly specify how the child relationships should be saved.
class ProfileSerializer(serializers.ModelSerializer):
user = AccountSerializer(required=True)
class Meta:
model = UserProfile
fields=('user','owner_of','total_years')
def create(self, validated_data):
user_data = validated_data.pop('user')
user= CustomUser.objects.create(**user_data)
user_profile=UserProfile.objects.create(user=user, **validated_data)
return user_profile
def update(self, instance, validated_data):
user_data = validated_data.pop('user', None)
user = instance.user
instance.owner_of = validated_data.get("owner_of", instance.owner_of)
instance.total_years= validated_data.get("total_years", instance.total_years)
instance.save()
if user_data:
user.first_name = user_data.get("first_name", user.first_name)
user.last_name = user_data.get("last_name", user.last_name)
user.email= user_data.get("email", user.email)
user.save()

Related

Trouble combining CustomUser and Profile into single endpoint in django

I have a CustomUser model and two separate Models for profile two types of User. I'm trying to combine the attribute of CustomUser and one of the Profile into single endpoint from which the user can see/update/delete the user/profile. For instance there are 2 types of users, doctor & patient. so if the user is doc then the endpoint will return the attributes of CustomUser+DoctorProfile and same for the Patient CustomUser+PatientProfile. Below is the code. I will explain the issue in the code base with comments. I will enormously appreciate any suggestion. One thing to mention is that I split the models.py file into 3 different folder and imported all of them into __init__.py of models folder.
CustomUser Model:
class CustomUser(AbstractBaseUser, PermissionsMixin):
class Types(models.TextChoices):
DOCTOR = "DOCTOR", "Doctor"
PATIENT = "PATIENT", "Patient"
# what type of user
type = models.CharField(_("Type"), max_length=50, choices=Types.choices, null=True, blank=False)
avatar = models.ImageField(upload_to="avatars/", null=True, blank=True)
email = models.EmailField(max_length=255, unique=True)
name = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
objects = CustomBaseUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['name', 'type'] #email is required by default
def get_full_name(self):
return self.name
def __str__(self):
return self.email
DoctorProfile Model:
class DoctorProfile(models.Model):
class DoctorType(models.TextChoices):
"""Doctor will choose profession category from enum"""
PSYCHIATRIST = "PSYCHIATRIST", "Psychiatrist"
PSYCHOLOGIST = "PSYCHOLOGIST", "Psychologist"
DERMATOLOGIST = "DERMATOLOGIST", "Dermatologist"
SEXUAL_HEALTH = "SEXUAL HEALTH", "Sexual health"
GYNECOLOGIST = "GYNECOLOGIST", "Gynecologist"
INTERNAL_MEDICINE = "INTERNAL MEDICINE", "Internal medicine"
DEVELOPMENTAL_THERAPIST = "DEVELOPMENTAL THERAPIST", "Developmental therapist"
owner = models.OneToOneField(CustomUser, on_delete=models.CASCADE, related_name='doctor_profile')
doctor_type = models.CharField(
_("Profession Type"),
max_length=70,
choices=DoctorType.choices,
null=True,
blank=False)
title = models.IntegerField(_('Title'), default=1, choices=TITLES)
date_of_birth = models.DateField(null=True, blank=False)
gender = models.IntegerField(_('Gender'), default=1, choices=GENDERS)
registration_number = models.IntegerField(_('Registration Number'), null=True, blank=False)
city = models.CharField(_('City'), max_length=255, null=True, blank=True)
country = models.CharField(_('Country'), max_length=255, null=True, blank=True)
def __str__(self):
return f'profile-{self.id}-{self.title} {self.owner.get_full_name()}'
Serializer:
class DoctorProfileFields(serializers.ModelSerializer):
"""To get required attributes from DoctorProfile model"""
class Meta:
model = DoctorProfile
fields = ('doctor_type', 'title', 'date_of_birth', 'registration_number', 'gender', 'city', 'country', )
class DoctorProfileSerializer(serializers.ModelSerializer):
"""Above Serializer is used in a new attribute profile. So that I can combine the CustomUser and DoctorProfile."""
profile = DoctorProfileFields(source='*')
"""
if I use source in the above line the serializer returns the json in the
expected format while I use get method, otherwise it return error saying profile
is not an attribute of CustomUser. but for put method the json payload is getting
received in a wrong formation. attributes of nested Profile object is getting
combined in the same level of Custom user,
{"name": "jon", "avatar": null, "doctor_type": "anything"}
but it has to receive like this
{"name": "jon", "avatar": null, "profile": {"doctor_type": "anything}}
"""
class Meta:
model = User
fields = ('name', 'avatar', 'profile', )
#transaction.atomic
def update(self, instance, validated_data):
ModelClass = self.Meta.model
"""print("=======validated_data=========: ", validated_data). I found in this
line that the payload is wrong"""
profile = validated_data.pop('profile', {})
"""print("=======profile=========: ", profile) profile is not in validated data
that's why profile = {}"""
ModelClass.objects.filter(id=instance.id).update(**validated_data)
if profile:
DoctorProfile.objects.filter(owner=instance).update(**profile)
new_instance = ModelClass.objects.get(id = instance.id)
return new_instance
On the other hand if I don't use source the error is same for both get and put method.
View:
class DoctorProfileAPIView(generics.RetrieveUpdateDestroyAPIView):
"""To get the doctor profile fields and update and delete"""
serializer_class = DoctorProfileSerializer
queryset = User.objects.all()
def get_object(self):
return get_object_or_404(User, id=self.request.user.id, is_active=True)
After using source the get method returns in the expected format:
{
"name": "Maruf updated again",
"avatar": null,
"profile": {
"doctor_type": null,
"date_of_birth": null,
"registration_number": null,
"city": null,
"country": null
}
}
But the problem is with the Put method.
Another note: with using source and not overriding the update method in the serializer, only the CustomUser attributes is getting updated.
my objective is to get both CustomUser+Profile in the same endpoint.
And when updating, the CustomUser and the Profile will be updated in their own table but through the same endpoint.

Depending on field value change nested serializer

I am currently trying to implement the following serializer to the Profile serializer. But I would like to add a condition to it.
Profile serializer
class UserProfileSerializer(serializers.ModelSerializer):
role = serializers.ChoiceField(choices=(('Reader', u'Reader'), ('Author', u'Author'), ('Admin', u'Admin')))
role_display = serializers.SerializerMethodField()
class Meta:
model = Profile
fields = ('gender', 'birthday', 'icon', 'role', 'role_display')
depth = 1
Author serializer
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = '__all__'
Reader serializer
class ReaderSerializer(serializers.ModelSerializer):
class Meta:
model = Reader
fields = '__all__'
Both author and reader table has a one-to-one relationship towards profile table.
Depending on the role option I would like to show a specific nested serializer.
Example:
{
"id": 19,
"username": "maoji1",
"password": "pbkdf2_sha256$180000$YhzDiqzJ4OyC$syzkwR5X3/H2p5NTB0JEK2zS5nvYu5ddHrTgy3cYU/E=",
"email": "pbkdf2_sha256$180000$YhzDiqzJ4OyC$syzkwR5X3/H2p5NTB0JEK2zS5nvYu5ddHrTgy3cYU/E=",
"profile": {
"gender": "male",
"birthday": "2020-03-10",
"icon": null,
"role": {
is_vip:True,
validate_date:...
}
}
}
Reader model
class Reader(models.Model):
user = models.OneToOneField(Profile, on_delete=models.CASCADE, related_name='reader', verbose_name='user')
is_user_vip = models.CharField(choices=(('normal', u'Normal'), ('vip', u'VIP')),
default='normal',
max_length=10,
verbose_name=u'Vip status')
vip_validate = models.DateField(verbose_name=u"Validate Until",
auto_now_add=True, null=True,
editable=False)
Author model
class Author(models.Model):
user = models.OneToOneField(Profile, on_delete=models.CASCADE, related_name='author')
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='Book', null=True)
contract_number = models.IntegerField(verbose_name='Contact number', null=True, blank=True)
Profile model
class Profile(models.Model):
role_choice = (
('Reader', u'Reader'),
('Author', u'Author'),
('Editor', u'Editor'),
('Admin', u'Admin')
)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile', verbose_name='user')
gender = models.CharField(choices=(("male", u"Male"), ("female", u"Female")),
default="Female",
max_length=150, verbose_name='Gender')
birthday = models.DateField(null=True, blank=True, verbose_name="Birthday")
icon = models.ImageField(upload_to="media/image/%Y/%m",
default=u"media/image/default.png",
max_length=1000,
verbose_name=u"User icon", null=True)
role = models.CharField(choices=role_choice,
max_length=150,
default='Admin',
verbose_name='Role')
You can use SerializerMethodField and decide which serializer you must use inside it's method:
class UserSerializer(serializers.ModelSerializer):
profile = serializers.MethodField()
class Meta:
model = Profile
fields = ('id', 'username', 'password', 'email','profile')
def get_profile(self,obj):
return ProfileSerializer(obj.profile).data
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields ='__all__'
class ReaderSerializer(serializers.ModelSerializer):
class Meta:
model = Reader
fields ='__all__'
class ProfileSerializer(serializers.ModelSerializer):
role = serializers.MethodField()
class Meta:
model = Profile
fields = ('gender', 'birthday', 'icon', 'role')
def get_role(self,obj):
if hasattr(obj, 'author')
serializer = AuthorSerializer(obj.author)
return serializer.data
elif hasattr(self,'reader'):
serializer = ReaderSerializer(obj.reader)
return serializer.data
return {} # if your profile doesn't have any relation with author or reader
class UserSerializer(serializers.ModelSerializer):
profile = serializers.MethodField()
class Meta:
model = User
fields = ('id', 'username', 'password', 'email','profile')
def get_profile(self,obj):
return ProfileSerializer(obj.profile).data

Django Rest - Serializer: must be a instance on create

I'm trying to create create a nested serializer using the Django Rest framework. The relationship is Profile X User but when i use Profile.objects.create(user=profile, **user_data) i get ValueError: Cannot assign "<Profile: Profile object (7)>": "Profile.user" must be a "User" instance..
This should be some rookie misunderstanding of models relationship definitions or the serializer declaration itself but I can't find anything around the docs. If someone can point me a direction I'll be gracefull.
models.py
class User(models.Model):
name = models.CharField(max_length=100, blank=False)
email = models.CharField(max_length=100, blank=True, default='')
password = models.CharField(max_length=100, blank=True, default='')
timestamp = models.DateTimeField(default= timezone.now)
class Meta:
ordering = ['timestamp']
class Profile(models.Model):
# choices [...]
user = models.OneToOneField(User, on_delete=models.CASCADE, null=True)
profile_type = models.CharField(max_length=2,choices=PROFILE_CHOICES,default=TEAMMEMBER)
authentication_token = models.CharField(max_length=100, null=True)
avatar_url = models.CharField(max_length=100, default='')
permissions = models.CharField(max_length=100, null=True)
timestamp = models.DateTimeField(default= timezone.now)
class Meta:
ordering = ['timestamp']
serializer.py
class UserSerlializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['name', 'email', 'password']
class ProfileSerializer(serializers.ModelSerializer):
user = UserSerlializer()
class Meta:
model = Profile
fields = ['user', 'profile_type']
def create(self, validated_data):
user_data = validated_data.pop('user')
profile = Profile.objects.create(**validated_data)
Profile.objects.create(user=profile, **user_data)
return Profile
POST
{
"profile_type" : "ST",
"user": {
"name" : "test",
"email" : "test#test.com",
"password" : "123456"
}
}
You are creating instances in wrong way. Change your create(...) method as,
class ProfileSerializer(serializers.ModelSerializer):
user = UserSerlializer()
class Meta:
model = Profile
fields = ['user', 'profile_type']
def create(self, validated_data):
user_data = validated_data.pop('user')
user_instance = User.objects.create(**user_data)
profile_instance = Profile.objects.create(user=user_instance, **validated_data)
return profile_instance
Profile.user should beUser instance, but you are assigning Profile instance.
Change your create method to this:
class ProfileSerializer(serializers.ModelSerializer):
user = UserSerlializer()
class Meta:
model = Profile
fields = ['user', 'profile_type']
def create(self, validated_data):
user_data = validated_data.pop('user')
profile = Profile.objects.create(**validated_data)
user = User.objects.create(**user_data) # 1. creating user
profile.user = user # 2. assigning user
profile.save() # 3. saving profile after adding user
return profile # returning Profile instance.
inherit your user model from django contrib auth module also, and make a one to one relation with profile
from django.contrib.auth.models import User

Django access nested object field in serializer

I Django-Rest have a class User that contain the field first_name, and a class Account that contain the fields username and a_class_ref that is a one-to-one' relation.
How it is possible in the serializer of B to do something like :
class AccountSerializer():
class Meta:
model= Account
fields= [
'username',
'firstname`
]
Account :
class Account(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
related_name='account',
on_delete=models.CASCADE
)
def username(self):
return self.user.username <== this is the solution that I'm trying to avoid
And User is the extended AbstractUser from Django-rest-framework, that comes with a first_name = models.CharField(_('first name'), max_length=30, blank=True)
Thank you
You can declare a custom field with the source attribute:
class BSerializer(serializers.ModelSerializer):
a_field = serializers.CharField(source='a_class_ref.a_field')
class Meta:
model= B
fields= ['b_field', 'a_field']
Edit
Based on the models you posted, the following should work:
class Account(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
related_name='account',
on_delete=models.CASCADE
)
class AccountSerializer(serializers.ModelSerializer):
username = serializers.CharField(source='user.username')
firstname = serializers.CharField(source='user.first_name')
class Meta:
model= Account
fields= ['username', 'firstname']

Getting attribute error when trying to access the nested serializer in Django Rest Framework

I am getting following error while using the PostSerializer:
Got AttributeError when attempting to get a value for field
full_name on serializer UserSerializer. The serializer field might
be named incorrectly and not match any attribute or key on the long
instance. Original exception text was: 'long' object has no attribute
'full_name'.
Serializers are as follows:
class PostSerializer(serializers.ModelSerializer):
author = UserSerializer(required=False, allow_null=True)
class Meta:
model = Post
fields = ('id', 'author', 'message', 'rating', 'create_date', 'close_date',)
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'full_name',)
View:
class PostMixin(object):
model = Post
serializer_class = PostSerializer
permission_classes = [
PostAuthorCanEditPermission
]
queryset = model.objects.all()
def pre_save(self, obj):
"""Force author to the current user on save"""
obj.author = self.request.user
return super(PostMixin, self).pre_save(obj)
class PostList(PostMixin, generics.ListCreateAPIView):
pass
User model:
class User(AbstractBaseUser):
email = models.EmailField(unique=True)
username = models.CharField(max_length=40, unique=True, null=True)
full_name = models.CharField(max_length=50, blank=False)
phone = models.CharField(max_length=20, unique=True, null=True)
about = models.CharField(max_length=255, blank=True)
type = models.CharField(max_length=1, default='U')
is_active = models.BooleanField(default=True)
date_joined = models.DateTimeField(auto_now_add=True)
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['full_name']
def __unicode__(self):
return self.email
def get_full_name(self):
return self.full_name
def get_short_name(self):
return self.full_name
Problem
Got AttributeError when attempting to get a value for field full_name on serializer UserSerializer.
The model User in Django has no such field called full_name.
There is though a method get_full_name() that does what you want.
Solution
So try using it through a SerializerMethodField
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username') # no full_name here
full_name = serializers.SerializerMethodField('get_full_name')
This will add a field called full_name to your serialized object, with the value pulled from User.get_full_name()
Check you are using your custom model and not Django's User model
You've customized your own User model, but since that models has full_name, you shouldn't have gotten that error in the first place, so double check you are not referencing Django's default User model first.
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User # <--- Make sure this is your app.models.User,
# and not Django's User model
fields = ('id', 'username', 'full_name',) # This is OK on your User model
or just
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'first_name', 'last_name')

Categories

Resources