Лабораторная работа №7. REST API для ведения заметок на Django
В этой работе мы напишем простой REST API сервис для ведения заметок. Если вы не знаете, что такое REST API, то советую обратиться к этой ссылке: http://www.restapitutorial.ru/lessons/whatisrest.html.
Установите Django, если он еще не установлен:
(cs102) $ pip install django
Создайте новый проект:
(cs102) $ django-admin startproject djangorest
Установите Django Rest Framework:
(cs102) $ pip install djangorestframework
Подключим фреймворк для использования в нашем проекте. Для этого в файле djangorest/settings.py
необходимо добавить строку rest_framework
:
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
]
В одном проекте может быть задействовано несколько приложений, поэтому создадим отдельное (и единственное) приложение для заметок:
(cs102) $ cd djangorest
(cs102) $ python manage.py startapp todolist
Добавим наше приложение в файле djangorest/settings.py
:
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'todolist',
]
В этой работе для проверки нашего кода мы будем писать тесты согласно принципу (философии) TDD, простыми словами: сначала пишем тест - потом код. В Django для создания тестов используется unittest-фреймворк.
Создадим наш первый тест, который проверяет можем ли мы создавать модели и сохранять их в БД. Для этого в файле todolist/tests.py
добавим следующий класс:
from django.test import TestCase
from .models import Task
class ModelTestCase(TestCase):
def setUp(self):
self.task_name = "Выполнить лабораторную работу №9"
self.task = Task(name=self.task_name)
def test_model_can_create_task(self):
old_count = Task.objects.count()
self.task.save()
new_count = Task.objects.count()
self.assertNotEqual(old_count, new_count)
Теперь создадим модель Task
в файле todolist/models.py
:
from django.db import models
class Task(models.Model):
pass
И запустим наш тест:
(cs102) $ python manage.py test
Вы увидите множество ошибок, которые связаны с тем, что мы еще не создали БД и не добавили модель Task
. Давайте сделаем это:
(cs102) $ python manage.py makemigrations
Migrations for 'todolist':
todolist/migrations/0001_initial.py:
- Create model Task
(cs102) $ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, todolist
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying sessions.0001_initial... OK
Applying todolist.0001_initial... OK
И еще раз запустим наш тест:
(cs102) $ python manage.py test
Creating test database for alias 'default'...
E
======================================================================
ERROR: test_model_can_create_task (todolist.tests.ModelTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/dementiy/Projects/python-projects/django-tutorials/djangorest/todolist/tests.py", line 9, in setUp
self.task = Task(name=self.task_name)
File "/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/site-packages/django/db/models/base.py", line 555, in __init__
raise TypeError("'%s' is an invalid keyword argument for this function" % list(kwargs)[0])
TypeError: 'name' is an invalid keyword argument for this function
----------------------------------------------------------------------
Ran 1 test in 0.006s
FAILED (errors=1)
Destroying test database for alias 'default'...
В этот раз сообщений об ошибках не так много и они стали более информативными. В нашем примере не найден атрибут name
у модели Task
. Давайте изменим нашу модель:
class Task(models.Model):
name = models.CharField(max_length=200, blank=True)
description = models.TextField(max_length=1000, blank=True)
completed = models.BooleanField(default=False)
date_created = models.DateField(auto_now_add=True)
due_date = models.DateField(null=True, blank=True)
date_modified = models.DateField(auto_now=True)
PRIORITY = (
('h', 'High'),
('m', 'Medium'),
('l', 'Low'),
('n', 'None')
)
priority = models.CharField(max_length=1, choices=PRIORITY, default='n')
def __str__(self):
return "{}".format(self.name)
И сохраним изменения в БД:
(cs102) $ python manage.py makemigrations
Migrations for 'todolist':
todolist/migrations/0002_auto_20170225_1224.py:
- Add field completed to task
- Add field date_created to task
- Add field date_modified to task
- Add field description to task
- Add field due_date to task
- Add field name to task
- Add field priority to task
MacBook-Air:djangorest dementiy$ python3 manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, todolist
Running migrations:
Applying todolist.0002_auto_20170225_1224... OK
Замечание: ....
Снова запустим тест:
(cs102) $ python manage.py test
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.003s
OK
Destroying test database for alias 'default'...
В этот раз уже никаких ошибок не было, а это означает, что мы можем создавать модели и сохранять их в БД.
Теперь нам нужно создать сериализатор. Сериализаторы позволяют представить множество сложных объектов, которые возможно мы получили из БД, в простой и человеко-читаемой форме, например, в формате JSON или XML.
Создадим файл todolist/serializers.py
со следующим содержимым:
from rest_framework import serializers
from .models import Task
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = ('id', 'name', 'description', 'completed', 'date_created', 'date_modified', 'due_date', 'priority')
read_only_fields = ('date_created', 'date_modified')
Нам нужно создать несколько "вьюеров" (views), которые позволят:
- Создать заметку - POST-запрос
- Удалить заметку - DELETE-запрос
- Обновить заметку - PUT-запрос
- Просмотреть одну или несколько заметок - GET-запрос
Снова создадим тест в файле todolist/tests.py
:
from rest_framework.test import APIClient
from rest_framework import status
from django.core.urlresolvers import reverse
class ViewTestCase(TestCase):
def setUp(self):
self.client = APIClient()
self.task_data = {
'name': 'Выполнить 9 лабораторную работу',
'description': 'Прочитать руководство по django rest framework',
'priority': 'h'
}
self.response = self.client.post(reverse('create'), self.task_data, format='json')
def test_api_can_create_a_task(self):
self.assertEqual(self.response.status_code, status.HTTP_201_CREATED)
Запустите тест также как мы делали это ранее.
Теперь в файле todolist/views.py
создадим два вьювера для создания и просмотра заметок:
from rest_framework import generics
from .serializers import TaskSerializer
from .models import Task
class TaskCreateView(generics.ListCreateAPIView):
queryset = Task.objects.all()
serializer_class = TaskSerializer
class TaskDetailsView(generics.RetrieveAPIView):
queryset = Task.objects.all()
serializer_class = TaskSerializer
Теперь нам нужно связать вьюеры с соответствующими URL, для этого создайте файл todolist/urls.py
:
from django.conf.urls import url, include
from rest_framework.urlpatterns import format_suffix_patterns
from .views import TaskCreateView, TaskDetailsView
urlpatterns = {
url(r'^todolists/$', TaskCreateView.as_view(), name="create"),
url(r'^todolists/(?P<pk>[0-9]+)/$', TaskDetailsView.as_view(), name="detail"),
}
urlpatterns = format_suffix_patterns(urlpatterns)
Теперь нам этот файл нужно включить в список всех маршрутов djangorest/urls.py
:
from django.conf.urls import url, include
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^', include('todolist.urls')),
]
Итак, давайте запустим сервер:
(cs102) $ python manage.py runserver
Performing system checks...
System check identified no issues (0 silenced).
February 25, 2017 - 13:48:20
Django version 1.10.5, using settings 'djangorest.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Пройдите по адресу http://127.0.0.1:8000/todolists
, вы должны увидеть следующее окно:
В нашей БД еще нет ни одной записи, давайте добавим новую запись, для этого достаточно заполнить поля и нажать на POST
:
Также мы можем обратиться по адресу http://127.0.0.1:8000/todolists/1/
, чтобы получить заметку с идентификатором 1:
Теперь давайте добавим возможность объединения заметок в группы, назовем их "Списком задач". Для этого нам нужно добавить модель для списка задач todolist/models.py
:
class Tasklist(models.Model):
name = models.CharField(max_length=200)
def __str__(self):
return "{}".format(self.name)
Теперь необходимо отразить, что задача входит в конкретный список задач (то есть отношение один ко многим - один список задач включает множество задач), сделаем это с помощью внешнего ключа (про работу с внешними ключами можно почитать тут:
class Task(models.Model):
# ...
tasklist = models.ForeignKey(Tasklist, related_name='tasks', on_delete=models.CASCADE)
# ...
Также нам надо добавить сериализатор для списка задач:
from .models import Tasklist
class TasklistSerializer(serializers.ModelSerializer):
tasks = serializers.StringRelatedField(many=True)
class Meta:
model = Tasklist
fields = ('name', 'tasks')
Все изменения надо зафиксировать в БД:
(cs102) $ python manage.py makemigrations
(cs102) $ python manage.py migrate
Прежде чем мы продолжим давайте определимся со списком методов, которые будем предоставлять пользователю:
/todolists
- получение всех списков задач или создание нового списка (GET и POST методы)/todolists/list_id
- редактирование или удаление списка задач с идентификаторомlist_id
(GET, PUT, DELETE)/todolists/list_id/tasks
- просмотр задач или создание новой задачи в списке задач с идентификаторомlist_id
(GET и POST)/todolists/list_id/tasks/task_id
- редактирование или удаление задачиtask_id
в списке задач с идентификаторомlist_id
(GET, PUT, DELETE)
Давайте отразим этот список методов в наших маршрутах todolists/urls.py
:
from django.conf.urls import url, include
from rest_framework.urlpatterns import format_suffix_patterns
from .views import TasklistCreateView, TasklistDetailsView, TaskCreateView, TaskDetailsView
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = {
url(r'^todolists/$', TasklistCreateView.as_view(), name="lists"),
url(r'^todolists/(?P<pk>[0-9]+)/$', TasklistDetailsView.as_view(), name="list-detail"),
url(r'^todolists/(?P<list_id>[0-9]+)/tasks', TaskCreateView.as_view(), name="tasks"),
url(r'^todolists/(?P<list_id>[0-9]+)/tasks/(?P<pk>[0-9]+)', TaskDetailsView.as_view(), name="task-detail"),
}
urlpatterns = format_suffix_patterns(urlpatterns)
Нам нужно добавить вьюверы для списков задач и изменить вьюверы для самих задач:
class TasklistCreateView(generics.ListCreateAPIView):
queryset = Tasklist.objects.all()
serializer_class = TasklistSerializer
class TasklistDetailsView(generics.RetrieveUpdateDestroyAPIView):
queryset = Tasklist.objects.all()
serializer_class = TasklistSerializer
class TaskCreateView(generics.ListCreateAPIView):
serializer_class = TaskSerializer
def get_queryset(self):
queryset = Task.objects.all()
list_id = self.kwargs.get('list_id', None)
if list_id is not None:
queryset = queryset.filter(tasklist_id = list_id)
return queryset
def perform_create(self, serializer):
list_id = self.kwargs.get('list_id', None)
try:
tasklist = Tasklist.objects.get(pk=list_id)
except Tasklist.DoesNotExist:
raise NotFound()
serializer.save(tasklist=tasklist)
class TaskDetailsView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = TaskSerializer
def get_queryset(self):
queryset = Task.objects.all()
list_id = self.kwargs.get('list_id', None)
if list_id is not None:
queryset = queryset.filter(tasklist_id = list_id)
return queryset
Обратите внимание, что мы теперь получаем задачи предварительно фильтруя их по идентификатору списка задач.
Задание:
- Добавьте возможность указания тегов для задач. Для этого вам необходимо создать новую модель
Tag
, у тега будет поле с именем (name
). Следует иметь ввиду, что одна задача может иметь множество тегов, а одинаковые теги, могут быть применимы к разным задачам. Таким образом, это отношение многие ко многим. Посмотрите этот вопрос и ответ для решения этого задания. - Добавьте поле владелец (
owner
) к списку задач, таким образом, пользователь должен видеть только те списки задач (или задачи), которые создавал он. Для выполнения этого задания вам необходимо иметь возможность создавать пользователей.