Skip to content

Commit bf9c8c6

Browse files
committed
Ensure method is allowed and defined
1 parent 70cb77a commit bf9c8c6

File tree

4 files changed

+125
-4
lines changed

4 files changed

+125
-4
lines changed

drf_sideloading/mixins.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.utils.translation import gettext_lazy as _
88
from rest_framework.exceptions import ValidationError
99
from rest_framework.generics import get_object_or_404
10+
from rest_framework.mixins import RetrieveModelMixin, ListModelMixin
1011
from rest_framework.response import Response
1112
from rest_framework.serializers import ListSerializer
1213

@@ -152,8 +153,17 @@ def get_sideloading_serializer_context(self):
152153
# modified DRF methods
153154

154155
def retrieve(self, request, *args, **kwargs):
156+
if not isinstance(self, RetrieveModelMixin):
157+
# The viewset does not have RetrieveModelMixin and therefore the method is not allowed
158+
return self.http_method_not_allowed(request, *args, **kwargs)
159+
155160
if self.sideloading_query_param_name not in request.query_params:
156-
return super().retrieve(request, *args, **kwargs)
161+
try:
162+
return super().retrieve(request=request, *args, **kwargs)
163+
except AttributeError:
164+
# self.retrieve() method was not declared before this mixin.
165+
# Make sure the SideloadableRelationsMixin is defined higher than RetrieveModelMixin.
166+
return self.http_method_not_allowed(request, *args, **kwargs)
157167

158168
(
159169
sideloading_serializer_class,
@@ -166,7 +176,12 @@ def retrieve(self, request, *args, **kwargs):
166176
) = self.get_sideloading_variables_from_serializer(request=request)
167177

168178
if not relations_to_sideload:
169-
return super().retrieve(request, *args, **kwargs)
179+
try:
180+
return super().retrieve(request=request, *args, **kwargs)
181+
except AttributeError:
182+
# self.retrieve() method was not declared before this mixin.
183+
# Make sure the SideloadableRelationsMixin is defined higher than RetrieveModelMixin.
184+
return self.http_method_not_allowed(request, *args, **kwargs)
170185

