Skip to content

Commit 9ec10c1

Browse files
authored
Merge pull request #13 from ShipChain/feature/configurable-view-set
Configurable ViewSet
2 parents d18f535 + 5f22173 commit 9ec10c1

File tree

8 files changed

+932
-111
lines changed

8 files changed

+932
-111
lines changed

conf/test_settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
ENVIRONMENT = 'TEST'
1111

12+
FORMAT_SUFFIX_KWARG = 'format'
1213

1314
DEBUG = True
1415

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "shipchain-common"
3-
version = "1.0.8"
3+
version = "1.0.9"
44
description = "A PyPI package containing shared code for ShipChain's Python/Django projects."
55

66
license = "Apache-2.0"

src/shipchain_common/mixins.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
"""
2+
Copyright 2019 ShipChain, Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
"""
16+
from rest_framework import status, mixins
17+
from rest_framework.response import Response
18+
19+
20+
class SerializationType:
21+
REQUEST = 0
22+
RESPONSE = 1
23+
24+
25+
class MultiSerializerViewSetMixin:
26+
def get_serializer_class(self):
27+
"""
28+
Look for serializer class in self.action_serializer_classes, which
29+
should be a dict mapping action name (key) to serializer class (value),
30+
i.e.:
31+
32+
class MyViewSet(MultiSerializerViewSetMixin, ViewSet):
33+
serializer_class = MyDefaultSerializer
34+
action_serializer_classes = {
35+
'list': MyListSerializer,
36+
'my_action': MyActionSerializer,
37+
}
38+
39+
@action
40+
def my_action:
41+
...
42+
43+
If there's no entry for that action then just fallback to the regular
44+
get_serializer_class lookup: self.serializer_class, DefaultSerializer.
45+
46+
Built-in actions:
47+
create
48+
retrieve
49+
update PUT
50+
partial_update PATCH
51+
destroy
52+
list
53+
"""
54+
action = self.action
55+
56+
try:
57+
# PUT and PATCH should use the same serializers if not separately defined
58+
if action == 'partial_update' and 'partial_update' not in self.action_serializer_classes:
59+
action = 'update'
60+
61+
if 'format' in self.kwargs:
62+
try:
63+
return self.action_serializer_classes[f'{action}.{self.kwargs["format"]}']
64+
except (KeyError, AttributeError):
65+
pass
66+
67+
return self.action_serializer_classes[action]
68+
69+
except (KeyError, AttributeError):
70+
return super(MultiSerializerViewSetMixin, self).get_serializer_class()
71+
72+
73+
class MultiPermissionViewSetMixin:
74+
def get_permissions(self):
75+
"""
76+
Look for permission classes in self.action_permission_classes, which
77+
should be a dict mapping action name (key) to list of permission classes (value),
78+
i.e.:
79+
80+
class MyViewSet(MultiPermissionViewSetMixin, ViewSet):
81+
permission_classes = (HasViewSetActionPermissions,)
82+
action_permission_classes = {
83+
'list': (permissions.AllowAny,),
84+
'my_action': (HasViewSetActionPermissions, isOwner,),
85+
}
86+
87+
@action
88+
def my_action:
89+
...
90+
91+
If there's no entry for that action then just fallback to the regular permission_classes
92+
93+
Built-in actions:
94+
create
95+
retrieve
96+
update PUT
97+
partial_update PATCH
98+
destroy
99+
list
100+
"""
101+
permission_classes = self.permission_classes
102+
103+
try:
104+
action = self.action
105+
106+
# PUT and PATCH should use the same permission classes if not separately defined
107+
if action == 'partial_update' and 'partial_update' not in self.action_permission_classes:
108+
action = 'update'
109+
110+
permission_classes = self.action_permission_classes[action]
111+
112+
except (KeyError, AttributeError):
113+
pass
114+
115+
return [permission() for permission in permission_classes]
116+
117+
118+
class ConfigurableCreateModelMixin(mixins.CreateModelMixin):
119+
def create(self, request, *args, **kwargs):
120+
"""
121+
Provide SerializationType to get_serializer, re-serialize response if necessary, change status code if wanted
122+
"""
123+
config = self.get_configuration()
124+
125+
serializer = self.get_serializer(data=request.data)
126+
serializer.is_valid(raise_exception=config.raise_validation)
127+
instance = self.perform_create(serializer)
128+
headers = self.get_success_headers(serializer.data)
129+
130+
if config.re_serialize_response:
131+
serializer = self.get_serializer(instance=instance, serialization_type=SerializationType.RESPONSE)
132+
133+
return Response(serializer.data,
134+
status=config.success_status or status.HTTP_201_CREATED,
135+
headers=headers)
136+
137+
def perform_create(self, serializer):
138+
"""Returned the saved instance for later processing"""
139+
return serializer.save()
140+
141+
142+
class ConfigurableRetrieveModelMixin(mixins.RetrieveModelMixin):
143+
"""
144+
Retrieve a model instance. Provide SerializationType to get_serializer
145+
"""
146+
def retrieve(self, request, *args, **kwargs):
147+
instance = self.get_object()
148+
serializer = self.get_serializer(instance=instance, serialization_type=SerializationType.RESPONSE)
149+
return Response(serializer.data)
150+
151+
152+
class ConfigurableUpdateModelMixin(mixins.UpdateModelMixin):
153+
def update(self, request, *args, **kwargs):
154+
"""
155+
Provide SerializationType to get_serializer, re-serialize response if necessary, change status code if wanted
156+
"""
157+
config = self.get_configuration()
158+
159+
partial = kwargs.pop('partial', False)
160+
instance = self.get_object()
161+
serializer = self.get_serializer(instance=instance, data=request.data, partial=partial)
162+
serializer.is_valid(raise_exception=config.raise_validation)
163+
updated_instance = self.perform_update(serializer)
164+
165+
if getattr(instance, '_prefetched_objects_cache', None):
166+
# If 'prefetch_related' has been applied to a queryset, we need to
167+
# forcibly invalidate the prefetch cache on the instance.
168+
instance._prefetched_objects_cache = {} # pylint: disable=protected-access
169+
170+
if config.re_serialize_response:
171+
serializer = self.get_serializer(instance=updated_instance, serialization_type=SerializationType.RESPONSE)
172+
173+
return Response(serializer.data, status=config.success_status or status.HTTP_200_OK)
174+
175+
def perform_update(self, serializer):
176+
"""Returned the updated instance for later processing"""
177+
return serializer.save()
178+
179+
180+
class ConfigurableDestroyModelMixin(mixins.DestroyModelMixin):
181+
"""No specific Configurable changes; creating mixin for consistency"""
182+
183+
184+
class ConfigurableListModelMixin(mixins.ListModelMixin):
185+
"""
186+
List a queryset. Provide SerializationType to get_serializer
187+
"""
188+
def list(self, request, *args, **kwargs):
189+
queryset = self.filter_queryset(self.get_queryset())
190+
191+
page = self.paginate_queryset(queryset)
192+
if page is not None:
193+
serializer = self.get_serializer(page, many=True, serialization_type=SerializationType.RESPONSE)
194+
return self.get_paginated_response(serializer.data)
195+
196+
serializer = self.get_serializer(queryset, many=True, serialization_type=SerializationType.RESPONSE)
197+
return Response(serializer.data)

src/shipchain_common/test_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from requests.models import Response
2828
from rest_framework_simplejwt.utils import aware_utcnow, datetime_to_epoch
2929

30-
from src.shipchain_common.utils import validate_uuid4
30+
from .utils import validate_uuid4 # Imported for backwards compatibility usage of `from shipchain_common.test_utils`...
3131

3232

3333
def create_form_content(data):

src/shipchain_common/views.py

Lines changed: 0 additions & 108 deletions
This file was deleted.

0 commit comments

Comments
 (0)