4

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?

Chozabu
  • 1,015
  • 1
  • 10
  • 33
  • What you want to implement is generic 1-to-1 relation. It's not supported out of the box. Take a look at [this question](http://stackoverflow.com/q/7837330/1377864) for possible workaround: `voteable = GenericRelation(Voteable)` becomes `voteables = GenericRelation(Voteable)` + `voteable` property on the model level. With this approach your first option should work fine. – Yaroslav Admin Dec 30 '15 at 01:04
  • 1
    I see - I guess my other option is to use a regular ForeignKey in question (and other classes I want to vote on) pointing at Voteable instead of a GenericRelation... – Chozabu Dec 30 '15 at 01:47

3 Answers3

2

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!

Chozabu
  • 1,015
  • 1
  • 10
  • 33
  • Marking this as Accepted - as it seems to be the correct answer to the question title. Though, my other answer is probably a better solution to the exact situation described. – Chozabu Dec 30 '15 at 11:15
  • 1
    That part, `value.get_queryset()[0]` was exactly what I needed, but I agree that it doesn't feel like the best solution. – Kyle Falconer Apr 14 '16 at 17:02
  • Glad to hear I helped! In my project I actually ended up _not_ using GenericRelations for situations except where they are vital, as many queries are hard to run through that kind of relation. Of course, I have a new set of problems - graphingDBs are looking more and more tempting... – Chozabu Apr 14 '16 at 19:58
2

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')
Chozabu
  • 1,015
  • 1
  • 10
  • 33
1

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')
        ...
Edouard C.
  • 21
  • 2
  • I am no longer working on this project - but looking at your answer.. I quoted the line you quoted - and even tried the solution you suggest! (with many=false) – Chozabu Aug 05 '17 at 09:21
  • Though, the extension you link to may well do what is needed - and it is possible that DRF has fixed an issue making both of our code work - unfortunately I am unable to test – Chozabu Aug 05 '17 at 09:22