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.
Related
In DRF I am facing an issue, whenever I do a POST request on the endpoint, on the field "name" which is a text field I get an exception "Field 'id' expected a number but got 'TITLE'", but when I change the value of "name" to an integer the request is successful I don't understand it becauses name is TextField in model and why its mixing Id and Name field with each other. I have deleted the migration files from the Project and DB and re-run the Migrations, but still facing this issue.
Following is my code:
models.py
class Project(models.Model):
admin = models.ForeignKey(User, on_delete=models.CASCADE, related_name='project_crated_by')
name = models.TextField(max_length=225, blank=False, null=False)
project_members = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='members', null=True, blank=True)
created_on = models.DateField(default=timezone.now)
tags = ArrayField(models.CharField(max_length=225, default=''), blank=True)
def __str__(self):
return self.name
objects = models.Manager()
views.py
class ProjectView(viewsets.ViewSet):
def create(self, request):
project_name_exist = Project.verify_project_name(request.data['admin'], request.data['name'])
if project_name_exist:
return Response({'message': 'You already have a project with this name',
'status': status.HTTP_200_OK})
serialized_project = ProjectSerializer(data=request.data)
if serialized_project.is_valid():
serialized_project.save()
return Response({'message': 'Project Created Successfully', 'status': status.HTTP_201_CREATED})
else:
return Response({'error': serialized_project.errors, 'status': status.HTTP_400_BAD_REQUEST})
serializer.py
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = '__all__'
A more generic and non-DIY solution is to use UniqueTogetherValidator on your serializer and let Django sort it out.
from rest_framework.validators import UniqueTogetherValidator
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = '__all__'
validators = [
UniqueTogetherValidator(
queryset=Project.objects.all(),
fields=['admin', 'name'],
message='You already have a project with this name'
)
]
And/or add it to the model for enforcing it on the database.
class Project(models.Model):
admin = models.ForeignKey(User, on_delete=models.CASCADE, related_name='project_crated_by')
name = models.TextField(max_length=225, blank=False, null=False)
project_members = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='members', null=True, blank=True)
created_on = models.DateField(default=timezone.now)
tags = ArrayField(models.CharField(max_length=225, default=''), blank=True)
def __str__(self):
return self.name
objects = models.Manager()
class Meta:
unique_together = ("admin", "name")
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
I am creating an api where the list of groups are shown along with the devices id that falls under that groups. For example if there is a device named Speedometer, Humidifier and they fall under Home group then my api should include
{
"id": 1,
"name": "Home"
"device_list": [
{
"id": "b45c56ioxa1"
},
{
"id": "h4oc2d5ofa9"
}
]
},
but my code does not produce device_list in the api. It only shows name and id
device_list is the list of all the devices id that are in a certain group.
Here is my code
class DeviceIdSerializer(serializers.ModelSerializer):
id = serializers.UUIDField(source='token', format='hex', read_only=True)
class Meta:
model = Device
fields = ('id')
class DeviceGroupSerializer(serializers.ModelSerializer):
name = serializers.StringRelatedField()
device_list = DeviceIdSerializer(read_only=False, many=True, required=False, source="groups")
class Meta:
model = DeviceGroup
fields = ('id', 'name', 'device_list')
class DevicesGroupsAPIView(APIView):
permission_classes = (permissions.IsAuthenticated,)
def get(self, request, format=None):
"""
Returns a list of groups
"""
reply = {}
try:
groups = DeviceGroup.objects.all()
print ('reply', groups)
reply['data'] = DeviceGroupSerializer(groups, many=True).data
except:
reply['data'] = []
return Response(reply, status.HTTP_200_OK)
class BaseDevice(PolymorphicModel):
# User's own identifier of the product
name = models.CharField(max_length=250, blank=False, null=False)
# Any device should have a owner, right from the creation
owner = models.ForeignKey(User, blank=False, null=False)
token = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
group = models.ForeignKey('DeviceGroup', related_name="groups", null=True, blank=True)
class Device(BaseDevice):
description = models.TextField(blank=True, null=True)
class DeviceGroup(models.Model):
name = models.CharField(max_length=250, blank=False, null=False)
I tried out the very same code you have except I used models.Model as the base model.
The first time I got an error
The fields option must be a list or tuple or "all". Got str.
which clearly states where your problem is.
So I changed class the fields option in DeviceIdSerializer
DeviceIdSerializer(serializers.ModelSerializer):
id = serializers.UUIDField(source='token', format='hex', read_only=True)
class Meta:
model = Device
fields = ('id',)
Note that I've added a comma (",") which makes fields a tuple instead of a string as it was before.
Now the data I get is
{
"id":1,
"name":"test",
"device_list":[
{
"id":"38ec7e152f9d49a38008c859a1022525"
},
{
"id":"b0d799509260474cb092899ef84ce49c"
},
{
"id":"e5c7cf8f9f5043c68c34c7b962569b08"
}
]
}
which is the same as what you are looking for...
I think your serializers need to look like this:
class DeviceIdSerializer(serializers.ModelSerializer):
id = serializers.UUIDField(source='token', format='hex', read_only=True)
class Meta:
model = Device
fields = ('id')
class DeviceGroupSerializer(serializers.ModelSerializer):
name = serializers.StringRelatedField()
groups = DeviceIdSerializer(read_only=False, many=True, required=False)
class Meta:
model = DeviceGroup
fields = ('id', 'name', 'groups')
Or change this:
class BaseDevice(PolymorphicModel):
# User's own identifier of the product
name = models.CharField(max_length=250, blank=False, null=False)
# Any device should have a owner, right from the creation
owner = models.ForeignKey(User, blank=False, null=False)
token = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
group = models.ForeignKey('DeviceGroup', related_name="device_list", null=True, blank=True)
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()
My API call to api/business-review/3abe3a1e-199c-4a4b-9d3b-e7cb522d6bed/ currently returns the following:
[
{
"id": "3abe3a1e-199c-4a4b-9d3b-e7cb522d6bed",
"date_time": "2016-05-31T19:18:24Z",
"review": "Another quality job, Anna has a no fuss approach to his job and clearly takes pride in what he does. Will continue to use again and again.",
"rating": "4.0",
"person": "c1cc5684-1be1-4120-9d81-05aec29f352a",
"employee": "ecdc1f99-138c-4f9f-9e1f-b959d59209aa",
"service": "1dfa408f-d5bc-4eb2-96ae-e07e7999a01a",
}
]
Now I want to create three new fields:
person_name - which grabs the first_name and last_name of the reviewer
employee_name - which grabs the first_name and last_name of the employee
service_name - which grabs the title of the service
I've tried the following so far but it doesn't create any new variables:
serializers.py
class ReviewSerializer(serializers.ModelSerializer):
"""
Class to serialize Review objects
"""
person_name = serializers.CharField(source='person.reviewer.first_name', read_only=True)
employee_name = serializers.CharField(source='person.employer.first_name', read_only=True)
service_name = serializers.CharField(source='service.title', read_only=True)
class Meta:
model = Review
fields = '__all__'
read_only_fields = 'id'
models.py
class Review(models.Model):
"""
Review model
"""
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
date_time = models.DateTimeField(blank=True, null=True, default=None)
person = models.ForeignKey(Person, null=True, default=None, related_name='reviewer')
employee = models.ForeignKey(Person, null=True, default=None, related_name='employee')
review = models.TextField(null=True, default=None)
service = models.ForeignKey(Service, null=True, default=None)
rating = models.DecimalField(max_digits=4, decimal_places=1)
class Person(models.Model):
"""
Person entity
"""
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
class Service(models.Model):
"""
Service model
"""
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
title = models.CharField(max_length=255)
Please try adding the field name explicitly instead of using __all__ which only picks up fields which are present in model and not those defined in serializer like this
fields = ['person', 'service', 'review', 'employee', 'person_name', 'service_name', 'employee_name', 'date_time']
Since you want add data to the serialized representation of your object, its better to use SerializerMethodField() for person_name, service_name and employee_name fields.
This is a read-only field. It gets its value by calling a method on
the serializer class it is attached to. It can be used to add any sort
of data to the serialized representation of your object.
Also, since the ForeignKey fields person, employee and service allow null values, you will have to handle the case when they are actually null. Otherwise, AttributeError exception will be raised on the serializer.
class ReviewSerializer(serializers.ModelSerializer):
"""
Class to serialize Review objects
"""
person_name = serializers.SerializerMethodField()
employee_name = serializers.SerializerMethodField()
service_name = serializers.SerializerMethodField()
class Meta:
model = Review
fields = ['person', 'service', 'review', 'employee', 'person_name', 'service_name', 'employee_name', 'date_time']
read_only_fields = 'id'
def get_person_name(self, obj):
try:
person_name = obj.person.first_name + obj.person.last_name
except AttributeError: # handle case if person is null
return None
else:
return person_name
def get_employee_name(self, obj):
try:
employee_name = obj.employee.first_name + obj.employee.last_name
except AttributeError: # handle case if employee is null
return None
else:
return employee_name
def get_service_name(self, obj):
try:
service_name = obj.service.title
except AttributeError: # handle case if service is null
return None
else:
return service_name