Home > other >  How to implement custom django filter for aggregated data from related model
How to implement custom django filter for aggregated data from related model

Time:11-08

I've built a simple API using the Django Rest Framework

Two Models: Person and Hike (person is FK)

I have these PIP packages installed:

package version
Django 3.2.7
django-filter 21.1
django-mathfilters 1.0.0
djangorestframework 3.12.4
djangorestframework-api-key 2.1.0

models.py

...
class Person(models.Model):
    first_name          = models.CharField(max_length=100)
    last_name           = models.CharField(max_length=100)

class Hike(models.Model):
    hiker               = models.ForeignKey(Person, on_delete=models.CASCADE, related_name='hikes')
    hike_date           = models.DateField(max_length=100, blank=False)
    distance_mi         = models.FloatField(blank=False)

views.py

...
class PersonViewSet(viewsets.ModelViewSet):
    queryset = Person.objects.all() 
    serializer_class = PersonSerializer

serializers.py

class PersonSerializer(serializers.ModelSerializer):
    hikes = serializers.PrimaryKeyRelatedField(many=True, read_only=True)

    all_hikes = Hike.objects.all()
    def total_mi(self, obj):
        result = self.all_hikes.filter(hiker__id=obj.id).aggregate(Sum('distance_mi'))
        return round(result['distance_mi__sum'], 2)

...
total_miles = serializers.SerializerMethodField('total_mi')

...
class Meta:
    model = Person
    fields = ('id','first_name','last_name','hikes','total_miles')

filters.py

class HikerFilter(django_filters.FilterSet):
    hiker = django_filters.ModelChoiceFilter(field_name="hiker",
                                             queryset=Person.objects.all())
    class Meta:
        model = Hike
        fields = {
            'hiker': ['exact'],
            'hike_date': ['gte', 'lte', 'exact', 'gt', 'lt'],
            'distance_mi': ['gte', 'lte', 'exact', 'gt', 'lt'],
        }

sample data: hikes

id hike_date distance_mi
2 2020-11-02 4.5
3 2021-03-16 3.3
5 2021-08-11 5.3
7 2021-10-29 4.3

The Person view includes "total_miles" stat added via the Serializer (total_mi).

Person endpoint http://localhost:8000/persons/2/

    {
        "id": 2,
        "first_name": "Miles",
        "last_name": "Marmot",
        "hikes": [
            2,
            3,
            5,
            7
        ],
        "total_miles": 17.4,
    },

Currently, the "total_miles" is for all years.

My QUESTION: how can I filter "total_miles" (float) and "hikes" (list) in the Person view by a specific year by passing a URL argument?

e.g. http://localhost:8000/persons/2/?year=2020 > "total_miles": 4.5,

e.g. http://localhost:8000/persons/2/?year=2021 > "total_miles": 12.9,

-- I was able to limit "total_miles" by year in Serializer.py with all_hikes = Hike.objects.filter(hike_date__year='2020') but the year is hard-coded for testing only.

Can I pass an argument/var to the Serializer function?

Or can this be implemented with a custom filter?

OPTIONAL/BONUS:

Can the "hikes" (list of ids) in the Person view be filtered by Year as well?

e.g. http://localhost:8000/persons/2/?year=2020 > "total_miles": 4.5, "hikes": [2]

e.g. http://localhost:8000/persons/2/?year=2021 > "total_miles": 12.9, "hikes": [3, 5, 7]

Thanks in advance! Best~

CodePudding user response:

the request is already passed to the serializer's __init__ and stored in the context attribute

So in the serializer you could do something like:

year = self.context["request"].GET.get("year") 
if year:
    all_hikes = Hike.objects.filter(hike_date__year=year)

c.f.

CodePudding user response:

Here is how I implemented @pleasedontbelong's answer.

  1. For hiker > total_miles - if year is passed, then total is calculated for that year only. if not, total is calculated for all years.

serializers.py

...
    def total_mi(self, obj):
        all_hikes = Hike.objects.all()
        year = self.context["request"].GET.get("year")
        if year:
            all_hikes = all_hikes.filter(hike_date__year=year)
        result = all_hikes.filter(hiker__id=obj.id).aggregate(Sum('distance_mi'))
        return round(result['distance_mi__sum'], 2)
  1. For hiker > hikes[] - if year is passed, then list includes hike ids for that year only. if not, hike ids for all years are included.

I replaced hikes = serializers.PrimaryKeyRelatedField(many=True, read_only=True)

with the following get_user_hikes method in my PersonSerializer:

serializers.py

...
    def get_user_hikes(self, obj):
        all_hikes = Hike.objects.filter(hiker__id=obj.id)
        year = self.context["request"].GET.get("year")
        if year:
            all_hikes = all_hikes.filter(hike_date__year=year)
        result = all_hikes.values_list('pk', flat=True)
        return list(result)
...
hikes = serializers.SerializerMethodField('get_user_hikes')
  1. In order to reuse year=2020 to filter Hikes, I added year to the existing HikerFilter.

filters.py

...
year = django_filters.NumberFilter(field_name='hike_date', lookup_expr='year')

Success!

http://localhost:8000/hikes/?hiker=2&year=2020

[
    {
        "id": 2,
        "hike_date": "2020-11-02",
        "location": "Frog Lake",
        "state": "OR",
        "distance_mi": 4.5,
        "elevation_gain_ft": 1275,
        "highest_elev_ft": 6739,
        "alltrails_url": null,
        "blogger_url": null,
        "hiker": {
            "id": 2,
            "first_name": "Miles",
            "last_name": "Marmot",
            "slug": "miles-marmot",
            "join_date": "2020-10-11T11:45:02-07:00",
            "email": "[email protected]",
            "profile_img": "http://localhost:8000/static/images/2021/11/01/hat_miles_2020-1.jpeg",
            "hikes": [
                2
            ],
            "total_hikes": 1,
            "total_miles": 4.5,
            "total_elev_feet": 1275,
            "highest_elev_feet": 6739
        }
    }
]
  • Related