Как реализовать пользовательский фильтр 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.
- 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)
- 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')
- Чтобы повторно использовать
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
}
}
]