Skip to content

Commit

Permalink
✨(backend) add bulk delete route
Browse files Browse the repository at this point in the history
Add route to admin api to bulk delete objects
  • Loading branch information
samy-aitouakli-polyconseil committed Jul 12, 2023
1 parent 933aa09 commit 06bf7b9
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to

## Added

- Add admin endpoint to bulk delete objects
- Add admin endpoint to search users
- Add endpoints to get course product relations and courses from organization
- Add `max_validated_orders` field to CourseProductRelation model
Expand Down
143 changes: 143 additions & 0 deletions src/backend/joanie/core/api/admin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"""
Admin API Endpoints
"""
from django.db import IntegrityError

import django_filters.rest_framework
from rest_framework import authentication, mixins, permissions, viewsets
from rest_framework.response import Response

from joanie.core import filters, models, serializers

Expand All @@ -20,6 +23,34 @@ class OrganizationViewSet(viewsets.ModelViewSet):
filter_backends = [django_filters.rest_framework.DjangoFilterBackend]
filterset_class = filters.OrganizationAdminFilterSet

def delete(self, request, *args, **kwargs):
"""
Bulk deletion of Organizations
data = {"id": [<id_0>, <id_1>]}
"""
id_to_delete = request.data.get("id", [])
elements_to_delete = self.queryset.filter(id__in=id_to_delete)
error_elements = []
deleted_elements = []
for element in elements_to_delete.all():
try:
deleted_element = self.serializer_class(element).data
element.delete()
deleted_elements.append(deleted_element)
except IntegrityError as error:
error_elements.append(
{
"organization": self.serializer_class(element).data,
"error": str(error),
}
)
response = {}
if len(deleted_elements) > 0:
response["deleted"] = deleted_elements
if len(error_elements) > 0:
response["error"] = error_elements
return Response(response)


class ProductViewSet(viewsets.ModelViewSet):
"""
Expand All @@ -32,6 +63,34 @@ class ProductViewSet(viewsets.ModelViewSet):
queryset = models.Product.objects.all()
filterset_class = filters.ProductAdminFilterSet

def delete(self, request, *args, **kwargs):
"""
Bulk deletion of Products
data = {"id": [<id_0>, <id_1>]}
"""
id_to_delete = request.data.get("id", [])
elements_to_delete = self.queryset.filter(id__in=id_to_delete)
error_elements = []
deleted_elements = []
for element in elements_to_delete.all():
try:
deleted_element = self.serializer_class(element).data
element.delete()
deleted_elements.append(deleted_element)
except IntegrityError as error:
error_elements.append(
{
"product": self.serializer_class(element).data,
"error": str(error),
}
)
response = {}
if len(deleted_elements) > 0:
response["deleted"] = deleted_elements
if len(error_elements) > 0:
response["error"] = error_elements
return Response(response)


class CourseViewSet(viewsets.ModelViewSet):
"""
Expand All @@ -45,6 +104,34 @@ class CourseViewSet(viewsets.ModelViewSet):
filter_backends = [django_filters.rest_framework.DjangoFilterBackend]
filterset_class = filters.CourseAdminFilterSet

def delete(self, request, *args, **kwargs):
"""
Bulk deletion of Courses
data = {"id": [<id_0>, <id_1>]}
"""
id_to_delete = request.data.get("id", [])
elements_to_delete = self.queryset.filter(id__in=id_to_delete)
error_elements = []
deleted_elements = []
for element in elements_to_delete.all():
try:
deleted_element = self.serializer_class(element).data
element.delete()
deleted_elements.append(deleted_element)
except IntegrityError as error:
error_elements.append(
{
"course": self.serializer_class(element).data,
"error": str(error),
}
)
response = {}
if len(deleted_elements) > 0:
response["deleted"] = deleted_elements
if len(error_elements) > 0:
response["error"] = error_elements
return Response(response)