171186
# return object with sideloading serializer
172187
queryset, relations_sources = self.get_sideloadable_object_as_queryset(
@@ -189,8 +204,17 @@ def retrieve(self, request, *args, **kwargs):
189204
return Response(serializer.data)
190205

191206
def list(self, request, *args, **kwargs):
207+
if not isinstance(self, ListModelMixin):
208+
# The viewset does not have ListModelMixin and therefore the method is not allowed
209+
return self.http_method_not_allowed(request, *args, **kwargs)
210+
192211
if request.method != "GET" or self.sideloading_query_param_name not in request.query_params:
193-
return super().list(request, *args, **kwargs)
212+
try:
213+
return super().list(request=request, *args, **kwargs)
214+
except AttributeError:
215+
# self.list() method was not declared before this mixin.
216+
# Make sure the SideloadableRelationsMixin is defined higher than ListModelMixin.
217+
return self.http_method_not_allowed(request, *args, **kwargs)
194218

195219
(
196220
sideloading_serializer_class,
@@ -203,7 +227,12 @@ def list(self, request, *args, **kwargs):
203227
) = self.get_sideloading_variables_from_serializer(request=request)
204228

205229
if not relations_to_sideload:
206-
return super().list(request, *args, **kwargs)
230+
try:
231+
return super().list(request=request, *args, **kwargs)
232+
except AttributeError:
233+
# self.list() method was not declared before this mixin.
234+
# Make sure the SideloadableRelationsMixin is defined higher than ListModelMixin.
235+
return self.http_method_not_allowed(request, *args, **kwargs)
207236

208237
# After this `relations_to_sideload` is safe to use
209238
queryset = self.get_queryset()

tests/test_products_api.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,66 @@ class Meta:
883883
# )
884884

885885

886+
class TestDrfSideloadingWithoutListModelMixin(BaseTestCase):
887+
def test_list(self):
888+
response = self.client.get(
889+
path=reverse("productretreiveonly-list"),
890+
data={"sideload": "categories"},
891+
**self.DEFAULT_HEADERS,
892+
)
893+
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
894+
895+
def test_detail(self):
896+
response = self.client.get(
897+
path=reverse("productretreiveonly-detail", args=[self.product1.id]),
898+
data={"sideload": "categories"},
899+
**self.DEFAULT_HEADERS,
900+
)
901+
self.assertEqual(response.status_code, status.HTTP_200_OK, response.json())
902+
903+
904+
class TestDrfSideloadingWithoutRetreiveModelMixin(BaseTestCase):
905+
def test_list(self):
906+
response = self.client.get(
907+
path=reverse("productlistonly-list"),
908+
data={"sideload": "categories"},
909+
**self.DEFAULT_HEADERS,
910+
)
911+
self.assertEqual(response.status_code, status.HTTP_200_OK, response.json())
912+
913+
def test_detail(self):
914+
response = self.client.get(
915+
path=reverse("productlistonly-detail", args=[self.product1.id]),
916+
data={"sideload": "categories"},
917+
**self.DEFAULT_HEADERS,
918+
)
919+
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
920+
921+
922+
class TestDrfSideloadingListModelMixinAfterSideloading(BaseTestCase):
923+
# in case the DRF views are added after sideloading,
924+
# the list and retreive methods will be overwritten and no sideloading happens.
925+
926+
def test_list(self):
927+
response = self.client.get(
928+
path=reverse("productwrongmixinorder-list"),
929+
data={"sideload": "categories,suppliers,filtered_suppliers,partners"},
930+
**self.DEFAULT_HEADERS,
931+
)
932+
self.assertEqual(response.status_code, status.HTTP_200_OK, response.json())
933+
self.assertIsInstance(response.json(), list)
934+
935+
def test_detail(self):
936+
response = self.client.get(
937+
path=reverse("productwrongmixinorder-detail", args=[self.product1.id]),
938+
data={"sideload": "categories,suppliers,filtered_suppliers,partners"},
939+
**self.DEFAULT_HEADERS,
940+
)
941+
self.assertEqual(response.status_code, status.HTTP_200_OK, response.json())
942+
self.assertIsInstance(response.json(), dict)
943+
self.assertListEqual(["name", "category", "supplier", "partners"], list(response.json().keys()))
944+
945+
886946
class TestDrfSideloadingBrowsableApiPermissions(BaseTestCase):
887947
"""Run tests while including mixin but not defining sideloading"""
888948

tests/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55

66
router = routers.DefaultRouter()
77
router.register(r"product", viewsets.ProductViewSet)
8+
router.register(r"productlistonly", viewsets.ListOnlyProductViewSet, basename="productlistonly")
9+
router.register(
10+
r"productwrongmixinorder", viewsets.ProductViewSetSideloadingBeforeViews, basename="productwrongmixinorder"
11+
)
12+
router.register(r"productretreiveonly", viewsets.RetreiveOnlyProductViewSet, basename="productretreiveonly")
813
router.register(r"category", viewsets.CategoryViewSet)
914
router.register(r"supplier", viewsets.SupplierViewSet)
1015
router.register(r"partner", viewsets.PartnerViewSet)

tests/viewsets.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
from rest_framework import viewsets, filters, versioning
2+
from rest_framework.mixins import (
3+
CreateModelMixin,
4+
RetrieveModelMixin,
5+
UpdateModelMixin,
6+
DestroyModelMixin,
7+
ListModelMixin,
8+
)
9+
from rest_framework.viewsets import GenericViewSet
210

311
from drf_sideloading.mixins import SideloadableRelationsMixin
412
from tests.mixins import OtherMixin
@@ -28,6 +36,7 @@ class ProductViewSet(SideloadableRelationsMixin, OtherMixin, viewsets.ModelViewS
2836
# django_filters.rest_framework.DjangoFilterBackend,
2937
]
3038
search_fields = ["name"]
39+
3140
# filter_fields = ["confirmed"]
3241

3342
def get_serializer_class(self):
@@ -41,6 +50,24 @@ def get_sideloading_serializer_class(self):
4150
return self.sideloading_serializer_class
4251

4352

53+
class ListOnlyProductViewSet(SideloadableRelationsMixin, OtherMixin, ListModelMixin, GenericViewSet):
54+
queryset = Product.objects.all()
55+
serializer_class = ProductSerializer
56+
sideloading_serializer_class = ProductSideloadableSerializer
57+
58+
59+
class RetreiveOnlyProductViewSet(SideloadableRelationsMixin, OtherMixin, RetrieveModelMixin, GenericViewSet):
60+
queryset = Product.objects.all()
61+
serializer_class = ProductSerializer
62+
sideloading_serializer_class = ProductSideloadableSerializer
63+
64+
65+
class ProductViewSetSideloadingBeforeViews(viewsets.ModelViewSet, SideloadableRelationsMixin, OtherMixin):
66+
queryset = Product.objects.all()
67+
serializer_class = ProductSerializer
68+
sideloading_serializer_class = ProductSideloadableSerializer
69+
70+
4471
class CategoryViewSet(SideloadableRelationsMixin, viewsets.ModelViewSet):
4572
queryset = Category.objects.all()
4673
serializer_class = CategorySerializer

0 commit comments

Comments
 (0)