Codebase list drf-extensions / f306fc8
New upstream version 0.6.0 Michael Fladischer 4 years ago
69 changed file(s) with 1025 addition(s) and 2391 deletion(s). Raw diff Collapse all Expand all
44 *.egg
55 .idea
66 env
7 build
78 dist
89 .DS_Store
00 language: python
11 cache: pip
2 dist: xenial
2 dist: bionic
33 sudo: false
44
55 python:
6 - 3.5
76 - 3.6
87 - 3.7
8 - 3.8
99
1010 install:
1111 - pip install tox tox-travis
00 build_docs:
11 PYTHONIOENCODING=utf-8 python docs/backdoc.py --title "Django Rest Framework extensions documentation" < docs/index.md > docs/index.html
2 python docs/post_process_docs.py
32
43 watch_docs:
54 make build_docs
1313
1414 ## Requirements
1515
16 * Tested for python 3.4, 3.5, 3.6 and 3.7 versions
17 * Tested for releases of Django Rest Framework 3.9
18 * Tested for Django from 1.11 to 2.2 versions
16 * Tested for Python 3.6, 3.7 and 3.8
17 * Tested for Django Rest Framework 3.9, 3.10 and 3.11
18 * Tested for Django 1.11, 2.1, 2.2 and 3.0
1919 * Tested for django-filter 2.1.0
2020
2121 ## Installation:
4646
4747 Running test for exact environment:
4848
49 $ tox -e py35 -- tests_app
49 $ tox -e py38 -- tests_app
5050
5151 Recreate envs before running tests:
5252
+0
-3
build/lib/rest_framework_extensions/__init__.py less more
0 __version__ = '0.4.0' # from 0.3.1
1
2 VERSION = __version__
+0
-1
build/lib/rest_framework_extensions/bulk_operations/__init__.py less more
0
+0
-102
build/lib/rest_framework_extensions/bulk_operations/mixins.py less more
0 # -*- coding: utf-8 -*-
1 from django.utils.encoding import force_text
2
3 from rest_framework import status
4 from rest_framework.response import Response
5 from rest_framework_extensions.settings import extensions_api_settings
6 from rest_framework_extensions import utils
7
8
9 class BulkOperationBaseMixin(object):
10 def is_object_operation(self):
11 return bool(self.get_object_lookup_value())
12
13 def get_object_lookup_value(self):
14 return self.kwargs.get(getattr(self, 'lookup_url_kwarg', None) or self.lookup_field, None)
15
16 def is_valid_bulk_operation(self):
17 if extensions_api_settings.DEFAULT_BULK_OPERATION_HEADER_NAME:
18 header_name = utils.prepare_header_name(extensions_api_settings.DEFAULT_BULK_OPERATION_HEADER_NAME)
19 return bool(self.request.META.get(header_name, None)), {
20 'detail': 'Header \'{0}\' should be provided for bulk operation.'.format(
21 extensions_api_settings.DEFAULT_BULK_OPERATION_HEADER_NAME
22 )
23 }
24 else:
25 return True, {}
26
27
28 class ListDestroyModelMixin(BulkOperationBaseMixin):
29 def delete(self, request, *args, **kwargs):
30 if self.is_object_operation():
31 return super(ListDestroyModelMixin, self).destroy(request, *args, **kwargs)
32 else:
33 return self.destroy_bulk(request, *args, **kwargs)
34
35 def destroy_bulk(self, request, *args, **kwargs):
36 is_valid, errors = self.is_valid_bulk_operation()
37 if is_valid:
38 queryset = self.filter_queryset(self.get_queryset())
39 self.pre_delete_bulk(queryset) # todo: test and document me
40 queryset.delete()
41 self.post_delete_bulk(queryset) # todo: test and document me
42 return Response(status=status.HTTP_204_NO_CONTENT)
43 else:
44 return Response(errors, status=status.HTTP_400_BAD_REQUEST)
45
46 def pre_delete_bulk(self, queryset):
47 """
48 Placeholder method for calling before deleting an queryset.
49 """
50 pass
51
52 def post_delete_bulk(self, queryset):
53 """
54 Placeholder method for calling after deleting an queryset.
55 """
56 pass
57
58
59 class ListUpdateModelMixin(BulkOperationBaseMixin):
60 def patch(self, request, *args, **kwargs):
61 if self.is_object_operation():
62 return super(ListUpdateModelMixin, self).partial_update(request, *args, **kwargs)
63 else:
64 return self.partial_update_bulk(request, *args, **kwargs)
65
66 def partial_update_bulk(self, request, *args, **kwargs):
67 is_valid, errors = self.is_valid_bulk_operation()
68 if is_valid:
69 queryset = self.filter_queryset(self.get_queryset())
70 update_bulk_dict = self.get_update_bulk_dict(serializer=self.get_serializer_class()(), data=request.data)
71 self.pre_save_bulk(queryset, update_bulk_dict) # todo: test and document me
72 try:
73 queryset.update(**update_bulk_dict)
74 except ValueError as e:
75 errors = {
76 'detail': force_text(e)
77 }
78 return Response(errors, status=status.HTTP_400_BAD_REQUEST)
79 self.post_save_bulk(queryset, update_bulk_dict) # todo: test and document me
80 return Response(status=status.HTTP_204_NO_CONTENT)
81 else:
82 return Response(errors, status=status.HTTP_400_BAD_REQUEST)
83
84 def get_update_bulk_dict(self, serializer, data):
85 update_bulk_dict = {}
86 for field_name, field in serializer.fields.items():
87 if field_name in data and not field.read_only:
88 update_bulk_dict[field.source or field_name] = data[field_name]
89 return update_bulk_dict
90
91 def pre_save_bulk(self, queryset, update_bulk_dict):
92 """
93 Placeholder method for calling before deleting an queryset.
94 """
95 pass
96
97 def post_save_bulk(self, queryset, update_bulk_dict):
98 """
99 Placeholder method for calling after deleting an queryset.
100 """
101 pass
+0
-0
build/lib/rest_framework_extensions/cache/__init__.py less more
(Empty file)
+0
-108
build/lib/rest_framework_extensions/cache/decorators.py less more
0 # -*- coding: utf-8 -*-
1 from functools import wraps
2
3 from django.http.response import HttpResponse
4 from django.utils.decorators import available_attrs
5
6
7 from rest_framework_extensions.settings import extensions_api_settings
8 from django.utils import six
9
10
11 def get_cache(alias):
12 from django.core.cache import caches
13 return caches[alias]
14
15
16 class CacheResponse(object):
17 def __init__(self,
18 timeout=None,
19 key_func=None,
20 cache=None,
21 cache_errors=None):
22 if timeout is None:
23 self.timeout = extensions_api_settings.DEFAULT_CACHE_RESPONSE_TIMEOUT
24 else:
25 self.timeout = timeout
26
27 if key_func is None:
28 self.key_func = extensions_api_settings.DEFAULT_CACHE_KEY_FUNC
29 else:
30 self.key_func = key_func
31
32 if cache_errors is None:
33 self.cache_errors = extensions_api_settings.DEFAULT_CACHE_ERRORS
34 else:
35 self.cache_errors = cache_errors
36
37 self.cache = get_cache(cache or extensions_api_settings.DEFAULT_USE_CACHE)
38
39 def __call__(self, func):
40 this = self
41 @wraps(func, assigned=available_attrs(func))
42 def inner(self, request, *args, **kwargs):
43 return this.process_cache_response(
44 view_instance=self,
45 view_method=func,
46 request=request,
47 args=args,
48 kwargs=kwargs,
49 )
50 return inner
51
52 def process_cache_response(self,
53 view_instance,
54 view_method,
55 request,
56 args,
57 kwargs):
58 key = self.calculate_key(
59 view_instance=view_instance,
60 view_method=view_method,
61 request=request,
62 args=args,
63 kwargs=kwargs
64 )
65 response = self.cache.get(key)
66 if not response:
67 response = view_method(view_instance, request, *args, **kwargs)
68 response = view_instance.finalize_response(request, response, *args, **kwargs)
69 response.render() # should be rendered, before picklining while storing to cache
70
71 if not response.status_code >= 400 or self.cache_errors:
72 response_dict = (
73 response.rendered_content,
74 response.status_code,
75 response._headers
76 )
77 self.cache.set(key, response_dict, self.timeout)
78 else:
79 content, status, headers = response
80 response = HttpResponse(content=content, status=status)
81 response._headers = headers
82
83 if not hasattr(response, '_closable_objects'):
84 response._closable_objects = []
85
86 return response
87
88 def calculate_key(self,
89 view_instance,
90 view_method,
91 request,
92 args,
93 kwargs):
94 if isinstance(self.key_func, six.string_types):
95 key_func = getattr(view_instance, self.key_func)
96 else:
97 key_func = self.key_func
98 return key_func(
99 view_instance=view_instance,
100 view_method=view_method,
101 request=request,
102 args=args,
103 kwargs=kwargs,
104 )
105
106
107 cache_response = CacheResponse
+0
-27
build/lib/rest_framework_extensions/cache/mixins.py less more
0 # -*- coding: utf-8 -*-
1 from rest_framework_extensions.cache.decorators import cache_response
2 from rest_framework_extensions.settings import extensions_api_settings
3
4
5 class BaseCacheResponseMixin(object):
6 # todo: test me. Create generic test like
7 # test_cache_reponse(view_instance, method, should_rebuild_after_method_evaluation)
8 object_cache_key_func = extensions_api_settings.DEFAULT_OBJECT_CACHE_KEY_FUNC
9 list_cache_key_func = extensions_api_settings.DEFAULT_LIST_CACHE_KEY_FUNC
10
11
12 class ListCacheResponseMixin(BaseCacheResponseMixin):
13 @cache_response(key_func='list_cache_key_func')
14 def list(self, request, *args, **kwargs):
15 return super(ListCacheResponseMixin, self).list(request, *args, **kwargs)
16
17
18 class RetrieveCacheResponseMixin(BaseCacheResponseMixin):
19 @cache_response(key_func='object_cache_key_func')
20 def retrieve(self, request, *args, **kwargs):
21 return super(RetrieveCacheResponseMixin, self).retrieve(request, *args, **kwargs)
22
23
24 class CacheResponseMixin(RetrieveCacheResponseMixin,
25 ListCacheResponseMixin):
26 pass
+0
-19
build/lib/rest_framework_extensions/compat.py less more
0 """
1 The `compat` module provides support for backwards compatibility with older
2 versions of django/python, and compatibility wrappers around optional packages.
3 """
4 from __future__ import unicode_literals
5
6 from django.utils import six
7
8
9 # handle different QuerySet representations
10 def queryset_to_value_list(queryset):
11 assert isinstance(queryset, six.string_types)
12
13 # django 1.10 introduces syntax "<QuerySet [(#1), (#2), ...]>"
14 # we extract only the list of tuples from the string
15 idx_bracket_open = queryset.find(u'[')
16 idx_bracket_close = queryset.rfind(u']')
17
18 return queryset[idx_bracket_open:idx_bracket_close + 1]
+0
-1
build/lib/rest_framework_extensions/etag/__init__.py less more
0
+0
-204
build/lib/rest_framework_extensions/etag/decorators.py less more
0 # -*- coding: utf-8 -*-
1 import logging
2 from functools import wraps
3
4 from django.utils.decorators import available_attrs
5 from django.utils.http import parse_etags, quote_etag
6
7 from rest_framework import status
8 from rest_framework.permissions import SAFE_METHODS
9 from rest_framework.response import Response
10 from rest_framework_extensions.exceptions import PreconditionRequiredException
11
12 from rest_framework_extensions.utils import prepare_header_name
13 from rest_framework_extensions.settings import extensions_api_settings
14 from django.utils import six
15
16 logger = logging.getLogger('django.request')
17
18
19 class ETAGProcessor(object):
20 """Based on https://github.com/django/django/blob/master/django/views/decorators/http.py"""
21
22 def __init__(self, etag_func=None, rebuild_after_method_evaluation=False):
23 if not etag_func:
24 etag_func = extensions_api_settings.DEFAULT_ETAG_FUNC
25 self.etag_func = etag_func
26 self.rebuild_after_method_evaluation = rebuild_after_method_evaluation
27
28 def __call__(self, func):
29 this = self
30
31 @wraps(func, assigned=available_attrs(func))
32 def inner(self, request, *args, **kwargs):
33 return this.process_conditional_request(
34 view_instance=self,
35 view_method=func,
36 request=request,
37 args=args,
38 kwargs=kwargs,
39 )
40
41 return inner
42
43 def process_conditional_request(self,
44 view_instance,
45 view_method,
46 request,
47 args,
48 kwargs):
49 etags, if_none_match, if_match = self.get_etags_and_matchers(request)
50 res_etag = self.calculate_etag(
51 view_instance=view_instance,
52 view_method=view_method,
53 request=request,
54 args=args,
55 kwargs=kwargs,
56 )
57
58 if self.is_if_none_match_failed(res_etag, etags, if_none_match):
59 if request.method in SAFE_METHODS:
60 response = Response(status=status.HTTP_304_NOT_MODIFIED)
61 else:
62 response = self._get_and_log_precondition_failed_response(request=request)
63 elif self.is_if_match_failed(res_etag, etags, if_match):
64 response = self._get_and_log_precondition_failed_response(request=request)
65 else:
66 response = view_method(view_instance, request, *args, **kwargs)
67 if self.rebuild_after_method_evaluation:
68 res_etag = self.calculate_etag(
69 view_instance=view_instance,
70 view_method=view_method,
71 request=request,
72 args=args,
73 kwargs=kwargs,
74 )
75
76 if res_etag and not response.has_header('ETag'):
77 response['ETag'] = quote_etag(res_etag)
78
79 return response
80
81 def get_etags_and_matchers(self, request):
82 etags = None
83 if_none_match = request.META.get(prepare_header_name("if-none-match"))
84 if_match = request.META.get(prepare_header_name("if-match"))
85 if if_none_match or if_match:
86 # There can be more than one ETag in the request, so we
87 # consider the list of values.
88 try:
89 etags = parse_etags(if_none_match or if_match)
90 except ValueError:
91 # In case of invalid etag ignore all ETag headers.
92 # Apparently Opera sends invalidly quoted headers at times
93 # (we should be returning a 400 response, but that's a
94 # little extreme) -- this is Django bug #10681.
95 if_none_match = None
96 if_match = None
97 return etags, if_none_match, if_match
98
99 def calculate_etag(self,
100 view_instance,
101 view_method,
102 request,
103 args,
104 kwargs):
105 if isinstance(self.etag_func, six.string_types):
106 etag_func = getattr(view_instance, self.etag_func)
107 else:
108 etag_func = self.etag_func
109 return etag_func(
110 view_instance=view_instance,
111 view_method=view_method,
112 request=request,
113 args=args,
114 kwargs=kwargs,
115 )
116
117 def is_if_none_match_failed(self, res_etag, etags, if_none_match):
118 if res_etag and if_none_match:
119 etags = [etag.strip('"') for etag in etags]
120 return res_etag in etags or '*' in etags
121 else:
122 return False
123
124 def is_if_match_failed(self, res_etag, etags, if_match):
125 if res_etag and if_match:
126 return res_etag not in etags and '*' not in etags
127 else:
128 return False
129
130 def _get_and_log_precondition_failed_response(self, request):
131 logger.warning('Precondition Failed: %s', request.path,
132 extra={
133 'status_code': status.HTTP_412_PRECONDITION_FAILED,
134 'request': request
135 }
136 )
137 return Response(status=status.HTTP_412_PRECONDITION_FAILED)
138
139
140 class APIETAGProcessor(ETAGProcessor):
141 """
142 This class is responsible for calculating the ETag value given (a list of) model instance(s).
143
144 It does not make sense to compute a default ETag here, because the processor would always issue a 304 response,
145 even if the response was modified meanwhile.
146 Therefore the `APIETAGProcessor` cannot be used without specifying an `etag_func` as keyword argument.
147
148 According to RFC 6585, conditional headers may be enforced for certain services that support conditional
149 requests. For optimistic locking, the server should respond status code 428 including a description on how
150 to resubmit the request successfully, see https://tools.ietf.org/html/rfc6585#section-3.
151 """
152
153 # require a pre-conditional header (e.g. If-Match) for unsafe HTTP methods (RFC 6585)
154 # override this defaults, if required
155 precondition_map = {'PUT': ['If-Match'],
156 'PATCH': ['If-Match'],
157 'DELETE': ['If-Match']}
158
159 def __init__(self, etag_func=None, rebuild_after_method_evaluation=False, precondition_map=None):
160 assert etag_func is not None, ('None-type functions are not allowed for processing API ETags.'
161 'You must specify a proper function to calculate the API ETags '
162 'using the "etag_func" keyword argument.')
163
164 if precondition_map is not None:
165 self.precondition_map = precondition_map
166 assert isinstance(self.precondition_map, dict), ('`precondition_map` must be a dict, where '
167 'the key is the HTTP verb, and the value is a list of '
168 'HTTP headers that must all be present for that request.')
169
170 super(APIETAGProcessor, self).__init__(etag_func=etag_func,
171 rebuild_after_method_evaluation=rebuild_after_method_evaluation)
172
173 def get_etags_and_matchers(self, request):
174 """Get the etags from the header and perform a validation against the required preconditions."""
175 # evaluate the preconditions, raises 428 if condition is not met
176 self.evaluate_preconditions(request)
177 # alright, headers are present, extract the values and match the conditions
178 return super(APIETAGProcessor, self).get_etags_and_matchers(request)
179
180 def evaluate_preconditions(self, request):
181 """Evaluate whether the precondition for the request is met."""
182 if request.method.upper() in self.precondition_map.keys():
183 required_headers = self.precondition_map.get(request.method.upper(), [])
184 # check the required headers
185 for header in required_headers:
186 if not request.META.get(prepare_header_name(header)):
187 # raise an error for each header that does not match
188 logger.warning('Precondition required: %s', request.path,
189 extra={
190 'status_code': status.HTTP_428_PRECONDITION_REQUIRED,
191 'request': request
192 }
193 )
194 # raise an RFC 6585 compliant exception
195 raise PreconditionRequiredException(detail='Precondition required. This "%s" request '
196 'is required to be conditional. '
197 'Try again using "%s".' % (request.method, header)
198 )
199 return True
200
201
202 etag = ETAGProcessor
203 api_etag = APIETAGProcessor
+0
-87
build/lib/rest_framework_extensions/etag/mixins.py less more
0 # -*- coding: utf-8 -*-
1 from rest_framework_extensions.etag.decorators import etag, api_etag
2 from rest_framework_extensions.settings import extensions_api_settings
3
4
5 class BaseETAGMixin(object):
6 # todo: test me. Create generic test like test_etag(view_instance, method, should_rebuild_after_method_evaluation)
7 object_etag_func = extensions_api_settings.DEFAULT_OBJECT_ETAG_FUNC
8 list_etag_func = extensions_api_settings.DEFAULT_LIST_ETAG_FUNC
9
10
11 class ListETAGMixin(BaseETAGMixin):
12 @etag(etag_func='list_etag_func')
13 def list(self, request, *args, **kwargs):
14 return super(ListETAGMixin, self).list(request, *args, **kwargs)
15
16
17 class RetrieveETAGMixin(BaseETAGMixin):
18 @etag(etag_func='object_etag_func')
19 def retrieve(self, request, *args, **kwargs):
20 return super(RetrieveETAGMixin, self).retrieve(request, *args, **kwargs)
21
22
23 class UpdateETAGMixin(BaseETAGMixin):
24 @etag(etag_func='object_etag_func', rebuild_after_method_evaluation=True)
25 def update(self, request, *args, **kwargs):
26 return super(UpdateETAGMixin, self).update(request, *args, **kwargs)
27
28
29 class DestroyETAGMixin(BaseETAGMixin):
30 @etag(etag_func='object_etag_func')
31 def destroy(self, request, *args, **kwargs):
32 return super(DestroyETAGMixin, self).destroy(request, *args, **kwargs)
33
34
35 class ReadOnlyETAGMixin(RetrieveETAGMixin,
36 ListETAGMixin):
37 pass
38
39
40 class ETAGMixin(RetrieveETAGMixin,
41 UpdateETAGMixin,
42 DestroyETAGMixin,
43 ListETAGMixin):
44 pass
45
46
47 class APIBaseETAGMixin(object):
48 # todo: test me. Create generic test like test_etag(view_instance, method, should_rebuild_after_method_evaluation)
49 api_object_etag_func = extensions_api_settings.DEFAULT_API_OBJECT_ETAG_FUNC
50 api_list_etag_func = extensions_api_settings.DEFAULT_API_LIST_ETAG_FUNC
51
52
53 class APIListETAGMixin(APIBaseETAGMixin):
54 @api_etag(etag_func='api_list_etag_func')
55 def list(self, request, *args, **kwargs):
56 return super(APIListETAGMixin, self).list(request, *args, **kwargs)
57
58
59 class APIRetrieveETAGMixin(APIBaseETAGMixin):
60 @api_etag(etag_func='api_object_etag_func')
61 def retrieve(self, request, *args, **kwargs):
62 return super(APIRetrieveETAGMixin, self).retrieve(request, *args, **kwargs)
63
64
65 class APIUpdateETAGMixin(APIBaseETAGMixin):
66 @api_etag(etag_func='api_object_etag_func', rebuild_after_method_evaluation=True)
67 def update(self, request, *args, **kwargs):
68 return super(APIUpdateETAGMixin, self).update(request, *args, **kwargs)
69
70
71 class APIDestroyETAGMixin(APIBaseETAGMixin):
72 @api_etag(etag_func='api_object_etag_func')
73 def destroy(self, request, *args, **kwargs):
74 return super(APIDestroyETAGMixin, self).destroy(request, *args, **kwargs)
75
76
77 class APIReadOnlyETAGMixin(APIRetrieveETAGMixin,
78 APIListETAGMixin):
79 pass
80
81
82 class APIETAGMixin(APIRetrieveETAGMixin,
83 APIUpdateETAGMixin,
84 APIDestroyETAGMixin,
85 APIListETAGMixin):
86 pass
+0
-9
build/lib/rest_framework_extensions/exceptions.py less more
0 from django.utils.translation import ugettext_lazy as _
1 from rest_framework import status
2 from rest_framework.exceptions import APIException
3
4
5 class PreconditionRequiredException(APIException):
6 status_code = status.HTTP_428_PRECONDITION_REQUIRED
7 default_detail = _('This "{method}" request is required to be conditional.')
8 default_code = 'precondition_required'
+0
-28
build/lib/rest_framework_extensions/fields.py less more
0 # -*- coding: utf-8 -*-
1 from rest_framework.relations import HyperlinkedRelatedField
2
3
4 class ResourceUriField(HyperlinkedRelatedField):
5 """
6 Represents a hyperlinking uri that points to the detail view for that object.
7
8 Example:
9 class SurveySerializer(serializers.ModelSerializer):
10 resource_uri = ResourceUriField(view_name='survey-detail')
11
12 class Meta:
13 model = Survey
14 fields = ('id', 'resource_uri')
15
16 ...
17 {
18 "id": 1,
19 "resource_uri": "http://localhost/v1/surveys/1/",
20 }
21 """
22 # todo: test me
23 read_only = True
24
25 def __init__(self, *args, **kwargs):
26 kwargs.setdefault('source', '*')
27 super(ResourceUriField, self).__init__(*args, **kwargs)
+0
-0
build/lib/rest_framework_extensions/key_constructor/__init__.py less more
(Empty file)
+0
-276
build/lib/rest_framework_extensions/key_constructor/bits.py less more
0 # -*- coding: utf-8 -*-
1 from django.utils.translation import get_language
2 from django.db.models.query import EmptyQuerySet
3 from django.db.models.sql.datastructures import EmptyResultSet
4
5 from django.utils.encoding import force_text
6
7 from rest_framework_extensions import compat
8
9
10 class AllArgsMixin(object):
11
12 def __init__(self, params='*'):
13 super(AllArgsMixin, self).__init__(params)
14
15
16 class KeyBitBase(object):
17 def __init__(self, params=None):
18 self.params = params
19
20 def get_data(self, params, view_instance, view_method, request, args, kwargs):
21 """
22 @rtype: dict
23 """
24 raise NotImplementedError()
25
26
27 class KeyBitDictBase(KeyBitBase):
28 """Base class for dict-like source data processing.
29
30 Look at HeadersKeyBit and QueryParamsKeyBit
31
32 """
33
34 def get_data(self, params, view_instance, view_method, request, args, kwargs):
35 data = {}
36
37 if params is not None:
38 source_dict = self.get_source_dict(
39 params=params,
40 view_instance=view_instance,
41 view_method=view_method,
42 request=request,
43 args=args,
44 kwargs=kwargs
45 )
46
47 if params == '*':
48 params = source_dict.keys()
49
50 for key in params:
51 value = source_dict.get(self.prepare_key_for_value_retrieving(key))
52 if value is not None:
53 data[self.prepare_key_for_value_assignment(key)] = force_text(value)
54
55 return data
56
57 def get_source_dict(self, params, view_instance, view_method, request, args, kwargs):
58 raise NotImplementedError()
59
60 def prepare_key_for_value_retrieving(self, key):
61 return key
62
63 def prepare_key_for_value_assignment(self, key):
64 return key
65
66
67 class UniqueViewIdKeyBit(KeyBitBase):
68 def get_data(self, params, view_instance, view_method, request, args, kwargs):
69 return u'.'.join([
70 view_instance.__module__,
71 view_instance.__class__.__name__
72 ])
73
74
75 class UniqueMethodIdKeyBit(KeyBitBase):
76 def get_data(self, params, view_instance, view_method, request, args, kwargs):
77 return u'.'.join([
78 view_instance.__module__,
79 view_instance.__class__.__name__,
80 view_method.__name__
81 ])
82
83
84 class LanguageKeyBit(KeyBitBase):
85 """
86 Return example:
87 u'en'
88
89 """
90
91 def get_data(self, params, view_instance, view_method, request, args, kwargs):
92 return force_text(get_language())
93
94
95 class FormatKeyBit(KeyBitBase):
96 """
97 Return example for json:
98 u'json'
99
100 Return example for html:
101 u'html'
102 """
103
104 def get_data(self, params, view_instance, view_method, request, args, kwargs):
105 return force_text(request.accepted_renderer.format)
106
107
108 class UserKeyBit(KeyBitBase):
109 """
110 Return example for anonymous:
111 u'anonymous'
112
113 Return example for authenticated (value is user id):
114 u'10'
115 """
116
117 def get_data(self, params, view_instance, view_method, request, args, kwargs):
118 if hasattr(request, 'user') and request.user and request.user.is_authenticated:
119 return force_text(self._get_id_from_user(request.user))
120 else:
121 return u'anonymous'
122
123 def _get_id_from_user(self, user):
124 return user.id
125
126
127 class HeadersKeyBit(KeyBitDictBase):
128 """
129 Return example:
130 {'accept-language': u'ru', 'x-geobase-id': '123'}
131
132 """
133 def get_source_dict(self, params, view_instance, view_method, request, args, kwargs):
134 return request.META
135
136 def prepare_key_for_value_retrieving(self, key):
137 from rest_framework_extensions.utils import prepare_header_name
138
139 return prepare_header_name(key.lower()) # Accept-Language => http_accept_language
140
141 def prepare_key_for_value_assignment(self, key):
142 return key.lower() # Accept-Language => accept-language
143
144
145 class RequestMetaKeyBit(KeyBitDictBase):
146 """
147 Return example:
148 {'REMOTE_ADDR': u'127.0.0.2', 'REMOTE_HOST': u'yandex.ru'}
149
150 """
151
152 def get_source_dict(self, params, view_instance, view_method, request, args, kwargs):
153 return request.META
154
155
156 class QueryParamsKeyBit(AllArgsMixin, KeyBitDictBase):
157 """
158 Return example:
159 {'part': 'Londo', 'callback': 'jquery_callback'}
160
161 """
162
163 def get_source_dict(self, params, view_instance, view_method, request, args, kwargs):
164 return request.GET
165
166
167 class PaginationKeyBit(QueryParamsKeyBit):
168 """
169 Return example:
170 {'page_size': 100, 'page': '1'}
171
172 """
173 def get_data(self, **kwargs):
174 kwargs['params'] = []
175 if hasattr(kwargs['view_instance'], 'paginator'):
176 if hasattr(kwargs['view_instance'].paginator, 'page_query_param'):
177 kwargs['params'].append(
178 kwargs['view_instance'].paginator.page_query_param)
179 if hasattr(kwargs['view_instance'].paginator,
180 'page_size_query_param'):
181 kwargs['params'].append(
182 kwargs['view_instance'].paginator.page_size_query_param)
183 return super(PaginationKeyBit, self).get_data(**kwargs)
184
185
186 class SqlQueryKeyBitBase(KeyBitBase):
187 def _get_queryset_query_string(self, queryset):
188 if isinstance(queryset, EmptyQuerySet):
189 return None
190 else:
191 try:
192 return force_text(queryset.query.__str__())
193 except EmptyResultSet:
194 return None
195
196
197 class ModelInstanceKeyBitBase(KeyBitBase):
198 """
199 Return the actual contents of the query set.
200 This class is similar to the `SqlQueryKeyBitBase`.
201 """
202 def _get_queryset_query_values(self, queryset):
203 if isinstance(queryset, EmptyQuerySet) or queryset.count() == 0:
204 return None
205 else:
206 try:
207 # run through the instances and collect all values in ordered fashion
208 return compat.queryset_to_value_list(force_text(queryset.values_list()))
209 except EmptyResultSet:
210 return None
211
212
213 class ListSqlQueryKeyBit(SqlQueryKeyBitBase):
214 def get_data(self, params, view_instance, view_method, request, args, kwargs):
215 queryset = view_instance.filter_queryset(view_instance.get_queryset())
216 return self._get_queryset_query_string(queryset)
217
218
219 class RetrieveSqlQueryKeyBit(SqlQueryKeyBitBase):
220 def get_data(self, params, view_instance, view_method, request, args, kwargs):
221 lookup_value = view_instance.kwargs[view_instance.lookup_field]
222 try:
223 queryset = view_instance.filter_queryset(view_instance.get_queryset()).filter(
224 **{view_instance.lookup_field: lookup_value}
225 )
226 except ValueError:
227 return None
228 else:
229 return self._get_queryset_query_string(queryset)
230
231
232 class RetrieveModelKeyBit(ModelInstanceKeyBitBase):
233 """
234 A key bit reflecting the contents of the model instance.
235 Return example:
236 u"[(3, False)]"
237 """
238 def get_data(self, params, view_instance, view_method, request, args, kwargs):
239 lookup_value = view_instance.kwargs[view_instance.lookup_field]
240 try:
241 queryset = view_instance.filter_queryset(view_instance.get_queryset()).filter(
242 **{view_instance.lookup_field: lookup_value}
243 )
244 except ValueError:
245 return None
246 else:
247 return self._get_queryset_query_values(queryset)
248
249
250 class ListModelKeyBit(ModelInstanceKeyBitBase):
251 """
252 A key bit reflecting the contents of a list of model instances.
253 Return example:
254 u"[(1, True), (2, True), (3, False)]"
255 """
256 def get_data(self, params, view_instance, view_method, request, args, kwargs):
257 queryset = view_instance.filter_queryset(view_instance.get_queryset())
258 return self._get_queryset_query_values(queryset)
259
260
261 class ArgsKeyBit(AllArgsMixin, KeyBitBase):
262
263 def get_data(self, params, view_instance, view_method, request, args, kwargs):
264 if params == '*':
265 return args
266 elif params is not None:
267 return [args[i] for i in params]
268 else:
269 return []
270
271
272 class KwargsKeyBit(AllArgsMixin, KeyBitDictBase):
273
274 def get_source_dict(self, params, view_instance, view_method, request, args, kwargs):
275 return kwargs
+0
-122
build/lib/rest_framework_extensions/key_constructor/constructors.py less more
0 # -*- coding: utf-8 -*-
1 import hashlib
2 import json
3
4 from rest_framework_extensions.key_constructor import bits
5 from rest_framework_extensions.settings import extensions_api_settings
6
7
8 class KeyConstructor(object):
9 def __init__(self, memoize_for_request=None, params=None):
10 if memoize_for_request is None:
11 self.memoize_for_request = extensions_api_settings.DEFAULT_KEY_CONSTRUCTOR_MEMOIZE_FOR_REQUEST
12 else:
13 self.memoize_for_request = memoize_for_request
14 if params is None:
15 self.params = {}
16 else:
17 self.params = params
18 self.bits = self.get_bits()
19
20 def get_bits(self):
21 _bits = {}
22 for attr in dir(self.__class__):
23 attr_value = getattr(self.__class__, attr)
24 if isinstance(attr_value, bits.KeyBitBase):
25 _bits[attr] = attr_value
26 return _bits
27
28 def __call__(self, **kwargs):
29 return self.get_key(**kwargs)
30
31 def get_key(self, view_instance, view_method, request, args, kwargs):
32 if self.memoize_for_request:
33 memoization_key = self._get_memoization_key(
34 view_instance=view_instance,
35 view_method=view_method,
36 args=args,
37 kwargs=kwargs
38 )
39 if not hasattr(request, '_key_constructor_cache'):
40 request._key_constructor_cache = {}
41 if self.memoize_for_request and memoization_key in request._key_constructor_cache:
42 return request._key_constructor_cache.get(memoization_key)
43 else:
44 value = self._get_key(
45 view_instance=view_instance,
46 view_method=view_method,
47 request=request,
48 args=args,
49 kwargs=kwargs
50 )
51 if self.memoize_for_request:
52 request._key_constructor_cache[memoization_key] = value
53 return value
54
55 def _get_memoization_key(self, view_instance, view_method, args, kwargs):
56 from rest_framework_extensions.utils import get_unique_method_id
57 return json.dumps({
58 'unique_method_id': get_unique_method_id(view_instance=view_instance, view_method=view_method),
59 'args': args,
60 'kwargs': kwargs,
61 'instance_id': id(self)
62 })
63
64 def _get_key(self, view_instance, view_method, request, args, kwargs):
65 _kwargs = {
66 'view_instance': view_instance,
67 'view_method': view_method,
68 'request': request,
69 'args': args,
70 'kwargs': kwargs,
71 }
72 return self.prepare_key(
73 self.get_data_from_bits(**_kwargs)
74 )
75
76 def prepare_key(self, key_dict):
77 return hashlib.md5(json.dumps(key_dict, sort_keys=True).encode('utf-8')).hexdigest()
78
79 def get_data_from_bits(self, **kwargs):
80 result_dict = {}
81 for bit_name, bit_instance in self.bits.items():
82 if bit_name in self.params:
83 params = self.params[bit_name]
84 else:
85 try:
86 params = bit_instance.params
87 except AttributeError:
88 params = None
89 result_dict[bit_name] = bit_instance.get_data(params=params, **kwargs)
90 return result_dict
91
92
93 class DefaultKeyConstructor(KeyConstructor):
94 unique_method_id = bits.UniqueMethodIdKeyBit()
95 format = bits.FormatKeyBit()
96 language = bits.LanguageKeyBit()
97
98
99 class DefaultObjectKeyConstructor(DefaultKeyConstructor):
100 retrieve_sql_query = bits.RetrieveSqlQueryKeyBit()
101
102
103 class DefaultListKeyConstructor(DefaultKeyConstructor):
104 list_sql_query = bits.ListSqlQueryKeyBit()
105 pagination = bits.PaginationKeyBit()
106
107
108 class DefaultAPIModelInstanceKeyConstructor(KeyConstructor):
109 """
110 Use this constructor when the values of the model instance are required
111 to identify the resource.
112 """
113 retrieve_model_values = bits.RetrieveModelKeyBit()
114
115
116 class DefaultAPIModelListKeyConstructor(KeyConstructor):
117 """
118 Use this constructor when the values of the model instance are required
119 to identify many resources.
120 """
121 list_model_values = bits.ListModelKeyBit()
+0
-84
build/lib/rest_framework_extensions/mixins.py less more
0 # -*- coding: utf-8 -*-
1 # Try to import six from Django, fallback to included `six`.
2
3 from django.utils import six
4
5
6 from rest_framework_extensions.cache.mixins import CacheResponseMixin
7 # from rest_framework_extensions.etag.mixins import ReadOnlyETAGMixin, ETAGMixin
8 from rest_framework_extensions.bulk_operations.mixins import ListUpdateModelMixin
9 from rest_framework_extensions.settings import extensions_api_settings
10 from django.http import Http404
11
12
13 class DetailSerializerMixin(object):
14 """
15 Add custom serializer for detail view
16 """
17 serializer_detail_class = None
18 queryset_detail = None
19
20 def get_serializer_class(self):
21 error_message = "'{0}' should include a 'serializer_detail_class' attribute".format(self.__class__.__name__)
22 assert self.serializer_detail_class is not None, error_message
23 if self._is_request_to_detail_endpoint():
24 return self.serializer_detail_class
25 else:
26 return super(DetailSerializerMixin, self).get_serializer_class()
27
28 def get_queryset(self, *args, **kwargs):
29 if self._is_request_to_detail_endpoint() and self.queryset_detail is not None:
30 return self.queryset_detail.all() # todo: test all()
31 else:
32 return super(DetailSerializerMixin, self).get_queryset(*args, **kwargs)
33
34 def _is_request_to_detail_endpoint(self):
35 if hasattr(self, 'lookup_url_kwarg'):
36 lookup = self.lookup_url_kwarg or self.lookup_field
37 return lookup and lookup in self.kwargs
38
39
40 class PaginateByMaxMixin(object):
41
42 def get_page_size(self, request):
43 if self.page_size_query_param and self.max_page_size and request.query_params.get(self.page_size_query_param) == 'max':
44 return self.max_page_size
45 return super(PaginateByMaxMixin, self).get_page_size(request)
46
47
48 # class ReadOnlyCacheResponseAndETAGMixin(ReadOnlyETAGMixin, CacheResponseMixin):
49 # pass
50
51
52 # class CacheResponseAndETAGMixin(ETAGMixin, CacheResponseMixin):
53 # pass
54
55
56 class NestedViewSetMixin(object):
57 def get_queryset(self):
58 return self.filter_queryset_by_parents_lookups(
59 super(NestedViewSetMixin, self).get_queryset()
60 )
61
62 def filter_queryset_by_parents_lookups(self, queryset):
63 parents_query_dict = self.get_parents_query_dict()
64 if parents_query_dict:
65 try:
66 return queryset.filter(**parents_query_dict)
67 except ValueError:
68 raise Http404
69 else:
70 return queryset
71
72 def get_parents_query_dict(self):
73 result = {}
74 for kwarg_name, kwarg_value in six.iteritems(self.kwargs):
75 if kwarg_name.startswith(extensions_api_settings.DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX):
76 query_lookup = kwarg_name.replace(
77 extensions_api_settings.DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX,
78 '',
79 1
80 )
81 query_value = kwarg_value
82 result[query_lookup] = query_value
83 return result
+0
-0
build/lib/rest_framework_extensions/models.py less more
(Empty file)
+0
-21
build/lib/rest_framework_extensions/permissions.py less more
0 # -*- coding: utf-8 -*-
1 from rest_framework.permissions import DjangoObjectPermissions
2
3
4 class ExtendedDjangoObjectPermissions(DjangoObjectPermissions):
5 hide_forbidden_for_read_objects = True
6
7 def has_object_permission(self, request, view, obj):
8 if self.hide_forbidden_for_read_objects:
9 return super(ExtendedDjangoObjectPermissions, self).has_object_permission(request, view, obj)
10 else:
11 model_cls = getattr(view, 'model', None)
12 queryset = getattr(view, 'queryset', None)
13
14 if model_cls is None and queryset is not None:
15 model_cls = queryset.model
16
17 perms = self.get_required_object_permissions(request.method, model_cls)
18 user = request.user
19
20 return user.has_perms(perms, obj)
+0
-71
build/lib/rest_framework_extensions/routers.py less more
0 # -*- coding: utf-8 -*-
1 from rest_framework.routers import DefaultRouter, SimpleRouter
2 from rest_framework_extensions.utils import compose_parent_pk_kwarg_name
3
4
5 class NestedRegistryItem(object):
6 def __init__(self, router, parent_prefix, parent_item=None, parent_viewset=None):
7 self.router = router
8 self.parent_prefix = parent_prefix
9 self.parent_item = parent_item
10 self.parent_viewset = parent_viewset
11
12 def register(self, prefix, viewset, basename, parents_query_lookups):
13 self.router._register(
14 prefix=self.get_prefix(current_prefix=prefix, parents_query_lookups=parents_query_lookups),
15 viewset=viewset,
16 basename=basename,
17 )
18 return NestedRegistryItem(
19 router=self.router,
20 parent_prefix=prefix,
21 parent_item=self,
22 parent_viewset=viewset
23 )
24
25 def get_prefix(self, current_prefix, parents_query_lookups):
26 return '{0}/{1}'.format(
27 self.get_parent_prefix(parents_query_lookups),
28 current_prefix
29 )
30
31 def get_parent_prefix(self, parents_query_lookups):
32 prefix = '/'
33 current_item = self
34 i = len(parents_query_lookups) - 1
35 while current_item:
36 parent_lookup_value_regex = getattr(current_item.parent_viewset, 'lookup_value_regex', '[^/.]+')
37 prefix = '{parent_prefix}/(?P<{parent_pk_kwarg_name}>{parent_lookup_value_regex})/{prefix}'.format(
38 parent_prefix=current_item.parent_prefix,
39 parent_pk_kwarg_name=compose_parent_pk_kwarg_name(parents_query_lookups[i]),
40 parent_lookup_value_regex=parent_lookup_value_regex,
41 prefix=prefix
42 )
43 i -= 1
44 current_item = current_item.parent_item
45 return prefix.strip('/')
46
47
48 class NestedRouterMixin(object):
49 def _register(self, *args, **kwargs):
50 return super(NestedRouterMixin, self).register(*args, **kwargs)
51
52 def register(self, *args, **kwargs):
53 self._register(*args, **kwargs)
54 return NestedRegistryItem(
55 router=self,
56 parent_prefix=self.registry[-1][0],
57 parent_viewset=self.registry[-1][1]
58 )
59
60
61 class ExtendedRouterMixin(NestedRouterMixin):
62 pass
63
64
65 class ExtendedSimpleRouter(ExtendedRouterMixin, SimpleRouter):
66 pass
67
68
69 class ExtendedDefaultRouter(ExtendedRouterMixin, DefaultRouter):
70 pass
+0
-44
build/lib/rest_framework_extensions/serializers.py less more
0 # -*- coding: utf-8 -*-
1 from rest_framework_extensions.utils import get_model_opts_concrete_fields
2
3
4 def get_fields_for_partial_update(opts, init_data, fields, init_files=None):
5 opts = opts.model._meta.concrete_model._meta
6 partial_fields = list((init_data or {}).keys()) + list((init_files or {}).keys())
7 concrete_field_names = []
8 for field in get_model_opts_concrete_fields(opts):
9 if not field.primary_key:
10 concrete_field_names.append(field.name)
11 if field.name != field.attname:
12 concrete_field_names.append(field.attname)
13 update_fields = []
14 for field_name in partial_fields:
15 if field_name in fields:
16 model_field_name = getattr(fields[field_name], 'source') or field_name
17 if model_field_name in concrete_field_names:
18 update_fields.append(model_field_name)
19 return update_fields
20
21
22 class PartialUpdateSerializerMixin(object):
23 def save(self, **kwargs):
24 self._update_fields = kwargs.get('update_fields', None)
25 return super(PartialUpdateSerializerMixin, self).save(**kwargs)
26
27 def update(self, instance, validated_attrs):
28 for attr, value in validated_attrs.items():
29 if hasattr(getattr(instance, attr, None), 'set'):
30 getattr(instance, attr).set(value)
31 else:
32 setattr(instance, attr, value)
33 if self.partial and isinstance(instance, self.Meta.model):
34 instance.save(
35 update_fields=getattr(self, '_update_fields') or get_fields_for_partial_update(
36 opts=self.Meta,
37 init_data=self.get_initial(),
38 fields=self.fields.fields
39 )
40 )
41 else:
42 instance.save()
43 return instance
+0
-46
build/lib/rest_framework_extensions/settings.py less more
0 # -*- coding: utf-8 -*-
1 from django.conf import settings
2
3 from rest_framework.settings import APISettings
4
5
6 USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK_EXTENSIONS', None)
7
8 DEFAULTS = {
9 # caching
10 'DEFAULT_USE_CACHE': 'default',
11 'DEFAULT_CACHE_RESPONSE_TIMEOUT': None,
12 'DEFAULT_CACHE_ERRORS': True,
13 'DEFAULT_CACHE_KEY_FUNC': 'rest_framework_extensions.utils.default_cache_key_func',
14 'DEFAULT_OBJECT_CACHE_KEY_FUNC': 'rest_framework_extensions.utils.default_object_cache_key_func',
15 'DEFAULT_LIST_CACHE_KEY_FUNC': 'rest_framework_extensions.utils.default_list_cache_key_func',
16
17 # ETAG
18 'DEFAULT_ETAG_FUNC': 'rest_framework_extensions.utils.default_etag_func',
19 'DEFAULT_OBJECT_ETAG_FUNC': 'rest_framework_extensions.utils.default_object_etag_func',
20 'DEFAULT_LIST_ETAG_FUNC': 'rest_framework_extensions.utils.default_list_etag_func',
21
22 # API - ETAG
23 'DEFAULT_API_OBJECT_ETAG_FUNC': 'rest_framework_extensions.utils.default_api_object_etag_func',
24 'DEFAULT_API_LIST_ETAG_FUNC': 'rest_framework_extensions.utils.default_api_list_etag_func',
25
26 # other
27 'DEFAULT_KEY_CONSTRUCTOR_MEMOIZE_FOR_REQUEST': False,
28 'DEFAULT_BULK_OPERATION_HEADER_NAME': 'X-BULK-OPERATION',
29 'DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX': 'parent_lookup_'
30 }
31
32 IMPORT_STRINGS = [
33 'DEFAULT_CACHE_KEY_FUNC',
34 'DEFAULT_OBJECT_CACHE_KEY_FUNC',
35 'DEFAULT_LIST_CACHE_KEY_FUNC',
36 'DEFAULT_ETAG_FUNC',
37 'DEFAULT_OBJECT_ETAG_FUNC',
38 'DEFAULT_LIST_ETAG_FUNC',
39 # API - ETAG
40 'DEFAULT_API_OBJECT_ETAG_FUNC',
41 'DEFAULT_API_LIST_ETAG_FUNC',
42 ]
43
44
45 extensions_api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
+0
-173
build/lib/rest_framework_extensions/test.py less more
0 # -- coding: utf-8 --
1
2 # Note that we import as `DjangoRequestFactory` and `DjangoClient` in order
3 # to make it harder for the user to import the wrong thing without realizing.
4 from __future__ import unicode_literals
5 import django
6 from django.conf import settings
7 from django.test.client import Client as DjangoClient
8 from django.test.client import ClientHandler
9 from django.test import testcases
10 from django.utils.http import urlencode
11 from rest_framework.settings import api_settings
12 from django.test.client import RequestFactory # changed here
13 from django.utils.encoding import force_bytes, six # changed here
14
15
16 def force_authenticate(request, user=None, token=None):
17 request._force_auth_user = user
18 request._force_auth_token = token
19
20
21 class APIRequestFactory(RequestFactory):
22 renderer_classes_list = api_settings.TEST_REQUEST_RENDERER_CLASSES
23 default_format = api_settings.TEST_REQUEST_DEFAULT_FORMAT
24
25 def __init__(self, enforce_csrf_checks=False, **defaults):
26 self.enforce_csrf_checks = enforce_csrf_checks
27 self.renderer_classes = {}
28 for cls in self.renderer_classes_list:
29 self.renderer_classes[cls.format] = cls
30 super(APIRequestFactory, self).__init__(**defaults)
31
32 def _encode_data(self, data, format=None, content_type=None):
33 """
34 Encode the data returning a two tuple of (bytes, content_type)
35 """
36
37 if not data:
38 return ('', None)
39
40 assert format is None or content_type is None, (
41 'You may not set both `format` and `content_type`.'
42 )
43
44 if content_type:
45 # Content type specified explicitly, treat data as a raw bytestring
46 ret = force_bytes(data, settings.DEFAULT_CHARSET)
47
48 else:
49 format = format or self.default_format
50
51 assert format in self.renderer_classes, (
52 "Invalid format '{0}'."
53 "Available formats are {1}. Set TEST_REQUEST_RENDERER_CLASSES "
54 "to enable extra request formats.".format(
55 format,
56 ', '.join(
57 ["'" + fmt + "'" for fmt in self.renderer_classes.keys()])
58 )
59 )
60
61 # Use format and render the data into a bytestring
62 renderer = self.renderer_classes[format]()
63 ret = renderer.render(data)
64
65 # Determine the content-type header from the renderer
66 content_type = "{0}; charset={1}".format(
67 renderer.media_type, renderer.charset
68 )
69
70 # Coerce text to bytes if required.
71 if isinstance(ret, six.text_type):
72 ret = bytes(ret.encode(renderer.charset))
73
74 return ret, content_type
75
76 def get(self, path, data=None, **extra):
77 r = {
78 'QUERY_STRING': urlencode(data or {}, doseq=True),
79 }
80 # Fix to support old behavior where you have the arguments in the url
81 # See #1461
82 if not data and '?' in path:
83 r['QUERY_STRING'] = path.split('?')[1]
84 r.update(extra)
85 return self.generic('GET', path, **r)
86
87 def post(self, path, data=None, format=None, content_type=None, **extra):
88 data, content_type = self._encode_data(data, format, content_type)
89 return self.generic('POST', path, data, content_type, **extra)
90
91 def put(self, path, data=None, format=None, content_type=None, **extra):
92 data, content_type = self._encode_data(data, format, content_type)
93 return self.generic('PUT', path, data, content_type, **extra)
94
95 def patch(self, path, data=None, format=None, content_type=None, **extra):
96 data, content_type = self._encode_data(data, format, content_type)
97 return self.generic('PATCH', path, data, content_type, **extra)
98
99 def delete(self, path, data=None, format=None, content_type=None, **extra):
100 data, content_type = self._encode_data(data, format, content_type)
101 return self.generic('DELETE', path, data, content_type, **extra)
102
103 def options(self, path, data=None, format=None, content_type=None, **extra):
104 data, content_type = self._encode_data(data, format, content_type)
105 return self.generic('OPTIONS', path, data, content_type, **extra)
106
107 def request(self, **kwargs):
108 request = super(APIRequestFactory, self).request(**kwargs)
109 request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
110 return request
111
112
113 class ForceAuthClientHandler(ClientHandler):
114 """
115 A patched version of ClientHandler that can enforce authentication
116 on the outgoing requests.
117 """
118
119 def __init__(self, *args, **kwargs):
120 self._force_user = None
121 self._force_token = None
122 super(ForceAuthClientHandler, self).__init__(*args, **kwargs)
123
124 def get_response(self, request):
125 # This is the simplest place we can hook into to patch the
126 # request object.
127 force_authenticate(request, self._force_user, self._force_token)
128 return super(ForceAuthClientHandler, self).get_response(request)
129
130
131 class APIClient(APIRequestFactory, DjangoClient):
132 def __init__(self, enforce_csrf_checks=False, **defaults):
133 super(APIClient, self).__init__(**defaults)
134 self.handler = ForceAuthClientHandler(enforce_csrf_checks)
135 self._credentials = {}
136
137 def credentials(self, **kwargs):
138 """
139 Sets headers that will be used on every outgoing request.
140 """
141 self._credentials = kwargs
142
143 def force_authenticate(self, user=None, token=None):
144 """
145 Forcibly authenticates outgoing requests with the given
146 user and/or token.
147 """
148 self.handler._force_user = user
149 self.handler._force_token = token
150 if user is None:
151 self.logout() # Also clear any possible session info if required
152
153 def request(self, **kwargs):
154 # Ensure that any credentials set get added to every request.
155 kwargs.update(self._credentials)
156 return super(APIClient, self).request(**kwargs)
157
158
159 class APITransactionTestCase(testcases.TransactionTestCase):
160 client_class = APIClient
161
162
163 class APITestCase(testcases.TestCase):
164 client_class = APIClient
165
166
167 if django.VERSION >= (1, 4):
168 class APISimpleTestCase(testcases.SimpleTestCase):
169 client_class = APIClient
170
171 class APILiveServerTestCase(testcases.LiveServerTestCase):
172 client_class = APIClient
+0
-71
build/lib/rest_framework_extensions/utils.py less more
0 # -*- coding: utf-8 -*-
1 import itertools
2 from distutils.version import LooseVersion
3
4 import rest_framework
5
6 from rest_framework_extensions.key_constructor.constructors import (
7 DefaultKeyConstructor,
8 DefaultObjectKeyConstructor,
9 DefaultListKeyConstructor,
10 DefaultAPIModelInstanceKeyConstructor,
11 DefaultAPIModelListKeyConstructor
12 )
13 from rest_framework_extensions.settings import extensions_api_settings
14
15
16 def get_rest_framework_version():
17 return tuple(LooseVersion(rest_framework.VERSION).version)
18
19
20 def flatten(list_of_lists):
21 """
22 Takes an iterable of iterables,
23 returns a single iterable containing all items
24 """
25 # todo: test me
26 return itertools.chain(*list_of_lists)
27
28
29 def prepare_header_name(name):
30 """
31 >> prepare_header_name('Accept-Language')
32 http_accept_language
33 """
34 return 'http_{0}'.format(name.strip().replace('-', '_')).upper()
35
36
37 def get_unique_method_id(view_instance, view_method):
38 # todo: test me as UniqueMethodIdKeyBit
39 return u'.'.join([
40 view_instance.__module__,
41 view_instance.__class__.__name__,
42 view_method.__name__
43 ])
44
45
46 def get_model_opts_concrete_fields(opts):
47 # todo: test me
48 if not hasattr(opts, 'concrete_fields'):
49 opts.concrete_fields = [f for f in opts.fields if f.column is not None]
50 return opts.concrete_fields
51
52
53 def compose_parent_pk_kwarg_name(value):
54 return '{0}{1}'.format(
55 extensions_api_settings.DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX,
56 value
57 )
58
59
60 default_cache_key_func = DefaultKeyConstructor()
61 default_object_cache_key_func = DefaultObjectKeyConstructor()
62 default_list_cache_key_func = DefaultListKeyConstructor()
63
64 default_etag_func = default_cache_key_func
65 default_object_etag_func = default_object_cache_key_func
66 default_list_etag_func = default_list_cache_key_func
67
68 # API (object-centered) functions
69 default_api_object_etag_func = DefaultAPIModelInstanceKeyConstructor()
70 default_api_list_etag_func = DefaultAPIModelListKeyConstructor()
0 # -*- coding: utf-8 -*-
10 #!/usr/bin/env python
21 """
32 Backdoc is a tool for backbone-like documentation generation.
87 import sys
98 import argparse
109
11 # -*- coding: utf-8 -*-
12 #!/usr/bin/env python
1310 # Copyright (c) 2012 Trent Mick.
1411 # Copyright (c) 2007-2008 ActiveState Corp.
1512 # License: MIT (http://www.opensource.org/licenses/mit-license.php)
184181 link_patterns=link_patterns,
185182 use_file_vars=use_file_vars).convert(text)
186183
187 class Markdown(object):
184 class Markdown:
188185 # The dict of "extras" to enable in processing -- a mapping of
189186 # extra name to argument for the extra. Most extras do not have an
190187 # argument, in which case the value is None.
20942091 return ''.join(lines)
20952092
20962093
2097 class _memoized(object):
2094 class _memoized:
20982095 """Decorator that caches a function's return value each time it is called.
20992096 If called later with the same arguments, the cached value is returned, and
21002097 not re-evaluated.
26592656 return text.decode('utf-8')
26602657
26612658
2662 class BackDoc(object):
2659 class BackDoc:
26632660 def __init__(self, markdown_converter, template_html, stdin, stdout):
26642661 self.markdown_converter = markdown_converter
26652662 self.template_html = force_text(template_html)
Binary diff not shown
7878
7979 #### Cache/ETAG mixins
8080
81 **ReadOnlyCacheResponseAndETAGMixin**
82
83 This mixin combines `ReadOnlyETAGMixin` and `CacheResponseMixin`. It could be used with
84 [ReadOnlyModelViewSet](http://www.django-rest-framework.org/api-guide/viewsets.html#readonlymodelviewset) and helps
85 to process caching + etag calculation for `retrieve` and `list` methods:
86
87 from myapps.serializers import UserSerializer
88 from rest_framework_extensions.mixins import (
89 ReadOnlyCacheResponseAndETAGMixin
90 )
91
92 class UserViewSet(ReadOnlyCacheResponseAndETAGMixin,
93 viewsets.ReadOnlyModelViewSet):
94 serializer_class = UserSerializer
95
96 **CacheResponseAndETAGMixin**
97
98 This mixin combines `ETAGMixin` and `CacheResponseMixin`. It could be used with
99 [ModelViewSet](http://www.django-rest-framework.org/api-guide/viewsets.html#modelviewset) and helps
100 to process:
101
102 * Caching for `retrieve` and `list` methods
103 * Etag for `retrieve`, `list`, `update` and `destroy` methods
104
105 Usage:
106
107 from myapps.serializers import UserSerializer
108 from rest_framework_extensions.mixins import CacheResponseAndETAGMixin
109
110 class UserViewSet(CacheResponseAndETAGMixin,
111 viewsets.ModelViewSet):
112 serializer_class = UserSerializer
113
114 Please, read more about [caching](#caching), [key construction](#key-constructor) and [conditional requests](#conditional-requests).
115
81 <!--**ReadOnlyCacheResponseAndETAGMixin**-->
82
83 <!--This mixin combines `ReadOnlyETAGMixin` and `CacheResponseMixin`. It could be used with-->
84 <!--[ReadOnlyModelViewSet](http://www.django-rest-framework.org/api-guide/viewsets.html#readonlymodelviewset) and helps-->
85 <!--to process caching + etag calculation for `retrieve` and `list` methods:-->
86
87 <!-- from myapps.serializers import UserSerializer-->
88 <!-- from rest_framework_extensions.mixins import (-->
89 <!-- ReadOnlyCacheResponseAndETAGMixin-->
90 <!-- )-->
91
92 <!-- class UserViewSet(ReadOnlyCacheResponseAndETAGMixin,-->
93 <!-- viewsets.ReadOnlyModelViewSet):-->
94 <!-- serializer_class = UserSerializer-->
95
96 <!--**CacheResponseAndETAGMixin**-->
97
98 <!--This mixin combines `ETAGMixin` and `CacheResponseMixin`. It could be used with-->
99 <!--[ModelViewSet](http://www.django-rest-framework.org/api-guide/viewsets.html#modelviewset) and helps-->
100 <!--to process:-->
101
102 <!--* Caching for `retrieve` and `list` methods-->
103 <!--* Etag for `retrieve`, `list`, `update` and `destroy` methods-->
104
105 <!--Usage:-->
106
107 <!-- from myapps.serializers import UserSerializer-->
108 <!-- from rest_framework_extensions.mixins import CacheResponseAndETAGMixin-->
109
110 <!-- class UserViewSet(CacheResponseAndETAGMixin,-->
111 <!-- viewsets.ModelViewSet):-->
112 <!-- serializer_class = UserSerializer-->
113
114 <!--Please, read more about [caching](#caching), [key construction](#key-constructor) and [conditional requests](#conditional-requests).-->
115 The etag functionality is pending an overhaul has been temporarily removed since 0.4.0.
116
117 ReadOnlyCacheResponseAndETAGMixin and CacheResponseAndETAGMixin are no longer available to use.
118
119 See discussion in [Issue #177](https://github.com/chibisov/drf-extensions/issues/177)
116120
117121 ### Routers
118122
10201024 # views.py
10211025 import datetime
10221026 from django.core.cache import cache
1023 from django.utils.encoding import force_text
1027 from django.utils.encoding import force_str
10241028 from yourapp.serializers import GroupSerializer, ProfileSerializer
10251029 from rest_framework_extensions.cache.decorators import cache_response
10261030 from rest_framework_extensions.key_constructor.constructors import (
10401044 if not value:
10411045 value = datetime.datetime.utcnow()
10421046 cache.set(key, value=value)
1043 return force_text(value)
1047 return force_str(value)
10441048
10451049 class CustomObjectKeyConstructor(DefaultKeyConstructor):
10461050 retrieve_sql = RetrieveSqlQueryKeyBit()
13221326
13231327 ### Conditional requests
13241328
1325 *This documentation section uses information from [RESTful Web Services Cookbook](http://shop.oreilly.com/product/9780596801694.do) 10-th chapter.*
1326
1327 Conditional HTTP requests allow API clients to accomplish 2 goals:
1328
1329 1. Conditional HTTP GET saves client and server [time and bandwidth](#saving-time-and-bandwidth).
1330 * For unsafe requests such as PUT, POST, and DELETE, conditional requests provide [concurrency control](#concurrency-control).
1331
1332 The second goal addresses the lost update problem, where a resource is altered and saved by user B while user A is still editing.
1333 In both cases, the 'condition' included in the request needs to be a unique identifier (e.g. unique semantic fingerprint) of the requested resource in order to detect changes.
1334 This fingerprint can be transient (i.e. using the cache as with `UpdatedAtKeyBit`), or persistent, i.e. computed from the model instance attribute values from the database.
1335 While the `UpdatedAtKeyBit` approach requires to add triggers to your models, the semantic fingerprint option is designed to be pluggable and does not require to alter your model code.
1336
1337
1338 #### HTTP ETag
1339
1340 *An ETag or entity tag, is part of HTTP, the protocol for the World Wide Web.
1341 It is one of several mechanisms that HTTP provides for web cache validation, and which allows a client to make conditional requests.* - [Wikipedia](http://en.wikipedia.org/wiki/HTTP_ETag)
1342
1343 For ETag calculation and conditional request processing you should use the decorators from `rest_framework_extensions.etag.decorators`.
1344 The `@etag` decorator works similar to the native [django decorator](https://docs.djangoproject.com/en/dev/topics/conditional-view-processing/).
1345
1346 <!-- THIS REFERS TO THE DEFAULT_OBJ_ETAG_FUNC -->
1347 The [default ETag function](#default-etag-function) used by the `@etag` decorator computes the value with respect to the particular view and HTTP method in the request and therefore *cannot detect changes in individual model instances*.
1348 If you need to compute the *semantic* fingerprint of a model independent of a particular view and method, implement your custom `etag_func`.
1349 Alternatively you could use the `@api_etag` decorator and specify the `viewset` in the view.
1350
1351
1352 from rest_framework_extensions.etag.decorators import etag
1353
1354 class CityView(views.APIView):
1355 @etag()
1356 def get(self, request, *args, **kwargs):
1357 cities = City.objects.all().values_list('name', flat=True)
1358 return Response(cities)
1359
1360 By default `@etag` would calculate the ETag header value with the same algorithm as [cache key](#cache-key) default calculation performs.
1361
1362 # Request
1363 GET /cities/ HTTP/1.1
1364 Accept: application/json
1365
1366 # Response
1367 HTTP/1.1 200 OK
1368 Content-Type: application/json; charset=UTF-8
1369 ETag: "e7b50490dc546d116635a14cfa58110306dd6c5434146b6740ec08bf0a78f9a2"
1370
1371 ['Moscow', 'London', 'Paris']
1372
1373 You can define a custom function for Etag value calculation with `etag_func` argument:
1374
1375 from rest_framework_extensions.etag.decorators import etag
1376
1377 def calculate_etag(view_instance, view_method,
1378 request, args, kwargs):
1379 return '.'.join([
1380 len(args),
1381 len(kwargs)
1382 ])
1383
1384 class CityView(views.APIView):
1385 @etag(etag_func=calculate_etag)
1386 def get(self, request, *args, **kwargs):
1387 cities = City.objects.all().values_list('name', flat=True)
1388 return Response(cities)
1389
1390
1391 You can implement a view method and use it for Etag calculation by specifying `etag_func` argument as string:
1392
1393 from rest_framework_extensions.etag.decorators import etag
1394
1395 class CityView(views.APIView):
1396 @etag(etag_func='calculate_etag_from_method')
1397 def get(self, request, *args, **kwargs):
1398 cities = City.objects.all().values_list('name', flat=True)
1399 return Response(cities)
1400
1401 def calculate_etag_from_method(self, view_instance, view_method,
1402 request, args, kwargs):
1403 return '.'.join([
1404 len(args),
1405 len(kwargs)
1406 ])
1407
1408 ETag calculation function will be called with following parameters:
1409
1410 * **view_instance** - view instance of decorated method
1411 * **view_method** - decorated method
1412 * **request** - decorated method request
1413 * **args** - decorated method positional arguments
1414 * **kwargs** - decorated method keyword arguments
1415
1416
1417 #### Default ETag function
1418
1419 If `@etag` decorator used without `etag_func` argument then default etag function will be used. You can change this function in
1420 settings:
1421
1422 REST_FRAMEWORK_EXTENSIONS = {
1423 'DEFAULT_ETAG_FUNC':
1424 'rest_framework_extensions.utils.default_etag_func'
1425 }
1426
1427 `default_etag_func` uses [DefaultKeyConstructor](#default-key-constructor) as a base for etag calculation.
1428
1429
1430 #### API ETag function
1431 <!-- This refers to the APIETagProcessor and @api_etag decorator -->
1432
1433 *New in DRF-extensions 0.3.2*
1434
1435 In addition, `APIETAGProcessor` explicitly requires a function that (ideally) creates an ETag value from model instances.
1436 If the `@api_etag` decorator is used without `etag_func` the framework will raise an `AssertionError`.
1437 Hence, the following snipped would not work:
1438
1439 # BEGIN BAD CODE:
1440 class View(views.APIView):
1441 @api_etag()
1442 def get(self, request, *args, **kwargs):
1443 return super(View, self).get(request, *args, **kwargs)
1444 # END BAD CODE
1445
1446 **Why's that?**
1447 It does not make sense to compute a default ETag here, because the processor would lock us out from the API by always issuing a `304`
1448 response on conditional requests, even if the resource was modified meanwhile.
1449 Therefore the `APIETAGProcessor` cannot be used without specifying an `etag_func` as keyword argument and there exists convenient
1450 [mixin classes](#apietagmixin).
1451
1452 You can use the decorator in regular `APIView`, and subclasses from the `rest_framework.generics` module,
1453 but ensure to include a `queryset` attribute or override `get_queryset()`:
1454
1455 from rest_framework import generics
1456 from rest_framework.response import Response
1457 from rest_framework_extensions.utils import default_api_object_etag_func
1458 from my_app.models import Book
1459
1460 class BookCustomDestroyView(generics.DestroyAPIView):
1461 # include the queryset here to enable the object lookup
1462 queryset = Book.objects.all()
1463
1464 @api_etag(etag_func=default_api_object_etag_func)
1465 def delete(self, request, *args, **kwargs):
1466 obj = Book.objects.get(id=kwargs['pk'])
1467 # ... perform some custom operations here ...
1468 obj.delete()
1469 return Response(status=status.HTTP_204_NO_CONTENT)
1470
1471
1472 The next difference to the `@etag` decorator is that it defines an explicit map of
1473 required headers for each HTTP request verb, using the following default values for unsafe methods:
1474
1475 precondition_map = {'PUT': ['If-Match'],
1476 'PATCH': ['If-Match'],
1477 'DELETE': ['If-Match']}
1478
1479 You can specify a custom set of headers in the decorator by passing the `precondition_map` keyword argument.
1480 For instance, this statement
1481
1482 @api_etag(etag_func=default_api_object_etag_func, precondition_map={'PUT': ['X-mycorp-custom']})
1483 def put(self, request, *args, **kwargs):
1484 obj = Book.objects.get(id=kwargs['pk'])
1485 # ... perform some custom operations here ...
1486 obj.save()
1487 return Response(status=status.HTTP_200_OK)
1488
1489 checks for the presence of a custom header `X-mycorp-custom` in the request and permits the request, if it is present,
1490 or returns a `428 PRECONDITION REQUIRED` response.
1491
1492 Similarly, to disable all checks for a particular method simply pass an empty dict:
1493
1494 @api_etag(etag_func=default_api_object_etag_func, precondition_map={})
1495 def put(self, request, *args, **kwargs):
1496 obj = Book.objects.get(id=kwargs['pk'])
1497 # ... perform some custom operations here ...
1498 obj.save()
1499 return Response(status=status.HTTP_200_OK)
1500
1501 Please note that passing `None` in the `precondition_map` argument falls back to using the default map.
1502
1503 #### Usage ETag with caching
1504
1505 As you can see `@etag` and `@cache_response` decorators has similar key calculation approaches.
1506 They both can take key from simple callable function. And more than this - in many cases they share the same calculation logic.
1507 In the next example we use both decorators, which share one calculation function:
1508
1509 from rest_framework_extensions.etag.decorators import etag
1510 from rest_framework_extensions.cache.decorators import cache_response
1511 from rest_framework_extensions.key_constructor import bits
1512 from rest_framework_extensions.key_constructor.constructors import (
1513 KeyConstructor
1514 )
1515
1516 class CityGetKeyConstructor(KeyConstructor):
1517 format = bits.FormatKeyBit()
1518 language = bits.LanguageKeyBit()
1519
1520 class CityView(views.APIView):
1521 key_constructor_func = CityGetKeyConstructor()
1522
1523 @etag(key_constructor_func)
1524 @cache_response(key_func=key_constructor_func)
1525 def get(self, request, *args, **kwargs):
1526 cities = City.objects.all().values_list('name', flat=True)
1527 return Response(cities)
1528
1529 Note the decorators order. First goes `@etag` and then goes `@cache_response`. We want firstly perform conditional processing and after it response processing.
1530
1531 There is one more point for it. If conditional processing didn't fail then `key_constructor_func` would be called again in `@cache_response`.
1532 But in most cases first calculation is enough. To accomplish this goal you could use `KeyConstructor` initial argument `memoize_for_request`:
1533
1534 >>> key_constructor_func = CityGetKeyConstructor(memoize_for_request=True)
1535 >>> request1, request1 = 'request1', 'request2'
1536 >>> print key_constructor_func(request=request1) # full calculation
1537 request1-key
1538 >>> print key_constructor_func(request=request1) # data from cache
1539 request1-key
1540 >>> print key_constructor_func(request=request2) # full calculation
1541 request2-key
1542 >>> print key_constructor_func(request=request2) # data from cache
1543 request2-key
1544
1545 By default `memoize_for_request` is `False`, but you can change it in settings:
1546
1547 REST_FRAMEWORK_EXTENSIONS = {
1548 'DEFAULT_KEY_CONSTRUCTOR_MEMOIZE_FOR_REQUEST': True
1549 }
1550
1551
1552 It's important to note that this memoization is thread safe.
1553
1554 #### Saving time and bandwidth
1555
1556 When a server returns `ETag` header, you should store it along with the representation data on the client.
1557 When making GET and HEAD requests for the same resource in the future, include the `If-None-Match` header
1558 to make these requests "conditional".
1559
1560 For example, retrieve all cities:
1561
1562 # Request
1563 GET /cities/ HTTP/1.1
1564 Accept: application/json
1565
1566 # Response
1567 HTTP/1.1 200 OK
1568 Content-Type: application/json; charset=UTF-8
1569 ETag: "some_etag_value"
1570
1571 ['Moscow', 'London', 'Paris']
1572
1573 If you make same request with `If-None-Match` and there exists a cached value for this request,
1574 then server will respond with `304` status code without body data.
1575
1576 # Request
1577 GET /cities/ HTTP/1.1
1578 Accept: application/json
1579 If-None-Match: some_etag_value
1580
1581 # Response
1582 HTTP/1.1 304 NOT MODIFIED
1583 Content-Type: application/json; charset=UTF-8
1584 Etag: "some_etag_value"
1585
1586 After this response you can use existing cities data on the client.
1587
1588
1589
1590
1591
1592
1593
1594
1595 #### Concurrency control
1596
1597 Concurrency control ensures the correct processing of data under concurrent operations by clients.
1598 There are two ways to implement concurrency control:
1599
1600 * **Pessimistic concurrency control**. In this model, the client gets a lock, obtains
1601 the current state of the resource, makes modifications, and then releases the lock.
1602 During this process, the server prevents other clients from acquiring a lock on the same resource.
1603 Relational databases operate in this manner.
1604 * **Optimistic concurrency control**. In this model, the client first gets a token.
1605 Instead of obtaining a lock, the client attempts a write operation with the token included in the request.
1606 The operation succeeds if the token is still valid and fails otherwise.
1607
1608 HTTP, being a stateless application control, is designed for optimistic concurrency control.
1609 According to [RFC 6585](https://tools.ietf.org/html/rfc6585), the server can optionally require
1610 a condition for a request. This library returns a `428` status, if no ETag is supplied, but would be mandatory
1611 for a request to succeed.
1612
1613 Update:
1614
1615 PUT/PATCH
1616 +
1617 +-----------+------------+
1618 | ETag |
1619 | supplied? |
1620 ++-----------------+-----+
1621 | |
1622 Yes No
1623 | |
1624 +---------------------++ ++-------------+
1625 | Do preconditions | | Precondition |
1626 | match? | | required? |
1627 +---+-----------------++ ++------------++
1628 | | | |
1629 Yes No No Yes
1630 | | | |
1631 +----------+------+ +-------+----------+ +---+-----+ |
1632 | Does resource | | 412 Precondition | | 200 OK | |
1633 | exist? | | failed | | Update | |
1634 ++---------------++ +------------------+ +---------+ |
1635 | | +-----------+------+
1636 Yes No | 428 Precondition |
1637 | | | required |
1638 +----+----+ +----+----+ +------------------+
1639 | 200 OK | | 404 Not |
1640 | Update | | found |
1641 +---------+ +---------+
1642
1643
1644 Delete:
1645
1646 DELETE
1647 +
1648 +-----------+------------+
1649 | ETag |
1650 | supplied? |
1651 ++-----------------+-----+
1652 | |
1653 Yes No
1654 | |
1655 +---------------------++ ++-------------+
1656 | Do preconditions | | Precondition |
1657 | match? | | required? |
1658 +---+-----------------++ ++------------++
1659 | | | |
1660 Yes No No Yes
1661 | | | |
1662 +----------+------+ +-------+----------+ +---+-----+ |
1663 | Does resource | | 412 Precondition | | 204 No | |
1664 | exist? | | failed | | content | |
1665 ++---------------++ +------------------+ +---------+ |
1666 | | +-----------+------+
1667 Yes No | 428 Precondition |
1668 | | | required |
1669 +----+----+ +----+----+ +------------------+
1670 | 204 No | | 404 Not |
1671 | content | | found |
1672 +---------+ +---------+
1673
1674
1675
1676 **Example: transient key construction**
1677
1678 Here is an example implementation for all (C)RUD methods (except create, because it doesn't need concurrency control)
1679 wrapped with the default `etag` decorator. We use our [previous implementation](#custom-key-bit) of the `UpdatedAtKeyBit` that looks up the cache
1680 for the last timestamp the particular object was updated on the server. This required us to add `post_save` and `post_delete` signals
1681 to our models explicitly. See [below](#apietagmixin) for an example using `@api_etag` and mixins that computes the key from persistent data.
1682
1683 from rest_framework.viewsets import ModelViewSet
1684 from rest_framework_extensions.key_constructor import bits
1685 from rest_framework_extensions.key_constructor.constructors import (
1686 KeyConstructor
1687 )
1688
1689 from your_app.models import City
1690 # use our own implementation that detects an update timestamp in the cache
1691 from your_app.key_bits import UpdatedAtKeyBit
1692
1693 class CityListKeyConstructor(KeyConstructor):
1694 format = bits.FormatKeyBit()
1695 language = bits.LanguageKeyBit()
1696 pagination = bits.PaginationKeyBit()
1697 list_sql_query = bits.ListSqlQueryKeyBit()
1698 unique_view_id = bits.UniqueViewIdKeyBit()
1699
1700 class CityDetailKeyConstructor(KeyConstructor):
1701 format = bits.FormatKeyBit()
1702 language = bits.LanguageKeyBit()
1703 retrieve_sql_query = bits.RetrieveSqlQueryKeyBit()
1704 unique_view_id = bits.UniqueViewIdKeyBit()
1705 updated_at = UpdatedAtKeyBit()
1706
1707 class CityViewSet(ModelViewSet):
1708 list_key_func = CityListKeyConstructor(
1709 memoize_for_request=True
1710 )
1711 obj_key_func = CityDetailKeyConstructor(
1712 memoize_for_request=True
1713 )
1714
1715 @etag(list_key_func)
1716 @cache_response(key_func=list_key_func)
1717 def list(self, request, *args, **kwargs):
1718 return super(CityViewSet, self).list(request, *args, **kwargs)
1719
1720 @etag(obj_key_func)
1721 @cache_response(key_func=obj_key_func)
1722 def retrieve(self, request, *args, **kwargs):
1723 return super(CityViewSet, self).retrieve(request, *args, **kwargs)
1724
1725 @etag(obj_key_func)
1726 def update(self, request, *args, **kwargs):
1727 return super(CityViewSet, self).update(request, *args, **kwargs)
1728
1729 @etag(obj_key_func)
1730 def destroy(self, request, *args, **kwargs):
1731 return super(CityViewSet, self).destroy(request, *args, **kwargs)
1732
1733
1734 #### ETag for unsafe methods
1735
1736 From previous section you could see that unsafe methods, such `update` (PUT, PATCH) or `destroy` (DELETE), have the same `@etag`
1737 decorator wrapping manner as the safe methods.
1738
1739 But every unsafe method has one distinction from safe method - it changes the data
1740 which could be used for Etag calculation. In our case it is `UpdatedAtKeyBit`. It means that we should calculate Etag:
1741
1742 * Before building response - for `If-Match` and `If-None-Match` conditions validation
1743 * After building response (if necessary) - for clients
1744
1745 `@etag` decorator has special attribute `rebuild_after_method_evaluation`, which by default is `False`.
1746
1747 If you specify `rebuild_after_method_evaluation` as `True` then Etag will be rebuilt after method evaluation:
1748
1749 class CityViewSet(ModelViewSet):
1750 ...
1751 @etag(obj_key_func, rebuild_after_method_evaluation=True)
1752 def update(self, request, *args, **kwargs):
1753 return super(CityViewSet, self).update(request, *args, **kwargs)
1754
1755 @etag(obj_key_func)
1756 def destroy(self, request, *args, **kwargs):
1757 return super(CityViewSet, self).destroy(request, *args, **kwargs)
1758
1759 # Request
1760 PUT /cities/1/ HTTP/1.1
1761 Accept: application/json
1762
1763 {"name": "London"}
1764
1765 # Response
1766 HTTP/1.1 200 OK
1767 Content-Type: application/json; charset=UTF-8
1768 ETag: "4e63ef056f47270272b96523f51ad938b5ea141024b767880eac047d10a0b339"
1769
1770 {
1771 id: 1,
1772 name: "London"
1773 }
1774
1775 As you can see we didn't specify `rebuild_after_method_evaluation` for `destroy` method. That is because there is no
1776 sense to use returned ETag value on clients if object deletion already performed.
1777
1778 With `rebuild_after_method_evaluation` parameter Etag calculation for `PUT`/`PATCH` method would look like:
1779
1780 +--------------+
1781 | Request |
1782 +--------------+
1783 |
1784 +--------------------------+
1785 | Calculate Etag |
1786 | for condition matching |
1787 +--------------------------+
1788 |
1789 +--------------------+
1790 | Do preconditions |
1791 | match? |
1792 +--------------------+
1793 | |
1794 Yes No
1795 | |
1796 +--------------+ +--------------------+
1797 | Update the | | 412 Precondition |
1798 | resource | | failed |
1799 +--------------+ +--------------------+
1800 |
1801 +--------------------+
1802 | Calculate Etag |
1803 | again and add it |
1804 | to response |
1805 +--------------------+
1806 |
1807 +------------+
1808 | Return |
1809 | response |
1810 +------------+
1811
1812 `If-None-Match` example for `DELETE` method:
1813
1814 # Request
1815 DELETE /cities/1/ HTTP/1.1
1816 Accept: application/json
1817 If-None-Match: some_etag_value
1818
1819 # Response
1820 HTTP/1.1 304 NOT MODIFIED
1821 Content-Type: application/json; charset=UTF-8
1822 Etag: "some_etag_value"
1823
1824
1825 `If-Match` example for `DELETE` method:
1826
1827 # Request
1828 DELETE /cities/1/ HTTP/1.1
1829 Accept: application/json
1830 If-Match: another_etag_value
1831
1832 # Response
1833 HTTP/1.1 412 PRECONDITION FAILED
1834 Content-Type: application/json; charset=UTF-8
1835 Etag: "some_etag_value"
1836
1837
1838 #### ETAGMixin
1839
1840 It is common to process etags for standard [viewset](http://www.django-rest-framework.org/api-guide/viewsets)
1841 `retrieve`, `list`, `update` and `destroy` methods.
1842 That is why `ETAGMixin` exists. Just mix it into viewset
1843 implementation and those methods will use functions, defined in `REST_FRAMEWORK_EXTENSIONS` [settings](#settings):
1844
1845 * *"DEFAULT\_OBJECT\_ETAG\_FUNC"* for `retrieve`, `update` and `destroy` methods
1846 * *"DEFAULT\_LIST\_ETAG\_FUNC"* for `list` method
1847
1848 By default those functions are using [DefaultKeyConstructor](#default-key-constructor) and extends it:
1849
1850 * With `RetrieveSqlQueryKeyBit` for *"DEFAULT\_OBJECT\_ETAG\_FUNC"*
1851 * With `ListSqlQueryKeyBit` and `PaginationKeyBit` for *"DEFAULT\_LIST\_ETAG\_FUNC"*
1852
1853 You can change those settings for custom ETag generation:
1854
1855 REST_FRAMEWORK_EXTENSIONS = {
1856 'DEFAULT_OBJECT_ETAG_FUNC':
1857 'rest_framework_extensions.utils.default_object_etag_func',
1858 'DEFAULT_LIST_ETAG_FUNC':
1859 'rest_framework_extensions.utils.default_list_etag_func',
1860 }
1861
1862 Mixin example usage:
1863
1864 from myapps.serializers import UserSerializer
1865 from rest_framework_extensions.etag.mixins import ETAGMixin
1866
1867 class UserViewSet(ETAGMixin, viewsets.ModelViewSet):
1868 serializer_class = UserSerializer
1869
1870 You can change etag function by providing `object_etag_func` or
1871 `list_etag_func` methods in view class:
1872
1873 class UserViewSet(ETAGMixin, viewsets.ModelViewSet):
1874 serializer_class = UserSerializer
1875
1876 def object_etag_func(self, **kwargs):
1877 return 'some key for object'
1878
1879 def list_etag_func(self, **kwargs):
1880 return 'some key for list'
1881
1882 Of course you can use custom [key constructor](#key-constructor):
1883
1884 from yourapp.key_constructors import (
1885 CustomObjectKeyConstructor,
1886 CustomListKeyConstructor,
1887 )
1888
1889 class UserViewSet(ETAGMixin, viewsets.ModelViewSet):
1890 serializer_class = UserSerializer
1891 object_etag_func = CustomObjectKeyConstructor()
1892 list_etag_func = CustomListKeyConstructor()
1893
1894 It is important to note that ETags for unsafe method `update` is processed with parameter
1895 `rebuild_after_method_evaluation` equals `True`. You can read why from [this](#etag-for-unsafe-methods) section.
1896
1897 There are other mixins for more granular Etag calculation in `rest_framework_extensions.etag.mixins` module:
1898
1899 * **ReadOnlyETAGMixin** - only for `retrieve` and `list` methods
1900 * **RetrieveETAGMixin** - only for `retrieve` method
1901 * **ListETAGMixin** - only for `list` method
1902 * **DestroyETAGMixin** - only for `destroy` method
1903 * **UpdateETAGMixin** - only for `update` method
1904
1905
1906 #### APIETagMixin
1907
1908 *New in DRF-extensions 0.3.2*
1909
1910 In analogy to `ETAGMixin` the `APIETAGMixin` exists. Just mix it into DRF viewsets or `APIViews`
1911 and those methods will use the ETag functions, defined in `REST_FRAMEWORK_EXTENSIONS` [settings](#settings):
1912
1913 * *"DEFAULT\_API\_OBJECT\_ETAG\_FUNC"* for `retrieve`, `update` and `destroy` methods
1914 * *"DEFAULT\_API\_LIST\_ETAG\_FUNC"* for `list` method
1915
1916 By default those functions are using custom key constructors that create the key from **persisted model attributes**:
1917
1918 * `RetrieveModelKeyBit` (see [definition](#retrievemodelkeybit)) for *"DEFAULT\_API\_OBJECT\_ETAG\_FUNC"*
1919 * `ListModelKeyBit` (see [definition](#listmodelkeybit)) for *"DEFAULT\_API\_LIST\_ETAG\_FUNC"*
1920
1921 You can change those settings globally for your custom ETag generation, or use the default values, which should cover
1922 the most common use cases:
1923
1924 REST_FRAMEWORK_EXTENSIONS = {
1925 'DEFAULT_API_OBJECT_ETAG_FUNC':
1926 'rest_framework_extensions.utils.default_api_object_etag_func',
1927 'DEFAULT_API_LIST_ETAG_FUNC':
1928 'rest_framework_extensions.utils.default_api_list_etag_func',
1929 }
1930
1931 Mixin example usage:
1932
1933 from myapps.serializers import UserSerializer
1934 from rest_framework_extensions.etag.mixins import APIETAGMixin
1935
1936 class UserViewSet(APIETAGMixin, viewsets.ModelViewSet):
1937 serializer_class = UserSerializer
1938
1939 You can change etag function by providing `api_object_etag_func` or
1940 `api_list_etag_func` methods in view class:
1941
1942 class UserViewSet(APIETAGMixin, viewsets.ModelViewSet):
1943 serializer_class = UserSerializer
1944
1945 def api_object_etag_func(self, **kwargs):
1946 return 'some key for object'
1947
1948 def api_list_etag_func(self, **kwargs):
1949 return 'some key for list'
1950
1951 Of course you can use custom [key constructors](#key-constructor):
1952
1953 from yourapp.key_constructors import (
1954 CustomObjectKeyConstructor,
1955 CustomListKeyConstructor,
1956 )
1957
1958 class UserViewSet(APIETAGMixin, viewsets.ModelViewSet):
1959 serializer_class = UserSerializer
1960 api_object_etag_func = CustomObjectKeyConstructor()
1961 api_list_etag_func = CustomListKeyConstructor()
1962
1963 There are other mixins for more granular ETag calculation in `rest_framework_extensions.etag.mixins` module:
1964
1965 * **APIReadOnlyETAGMixin** - only for `retrieve` and `list` methods
1966 * **APIRetrieveETAGMixin** - only for `retrieve` method
1967 * **APIListETAGMixin** - only for `list` method
1968 * **APIDestroyETAGMixin** - only for `destroy` method
1969 * **APIUpdateETAGMixin** - only for `update` method
1970
1971 By default, all mixins require the conditional requests, i.e. they use the default `precondition_map` from the
1972 `APIETAGProcessor` class.
1973
1974
1975 #### Gzipped ETags
1976
1977 If you use [GZipMiddleware](https://docs.djangoproject.com/en/dev/ref/middleware/#module-django.middleware.gzip)
1978 and your client accepts Gzipped response, then you should return different ETags for compressed and not compressed responses.
1979 That's what `GZipMiddleware` does by default while processing response -
1980 it adds `;gzip` postfix to ETag response header if client requests compressed response.
1981 Lets see it in example. First request without compression:
1982
1983 # Request
1984 GET /cities/ HTTP/1.1
1985 Accept: application/json
1986
1987 # Response
1988 HTTP/1.1 200 OK
1989 Content-Type: application/json; charset=UTF-8
1990 ETag: "e7b50490dc"
1991
1992 ['Moscow', 'London', 'Paris']
1993
1994 Second request with compression:
1995
1996 # Request
1997 GET /cities/ HTTP/1.1
1998 Accept: application/json
1999 Accept-Encoding: gzip
2000
2001 # Response
2002 HTTP/1.1 200 OK
2003 Content-Type: application/json
2004 Content-Length: 675
2005 Content-Encoding: gzip
2006 ETag: "e7b50490dc;gzip"
2007
2008 wS?n?0?_%o?cc?Ҫ?Eʒ?Cժʻ?a\1?a?^T*7q<>[Nvh?[?^9?x:/Ms?79?Fd/???ۦjES?ڽ?&??c%^?C[K۲%N?w{?졭2?m?}?Q&Egz??
2009
2010 As you can see there is `;gzip` postfix in ETag response header.
2011 That's ok but there is one caveat - drf-extension doesn't know how you post-processed calculated ETag value.
2012 And your clients could have next problem with conditional request:
2013
2014 * Client sends request to retrieve compressed data about cities to `/cities/`
2015 * DRF-extensions decorator calculates ETag header for response equals, for example, `123`
2016 * `GZipMiddleware` adds `;gzip` postfix to ETag header response, and now it equals `123;gzip`
2017 * Client retrieves response with ETag equals `123;gzip`
2018 * Client again makes request to retrieve compressed data about cities,
2019 but now it's conditional request with `If-None-Match` header equals `123;gzip`
2020 * DRF-extensions decorator calculates ETag value for processing conditional request.
2021 But it doesn't know, that `GZipMiddleware` added `;gzip` postfix for previous response.
2022 DRF-extensions decorator calculates ETag equals `123`, compares it with `123;gzip` and returns
2023 response with status code 200, because `123` != `123;gzip`
2024
2025 You can solve this problem by stripping `;gzip` postfix on client side.
2026
2027 But there are so many libraries that just magically uses ETag response header without allowing to
2028 pre-process conditional requests (for example, browser). If that's you case then you could add custom middleware which removes `;gzip`
2029 postfix from header:
2030
2031 # yourapp/middleware.py
2032
2033 class RemoveEtagGzipPostfix(object):
2034 def process_response(self, request, response):
2035 if response.has_header('ETag') and response['ETag'][-6:] == ';gzip"':
2036 response['ETag'] = response['ETag'][:-6] + '"'
2037 return response
2038
2039 Don't forget to add this middleware in your settings before `GZipMiddleware`:
2040
2041 # settings.py
2042 MIDDLEWARE_CLASSES = (
2043 ...
2044 'yourapp.RemoveEtagGzipPostfix',
2045 'django.middleware.gzip.GZipMiddleware',
2046 'django.middleware.common.CommonMiddleware',
2047 ...
2048 )
2049
1329 The etag functionality is pending an overhaul has been temporarily removed since 0.4.0.
1330
1331 See discussion in [Issue #177](https://github.com/chibisov/drf-extensions/issues/177)
1332
1333 <!--*This documentation section uses information from [RESTful Web Services Cookbook](http://shop.oreilly.com/product/9780596801694.do) 10-th chapter.*-->
1334
1335 <!--Conditional HTTP requests allow API clients to accomplish 2 goals:-->
1336
1337 <!--1. Conditional HTTP GET saves client and server [time and bandwidth](#saving-time-and-bandwidth).-->
1338 <!--* For unsafe requests such as PUT, POST, and DELETE, conditional requests provide [concurrency control](#concurrency-control).-->
1339
1340 <!--The second goal addresses the lost update problem, where a resource is altered and saved by user B while user A is still editing.-->
1341 <!--In both cases, the 'condition' included in the request needs to be a unique identifier (e.g. unique semantic fingerprint) of the requested resource in order to detect changes.-->
1342 <!--This fingerprint can be transient (i.e. using the cache as with `UpdatedAtKeyBit`), or persistent, i.e. computed from the model instance attribute values from the database. -->
1343 <!--While the `UpdatedAtKeyBit` approach requires to add triggers to your models, the semantic fingerprint option is designed to be pluggable and does not require to alter your model code. -->
1344
1345
1346 <!--#### HTTP ETag-->
1347
1348 <!--*An ETag or entity tag, is part of HTTP, the protocol for the World Wide Web. -->
1349 <!--It is one of several mechanisms that HTTP provides for web cache validation, and which allows a client to make conditional requests.* - [Wikipedia](http://en.wikipedia.org/wiki/HTTP_ETag)-->
1350
1351 <!--For ETag calculation and conditional request processing you should use the decorators from `rest_framework_extensions.etag.decorators`.-->
1352 <!--The `@etag` decorator works similar to the native [django decorator](https://docs.djangoproject.com/en/dev/topics/conditional-view-processing/).-->
1353
1354 <!--<!-- THIS REFERS TO THE DEFAULT_OBJ_ETAG_FUNC -->
1355 <!--The [default ETag function](#default-etag-function) used by the `@etag` decorator computes the value with respect to the particular view and HTTP method in the request and therefore *cannot detect changes in individual model instances*.-->
1356 <!--If you need to compute the *semantic* fingerprint of a model independent of a particular view and method, implement your custom `etag_func`.-->
1357 <!--Alternatively you could use the `@api_etag` decorator and specify the `viewset` in the view.-->
1358
1359
1360 <!-- from rest_framework_extensions.etag.decorators import etag-->
1361
1362 <!-- class CityView(views.APIView):-->
1363 <!-- @etag()-->
1364 <!-- def get(self, request, *args, **kwargs):-->
1365 <!-- cities = City.objects.all().values_list('name', flat=True)-->
1366 <!-- return Response(cities)-->
1367
1368 <!--By default `@etag` would calculate the ETag header value with the same algorithm as [cache key](#cache-key) default calculation performs.-->
1369
1370 <!-- # Request-->
1371 <!-- GET /cities/ HTTP/1.1-->
1372 <!-- Accept: application/json-->
1373
1374 <!-- # Response-->
1375 <!-- HTTP/1.1 200 OK-->
1376 <!-- Content-Type: application/json; charset=UTF-8-->
1377 <!-- ETag: "e7b50490dc546d116635a14cfa58110306dd6c5434146b6740ec08bf0a78f9a2"-->
1378
1379 <!-- ['Moscow', 'London', 'Paris']-->
1380
1381 <!--You can define a custom function for Etag value calculation with `etag_func` argument:-->
1382
1383 <!-- from rest_framework_extensions.etag.decorators import etag-->
1384
1385 <!-- def calculate_etag(view_instance, view_method,-->
1386 <!-- request, args, kwargs):-->
1387 <!-- return '.'.join([-->
1388 <!-- len(args),-->
1389 <!-- len(kwargs)-->
1390 <!-- ])-->
1391
1392 <!-- class CityView(views.APIView):-->
1393 <!-- @etag(etag_func=calculate_etag)-->
1394 <!-- def get(self, request, *args, **kwargs):-->
1395 <!-- cities = City.objects.all().values_list('name', flat=True)-->
1396 <!-- return Response(cities)-->
1397
1398
1399 <!--You can implement a view method and use it for Etag calculation by specifying `etag_func` argument as string:-->
1400
1401 <!-- from rest_framework_extensions.etag.decorators import etag-->
1402
1403 <!-- class CityView(views.APIView):-->
1404 <!-- @etag(etag_func='calculate_etag_from_method')-->
1405 <!-- def get(self, request, *args, **kwargs):-->
1406 <!-- cities = City.objects.all().values_list('name', flat=True)-->
1407 <!-- return Response(cities)-->
1408
1409 <!-- def calculate_etag_from_method(self, view_instance, view_method,-->
1410 <!-- request, args, kwargs):-->
1411 <!-- return '.'.join([-->
1412 <!-- len(args),-->
1413 <!-- len(kwargs)-->
1414 <!-- ])-->
1415
1416 <!--ETag calculation function will be called with following parameters:-->
1417
1418 <!--* **view_instance** - view instance of decorated method-->
1419 <!--* **view_method** - decorated method-->
1420 <!--* **request** - decorated method request-->
1421 <!--* **args** - decorated method positional arguments-->
1422 <!--* **kwargs** - decorated method keyword arguments-->
1423
1424
1425 <!--#### Default ETag function-->
1426
1427 <!--If `@etag` decorator used without `etag_func` argument then default etag function will be used. You can change this function in-->
1428 <!--settings:-->
1429
1430 <!-- REST_FRAMEWORK_EXTENSIONS = {-->
1431 <!-- 'DEFAULT_ETAG_FUNC':-->
1432 <!-- 'rest_framework_extensions.utils.default_etag_func'-->
1433 <!-- }-->
1434
1435 <!--`default_etag_func` uses [DefaultKeyConstructor](#default-key-constructor) as a base for etag calculation.-->
1436
1437
1438 <!--#### API ETag function-->
1439 <!--<!-- This refers to the APIETagProcessor and @api_etag decorator -->
1440
1441 <!--*New in DRF-extensions 0.3.2*-->
1442
1443 <!--In addition, `APIETAGProcessor` explicitly requires a function that (ideally) creates an ETag value from model instances.-->
1444 <!--If the `@api_etag` decorator is used without `etag_func` the framework will raise an `AssertionError`. -->
1445 <!--Hence, the following snipped would not work:-->
1446
1447 <!-- # BEGIN BAD CODE:-->
1448 <!-- class View(views.APIView):-->
1449 <!-- @api_etag() -->
1450 <!-- def get(self, request, *args, **kwargs):-->
1451 <!-- return super(View, self).get(request, *args, **kwargs)-->
1452 <!-- # END BAD CODE-->
1453
1454 <!--**Why's that?**-->
1455 <!--It does not make sense to compute a default ETag here, because the processor would lock us out from the API by always issuing a `304` -->
1456 <!--response on conditional requests, even if the resource was modified meanwhile.-->
1457 <!--Therefore the `APIETAGProcessor` cannot be used without specifying an `etag_func` as keyword argument and there exists convenient -->
1458 <!--[mixin classes](#apietagmixin).-->
1459
1460 <!--You can use the decorator in regular `APIView`, and subclasses from the `rest_framework.generics` module, -->
1461 <!--but ensure to include a `queryset` attribute or override `get_queryset()`:-->
1462
1463 <!-- from rest_framework import generics-->
1464 <!-- from rest_framework.response import Response-->
1465 <!-- from rest_framework_extensions.utils import default_api_object_etag_func-->
1466 <!-- from my_app.models import Book-->
1467
1468 <!-- class BookCustomDestroyView(generics.DestroyAPIView): -->
1469 <!-- # include the queryset here to enable the object lookup -->
1470 <!-- queryset = Book.objects.all()-->
1471
1472 <!-- @api_etag(etag_func=default_api_object_etag_func)-->
1473 <!-- def delete(self, request, *args, **kwargs):-->
1474 <!-- obj = Book.objects.get(id=kwargs['pk'])-->
1475 <!-- # ... perform some custom operations here ...-->
1476 <!-- obj.delete()-->
1477 <!-- return Response(status=status.HTTP_204_NO_CONTENT)-->
1478
1479
1480 <!--The next difference to the `@etag` decorator is that it defines an explicit map of -->
1481 <!--required headers for each HTTP request verb, using the following default values for unsafe methods:-->
1482
1483 <!-- precondition_map = {'PUT': ['If-Match'],-->
1484 <!-- 'PATCH': ['If-Match'],-->
1485 <!-- 'DELETE': ['If-Match']}-->
1486
1487 <!--You can specify a custom set of headers in the decorator by passing the `precondition_map` keyword argument.-->
1488 <!--For instance, this statement-->
1489 <!-- @api_etag(etag_func=default_api_object_etag_func, precondition_map={'PUT': ['X-mycorp-custom']})-->
1490 <!-- def put(self, request, *args, **kwargs):-->
1491 <!-- obj = Book.objects.get(id=kwargs['pk'])-->
1492 <!-- # ... perform some custom operations here ...-->
1493 <!-- obj.save()-->
1494 <!-- return Response(status=status.HTTP_200_OK)-->
1495
1496 <!--checks for the presence of a custom header `X-mycorp-custom` in the request and permits the request, if it is present, -->
1497 <!--or returns a `428 PRECONDITION REQUIRED` response.-->
1498
1499 <!--Similarly, to disable all checks for a particular method simply pass an empty dict:-->
1500
1501 <!-- @api_etag(etag_func=default_api_object_etag_func, precondition_map={})-->
1502 <!-- def put(self, request, *args, **kwargs):-->
1503 <!-- obj = Book.objects.get(id=kwargs['pk'])-->
1504 <!-- # ... perform some custom operations here ...-->
1505 <!-- obj.save()-->
1506 <!-- return Response(status=status.HTTP_200_OK)-->
1507
1508 <!--Please note that passing `None` in the `precondition_map` argument falls back to using the default map.-->
1509
1510 <!--#### Usage ETag with caching-->
1511
1512 <!--As you can see `@etag` and `@cache_response` decorators has similar key calculation approaches. -->
1513 <!--They both can take key from simple callable function. And more than this - in many cases they share the same calculation logic. -->
1514 <!--In the next example we use both decorators, which share one calculation function:-->
1515
1516 <!-- from rest_framework_extensions.etag.decorators import etag-->
1517 <!-- from rest_framework_extensions.cache.decorators import cache_response-->
1518 <!-- from rest_framework_extensions.key_constructor import bits-->
1519 <!-- from rest_framework_extensions.key_constructor.constructors import (-->
1520 <!-- KeyConstructor-->
1521 <!-- )-->
1522
1523 <!-- class CityGetKeyConstructor(KeyConstructor):-->
1524 <!-- format = bits.FormatKeyBit()-->
1525 <!-- language = bits.LanguageKeyBit()-->
1526
1527 <!-- class CityView(views.APIView):-->
1528 <!-- key_constructor_func = CityGetKeyConstructor()-->
1529
1530 <!-- @etag(key_constructor_func)-->
1531 <!-- @cache_response(key_func=key_constructor_func)-->
1532 <!-- def get(self, request, *args, **kwargs):-->
1533 <!-- cities = City.objects.all().values_list('name', flat=True)-->
1534 <!-- return Response(cities)-->
1535
1536 <!--Note the decorators order. First goes `@etag` and then goes `@cache_response`. We want firstly perform conditional processing and after it response processing.-->
1537
1538 <!--There is one more point for it. If conditional processing didn't fail then `key_constructor_func` would be called again in `@cache_response`.-->
1539 <!--But in most cases first calculation is enough. To accomplish this goal you could use `KeyConstructor` initial argument `memoize_for_request`:-->
1540
1541 <!-- >>> key_constructor_func = CityGetKeyConstructor(memoize_for_request=True)-->
1542 <!-- >>> request1, request1 = 'request1', 'request2'-->
1543 <!-- >>> print key_constructor_func(request=request1) # full calculation-->
1544 <!-- request1-key-->
1545 <!-- >>> print key_constructor_func(request=request1) # data from cache-->
1546 <!-- request1-key-->
1547 <!-- >>> print key_constructor_func(request=request2) # full calculation-->
1548 <!-- request2-key-->
1549 <!-- >>> print key_constructor_func(request=request2) # data from cache-->
1550 <!-- request2-key-->
1551
1552 <!--By default `memoize_for_request` is `False`, but you can change it in settings:-->
1553
1554 <!-- REST_FRAMEWORK_EXTENSIONS = {-->
1555 <!-- 'DEFAULT_KEY_CONSTRUCTOR_MEMOIZE_FOR_REQUEST': True-->
1556 <!-- }-->
1557
1558
1559 <!--It's important to note that this memoization is thread safe.-->
1560
1561 <!--#### Saving time and bandwidth-->
1562
1563 <!--When a server returns `ETag` header, you should store it along with the representation data on the client.-->
1564 <!--When making GET and HEAD requests for the same resource in the future, include the `If-None-Match` header-->
1565 <!--to make these requests "conditional". -->
1566
1567 <!--For example, retrieve all cities:-->
1568
1569 <!-- # Request-->
1570 <!-- GET /cities/ HTTP/1.1-->
1571 <!-- Accept: application/json-->
1572
1573 <!-- # Response-->
1574 <!-- HTTP/1.1 200 OK-->
1575 <!-- Content-Type: application/json; charset=UTF-8-->
1576 <!-- ETag: "some_etag_value"-->
1577
1578 <!-- ['Moscow', 'London', 'Paris']-->
1579
1580 <!--If you make same request with `If-None-Match` and there exists a cached value for this request,-->
1581 <!--then server will respond with `304` status code without body data.-->
1582
1583 <!-- # Request-->
1584 <!-- GET /cities/ HTTP/1.1-->
1585 <!-- Accept: application/json-->
1586 <!-- If-None-Match: some_etag_value-->
1587
1588 <!-- # Response-->
1589 <!-- HTTP/1.1 304 NOT MODIFIED-->
1590 <!-- Content-Type: application/json; charset=UTF-8-->
1591 <!-- Etag: "some_etag_value"-->
1592
1593 <!--After this response you can use existing cities data on the client. -->
1594
1595
1596
1597
1598
1599
1600
1601
1602 <!--#### Concurrency control-->
1603
1604 <!--Concurrency control ensures the correct processing of data under concurrent operations by clients.-->
1605 <!--There are two ways to implement concurrency control:-->
1606
1607 <!--* **Pessimistic concurrency control**. In this model, the client gets a lock, obtains-->
1608 <!--the current state of the resource, makes modifications, and then releases the lock.-->
1609 <!--During this process, the server prevents other clients from acquiring a lock on the same resource.-->
1610 <!--Relational databases operate in this manner.-->
1611 <!--* **Optimistic concurrency control**. In this model, the client first gets a token.-->
1612 <!--Instead of obtaining a lock, the client attempts a write operation with the token included in the request.-->
1613 <!--The operation succeeds if the token is still valid and fails otherwise.-->
1614
1615 <!--HTTP, being a stateless application control, is designed for optimistic concurrency control.-->
1616 <!--According to [RFC 6585](https://tools.ietf.org/html/rfc6585), the server can optionally require -->
1617 <!--a condition for a request. This library returns a `428` status, if no ETag is supplied, but would be mandatory -->
1618 <!--for a request to succeed.-->
1619
1620 <!--Update:-->
1621
1622 <!-- PUT/PATCH-->
1623 <!-- +-->
1624 <!-- +-----------+------------+-->
1625 <!-- | ETag |-->
1626 <!-- | supplied? |-->
1627 <!-- ++-----------------+-----+-->
1628 <!-- | |-->
1629 <!-- Yes No-->
1630 <!-- | |-->
1631 <!-- +---------------------++ ++-------------+-->
1632 <!-- | Do preconditions | | Precondition |-->
1633 <!-- | match? | | required? |-->
1634 <!-- +---+-----------------++ ++------------++-->
1635 <!-- | | | |-->
1636 <!-- Yes No No Yes-->
1637 <!-- | | | |-->
1638 <!-- +----------+------+ +-------+----------+ +---+-----+ |-->
1639 <!-- | Does resource | | 412 Precondition | | 200 OK | |-->
1640 <!-- | exist? | | failed | | Update | |-->
1641 <!-- ++---------------++ +------------------+ +---------+ |-->
1642 <!-- | | +-----------+------+-->
1643 <!-- Yes No | 428 Precondition |-->
1644 <!-- | | | required |-->
1645 <!-- +----+----+ +----+----+ +------------------+-->
1646 <!-- | 200 OK | | 404 Not |-->
1647 <!-- | Update | | found |-->
1648 <!-- +---------+ +---------+-->
1649
1650
1651 <!--Delete:-->
1652
1653 <!-- DELETE-->
1654 <!-- +-->
1655 <!-- +-----------+------------+-->
1656 <!-- | ETag |-->
1657 <!-- | supplied? |-->
1658 <!-- ++-----------------+-----+-->
1659 <!-- | |-->
1660 <!-- Yes No-->
1661 <!-- | |-->
1662 <!-- +---------------------++ ++-------------+-->
1663 <!-- | Do preconditions | | Precondition |-->
1664 <!-- | match? | | required? |-->
1665 <!-- +---+-----------------++ ++------------++-->
1666 <!-- | | | |-->
1667 <!-- Yes No No Yes-->
1668 <!-- | | | |-->
1669 <!-- +----------+------+ +-------+----------+ +---+-----+ |-->
1670 <!-- | Does resource | | 412 Precondition | | 204 No | |-->
1671 <!-- | exist? | | failed | | content | |-->
1672 <!-- ++---------------++ +------------------+ +---------+ |-->
1673 <!-- | | +-----------+------+-->
1674 <!-- Yes No | 428 Precondition |-->
1675 <!-- | | | required |-->
1676 <!-- +----+----+ +----+----+ +------------------+-->
1677 <!-- | 204 No | | 404 Not |-->
1678 <!-- | content | | found |-->
1679 <!-- +---------+ +---------+-->
1680
1681
1682
1683 <!--**Example: transient key construction**-->
1684
1685 <!--Here is an example implementation for all (C)RUD methods (except create, because it doesn't need concurrency control)-->
1686 <!--wrapped with the default `etag` decorator. We use our [previous implementation](#custom-key-bit) of the `UpdatedAtKeyBit` that looks up the cache -->
1687 <!--for the last timestamp the particular object was updated on the server. This required us to add `post_save` and `post_delete` signals-->
1688 <!--to our models explicitly. See [below](#apietagmixin) for an example using `@api_etag` and mixins that computes the key from persistent data.-->
1689
1690 <!-- from rest_framework.viewsets import ModelViewSet-->
1691 <!-- from rest_framework_extensions.key_constructor import bits-->
1692 <!-- from rest_framework_extensions.key_constructor.constructors import (-->
1693 <!-- KeyConstructor-->
1694 <!-- )-->
1695
1696 <!-- from your_app.models import City-->
1697 <!-- # use our own implementation that detects an update timestamp in the cache -->
1698 <!-- from your_app.key_bits import UpdatedAtKeyBit-->
1699
1700 <!-- class CityListKeyConstructor(KeyConstructor):-->
1701 <!-- format = bits.FormatKeyBit()-->
1702 <!-- language = bits.LanguageKeyBit()-->
1703 <!-- pagination = bits.PaginationKeyBit()-->
1704 <!-- list_sql_query = bits.ListSqlQueryKeyBit()-->
1705 <!-- unique_view_id = bits.UniqueViewIdKeyBit()-->
1706
1707 <!-- class CityDetailKeyConstructor(KeyConstructor):-->
1708 <!-- format = bits.FormatKeyBit()-->
1709 <!-- language = bits.LanguageKeyBit()-->
1710 <!-- retrieve_sql_query = bits.RetrieveSqlQueryKeyBit()-->
1711 <!-- unique_view_id = bits.UniqueViewIdKeyBit()-->
1712 <!-- updated_at = UpdatedAtKeyBit()-->
1713
1714 <!-- class CityViewSet(ModelViewSet):-->
1715 <!-- list_key_func = CityListKeyConstructor(-->
1716 <!-- memoize_for_request=True-->
1717 <!-- )-->
1718 <!-- obj_key_func = CityDetailKeyConstructor(-->
1719 <!-- memoize_for_request=True-->
1720 <!-- )-->
1721
1722 <!-- @etag(list_key_func)-->
1723 <!-- @cache_response(key_func=list_key_func)-->
1724 <!-- def list(self, request, *args, **kwargs):-->
1725 <!-- return super(CityViewSet, self).list(request, *args, **kwargs)-->
1726
1727 <!-- @etag(obj_key_func)-->
1728 <!-- @cache_response(key_func=obj_key_func)-->
1729 <!-- def retrieve(self, request, *args, **kwargs):-->
1730 <!-- return super(CityViewSet, self).retrieve(request, *args, **kwargs)-->
1731
1732 <!-- @etag(obj_key_func)-->
1733 <!-- def update(self, request, *args, **kwargs):-->
1734 <!-- return super(CityViewSet, self).update(request, *args, **kwargs)-->
1735
1736 <!-- @etag(obj_key_func)-->
1737 <!-- def destroy(self, request, *args, **kwargs):-->
1738 <!-- return super(CityViewSet, self).destroy(request, *args, **kwargs)-->
1739
1740
1741 <!--#### ETag for unsafe methods-->
1742
1743 <!--From previous section you could see that unsafe methods, such `update` (PUT, PATCH) or `destroy` (DELETE), have the same `@etag`-->
1744 <!--decorator wrapping manner as the safe methods.-->
1745
1746 <!--But every unsafe method has one distinction from safe method - it changes the data-->
1747 <!--which could be used for Etag calculation. In our case it is `UpdatedAtKeyBit`. It means that we should calculate Etag:-->
1748
1749 <!--* Before building response - for `If-Match` and `If-None-Match` conditions validation-->
1750 <!--* After building response (if necessary) - for clients-->
1751
1752 <!--`@etag` decorator has special attribute `rebuild_after_method_evaluation`, which by default is `False`.-->
1753
1754 <!--If you specify `rebuild_after_method_evaluation` as `True` then Etag will be rebuilt after method evaluation:-->
1755
1756 <!-- class CityViewSet(ModelViewSet):-->
1757 <!-- ...-->
1758 <!-- @etag(obj_key_func, rebuild_after_method_evaluation=True)-->
1759 <!-- def update(self, request, *args, **kwargs):-->
1760 <!-- return super(CityViewSet, self).update(request, *args, **kwargs)-->
1761
1762 <!-- @etag(obj_key_func)-->
1763 <!-- def destroy(self, request, *args, **kwargs):-->
1764 <!-- return super(CityViewSet, self).destroy(request, *args, **kwargs)-->
1765
1766 <!-- # Request-->
1767 <!-- PUT /cities/1/ HTTP/1.1-->
1768 <!-- Accept: application/json-->
1769
1770 <!-- {"name": "London"}-->
1771
1772 <!-- # Response-->
1773 <!-- HTTP/1.1 200 OK-->
1774 <!-- Content-Type: application/json; charset=UTF-8-->
1775 <!-- ETag: "4e63ef056f47270272b96523f51ad938b5ea141024b767880eac047d10a0b339"-->
1776
1777 <!-- {-->
1778 <!-- id: 1,-->
1779 <!-- name: "London"-->
1780 <!-- }-->
1781
1782 <!--As you can see we didn't specify `rebuild_after_method_evaluation` for `destroy` method. That is because there is no-->
1783 <!--sense to use returned ETag value on clients if object deletion already performed.-->
1784
1785 <!--With `rebuild_after_method_evaluation` parameter Etag calculation for `PUT`/`PATCH` method would look like:-->
1786
1787 <!-- +--------------+-->
1788 <!-- | Request |-->
1789 <!-- +--------------+-->
1790 <!-- |-->
1791 <!-- +--------------------------+-->
1792 <!-- | Calculate Etag |-->
1793 <!-- | for condition matching |-->
1794 <!-- +--------------------------+-->
1795 <!-- |-->
1796 <!-- +--------------------+-->
1797 <!-- | Do preconditions |-->
1798 <!-- | match? |-->
1799 <!-- +--------------------+-->
1800 <!-- | |-->
1801 <!-- Yes No-->
1802 <!-- | |-->
1803 <!-- +--------------+ +--------------------+-->
1804 <!-- | Update the | | 412 Precondition |-->
1805 <!-- | resource | | failed |-->
1806 <!-- +--------------+ +--------------------+-->
1807 <!-- |-->
1808 <!-- +--------------------+-->
1809 <!-- | Calculate Etag |-->
1810 <!-- | again and add it |-->
1811 <!-- | to response |-->
1812 <!-- +--------------------+-->
1813 <!-- |-->
1814 <!-- +------------+-->
1815 <!-- | Return |-->
1816 <!-- | response |-->
1817 <!-- +------------+-->
1818
1819 <!--`If-None-Match` example for `DELETE` method:-->
1820
1821 <!-- # Request-->
1822 <!-- DELETE /cities/1/ HTTP/1.1-->
1823 <!-- Accept: application/json-->
1824 <!-- If-None-Match: some_etag_value-->
1825
1826 <!-- # Response-->
1827 <!-- HTTP/1.1 304 NOT MODIFIED-->
1828 <!-- Content-Type: application/json; charset=UTF-8-->
1829 <!-- Etag: "some_etag_value"-->
1830
1831
1832 <!--`If-Match` example for `DELETE` method:-->
1833
1834 <!-- # Request-->
1835 <!-- DELETE /cities/1/ HTTP/1.1-->
1836 <!-- Accept: application/json-->
1837 <!-- If-Match: another_etag_value-->
1838
1839 <!-- # Response-->
1840 <!-- HTTP/1.1 412 PRECONDITION FAILED-->
1841 <!-- Content-Type: application/json; charset=UTF-8-->
1842 <!-- Etag: "some_etag_value"-->
1843
1844
1845 <!--#### ETAGMixin-->
1846
1847 <!--It is common to process etags for standard [viewset](http://www.django-rest-framework.org/api-guide/viewsets)-->
1848 <!--`retrieve`, `list`, `update` and `destroy` methods.-->
1849 <!--That is why `ETAGMixin` exists. Just mix it into viewset-->
1850 <!--implementation and those methods will use functions, defined in `REST_FRAMEWORK_EXTENSIONS` [settings](#settings):-->
1851
1852 <!--* *"DEFAULT\_OBJECT\_ETAG\_FUNC"* for `retrieve`, `update` and `destroy` methods-->
1853 <!--* *"DEFAULT\_LIST\_ETAG\_FUNC"* for `list` method-->
1854
1855 <!--By default those functions are using [DefaultKeyConstructor](#default-key-constructor) and extends it:-->
1856
1857 <!--* With `RetrieveSqlQueryKeyBit` for *"DEFAULT\_OBJECT\_ETAG\_FUNC"*-->
1858 <!--* With `ListSqlQueryKeyBit` and `PaginationKeyBit` for *"DEFAULT\_LIST\_ETAG\_FUNC"*-->
1859
1860 <!--You can change those settings for custom ETag generation:-->
1861
1862 <!-- REST_FRAMEWORK_EXTENSIONS = {-->
1863 <!-- 'DEFAULT_OBJECT_ETAG_FUNC':-->
1864 <!-- 'rest_framework_extensions.utils.default_object_etag_func',-->
1865 <!-- 'DEFAULT_LIST_ETAG_FUNC':-->
1866 <!-- 'rest_framework_extensions.utils.default_list_etag_func',-->
1867 <!-- }-->
1868
1869 <!--Mixin example usage:-->
1870
1871 <!-- from myapps.serializers import UserSerializer-->
1872 <!-- from rest_framework_extensions.etag.mixins import ETAGMixin-->
1873
1874 <!-- class UserViewSet(ETAGMixin, viewsets.ModelViewSet):-->
1875 <!-- serializer_class = UserSerializer-->
1876
1877 <!--You can change etag function by providing `object_etag_func` or-->
1878 <!--`list_etag_func` methods in view class:-->
1879
1880 <!-- class UserViewSet(ETAGMixin, viewsets.ModelViewSet):-->
1881 <!-- serializer_class = UserSerializer-->
1882
1883 <!-- def object_etag_func(self, **kwargs):-->
1884 <!-- return 'some key for object'-->
1885
1886 <!-- def list_etag_func(self, **kwargs):-->
1887 <!-- return 'some key for list'-->
1888
1889 <!--Of course you can use custom [key constructor](#key-constructor):-->
1890
1891 <!-- from yourapp.key_constructors import (-->
1892 <!-- CustomObjectKeyConstructor,-->
1893 <!-- CustomListKeyConstructor,-->
1894 <!-- )-->
1895
1896 <!-- class UserViewSet(ETAGMixin, viewsets.ModelViewSet):-->
1897 <!-- serializer_class = UserSerializer-->
1898 <!-- object_etag_func = CustomObjectKeyConstructor()-->
1899 <!-- list_etag_func = CustomListKeyConstructor()-->
1900
1901 <!--It is important to note that ETags for unsafe method `update` is processed with parameter-->
1902 <!--`rebuild_after_method_evaluation` equals `True`. You can read why from [this](#etag-for-unsafe-methods) section.-->
1903
1904 <!--There are other mixins for more granular Etag calculation in `rest_framework_extensions.etag.mixins` module:-->
1905
1906 <!--* **ReadOnlyETAGMixin** - only for `retrieve` and `list` methods-->
1907 <!--* **RetrieveETAGMixin** - only for `retrieve` method-->
1908 <!--* **ListETAGMixin** - only for `list` method-->
1909 <!--* **DestroyETAGMixin** - only for `destroy` method-->
1910 <!--* **UpdateETAGMixin** - only for `update` method-->
1911
1912
1913 <!--#### APIETagMixin-->
1914
1915 <!--*New in DRF-extensions 0.3.2*-->
1916
1917 <!--In analogy to `ETAGMixin` the `APIETAGMixin` exists. Just mix it into DRF viewsets or `APIViews` -->
1918 <!--and those methods will use the ETag functions, defined in `REST_FRAMEWORK_EXTENSIONS` [settings](#settings):-->
1919
1920 <!--* *"DEFAULT\_API\_OBJECT\_ETAG\_FUNC"* for `retrieve`, `update` and `destroy` methods-->
1921 <!--* *"DEFAULT\_API\_LIST\_ETAG\_FUNC"* for `list` method-->
1922
1923 <!--By default those functions are using custom key constructors that create the key from **persisted model attributes**:-->
1924
1925 <!--* `RetrieveModelKeyBit` (see [definition](#retrievemodelkeybit)) for *"DEFAULT\_API\_OBJECT\_ETAG\_FUNC"*-->
1926 <!--* `ListModelKeyBit` (see [definition](#listmodelkeybit)) for *"DEFAULT\_API\_LIST\_ETAG\_FUNC"*-->
1927
1928 <!--You can change those settings globally for your custom ETag generation, or use the default values, which should cover -->
1929 <!--the most common use cases:-->
1930
1931 <!-- REST_FRAMEWORK_EXTENSIONS = {-->
1932 <!-- 'DEFAULT_API_OBJECT_ETAG_FUNC': -->
1933 <!-- 'rest_framework_extensions.utils.default_api_object_etag_func',-->
1934 <!-- 'DEFAULT_API_LIST_ETAG_FUNC': -->
1935 <!-- 'rest_framework_extensions.utils.default_api_list_etag_func',-->
1936 <!-- }-->
1937
1938 <!--Mixin example usage:-->
1939
1940 <!-- from myapps.serializers import UserSerializer-->
1941 <!-- from rest_framework_extensions.etag.mixins import APIETAGMixin-->
1942
1943 <!-- class UserViewSet(APIETAGMixin, viewsets.ModelViewSet):-->
1944 <!-- serializer_class = UserSerializer-->
1945
1946 <!--You can change etag function by providing `api_object_etag_func` or-->
1947 <!--`api_list_etag_func` methods in view class:-->
1948
1949 <!-- class UserViewSet(APIETAGMixin, viewsets.ModelViewSet):-->
1950 <!-- serializer_class = UserSerializer-->
1951
1952 <!-- def api_object_etag_func(self, **kwargs):-->
1953 <!-- return 'some key for object'-->
1954
1955 <!-- def api_list_etag_func(self, **kwargs):-->
1956 <!-- return 'some key for list'-->
1957
1958 <!--Of course you can use custom [key constructors](#key-constructor):-->
1959
1960 <!-- from yourapp.key_constructors import (-->
1961 <!-- CustomObjectKeyConstructor,-->
1962 <!-- CustomListKeyConstructor,-->
1963 <!-- )-->
1964
1965 <!-- class UserViewSet(APIETAGMixin, viewsets.ModelViewSet):-->
1966 <!-- serializer_class = UserSerializer-->
1967 <!-- api_object_etag_func = CustomObjectKeyConstructor()-->
1968 <!-- api_list_etag_func = CustomListKeyConstructor()-->
1969
1970 <!--There are other mixins for more granular ETag calculation in `rest_framework_extensions.etag.mixins` module:-->
1971
1972 <!--* **APIReadOnlyETAGMixin** - only for `retrieve` and `list` methods-->
1973 <!--* **APIRetrieveETAGMixin** - only for `retrieve` method-->
1974 <!--* **APIListETAGMixin** - only for `list` method-->
1975 <!--* **APIDestroyETAGMixin** - only for `destroy` method-->
1976 <!--* **APIUpdateETAGMixin** - only for `update` method-->
1977
1978 <!--By default, all mixins require the conditional requests, i.e. they use the default `precondition_map` from the -->
1979 <!--`APIETAGProcessor` class.-->
1980
1981
1982 <!--#### Gzipped ETags-->
1983
1984 <!--If you use [GZipMiddleware](https://docs.djangoproject.com/en/dev/ref/middleware/#module-django.middleware.gzip)-->
1985 <!--and your client accepts Gzipped response, then you should return different ETags for compressed and not compressed responses.-->
1986 <!--That's what `GZipMiddleware` does by default while processing response --->
1987 <!--it adds `;gzip` postfix to ETag response header if client requests compressed response.-->
1988 <!--Lets see it in example. First request without compression:-->
1989
1990 <!-- # Request-->
1991 <!-- GET /cities/ HTTP/1.1-->
1992 <!-- Accept: application/json-->
1993
1994 <!-- # Response-->
1995 <!-- HTTP/1.1 200 OK-->
1996 <!-- Content-Type: application/json; charset=UTF-8-->
1997 <!-- ETag: "e7b50490dc"-->
1998
1999 <!-- ['Moscow', 'London', 'Paris']-->
2000
2001 <!--Second request with compression:-->
2002
2003 <!-- # Request-->
2004 <!-- GET /cities/ HTTP/1.1-->
2005 <!-- Accept: application/json-->
2006 <!-- Accept-Encoding: gzip-->
2007
2008 <!-- # Response-->
2009 <!-- HTTP/1.1 200 OK-->
2010 <!-- Content-Type: application/json-->
2011 <!-- Content-Length: 675-->
2012 <!-- Content-Encoding: gzip-->
2013 <!-- ETag: "e7b50490dc;gzip"-->
2014
2015 <!-- wS?n?0?_%o?cc?Ҫ?Eʒ?Cժʻ?a\1?a?^T*7q<>[Nvh?[?^9?x:/Ms?79?Fd/???ۦjES?ڽ?&??c%^?C[K۲%N?w{?졭2?m?}?Q&Egz??-->
2016
2017 <!--As you can see there is `;gzip` postfix in ETag response header.-->
2018 <!--That's ok but there is one caveat - drf-extension doesn't know how you post-processed calculated ETag value.-->
2019 <!--And your clients could have next problem with conditional request:-->
2020
2021 <!--* Client sends request to retrieve compressed data about cities to `/cities/`-->
2022 <!--* DRF-extensions decorator calculates ETag header for response equals, for example, `123`-->
2023 <!--* `GZipMiddleware` adds `;gzip` postfix to ETag header response, and now it equals `123;gzip`-->
2024 <!--* Client retrieves response with ETag equals `123;gzip`-->
2025 <!--* Client again makes request to retrieve compressed data about cities,-->
2026 <!--but now it's conditional request with `If-None-Match` header equals `123;gzip`-->
2027 <!--* DRF-extensions decorator calculates ETag value for processing conditional request.-->
2028 <!--But it doesn't know, that `GZipMiddleware` added `;gzip` postfix for previous response.-->
2029 <!--DRF-extensions decorator calculates ETag equals `123`, compares it with `123;gzip` and returns-->
2030 <!--response with status code 200, because `123` != `123;gzip`-->
2031
2032 <!--You can solve this problem by stripping `;gzip` postfix on client side.-->
2033
2034 <!--But there are so many libraries that just magically uses ETag response header without allowing to-->
2035 <!--pre-process conditional requests (for example, browser). If that's you case then you could add custom middleware which removes `;gzip`-->
2036 <!--postfix from header:-->
2037
2038 <!-- # yourapp/middleware.py-->
2039
2040 <!-- class RemoveEtagGzipPostfix(object):-->
2041 <!-- def process_response(self, request, response):-->
2042 <!-- if response.has_header('ETag') and response['ETag'][-6:] == ';gzip"':-->
2043 <!-- response['ETag'] = response['ETag'][:-6] + '"'-->
2044 <!-- return response-->
2045
2046 <!--Don't forget to add this middleware in your settings before `GZipMiddleware`:-->
2047
2048 <!-- # settings.py-->
2049 <!-- MIDDLEWARE_CLASSES = (-->
2050 <!-- ...-->
2051 <!-- 'yourapp.RemoveEtagGzipPostfix',-->
2052 <!-- 'django.middleware.gzip.GZipMiddleware',-->
2053 <!-- 'django.middleware.common.CommonMiddleware',-->
2054 <!-- ...-->
2055 <!-- )-->
20502056
20512057
20522058
22062212 [Django REST framework documentation](https://www.django-rest-framework.org/community/release-notes/).
22072213
22082214
2215 #### Development version
2216
2217 * Added support for Django 3.0 (#276)
2218 * Dropped support for Django 2.0
2219 * Added support for DRF 3.10 and 3.11 (#261, #279)
2220 * Added support for Python 3.8 (#282)
2221 * Added paginate decorator (#266)
2222 * Added limit/offset and cursor pagination to PaginationKeyBit (#204)
2223
2224
22092225 #### 0.5.0
22102226
2211 * Maay 10, 2019 *
2227 *May 10, 2019*
22122228
22132229 * Dropped python 2.7 and 3.4
22142230 * Fix possible header mutation issue
+0
-1
docs/metrika_code.txt less more
0 <!-- Yandex.Metrika informer --><a href="https://metrika.yandex.ru/stat/?id=24865421&amp;from=informer" target="_blank" rel="nofollow"><img src="//bs.yandex.ru/informer/24865421/3_1_FFFFFFFF_EFEFEFFF_0_pageviews" style="width:88px; height:31px; border:0;" alt="Яндекс.Метрика" title="Яндекс.Метрика: данные за сегодня (просмотры, визиты и уникальные посетители)" onclick="try{Ya.Metrika.informer({i:this,id:24865421,lang:'ru'});return false}catch(e){}"/></a><!-- /Yandex.Metrika informer --><!-- Yandex.Metrika counter --><script type="text/javascript">(function (d, w, c) { (w[c] = w[c] || []).push(function() { try { w.yaCounter24865421 = new Ya.Metrika({id:24865421, webvisor:true, clickmap:true, trackLinks:true, accurateTrackBounce:true, trackHash:true}); } catch(e) { } }); var n = d.getElementsByTagName("script")[0], s = d.createElement("script"), f = function () { n.parentNode.insertBefore(s, n); }; s.type = "text/javascript"; s.async = true; s.src = (d.location.protocol == "https:" ? "https:" : "http:") + "//mc.yandex.ru/metrika/watch.js"; if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); } })(document, window, "yandex_metrika_callbacks");</script><noscript><div><img src="//mc.yandex.ru/watch/24865421" style="position:absolute; left:-9999px;" alt="" /></div></noscript><!-- /Yandex.Metrika counter -->
+0
-5
docs/post_process_docs.py less more
0 docs_file_path = 'docs/index.html'
1 docs_file_content = open(docs_file_path, 'r', encoding='utf8').read()[:-len('</html>')]
2 docs_file = open(docs_file_path, 'w', encoding='utf-8')
3 metrika_file_content = open('docs/metrika_code.txt', 'r', encoding='utf-8').read()
4 docs_file.write(docs_file_content + metrika_file_content + '</html>')
0 __version__ = '0.5.0' # from 0.4.0
0 __version__ = '0.6.0' # from 0.5.0
11
22 VERSION = __version__
0 from django.utils.encoding import force_text
0 from django.utils.encoding import force_str
11
22 from rest_framework import status
33 from rest_framework.response import Response
1414
1515 def is_valid_bulk_operation(self):
1616 if extensions_api_settings.DEFAULT_BULK_OPERATION_HEADER_NAME:
17 header_name = utils.prepare_header_name(extensions_api_settings.DEFAULT_BULK_OPERATION_HEADER_NAME)
17 header_name = utils.prepare_header_name(
18 extensions_api_settings.DEFAULT_BULK_OPERATION_HEADER_NAME)
1819 return bool(self.request.META.get(header_name, None)), {
1920 'detail': 'Header \'{0}\' should be provided for bulk operation.'.format(
2021 extensions_api_settings.DEFAULT_BULK_OPERATION_HEADER_NAME
6667 is_valid, errors = self.is_valid_bulk_operation()
6768 if is_valid:
6869 queryset = self.filter_queryset(self.get_queryset())
69 update_bulk_dict = self.get_update_bulk_dict(serializer=self.get_serializer_class()(), data=request.data)
70 self.pre_save_bulk(queryset, update_bulk_dict) # todo: test and document me
70 update_bulk_dict = self.get_update_bulk_dict(
71 serializer=self.get_serializer_class()(), data=request.data)
72 # todo: test and document me
73 self.pre_save_bulk(queryset, update_bulk_dict)
7174 try:
7275 queryset.update(**update_bulk_dict)
7376 except ValueError as e:
7477 errors = {
75 'detail': force_text(e)
78 'detail': force_str(e)
7679 }
7780 return Response(errors, status=status.HTTP_400_BAD_REQUEST)
78 self.post_save_bulk(queryset, update_bulk_dict) # todo: test and document me
81 # todo: test and document me
82 self.post_save_bulk(queryset, update_bulk_dict)
7983 return Response(status=status.HTTP_204_NO_CONTENT)
8084 else:
8185 return Response(errors, status=status.HTTP_400_BAD_REQUEST)
0 from functools import wraps
0 from functools import wraps, WRAPPER_ASSIGNMENTS
11
22 from django.http.response import HttpResponse
3 from django.utils.decorators import available_attrs
43
54
65 from rest_framework_extensions.settings import extensions_api_settings
4948 def __call__(self, func):
5049 this = self
5150
52 @wraps(func, assigned=available_attrs(func))
51 @wraps(func, assigned=WRAPPER_ASSIGNMENTS)
5352 def inner(self, request, *args, **kwargs):
5453 return this.process_cache_response(
5554 view_instance=self,
9594 # build smaller Django HttpResponse
9695 content, status, headers = response_triple
9796 response = HttpResponse(content=content, status=status)
98 response._headers = headers
97 for k, v in headers.values():
98 response[k] = v
9999
100100 if not hasattr(response, '_closable_objects'):
101101 response._closable_objects = []
11 from rest_framework_extensions.settings import extensions_api_settings
22
33
4 class BaseCacheResponseMixin(object):
4 class BaseCacheResponseMixin:
55 # todo: test me. Create generic test like
66 # test_cache_reponse(view_instance, method, should_rebuild_after_method_evaluation)
77 object_cache_key_func = extensions_api_settings.DEFAULT_OBJECT_CACHE_KEY_FUNC
11 The `compat` module provides support for backwards compatibility with older
22 versions of django/python, and compatibility wrappers around optional packages.
33 """
4 from __future__ import unicode_literals
54
65
76 # handle different QuerySet representations
0 def paginate(pagination_class=None, **kwargs):
1 """
2 Decorator that adds a pagination_class to GenericViewSet class.
3 Custom pagination class also available.
4
5 Usage :
6 from rest_framework.pagination import CursorPagination
7
8 @paginate(pagination_class=CursorPagination, page_size=5, ordering='-created_at')
9 class FooViewSet(viewsets.GenericViewSet):
10 ...
11
12 """
13 assert pagination_class is not None, (
14 "@paginate missing required argument: 'pagination_class'"
15 )
16
17 class _Pagination(pagination_class):
18 def __init__(self):
19 self.__dict__.update(kwargs)
20
21 def decorator(_class):
22 _class.pagination_class = _Pagination
23 return _class
24
25 return decorator
00 import logging
1 from functools import wraps
2
3 from django.utils.decorators import available_attrs
1 from functools import wraps, WRAPPER_ASSIGNMENTS
2
43 from django.utils.http import parse_etags, quote_etag
54
65 from rest_framework import status
2625 def __call__(self, func):
2726 this = self
2827
29 @wraps(func, assigned=available_attrs(func))
28 @wraps(func, assigned=WRAPPER_ASSIGNMENTS)
3029 def inner(self, request, *args, **kwargs):
3130 return this.process_conditional_request(
3231 view_instance=self,
167166 'the key is the HTTP verb, and the value is a list of '
168167 'HTTP headers that must all be present for that request.')
169168
170 super(APIETAGProcessor, self).__init__(etag_func=etag_func,
169 super().__init__(etag_func=etag_func,
171170 rebuild_after_method_evaluation=rebuild_after_method_evaluation)
172171
173172 def get_etags_and_matchers(self, request):
175174 # evaluate the preconditions, raises 428 if condition is not met
176175 self.evaluate_preconditions(request)
177176 # alright, headers are present, extract the values and match the conditions
178 return super(APIETAGProcessor, self).get_etags_and_matchers(request)
177 return super().get_etags_and_matchers(request)
179178
180179 def evaluate_preconditions(self, request):
181180 """Evaluate whether the precondition for the request is met."""
1111 class ListETAGMixin(BaseETAGMixin):
1212 @etag(etag_func='list_etag_func')
1313 def list(self, request, *args, **kwargs):
14 return super(ListETAGMixin, self).list(request, *args, **kwargs)
14 return super().list(request, *args, **kwargs)
1515
1616
1717 class RetrieveETAGMixin(BaseETAGMixin):
0 from django.utils.translation import ugettext_lazy as _
0 from django.utils.translation import gettext_lazy as _
11 from rest_framework import status
22 from rest_framework.exceptions import APIException
33
0 # -*- coding: utf-8 -*-
10 from rest_framework.relations import HyperlinkedRelatedField
21
32
43 class ResourceUriField(HyperlinkedRelatedField):
54 """
6 Represents a hyperlinking uri that points to the detail view for that object.
5 Represents a hyperlinking uri that points to the
6 detail view for that object.
77
88 Example:
99 class SurveySerializer(serializers.ModelSerializer):
11 from django.db.models.query import EmptyQuerySet
22 from django.db.models.sql.datastructures import EmptyResultSet
33
4 from django.utils.encoding import force_text
4 from django.utils.encoding import force_str
55
66 from rest_framework_extensions import compat
77
88
9 class AllArgsMixin(object):
9 class AllArgsMixin:
1010
1111 def __init__(self, params='*'):
12 super(AllArgsMixin, self).__init__(params)
13
14
15 class KeyBitBase(object):
12 super().__init__(params)
13
14
15 class KeyBitBase:
1616 def __init__(self, params=None):
1717 self.params = params
1818
4747 params = source_dict.keys()
4848
4949 for key in params:
50 value = source_dict.get(self.prepare_key_for_value_retrieving(key))
50 value = source_dict.get(
51 self.prepare_key_for_value_retrieving(key))
5152 if value is not None:
52 data[self.prepare_key_for_value_assignment(key)] = force_text(value)
53 data[self.prepare_key_for_value_assignment(
54 key)] = force_str(value)
5355
5456 return data
5557
6567
6668 class UniqueViewIdKeyBit(KeyBitBase):
6769 def get_data(self, params, view_instance, view_method, request, args, kwargs):
68 return u'.'.join([
70 return '.'.join([
6971 view_instance.__module__,
7072 view_instance.__class__.__name__
7173 ])
7375
7476 class UniqueMethodIdKeyBit(KeyBitBase):
7577 def get_data(self, params, view_instance, view_method, request, args, kwargs):
76 return u'.'.join([
78 return '.'.join([
7779 view_instance.__module__,
7880 view_instance.__class__.__name__,
7981 view_method.__name__
8385 class LanguageKeyBit(KeyBitBase):
8486 """
8587 Return example:
86 u'en'
87
88 """
89
90 def get_data(self, params, view_instance, view_method, request, args, kwargs):
91 return force_text(get_language())
88 'en'
89
90 """
91
92 def get_data(self, params, view_instance, view_method, request, args, kwargs):
93 return force_str(get_language())
9294
9395
9496 class FormatKeyBit(KeyBitBase):
101103 """
102104
103105 def get_data(self, params, view_instance, view_method, request, args, kwargs):
104 return force_text(request.accepted_renderer.format)
106 return force_str(request.accepted_renderer.format)
105107
106108
107109 class UserKeyBit(KeyBitBase):
115117
116118 def get_data(self, params, view_instance, view_method, request, args, kwargs):
117119 if hasattr(request, 'user') and request.user and request.user.is_authenticated:
118 return force_text(self._get_id_from_user(request.user))
119 else:
120 return u'anonymous'
120 return force_str(self._get_id_from_user(request.user))
121 else:
122 return 'anonymous'
121123
122124 def _get_id_from_user(self, user):
123125 return user.id
129131 {'accept-language': u'ru', 'x-geobase-id': '123'}
130132
131133 """
134
132135 def get_source_dict(self, params, view_instance, view_method, request, args, kwargs):
133136 return request.META
134137
135138 def prepare_key_for_value_retrieving(self, key):
136139 from rest_framework_extensions.utils import prepare_header_name
137140
138 return prepare_header_name(key.lower()) # Accept-Language => http_accept_language
141 # Accept-Language => http_accept_language
142 return prepare_header_name(key.lower())
139143
140144 def prepare_key_for_value_assignment(self, key):
141145 return key.lower() # Accept-Language => accept-language
169173 {'page_size': 100, 'page': '1'}
170174
171175 """
176 paginator_attrs = [
177 'page_query_param', 'page_size_query_param',
178 'limit_query_param', 'offset_query_param',
179 'cursor_query_param',
180 ]
181
172182 def get_data(self, **kwargs):
173183 kwargs['params'] = []
174 if hasattr(kwargs['view_instance'], 'paginator'):
175 if hasattr(kwargs['view_instance'].paginator, 'page_query_param'):
176 kwargs['params'].append(
177 kwargs['view_instance'].paginator.page_query_param)
178 if hasattr(kwargs['view_instance'].paginator,
179 'page_size_query_param'):
180 kwargs['params'].append(
181 kwargs['view_instance'].paginator.page_size_query_param)
182 return super(PaginationKeyBit, self).get_data(**kwargs)
184 paginator = getattr(kwargs['view_instance'], 'paginator', None)
185
186 if paginator:
187 for attr in self.paginator_attrs:
188 param = getattr(paginator, attr, None)
189 if param:
190 kwargs['params'].append(param)
191
192 return super().get_data(**kwargs)
183193
184194
185195 class SqlQueryKeyBitBase(KeyBitBase):
188198 return None
189199 else:
190200 try:
191 return force_text(queryset.query.__str__())
201 return force_str(queryset.query.__str__())
192202 except EmptyResultSet:
193203 return None
194204
198208 Return the actual contents of the query set.
199209 This class is similar to the `SqlQueryKeyBitBase`.
200210 """
211
201212 def _get_queryset_query_values(self, queryset):
202213 if isinstance(queryset, EmptyQuerySet) or queryset.count() == 0:
203214 return None
204215 else:
205216 try:
206217 # run through the instances and collect all values in ordered fashion
207 return compat.queryset_to_value_list(force_text(queryset.values_list()))
218 return compat.queryset_to_value_list(force_str(queryset.values_list()))
208219 except EmptyResultSet:
209220 return None
210221
234245 Return example:
235246 u"[(3, False)]"
236247 """
248
237249 def get_data(self, params, view_instance, view_method, request, args, kwargs):
238250 lookup_value = view_instance.kwargs[view_instance.lookup_field]
239251 try:
252264 Return example:
253265 u"[(1, True), (2, True), (3, False)]"
254266 """
267
255268 def get_data(self, params, view_instance, view_method, request, args, kwargs):
256269 queryset = view_instance.filter_queryset(view_instance.get_queryset())
257270 return self._get_queryset_query_values(queryset)
0 # -*- coding: utf-8 -*-
1 # Try to import six from Django, fallback to included `six`.
2
3
40 from rest_framework_extensions.cache.mixins import CacheResponseMixin
51 # from rest_framework_extensions.etag.mixins import ReadOnlyETAGMixin, ETAGMixin
6 from rest_framework_extensions.bulk_operations.mixins import ListUpdateModelMixin
2 from rest_framework_extensions.bulk_operations.mixins import ListUpdateModelMixin, ListDestroyModelMixin
73 from rest_framework_extensions.settings import extensions_api_settings
84 from django.http import Http404
95
+0
-0
rest_framework_extensions/models.py less more
(Empty file)
0 # -*- coding: utf-8 -*-
10 from rest_framework.permissions import DjangoObjectPermissions
21
32
0 # -*- coding: utf-8 -*-
10 from rest_framework.routers import DefaultRouter, SimpleRouter
21 from rest_framework_extensions.utils import compose_parent_pk_kwarg_name
32
102102 return self.generic('OPTIONS', path, data, content_type, **extra)
103103
104104 def request(self, **kwargs):
105 request = super(APIRequestFactory, self).request(**kwargs)
105 request = super().request(**kwargs)
106106 request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
107107 return request
108108
127127
128128 class APIClient(APIRequestFactory, DjangoClient):
129129 def __init__(self, enforce_csrf_checks=False, **defaults):
130 super(APIClient, self).__init__(**defaults)
130 super().__init__(**defaults)
131131 self.handler = ForceAuthClientHandler(enforce_csrf_checks)
132132 self._credentials = {}
133133
3535
3636 def get_unique_method_id(view_instance, view_method):
3737 # todo: test me as UniqueMethodIdKeyBit
38 return u'.'.join([
38 return '.'.join([
3939 view_instance.__module__,
4040 view_instance.__class__.__name__,
4141 view_method.__name__
00 # Django settings for testproject project.
11 import multiprocessing
2 import os
32
43 DEBUG = True
54 DEBUG_PROPAGATE_EXCEPTIONS = True
0 # -*- coding: utf-8 -*-
10 try:
21 from django.utils.deprecation import MiddlewareMixin
32 except ImportError:
0 # -*- coding: utf-8 -*-
10 from django.test import TestCase, override_settings
21
32
0 # -*- coding: utf-8 -*-
10 from django.conf.urls import url
21
32 from .views import MyView
0 # -*- coding: utf-8 -*-
10 from django.views import View
21 from django.http import HttpResponse
32
00 from django.test import TestCase, override_settings
1 from django.utils.encoding import force_text
1 from django.utils.encoding import force_str
22
33
44 @override_settings(ROOT_URLCONF='tests_app.tests.functional.cache.decorators.urls')
66
77 def test_should_return_response(self):
88 resp = self.client.get('/hello/')
9 self.assertEqual(force_text(resp.content), '"Hello world"')
9 self.assertEqual(force_str(resp.content), '"Hello world"')
1010
1111 def test_should_return_same_response_if_cached(self):
1212 resp_1 = self.client.get('/hello/')
0 # -*- coding: utf-8 -*-
10 from django.conf.urls import url
21
32 from .views import HelloView
66 class HelloView(views.APIView):
77 @cache_response()
88 def get(self, request, *args, **kwargs):
9 return Response('Hello world')
9 return Response('Hello world')
0 # -*- coding: utf-8 -*-
10 from rest_framework import routers
21
32 from .views import UserModelViewSet
0 # -*- coding: utf-8 -*-
10 from rest_framework import routers
21
32 from .views import (
3838 queryset_detail = Comment.objects.filter(id=1)
3939
4040 def get_queryset(self):
41 return super(CommentWithDetailSerializerAndNoArgsForGetQuerySetViewSet, self).get_queryset()
41 return super().get_queryset()
11
22 import unittest
33
4 import django
45 from django.test import override_settings
56
67 from rest_framework_extensions.test import APITestCase
154155 self.fail('Errors with invalid for DB data should be caught')
155156 else:
156157 self.assertEqual(resp.status_code, 400)
157 expected_message = {
158 'detail': "invalid literal for int() with base 10: 'Not integer value'"
159 }
158 if django.VERSION < (3, 0, 0):
159 expected_message = {
160 'detail': "invalid literal for int() with base 10: 'Not integer value'"
161 }
162 else:
163 expected_message = {
164 'detail': "Field 'age' expected a number but got 'Not integer value'."
165 }
160166 self.assertEqual(resp.data, expected_message)
161167
162168 def test_should_use_source_if_it_set_in_serializer(self):
7979
8080 class TaskCommentViewSet(CommentViewSet):
8181 def get_queryset(self):
82 return super(TaskCommentViewSet, self).get_queryset().filter(
82 return super().get_queryset().filter(
8383 content_type=ContentType.objects.get_for_model(TaskModel)
8484 )
8585
8686
8787 class BookCommentViewSet(CommentViewSet):
8888 def get_queryset(self):
89 return super(BookCommentViewSet, self).get_queryset().filter(
89 return super().get_queryset().filter(
9090 content_type=ContentType.objects.get_for_model(BookModel)
9191 )
9292
220220
221221 class ETAGProcessorTestBehavior_if_none_match(ETAGProcessorTestBehaviorMixin, TestCase):
222222 def setUp(self):
223 super(ETAGProcessorTestBehavior_if_none_match, self).setUp()
223 super().setUp()
224224 self.header_name = 'if-none-match'
225225 self.experiments = [
226226 {
274274
275275 class ETAGProcessorTestBehavior_if_match(ETAGProcessorTestBehaviorMixin, TestCase):
276276 def setUp(self):
277 super(ETAGProcessorTestBehavior_if_match, self).setUp()
277 super().setUp()
278278 self.header_name = 'if-match'
279279 self.experiments = [
280280 {
341341 class View(views.APIView):
342342 @api_etag()
343343 def get(self, request, *args, **kwargs):
344 return super(View, self).get(request, *args, **kwargs)
344 return super().get(request, *args, **kwargs)
345345
346346 def test_should_raise_assertion_error_if_precondition_map_not_a_dict(self):
347347 with self.assertRaises(AssertionError):
352352 class View(views.APIView):
353353 @api_etag(dummy_api_etag_func, precondition_map=['header-name'])
354354 def get(self, request, *args, **kwargs):
355 return super(View, self).get(request, *args, **kwargs)
355 return super().get(request, *args, **kwargs)
356356
357357 def test_should_add_object_etag_value(self):
358358 class TestView(views.APIView):
607607
608608 class APIETAGProcessorTestBehavior_if_match(APIETAGProcessorTestBehaviorMixin, TestCase):
609609 def setUp(self):
610 super(APIETAGProcessorTestBehavior_if_match, self).setUp()
610 super().setUp()
611611 self.header_name = 'if-match'
612612 self.experiments = [
613613 {
661661
662662 class APIETAGProcessorTestBehavior_if_none_match(APIETAGProcessorTestBehaviorMixin, TestCase):
663663 def setUp(self):
664 super(APIETAGProcessorTestBehavior_if_none_match, self).setUp()
664 super().setUp()
665665 self.header_name = 'if-none-match'
666666 self.experiments = [
667667 {
1313
1414 class CacheResponseTest(TestCase):
1515 def setUp(self):
16 super(CacheResponseTest, self).setUp()
16 super().setUp()
1717 self.request = factory.get('')
1818 self.cache = caches[extensions_api_settings.DEFAULT_USE_CACHE]
1919
3737
3838 view_instance = TestView()
3939 response = view_instance.dispatch(request=self.request)
40 self.assertEqual(self.cache.get('cache_response_key')[0], response.content)
40 self.assertEqual(self.cache.get('cache_response_key')
41 [0], response.content)
4142 self.assertEqual(type(response), Response)
4243
4344 def test_should_store_response_in_cache_by_key_function_which_specified_in_arguments(self):
5152
5253 view_instance = TestView()
5354 response = view_instance.dispatch(request=self.request)
54 self.assertEqual(self.cache.get('cache_response_key_from_func')[0], response.content)
55 self.assertEqual(self.cache.get(
56 'cache_response_key_from_func')[0], response.content)
5557 self.assertEqual(type(response), Response)
5658
5759 def test_should_store_response_in_cache_by_key_which_calculated_by_view_method__if__key_func__is_string(self):
6567
6668 view_instance = TestView()
6769 response = view_instance.dispatch(request=self.request)
68 self.assertEqual(self.cache.get('cache_response_key_from_method')[0], response.content)
70 self.assertEqual(self.cache.get(
71 'cache_response_key_from_method')[0], response.content)
6972 self.assertEqual(type(response), Response)
7073
7174 def test_key_func_call_arguments(self):
8285
8386 view_instance = TestView()
8487 response = view_instance.dispatch(self.request, 'hello', hello='world')
85 self.assertEqual(called_with_kwargs.get('view_instance'), view_instance)
88 self.assertEqual(called_with_kwargs.get(
89 'view_instance'), view_instance)
8690 # self.assertEqual(called_with_kwargs.get('view_method'), view_instance.get) # todo: test me
8791 self.assertEqual(called_with_kwargs.get('args'), ('hello',))
8892 self.assertEqual(called_with_kwargs.get('kwargs'), {'hello': 'world'})
126130 cache_response_decorator.cache.set.call_args_list[0][0][2], 3)
127131
128132 def test_should_store_response_in_cache_with_timeout_from_object_cache_timeout_property(self):
129 cache_response_decorator = cache_response(timeout='object_cache_timeout')
133 cache_response_decorator = cache_response(
134 timeout='object_cache_timeout')
130135 cache_response_decorator.cache.set = Mock()
131136
132137 class TestView(views.APIView):
141146 self.assertTrue(
142147 cache_response_decorator.cache.set.called,
143148 'Cache saving should be performed')
144 self.assertEqual(cache_response_decorator.cache.set.call_args_list[0][0][2], 20)
149 self.assertEqual(
150 cache_response_decorator.cache.set.call_args_list[0][0][2], 20)
145151
146152 def test_should_store_response_in_cache_with_timeout_from_list_cache_timeout_property(self):
147153 cache_response_decorator = cache_response(timeout='list_cache_timeout')
159165 self.assertTrue(
160166 cache_response_decorator.cache.set.called,
161167 'Cache saving should be performed')
162 self.assertEqual(cache_response_decorator.cache.set.call_args_list[0][0][2], 10)
168 self.assertEqual(
169 cache_response_decorator.cache.set.call_args_list[0][0][2], 10)
163170
164171 def test_should_return_response_from_cache_if_it_is_in_it(self):
165172 def key_func(**kwargs):
172179
173180 view_instance = TestView()
174181 view_instance.headers = {}
175 cached_response = Response(u'Cached response from method 4')
176 view_instance.finalize_response(request=self.request, response=cached_response)
182 cached_response = Response('Cached response from method 4')
183 view_instance.finalize_response(
184 request=self.request, response=cached_response)
177185 cached_response.render()
178186 response_dict = (
179187 cached_response.rendered_content,
185193 response = view_instance.dispatch(request=self.request)
186194 self.assertEqual(
187195 response.content.decode('utf-8'),
188 u'"Cached response from method 4"')
196 '"Cached response from method 4"')
189197
190198 @override_extensions_api_settings(
191199 DEFAULT_USE_CACHE='special_cache'
221229
222230 view_instance = TestView()
223231 view_instance.dispatch(request=self.request)
224 data_from_cache = caches['another_special_cache'].get('cache_response_key')
232 data_from_cache = caches['another_special_cache'].get(
233 'cache_response_key')
225234 self.assertEqual(len(data_from_cache), 3)
226 self.assertEqual(data_from_cache[0].decode('utf-8'), u'"Response from method 6"')
235 self.assertEqual(data_from_cache[0].decode(
236 'utf-8'), u'"Response from method 6"')
227237
228238 def test_should_reuse_cache_singleton(self):
229239 """
232242 """
233243 cache_response_instance = cache_response()
234244 another_cache_response_instance = cache_response()
235 self.assertTrue(cache_response_instance.cache is another_cache_response_instance.cache)
245 self.assertTrue(
246 cache_response_instance.cache is another_cache_response_instance.cache)
236247
237248 def test_dont_cache_response_with_error_if_cache_error_false(self):
238249 cache_response_decorator = cache_response(cache_errors=False)
241252
242253 def __init__(self, status, *args, **kwargs):
243254 self.status = status
244 super(TestView, self).__init__(*args, **kwargs)
255 super().__init__(*args, **kwargs)
245256
246257 @cache_response_decorator
247258 def get(self, request, *args, **kwargs):
261272
262273 def __init__(self, status, *args, **kwargs):
263274 self.status = status
264 super(TestView, self).__init__(*args, **kwargs)
275 super().__init__(*args, **kwargs)
265276
266277 @cache_response_decorator
267278 def get(self, request, *args, **kwargs):
285296 )
286297 def test_should_use_cache_error_from_decorator_if_it_is_specified(self):
287298 self.assertTrue(cache_response(cache_errors=True).cache_errors)
299
300 def test_should_return_response_with_tuple_headers(self):
301 def key_func(**kwargs):
302 return 'cache_response_key'
303
304 class TestView(views.APIView):
305 @cache_response(key_func=key_func)
306 def get(self, request, *args, **kwargs):
307 return Response(u'')
308
309 view_instance = TestView()
310 view_instance.headers = {'Test': 'foo'}
311 cached_response = Response(u'')
312 view_instance.finalize_response(
313 request=self.request, response=cached_response)
314 cached_response.render()
315 response_dict = (
316 cached_response.rendered_content,
317 cached_response.status_code,
318 {k: list(v) for k, v in cached_response._headers.items()}
319 )
320 self.cache.set('cache_response_key', response_dict)
321
322 response = view_instance.dispatch(request=self.request)
323 self.assertTrue(all(isinstance(v, tuple)
324 for v in response._headers.values()))
325 self.assertEqual(response._headers['test'], ('Test', 'foo'))
0 from django.test import TestCase
1 from rest_framework import pagination, viewsets
2 from rest_framework_extensions.decorators import paginate
3
4
5 class TestPaginateDecorator(TestCase):
6
7 def test_empty_pagination_class(self):
8 msg = "@paginate missing required argument: 'pagination_class'"
9 with self.assertRaisesMessage(AssertionError, msg):
10 @paginate()
11 class MockGenericViewSet(viewsets.GenericViewSet):
12 pass
13
14 def test_adding_page_number_pagination(self):
15 """
16 Other default pagination classes' test result will be same as this even if kwargs changed to anything.
17 """
18
19 @paginate(pagination_class=pagination.PageNumberPagination, page_size=5, ordering='-created_at')
20 class MockGenericViewSet(viewsets.GenericViewSet):
21 pass
22
23 assert hasattr(MockGenericViewSet, 'pagination_class')
24 assert MockGenericViewSet.pagination_class().page_size == 5
25 assert MockGenericViewSet.pagination_class().ordering == '-created_at'
26
27 def test_adding_custom_pagination(self):
28 class CustomPagination(pagination.BasePagination):
29 pass
30
31 @paginate(pagination_class=CustomPagination, kwarg1='kwarg1', kwarg2='kwarg2')
32 class MockGenericViewSet(viewsets.GenericViewSet):
33 pass
34
35 assert hasattr(MockGenericViewSet, 'pagination_class')
36 assert MockGenericViewSet.pagination_class().kwarg1 == 'kwarg1'
37 assert MockGenericViewSet.pagination_class().kwarg2 == 'kwarg2'
339339 'params': None,
340340 'view_instance': Mock(spec_set=['paginator']),
341341 'view_method': None,
342 'request': factory.get('?page_size=10&page=1'),
342 'request': factory.get('?page_size=10&page=1&limit=5&offset=15&cursor=foo'),
343343 'args': None,
344344 'kwargs': None
345345 }
356356 def test_view_with_page_kwarg(self):
357357 self.kwargs['view_instance'].paginator.page_query_param = 'page'
358358 self.kwargs['view_instance'].paginator.page_size_query_param = None
359 self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {'page': u'1'})
359 self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {'page': '1'})
360360
361361 def test_view_with_paginate_by_param(self):
362362 self.kwargs['view_instance'].paginator.page_query_param = None
363363 self.kwargs['view_instance'].paginator.page_size_query_param = 'page_size'
364 self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {'page_size': u'10'})
364 self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {'page_size': '10'})
365365
366366 def test_view_with_all_pagination_attrs(self):
367367 self.kwargs['view_instance'].paginator.page_query_param = 'page'
368368 self.kwargs['view_instance'].paginator.page_size_query_param = 'page_size'
369 self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {'page_size': u'10', 'page': u'1'})
369 self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {'page_size': '10', 'page': '1'})
370370
371371 def test_view_with_all_pagination_attrs__without_query_params(self):
372372 self.kwargs['view_instance'].paginator.page_query_param = 'page'
373373 self.kwargs['view_instance'].paginator.page_size_query_param = 'page_size'
374374 self.kwargs['request'] = factory.get('')
375375 self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {})
376
377 def test_view_with_offset_pagination_attrs(self):
378 self.kwargs['view_instance'].paginator.limit_query_param = 'limit'
379 self.kwargs['view_instance'].paginator.offset_query_param = 'offset'
380 self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {'limit': '5', 'offset': '15'})
381
382 def test_view_with_cursor_pagination_attrs(self):
383 self.kwargs['view_instance'].paginator.cursor_query_param = 'cursor'
384 self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {'cursor': 'foo'})
376385
377386
378387 class ListSqlQueryKeyBitTest(TestCase):
0 # -*- coding: utf-8 -*-
10 from copy import deepcopy
21 import hashlib
32 import json
00 [tox]
1 envlist = py{35,36,37}-django{111}-drf39,
2 py{35,36,37}-django{20,21,22}-drf39
1 envlist = py{36,37}-django{111}-drf39,
2 py{36,37}-django{21}-drf{39,310,311}
3 py{36,37,38}-django{22}-drf{39,310,311}
4 py{36,37,38}-django{30}-drf{310,311}
5
36
47 [testenv]
58 deps=
710 django-guardian>=1.4.4
811 drf39: djangorestframework>=3.9.3,<3.10
912 djangorestframework-guardian
13 drf310: djangorestframework>=3.10,<3.11
14 djangorestframework-guardian
15 drf311: djangorestframework>=3.11,<3.12
16 djangorestframework-guardian
1017 django111: Django>=1.11,<2.0
11 django20: Django>=2.0,<2.1
1218 django21: Django>=2.1,<2.2
1319 django22: Django>=2.2,<3.0
20 django30: Django>=3.0,<3.1
1421 setenv =
1522 PYTHONPATH = {toxinidir}:{toxinidir}/tests_app
1623 commands =