Search code examples
djangodjango-rest-frameworkdjango-serializer

Why dropdown field is disappeared in Django Rest Framework when using custom serializer for a field?


I am trying to use Django, DRF, ViewSet and Serializers to build a simple weblog. I have a main app and inside it:

main\models.py:

from django.db import models
from django.contrib.auth.models import User

class TimeStampMixin(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

class Category(TimeStampMixin):
    name=models.CharField(max_length=255)
    parent = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True)

    def __str__(self):
        return self.name

class PostStatus(models.Model):
    status = models.CharField(max_length=50)

    def __str__(self):
        return self.status
    
def get_draft_status():
    return PostStatus.objects.get_or_create(status='draft')[0].id

class Post(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
    title = models.CharField(max_length=200)
    description = models.TextField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True)
    status = models.ForeignKey(PostStatus, on_delete=models.CASCADE, default=get_draft_status)

    def __str__(self):
        return self.title

class RelatedPost(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='related_posts')
    related_post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='related_to')

    def __str__(self):
        return f"{self.post.title} related to {self.related_post.title}"

main\serializers.py:

from rest_framework import serializers, status
from rest_framework.response import Response
from .models import Post, RelatedPost, PostStatus, Category

class PostStatusSerializer(serializers.ModelSerializer):
    class Meta:
        model = PostStatus
        fields = '__all__'

class RelatedPostSerializer(serializers.ModelSerializer):
    class Meta:
        model = RelatedPost
        fields = '__all__'

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = '__all__'

class PostSerializer(serializers.ModelSerializer):
    related_posts = serializers.SerializerMethodField()
    category = serializers.SerializerMethodField()

    class Meta:
        model = Post
        fields = '__all__'
    
    def get_related_posts(self, obj):
        related_posts = obj.related_posts.all()
        return [{'id': rp.related_post.id, 'title': rp.related_post.title} for rp in related_posts]
    
    def get_category(self, obj):
        if obj.category:
            return {'id': obj.category.id, 'name': obj.category.name}
        else:
            return {}

main\views.py:

from .models import PostStatus, Post, RelatedPost, Category
from .serializers import PostSerializer, RelatedPostSerializer, PostStatusSerializer, CategorySerializer

class CategoryViewSet(viewsets.ModelViewSet):
    queryset = Category.objects.all()
    serializer_class = CategorySerializer

class PostStatusViewSet(viewsets.ModelViewSet):
    queryset = PostStatus.objects.all()
    serializer_class = PostStatusSerializer

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

class RelatedPostViewSet(viewsets.ModelViewSet):
    queryset = RelatedPost.objects.all()
    serializer_class = RelatedPostSerializer

main\urls.py:

from django.contrib import admin
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import PostStatusViewSet, PostViewSet, RelatedPostViewSet, CategoryViewSet

router = DefaultRouter()
router.register(r'category', CategoryViewSet, basename='category')
router.register(r'post-status', PostStatusViewSet, basename='post-status')
router.register(r'posts', PostViewSet, basename='post')
router.register(r'related-posts', RelatedPostViewSet, basename='related-post')

urlpatterns = [
    #path('admin/', admin.site.urls),
    path('', include(router.urls)),
]

Problem:

I need to return category include it's id and it's name (GET /posts/1), so I added category = serializers.SerializerMethodField() and

def get_category(self, obj):
    if obj.category:
        return {'id': obj.category.id, 'name': obj.category.name}
    else:
        return {}

into PostSerializer and it is working correctly as you can see in this screenshot (section number 1): enter image description here

before adding those parts, it was like following screenshot: enter image description here

The problem is, when I am using category = serializers.SerializerMethodField() category's dropdown list in HTML form will get disappeared (section number 2 in first image). How I can have something include section number 1 and number 4 at the same time to have customized category response and a dropdown of category in HTML form?


Solution

  • You can simply override the serializer serialization method and replace category value with the "correct" format.

    class PostSerializer(serializers.ModelSerializer):
        related_posts = serializers.SerializerMethodField()
    
        class Meta:
            ...
    
        def get_related_posts(self, obj):
            ...
    
        def to_representation(self, instance):
            new_cat_repr = {
                'id': instance.category.id, 
                'name': instance.category.name
            }
            representation = super().to_representation(instance)
            representation.pop("category")
            representation['category'] = new_cat_repr
            return representation
    

    P.S. SerializerMethodField is read-only.