Оглавление
Как создать и запустить простое Django-приложение?
Как создать URL с параметрами?
Как вернуть данные для карточки объекта?
Как принять query-параметры из URL?
Как создать и запустить простое Django-приложение?
Работа из PyCharm Создаем новый проект и виртуальное окружение. PyCharm Community Edition
# Установить poetry (опционально)
pip install poetry
poetry init
# Установить пакет Django
poetry add django # через poetry
pip install django # с помощью pip
# Создать Django-приложение
django-admin startproject hunting .
# Создать app — Python-пакет с обособленный куском функционала
# например, функционал работы с вакансиями
./manage.py startapp vacancies
# Запустить сервер
./manage.py runserver
Как создать модель?
**Модель** — это класс, который описывает все поля (столбцы), которые будут храниться в таблице в базе данных. Для того чтобы создать модель, нужно объяснить класс-наследник models.Model. Например, вот так будет выглядеть модель вакансии с текстовым полем text:
# models.py
class Vacancy(models.Model):
text = models.CharField(max_length=1000)
Работа из консоли (terminal)
Можно создать Django приложение заранее из консоли, а затем уже открыть в PyCharm.Note: Виртуальное окружение можно создать любым удобным способом - ниже в примере команда для создания виртуального окружения с помощью `[virtualenvwrapper](https://virtualenvwrapper.readthedocs.io/en/latest/install.html)` (его предварительно надо установить).
# Создать виртуальное окружение
mkvirtualenv hanting
# Создать папку для приложения
mkdir hanting
cd hanting
# Установить poetry (опционально)
pip install poetry
poetry init
# Установить пакет Django
poetry add django # через poetry
pip install django # с помощью pip
# Создать Django-приложение
django-admin startproject hunting .
# Создать app — Python-пакет с обособленный куском функционала
# например, функционал работы с вакансиями
./manage.py startapp vacancies
# Запустить сервер
./manage.py runserver
Как создать миграцию
После того, как мы создали модель, необходимо создать и накатить миграцию — скрипт, который применит наши изменения на базе данных:
./manage.py makemigrations
./manage.py migrate
Как создать view?
View — это функция, которая определяется в файле views.py вашего app. Эта функция обязательно должна принимать первым параметром request.
# views.py
def index(request):
pass
Как создать URL?
Для того чтобы создать URL в нашем веб-приложении, необходимо добавить его в файл urls.py.
# urls.py
from django.contrib import admin
from django.urls import path
from vacancy import views
urlpatterns = [
path('admin/', admin.site.urls),
path('vacancy/', views.index)
]
Как вернуть список?
Для этого нужно сначала вытащить его из базы данных через ORM:
# views.py
vacancies = Vacancy.objects.all()
После этого перевести полученный список в JSON и вернуть его, завернув в JsonResponse.
response = []
for vacancy in vacancies:
response.append({
"id": vacancy.id,
"text": vacancy.text,
})
return JsonResponse(response, safe=False)
Пример полной вью, которая возвращает список всех вакансий:
# views.py
def index(request):
# Вытаскиваем все объекты
vacancies = Vacancy.objects.all()
# Переводим в JSON
response = []
for vacancy in vacancies:
response.append({
"id": vacancy.id,
"text": vacancy.text,
})
# Возвращаем JsonResponse
return JsonResponse(response, safe=False)
Как создать URL с параметрами?
Для этого нужно указать параметр в URL в формате <тип:имя>. Например:
# urls.py
from django.contrib import admin
from django.urls import path
from blog import views
urlpatterns = [
path('admin/', admin.site.urls),
path('vacancy/', views.index),
path('vacancy/<int:vacancy_id>', views.get),
]
Доступны следующие типы:
- `str` — любая непустая строка.
- `int` — 0 или любое положительное число.
- `slug` — строка из ASCII букв или чисел, а также дефисы и подчеркивание. Например, building-your-1st-django_site
- `uuid` — универсальный уникальный идентификатор. Например, 075194d3-6885-417e-a8a8-6c931e272f00
- `path` — непустая строка, включая /.
Во вью эти параметры передаются в качестве аргументов функции:
# views.py
def get(request, vacancy_id):
pass
Как вернуть данные для карточки объекта?
Для этого нужно создать URL с параметром pk.
# urls.py
from django.contrib import admin
from django.urls import path
from blog import views
urlpatterns = [
path('admin/', admin.site.urls),
path('vacancy/', views.index),
path('vacancy/<int:vacancy_id>', views.get),
]
А во вью достать данные с помощью ORM метода get.
vacancy = Vacancy.objects.get(pk=vacancy_id)
Пример готовой view для вакансии:
# views.py
def get(request, vacancy_id):
if request.method == "GET":
vacancy = Vacancy.objects.get(pk=vacancy_id)
return JsonResponse({
"id": vacancy.id,
"text": vacancy.text,
})
Как вернуть ошибку 404?
Для того чтобы вернуть ошибку 404 — объект не найден, нужно обработать исключение DoesNotExists.
# views.py
def get(request, vacancy_id):
if request.method == "GET":
try:
vacancy = Vacancy.objects.get(pk=vacancy_id)
except Vacancy.DoesNotExist:
return JsonResponse({"error": "Not found"}, status=404)
return JsonResponse({
"id": vacancy.id,
"text": vacancy.text,
})
Как принять query-параметры из URL?
Query-параметры не указываются в самом URL, а обрабатываются во view.
# Создать # views.py
search_text = request.GET.get("text", None) окружение
Пример вью, которая вернет переданный ей параметр text в JSON:
# views.py
def get(request, vacancy_id):
if request.method == "GET":
search_text = request.GET.get("text", None)
return JsonResponse({"text": search_text})
Оглавление
Как сделать сохранение модели?
Как написать собственные class-based view?
Глоссарий
Generic views — встроенные в Django классы, реализующие в себе базовые методы и шаблоны, упрощающие написание собственных views.
Как сделать сохранение модели?
## csrf_exempt В Django из коробки встроена защита от CSRF-атак. Это здорово, но мешает нам обращаться к нашему API со сторонних сервисов (например, тестировать через Postman). Эту проверку можно отключить с помощью декоратора `@csrf_exempt` вот так:
# views.py
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def index(request):
if request.method == "GET":
vacancies = Vacancy.objects.all()
search_text = request.GET.get("text", None)
if search_text:
vacancies = vacancies.filter(text=search_text)
response = []
for vacancy in vacancies:
response.append({
"id": vacancy.id,
"text": vacancy.text,
})
return JsonResponse(response, safe=False)
elif request.method == "POST":
vacancy_data = json.loads(request.body)
vacancy = Vacancy()
vacancy.text = vacancy_data["text"]
vacancy.save()
return JsonResponse({
"id": vacancy.id,
"text": vacancy.text,
})
Как написать собственные class-based view?
Для того чтобы написать собственные class-based view, нам нужно проделать действия, схожие с теми, что мы делали для function-based view: - написать класс, отнаследованный от view; - настроить URL для нашей view.
Наследуемся от view
Чтобы создать view, нужно: 1. Создать класс-наследник от view. 2. Переопределить нужные нам методы (get/post). 3. Добавить декоратор `@method_decorator(csrf_exempt, name='dispatch')`при необходимости.
# views.py
import json
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from blog.models import Vacancy
@method_decorator(csrf_exempt, name='dispatch')
class VacancyView(View):
def get(self, request):
vacancies = Vacancy.objects.all()
search_text = request.GET.get("text", None)
if search_text:
vacancies = vacancies.filter(text=search_text)
response = []
for vacancy in vacancies:
response.append({
"id": vacancy.id,
"text": vacancy.text,
})
return JsonResponse(response, safe=False)
def post(self, request):
vacancy_data = json.loads(request.body)
vacancy = Vacancy.objects.create(
text=vacancy_data["text"],
)
return JsonResponse({
"id": vacancy.id,
"text": vacancy.text,
})
Настраиваем URL
Основное отличие настройки URL для class-based view: нам нужно вызвать у нашего класса метод as_view.
# urls.py
urlpatterns = [
path('admin/', admin.site.urls),
path('vacancy/', VacancyView.as_view()),
path('vacancy/<int:pk>/', VacancyDetailView.as_view()),
]
Наследуемся от generic view
Работа с generic view сильно похожа на то, что мы делаем с view, но заранее готового в generic view больше, а значит, нашего кода меньше. Порядок действий: 1. Наследуемся от нужной view. 2. Определяем атрибут model. 3. Переопределяем необходимый метод. Также помним, что для доступа к объекту модели (для DetailView) доступен вызов `self.get_object()`, а для списка записей (для ListView) — `self.object_list`.
# views.py
from django.http import JsonResponse
from django.views.generic import DetailView
from blog.models import Vacancy
class VacancyDetailView(DetailView):
model = Vacancy
def get(self, request, *args, **kwargs):
try:
vacancy = self.get_object()
except Vacancy.DoesNotExist:
return JsonResponse({"error": "Not found"}, status=404)
return JsonResponse({
"id": vacancy.id,
"text": vacancy.text,
})
Какие есть поля в модели?
В Django предусмотрено множество видов полей, практически на все случаи жизни. Для удобства мы приготовили схему всех базовых полей, доступных «из коробки».
Как вывести модель в админку?
Для того чтобы наша модель появилась в панели администратора Django (еще одна прекрасная встроенная возможность)
# admin.py
admin.site.register(Vacancy)
Оглавление
Как отобразить M2M в JSON вручную
Глоссарий
PostgreSQL — реляционная база данных с открытым исходным кодом.
PostgreSQL
Есть два варианта установки PostgreSQL: 1. Установить к себе на компьютер по инструкции из [официальной документации](https://www.postgresql.org/download/). 2. Установить docker-контейнер с уже готовой и настроенной СУБД.
docker run --name skypro-postgres -e POSTGRES_PASSWORD=postgres -d postgres
Настройка Django-приложения
После того как мы установили PostgreSQL, необходимо также сказать нашему приложению способы обращения к базе. Делается это в файле settings.py.
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'postgres',
'USER': 'postgres',
'PASSWORD': 'postgres',
'HOST': 'localhost',
'PORT': '5432',
}
}
Описание полей:
- ENGINE — оставляем без изменений.
- NAME — название нашей базы (если ничего не меняли, оставляем как в примере).
- USER — имя пользователя (если ничего не меняли, оставляем как в примере).
- PASSWORD — пароль от нашего пользователя (если ничего не меняли, оставляем как в примере).
- HOST — URL, на котором развернута база (в нашем случае localhost).
- PORT — порт, на котором поднята СУБД (если ничего не меняли, оставляем как в примере).
После всех манипуляций выше нам еще понадобится поставить отдельный пакет для работы с нашей СУБД.
poetry add psycopg2
Связи между моделями
Связь One2Many в Django реализована с помощью поля `ForeignKey`. При использовании этого поля необходимо указать два обязательных параметра: - модель, на которую будет ссылаться это поле (позиционный аргумент); - `on_delete` — действия при удалении записи, на которую мы ссылаемся. Например, вот так можно создать модель Parent, ссылающуюся на модель User:
# models.py
from django.db import models
class Parent(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
Many2Many
Связь Many2Many в Django реализована с помощью поля `ManyToManyField`. В отличие от `ForeignKey` у этого поля только один обязательный аргумент — модель, на которую мы будем ссылаться. Например, вот так мы в уроке создавали Many2Many связь между вакансией и скилами:
# models.py
from django.db import models
class Vacancy(models.Model):
skills = models.ManyToManyField(Skill)
Как отобразить M2M в JSON вручную
Поскольку M2M-связь — это целый запрос в другую таблицу, то вывести содержимое поля, как мы это делали раньше, не получится. Для того чтобы при отображении JSON нашей модели сделать M2M-поле «читаемым», можно воспользоваться строчкой ниже.
your_field = list(self.object.skills.all().values_list("name", flat=True))
Meta
Помимо полей, в модели Django есть еще вложенный класс — `Meta`. Он используется для определения служебной информации о модели: - название таблицы в БД, - сортировка по умолчанию, - пары (или тройки, четверки и т. д.) уникальных полей - и т. д. Самое частое использование класса Meta — это определение русскоязычного названия нашей модели в админ-панели.
# models.py
from django.db import models
class Skill(models.Model):
name = models.CharField(max_length=20)
class Meta:
verbose_name = "Навык"
verbose_name_plural = "Навыки"
Еще немного о GenericView
В прошлом уроке мы начали изучать GenericView. Ниже — примеры того, как написать CreateView, UpdateView и DeleteView.
СreateView
# models.py
from django.db import models
class Skill(models.Model):
name = models.CharField(max_length=20)
class Meta:
verbose_name = "Навык"
verbose_name_plural = "Навыки"
UpdateView
#
# views.py
@method_decorator(csrf_exempt, name='dispatch')
class VacancyUpdateView(UpdateView):
model = Vacancy
fields = ["slug", "text", "status", "skills"]
def post(self, request, *args, **kwargs):
super().post(request, *args, **kwargs)
vacancy_data = json.loads(request.body)
self.object.slug = vacancy_data["slug"]
self.object.text = vacancy_data["text"]
self.object.status = vacancy_data["status"]
try:
self.object.full_clean()
except ValidationError as e:
return JsonResponse(e.message_dict, status=422)
self.object.save()
return JsonResponse({
"id": self.object.id,
"user_id": self.object.user_id,
"slug": self.object.slug,
"text": self.object.text,
"status": self.object.status,
"skills": self.object.skills,
"created": self.object.created,
}) виртуальное окружение
DeleteView
# views.py
@method_decorator(csrf_exempt, name='dispatch')
class VacancyDeleteView(DeleteView):
model = Vacancy
success_url = "/"
def delete(self, request, *args, **kwargs):
super().delete(request, *args, **kwargs)
return JsonResponse({"status": "ok"}, status=200)
Обработка DoesNotExists
DoesNotExists — это ошибка, которая возникает в Django, когда искомая нами запись не существует. Обработка этой ошибки — достаточно типичная и рутинная работа в веб-разработке. Как и для всего рутинного, в Django есть готовые решения: - get_or_create — этот метод возвращает нам запись либо же создает ее, если возникла ошибка. - update_or_create — этот метод обновляет запись в БД или же создает новую, если по указанным параметрам ничего не было найдено. - get_object_or_404 — этот метод возвращает запись или «кидает» ошибку 404.
Например, вот так мы с вами переписали в уроке работу со skill:
for skill in vacancy_data["skills"]:
skill_obj, _ = Skill.objects.get_or_create(name=skill)
self.object.skills.add(skill_obj)
Оглавление
Сортировка
Для сортировки в Django ORM используется метод order_by. Он принимает параметром названия столбцов через запятую. Если необходимо сортировать по убыванию, просто поставь перед название столбца “-”.
### По умолчанию Так же в Django есть возможность настроить сортировку по умолчанию: эта сортировка будет применяться ко всем запросам написанным для данной модели. Настраивается такая сортировка через параметр `ordering` класса `Meta`
# views.py
self.object_list = self.object_list.order_by('name')
# models.py
class Vacancy(models.Model):
# YOUR CODE HERE
class Meta:
ordering = ['name']
Limit и offset
SQL выражения limit и offset в Django ORM реализованы с помощью slice:
# views.py
# вот так можно вытащить первые 10 записей
self.object_list = Vacancy.objects.all()[:10]
# а вот так 5 записей начиная со 2
self.object_list = Vacancy.objects.all()[2:5]
Пагинация
Настроить пагинацию самостоятельно можно в 2 простых шага: 1. Добавляем в настройки проекта константу с количеством записей на странице
Добавляем в отображение списка обработку страниц
# settings.py
TOTAL_ON_PAGE = 10
# views.py
class VacancyListView(ListView):
model = Vacancy
def get(self, request, *args, **kwargs):
# YOUR CODE HERE
# считаем сколько всего записей в таблице
total_vacancies = self.object_list.count()
# сохраняем номер страницы, который от нас требуется
page = int(request.GET.get("page", 0))
# высчитываем с какого по какой объект нужно взять на страницу
offset = page * settings.TOTAL_ON_PAGE
if offset > total_vacancies:
self.object_list = []
elif offset:
self.object_list = self.object_list[offset:settings.TOTAL_ON_PAGE]
else:
self.object_list = self.object_list[:settings.TOTAL_ON_PAGE]
vacancies = []
for vacancy in self.object_list:
vacancies.append({
"id": vacancy.id,
"name": vacancy.name,
"text": vacancy.text,
})
# добавляем в ответ параметры в общим числом записей и
# количеством записей на странице
response = {
"items": vacancies,
"total": total_vacancies,
"per_page": settings.TOTAL_ON_PAGE,
}
return JsonResponse(response, safe=False)
Группировка
Для группировки в Django есть два метода: `annotate` и `aggregate` ### Annotate Это метод добавляет к итоговой таблице колонку, где для каждой строки будет посчитан результат. Например вот так можно посчитать количество вакансий у каждого юзера:
# views.py
# здесь будет храниться QS, в котором все поля модели User + поле vacancies
users_qs = User.objects.annotate(vacancies=Count('vacancy'))
Aggregate
А этот метод посчитает общее значение по всей таблице и выдаст одну строку. Например вот так можно посчитать максимальную цену книги:
# views.py
# здесь будет храниться словарь вида {'price__max': 81}
max_price = Book.objects.all().aggregate(Max('price'))
Join
По умолчанию Django не делает join, поэтому если вы знаете, что будете обращаться к колонкам связанной модели, необходимо самостоятельно вызвать метод `select_related`, в который передать список моделей. Так же, Django умеет заранее запрашивать данные из моделей со связью m2m, для этого необходимо вызвать метод `prefetch_related` Например вот так мы с вами в уроке подтягивали для вакансии данные об авторе и скилах:
# views.py
self.object_list = self.object_list.select_related('user').prefetch_related('skills')
Оглавление
Зарегистрировать приложение в settings.py
Как вернуть в ListView пагинацию
Как написать RetrieveView по slug
ModelSerializer (для всех полей)
ModelSerializer (для некоторых полей)
ModelSerializer (с исключением полей)
Serializer для связанной модели skills (по id)
Serializer для связанной модели skills (по name)
Serializer для связанной модели skills (через связанную модель)
Глоссарий
**DRF** — фреймворк поверх Django, с помощью которого можно очень удобно писать REST API на Python. **GenericView** — готовые и простые в использовании классы из DRF. Мы применяем их, чтобы написать view для какого-то конкретного URL. **Mixin** — класс-примесь, который реализует один нужный нам метод. **Serializer** — класс, который превращает JSON в Python-объект и, наоборот, Python-объект в JSON.
Как установить DRF
poetry add djangorestframework
Зарегистрировать приложение в settings.py
# settings.py
INSTALLED_APPS = [
# тут стандартные apps из джанго
'rest_framework',
# тут ваши apps
]
Как выбрать Generic View
CreateAPIView => Cоздать запись
ListAPIView => Вытащить все записи
RetrieveAPIView => Вытащить одну запись
DestroyAPIView => Удалить одну запись
UpdateAPIView => Изменить одну запись
ListCreateAPIView => Вытащить все записи и создать
RetrieveUpdateAPIView => Вытащить одну запись и обновить
RetrieveDestroyAPIView => Вытащить одну запись и удалить
RetrieveUpdateDestroyAPIView => Вытащить одну запись, обновить и удалить
Как написать ListView
# views.py
from rest_framework.generics import ListAPIView
class VacancyListView(ListAPIView):
queryset = Vacancy.objects.all()
serializer_class = VacancySerializer
Как вернуть в ListView пагинацию
# settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}
Как написать RetrieveView
# views.py
from django.views.generic import RetrieveAPIView
class VacancyDetailView(RetrieveAPIView):
queryset = Vacancy.objects.all()
serializer_class = VacancySerializer
Как написать RetrieveView по slug
# views.py
from django.views.generic import RetrieveAPIView
class VacancyDetailView(RetrieveAPIView):
queryset = Vacancy.objects.all()
serializer_class = VacancySerializer
lookup_field = 'slug_field'
Как написать UpdateView
# views.py
from django.views.generic import UpdateAPIView
class VacancyUpdateView(UpdateAPIView):
queryset = Vacancy.objects.all()
serializer_class = VacancySerializer
Виды Serializer-классов
### Serializer для простого Python-объекта Используем, когда данные не привязаны к модели.
# serializers.py
class VacancySerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField(max_length=50)
text = serializers.CharField(max_length=1000)
username = serializers.CharField()
ModelSerializer (для всех полей)
Используем, когда нужны все поля модели.
# serializers.py
class VacancySerializer(serializers.ModelSerializer):
class Meta:
model = Vacancy
fields = '__all__'
ModelSerializer (для некоторых полей)
Используем, когда нужно только несколько определенных полей в модели.
# serializers.py
class VacancySerializer(serializers.ModelSerializer):
class Meta:
model = Vacancy
fields = ['id', 'name', 'text', 'username']
ModelSerializer (с исключением полей)
Используем, когда нужны все поля модели, кроме некоторых.
# serializers.py
class VacancyCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Vacancy
exclude = ["id", "skills"]
Serializer для связанной модели skills (по id)
# serializers.py
class VacancySerializer(serializers.ModelSerializer):
class Meta:
model = Vacancy
fields =['id','name','text','username', 'skills']
Serializer для связанной модели skills (по name)
# serializers.py
class VacancySerializer(serializers.ModelSerializer):
skills = serializers.SlugRelatedField(many=True,
read_only=True,
slug_field='name'
)
class Meta:
model = Vacancy
fields = ['id', 'name', 'text', 'username']
Serializer для связанной модели skills (через связанную модель)
# serializers.py
class SkillsSerializer(serializers.ModelSerializer):
class Meta:
model = Skill
fields = '__all__'
class VacancySerializer(serializers.ModelSerializer):
skills = SkillsSerializer(many=True)
class Meta:
model = Vacancy
fields = ['id', 'name', 'text', 'username', 'skills']
Оглавление
Глоссарий
**ViewSet** — класс, который содержит в себе все базовые API-методы для одной модели. **Router** — класс, который помогает превратить ViewSet в набор URL. **Lookup fields** — ключевые слова в Django ORM для формирования WHERE-блока SQL-запроса. **Q-запросы** — запросы с использованием класса Q(), пример для использования ИЛИ в WHERE-блоке SQL-запроса. **F-запросы** — запросы с использованием класса F() для обращения к значению текущего столбца.
ViewSets
Как это подключить, чтобы заработало?
# views.py
# from rest_framework.viewsets import <нужный вам viewset>
from rest_framework.viewsets import ModelViewSet
Разновидности ViewSets в DRF
Класс, в котором нет готовых action-методов (самый базовый) => ViewSet
Класс, в котором есть несколько полезных готовых методов, но нет action => GenericViewSet
Штука, которая содержит в себе базовые методы работы с модельками => ModelsViewSet
Класс, который годится только для чтения данных => GenericViewSet ReadOnlyViewSet
Как написать ViewSet
# views.py
from rest_framework import viewsets
class UserViewSet(viewsets.ViewSet):
def list(self, request):
queryset = User.objects.all()
serializer = UserSerializer(queryset, many=True)
return Response(serializer.data)
def create(self, request):
serializer = UserSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(serializer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def retrieve(self, request, pk=None):
queryset = User.objects.all()
user = get_object_or_404(queryset, pk=pk)
serializer = UserSerializer(user)
return Response(serializer.data)
def update(self, request, pk=None):
queryset = User.objects.all()
user = get_object_or_404(queryset, pk=pk)
serializer = UserSerializer(user, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
serializer.save(serializer)
return Response(serializer.data)
def partial_update(self, request, pk=None):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
def destroy(self, request, pk=None):
queryset = User.objects.all()
user = get_object_or_404(queryset, pk=pk)
user.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
Как написать GenericViewSet
# views.py
from rest_framework import viewsets
class ItemViewSet(viewsets.GenericViewSet):
serializer_class = ItemSerializer
queryset = Item.objects.all()
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(serializer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def list(self, request):
serializer = self.get_serializer(self.get_queryset(), many=True)
return self.get_paginated_response(self.paginate_queryset(serializer.data))
def retrieve(self, request, pk):
item = self.get_object()
serializer = self.get_serializer(item)
return Response(serializer.data)
def destroy(self, request):
item = self.get_object()
item.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
Как написать ModelViewset
# views.py
class SkillViewSet(ModelViewSet):
queryset = Skill.objects.all()
serializer_class = SkillSerializer
Стандартные методы в ViewSet
.list() => Выводит список всех записей из модели
.retrieve() => Выводит одну запись из модели по ключу
.create() => Создает запись
.update() => Обновляет все поля записи
.partial_update() => Обновляет только переданные поля записи
.destroy() => Удаляет запись по ключу
Как настроить URL для ViewSet
# urls.py
from rest_framework import routers
router = routers.SimpleRouter()
router.register('skill', SkillViewSet)
urlpatterns = [
# тут все остальные маршруты
]
urlpatterns += router.urls
Поиск с помощью ORM
Lookup для связей
# views.py
from django.db.models import Q
class VacancyListView(ListAPIView):
queryset = Vacancy.objects.all()
serializer_class = VacancySerializer
def get(self, request, *args, **kwargs):
skill_name = request.GET.getlist("skill", None)
self.queryset = self.queryset.filter(
skills__name__contains=skill_name
)
....
Q-запросы
# views.py
from django.db.models import Q
class VacancyListView(ListAPIView):
queryset = Vacancy.objects.all()
serializer_class = VacancySerializer
def get(self, request, *args, **kwargs):
skills = request.GET.getlist("skill", None)
skills_q = None
for skill_name in skills:
if not skills_q:
skills_q = Q(skills__name__contains=skill_name)
else:
skills_q |= Q(skills__name__contains=skill_name)
if skills_q:
self.queryset = self.queryset.filter(skills_q)
...
F-запросы
# views.py
from django.db.models import F
class ReporterUpdateView(UpdateAPIView):
queryset = Reporter.objects.all()
serializer_class = ReporterSerializer
def patch(self, request, *args, **kwargs):
reporter = Reporters.objects.get(name='Tintin')
reporter.stories_filed = F('stories_filed') + 1
reporter.save()
...
Цепочки запросов
# views.py
class VacancyLikeView(ListModelMixin, UpdateAPIView):
queryset = Vacancy.objects.all()
serializer_class = VacancySerializer
def put(self, request, *args, **kwargs):
Vacancy.objects.filter(pk__in=request.data).update(likes=F('likes') + 1)
...
Оглавление
Как создать своего пользователя
# Как отправить запрос с токеном ## AuthToken
Как создать своего пользователя
**Шаг 0 (если уже были применены миграции)** Отменяем миграции для auth.
./manage.py migrate auth zero
Наследуемся от AbstactUser (не забудьте переписать save!).
# models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
MALE = "m"
FEMALE = "f"
SEX = [(MALE, "Male"), (FEMALE, "Female")]
sex = models.CharField(max_length=1, choices=SEX)
NOTE: не забудьте, что при создании такого пользователя, необходимо будет вызвать метод set_password. Например, в сериализаторе:
# serializer.py
class UserCreateSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = '__all__'
def create(self, validated_data):
user = User.objects.create(**validated_data)
user.set_password(validated_data["password"])
user.save()
return user
Переопределяем модель user для всего приложения.
# settings.py
AUTH_USER_MODEL = 'authentification.User'
Применяем миграции.
./manage.py makemigrations
./manage.py migrate
Как сделать регистрацию
Регистрация — это обычное сохранение модели пользователя.
# serializers.py
from rest_framework import serializers
from authentication.models import User
class UserCreateSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = '__all__'
# urls.py
from django.urls import path
from authentication.views import UserCreateView
urlpatterns = [
path('create/', UserCreateView.as_view()),
]
# views.py
from rest_framework.generics import CreateAPIView
from authentification.models import User
from authentification.serializers import UserCreateSerializer
class UserCreateView(CreateAPIView):
model = User
serializer_class = UserCreateSerializer
Авторизация по токену в DRF
Добавляем приложение AuthToken.
# settings.py
INSTALLED_APPS = [
'rest_framework',
'rest_framework.authtoken',
]
Далее накатываем миграции. Настраиваем приложение на проверку токена.
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
]
}
Добавляем ручки для авторизации.
# urls.py
from rest_framework.authtoken import views
urlpatterns = [
...,
path('login/', views.obtain_auth_token)
]
Реализация «выхода»
# views.py
class Logout(APIView):
def get(self, request, format=None):
request.user.auth_token.delete()
return Response(status=status.HTTP_200_OK)
Авторизация по JWT
Устанавливаем пакет djangorestframework-simplejwt.
poetry add djangorestframework-simplejwt
Добавляем библиотеку в приложение и настраиваем его на проверку JWT.
# settings.py
INSTALLED_APPS = [
'rest_framework_simplejwt',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
]
}
Добавляем ручки.
# urls.py
urlpatterns = [
...,
path('token/', TokenObtainPairView.as_view()),
path('token/refresh/', TokenRefreshView.as_view()),
]
# Как отправить запрос с токеном ## AuthToken
В заголовок запроса добавить header.
Authorization: Token <your_token>
JWT
В заголовок запроса добавить header.
Authorization: Bearer <your_token>
Оглавление
Как защитить view авторизацией
Как добавить пользователю роль
Глоссарий
Permission — класс, который отвечает за проверку доступа пользователя к какой-то функциональности, например, чтению сообщений или редактированию пользователей.
Как защитить view авторизацией
Заполнить во view атрибут permission_classes.
# views.py
from rest_framework.permissions import IsAuthenticated
class VacancyDetailView(RetrieveAPIView):
queryset = Vacancy.objects.all()
serializer_class = VacancySerializer
permission_classes = [IsAuthenticated]
Как добавить пользователю роль
В Django есть своя ролевая модель, реализованная через Groups, но ею редко пользуются, предпочитая писать свои роли.
# models.py
class User(AbstractUser):
UNKNOWN = "unknown"
EMPLOYEE = "employee"
HR = "hr"
ROLE = [(UNKNOWN, "unknown"), (EMPLOYEE, "employee"), (HR, "hr")]
role = models.CharField(max_length=8, choices=ROLE, default=UNKNOWN)
Как написать свой Permission
- Отнаследоваться от `permissions.BasePermission`. - Переопределить `has_permission` и вернуть значение boolean.
# permissions.py
from rest_framework import permissions
from authentification.models import User
class VacancyCreatePermission(permissions.BasePermission):
message = 'Adding vacancies for non hr user not allowed.'
def has_permission(self, request, view):
if request.user.role != User.HR:
return False
return True
Оглавление
Валидация модели
В Django встроено два варианта валидации: с помощью атрибутов и с помощью дополнительных классов. Ниже мы рассмотрим пример каждой из них.
Доступные атрибуты
Мы подготовили для вас таблицу-шпаргалку всех доступных атрибутов полей моделей.
Вы уже умеете применять этот тип валидаторов. Например, мы часто использовали его, когда ограничивали количество символов в текстовом поле модели.
# models.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=100)
slug = models.SlugField(max_length=20)
text = models.CharField(max_length=1000)
Дополнительные классы
Если среди атрибутов не нашлось ничего, что подходило бы под задачи, можно посмотреть классы валидаторов вот здесь: [https://docs.djangoproject.com/en/4.0/ref/validators/#built-in-validators](https://docs.djangoproject.com/en/4.0/ref/validators/#built-in-validators). Используются они следующим образом:
#models.py
from django.core.validators import MinValueValidator
class Vacancy(models.Model):
min_experience = models.IntegerField(null=True, validators=[MinValueValidator(0)])
Кастомная
Если совсем ничего не подошло, можно написать собственный валидатор.
Валидатор – это метод, принимающий value — значение поля, которое мы хотим сохранить, и возвращающий ValidationError в случае ошибки.
Подключается валидатор так же, как и классы валидаторов выше: через атрибут validators.
# models.py
from datetime import date
def check_date_not_past(value: date):
if value < date.today():
raise ValidationError(
'%(value)s is in the past',
params={'value': value},
)
class Vacancy(models.Model):
created = models.DateField(auto_now_add=True, validators=[check_date_not_past])
Валидация в сериализаторах
## Встроенная Поля валидаторов, как и поля моделей, имеют собственные атрибуты для проверки данных.
# serializers.py
class VacancySerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField(max_length=50)
text = serializers.CharField(max_length=1000)
username = serializers.CharField()
Полный список атрибутов можно посмотреть здесь: [https://www.django-rest-framework.org/api-guide/fields/#core-arguments](https://www.django-rest-framework.org/api-guide/fields/#core-arguments). Однако в случае с валидаторами моделей все атрибуты наследуются из модели, поэтому переписывать их не имеет смысла. Как и у моделей, у валидаторов есть дополнительные классы для проверки данных.
# serializers.py
class VacancyCreateSerializer(serializers.ModelSerializer):
slug = serializers.SlugField(
validators=[UniqueValidator(queryset=Vacancy.objects.all())]
)
class Meta:
model = Vacancy
exclude = ["id"]
Кастомная
Если встроенных возможностей не хватило, можно написать свою проверку.
Проверка через функцию
Валидаторы поддерживают методы проверок, аналогичные тем, что мы писали в моделях.
# serializers.py
def even_number(value):
if value % 2 != 0:
raise serializers.ValidationError('This field must be an even number.')
class VacancySerializer(serializers.Serializer):
id = serializers.IntegerField(validators=[even_number])
name = serializers.CharField(max_length=50)
text = serializers.CharField(max_length=1000)
username = serializers.CharField()
Проверка через класс
Также в валидаторах появляется возможность писать проверки классами: для этого нужно определить метод __call__. Он должен принимать параметр value и возвращать serializers.ValidationError в случае ошибки (как и в методе-валидаторе). Помимо метода __call__, можно еще определить __init__, что позволяет нам передавать в валидатор дополнительные параметры.
# serializers.py
class NotInStatusValidator:
def __init__(self, statuses):
if not isinstance(statuses, list):
statuses = [statuses]
self.statuses = statuses
def __call__(self, value):
if value in self.statuses:
raise serializers.ValidationError("Incorrect status")
class VacancyCreateSerializer(serializers.ModelSerializer):
slug = serializers.SlugField(
validators=[UniqueValidator(queryset=Vacancy.objects.all(), lookup='contains')]
)
status = serializers.CharField(validators=[NotInStatusValidator(["open", "closed"])])
class Meta:
model = Vacancy
exclude = ["id"]
Оглавление
Как отправить запрос на ручку?
Как создавать пользователей в тестах?
Глоссарий
Фабрика — класс, на основе которого будут генерироваться и предзаполняться данными модели в тестах. Фикстуры — блок кода, обернутый в декоратор `@pytest.fixture()`, который затем можно использовать как аргумент для функции-теста. Фабрики также могут использоваться в качестве фикстур. pytest-django — python-пакет для работы с pytest в Django. pytest-factoryboy — python-пакет для создания фабрик в Django. Параметризация — использование переменных в коде тестов вместо конкретных значений.
Настройка окружения
Прежде чем начать тестировать Django-приложение, необходимо поставить пакет для тестирования.
poetry add pytest-django
Затем нужно настроить pytest, добавив файл pytest.ini с содержимым, описанным ниже, в корень проекта.
# pytests.ini
[pytest]
DJANGO_SETTINGS_MODULE = hanting.settings
Как отправить запрос на ручку?
Для того чтобы имитировать запросы пользователей в pytest-django, есть специальная фикстура client, которая умеет делать запросы.
# simple_test.py
def test_root_not_found(client):
response = client.get("/")
assert response.status_code == 404
Как написать свою фикстуру?
Чтобы написать свою фикстуру, нужно создать функцию в файле fixtures.py, добавить ей декоратор @pytest.fixture() и подключить ее в conftest.py, как показано ниже.
# fixtures.py
import pytest
@pytest.fixture()
def my_fixture():
return 1
# conftest.py
pytest_plugins = "tests.fixtures"
# test.py
def test_fixture(my_fixture):
assert my_fixture == 1
А еще можно писать фикстуры сразу в conftest.py.
# conftest.py
import pytest
@pytest.fixture()
def my_fixture():
return 1
# test.py
def test_fixture(my_fixture):
assert my_fixture == 1
Как работать с базой данных?
Для того чтобы в тесте сохранять данные в БД и проверять, сохранились они или нет, необходимо добавить декоратор @pytest.mark.django_db. Ниже — пример теста на сохранение данных в БД.
# vacancy_create_test.py
from datetime import datetime
import pytest
from vacancies.models import Vacancy
@pytest.mark.django_db
def test_vacancy_detail(client):
vacancy = Vacancy.objects.create(
slug="123",
name="123"
)
expected_response = {
"id": vacancy.pk,
"slug": "123",
"name": "123"
}
response = client.get(f"/vacancy/{vacancy.pk}/")
assert response.status_code == 200
assert response.data == expected_response
Фабрики
Для того чтобы не писать сохранение моделей каждый раз, используются фабрики моделей. Для их созданий потребуется дополнительный пакет.
poetry add pytest-factoryboy
Как создать фабрику?
Чтобы создать фабрику, нужно унаследовать класс factory.django.DjangoModelFactory, прописать, для какой модели эта фабрика и какие поля мы хотим определить по умолчанию.
# factories.py
import factory
from vacancies.models import Vacancy
class VacancyFactory(factory.django.DjangoModelFactory):
class Meta:
model = Vacancy
slug = "test"
name = "test"
text = "test text"
Как пользоваться фабрикой?
Можно использовать фабрику как фикстуру. Для этого необходимо подключить ее в conftest, как показано ниже.
# conftest.py
from pytest_factoryboy import register
from tests.factories import SkillFactory
# Factories
register(VacancyFactory)
Использование фикстурой представлено ниже.
from datetime import datetime
import pytest
@pytest.mark.django_db
def test_vacancy_detail(client, vacancy):
expected_response = {
"id": vacancy.pk,
"created": datetime.now().strftime("%Y-%m-%d"),
"skills": [],
"slug": "test",
"name": "test",
"text": "test text",
"status": "draft",
"is_archived": False,
"min_experience": None,
"likes": 0,
"user": None
}
response = client.get(f"/vacancy/{vacancy.pk}/")
assert response.status_code == 200
assert response.data == expected_response
Еще можно использовать фабрику напрямую, как мы это делали в тесте на список.
# vacancy_list_test.py
import pytest
from tests.factories import VacancyFactory
from vacancies.serializers import VacancySerializer
@pytest.mark.django_db
def test_list_view(client):
vacancies = VacancyFactory.create_batch(10)
response = client.get("/vacancy/")
assert response.status_code == 200
assert response.data == VacancySerializer(vacancies, many=True).data
Как создавать пользователей в тестах?
Чтобы создать пользователя в тестах, нам понадобится фикстура django_user_model. Ниже увидим пример из урока, где мы создавали пользователя с ролью «hr» и получали его токен.
@pytest.fixture()
@pytest.mark.django_db
def hr_token(client, django_user_model):
username = "hr"
password = "123qwe"
django_user_model.objects.create_user(
username=username, password=password, role="hr")
response = client.post(
"/auth/login/",
{"username": username, "password": password},
format='json'
)
return response.data["token"]
# vacancy_create_test.py
@pytest.mark.django_db
def test_create_vacancy(client, hr_token):
expected_response = {
"slug": "123",
"name": "123"
}
Skill.objects.create(name="test")
response = client.post("/vacancy/create/", data={
"slug": "123",
"name": "123"
}, HTTP_AUTHORIZATION="Token " + hr_token)
assert response.status_code == 201
assert response.data == expected_response
@pytest.mark.parametrize
@pytest.mark.parametrize — декоратор, который помогает сократить количество кода в тестах. Он используется тогда, когда у нас есть несколько тестов с одинаковым кодом, который отличается только значением нескольких параметров. Представленные ниже куски кода идентичны и показывают, как работает этот декоратор.
@pytest.mark.parametrize("test_input,expected", [("1+1", 2), ("1+2", 3)])
def test_eval(test_input, expected):
assert eval(test_input) == expected
def test_eval_1():
assert eval(“1+1”) == 2
def test_eval_2():
assert eval(“1+2”) == 3
Структура проекта с тестами
Для более легкого понимания также прикладываем пример полной структуры дерева проекта с тестами.
Оглавление
## Базовая настройка документации ### Установка пакетов
## Базовая настройка документации ### Установка пакетов
Есть два самых популярных приложения для оформления документации: drf-yasg и drf-spectacular. Оба поддерживают Swagger и ReDoc, однако drf-spectacular более новый и поддерживает формат OpenAPI 3.0, поэтому в нашем курсе рассматриваем его. Устанавливается он как обычный python-пакет.
poetry add drf-spectacular
После установки нужно добавить приложение в INSTALLED_APPS в файл settings.py и настроить константу DEFAULT_SCHEMA_CLASS, как показано ниже:
# settings.py
INSTALLED_APPS = [
# YOUR APPS
'drf_spectacular',
]
# YOUR SETTINGS
REST_FRAMEWORK = {
# OTHER REST FRAMEWORK SETTINGS
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
Больше ничего делать не Нужно. Но, если хочется, можно еще переопределить базовые заголовки нашей документации.
# settings.py
SPECTACULAR_SETTINGS = {
'TITLE': 'Hunting API',
'DESCRIPTION': 'Awesome hunting project',
'VERSION': '1.0.0',
}
Настройка URL
Для того чтобы настроить URL документации, нужно прописать не один, а два URL: - Первый (в нашем примере ’api/schema/’) — URL, по которому будет отдаваться схема OpenAPI. - Второй (в нашем примере ‘api/schema/swagger-ui/’) — URL, по которому будет отдаваться сама документация. Обратите также внимание на параметр `url_name='schema'` в вызове `SpectacularSwaggerView.as_view`. Он должен совпадать с именем URL схемы OpenAPI.
# urls.py
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
urlpatterns = [
# YOUR ROUTES HERE
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/schema/swagger-ui/',
SpectacularSwaggerView.as_view(url_name='schema'),
name='swagger-ui'),
]
Кастомизация
### Кастомизация View Для переопределения чего-то в сгенерированной по методу View документации используется декоратор `@extend_schema`. Он умеет переопределять практически всё. Из самого важного: - request определяет Serializer для входящих данных (если автоматически определился не тот, что нужен). - responses определяет список возможных Serializer для ответа. - description определяет описание метода в документации. - summary определяет короткое описание метода. - deprecated отмечает, что метод устарел и будет удалён в ближайшее время. Пример использования:
# views.py
class VacancyListView(ListAPIView):
queryset = Vacancy.objects.all()
serializer_class = VacancySerializer
@extend_schema(
description="Retrieve vacancy list",
summary="Vacancy list"
)
def get(self, request, *args, **kwargs):
# Method code here
Кастомизация ViewSet
Для ViewSet используется отдельный декоратор `@extend_schema_view`, т. к. обычно мы обычно не пишем методы во ViewSet. В этом декораторе объявлены атрибуты по названиям методов, которые есть у ViewSet. Для переопределения документации в атрибут с соответствующим названием метода необходимо передать результат вызова `@extend_schema`. Например, ниже мы переопределяем документацию для метода `list`:
# views.py
@extend_schema_view(
list=extend_schema(description="Retrieve skills", summary="Skills list"),
)
class SkillViewSet(ModelViewSet):
queryset = Skill.objects.all()
serializer_class = SkillSerializer