class CourseRunViewSet(viewsets.ModelViewSet):
"""
Expand All @@ -58,6 +145,34 @@ class CourseRunViewSet(viewsets.ModelViewSet):
filter_backends = [django_filters.rest_framework.DjangoFilterBackend]
filterset_class = filters.CourseRunAdminFilterSet

def delete(self, request, *args, **kwargs):
"""
Bulk deletion of CourseRuns
data = {"id": [<id_0>, <id_1>]}
"""
id_to_delete = request.data.get("id", [])
elements_to_delete = self.queryset.filter(id__in=id_to_delete)
error_elements = []
deleted_elements = []
for element in elements_to_delete.all():
try:
deleted_element = self.serializer_class(element).data
element.delete()
deleted_elements.append(deleted_element)
except IntegrityError as error:
error_elements.append(
{
"course_run": self.serializer_class(element).data,
"error": str(error),
}
)
response = {}
if len(deleted_elements) > 0:
response["deleted"] = deleted_elements
if len(error_elements) > 0:
response["error"] = error_elements
return Response(response)


class CertificateDefinitionViewSet(viewsets.ModelViewSet):
"""
Expand All @@ -71,6 +186,34 @@ class CertificateDefinitionViewSet(viewsets.ModelViewSet):
filter_backends = [django_filters.rest_framework.DjangoFilterBackend]
filterset_class = filters.CertificateDefinitionAdminFilterSet

def delete(self, request, *args, **kwargs):
"""
Bulk deletion of CertificateDefinitions
data = {"id": [<id_0>, <id_1>]}
"""
id_to_delete = request.data.get("id", [])
elements_to_delete = self.queryset.filter(id__in=id_to_delete)
error_elements = []
deleted_elements = []
for element in elements_to_delete.all():
try:
deleted_element = self.serializer_class(element).data
element.delete()
deleted_elements.append(deleted_element)
except IntegrityError as error:
error_elements.append(
{
"certificate_definition": self.serializer_class(element).data,
"error": str(error),
}
)
response = {}
if len(deleted_elements) > 0:
response["deleted"] = deleted_elements
if len(error_elements) > 0:
response["error"] = error_elements
return Response(response)


class UserViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from django.test import TestCase

from joanie.core import factories
from joanie.core import factories, models


class CertificateDefinitionAdminApiTest(TestCase):
Expand Down Expand Up @@ -250,3 +250,28 @@ def test_admin_api_certificate_definition_delete(self):
)

self.assertEqual(response.status_code, 204)

def test_admin_api_certificate_definition_bulk_delete(self):
"""
Staff user should be able to delete multiple certificate_definitions.
"""
admin = factories.UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=admin.username, password="password")
certificate_definitions = factories.CertificateDefinitionFactory.create_batch(3)
factories.CertificateDefinitionFactory()
data = {
"id": [
str(certificate_definition.id)
for certificate_definition in certificate_definitions
]
}
response = self.client.delete(
"/api/v1.0/admin/certificate-definitions/",
data=data,
content_type="application/json",
)
content = response.json()
self.assertEqual(response.status_code, 200)
self.assertEqual(models.CertificateDefinition.objects.count(), 1)
self.assertEqual(len(content["deleted"]), 3)
self.assertFalse("error" in content)
20 changes: 19 additions & 1 deletion src/backend/joanie/tests/core/test_api_admin_course_runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from django.test import TestCase

from joanie.core import factories
from joanie.core import factories, models


class CourseRunAdminApiTest(TestCase):
Expand Down Expand Up @@ -233,3 +233,21 @@ def test_admin_api_course_runs_delete(self):
response = self.client.delete(f"/api/v1.0/admin/course-runs/{course_run.id}/")

self.assertEqual(response.status_code, 204)

def test_admin_api_course_runs_bulk_delete(self):
"""
Staff user should be able to delete multiple course runs.
"""
admin = factories.UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=admin.username, password="password")
course_runs = factories.CourseRunFactory.create_batch(3)
factories.CourseRunFactory()
data = {"id": [str(course_run.id) for course_run in course_runs]}
response = self.client.delete(
"/api/v1.0/admin/course-runs/", data=data, content_type="application/json"
)
content = response.json()
self.assertEqual(response.status_code, 200)
self.assertEqual(models.CourseRun.objects.count(), 1)
self.assertEqual(len(content["deleted"]), 3)
self.assertFalse("error" in content)
43 changes: 42 additions & 1 deletion src/backend/joanie/tests/core/test_api_admin_courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from django.test import TestCase

