Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Django REST framework workshop @Djangocon Europe 2015

Django REST framework workshop @Djangocon Europe 2015

xordoquy

June 05, 2015
Tweet

More Decks by xordoquy

Other Decks in Programming

Transcript

  1. REST • Client - Server • Stateless • Caching •

    Unified interface: • Resources identification • Resources manipulation • Auto-described message • Hypermedia • Layered architecture
  2. DRF Content negotiation / Parser Router Authentication / Permissions /

    Throttling Deserialization Processing / Pagination / Filtering Serialization Renderer Middleware
  3. Parsers / Renderers b'{"demo": 1}’ <class bytes> b'"2015-06-03"' <class bytes>

    {"demo": 1} <class dict> "2015-06-03" <class str> JSONParser Binary world Python land JSONRenderer
  4. Authentication Permissions Throttling • Authentication: Who are you ? •

    Permissions: Can you do this ? • Throttling: Did you abuse our ressources ?
  5. (De)serialization • Python base types <-> complex objects ‣ Representation

    step ‣ "2015-06-03" <-> datetime.date(2015, 6, 3) • Sanitize the inputs: ‣ type checking ‣ escaping
  6. Serializers {"demo": 1} <class dict> "2015-06-03" <class str> <SomeObj> <class

    SomeObj> 2015-06-03 <class date> Serializer Unsafe Safe
  7. Two levels of settings # global: settings.py REST_FRAMEWORK = {

    'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', ) } # Per class class ExampleView(APIView): authentication_classes = ( BasicAuthentication, SessionAuthentication )
  8. Virtual environment $ virtualenv workshop_drf_env Using base prefix '/opt/local/Python.framework/Versions/3.4' New

    python executable in workshop_drf_env/bin/python3.4 Also creating executable in workshop_drf_env/bin/python Installing setuptools, pip...done. $ cd workshop_drf_env/ $ source bin/activate (workshop_drf_env) $
  9. Dependencies (workshop_drf_env)$ pip install Django djangorestframework django- filter Collecting Django==1.8.2

    Downloading Django-1.8.2-py2.py3-none-any.whl (6.2MB) 100% |████████████████████████████████| 6.2MB 23.2MB/s Collecting djangorestframework==3.1.2 Downloading djangorestframework-3.1.2-py2.py3-none-any.whl (463kB) 100% |████████████████████████████████| 466kB 23.1MB/s … Installing collected packages: Django, djangorestframework, django- filter Successfully installed Django-1.8.2 django-filter-0.10.0 djangorestframework-3.1.2 (workshop_drf_env)$
  10. Layout workshop_drf + manage.py + workshop_drf + __init__.py + settings.py

    + urls.py + wsgi.py + todo + __init__.py + admin.py + models.py + tests.py + views.py + migrations + __init__.py
  11. todo/models.py from django.db import models from django.conf import settings class

    Category(models.Model): name = models.CharField(max_length=64) def __str__(self): return self.name class Meta: verbose_name_plural = "Categories" class Task(models.Model): name = models.CharField(max_length=64) owner = models.ForeignKey(settings.AUTH_USER_MODEL) categories = models.ManyToManyField( Category, related_name="tasks") done = models.BooleanField(default=False) def __str__(self): return self.name
  12. Migrations creation (workshop_drf_env)$ python manage.py check System check identified no

    issues (0 silenced). (workshop_drf_env)$ python manage.py makemigrations Migrations for 'todo': 0001_initial.py: - Create model Category - Create model Task (workshop_drf_env)$
  13. Database creation (workshop_drf_env)$ python manage.py migrate Operations to perform: Synchronize

    unmigrated apps: messages, staticfiles, rest_framework Apply all migrations: admin, contenttypes, sessions, todo, auth Synchronizing apps without migrations: Creating tables... Running deferred SQL... Installing custom SQL... Running migrations: Rendering model states... DONE Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK […] Applying sessions.0001_initial... OK Applying todo.0001_initial... OK
  14. todo/serializers.py from rest_framework import serializers from . import models class

    CategorySerializer(serializers.ModelSerializer): class Meta: model = models.Category class TaskSerializer(serializers.ModelSerializer): class Meta: model = models.Task
  15. todo/views.py from rest_framework import viewsets from . import serializers, models

    class CategoryViewSet(viewsets.ModelViewSet): queryset = models.Category.objects.all() serializer_class = serializers.CategorySerializer class TaskViewSet(viewsets.ModelViewSet): queryset = models.Task.objects.all() serializer_class = serializers.TaskSerializer
  16. todo/urls.py from django.conf.urls import url, include from rest_framework.routers import DefaultRouter

    from workshop_drf.todo import views router = DefaultRouter() router.register(r'category', views.CategoryViewSet) router.register(r'task', views.TaskViewSet) urlpatterns = [ url(r'^admin/', include(admin.site.urls)), url(r'^', include(router.urls)), url(r'^api-auth/', include( 'rest_framework.urls', namespace=‘rest_framework’)), ]
  17. todo/urls.py from django.conf.urls import url, include from rest_framework.routers import DefaultRouter

    from . import views router = DefaultRouter() router.register(r'category', views.CategoryViewSet) router.register(r'task', views.TaskViewSet) urlpatterns = [ url(r'^', include(router.urls)), url(r'^api-auth/', include( 'rest_framework.urls', namespace='rest_framework')) ]
  18. urls.py from django.conf.urls import include, url from django.contrib import admin

    import workshop_drf.todo.urls urlpatterns = [ url(r'^admin/', include(admin.site.urls)), url(r'', include(workshop_drf.todo.urls)), ]
  19. Browse • Overview: ➡ http://localhost:8000/ • List / create categories:

    ➡ http://localhost:8000/category/ • Show / update / delete a category: ➡ http://localhost:8000/category/1/ • As JSon: ➡ http://localhost:8000/category/?format=json
  20. Tests ! from django.core.urlresolvers import reverse from rest_framework import status

    from rest_framework.test import APITestCase from django.contrib.auth import User from . import models
  21. Tests !! class TestTask(APITestCase): def test_task_creation(self): user = User.objects.create(username="admin") category

    = models.Category.objects.create(name="Django") url = reverse('task-list') data = { "name": "demo", "owner": user.id, "categories": [category.id], "done": False, } response = self.client.post(url, data, format='json') self.assertEqual( response.status_code, status.HTTP_201_CREATED, response.content) data['id'] = 1 self.assertEqual(response.data, data) self.assertEqual(category.tasks.count(), 1)
  22. Tests !! class TestTask(APITestCase): def test_task_creation(self): user = User.objects.create(username="admin") category

    = models.Category.objects.create(name="Django") url = reverse('task-list') data = { "name": "demo", "owner": user.id, "categories": [category.id], "done": False, } response = self.client.post(url, data, format='json') self.assertEqual( response.status_code, status.HTTP_201_CREATED response.content) data['id'] = 1 self.assertEqual(response.data, data) self.assertEqual(category.tasks.count(), 1)
  23. Tests !! class TestTask(APITestCase): def test_task_creation(self): user = User.objects.create(username="admin") category

    = models.Category.objects.create(name="Django") url = reverse('task-list') data = { "name": "demo", "owner": user.id, "categories": [category.id], "done": False, } response = self.client.post(url, data, format='json') self.assertEqual( response.status_code, status.HTTP_201_CREATED, response.content) data['id'] = 1 self.assertEqual(response.data, data) self.assertEqual(category.tasks.count(), 1)
  24. Tests !! class TestTask(APITestCase): def test_task_creation(self): user = User.objects.create(username="admin") category

    = models.Category.objects.create(name="Django") url = reverse('task-list') data = { "name": "demo", "owner": user.id, "categories": [category.id], "done": False, } response = self.client.post(url, data, format='json') self.assertEqual( response.status_code, status.HTTP_201_CREATED, response.content) data['id'] = 1 self.assertEqual(response.data, data) self.assertEqual(category.tasks.count(), 1)
  25. Tests !! class TestTask(APITestCase): def test_task_creation(self): user = User.objects.create(username="admin") category

    = models.Category.objects.create(name="Django") url = reverse('task-list') data = { "name": "demo", "owner": user.id, "categories": [category.id], "done": False, } response = self.client.post(url, data, format='json') self.assertEqual( response.status_code, status.HTTP_201_CREATED, response.content) data['id'] = 1 self.assertEqual(response.data, data) self.assertEqual(category.tasks.count(), 1)
  26. Tests !! class TestTask(APITestCase): def test_task_creation(self): user = User.objects.create(username="admin") category

    = models.Category.objects.create(name="Django") url = reverse('task-list') data = { "name": "demo", "owner": user.id, "categories": [category.id], "done": False, } response = self.client.post(url, data, format='json') self.assertEqual( response.status_code, status.HTTP_201_CREATED, response.content) data['id'] = 1 self.assertEqual(response.data, data) self.assertEqual(category.tasks.count(), 1)
  27. Tests !! class TestTask(APITestCase): def test_task_creation(self): user = User.objects.create(username="admin") category

    = models.Category.objects.create(name="Django") url = reverse('task-list') data = { "name": "demo", "owner": user.id, "categories": [category.id], "done": False, } response = self.client.post(url, data, format='json') self.assertEqual( response.status_code, status.HTTP_201_CREATED, response.content) data['id'] = 1 self.assertEqual(response.data, data) self.assertEqual(category.tasks.count(), 1)
  28. Running tests (workshop_drf_env)$ python manage.py test Creating test database for

    alias 'default'... . ---------------------------------------------------------------- Ran 1 test in 0.053s OK Destroying test database for alias 'default'... (workshop_drf_env)$
  29. todo/serializers.py 1/2 from rest_framework import serializers from django.contrib.auth.models import User

    from . import models class CategorySerializer(serializers.ModelSerializer): class Meta: model = models.Category fields = ('id', 'name')
  30. todo/serializers.py 2/2 class TaskSerializer(serializers.ModelSerializer): owner = serializers.SlugRelatedField( slug_field='username', queryset=User.objects.all()) categories

    = serializers.SlugRelatedField( slug_field='name', queryset=models.Category.objects.all(), many=True) class Meta: model = models.Task fields = ('id', 'name', 'owner', 'categories', 'done')
  31. Tests (workshop_drf_env)$ python manage.py test Creating test database for alias

    'default'... F =================================================================== FAIL: test_task_creation (workshop_drf.todo.tests.TestTask) ------------------------------------------------------------------- Traceback (most recent call last): File "/Users/xordoquy/Documents/Devs/workshop_drf_djangoconeu2015/ workshop_drf/todo/tests.py", line 24, in test_task_creation response.status_code, status.HTTP_201_CREATED) AssertionError: 400 != 201 : b'{"categories":["Object with name=1 does not exist."],"owner":["Object with username=1 does not exist."]}' ------------------------------------------------------------------- Ran 1 test in 0.055s FAILED (failures=1) Destroying test database for alias 'default'... (workshop_drf_env)$
  32. Tests 400 != 201 { "categories": ["Object with name=1 does

    not exist."], "owner": ["Object with username=1 does not exist."] }
  33. Updated test class TestTask(APITestCase): def test_task_creation(self): user = User.objects.create(username="admin") category

    = models.Category.objects.create(name="Django") url = reverse('task-list') data = { "name": "demo", "owner": user.username, "categories": [category.name], "done": False, } response = self.client.post(url, data, format='json') self.assertEqual( response.status_code, status.HTTP_201_CREATED, response.content) data['id'] = 1 self.assertEqual(response.data, data) self.assertEqual(category.tasks.count(), 1)
  34. class TaskSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField( 'task-detail', source='id', read_only=True) owner =

    serializers.SlugRelatedField( slug_field='username', queryset=get_user_model().objects.all()) categories = serializers.SlugRelatedField( slug_field='name', queryset=models.Category.objects.all(), many=True) class Meta: model = models.Task fields = ('id', 'name', 'owner', 'categories', 'done', 'url') todo/serializers.py
  35. class TestTask(APITestCase): def test_task_creation(self): user = User.objects.create(username="admin") category = models.Category.objects.create(name="Django")

    url = reverse('task-list') data = { "name": "demo", "owner": user.username, "categories": [category.name], "done": False, 'url': 'http://testserver/task/1/', } response = self.client.post(url, data, format='json') self.assertEqual( response.status_code, status.HTTP_201_CREATED, response.content) data['id'] = 1 self.assertEqual(response.data, data) self.assertEqual(category.tasks.count(), 1) todo/tests.py
  36. Updated test def test_task_creation(self): user = User.objects.create(username="admin") user.set_password("a") user.save() self.client.login(username='admin',

    password='a') category = models.Category.objects.create(name="Django") […] self.assertEqual(category.tasks.count(), 1) task = models.Task.objects.get(id=1) self.assertEqual(task.owner_id, user.id)
  37. class TaskSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField( 'task-detail', source='id', read_only=True) owner =

    serializers.SlugRelatedField( slug_field='username', read_only=True) categories = serializers.SlugRelatedField( slug_field='name', queryset=models.Category.objects.all(), many=True) class Meta: model = models.Task fields = ('id', 'name', 'owner', 'categories', 'done', ‘url’) def create(self, validated_data): categories = validated_data.pop('categories') task = models.Task.objects.create(**validated_data) task.categories = categories return task todo/serializers.py
  38. class TaskSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField( 'task-detail', source='id', read_only=True) owner =

    serializers.SlugRelatedField( slug_field='username', read_only=True, default=serializers.CurrentUserDefault()) categories = serializers.SlugRelatedField( slug_field='name', queryset=models.Category.objects.all(), many=True) class Meta: model = models.Task fields = ('id', 'name', 'owner', 'categories', 'done', ‘url’) todo/serializers.py
  39. Categories [{ "id": 4, "name": "Django", "tasks": [{ "id": 1,

    "name": "Faire les slides", "owner": "admin", "categories": ["Django"], "done": false }, { "id": 3, "name": "Fix #546", "owner": "admin", "categories": ["Django"], "done": false }] }]
  40. My tasks • All my tasks: /task/mine/ • My tasks

    within a category: /category/<id>/mine/
  41. from rest_framework.response import Response from rest_framework.decorators import detail_route, list_route class

    TaskViewSet(viewsets.ModelViewSet): […] @list_route() def mine(self, request): queryset = self.filter_queryset( self.get_queryset().filter(owner=request.user)) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) todo/views.py
  42. todo/views.py class CategoryViewSet(viewsets.ModelViewSet): queryset = models.Category.objects.all() serializer_class = serializers.CategorySerializer @detail_route()

    def mine(self, request, *args, **kwargs): category = self.get_object() category.my_tasks = category.tasks.filter( owner=request.user) serializer = serializers.MyCategory( category, context={‘request’: request} ) return Response(serializer.data)
  43. todo/views.py class CategoryViewSet(viewsets.ModelViewSet): queryset = models.Category.objects.all() serializer_class = serializers.CategorySerializer @detail_route()

    def mine(self, request, *args, **kwargs): category = self.get_object() category.my_tasks = category.tasks.filter( owner=request.user) serializer = serializers.MyCategory( category, context={‘request’: request} ) return Response(serializer.data)
  44. todo/views.py class CategoryViewSet(viewsets.ModelViewSet): queryset = models.Category.objects.all() serializer_class = serializers.CategorySerializer @detail_route()

    def mine(self, request, *args, **kwargs): category = self.get_object() category.my_tasks = category.tasks.filter( owner=request.user) serializer = serializers.MyCategory( category, context={‘request’: request} ) return Response(serializer.data)
  45. todo/views.py class CategoryViewSet(viewsets.ModelViewSet): queryset = models.Category.objects.all() serializer_class = serializers.CategorySerializer @detail_route()

    def mine(self, request, *args, **kwargs): category = self.get_object() category.my_tasks = category.tasks.filter( owner=request.user) serializer = serializers.MyCategory( category, context={‘request’: request} ) return Response(serializer.data)
  46. todo/views.py class CategoryViewSet(viewsets.ModelViewSet): queryset = models.Category.objects.all() serializer_class = serializers.CategorySerializer @detail_route()

    def mine(self, request, *args, **kwargs): category = self.get_object() category.my_tasks = category.tasks.filter( owner=request.user) serializer = serializers.MyCategory( category, context={‘request’: request} ) return Response(serializer.data)
  47. todo/views.py from rest_framework import view sets from rest_framework.filters import DjangoFilterBackend

    from . import serializers, models, filters class Task(viewsets.ModelViewSet): queryset = models.Task.objects.all() serializer_class = serializers.TaskSerializer filter_backends = (DjangoFilterBackend,) filter_class = filters.TaskFilter
  48. todo/filters.py import django_filters from django.contrib.auth import get_user_model from . import

    models class TaskFilterSet(django_filters.FilterSet): owner = django_filters.ModelChoiceFilter( to_field_name="username", queryset=get_user_model().objects.all()) categories = django_filters.ModelMultipleChoiceFilter( to_field_name="name", queryset=models.Category.objects.all()) class Meta: model = models.Task fields = ['done', 'owner', 'categories']
  49. todo/views.py from rest_framework import viewsets from rest_framework.filters import DjangoFilterBackend from

    . import serializers, models, filters class TaskViewSet(viewsets.ModelViewSet): queryset = models.Task.objects.all() serializer_class = serializers.TaskSerializer filter_backends = (DjangoFilterBackend,) filter_class = filters.TaskFilter
  50. • Montrer les classes et le source DRF • Modifier

    un get/post • Montrer les docstrings dans l’interface d’admin ! • Validators • Passer l’ensemble sous forme d’exercices