Как реализовать пользовательский фильтр django для агрегированных данных из связанной модели

Я создал простой API, используя Django Rest Framework

Две модели: Человек и Поход (человек - FK)

У меня установлены следующие пакеты PIP:

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'))
        try:
            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'],
        }

выборочные данные: походы

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

Представление Person включает статистику "total_miles", добавленную через сериализатор (total_mi).

Конечная точка человека http://localhost:8000/persons/2/

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

В настоящее время "total_miles" рассчитан для всех лет.

МОЙ ВОПРОС: как я могу фильтровать "total_miles" (float) и "hikes" (list) в представлении Person по определенному году, передавая аргумент URL?

например, http://localhost:8000/persons/2/?year=2020 > "total_miles": 4.5,

например, http://localhost:8000/persons/2/?year=2021 > "total_miles": 12.9,

-- Я смог ограничить "total_miles" по году в Serializer.py с помощью all_hikes = Hike.objects.filter(hike_date__year='2020'), но год жестко закодирован только для тестирования.

Могу ли я передать аргумент/var в функцию Serializer?

Или это можно реализовать с помощью пользовательского фильтра?

ОПЦИОНАЛЬНО/БОНУС:

Можно ли фильтровать "походы" (список идентификаторов) в представлении "Человек" также по году?

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]

Заранее спасибо! Best~

запрос уже передан в сериализатор __init__ и сохранен в атрибуте context

Таким образом, в сериализаторе вы можете сделать что-то вроде:

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

c.f.

Вот как я реализовал ответ @pleasedontbelong.

  1. For hiker > total_miles - если year пройден, то total рассчитывается только для этого года. если нет, то total рассчитывается для всех лет.

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[] - если year передан, то список включает hikeids только для этого года. Если нет, то в список включаются походные идентификаторы для всех лет.

Я заменил hikes = serializers.PrimaryKeyRelatedField(many=True, read_only=True)

со следующим get_user_hikes методом в моем 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. Чтобы повторно использовать year=2020 для фильтрации Походов, я добавил year к существующим HikerFilter.

filters.py

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

Успех!

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": "miles.marmot@wta.com",
            "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
        }
    }
]
Вернуться на верх