from joanie.core import factories
from joanie.core import factories, models


class CourseAdminApiTest(TestCase):
Expand Down Expand Up @@ -278,3 +278,44 @@ def test_admin_api_course_delete(self):
response = self.client.delete(f"/api/v1.0/admin/courses/{course.id}/")

self.assertEqual(response.status_code, 204)

def test_admin_api_course_bulk_delete(self):
"""
Staff user should be able to delete multiple courses.
"""
admin = factories.UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=admin.username, password="password")
courses = factories.CourseFactory.create_batch(3)
factories.CourseFactory()
data = {"id": [str(course.id) for course in courses]}
response = self.client.delete(
"/api/v1.0/admin/courses/", data=data, content_type="application/json"
)
content = response.json()
self.assertEqual(response.status_code, 200)
self.assertEqual(models.Course.objects.count(), 1)
self.assertEqual(len(content["deleted"]), 3)
self.assertFalse("error" in content)

def test_admin_api_course_bulk_delete_error(self):
"""
Courses with linked CourseRun cannot be deleted.
"""
admin = factories.UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=admin.username, password="password")
courses = factories.CourseFactory.create_batch(3)
factories.CourseFactory()
factories.CourseRunFactory(course=courses[0])
data = {"id": [str(course.id) for course in courses]}
response = self.client.delete(
"/api/v1.0/admin/courses/", data=data, content_type="application/json"
)
content = response.json()
self.assertEqual(response.status_code, 200)
self.assertEqual(models.Course.objects.count(), 2)
self.assertEqual(len(content["deleted"]), 2)
self.assertEqual(len(content["error"]), 1)
self.assertEqual(content["error"][0]["course"]["id"], str(courses[0].id))
self.assertTrue(
"through protected foreign keys" in content["error"][0]["error"]
)
22 changes: 21 additions & 1 deletion src/backend/joanie/tests/core/test_api_admin_organizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import factory.django

from joanie.core import factories
from joanie.core import factories, models
from joanie.core.serializers import fields


Expand Down Expand Up @@ -307,3 +307,23 @@ def test_admin_api_organization_delete(self):
)

self.assertEqual(response.status_code, 204)

def test_admin_api_organization_bulk_delete(self):
"""
Staff user should be able to delete multiple organizations.
"""
admin = factories.UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=admin.username, password="password")
organizations = factories.OrganizationFactory.create_batch(3)
factories.OrganizationFactory()
data = {"id": [str(organization.id) for organization in organizations]}
response = self.client.delete(
"/api/v1.0/admin/organizations/",
data=data,
content_type="application/json",
)
content = response.json()
self.assertEqual(response.status_code, 200)
self.assertEqual(models.Organization.objects.count(), 1)
self.assertEqual(len(content["deleted"]), 3)
self.assertFalse("error" in content)
20 changes: 19 additions & 1 deletion src/backend/joanie/tests/core/test_api_admin_products.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from django.test import TestCase

from joanie.core import factories
from joanie.core import factories, models


class ProductAdminApiTest(TestCase):
Expand Down Expand Up @@ -148,3 +148,21 @@ def test_admin_api_product_delete(self):
response = self.client.delete(f"/api/v1.0/admin/products/{product.id}/")

self.assertEqual(response.status_code, 204)

def test_admin_api_product_bulk_delete(self):
"""
Staff user should be able to delete multiple products.
"""
admin = factories.UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=admin.username, password="password")
products = factories.ProductFactory.create_batch(3)
factories.ProductFactory()
data = {"id": [str(product.id) for product in products]}
response = self.client.delete(
"/api/v1.0/admin/products/", data=data, content_type="application/json"
)
content = response.json()
self.assertEqual(response.status_code, 200)
self.assertEqual(models.Product.objects.count(), 1)
self.assertEqual(len(content["deleted"]), 3)
self.assertFalse("error" in content)

0 comments on commit 06bf7b9

Please sign in to comment.