Tom Manterfield
Tom Manterfield

Reputation: 7073

Adding extra data to Django Rest Framework results for entire result set

I'm using Django Rest Framework and need to add extra data to a result set. Specifically, where you would usually have:

{
    "count": 45, 
    "next": "http://localhost:8000/foo/bar?page=2", 
    "previous": null, 
    "results": [
        {...}
    ]
}

I would like to add extra counts like so:

{
    "count": 45,
    "10_mi_count": 10,
    "20_mi_count": 30,
    "30_mi_count": 45,
    "next": "http://localhost:8000/foo/bar?page=2", 
    "previous": null, 
    "results": [
        {...}
    ]
}

The extra counts in this example are just how many of the objects have a field distance with a value less than the miles described in the key.

My issue is that I have no idea where the best place to extend and insert this behaviour is.

Ideally I'd like this to work whether or not the results are paginated, making no assumptions.

What I'm really after here is a nod in the right direction (and why that is the right place for it go).

I've checked the docs and can't find anything that describes how to add things like this, but I would be more than happy to be proven wrong on that score.

Upvotes: 44

Views: 20963

Answers (6)

Vit Amin
Vit Amin

Reputation: 683

if you want to use filtered queryset (for example to retrieve values for UI filter), you can override list method like this:

def list(self, request, *args, **kwargs):
    queryset = self.filter_queryset(self.get_queryset())

    # TODO you can move is in a separate method
    featute1_filter_values = queryset.annotate(
        feature1_id=F('feature1_id'), # to filter from UI by id
        feature1_name=F('feature1__name'), # to show filter labels
    ).values(
        'feature1_id', 
        'feature1_name',
    ).distinct().order_by('form_name') #you must use order_by to make 'distinct' work

    page = self.paginate_queryset(queryset)
    serializer = self.get_serializer(page, many=True)
    
    if page is not None:
        response = self.get_paginated_response(serializer.data)
    else:
        response = Response(serializer.data)

    response.data['filters'] = {}
    response.data['filters']['feature1'] = list(featute1_filter_values)

    return response

Upvotes: 0

John Chinte
John Chinte

Reputation: 31

I'm piggybacking off the accepted answer here!

I think the main issue is that the serializer returns a ReturnList, which is just a regular list with some extra context.

You can just replace the data with your own OrderedDict like so:

response = super().list(request, *args, **kwargs)
new_data = OrderedDict()
new_data.update({
    'results': response.data,
    '10-mi-count': 10,
    #etc...
})
response.data = new_data

return response 

Upvotes: 3

Mitalee Rao
Mitalee Rao

Reputation: 191

I used DRF with pagination as well and wanted to add a summary of the returned queryset itself, so first I had to get the queryset in the list method.

def list(self, request, *args, **kwargs):
    queryset = self.filter_queryset(self.get_queryset())
    summary = {}
    summary['x'] = queryset.filter(transaction_type='x').count()
    summary['y'] = queryset.filter(transaction_type='y').count()

    page = self.paginate_queryset(queryset)
    if page is not None:
        serializer = self.get_serializer(page, many=True)
        data = {}
        data['summary'] = summary
        data['details'] = serializer.data
        return self.get_paginated_response(data)


    serializer = self.get_serializer(queryset, many=True)
    data = {}
    data['summary'] = summary
    data['details'] = serializer.data
    return Response(data)

This gave me the following response:

{
  "count": 20,
  "next": "http://..",
  "previous": null,
  "results": {
    "summary": {
      "x": 15,
      "y": 5,
    },
    "details": [
      {.....

Note: this will work for non-paginated results as well.

Upvotes: 2

gaizka
gaizka

Reputation: 721

Since you seem to be using one of the ListViews from the Rest Framework, you could override the list() method in your class and set new values on the resulting data, like this:

    def list(self, request, *args, **kwargs):
        response = super().list(request, args, kwargs)
        # Add data to response.data Example for your object:
        response.data['10_mi_count'] = 10 # Or wherever you get this values from
        response.data['20_mi_count'] = 30
        response.data['30_mi_count'] = 45
        return response

Notice that your class must inherit the ListModelMixin directly or via a GenericView from the Rest Framework API (http://www.django-rest-framework.org/api-guide/generic-views#listmodelmixin). I really don't know if it is the right way to do this, but it is a quick fix.

Hope it helps!

Upvotes: 52

user42488
user42488

Reputation: 1455

Use SerializerMethodField as mentioned in this solution.

It can be used to add any sort of data to the serialized representation of your object. (REST framework doc)

Example from the documentation:

from django.contrib.auth.models import User
from django.utils.timezone import now
from rest_framework import serializers

class UserSerializer(serializers.ModelSerializer):
    days_since_joined = serializers.SerializerMethodField()

    class Meta:
        model = User

    def get_days_since_joined(self, obj):
        return (now() - obj.date_joined).days

Upvotes: 12

Tom Manterfield
Tom Manterfield

Reputation: 7073

In the end I just created a custom pagination serializer with a field like so:

class DistanceCountField(serializers.Field):
    def to_native(self, value):
        try:
            distance_counts = {
                '1_mile': self._count_lte_miles(value, 1),
                '5_mile': self._count_lte_miles(value, 5),
                '10_mile': self._count_lte_miles(value, 10),
                '20_mile': self._count_lte_miles(value, 20),
            }
        except FieldError:
            distance_counts = None

        return distance_counts

    def _count_lte_miles(self, value, miles):
        meters = miles * 1609.344
        return value.filter(distance__lte=meters).count()


class PaginatedCountSerializer(pagination.PaginationSerializer):
    distance_counts = DistanceCountField(source='paginator.object_list')

    class Meta:
        # Class omitted, just a standard model serializer
        object_serializer_class = MyModelSerializer 

I also added a distance annotation to each object in the queryset for the filtering to work on.

Upvotes: 5

Related Questions