mirror of
https://github.com/HackSoftware/Django-Styleguide.git
synced 2024-11-26 03:23:45 +03:00
Merge pull request #64 from HackSoftware/apis/improve-list-apis-example
APIs: Improve examples for list APIs
This commit is contained in:
commit
e16f3a0fc8
192
README.md
192
README.md
|
@ -26,6 +26,8 @@ If you want to check an existing project showing most of the styleguide, [check
|
||||||
- [APIs & Serializers](#apis--serializers)
|
- [APIs & Serializers](#apis--serializers)
|
||||||
* [Naming convention](#naming-convention-2)
|
* [Naming convention](#naming-convention-2)
|
||||||
* [An example list API](#an-example-list-api)
|
* [An example list API](#an-example-list-api)
|
||||||
|
+ [Plain](#plain)
|
||||||
|
+ [Filters + Pagination](#filters--pagination)
|
||||||
* [An example detail API](#an-example-detail-api)
|
* [An example detail API](#an-example-detail-api)
|
||||||
* [An example create API](#an-example-create-api)
|
* [An example create API](#an-example-create-api)
|
||||||
* [An example update API](#an-example-update-api)
|
* [An example update API](#an-example-update-api)
|
||||||
|
@ -382,21 +384,199 @@ Here are few examples: `UserCreateApi`, `UserSendResetPasswordApi`, `UserDeactiv
|
||||||
|
|
||||||
### An example list API
|
### An example list API
|
||||||
|
|
||||||
|
#### Plain
|
||||||
|
|
||||||
|
A dead-simple list API would look like that:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class CourseListApi(SomeAuthenticationMixin, APIView):
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from styleguide_example.users.selectors import user_list
|
||||||
|
from styleguide_example.users.models import BaseUser
|
||||||
|
|
||||||
|
|
||||||
|
class UserListApi(APIView):
|
||||||
class OutputSerializer(serializers.ModelSerializer):
|
class OutputSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Course
|
model = BaseUser
|
||||||
fields = ('id', 'name', 'start_date', 'end_date')
|
fields = (
|
||||||
|
'id',
|
||||||
|
'email'
|
||||||
|
)
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
courses = get_courses()
|
users = user_list()
|
||||||
|
|
||||||
serializer = self.OutputSerializer(courses, many=True)
|
data = self.OutputSerializer(users, many=True).data
|
||||||
|
|
||||||
return Response(serializer.data)
|
return Response(data)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Keep in mind this API is public by default. Authentication is up to you.
|
||||||
|
|
||||||
|
#### Filters + Pagination
|
||||||
|
|
||||||
|
On first glance, this is tricky, since our APIs are inheriting the plain `APIView` from DRF, while filtering and pagination is baked into the generic ones:
|
||||||
|
|
||||||
|
1. [DRF Filtering](https://www.django-rest-framework.org/api-guide/filtering/)
|
||||||
|
1. [DRF Pagination](https://www.django-rest-framework.org/api-guide/pagination/)
|
||||||
|
|
||||||
|
That's why, we take the following approach:
|
||||||
|
|
||||||
|
1. Selectors take care of the actual filtering.
|
||||||
|
1. APIs take care of filter parameter serialization.
|
||||||
|
1. APIs take care of pagination.
|
||||||
|
|
||||||
|
Lets see an example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from styleguide_example.api.mixins import ApiErrorsMixin
|
||||||
|
from styleguide_example.api.pagination import get_paginated_response, LimitOffsetPagination
|
||||||
|
|
||||||
|
from styleguide_example.users.selectors import user_list
|
||||||
|
from styleguide_example.users.models import BaseUser
|
||||||
|
|
||||||
|
|
||||||
|
class UserListApi(ApiErrorsMixin, APIView):
|
||||||
|
class Pagination(LimitOffsetPagination):
|
||||||
|
default_limit = 1
|
||||||
|
|
||||||
|
class FilterSerializer(serializers.Serializer):
|
||||||
|
id = serializers.IntegerField(required=False)
|
||||||
|
# Important: If we use BooleanField, it will default to False
|
||||||
|
is_admin = serializers.NullBooleanField(required=False)
|
||||||
|
email = serializers.EmailField(required=False)
|
||||||
|
|
||||||
|
class OutputSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = BaseUser
|
||||||
|
fields = (
|
||||||
|
'id',
|
||||||
|
'email',
|
||||||
|
'is_admin'
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
# Make sure the filters are valid, if passed
|
||||||
|
filters_serializer = self.FilterSerializer(data=request.query_params)
|
||||||
|
filters_serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
users = user_list(filters=filters_serializer.validated_data)
|
||||||
|
|
||||||
|
return get_paginated_response(
|
||||||
|
pagination_class=self.Pagination,
|
||||||
|
serializer_class=self.OutputSerializer,
|
||||||
|
queryset=users,
|
||||||
|
request=request,
|
||||||
|
view=self
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
When we look at the API, we can idenfity few things:
|
||||||
|
|
||||||
|
1. There's a `FilterSerializer`, which will take care of the query parameters. If we don't do this here, we'll have to do it elsewhere & DRF serializers are great at this job.
|
||||||
|
1. We pass the filters to the `user_list` selector
|
||||||
|
1. We use the `get_paginated_response` utility, to return a .. paginated response.
|
||||||
|
|
||||||
|
Now, lets look at the selector:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import django_filters
|
||||||
|
|
||||||
|
from styleguide_example.users.models import BaseUser
|
||||||
|
|
||||||
|
|
||||||
|
class BaseUserFilter(django_filters.FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = BaseUser
|
||||||
|
fields = ('id', 'email', 'is_admin')
|
||||||
|
|
||||||
|
|
||||||
|
def user_list(*, filters=None):
|
||||||
|
filters = filters or {}
|
||||||
|
|
||||||
|
qs = BaseUser.objects.all()
|
||||||
|
|
||||||
|
return BaseUserFilter(filters, qs).qs
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, we are leveraging the powerful [`django-filter`](https://django-filter.readthedocs.io/en/stable/) library.
|
||||||
|
|
||||||
|
But you can do whatever suits you best here. We have projects, where we implemented our own filtering layer & used it here.
|
||||||
|
|
||||||
|
The key thing is - **selectors take care of filtering**.
|
||||||
|
|
||||||
|
Finally, lets look at `get_paginated_response`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
|
||||||
|
def get_paginated_response(*, pagination_class, serializer_class, queryset, request, view):
|
||||||
|
paginator = pagination_class()
|
||||||
|
|
||||||
|
page = paginator.paginate_queryset(queryset, request, view=view)
|
||||||
|
|
||||||
|
if page is not None:
|
||||||
|
serializer = serializer_class(page, many=True)
|
||||||
|
return paginator.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
serializer = serializer_class(queryset, many=True)
|
||||||
|
|
||||||
|
return Response(data=serializer.data)
|
||||||
|
```
|
||||||
|
|
||||||
|
This is basically a code, extracted from within DRF.
|
||||||
|
|
||||||
|
Same goes for the `LimitOffsetPagination:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from rest_framework.pagination import LimitOffsetPagination as _LimitOffsetPagination
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
|
||||||
|
class LimitOffsetPagination(_LimitOffsetPagination):
|
||||||
|
default_limit = 10
|
||||||
|
max_limit = 50
|
||||||
|
|
||||||
|
def get_paginated_data(self, data):
|
||||||
|
return OrderedDict([
|
||||||
|
('limit', self.limit),
|
||||||
|
('offset', self.offset),
|
||||||
|
('count', self.count),
|
||||||
|
('next', self.get_next_link()),
|
||||||
|
('previous', self.get_previous_link()),
|
||||||
|
('results', data)
|
||||||
|
])
|
||||||
|
|
||||||
|
def get_paginated_response(self, data):
|
||||||
|
"""
|
||||||
|
We redefine this method in order to return `limit` and `offset`.
|
||||||
|
This is used by the frontend to construct the pagination itself.
|
||||||
|
"""
|
||||||
|
return Response(OrderedDict([
|
||||||
|
('limit', self.limit),
|
||||||
|
('offset', self.offset),
|
||||||
|
('count', self.count),
|
||||||
|
('next', self.get_next_link()),
|
||||||
|
('previous', self.get_previous_link()),
|
||||||
|
('results', data)
|
||||||
|
]))
|
||||||
|
```
|
||||||
|
|
||||||
|
What we basically did is reverse-engineered the generic APIs, since pagination should be able to live outside the layers of complexity there.
|
||||||
|
|
||||||
|
**A possible future implementation should be able to paginate without needing the request / response of the APIView.**
|
||||||
|
|
||||||
|
You can find the code for the example list API with filters & pagination in the [Styleguide Example](https://github.com/HackSoftware/Styleguide-Example#example-list-api) project.
|
||||||
|
|
||||||
### An example detail API
|
### An example detail API
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|
Loading…
Reference in New Issue
Block a user