Can I return multiple values from a SerializerMethodField? I am getting an error telling me I can't. How do I then get the values?

Question:

I have a post model that represents a normal post with images and possibly a video. I have a post reply model that represents comments or replies to a post.

Here is the models.py file:

class Category(models.Model):
    name = models.CharField(max_length=100, verbose_name="name")
    slug = AutoSlugField(populate_from=["name"])
    description = models.TextField(max_length=300)
    parent = models.ForeignKey(
        "self", on_delete=models.CASCADE, blank=True, null=True, related_name="children"
    )

    created_at = models.DateTimeField(auto_now_add=True, verbose_name="created at")
    updated_at = models.DateTimeField(auto_now=True, verbose_name="updated at")

    class Meta:
        verbose_name = "category"
        verbose_name_plural = "categories"
        ordering = ["name"]
        db_table = "post_categories"

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return self.slug


def video_directory_path(instance, filename):
    return "{0}/posts/videos/{1}".format(instance.user.id, filename)


def post_directory_path(instance, filename):
    return "posts/{0}/images/{1}".format(instance.post.id, filename)


def reply_directory_path(instance, filename):
    return "replies/{0}/images/{1}".format(instance.reply.id, filename)


def reply_videos_directory_path(instance, filename):
    return "{0}/posts/{1}/replies/{2}/videos/{3}".format(instance.user.id, instance.post.id, instance.reply.id, filename)


class Post(models.Model):

    EV = "Everybody"
    FO = "Followers"
    FR = "Friends"
    AUDIENCE = [
        (EV, "Everybody"),
        (FO, "Followers"),
        (FR, "Friends"),
    ]
    category = models.ForeignKey(Category, on_delete=models.SET_DEFAULT, default=1)
    body = models.TextField("content", blank=True, null=True, max_length=5000)
    slug = AutoSlugField(populate_from=["category", "created_at"])
    video = models.FileField(upload_to=video_directory_path, null=True, blank=True)
    can_view = models.CharField(max_length=10, choices=AUDIENCE, default=EV)
    can_comment = models.CharField(max_length=10, choices=AUDIENCE, default=EV)
    user = models.ForeignKey(
        User, on_delete=models.CASCADE, verbose_name="user", related_name="user"
    )
    published = models.BooleanField(default=False)

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = "post"
        verbose_name_plural = "posts"
        db_table = "posts"
        ordering = ["created_at"]

    def __str__(self):
        return self.body[0:30]

    def get_absolute_url(self):
        return self.slug


class PostImage(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="images")
    image = models.FileField(
        upload_to=post_directory_path, default="posts/default.png", null=True
    )

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "post_images"
        ordering = ["post"]

    def __str__(self):
        return self.post

    def image_tag(self):
        return mark_safe(
            '<img src="/storage/%s" width="50" height="50" />' % (self.image)
        )

    image_tag.short_description = "Image"


class Reply(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    video = models.FileField(
        upload_to=reply_videos_directory_path, null=True, blank=True
    )
    body = models.TextField(max_length=256, default=None)
    parent = models.ForeignKey(
        "self", on_delete=models.CASCADE, null=True, blank=True, related_name="replies"
    )

    created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created at")
    updated_at = models.DateTimeField(auto_now=True, verbose_name="Updated at")

    class Meta:
        verbose_name = "post reply"
        verbose_name_plural = "post replies"
        db_table = "post_reply"

    def __str__(self):
        return self.reply[0:30]

I have the following serializers to deal with the posts and the replies:

class PostImageSerializer(serializers.ModelSerializer):
    class Meta:
        model = PostImage
        fields = ["id", "image", "post"]
        extra_kwargs = {
            "post": {"required": True},
        }

class PostSerializer(serializers.ModelSerializer):
    images = PostImageSerializer(many=True, read_only=True, required=False)
    profile = serializers.SerializerMethodField()

    class Meta:
        model = Post
        fields = [
            "id",
            "can_view",
            "can_comment",
            "category",
            "body",
            "images",
            "video",
            "profile",
            "published",
            "created_at",
            "updated_at",
        ]
        depth = 1

    def get_profile(self, obj):
        profile_obj = Profile.objects.get(id=obj.user.profile.id)
        profile = ShortProfileSerializer(profile_obj)
        return profile.data

    def create(self, validated_data):
        user = User.objects.get(id=self.context['request'].data.get('user'))
        category = Category.objects.get(id=self.context['request'].data.get('category'))
        new_post = Post.objects.create(**validated_data, category=category, user=user)
        images = dict((self.context['request'].FILES).lists()).get('images', None)
        if images:
            for image in images:
                PostImage.objects.create(
                    image=image, post=new_post
                )
        return new_post

    def update(self, id):
        breakpoint()
        post = Post.object.all().filter(id=id)


class ReplyImageSerializer(serializers.ModelSerializer):
    class Meta:
        model = ReplyImage
        fields = ["id", "image", "reply"]
        extra_kwargs = {
            "reply": {"required": True},
        }


class ReplySerializer(serializers.ModelSerializer):
    images = ReplyImageSerializer(many=True, read_only=True, required=False)
    profile = serializers.SerializerMethodField()
    post_images = serializers.SerializerMethodField(many=True)

    class Meta:
        model = Reply
        fields = [
            "id",
            "post",
            "post_images",
            "video",
            "images",
            "body",
            "parent",
            "profile",
            "created_at",
            "updated_at",
        ]
        depth = 1

    def get_profile(self, obj):
        profile_obj = Profile.objects.get(id=obj.user.profile.id)
        profile = ShortProfileSerializer(profile_obj)
        return profile.data

    def get_post_images(self, obj):
        post_obj = PostImage.objects.get(post=obj.post.id)
        post_images = ShortProfileSerializer(post_obj)
        return post_images.data        
    
    def create(self, validated_data):
        new_reply = Reply.objects.create(**validated_data)
        images = dict((self.context['request'].FILES).lists()).get('images', None)
        if images:
            for image in images:
                ReplyImage.objects.create(
                    image=image, reply=new_reply
                )
        return new_reply

I use the following view to see a specific reply to a post but the related images are missing and I would like to get the related images into the endpoint. I would also like to get the category name and display that instead of the id, but images for now are more important.

I have added a serializer method field to the reply serializer to get the post images into the end point and it gets the images but it gives me an error:

get() returned more than one PostImage -- it returned 4!

Is there something I can do to get multiple objects from a serializer method field, to include in the endpoint or should I use a different approach?

Also how can I get the post category name instead of the ID?

Thank you

Asked By: crawlingdev

||

Answers:

Use filter() method instead of get(), and pass the resulting QuerySet object to the serializer class with many=True

class ReplySerializer(serializers.ModelSerializer):
    ...
    post_images = serializers.SerializerMethodField()

    class Meta:
        model = Reply
        fields = [
            "...",
            "post_images",
        ]
        depth = 1

    def get_post_images(self, obj):
        images = PostImage.objects.filter(post=obj.post.id)
        serializer = PostImageSerializer(images, many=True)
        return serializer.data
Answered By: JPG