From d08d5622ef92b35e905c8f1b57fabd7dcbd2cca6 Mon Sep 17 00:00:00 2001 From: Nicolas Clerc Date: Wed, 4 Dec 2024 14:25:56 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(backend)=20add=20order=20export=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As we want to export order csv, an api endpoint is needed. --- src/backend/joanie/core/api/admin/__init__.py | 29 +++++++- src/backend/joanie/core/serializers/admin.py | 25 +++++++ .../core/api/admin/orders/test_export.py | 68 +++++++++++++++++++ .../joanie/tests/swagger/admin-swagger.json | 37 ++++++++++ 4 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 src/backend/joanie/tests/core/api/admin/orders/test_export.py diff --git a/src/backend/joanie/core/api/admin/__init__.py b/src/backend/joanie/core/api/admin/__init__.py index b50e3bd22..2091a2e26 100755 --- a/src/backend/joanie/core/api/admin/__init__.py +++ b/src/backend/joanie/core/api/admin/__init__.py @@ -2,11 +2,12 @@ Admin API Endpoints """ +import csv from http import HTTPStatus from django.core.cache import cache from django.core.exceptions import ValidationError -from django.http import JsonResponse +from django.http import HttpResponse, JsonResponse from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.types import OpenApiTypes @@ -614,6 +615,7 @@ class OrderViewSet( permission_classes = [permissions.IsAdminUser & permissions.DjangoModelPermissions] serializer_classes = { "list": serializers.AdminOrderLightSerializer, + "export": serializers.AdminOrderExportSerializer, } default_serializer_class = serializers.AdminOrderSerializer filterset_class = filters.OrderAdminFilterSet @@ -689,6 +691,31 @@ def refund(self, request, pk=None): # pylint:disable=unused-argument return Response(status=HTTPStatus.ACCEPTED) + @extend_schema( + request=None, + responses={ + (200, "text/csv"): OpenApiTypes.OBJECT, + 404: serializers.ErrorResponseSerializer, + }, + ) + @action(methods=["GET"], detail=False) + def export(self, request): + """ + Export orders to a CSV file. + """ + queryset = self.filter_queryset(self.get_queryset()).order_by("created_on") + serializer = self.get_serializer(queryset, many=True) + data = serializer.data + + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = 'attachment; filename="orders.csv"' + writer = csv.writer(response) + + writer.writerow(data[0].keys()) + writer.writerows([row.values() for row in data]) + + return response + class OrganizationAddressViewSet( mixins.CreateModelMixin, diff --git a/src/backend/joanie/core/serializers/admin.py b/src/backend/joanie/core/serializers/admin.py index 871cf870c..721217d33 100755 --- a/src/backend/joanie/core/serializers/admin.py +++ b/src/backend/joanie/core/serializers/admin.py @@ -1269,6 +1269,31 @@ def get_owner_name(self, instance) -> str: return instance.owner.get_full_name() or instance.owner.username +class AdminOrderExportSerializer(serializers.ModelSerializer): + """ + Read only light serializer for Order export. + """ + + owner = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = models.Order + fields = ( + "id", + "created_on", + "owner", + "total", + ) + read_only_fields = fields + + def get_owner(self, instance) -> str: + """ + Return the full name of the order's owner if available, + otherwise fallback to the username + """ + return instance.owner.get_full_name() or instance.owner.username + + class AdminEnrollmentLightSerializer(serializers.ModelSerializer): """ Light Serializer for Enrollment model diff --git a/src/backend/joanie/tests/core/api/admin/orders/test_export.py b/src/backend/joanie/tests/core/api/admin/orders/test_export.py new file mode 100644 index 000000000..99cf731ab --- /dev/null +++ b/src/backend/joanie/tests/core/api/admin/orders/test_export.py @@ -0,0 +1,68 @@ +"""Test suite for the admin orders API export endpoint.""" + +from http import HTTPStatus + +from django.test import TestCase + +from joanie.core import factories +from joanie.tests import format_date + + +class OrdersAdminApiExportTestCase(TestCase): + """Test suite for the admin orders API export endpoint.""" + + maxDiff = None + + def test_api_admin_orders_export_csv_anonymous_user(self): + """ + Anonymous users should not be able to export orders as CSV. + """ + response = self.client.get("/api/v1.0/admin/orders/export/") + + self.assertEqual(response.status_code, HTTPStatus.UNAUTHORIZED) + + def test_api_admin_orders_export_csv_lambda_user(self): + """ + Lambda users should not be able to export orders as CSV. + """ + admin = factories.UserFactory(is_staff=False, is_superuser=False) + self.client.login(username=admin.username, password="password") + + response = self.client.get("/api/v1.0/admin/orders/export/") + + self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN) + + def test_api_admin_orders_export_csv(self): + """ + Admin users should be able to export orders as CSV. + """ + orders = factories.OrderFactory.create_batch(3) + admin = factories.UserFactory(is_staff=True, is_superuser=True) + self.client.login(username=admin.username, password="password") + + response = self.client.get("/api/v1.0/admin/orders/export/") + + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertEqual(response["Content-Type"], "text/csv") + self.assertEqual( + response["Content-Disposition"], + 'attachment; filename="orders.csv"', + ) + csv_content = response.content.decode().splitlines() + csv_header = csv_content.pop(0) + self.assertEqual( + csv_header.split(","), + [ + "id", + "created_on", + "owner", + "total", + ], + ) + + for order, csv_line in zip(orders, csv_content, strict=False): + csv_row = csv_line.split(",") + self.assertEqual(csv_row[0], str(order.id)) + self.assertEqual(csv_row[1], format_date(order.created_on)) + self.assertEqual(csv_row[2], order.owner.username) + self.assertEqual(csv_row[3], str(order.total)) diff --git a/src/backend/joanie/tests/swagger/admin-swagger.json b/src/backend/joanie/tests/swagger/admin-swagger.json index 4af1a458d..0febfb310 100644 --- a/src/backend/joanie/tests/swagger/admin-swagger.json +++ b/src/backend/joanie/tests/swagger/admin-swagger.json @@ -3192,6 +3192,43 @@ } } }, + "/api/v1.0/admin/orders/export/": { + "get": { + "operationId": "orders_export_retrieve", + "description": "Export orders to a CSV file.", + "tags": [ + "orders" + ], + "security": [ + { + "cookieAuth": [] + } + ], + "responses": { + "200": { + "content": { + "text/csv": { + "schema": { + "type": "object", + "additionalProperties": {} + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "" + } + } + } + }, "/api/v1.0/admin/organizations/": { "get": { "operationId": "organizations_list",