From cd4f2afd3f0b90b2c8ab9d758765a17868e42e3d Mon Sep 17 00:00:00 2001 From: Samy Ait-Ouakli Date: Wed, 12 Jul 2023 01:30:22 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(backend)=20add=20bulk=20delete=20rout?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add route to admin api to bulk delete objects --- CHANGELOG.md | 1 + src/backend/joanie/core/api/admin.py | 133 ++++++++++++++++++ .../test_api_admin_certificate_definitions.py | 27 +++- .../tests/core/test_api_admin_course_runs.py | 20 ++- .../tests/core/test_api_admin_courses.py | 40 +++++- .../core/test_api_admin_organizations.py | 22 ++- .../tests/core/test_api_admin_products.py | 20 ++- 7 files changed, 258 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddafbba46e..c203efd92e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/backend/joanie/core/api/admin.py b/src/backend/joanie/core/api/admin.py index d5c737e8b2..823842db40 100755 --- a/src/backend/joanie/core/api/admin.py +++ b/src/backend/joanie/core/api/admin.py @@ -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 @@ -20,6 +23,32 @@ 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_to_delete = request.data.get("id", []) + elements_to_delete = self.queryset.filter(id__in=id_to_delete) + elements = list(elements_to_delete.all()) + error_elements = [] + for element in elements_to_delete.all(): + try: + element.delete() + except IntegrityError: + error_elements.append(element) + deleted_elements = [ + element for element in elements if element not in error_elements + ] + response = {} + if len(deleted_elements) > 0: + response["deleted"] = self.serializer_class( + deleted_elements, many=True + ).data + if len(error_elements) > 0: + response["error"] = self.serializer_class(error_elements, many=True).data + return Response(response) + class ProductViewSet(viewsets.ModelViewSet): """ @@ -32,6 +61,32 @@ 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_to_delete = request.data.get("id", []) + elements_to_delete = self.queryset.filter(id__in=id_to_delete) + elements = list(elements_to_delete.all()) + error_elements = [] + for element in elements_to_delete.all(): + try: + element.delete() + except IntegrityError: + error_elements.append(element) + deleted_elements = [ + element for element in elements if element not in error_elements + ] + response = {} + if len(deleted_elements) > 0: + response["deleted"] = self.serializer_class( + deleted_elements, many=True + ).data + if len(error_elements) > 0: + response["error"] = self.serializer_class(error_elements, many=True).data + return Response(response) + class CourseViewSet(viewsets.ModelViewSet): """ @@ -45,6 +100,32 @@ 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_to_delete = request.data.get("id", []) + elements_to_delete = self.queryset.filter(id__in=id_to_delete) + elements = list(elements_to_delete.all()) + error_elements = [] + for element in elements_to_delete.all(): + try: + element.delete() + except IntegrityError: + error_elements.append(element) + deleted_elements = [ + element for element in elements if element not in error_elements + ] + response = {} + if len(deleted_elements) > 0: + response["deleted"] = self.serializer_class( + deleted_elements, many=True + ).data + if len(error_elements) > 0: + response["error"] = self.serializer_class(error_elements, many=True).data + return Response(response) + class CourseRunViewSet(viewsets.ModelViewSet): """ @@ -58,6 +139,32 @@ 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_to_delete = request.data.get("id", []) + elements_to_delete = self.queryset.filter(id__in=id_to_delete) + elements = list(elements_to_delete.all()) + error_elements = [] + for element in elements_to_delete.all(): + try: + element.delete() + except IntegrityError: + error_elements.append(element) + deleted_elements = [ + element for element in elements if element not in error_elements + ] + response = {} + if len(deleted_elements) > 0: + response["deleted"] = self.serializer_class( + deleted_elements, many=True + ).data + if len(error_elements) > 0: + response["error"] = self.serializer_class(error_elements, many=True).data + return Response(response) + class CertificateDefinitionViewSet(viewsets.ModelViewSet): """ @@ -71,6 +178,32 @@ 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_to_delete = request.data.get("id", []) + elements_to_delete = self.queryset.filter(id__in=id_to_delete) + elements = list(elements_to_delete.all()) + error_elements = [] + for element in elements_to_delete.all(): + try: + element.delete() + except IntegrityError: + error_elements.append(element) + deleted_elements = [ + element for element in elements if element not in error_elements + ] + response = {} + if len(deleted_elements) > 0: + response["deleted"] = self.serializer_class( + deleted_elements, many=True + ).data + if len(error_elements) > 0: + response["error"] = self.serializer_class(error_elements, many=True).data + return Response(response) + class UserViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): """ diff --git a/src/backend/joanie/tests/core/test_api_admin_certificate_definitions.py b/src/backend/joanie/tests/core/test_api_admin_certificate_definitions.py index a254b8d16e..b194f6c40e 100644 --- a/src/backend/joanie/tests/core/test_api_admin_certificate_definitions.py +++ b/src/backend/joanie/tests/core/test_api_admin_certificate_definitions.py @@ -5,7 +5,7 @@ from django.test import TestCase -from joanie.core import factories +from joanie.core import factories, models class CertificateDefinitionAdminApiTest(TestCase): @@ -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) diff --git a/src/backend/joanie/tests/core/test_api_admin_course_runs.py b/src/backend/joanie/tests/core/test_api_admin_course_runs.py index f1ac63de5c..fc57c7642b 100644 --- a/src/backend/joanie/tests/core/test_api_admin_course_runs.py +++ b/src/backend/joanie/tests/core/test_api_admin_course_runs.py @@ -5,7 +5,7 @@ from django.test import TestCase -from joanie.core import factories +from joanie.core import factories, models class CourseRunAdminApiTest(TestCase): @@ -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) diff --git a/src/backend/joanie/tests/core/test_api_admin_courses.py b/src/backend/joanie/tests/core/test_api_admin_courses.py index d3645645ba..30fcd2853a 100644 --- a/src/backend/joanie/tests/core/test_api_admin_courses.py +++ b/src/backend/joanie/tests/core/test_api_admin_courses.py @@ -5,7 +5,7 @@ from django.test import TestCase -from joanie.core import factories +from joanie.core import factories, models class CourseAdminApiTest(TestCase): @@ -278,3 +278,41 @@ 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]["id"], str(courses[0].id)) diff --git a/src/backend/joanie/tests/core/test_api_admin_organizations.py b/src/backend/joanie/tests/core/test_api_admin_organizations.py index 1d60ef2a7a..b932e841ac 100755 --- a/src/backend/joanie/tests/core/test_api_admin_organizations.py +++ b/src/backend/joanie/tests/core/test_api_admin_organizations.py @@ -10,7 +10,7 @@ import factory.django -from joanie.core import factories +from joanie.core import factories, models from joanie.core.serializers import fields @@ -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) diff --git a/src/backend/joanie/tests/core/test_api_admin_products.py b/src/backend/joanie/tests/core/test_api_admin_products.py index bd5323f4f4..8810f8f8e8 100644 --- a/src/backend/joanie/tests/core/test_api_admin_products.py +++ b/src/backend/joanie/tests/core/test_api_admin_products.py @@ -5,7 +5,7 @@ from django.test import TestCase -from joanie.core import factories +from joanie.core import factories, models class ProductAdminApiTest(TestCase): @@ -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)