How do I use GenericRelation with Django Rest Framework?

3.3k views Asked by At

I want to include a model with a GenericRelation backrefrence in DRF

The docs indicate this should be easy ( just above: http://www.django-rest-framework.org/api-guide/relations/#manytomanyfields-with-a-through-model ) - but I am missing something!

Note that reverse generic keys, expressed using the GenericRelation field, can be serialized using the regular relational field types, since the type of the target in the relationship is always known.

For more information see the Django documentation on generic relations.

my models:

class Voteable(models.Model):
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    direct_vote_count = models.IntegerField(default=0)

class Question(models.Model):
    user = models.ForeignKey(UserExtra, related_name='questions_asked')
    voteable = GenericRelation(Voteable)
    question = models.CharField(max_length=200)

and my serializers:

class VoteableSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Voteable
        fields = ('pk', 'id', 'url', 'direct_vote_count')


class QuestionSerializer(serializers.HyperlinkedModelSerializer):
    #voteable = VoteableSerializer(read_only=True, many=False)
    #voteable = serializers.PrimaryKeyRelatedField(many=False, read_only=True)

    class Meta:
        depth = 1
        model = Question
        fields = ('url', 'question', 'user', 'voteable')

The two commented out lines are my attempts at telling DRF how to serialize voteable inside Question
The first gives me

'GenericRelatedObjectManager' object has no attribute 'pk'

and the second

<django.contrib.contenttypes.fields.create_generic_related_manager.<locals>.GenericRelatedObjectManager object at 0x7f7f3756cf60> is not JSON serializable

So, clearly I am misunderstanding something, any idea what?

3

There are 3 answers

3
Chozabu On

Well, I have a working solution, though it does not feel like the correct solution....

class VoteableSerializer(serializers.ModelSerializer):
    class Meta:
        model = Voteable
        fields = ('pk', 'direct_vote_count')


class VoteableRelatedField(serializers.RelatedField):
    def to_representation(self, value):
        serializer = VoteableSerializer(value.get_queryset()[0])
        return serializer.data

class QuestionSerializer(serializers.HyperlinkedModelSerializer):
    #voteable = VoteableSerializer(read_only=True, many=False)
    #voteable = serializers.PrimaryKeyRelatedField(many=False, read_only=True)

    voteable = VoteableRelatedField(read_only=True)

    class Meta:
        depth = 1
        model = Question
        fields = ('url', 'question', 'user', 'voteable')
        read_only_fields = ('voteable',)
  • remove url from VoteableSerializer
  • change VoteableSerializer to ModelSerializer from HyperlinkedModelSerializer
  • add VoteableRelatedField and get the first item from the queryset (this in particular feels wrong)

I'll not mark this as accepted yet, in the hope that someone can enlighten me on how it should be done!

0
Chozabu On

Alternative idea for a solution, which seems to fit GenericRelation better...

  • Make Voteable an abstract Model
  • Alter Vote class (not shown in this question) to point at anything using GenericForeignKey.

Pros:
This would mean the vote info is always right on the relevant object, simplifying sorting & querying and avoiding joins.

Cons:
Votes would take a little more space

class Voteable(models.Model):
    votes = GenericRelation(Vote)
    direct_vote_count = models.IntegerField(default=0)

    class Meta:
        abstract = True

class Question(Voteable):
    user = models.ForeignKey(UserExtra, related_name='questions_asked')
    question = models.CharField(max_length=200)

class Vote(models.Model):
    user = models.ForeignKey(UserExtra, related_name='questions_asked')
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

Perhaps more optimal, but less DRY would be to have a seperate "Vote" class for each type of object that inherits Voteable

class Voteable(models.Model):
    direct_vote_count = models.IntegerField(default=0)

    class Meta:
        abstract = True

class Question(Voteable):
    user = models.ForeignKey(UserExtra, related_name='questions_asked')
    question = models.CharField(max_length=200)

class QuestionVote(models.Model):#This class also repeated for each item that can be voted on
    user = models.ForeignKey(UserExtra, related_name='questions_asked')
    parent = models.ForeignKey(Question, related_name='votes')
2
Edouard C. On

A custom related serializer field seems unecessary for the doc has this footnote for GenericRelation:

Note that reverse generic keys, expressed using the GenericRelation field, can be serialized using the regular relational field types, since the type of the target in the relationship is always known.

Application below. Also check this DRF3 extension.

class QuestionSerializer(serializers.HyperlinkedModelSerializer):
    voteable = VoteableSerializer(read_only=True)

    class Meta:
        model = Question
        fields = ('url', 'question', 'user', 'voteable')
        ...