текст перекладу
Одна з чудових особливостей DRF (Django REST Framework) — це Viewset. Вона дозволяє розробнику визначати набір вбудованих обробників дій для ресурсу. Після реєстрації з використанням стандартного маршрутизатора DRF, вона може надати стандартний набір операцій CRUD для ресурсу.
Ця стаття пояснює, як додати підтримку для користувацької дії під назвою bulk-destroy
, яка дозволяє виконати видалення кількох об'єктів одночасно за допомогою демонстраційного додатку.
Досвід роботи з django та DRF буде корисний для кращого розуміння статті.
Завдання
В одному з наших проєктів ми активно використовували viewsets (ModelViewSet) в нашому Django додатку для виконання простих операцій CRUD з ресурсами. Було кілька ресурсів, де автентифіковані та авторизовані користувачі повинні були виконувати операцію видалення кількох записів/об'єктів одночасно. Якщо б це стосувалося одного ресурсу, ми могли б використовувати спеціальну реалізацію виду з класом APIView
або інший спосіб. Однак додаток зростав, і нам потрібно було знайти загальний підхід для вирішення цього завдання. Зрештою, ми вирішили створити дію bulk-destroy
, використовуючи маршрут для списку, щоб її можна було викликати за допомогою HTTP методу DELETE, подібно до того, як метод list
викликається за допомогою методу GET. Звісно, щоб виконати bulk-destroy
, потрібно передати ідентифікатори об'єктів, які потрібно видалити, щоб додаток міг перевірити права користувача та успішно видалити запитувані об'єкти.
Фото від Yoko Correia Nishimiya на Unsplash
Демонстраційний додаток
Ми розглянемо демонстраційний додаток, який використовує дію bulk-destroy
. Повний код додатку доступний тут.
В цьому демонстраційному додатку я створив простий ресурс Note
, для якого Django модель виглядає наступним чином:
from django.conf import settings
from django.db import models
class Note(models.Model):
title = models.CharField(max_length=1024, blank=True, null=True)
body = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True, editable=False)
modified_at = models.DateTimeField(auto_now=True, editable=False)
owner = models.ForeignKey(settings.AUTH_USER_MODEL,
null=True, editable=False, db_index=True,
on_delete=models.SET_NULL, related_name="note_owner")
class Meta:
db_table = "note"
def __str__(self):
return f"(pk: {self.pk}, title: {self.title})"
Ідея полягає в тому, що користувач може володіти кількома нотатками з заданими title
та body
. Модель користувача є стандартною моделлю Django auth-user.
Тепер, щоб підтримати дію bulk-destroy
у вигляді HTTP REST API з методом DELETE
, нам потрібно оновити налаштування маршрутизатора в DRF, який надає URL-адреси клієнту.
# router.py
from rest_framework.routers import DefaultRouter
from restapis import views
class CustomAPIRouter(DefaultRouter):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# за визначенням DRF, це може змінюватися залежно від версії DRF
list_route = self.routes[0]
list_route.mapping.update({
"delete": "bulk_destroy"
})
# Маршрутизатори забезпечують легкий спосіб автоматично визначати конфігурацію URL
APIRouter = CustomAPIRouter()
# маршрутизатори для нотаток API
APIRouter.register(r'notes', views.NoteViewSet, basename="notes")
Як видно з наведеного фрагмента коду, ми перевизначили маршрутизатор, успадкувавши клас DefaultRouter
від DRF. Ми витягли маршрут для списку та оновили його маппінг, додавши HTTP метод delete
до методу bulk_destroy
виду. Іншими словами, тепер viewset може реалізувати метод bulk_destroy
для обробки запиту на видалення кількох об'єктів для цього ресурсу.
Клас
DefaultRouter
успадковує класSimpleRouter
.
текст перекладу
Подивіться на код класуSimpleRouter
тут, щоб зрозуміти, чому ми взяли перший елемент з маршрутів, щоб вважати його маршрутом для списку. Більше деталей про кастомні маршрути тут.
Для підтримки загальної поведінки дії bulk destroy в viewset на основі моделі, ми можемо ввести міксін під назвою BulkDestroyModelMixin
. Насправді, ModelViewSet — це просто група міксінів для підтримки базових операцій CRUD для ресурсу. Реалізацію можна побачити тут.
# serializers
from rest_framework import serializers
class BulkDestroyInstanceSerializer(serializers.Serializer):
"""
ids усіх об'єктів, що мають бути знищені
"""
ids = serializers.ListField(
child=serializers.IntegerField(min_value=1),
write_only=True
)
# mixins.py
import logging
from rest_framework import status
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError, PermissionDenied
from ..serializers import BulkDestroyInstanceSerializer
log = logging.getLogger(__name__)
class BulkDestroyModelMixin:
"""
BulkDestroyModelMixin надає гнучкість для видалення
кількох ресурсів через кінцеву точку списку за допомогою методу delete.
"""
# на випадок, якщо у viewset не визначена кількість для bulk-destroy
__DEFAULT_BULK_DESTROY_COUNT = 5
# максимальна кількість для безпечного видалення
__MAX_BULK_DESTROY_COUNT = 10
def bulk_destroy(self, request, *args, **kwargs):
# якщо не дозволено, то повернути помилку
if not getattr(self, "allow_bulk_destroy_method", False):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
serializer = BulkDestroyInstanceSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# отримуємо ids з перевірених даних серіалізатора
destroy_instance_id_list = serializer.validated_data.get("ids", [])
# перевірка: хоча б один id має бути наданий
if len(destroy_instance_id_list) == 0:
raise ValidationError("необхідно вказати хоча б один id об'єкта для виконання операції пакетного видалення")
# максимальна дозволена кількість для bulk destroy
bulk_destroy_count = getattr(self, "bulk_destroy_count", 0) or self.__DEFAULT_BULK_DESTROY_COUNT
bulk_destroy_count = min(bulk_destroy_count, self.__MAX_BULK_DESTROY_COUNT)
if len(destroy_instance_id_list) > bulk_destroy_count:
raise PermissionDenied(f"Ви не маєте дозволу видаляти більше ніж {bulk_destroy_count} об'єктів")
# можуть бути валідні id, до яких запитуваний користувач не має доступу
# тому повертаємо помилку доступу
valid_bulk_instance_qs = self.get_queryset().filter(pk__in=destroy_instance_id_list)
if len(destroy_instance_id_list) != valid_bulk_instance_qs.count():
raise PermissionDenied(detail="Ви не маєте дозволу видаляти один або кілька об'єктів")
for permission in self.get_permissions():
if (
hasattr(permission, "has_bulk_destroy_permission") and
not permission.has_bulk_destroy_permission(request, self, valid_bulk_instance_qs)
):
raise PermissionDenied(detail="Ви не маєте дозволу видаляти один або кілька об'єктів")
# після всіх перевірок виконуємо пакетне видалення
self.perform_bulk_destroy(valid_bulk_instance_qs)
# повертаємо відповідь
return Response(status=status.HTTP_204_NO_CONTENT)
def perform_bulk_destroy(self, bulk_instance_qs):
log.info(f"bulk destroy, instance list: {list(bulk_instance_qs)}")
# використовуємо відфільтрований queryset для видалення
bulk_instance_qs.delete()
Цей міксін реалізує метод виду для дії bulk_destroy
, яку ми оновили в попередньому кастомному маршруті.
текст перекладу
Простими словами, цей міксін приймає ids
, список ідентифікаторів об'єктів, що мають бути видалені, проводить перевірку на валідність, перевірку прав доступу і, якщо дозволено, видаляє запитувані об'єкти.
Тепер нам потрібно успадкувати цей міксін BulkDestroyModelMixin
в кінцевому viewset. У нашому прикладі ресурсу Note
це можна використати так:
# views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from ..models import Note
from ..serializers import NoteSerializer
from ..permissions import NotePermission
from .mixins import BulkDestroyModelMixin
class NoteViewSet(BulkDestroyModelMixin, viewsets.ModelViewSet):
"""
NoteViewSet для обробки CRUD операцій з bulk-destroy.
"""
queryset = Note.objects.all()
serializer_class = NoteSerializer
permission_classes = (IsAuthenticated, NotePermission, )
allow_bulk_destroy_method = True
bulk_destroy_count = 5
def get_queryset(self):
return Note.objects.filter(owner=self.request.user)
Зверніть увагу, що міксін також підтримує наступні атрибути, які можна визначити у viewset, якщо це потрібно:
allow_bulk_destroy_method
: якщо viewset дозволяє bulk destroybulk_destroy_count
: кількість об'єктів, які можна видалити за один запит
Також, якщо з якихось причин потрібна додаткова кастомна перевірка доступу для дії пакетного видалення, можна визначити метод has_bulk_destroy_permission
у класі дозволів (permissions) для viewset. Більше деталей про дозволи тут!
Нижче — попередньо записаний відео-демо, яке демонструє дію bulk-destroy для ресурсу note.
Щоб зареєструвати користувачів у django shell:
root@17548402114a:/usr/src/app/mysite# python manage.py shell
Python 3.12.7 (main, Oct 1 2024, 22:28:30) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.models import User
>>> r = User.objects.create(username="[email protected]", email="[email protected]", first_name="Ramesh")
>>> r.set_password("r#2025")
>>> s = User.objects.create(username="[email protected]", email="[email protected]", first_name="Suresh")
>>> s.set_password("s#2025")
Щоб згенерувати токен авторизації для користувачів:
root@17548402114a:/usr/src/app/mysite# python manage.py drf_create_token -r [email protected]
Generated token for user [email protected]
root@17548402114a:/usr/src/app/mysite# python manage.py drf_create_token -r [email protected]
Generated token for user [email protected]
Висновок
Цей підхід до обробки пакетного видалення був дуже корисним у одному з минулих проектів, де додаток django+DRF активно використовував viewsets на основі моделей. Сподіваюся, що це надихне вас на проектування та реалізацію будь-якої кастомної дії в DRF.
Дякую за те, що прочитали статтю!
Перекладено з: Bulk destroy in Django REST Framework (DRF) using ModelViewSet