De0aDjangoREST
Taller para iniciarse en el desarrollo de API REST con Django REST Framework
-
Vídeo Parte 1: https://youtu.be/1eQjkdJI9e4
-
Vídeo Parte 2: https://youtu.be/B7ehNojsjyk
-
Diapositivas Parte 1: https://docs.google.com/presentation/d/1BMlLldDGxuSOZ3WSHzqGhakTpPLD3KzY2ixU25gpeOc/
-
Diapositivas Parte 2: https://docs.google.com/presentation/d/1X_pNijQj0DBbR5lYc-pQlECGSSKEtlm5EM5L_uwOMhw/
-
Colección de peticiones para importar en Postman: https://drive.google.com/file/d/1SALDsUzpykG9QktJKN8mdYGpteBLJWlW/
Autor Miguel Jiménez - www.migueljimenezgarcia.com
Step 1 (Inicialización)
-
Crear Entorno virtual con
virtualenv .en la terminal -
Entrar en la carpeta De0aDjangoREST
-
Activar el virtualenv con
source bin/activateen la terminal -
Crear archivo requirements.pip en esta carpeta con:
Django==3.0.1 djangorestframework==3.10.3 ipdb==0.11 -
Introducir
pip install -r requirements.pipen la terminal
Step 2 (Proyecto)
-
Inicializamos el proyecto con
django-admin startproject De0aDjangoRESTen la terminal -
Entramos en la carpeta del proyecto llamada
De0aDjangoRESTque se acaba de crear y trabajaremos sobre esta todo lo que queda del workshop
Step 3 (App)
- Creamos la app dentro del proyecto con
python manage.py startapp carsen la terminal
Step 4 (Settings y Urls)
-
Abrir
De0aDjangoREST/settings.pyy modificar:INSTALLED_APPS = [ ... 'rest_framework', 'cars', ] -
Seguimos en De0aDjangoREST/settings.py y añadimos al final:
REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' ] } -
Abrir
De0aDjangoREST/urls.py, borrarlo e introducir:from django.urls import path from django.conf.urls import include urlpatterns = [ path('v1/cars/', include('cars.urls')) ] -
Crear
cars/urls.pyy añadir:from django.urls import path urlpatterns = [ ] -
Inicializamos el servidor con
python manage.py runserveren la terminal -
Entramos en el navegador en http://127.0.0.1:8000/ para comprobar que funcione todo hasta ahora
Step 5 (Modelo y base de datos)
-
Abrir cars/models.py e introducimos el primer modelo:
class Brand(models.Model): created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) name = models.CharField(max_length=15) -
Creamos las migraciones para la base de datos con
python manage.py makemigrationsen la terminal -
Corremos las migraciones con
python manage.py migrateen la terminal y así reflejamos los cambios en los modelos en la base de datos.
Step 6 (Petición GET, obtener recursos)
-
Definimos el Serializador en
cars/serializers.pyfrom rest_framework import serializers from .models import Brand class BrandSerializer(serializers.ModelSerializer): class Meta: model = Brand fields = ('id', 'name', ) -
Definimos la Vista en
cars/views.pyfrom rest_framework.generics import ListAPIView from .models import Brand from .serializers import BrandSerializer class BrandListView(ListAPIView): serializer_class = BrandSerializer permission_classes = () queryset = Brand.objects.all() -
Definimos URL en
cars/urls.pyfrom django.urls import path from .views import BrandListView urlpatterns = [ path('brands/', BrandListView.as_view(), name='brands'), ] -
Comprobar en http://127.0.0.1:8000/v1/cars/brands/
Step 7 (Petición POST, crear un recurso)
-
Definimos Vista en
cars/views.pyfrom rest_framework.generics import CreateAPIView, ListAPIView ... class BrandCreateView(CreateAPIView): serializer_class = BrandSerializer permission_classes = () -
Definimos URL en
cars/urls.pyfrom django.urls import path from .views import BrandCreateView, BrandListView urlpatterns = [ path('brands/', BrandListView.as_view(), name='brands'), path('brands/create/', BrandCreateView.as_view(), name='brand_create'), ] -
Usar postman u otro cliente http para lanzar un POST al endpoint http://127.0.0.1:8000/v1/cars/brands/create/, con la variable name inicializada en el payload de un form-data.
Step 8 (Petición GET, obtener un recurso)
-
Definimos Vista en
cars/views.pyfrom rest_framework.generics import (CreateAPIView, ListAPIView, RetrieveAPIView) ... class BrandRetrieveView(RetrieveAPIView): serializer_class = BrandSerializer permission_classes = () queryset = Brand.objects.all() lookup_field = 'id' -
Definimos URL en
cars/urls.pyfrom django.urls import path from .views import BrandCreateView, BrandListView, BrandRetrieveView urlpatterns = [ path('brands/', BrandListView.as_view(), name='brands'), path('brands/create/', BrandCreateView.as_view(), name='brand_create'), path('brands/< int:id >/', BrandRetrieveView.as_view(), name='brand'), ] -
Desde el navegador o postman lanzar un GET al endpoint http://127.0.0.1:8000/v1/cars/brands/1/
Step 9 (Petición PUT/PATCH, modificar un recurso)
-
Definimos Vista en
cars/views.pyfrom rest_framework.generics import (CreateAPIView, ListAPIView, RetrieveAPIView, UpdateAPIView) ... class BrandUpdateView(UpdateAPIView): serializer_class = BrandSerializer permission_classes = () queryset = Brand.objects.all() lookup_field = 'id' -
Definimos URL en
cars/urls.py... from .views import (BrandCreateView, BrandListView, BrandRetrieveView, BrandUpdateView) urlpatterns = [ ... path('brands/< int:id >/update/', BrandUpdateView.as_view(), name='brand_update'), ] -
Usar postman para lanzar un PUT o PATCH al endpoint http://127.0.0.1:8000/v1/cars/brands/1/ con la variable name inicializada en el payload de un form-data
Step 10 (Petición DELETE, eliminar un recurso)
-
Definimos Vista en
cars/views.pyfrom rest_framework.generics import (CreateAPIView, DestroyAPIView, ListAPIView, RetrieveAPIView, UpdateAPIView) … class BrandDestroyView(DestroyAPIView): permission_classes = () queryset = Brand.objects.all() lookup_field = 'id' -
Definimos URL en
cars/urls.py... from .views import (BrandCreateView, BrandDestroyView, BrandListView, BrandRetrieveView, BrandUpdateView) urlpatterns = [ ... path('brands/< int:id >/delete/', BrandDestroyView.as_view(), name='brand_delete'), ] -
Usar postman para lanzar un DELETE al endpoint http://127.0.0.1:8000/v1/cars/brands/3/delete/
Step 11 (Más sobre Modelos)
-
Definimos un nuevo modelo con más fantasía en
cars/models.py:class Car(models.Model): created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) model = models.CharField(max_length=25) manufacturing_date = models.DateField() brand = models.ForeignKey( 'Brand', related_name='cars', on_delete=models.CASCADE ) -
Y le añadimos el orden y el índice:
class Car(models.Model): ... class Meta: ordering = ['updated'] indexes = [models.Index(fields=['brand'])] -
Ejecutamos en la terminal
python manage.py makemigrations python manage.py migrate
Step 12 (Mejorando Serializadores)
-
Modificamos el serializador en
cars/serializers.py:from rest_framework import serializers ... class BrandSerializer(serializers.ModelSerializer): total_cars = serializers.SerializerMethodField() class Meta: model = Brand fields = ('id', 'name', 'total_cars', ) read_only_fields = ('id', ) def get_total_cars(self, obj): return obj.cars.count() -
Añadimos un nuevo serializador en
cars/serializers.py:... from .models import Brand, Car ... class CarSerializer(serializers.ModelSerializer): class Meta: model = Car fields = ('id', 'model', 'brand', 'manufacturing_date', ) read_only_fields = ('id', ) extra_kwargs = { 'manufacturing_date': {'write_only': True}, }
Step 13 (Paginación)
1.Creamos un archivo cars/paginations.py:
from rest_framework.pagination import PageNumberPagination
class SmallResultsSetPagination(PageNumberPagination):
page_size = 10
page_query_param = 'page'
-
Abrimos
cars/views.pyy modificamosBrandListView:from .paginations import SmallResultsSetPagination ... class BrandListView(ListAPIView): serializer_class = BrandSerializer permission_classes = () queryset = Brand.objects.all() pagination_class = SmallResultsSetPagination -
Lo probamos en postman haciendo un GET como http://127.0.0.1:8000/v1/cars/brands/?page=< page-num >, siendo < page-num > el número de la página, quedando para pedir la primera: http://127.0.0.1:8000/v1/cars/brands/?page=1
Step 14 (Filtros y Orden)
-
Abrimos
cars/views.pyy modificamosBrandListViewpara introducir los filtros:from rest_framework import generics from rest_framework import filters as df ... class BrandListView(generics.ListAPIView): serializer_class = BrandSerializer permission_classes = () queryset = Brand.objects.all() pagination_class = SmallResultsSetPagination filter_backends = (df.SearchFilter, ) search_fields = ('name', ) -
Seguimos modificando
BrandListView:class BrandListView(generics.ListAPIView): serializer_class = BrandSerializer permission_classes = () queryset = Brand.objects.all() pagination_class = SmallResultsSetPagination filter_backends = (df.OrderingFilter, df.SearchFilter, ) search_fields = ('name', ) ordering_fields = ('name', ) -
Lo probamos en postman haciendo un GET de: http://127.0.0.1:8000/v1/cars/brands/?search=< str-search > , siendo < str-search > la cadena por la que se quiere buscar, quedando por ejemplo: http://127.0.0.1:8000/v1/cars/brands/?search=for
-
Y combinandolo: http://127.0.0.1:8000/v1/cars/brands/?page=1&search=olks
Step 15 (Vistas Conjuntas)
Obtener y Crear Recursos (GET y POST)
- En
cars/views.pyintroducimos:
from .models import Brand, Car
...
from .serializers import BrandSerializer, CarSerializer
...
class CarListCreateView(generics.ListCreateAPIView):
serializer_class = CarSerializer
permission_classes = ()
queryset = Car.objects.all()
pagination_class = SmallResultsSetPagination
filter_backends = (df.OrderingFilter, df.SearchFilter, )
search_fields = ('model', )
ordering_fields = ('model', )
- Y en
cars/urls.pyintroducimos:
from . import views
urlpatterns = [
...
path('cars/', views.CarListCreateView.as_view(), name='cars'),
]
- Y lo probamos haciendo GET y POST de: http://127.0.0.1:8000/v1/cars/cars/
Obtener, Editar, Eliminar un Recurso (GET, PUT/PATCH y DELETE)
- En
cars/views.pyintroducimos:
class CarRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = CarSerializer
permission_classes = ()
queryset = Car.objects.all()
lookup_field = 'id'
- Y en
cars/urls.pyintroducimos:
urlpatterns = [
...
path('cars/<int:id>/', views.CarRetrieveUpdateDestroyView.as_view(), name='car'),
]
- Y lo probamos haciendo GET, PATCH y DELETE de: http://127.0.0.1:8000/v1/cars/cars/< id >/ , siendo id por ejemplo 1
Step 16 (Permisos)
-
Entramos en
cars/views.py:from rest_framework.permissions import IsAuthenticated class BrandCreateView(generics.CreateAPIView): ... permission_classes = (IsAuthenticated, ) ...
Step 17 (Oauth2)
Recordad que hay que tener el virtualenv activo: source bin/activate
-
Abrimos requirements.pip que está en la carpeta principal e incluimos:
django-oauth-toolkit==1.3.2 -
Y volvemos a instalar los módulos introduciendo en la terminal:
pip install -r requirements.pip -
Abrimos De0aDjangoREST/settings.py y cambiamos:
INSTALLED_APPS = [ 'django.contrib.admin', ... 'rest_framework', 'cars', 'oauth2_provider', ] -
Abrimos De0aDjangoREST/settings.py y cambiamos:
REST_FRAMEWORK = { ... 'DEFAULT_AUTHENTICATION_CLASSES': ( 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', ), 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', ) } ... OAUTH2_PROVIDER = { 'SCOPES': {'read': 'Read scope', 'write': 'Write scope'} } -
Abrimos De0aDjangoREST/urls.py y cambiamos:
from django.conf.urls import include urlpatterns = [ ... path('v1/o/', include('oauth2_provider.urls', namespace='oauth2_provider')), ] -
Corremos las nuevas migraciones para usar oauth2 ejecutando en la terminal:
python manage.py migrate -
Abrimos la shell de django (Recordad que hay que tener el virtualenv activo:
source bin/activate):python manage.py shell -
Introducimos en la shell de Django:
from oauth2_provider.models import get_application_model Application = get_application_model() Application.objects.create( name="Application", client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_PASSWORD, ) vars(Application.objects.first()) quit()
Y copiamos el client id y el secret id
-
Creamos un superuser:
python manage.py createsuperuser -
Obtenemos el token desde postman o haciendo:
curl -X POST -d "grant_type=password&username=< user_name >&password=< password >" -u"< client_id >:< client_secret >" http://localhost:8000/v1/o/token/Sustituyendo los campose entre <> por los datos correspondientes.
- A partir de aquí, para validar el usuario habra que meter una cabecera en las peticiones con el token que nos devuelve.
Authorization: Bearer <your_access_token>
Más info: https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html
Step 18 (Testing)
Recordad que hay que tener el virtualenv activo: source bin/activate
-
Abrimos requirements.pip que está en la carpeta principal e incluimos:
model_bakery==1.1.1 -
Y volvemos a instalar los módulos ejecutando en la terminal:
pip install -r requirements.pip -
Abrimos
cars/test.pyfrom model_bakery import baker from rest_framework import status from rest_framework.test import APITestCase from django.urls import reverse class GetBrandsTest(APITestCase): """ Test module for GET brands """ def setUp(self): self.brands = [] self.brands.append(baker.make('cars.Brand', name='a')) self.brands.append(baker.make('cars.Brand', name='b')) self.brands_counter = len(self.brands) self.url = reverse('brands') def test_get_brands_valid(self): response = self.client.get(self.url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['results']), self.brands_counter) self.assertEqual(response.data['results'][0]['id'], self.brands[0].id) self.assertEqual(response.data['results'][1]['id'], self.brands[1].id) -
Ejecuta los tests en la terminal:
python manage.py test
No todo en la vida es tan fácil como en este workshop, para seguir aprendiendo: https://www.django-rest-framework.org/api-guide/generic-views/
Y algo más de testing: