Skip to content

Commit

Permalink
Adding api/admin/baskets endpoint (#363)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
gmaOCR authored Nov 25, 2024
1 parent a12c7c7 commit da010c8
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 1 deletion.
13 changes: 13 additions & 0 deletions oscarapi/serializers/admin/basket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from rest_framework import serializers

from oscar.core.loading import get_model

from oscarapi.utils.loading import get_api_class


Basket = get_model("basket", "Basket")
BasketSerializer = get_api_class("serializers.basket", "BasketSerializer")


class AdminBasketSerializer(BasketSerializer):
url = serializers.HyperlinkedIdentityField(view_name="admin-basket-detail")
1 change: 1 addition & 0 deletions oscarapi/tests/unit/testadminapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def test_root_view(self):
"partners",
"users",
"attributeoptiongroups",
"baskets",
]

for api in admin_apis:
Expand Down
156 changes: 156 additions & 0 deletions oscarapi/tests/unit/testbasket.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from unittest import skipIf

from unittest.mock import patch
from django.test import override_settings
Expand All @@ -9,6 +10,7 @@

from oscarapi.basket.operations import get_basket, get_user_basket
from oscarapi.tests.utils import APITest
from oscarapi import settings


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


@skipIf(settings.BLOCK_ADMIN_API_ACCESS, "Admin API is enabled")
class BasketAdminTest(APITest):
"""
Test suite for admin basket list operations.
Covers access permissions, pagination, and ordering.
"""

fixtures = [
"product",
"productcategory",
"productattribute",
"productclass",
"productattributevalue",
"category",
"attributeoptiongroup",
"attributeoption",
"stockrecord",
"partner",
"option",
]

def test_basket_admin_list_access(self):
"""
Test access permissions for basket list view.
Verifies that:
- Unauthenticated users are forbidden
- Standard users are forbidden
- Admin users can access the list
"""
url = reverse("admin-basket-list")

# Test unauthenticated access
response = self.client.get(url)
self.assertEqual(response.status_code, 403)

# Test standard user access
self.login("nobody", "nobody")
response = self.client.get(url)
self.assertEqual(response.status_code, 403)

# Test admin access
self.login("admin", "admin")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)

def test_basket_admin_list_pagination(self):
"""
Test pagination functionality for basket list view.
Checks:
- Default page size is 100
- Custom page size works correctly
- Next page link is present
"""

# Create baskets for testing
admin_user = User.objects.get(username="admin")
for _ in range(300):
Basket.objects.create(owner=admin_user)

self.login("admin", "admin")
url = reverse("admin-basket-list")

# Test first page pagination
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data["results"]), 100)

# Check next link exists on first page
self.assertIsNotNone(
response.data["next"], "Next page link should be present on first page"
)

# Verify no previous link on first page
self.assertIsNone(
response.data["previous"], "First page should not have a previous link"
)

# Get the next page
next_page_url = response.data["next"]
next_page_response = self.client.get(next_page_url)

self.assertEqual(next_page_response.status_code, 200)
self.assertEqual(len(next_page_response.data["results"]), 100)

# Check links on second page
self.assertIsNotNone(
next_page_response.data["next"],
"Next page link should be present on second page",
)
self.assertIsNotNone(
next_page_response.data["previous"],
"Second page should have a previous link",
)

# Test custom page size
response = self.client.get(f"{url}?page_size=5")
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data["results"]), 5)

def test_basket_admin_list_ordering(self):
"""
Test ordering of basket list view.
Verifies that baskets are ordered by ID in descending order.
"""
# Create baskets for testing
admin_user = User.objects.get(username="admin")
for _ in range(200):
Basket.objects.create(owner=admin_user)

self.login("admin", "admin")
url = reverse("admin-basket-list")

# Fetch and verify ordering
response = self.client.get(url)
self.assertEqual(response.status_code, 200)

basket_ids = [basket["id"] for basket in response.data["results"]]
self.assertEqual(basket_ids, sorted(basket_ids, reverse=True))

def test_assign_basket_strategy_call_frequency(self):
admin_user, _ = User.objects.get_or_create(
username="admin", defaults={"is_staff": True, "password": "admin"}
)
total_baskets = 350

# Populate baskets for the test
Basket.objects.bulk_create(
[Basket(owner=admin_user) for _ in range(total_baskets)]
)

# Log in as admin
self.client.login(username="admin", password="admin")

url = reverse("admin-basket-list")

# Mock assign_basket_strategy and bypass serialization
with patch("oscarapi.views.admin.basket.assign_basket_strategy") as mock_assign:
with patch(
"oscarapi.serializers.basket.BasketSerializer.to_representation",
return_value={},
):
self.client.get(url)

# Assert that the mock was called exactly 100 times instead of 350
self.assertEqual(
mock_assign.call_count,
100,
f"First page should have 100 assign_basket_strategy calls, got {mock_assign.call_count}",
)


@override_settings(
MIDDLEWARE=(
"django.middleware.common.CommonMiddleware",
Expand Down
17 changes: 17 additions & 0 deletions oscarapi/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,17 @@
],
)

(
BasketAdminList,
BasketAdminDetail,
) = get_api_classes(
"views.admin.basket",
[
"BasketAdminList",
"BasketAdminDetail",
],
)

(UserAdminList, UserAdminDetail) = get_api_classes(
"views.admin.user", ["UserAdminList", "UserAdminDetail"]
)
Expand Down Expand Up @@ -238,6 +249,12 @@
]

admin_urlpatterns = [
path("baskets/", BasketAdminList.as_view(), name="admin-basket-list"),
path(
"baskets/<int:pk>/",
BasketAdminDetail.as_view(),
name="admin-basket-detail",
),
path("products/", ProductAdminList.as_view(), name="admin-product-list"),
path(
"products/<int:pk>/",
Expand Down
60 changes: 60 additions & 0 deletions oscarapi/views/admin/basket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# pylint: disable=W0632
import functools

from rest_framework import generics

from oscar.core.loading import get_model
from oscarapi.basket.operations import (
assign_basket_strategy,
editable_baskets,
)

from oscarapi.utils.loading import get_api_class
from oscarapi.views.utils import QuerySetList, CustomPageNumberPagination

APIAdminPermission = get_api_class("permissions", "APIAdminPermission")
AdminBasketSerializer = get_api_class(
"serializers.admin.basket", "AdminBasketSerializer"
)
Basket = get_model("basket", "Basket")


class BasketAdminList(generics.ListCreateAPIView):
"""
List of all baskets for admin users
"""

serializer_class = AdminBasketSerializer
pagination_class = CustomPageNumberPagination
permission_classes = (APIAdminPermission,)

queryset = editable_baskets()

def get_queryset(self):
qs = super(BasketAdminList, self).get_queryset()
qs = qs.order_by("-id")
return qs

def list(self, request, *args, **kwargs):
qs = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(qs)

if page is not None:
mapped_with_baskets = list(
map(
functools.partial(assign_basket_strategy, request=self.request),
page,
)
)
serializer = self.get_serializer(mapped_with_baskets, many=True)
return self.get_paginated_response(serializer.data)

serializer = self.get_serializer(qs, many=True)
return QuerySetList(mapped_with_baskets, qs)


class BasketAdminDetail(generics.RetrieveUpdateDestroyAPIView):

queryset = Basket.objects.all()
serializer_class = AdminBasketSerializer
permission_classes = (APIAdminPermission,)
1 change: 1 addition & 0 deletions oscarapi/views/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def PUBLIC_APIS(r, f):

def ADMIN_APIS(r, f):
return [
("baskets", reverse("admin-basket-list", request=r, format=f)),
("productclasses", reverse("admin-productclass-list", request=r, format=f)),
("products", reverse("admin-product-list", request=r, format=f)),
("categories", reverse("admin-category-list", request=r, format=f)),
Expand Down
26 changes: 25 additions & 1 deletion oscarapi/views/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from django.core.exceptions import ValidationError

from oscar.core.loading import get_model

from oscarapi import permissions

from rest_framework import exceptions, generics
from rest_framework.utils.urls import replace_query_param
from rest_framework.pagination import PageNumberPagination
from rest_framework.relations import HyperlinkedRelatedField

__all__ = ("BasketPermissionMixin",)
Expand Down Expand Up @@ -54,3 +55,26 @@ def check_basket_permission(self, request, basket_pk=None, basket=None):
basket = generics.get_object_or_404(Basket.objects, pk=basket_pk)
self.check_object_permissions(request, basket)
return basket


class CustomPageNumberPagination(PageNumberPagination):
page_size = 100
page_size_query_param = "page_size"
max_page_size = 10000
page_query_param = "page"

def get_next_link(self):

if not self.page.has_next():
return None
url = self.request.build_absolute_uri()
page_number = self.page.next_page_number()

return replace_query_param(url, self.page_query_param, page_number)

def get_previous_link(self):
if not self.page.has_previous():
return None
url = self.request.build_absolute_uri()
page_number = self.page.previous_page_number()
return replace_query_param(url, self.page_query_param, page_number)

0 comments on commit da010c8

Please sign in to comment.