3

I would like to do the something similar to this example How to group an array of objects by key but I couldn't find a solution for API Get in Django.

Here's an example of what I have:

[
    {
        "id": 1,
        "nutrition_measurement_id": "1",
        "nutrition_type_id": "2",
        "feeding_time": "dinner"
    },
    {
        "id": 2,
        "nutrition_measurement_id": "2",
        "nutrition_type_id": "1",
        "feeding_time": "dinner"
    },
    {
        "id": 3,
        "nutrition_measurement_id": "3",
        "nutrition_type_id": "2",
        "feeding_time": "breakfast"
    },
    {
        "id": 4,
        "nutrition_measurement_id": "2",
        "nutrition_type_id": "1",
        "feeding_time": "breakfast"
    }
]
Here's an example of what i want to achieve:
{
 "dinner": [
 {
   "id": 3,
   "nutrition_type_id": 2,
   "nutrition_measurement_id": 1
 },
{
   "id": 3,
   "nutrition_type_id": 1,
   "nutrition_measurement_id": 2
 }
 ],

"breakfast": [
 {
   "id": 3,
   "nutrition_type_id": 2,
   "nutrition_measurement_id": 3
 },
{
    "id": 5,
    "nutrition_type_id": 1,
    "nutrition_measurement_id": 4
 }
]

}

Here are the models I have:

class HorseNutrition(models.Model):
    horse = models.ForeignKey(Horse, models.DO_NOTHING)
    nutrition_type = models.ForeignKey('NutritionType', models.DO_NOTHING)
    nutrition_measurement = models.ForeignKey('NutritionMeasurement', models.DO_NOTHING)
    feeding_time = models.CharField(max_length=10, blank=True, null=True)
    class Meta:
        managed = False
        db_table = 'horse_nutrition'

class NutritionMeasurement(models.Model):
    name = models.CharField(unique=True, max_length=30, blank=True, null=True)
    class Meta:
        managed = False
        db_table = 'nutrition_measurement'

    def __str__(self):
        return self.name
    def __unicode__(self):
        return self.name


class NutritionType(models.Model):
    name = models.CharField(unique=True, max_length=30, blank=True, null=True)
    class Meta:
        managed = False
        db_table = 'nutrition_type'

    def __str__(self):
        return self.name
    def __unicode__(self):
        return self.name

Here's my serializer

class HorseNutritionSerializer(serializers.ModelSerializer):

    nutrition_measurement_id = serializers.ReadOnlyField(source = 'nutrition_measurement.name')
    nutrition_type_id = serializers.ReadOnlyField(source = 'nutrition_type.name')
    
    class Meta: 
        model = HorseNutrition
        fields = ['id', 'nutrition_measurement_id', 'nutrition_type_id', 'feeding_time']

Here's my view:

class HorseNutritionView(APIView):
    
    def get (self, request, format = None):
        #id = int(request.GET.get(self.lookup_url_kwarg))
        id = 1
        if id != None:
            queryset = HorseNutrition.objects.filter(horse_id = id)
            serializer = HorseNutritionSerializer(queryset, many = True)             
            return Response(serializer.data)
        else:
            return Response(status = status.HTTP_400_BAD_REQUEST)
olczig
  • 65
  • 6

1 Answers1

2

You can use a custom ListSerializer (which we will set list_serializer_class to in the Meta of your serializer) and override its to_representation and use itertools.groupby to group your results:

from itertools import groupby

class HorseNutritionListSerializer(serializers.ListSerializer):
    def to_representation(self, data):
        ret = super().to_representation(data)
        return {key: list(group) for key, group in groupby(ret, key=lambda x: x['feeding_time'])}
    
    @property
    def data(self):
        ret = serializers.BaseSerializer.data.fget(self)
        return serializers.ReturnDict(ret, serializer=self)


class HorseNutritionSerializer(serializers.ModelSerializer):
    nutrition_measurement_id = serializers.ReadOnlyField(source = 'nutrition_measurement.name')
    nutrition_type_id = serializers.ReadOnlyField(source = 'nutrition_type.name')
    
    class Meta: 
        model = HorseNutrition
        fields = ['id', 'nutrition_measurement_id', 'nutrition_type_id', 'feeding_time']
        list_serializer_class = HorseNutritionListSerializer

Also for this grouping to be done properly your queryset must be ordered, which you can do in the view:

class HorseNutritionView(APIView):
    
    def get (self, request, format = None):
        #id = int(request.GET.get(self.lookup_url_kwarg))
        id = 1
        if id != None:
            queryset = HorseNutrition.objects.filter(horse_id = id).order_by('feeding_time')
            serializer = HorseNutritionSerializer(queryset, many = True)             
            return Response(serializer.data)
        else:
            return Response(status = status.HTTP_400_BAD_REQUEST)
Abdul Aziz Barkat
  • 19,475
  • 3
  • 20
  • 33
  • It makes a lot of sense but I don't know why it doesn't work :( – olczig May 20 '21 at 16:25
  • @olczig ah, my bad, querysets (when `many=True`) are internally serialized by a parent serializer `ListSerializer` hence we need to override it's `to_representation` instead, for which we will have to use a custom `ListSerializer` and set it as `list_serializer_class`. Check my edit. – Abdul Aziz Barkat May 20 '21 at 16:32
  • Now I only have a list of feeding times – olczig May 20 '21 at 16:37
  • @olczig a list of feeding times? You should be getting a _dictionary_ with feeding times as the keys and lists of serialized `HorseNutrition` as values as that is what we are returning. – Abdul Aziz Barkat May 20 '21 at 16:39
  • The only thing I get is [ "breakfast", "dinner" ] – olczig May 20 '21 at 16:41
  • 1
    @olczig I have learnt much from answering this question :), the problem is `ListSerializer.data` returns `ReturnList(ret, serializer=self)` so the dictionary turns into a list. I have edited the answer to solve this problem too (perhaps would have been easier to just use `itertools.groupby` in the view, but you learn new things this way ;)) – Abdul Aziz Barkat May 20 '21 at 16:52
  • I'm soooooo grateful for your answers!!!!! It works <3 Thank you so much :) – olczig May 20 '21 at 16:57