Skip to content

Commit da010c8

Browse files
authored
Adding api/admin/baskets endpoint (#363)
* Adding BasketAdminList to urls.py Creating admin basket view and serializer correction list comma added restore final update optimizing test * Adding CustomPageNumberPagination for baskets admin endpoint * Adding BasketAdminTest * PR #363 change request to include pagination * perf test test_assign_basket_strategy_call_frequency * perf test test_assign_basket_strategy_call_frequency and fix black linting * Refactoring for linter * Fixing test issue and add overriden decorator * Linter refactoring
1 parent a12c7c7 commit da010c8

File tree

7 files changed

+273
-1
lines changed

7 files changed

+273
-1
lines changed

oscarapi/serializers/admin/basket.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from rest_framework import serializers
2+
3+
from oscar.core.loading import get_model
4+
5+
from oscarapi.utils.loading import get_api_class
6+
7+
8+
Basket = get_model("basket", "Basket")
9+
BasketSerializer = get_api_class("serializers.basket", "BasketSerializer")
10+
11+
12+
class AdminBasketSerializer(BasketSerializer):
13+
url = serializers.HyperlinkedIdentityField(view_name="admin-basket-detail")

oscarapi/tests/unit/testadminapi.py

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def test_root_view(self):
8080
"partners",
8181
"users",
8282
"attributeoptiongroups",
83+
"baskets",
8384
]
8485

8586
for api in admin_apis:

oscarapi/tests/unit/testbasket.py

+156
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from unittest import skipIf
23

34
from unittest.mock import patch
45
from django.test import override_settings
@@ -9,6 +10,7 @@
910

1011
from oscarapi.basket.operations import get_basket, get_user_basket
1112
from oscarapi.tests.utils import APITest
13+
from oscarapi import settings
1214

1315

1416
Basket = get_model("basket", "Basket")
@@ -1149,6 +1151,160 @@ def test_get_user_basket_with_multiple_baskets(self):
11491151
self.assertEqual(user_basket, Basket.open.first())
11501152

11511153

