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.
- https://www.django-rest-framework.org/api-guide/serializers/#including-extra-context
- https://www.django-rest-framework.org/api-guide/generic-views/
CodePudding user response:
Here is how I implemented @pleasedontbelong's answer.
- 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)
- 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')
- In order to reuse
year=2020
to filter Hikes, I addedyear
to the existingHikerFilter
.
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
}
}
]