Search code examples
djangorestserializationdjango-rest-frameworkgeneric-relations

How to write a django-rest-framework serializer / field to merge data from generic relations?


I have objects with a generic relation pointing to various other objects, and I need them to be merged (inlined) so the serialized objects look like one whole objects.

E.G:

class Enrollement(models.Model):
    hq = models.ForeignKey(Hq)
    enrollement_date = models.Datetime()
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    object = generic.GenericForeignKey('content_type', 'object_id')

class Nurse(models.Model):
     hospital = models.ForeignKey(Hospital)
     enrollement = GenericRelation(Enrollement)

class Pilot(models.Model):
     plane = models.ForeignKey(plane)
     enrollement = GenericRelation(Enrollement)

When serialized, I'd like to get something like this:

{
    count: 50,
    next: 'http...',
    previous: null,
    results: [
        {
        type: "nurse",
        hq: 'http://url/to/hq-detail/view',
        enrollement_date: '2003-01-01 01:01:01',
        hospital: 'http://url/to/hospital-detail/view'

        },
        {
        type: "pilot",
        hq: 'http://url/to/hq-detail/view',
        enrollement_date: '2003-01-01 01:01:01',
        plante: 'http://url/to/plane-detail/view'

        },
    ]
}

Can I do it, and if yes, how ?

I can nest a generic relation, and I could post process the serilizer.data to obtain what I want, but I'm wondering if there is a better way.


Solution

  • DEAR FRIENDS FROM THE FUTURE: At time of writing, the Django REST Framework team seems to be working on adding more mature support for generic relations. But it is not yet finished. Before copy-pasting this answer into your code base, check https://github.com/tomchristie/django-rest-framework/pull/755 first to see if it's been merged into the repo. There may be a more elegant solution awaiting you. — Your ancient ancestor Tyler

    Given you're using Django REST Framework, if you did want to do some post-processing (even though you seem hesitant to) you can accomplish something your goal by overriding get_queryset or list in your view. Something like this:

    views.py:

    from rest_framework.generics import ListAPIView
    from rest_framework.response import Response
    from models import *
    from itertools import chain
    
    class ResultsList(ListAPIView):
        def list(self, request, *args, **kwargs):
            nurses = Nurse.objects.all()
            pilots = Pilot.objects.all()
    
            results = list()
            entries = list(chain(nurses, pilots)) # combine the two querysets
            for entry in entries:
                type = entry.__class__.__name__.lower() # 'nurse', 'pilot'
                if isinstance(entry, Nurse):
                    serializer = NurseSerializer(entry)
                    hospital = serializer.data['hospital']
                    enrollement_date = serializer.data['enrollement.date']
                    hq = serializer.data['enrollement.hq']
                    dictionary = {'type': type, 'hospital': hospital, 'hq': hq, 'enrollement_date': enrollement_date}
                if isinstance(entry, Pilot):
                    serializer = PilotSerializer(entry)
                    plane = serializer.data['plane']
                    enrollement_date = serializer.data['enrollement.date']
                    hq = serializer.data['enrollement.hq']
                    dictionary = {'type': type, 'plane': plane, 'hq': hq, 'enrollement_date': enrollement_date}
                results.append(dictionary)
            return Response(results)
    

    serializers.py

    class EnrollementSerializer(serializer.ModelSerializer):
        class Meta:
            model = Enrollement
            fields = ('hq', 'enrollement_date')
    
    class NurseSerializer(serializer.ModelSerializer):
        enrollement = EnrollementSerializer(source='enrollement.get')
    
        class Meta:
            model = Nurse
            fields = ('hospital', 'enrollement')
    
    class PilotSerializer(serializer.ModelSerializer):
        enrollement = EnrollementSerializer(source='enrollement.get')
    
        class Meta:
            model = Pilot
            fields = ('plane', 'enrollement')
    

    Returned response would look like:

      [
            {
                  type: "nurse",
                  hq: "http://url/to/hq-detail/view",
                  enrollement_date: "2003-01-01 01:01:01",
                  hospital: "http://url/to/hospital-detail/view"
            },
            {
                  type: "pilot",
                  hq: "http://url/to/hq-detail/view",
                  enrollement_date: "2003-01-01 01:01:01",
                  plane: "http://url/to/plane-detail/view"
            },
      ]
    

    Noteworthy:

    • My serializers.py may be a bit off here because my memory of how to represent generic relations in serializers is a bit foggy. YMMV.
    • Similarly to ^^ this assumes your serializers.py is in order and has properly set up its generic relationships in line with your models.
    • We do the get in source=enrollement.get because otherwise a GenericRelatedObjectManager object will be returned if we don't specify a source. That's because that's what a generic relation represents. Using .get forces a query (as in QuerySet query) which accesses the model you set as the source of the generic relation (in this case, class Enrollement(models.Model).
    • We have to use list(chain()) instead of the | operator because the querysets come from different models. That's why we can't do entries = nurses | pilots.
    • for entry in entries can surely be made more dry. GLHF.