1154+
@skipIf(settings.BLOCK_ADMIN_API_ACCESS, "Admin API is enabled")
1155+
class BasketAdminTest(APITest):
1156+
"""
1157+
Test suite for admin basket list operations.
1158+
Covers access permissions, pagination, and ordering.
1159+
"""
1160+
1161+
fixtures = [
1162+
"product",
1163+
"productcategory",
1164+
"productattribute",
1165+
"productclass",
1166+
"productattributevalue",
1167+
"category",
1168+
"attributeoptiongroup",
1169+
"attributeoption",
1170+
"stockrecord",
1171+
"partner",
1172+
"option",
1173+
]
1174+
1175+
def test_basket_admin_list_access(self):
1176+
"""
1177+
Test access permissions for basket list view.
1178+
1179+
Verifies that:
1180+
- Unauthenticated users are forbidden
1181+
- Standard users are forbidden
1182+
- Admin users can access the list
1183+
"""
1184+
url = reverse("admin-basket-list")
1185+
1186+
# Test unauthenticated access
1187+
response = self.client.get(url)
1188+
self.assertEqual(response.status_code, 403)
1189+
1190+
# Test standard user access
1191+
self.login("nobody", "nobody")
1192+
response = self.client.get(url)
1193+
self.assertEqual(response.status_code, 403)
1194+
1195+
# Test admin access
1196+
self.login("admin", "admin")
1197+
response = self.client.get(url)
1198+
self.assertEqual(response.status_code, 200)
1199+
1200+
def test_basket_admin_list_pagination(self):
1201+
"""
1202+
Test pagination functionality for basket list view.
1203+
1204+
Checks:
1205+
- Default page size is 100
1206+
- Custom page size works correctly
1207+
- Next page link is present
1208+
"""
1209+
1210+
# Create baskets for testing
1211+
admin_user = User.objects.get(username="admin")
1212+
for _ in range(300):
1213+
Basket.objects.create(owner=admin_user)
1214+
1215+
self.login("admin", "admin")
1216+
url = reverse("admin-basket-list")
1217+
1218+
# Test first page pagination
1219+
response = self.client.get(url)
1220+
self.assertEqual(response.status_code, 200)
1221+
self.assertEqual(len(response.data["results"]), 100)
1222+
1223+
# Check next link exists on first page
1224+
self.assertIsNotNone(
1225+
response.data["next"], "Next page link should be present on first page"
1226+
)
1227+
1228+
# Verify no previous link on first page
1229+
self.assertIsNone(
1230+
response.data["previous"], "First page should not have a previous link"
1231+
)
1232+
1233+
# Get the next page
1234+
next_page_url = response.data["next"]
1235+
next_page_response = self.client.get(next_page_url)
1236+
1237+
self.assertEqual(next_page_response.status_code, 200)
1238+
self.assertEqual(len(next_page_response.data["results"]), 100)
1239+
1240+
# Check links on second page
1241+
self.assertIsNotNone(
1242+
next_page_response.data["next"],
1243+
"Next page link should be present on second page",
1244+
)
1245+
self.assertIsNotNone(
1246+
next_page_response.data["previous"],
1247+
"Second page should have a previous link",
1248+
)
1249+
1250+
# Test custom page size
1251+
response = self.client.get(f"{url}?page_size=5")
1252+
self.assertEqual(response.status_code, 200)
1253+
self.assertEqual(len(response.data["results"]), 5)
1254+
1255+
def test_basket_admin_list_ordering(self):
1256+
"""
1257+
Test ordering of basket list view.
1258+
1259+
Verifies that baskets are ordered by ID in descending order.
1260+
"""
1261+
# Create baskets for testing
1262+
admin_user = User.objects.get(username="admin")
1263+
for _ in range(200):
1264+
Basket.objects.create(owner=admin_user)
1265+
1266+
self.login("admin", "admin")
1267+
url = reverse("admin-basket-list")
1268+
1269+
# Fetch and verify ordering
1270+
response = self.client.get(url)
1271+
self.assertEqual(response.status_code, 200)
1272+
1273+
basket_ids = [basket["id"] for basket in response.data["results"]]
1274+
self.assertEqual(basket_ids, sorted(basket_ids, reverse=True))
1275+
1276+
def test_assign_basket_strategy_call_frequency(self):
1277+
admin_user, _ = User.objects.get_or_create(
1278+
username="admin", defaults={"is_staff": True, "password": "admin"}
1279+
)
1280+
total_baskets = 350
1281+
1282+
# Populate baskets for the test
1283+
Basket.objects.bulk_create(
1284+
[Basket(owner=admin_user) for _ in range(total_baskets)]
1285+
)
1286+
1287+
# Log in as admin
1288+
self.client.login(username="admin", password="admin")
1289+
1290+
url = reverse("admin-basket-list")
1291+
1292+
# Mock assign_basket_strategy and bypass serialization
1293+
with patch("oscarapi.views.admin.basket.assign_basket_strategy") as mock_assign:
1294+
with patch(
1295+
"oscarapi.serializers.basket.BasketSerializer.to_representation",
1296+
return_value={},
1297+
):
1298+
self.client.get(url)
1299+
1300+
# Assert that the mock was called exactly 100 times instead of 350
1301+
self.assertEqual(
1302+
mock_assign.call_count,
1303+
100,
1304+
f"First page should have 100 assign_basket_strategy calls, got {mock_assign.call_count}",
1305+
)
1306+
1307+
11521308
@override_settings(
11531309
MIDDLEWARE=(
11541310
"django.middleware.common.CommonMiddleware",

oscarapi/urls.py

+17
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,17 @@
149149
],
150150
)
151151

152+
(
153+
BasketAdminList,
154+
BasketAdminDetail,
155+
) = get_api_classes(
156+
"views.admin.basket",
157+
[
158+
"BasketAdminList",
159+
"BasketAdminDetail",
160+
],
161+
)
162+
152163
(UserAdminList, UserAdminDetail) = get_api_classes(
153164
"views.admin.user", ["UserAdminList", "UserAdminDetail"]
154165
)
@@ -238,6 +249,12 @@
238249
]
239250

