Search code examples
jsondjangoserializationdjango-rest-frameworknested-json

Update multiple model data through one serializer


Please go through the description, I tried to describe everything i've encountered while trying to solve this issue.

I have two models, User and DoctorProfile. User model has OneToOne relation with DoctorProfile. I'm trying to update data of both model through one serializer. I've combined two models into one serilzer like below:

class DoctorProfileFields(serializers.ModelSerializer):
    """this will be used as value of profile key in DoctorProfileSerializer"""
    class Meta:
        model = DoctorProfile
        fields = ('doctor_type', 'title', 'date_of_birth', 'registration_number', 'gender', 'city', 'country', )

class DoctorProfileSerializer(serializers.ModelSerializer):
    """retrieve, update and delete profile"""

    profile = DoctorProfileFields(source='*')
    class Meta:
        model = User
        fields = ('name', 'avatar', 'profile', )
        
    @transaction.atomic
    def update(self, instance, validated_data):
        ModelClass = self.Meta.model
        profile = validated_data.pop('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 

When I send request with GET method, the DoctorProfileSerializer returns nested data(Combining two models User and DoctorProfile) in the desired fashion. But when I try to update both models through this serializer, it returns error saying User has no field named 'doctor_type'.

Let's have a look at the JSON i'm trying to send:

{
    "name": "Dr. Strange updated twice",
    "profile" : {
        "doctor_type": "PSYCHIATRIST"
    }
    
}

Let's have a look at how the serializer is receiving the JSON:

{
    "name": "Maruf updated trice",
    "doctor_type": "PSYCHIATRIST"
    
}

Models:

class CustomUser(AbstractBaseUser, PermissionsMixin):

    class Types(models.TextChoices):
        DOCTOR = "DOCTOR", "Doctor"
        PATIENT = "PATIENT", "Patient"

    #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


class DoctorProfile(models.Model):
    """Model for Doctors profile"""
    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()}'

How do I know that the serializer is getting wrong JSON? I debugged the validated_data in the DoctorProfileSerializer and it's showing that it's a flat JSON, there's no key named profile.

I'm assuming the problem is with the source that I've added in the DoctorProfileSerializer. But if I don't use the source the get method returns the following error Got AttributeError when attempting to get a value for field profile on serializer (DoctorProfileSerializer).

Please let me know if it's solvable also if it's a good approach to do it this way?


Solution

  • Ok, sorry if my answer is too long but let me try to answer step by step,

    Models:

    class DoctorProfile(models.Model):
        # everything as it is
        # except I feel comfortable using ForeignKey :D
        owner = models.ForeignKey(
            CustomUser, 
            on_delete=models.CASCADE, 
            related_name='doctor_profile'
        )
        # everything as it is
    
    class CustomUser(AbstractBaseUser, PermissionsMixin):
    # as it is
    

    Serializers:

    class DoctorProfileSerializer(serializers.ModelSerializer):
    """Serializer for DoctorProfile."""
    
        class Meta(object):
            model = DoctorProfile
            fields = [
                'id',
                'doctor_type', 
                'title', 
                'date_of_birth', 
                'registration_number', 
                'gender', 
                'city', 
                'country', 
            ]
            read_only_fields = [
                'id',
            ]
    
    class CustomUserSerializer(serializers.ModelSerializer):
    """Serializer for DoctorProfile."""
        
        # here I'm renaming the related object exactly as the 
        # related name you've provided on model
        doctor_profile = DoctorProfileSerializer(many=False)
    
        class Meta(object):
            model = CustomUser
            fields = [
                'name', 
                'avatar', 
                'doctor_profile', 
            ]
            read_only_fields = [
                'id',
            ]
    
        def update(self, instance, validated_data):
            # instance is the current row of CustomUser
            # validated_data is the new incoming data
            # use validated_data.pop('doctor_profile') to extract
            # doctor_profile data and do whatever is needed on 
            # DoctorProfile model 
            # compare them and perform your update method
            # as you wish on the DoctorProfile model
        # object after updating models, you can query the total
        # object again before returning if you want
        return updated_object   
    

    View:

    class CustomUserAPIView(RetrieveUpdateAPIView):
        """CustomUserAPIView."""
    
        permission_classes = [IsAuthenticated]
        model = CustomUser
        serializer_class = CustomUserSerializer
        lookup_field = 'id'
    
        #for returning logged in user info only
        def get_queryset(self):
            return CustomUser.objects.filter(id=self.request.user.id).first()
    
        def update(self, request, *args, **kwargs):
            """Update override."""
            partial = kwargs.pop('partial', False)
            instance = self.get_object()
            serializer = self.get_serializer(
                instance,
                data=request.data,
                partial=partial,
            )
            serializer.is_valid(raise_exception=True)
            self.perform_update(serializer)
            custom_user_obj = CustomUser.objects.filter(
                id=instance.id,
            ).first()
            serializer = CustomUserSerializer(custom_user_obj)
            return Response(serializer.data)
    

    Run the migration and let me know if you are getting the expected output on GET method. For UPDATE method if you face any problem let me know I will update the answer accordingly right away.

    For keeping all the Django Rest Framework related docs handy, use this link https://www.cdrf.co/