how to Combine UserProfile and the User model that are connected through OneToOne relation into a single endpoint?

Viewed 36

I have a custom user class and a profile class. Profile class has a OneToOne relation with the custom User. the Serializer is having User as Meta model with adding Profile model in a new field profile extended to the fields tuple. but When I try to get the detail view it returns an error saying Profile field is not an attribute of CustomUser. I would appreciate if you go over the code that I added below and help me through this.

The User 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

The Profile Model:

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()}'

Serializer:

class DoctorProfileFields(serializers.ModelSerializer):
    """To get the fields from the DoctorProfile. it will be used in the 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()
    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

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)

What I want is a json response in the detail view like below:

{
    "name": the name,
    "avatar": avatar,
    "profile": {
        "doctor_type": "PSYCHIATRIST",
        "title": 1,
        "date_of_birth": 11-11-1990,
        "registration_number": 21547,
    }
}

Can Anybody guide me through this..? Or is there any other design approach that meets my objective. My objective is to have the user info + profile info combined in a single endpoint as a whole Profile in the frontend from which the user will see/edit profile.

1 Answers

First of all move the foreign key OneToOne in the CustomUser model, add:

owner = models.OneToOneField('DoctorProfile', on_delete=models.CASCADE, related_name='doctor_profile')

and delete from DoctorProfile:

owner = models.OneToOneField(CustomUser, on_delete=models.CASCADE, related_name='doctor_profile')

Make all migrations, and now you have to set new data in the db. In the serializers you are using Nested relationships correctly, so add the attribute many set to False:

profile = DoctorProfileFields(many=False)        

Edit

If you cant edit the structure of your models, you can work with SerializerMethodField (not tested):

class DoctorProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = DoctorProfile
        fields = ('doctor_type', 'title', 'date_of_birth', 'registration_number')

class CustomDoctorProfileSerializer(serializers.Serializer):
    name = serializers.SerializerMethodField()
    avatar = serializers.SerializerMethodField()
    profile = DoctorProfileSerializer(many=False)

    def get_name(self, obj)
        return obj.doctor_profile.name

    def get_avatar(self, obj)
        return obj.doctor_profile.avatar
Related