240251
admin_urlpatterns = [
252+
path("baskets/", BasketAdminList.as_view(), name="admin-basket-list"),
253+
path(
254+
"baskets/<int:pk>/",
255+
BasketAdminDetail.as_view(),
256+
name="admin-basket-detail",
257+
),
241258
path("products/", ProductAdminList.as_view(), name="admin-product-list"),
242259
path(
243260
"products/<int:pk>/",

oscarapi/views/admin/basket.py

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# pylint: disable=W0632
2+
import functools
3+
4+
from rest_framework import generics
5+
6+
from oscar.core.loading import get_model
7+
from oscarapi.basket.operations import (
8+
assign_basket_strategy,
9+
editable_baskets,
10+
)
11+
12+
from oscarapi.utils.loading import get_api_class
13+
from oscarapi.views.utils import QuerySetList, CustomPageNumberPagination
14+
15+
APIAdminPermission = get_api_class("permissions", "APIAdminPermission")
16+
AdminBasketSerializer = get_api_class(
17+
"serializers.admin.basket", "AdminBasketSerializer"
18+
)
19+
Basket = get_model("basket", "Basket")
20+
21+
22+
class BasketAdminList(generics.ListCreateAPIView):
23+
"""
24+
List of all baskets for admin users
25+
"""
26+
27+
serializer_class = AdminBasketSerializer
28+
pagination_class = CustomPageNumberPagination
29+
permission_classes = (APIAdminPermission,)
30+
31+
queryset = editable_baskets()
32+
33+
def get_queryset(self):
34+
qs = super(BasketAdminList, self).get_queryset()
35+
qs = qs.order_by("-id")
36+
return qs
37+
38+
def list(self, request, *args, **kwargs):
39+
qs = self.filter_queryset(self.get_queryset())
40+
page = self.paginate_queryset(qs)
41+
42+
if page is not None:
43+
mapped_with_baskets = list(
44+
map(
45+
functools.partial(assign_basket_strategy, request=self.request),
46+
page,
47+
)
48+
)
49+
serializer = self.get_serializer(mapped_with_baskets, many=True)
50+
return self.get_paginated_response(serializer.data)
51+
52+
serializer = self.get_serializer(qs, many=True)
53+
return QuerySetList(mapped_with_baskets, qs)
54+
55+
56+
class BasketAdminDetail(generics.RetrieveUpdateDestroyAPIView):
57+
58+
queryset = Basket.objects.all()
59+
serializer_class = AdminBasketSerializer
60+
permission_classes = (APIAdminPermission,)

oscarapi/views/root.py

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def PUBLIC_APIS(r, f):
3434

3535
def ADMIN_APIS(r, f):
3636
return [
37+
("baskets", reverse("admin-basket-list", request=r, format=f)),
3738
("productclasses", reverse("admin-productclass-list", request=r, format=f)),
3839
("products", reverse("admin-product-list", request=r, format=f)),
3940
("categories", reverse("admin-category-list", request=r, format=f)),

oscarapi/views/utils.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from django.core.exceptions import ValidationError
22

33
from oscar.core.loading import get_model
4-
54
from oscarapi import permissions
65

76
from rest_framework import exceptions, generics
7+
from rest_framework.utils.urls import replace_query_param
8+
from rest_framework.pagination import PageNumberPagination
89
from rest_framework.relations import HyperlinkedRelatedField
910

1011
__all__ = ("BasketPermissionMixin",)
@@ -54,3 +55,26 @@ def check_basket_permission(self, request, basket_pk=None, basket=None):
5455
basket = generics.get_object_or_404(Basket.objects, pk=basket_pk)
5556
self.check_object_permissions(request, basket)
5657
return basket
58+
59+
60+
class CustomPageNumberPagination(PageNumberPagination):
61+
page_size = 100
62+
page_size_query_param = "page_size"
63+
max_page_size = 10000
64+
page_query_param = "page"
65+
66+
def get_next_link(self):
67+
68+
if not self.page.has_next():
69+
return None
70+
url = self.request.build_absolute_uri()
71+
page_number = self.page.next_page_number()
72+
73+
return replace_query_param(url, self.page_query_param, page_number)
74+
75+
def get_previous_link(self):
76+
if not self.page.has_previous():
77+
return None
78+
url = self.request.build_absolute_uri()
79+
page_number = self.page.previous_page_number()
80+
return replace_query_param(url, self.page_query_param, page_number)

0 commit comments

Comments
 (0)