1
|
|
# Python
|
2
|
3
|
import inspect
|
3
|
3
|
import logging
|
4
|
3
|
import time
|
5
|
3
|
import traceback
|
6
|
|
|
7
|
|
# Django
|
8
|
3
|
from django.conf import settings
|
9
|
3
|
from django.db import connection
|
10
|
3
|
from django.db.models.fields import FieldDoesNotExist
|
11
|
3
|
from django.db.models.fields.related import OneToOneRel
|
12
|
3
|
from django.http import QueryDict
|
13
|
3
|
from django.shortcuts import get_object_or_404
|
14
|
3
|
from django.template.loader import render_to_string
|
15
|
3
|
from django.utils.encoding import smart_text
|
16
|
3
|
from django.utils.safestring import mark_safe
|
17
|
3
|
from django.utils.translation import ugettext_lazy as _
|
18
|
3
|
from django.contrib.auth import views as auth_views
|
19
|
|
|
20
|
|
# Django REST Framework
|
21
|
3
|
from rest_framework.authentication import get_authorization_header
|
22
|
3
|
from rest_framework.exceptions import PermissionDenied, AuthenticationFailed
|
23
|
3
|
from rest_framework import generics
|
24
|
3
|
from rest_framework.response import Response
|
25
|
3
|
from rest_framework import status
|
26
|
3
|
from rest_framework import views
|
27
|
|
|
28
|
|
# CyBorgBackup
|
29
|
3
|
from cyborgbackup.api.filters import FieldLookupBackend
|
30
|
3
|
from cyborgbackup.main.utils.common import (get_object_or_400, camelcase_to_underscore,
|
31
|
|
getattrd, get_all_field_names, get_search_fields)
|
32
|
3
|
from cyborgbackup.api.versioning import URLPathVersioning, get_request_version
|
33
|
3
|
from cyborgbackup.api.metadata import SublistAttachDetatchMetadata
|
34
|
3
|
from cyborgbackup.api.mixins import LoggingViewSetMixin
|
35
|
|
|
36
|
3
|
__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
|
37
|
|
'ListCreateAPIView', 'SubListAPIView', 'SubListCreateAPIView',
|
38
|
|
'SubListDestroyAPIView',
|
39
|
|
'SubListCreateAttachDetachAPIView', 'RetrieveAPIView',
|
40
|
|
'RetrieveUpdateAPIView', 'RetrieveDestroyAPIView',
|
41
|
|
'RetrieveUpdateDestroyAPIView',
|
42
|
|
'SubDetailAPIView',
|
43
|
|
'ParentMixin', 'SubListLoopAPIView',
|
44
|
|
'DeleteLastUnattachLabelMixin',
|
45
|
|
'SubListAttachDetachAPIView', 'get_view_description', 'get_view_name']
|
46
|
|
|
47
|
3
|
logger = logging.getLogger('cyborgbackup.api.generics')
|
48
|
3
|
analytics_logger = logging.getLogger('cyborgbackup.analytics.performance')
|
49
|
|
|
50
|
|
|
51
|
3
|
class LoggedLoginView(auth_views.LoginView):
|
52
|
|
|
53
|
3
|
def post(self, request, *args, **kwargs):
|
54
|
0
|
original_user = getattr(request, 'user', None)
|
55
|
0
|
ret = super(LoggedLoginView, self).post(request, *args, **kwargs)
|
56
|
0
|
current_user = getattr(request, 'user', None)
|
57
|
0
|
if current_user and getattr(current_user, 'pk', None) and current_user != original_user:
|
58
|
0
|
logger.info("User {} logged in.".format(current_user.email))
|
59
|
0
|
if request.user.is_authenticated:
|
60
|
0
|
return ret
|
61
|
|
else:
|
62
|
0
|
ret.status_code = 401
|
63
|
0
|
return ret
|
64
|
|
|
65
|
|
|
66
|
3
|
class LoggedLogoutView(auth_views.LogoutView):
|
67
|
|
|
68
|
3
|
def dispatch(self, request, *args, **kwargs):
|
69
|
3
|
original_user = getattr(request, 'user', None)
|
70
|
3
|
ret = super(LoggedLogoutView, self).dispatch(request, *args, **kwargs)
|
71
|
3
|
current_user = getattr(request, 'user', None)
|
72
|
3
|
if (not current_user or not getattr(current_user, 'pk', True)) \
|
73
|
|
and current_user != original_user:
|
74
|
0
|
logger.info("User {} logged out.".format(original_user.email))
|
75
|
3
|
return ret
|
76
|
|
|
77
|
|
|
78
|
3
|
def get_view_name(cls, suffix=None):
|
79
|
|
'''
|
80
|
|
Wrapper around REST framework get_view_name() to support get_name() method
|
81
|
|
and view_name property on a view class.
|
82
|
|
'''
|
83
|
0
|
name = ''
|
84
|
0
|
if hasattr(cls, 'get_name') and callable(cls.get_name):
|
85
|
0
|
name = cls().get_name()
|
86
|
0
|
elif hasattr(cls, 'view_name'):
|
87
|
0
|
if callable(cls.view_name):
|
88
|
0
|
name = cls.view_name()
|
89
|
|
else:
|
90
|
0
|
name = cls.view_name
|
91
|
0
|
if name:
|
92
|
0
|
return ('%s %s' % (name, suffix)) if suffix else name
|
93
|
0
|
return views.get_view_name(cls)
|
94
|
|
|
95
|
|
|
96
|
3
|
def get_view_description(cls, request, html=False):
|
97
|
|
'''
|
98
|
|
Wrapper around REST framework get_view_description() to support
|
99
|
|
get_description() method and view_description property on a view class.
|
100
|
|
'''
|
101
|
3
|
if hasattr(cls, 'get_description') and callable(cls.get_description):
|
102
|
3
|
desc = cls().get_description(request, html=html)
|
103
|
3
|
cls = type(cls.__name__, (object,), {'__doc__': desc})
|
104
|
3
|
elif hasattr(cls, 'view_description'):
|
105
|
0
|
if callable(cls.view_description):
|
106
|
0
|
view_desc = cls.view_description()
|
107
|
|
else:
|
108
|
0
|
view_desc = cls.view_description
|
109
|
0
|
cls = type(cls.__name__, (object,), {'__doc__': view_desc})
|
110
|
3
|
desc = views.get_view_description(cls, html=html)
|
111
|
3
|
if html:
|
112
|
0
|
desc = '<div class="description">%s</div>' % desc
|
113
|
3
|
return mark_safe(desc)
|
114
|
|
|
115
|
|
|
116
|
3
|
def get_default_schema():
|
117
|
3
|
from cyborgbackup.api.swagger import AutoSchema
|
118
|
3
|
return AutoSchema()
|
119
|
|
|
120
|
|
|
121
|
3
|
class APIView(views.APIView):
|
122
|
|
|
123
|
3
|
schema = get_default_schema()
|
124
|
3
|
versioning_class = URLPathVersioning
|
125
|
|
|
126
|
3
|
def initialize_request(self, request, *args, **kwargs):
|
127
|
|
'''
|
128
|
|
Store the Django REST Framework Request object as an attribute on the
|
129
|
|
normal Django request, store time the request started.
|
130
|
|
'''
|
131
|
3
|
self.time_started = time.time()
|
132
|
3
|
if getattr(settings, 'SQL_DEBUG', True):
|
133
|
0
|
self.queries_before = len(connection.queries)
|
134
|
|
|
135
|
3
|
for custom_header in ['REMOTE_ADDR', 'REMOTE_HOST']:
|
136
|
3
|
if custom_header.startswith('HTTP_'):
|
137
|
0
|
request.environ.pop(custom_header, None)
|
138
|
|
|
139
|
3
|
drf_request = super(APIView, self).initialize_request(request, *args, **kwargs)
|
140
|
3
|
request.drf_request = drf_request
|
141
|
3
|
try:
|
142
|
3
|
request.drf_request_user = getattr(drf_request, 'user', False)
|
143
|
0
|
except AuthenticationFailed:
|
144
|
0
|
request.drf_request_user = None
|
145
|
3
|
return drf_request
|
146
|
|
|
147
|
3
|
def finalize_response(self, request, response, *args, **kwargs):
|
148
|
|
'''
|
149
|
|
Log warning for 400 requests. Add header with elapsed time.
|
150
|
|
'''
|
151
|
3
|
if response.status_code >= 400:
|
152
|
3
|
status_msg = "status %s received by user %s attempting to access %s from %s" % \
|
153
|
|
(response.status_code, request.user, request.path, request.META.get('REMOTE_ADDR', None))
|
154
|
3
|
if response.status_code == 401:
|
155
|
3
|
logger.info(status_msg)
|
156
|
|
else:
|
157
|
0
|
logger.warn(status_msg)
|
158
|
3
|
response = super(APIView, self).finalize_response(request, response, *args, **kwargs)
|
159
|
3
|
time_started = getattr(self, 'time_started', None)
|
160
|
3
|
if time_started:
|
161
|
3
|
time_elapsed = time.time() - self.time_started
|
162
|
3
|
response['X-API-Time'] = '%0.3fs' % time_elapsed
|
163
|
3
|
if getattr(settings, 'SQL_DEBUG', False):
|
164
|
0
|
queries_before = getattr(self, 'queries_before', 0)
|
165
|
0
|
for q in connection.queries:
|
166
|
0
|
logger.debug(q)
|
167
|
0
|
q_times = [float(q['time']) for q in connection.queries[queries_before:]]
|
168
|
0
|
response['X-API-Query-Count'] = len(q_times)
|
169
|
0
|
response['X-API-Query-Time'] = '%0.3fs' % sum(q_times)
|
170
|
|
|
171
|
3
|
return response
|
172
|
|
|
173
|
3
|
def get_authenticate_header(self, request):
|
174
|
|
"""
|
175
|
|
Determine the WWW-Authenticate header to use for 401 responses. Try to
|
176
|
|
use the request header as an indication for which authentication method
|
177
|
|
was attempted.
|
178
|
|
"""
|
179
|
3
|
for authenticator in self.get_authenticators():
|
180
|
3
|
resp_hdr = authenticator.authenticate_header(request)
|
181
|
3
|
if not resp_hdr:
|
182
|
0
|
continue
|
183
|
3
|
req_hdr = get_authorization_header(request)
|
184
|
3
|
if not req_hdr:
|
185
|
3
|
continue
|
186
|
0
|
if resp_hdr.split()[0] and resp_hdr.split()[0] == req_hdr.split()[0]:
|
187
|
0
|
return resp_hdr
|
188
|
|
# If it can't be determined from the request, use the last
|
189
|
|
# authenticator (should be Basic).
|
190
|
3
|
try:
|
191
|
3
|
return authenticator.authenticate_header(request)
|
192
|
0
|
except NameError:
|
193
|
0
|
pass
|
194
|
|
|
195
|
3
|
def get_view_description(self, html=False):
|
196
|
|
"""
|
197
|
|
Return some descriptive text for the view, as used in OPTIONS responses
|
198
|
|
and in the browsable API.
|
199
|
|
"""
|
200
|
3
|
func = self.settings.VIEW_DESCRIPTION_FUNCTION
|
201
|
3
|
return func(self.__class__, getattr(self, '_request', None), html)
|
202
|
|
|
203
|
3
|
def get_description_context(self):
|
204
|
3
|
return {
|
205
|
|
'view': self,
|
206
|
|
'docstring': type(self).__doc__ or '',
|
207
|
|
'deprecated': getattr(self, 'deprecated', False),
|
208
|
|
'swagger_method': getattr(self.request, 'swagger_method', None),
|
209
|
|
}
|
210
|
|
|
211
|
3
|
def get_description(self, request, html=False):
|
212
|
3
|
self.request = request
|
213
|
3
|
template_list = []
|
214
|
3
|
for klass in inspect.getmro(type(self)):
|
215
|
3
|
template_basename = camelcase_to_underscore(klass.__name__)
|
216
|
3
|
template_list.append('api/%s.md' % template_basename)
|
217
|
3
|
context = self.get_description_context()
|
218
|
|
|
219
|
3
|
default_version = int(1)
|
220
|
3
|
request_version = get_request_version(self.request)
|
221
|
3
|
if request_version is not None and request_version < default_version:
|
222
|
0
|
context['deprecated'] = True
|
223
|
|
|
224
|
3
|
description = render_to_string(template_list, context)
|
225
|
3
|
if context.get('deprecated') and context.get('swagger_method') is None:
|
226
|
|
# render deprecation messages at the very top
|
227
|
0
|
description = '\n'.join([render_to_string('api/_deprecated.md', context), description])
|
228
|
3
|
return description
|
229
|
|
|
230
|
3
|
def update_raw_data(self, data):
|
231
|
|
# Remove the parent key if the view is a sublist, since it will be set
|
232
|
|
# automatically.
|
233
|
0
|
parent_key = getattr(self, 'parent_key', None)
|
234
|
0
|
if parent_key:
|
235
|
0
|
data.pop(parent_key, None)
|
236
|
|
|
237
|
|
# Use request data as-is when original request is an update and the
|
238
|
|
# submitted data was rejected.
|
239
|
0
|
request_method = getattr(self, '_raw_data_request_method', None)
|
240
|
0
|
response_status = getattr(self, '_raw_data_response_status', 0)
|
241
|
0
|
if request_method in ('POST', 'PUT', 'PATCH') and response_status in range(400, 500):
|
242
|
0
|
return self.request.data.copy()
|
243
|
|
|
244
|
0
|
return data
|
245
|
|
|
246
|
3
|
def determine_version(self, request, *args, **kwargs):
|
247
|
3
|
return (
|
248
|
|
getattr(request, 'version', None),
|
249
|
|
getattr(request, 'versioning_scheme', None),
|
250
|
|
)
|
251
|
|
|
252
|
3
|
def dispatch(self, request, *args, **kwargs):
|
253
|
3
|
if self.versioning_class is not None:
|
254
|
3
|
scheme = self.versioning_class()
|
255
|
3
|
request.version, request.versioning_scheme = (
|
256
|
|
scheme.determine_version(request, *args, **kwargs),
|
257
|
|
scheme
|
258
|
|
)
|
259
|
3
|
if 'version' in kwargs:
|
260
|
3
|
kwargs.pop('version')
|
261
|
3
|
return super(APIView, self).dispatch(request, *args, **kwargs)
|
262
|
|
|
263
|
|
|
264
|
3
|
class GenericAPIView(LoggingViewSetMixin, generics.GenericAPIView, APIView):
|
265
|
|
# Base class for all model-based views.
|
266
|
|
|
267
|
|
# Subclasses should define:
|
268
|
|
# model = ModelClass
|
269
|
|
# serializer_class = SerializerClass
|
270
|
|
|
271
|
3
|
def get_serializer(self, *args, **kwargs):
|
272
|
|
|
273
|
3
|
serializer_class = self.get_serializer_class()
|
274
|
|
|
275
|
3
|
fields = None
|
276
|
3
|
if self.request.method == 'GET':
|
277
|
3
|
query_fields = self.request.query_params.get("fields", None)
|
278
|
|
|
279
|
3
|
if query_fields:
|
280
|
0
|
fields = tuple(query_fields.split(','))
|
281
|
|
|
282
|
3
|
kwargs['context'] = self.get_serializer_context()
|
283
|
3
|
if fields:
|
284
|
0
|
kwargs['fields'] = fields
|
285
|
|
|
286
|
3
|
serializer = serializer_class(*args, **kwargs)
|
287
|
|
# Override when called from browsable API to generate raw data form;
|
288
|
|
# update serializer "validated" data to be displayed by the raw data
|
289
|
|
# form.
|
290
|
3
|
if hasattr(self, '_raw_data_form_marker'):
|
291
|
|
# Always remove read only fields from serializer.
|
292
|
0
|
for name, field in list(serializer.fields.items()):
|
293
|
0
|
if getattr(field, 'read_only', None):
|
294
|
0
|
del serializer.fields[name]
|
295
|
0
|
serializer._data = self.update_raw_data(serializer.data)
|
296
|
3
|
return serializer
|
297
|
|
|
298
|
3
|
def get_queryset(self):
|
299
|
3
|
if self.queryset is not None:
|
300
|
0
|
return self.queryset._clone()
|
301
|
3
|
elif self.model is not None:
|
302
|
3
|
qs = self.model._default_manager.all()
|
303
|
3
|
return qs
|
304
|
|
else:
|
305
|
0
|
return super(GenericAPIView, self).get_queryset()
|
306
|
|
|
307
|
3
|
def get_description_context(self):
|
308
|
|
# Set instance attributes needed to get serializer metadata.
|
309
|
0
|
if not hasattr(self, 'request'):
|
310
|
0
|
self.request = None
|
311
|
0
|
if not hasattr(self, 'format_kwarg'):
|
312
|
0
|
self.format_kwarg = 'format'
|
313
|
0
|
d = super(GenericAPIView, self).get_description_context()
|
314
|
0
|
if hasattr(self.model, "_meta"):
|
315
|
0
|
if hasattr(self.model._meta, "verbose_name"):
|
316
|
0
|
d.update({
|
317
|
|
'model_verbose_name': smart_text(self.model._meta.verbose_name),
|
318
|
|
'model_verbose_name_plural': smart_text(self.model._meta.verbose_name_plural),
|
319
|
|
})
|
320
|
0
|
serializer = self.get_serializer()
|
321
|
0
|
for method, key in [
|
322
|
|
('GET', 'serializer_fields'),
|
323
|
|
('POST', 'serializer_create_fields'),
|
324
|
|
('PUT', 'serializer_update_fields')
|
325
|
|
]:
|
326
|
0
|
d[key] = self.metadata_class().get_serializer_info(serializer, method=method)
|
327
|
0
|
d['settings'] = settings
|
328
|
0
|
return d
|
329
|
|
|
330
|
|
|
331
|
3
|
class SimpleListAPIView(generics.ListAPIView, GenericAPIView):
|
332
|
|
|
333
|
3
|
def get_queryset(self):
|
334
|
0
|
return self.request.user.get_queryset(self.model)
|
335
|
|
|
336
|
|
|
337
|
3
|
class ListAPIView(generics.ListAPIView, GenericAPIView):
|
338
|
|
# Base class for a read-only list view.
|
339
|
|
|
340
|
3
|
def get_description_context(self):
|
341
|
0
|
if 'username' in get_all_field_names(self.model):
|
342
|
0
|
order_field = 'username'
|
343
|
|
else:
|
344
|
0
|
order_field = 'name'
|
345
|
0
|
d = super(ListAPIView, self).get_description_context()
|
346
|
0
|
d.update({
|
347
|
|
'order_field': order_field,
|
348
|
|
})
|
349
|
0
|
return d
|
350
|
|
|
351
|
3
|
@property
|
352
|
|
def search_fields(self):
|
353
|
3
|
return get_search_fields(self.model)
|
354
|
|
|
355
|
3
|
def get_queryset(self):
|
356
|
3
|
queryset = self.model.objects.all()
|
357
|
3
|
fields = self.request.query_params.get('fields', None)
|
358
|
3
|
if fields:
|
359
|
0
|
list_field = fields.split(',')
|
360
|
0
|
queryset = queryset.values(*list_field).distinct()
|
361
|
3
|
order = self.request.query_params.get('order', '-id')
|
362
|
3
|
if order and order in ('-id', 'foo', 'bar'):
|
363
|
3
|
queryset = queryset.order_by(order)
|
364
|
3
|
return queryset
|
365
|
|
|
366
|
3
|
@property
|
367
|
|
def related_search_fields(self):
|
368
|
0
|
def skip_related_name(name):
|
369
|
0
|
return (
|
370
|
|
name is None or name.endswith('_role') or name.startswith('_') or
|
371
|
|
name.startswith('deprecated_') or name.endswith('_set') or
|
372
|
|
name == 'polymorphic_ctype')
|
373
|
|
|
374
|
0
|
fields = set([])
|
375
|
0
|
for field in self.model._meta.fields:
|
376
|
0
|
if skip_related_name(field.name):
|
377
|
0
|
continue
|
378
|
0
|
if getattr(field, 'related_model', None):
|
379
|
0
|
fields.add('{}__search'.format(field.name))
|
380
|
0
|
for rel in self.model._meta.related_objects:
|
381
|
0
|
name = rel.related_name
|
382
|
0
|
if isinstance(rel, OneToOneRel) and self.model._meta.verbose_name.startswith('unified'):
|
383
|
|
# Add underscores for polymorphic subclasses for user utility
|
384
|
0
|
name = rel.related_model._meta.verbose_name.replace(" ", "_")
|
385
|
0
|
if skip_related_name(name) or name.endswith('+'):
|
386
|
0
|
continue
|
387
|
0
|
fields.add('{}__search'.format(name))
|
388
|
0
|
m2m_rel = []
|
389
|
0
|
m2m_rel += self.model._meta.local_many_to_many
|
390
|
0
|
for relationship in m2m_rel:
|
391
|
0
|
if skip_related_name(relationship.name):
|
392
|
0
|
continue
|
393
|
0
|
if relationship.related_model._meta.app_label != 'main':
|
394
|
0
|
continue
|
395
|
0
|
fields.add('{}__search'.format(relationship.name))
|
396
|
0
|
fields = list(fields)
|
397
|
|
|
398
|
0
|
allowed_fields = []
|
399
|
0
|
for field in fields:
|
400
|
0
|
try:
|
401
|
0
|
FieldLookupBackend().get_field_from_lookup(self.model, field)
|
402
|
0
|
except PermissionDenied:
|
403
|
0
|
pass
|
404
|
0
|
except FieldDoesNotExist:
|
405
|
0
|
allowed_fields.append(field)
|
406
|
|
else:
|
407
|
0
|
allowed_fields.append(field)
|
408
|
0
|
return allowed_fields
|
409
|
|
|
410
|
|
|
411
|
3
|
class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView):
|
412
|
|
# Base class for a list view that allows creating new objects.
|
413
|
3
|
pass
|
414
|
|
|
415
|
|
|
416
|
3
|
class ParentMixin(object):
|
417
|
3
|
parent_object = None
|
418
|
|
|
419
|
3
|
def get_parent_object(self):
|
420
|
0
|
if self.parent_object is not None:
|
421
|
0
|
return self.parent_object
|
422
|
0
|
parent_filter = {
|
423
|
|
self.lookup_field: self.kwargs.get(self.lookup_field, None),
|
424
|
|
}
|
425
|
0
|
self.parent_object = get_object_or_404(self.parent_model, **parent_filter)
|
426
|
0
|
return self.parent_object
|
427
|
|
|
428
|
3
|
def check_parent_access(self, parent=None):
|
429
|
0
|
parent = parent or self.get_parent_object()
|
430
|
0
|
parent_access = getattr(self, 'parent_access', 'read')
|
431
|
0
|
if parent_access in ('read', 'delete'):
|
432
|
0
|
args = (self.parent_model, parent_access, parent)
|
433
|
|
else:
|
434
|
0
|
args = (self.parent_model, parent_access, parent, None)
|
435
|
0
|
return args
|
436
|
|
|
437
|
|
|
438
|
3
|
class SubListAPIView(ParentMixin, ListAPIView):
|
439
|
|
# Base class for a read-only sublist view.
|
440
|
|
|
441
|
|
# Subclasses should define at least:
|
442
|
|
# model = ModelClass
|
443
|
|
# serializer_class = SerializerClass
|
444
|
|
# parent_model = ModelClass
|
445
|
|
# relationship = 'rel_name_from_parent_to_model'
|
446
|
|
# And optionally (user must have given access permission on parent object
|
447
|
|
# to view sublist):
|
448
|
|
# parent_access = 'read'
|
449
|
|
|
450
|
3
|
def get_description_context(self):
|
451
|
0
|
d = super(SubListAPIView, self).get_description_context()
|
452
|
0
|
d.update({
|
453
|
|
'parent_model_verbose_name': smart_text(self.parent_model._meta.verbose_name),
|
454
|
|
'parent_model_verbose_name_plural': smart_text(self.parent_model._meta.verbose_name_plural),
|
455
|
|
})
|
456
|
0
|
return d
|
457
|
|
|
458
|
3
|
def get_queryset(self):
|
459
|
0
|
parent = self.get_parent_object()
|
460
|
0
|
sublist_qs = getattrd(parent, self.relationship).distinct()
|
461
|
0
|
return sublist_qs
|
462
|
|
|
463
|
|
|
464
|
3
|
class SubListLoopAPIView(ParentMixin, ListAPIView):
|
465
|
|
# Base class for a read-only sublist view.
|
466
|
|
|
467
|
|
# Subclasses should define at least:
|
468
|
|
# model = ModelClass
|
469
|
|
# serializer_class = SerializerClass
|
470
|
|
# parent_model = ModelClass
|
471
|
|
# relationship = 'rel_name_from_parent_to_model'
|
472
|
|
# And optionally (user must have given access permission on parent object
|
473
|
|
# to view sublist):
|
474
|
|
# parent_access = 'read'
|
475
|
|
|
476
|
3
|
def get_description_context(self):
|
477
|
0
|
d = super(SubListLoopAPIView, self).get_description_context()
|
478
|
0
|
d.update({
|
479
|
|
'parent_model_verbose_name': smart_text(self.parent_model._meta.verbose_name),
|
480
|
|
'parent_model_verbose_name_plural': smart_text(self.parent_model._meta.verbose_name_plural),
|
481
|
|
})
|
482
|
0
|
return d
|
483
|
|
|
484
|
3
|
def get_queryset(self):
|
485
|
0
|
parent = self.get_parent_object()
|
486
|
0
|
sublist_qs = getattrd(parent, self.relationship).distinct()
|
487
|
0
|
return sublist_qs
|
488
|
|
|
489
|
|
|
490
|
3
|
class SubListDestroyAPIView(generics.DestroyAPIView, SubListAPIView):
|
491
|
|
"""
|
492
|
|
Concrete view for deleting everything related by `relationship`.
|
493
|
|
"""
|
494
|
3
|
check_sub_obj_permission = True
|
495
|
|
|
496
|
3
|
def destroy(self, request, *args, **kwargs):
|
497
|
0
|
instance_list = self.get_queryset()
|
498
|
0
|
if (not self.check_sub_obj_permission and
|
499
|
|
not request.user.can_access(self.parent_model, 'delete', self.get_parent_object())):
|
500
|
0
|
raise PermissionDenied()
|
501
|
0
|
self.perform_list_destroy(instance_list)
|
502
|
0
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
503
|
|
|
504
|
3
|
def perform_list_destroy(self, instance_list):
|
505
|
0
|
if self.check_sub_obj_permission:
|
506
|
|
# Check permissions for all before deleting, avoiding half-deleted lists
|
507
|
0
|
for instance in instance_list:
|
508
|
0
|
if self.has_delete_permission(instance):
|
509
|
0
|
raise PermissionDenied()
|
510
|
0
|
for instance in instance_list:
|
511
|
0
|
self.perform_destroy(instance, check_permission=False)
|
512
|
|
|
513
|
|
|
514
|
3
|
class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
515
|
|
# Base class for a sublist view that allows for creating subobjects
|
516
|
|
# associated with the parent object.
|
517
|
|
|
518
|
|
# In addition to SubListAPIView properties, subclasses may define (if the
|
519
|
|
# sub_obj requires a foreign key to the parent):
|
520
|
|
# parent_key = 'field_on_model_referring_to_parent'
|
521
|
|
|
522
|
3
|
def get_description_context(self):
|
523
|
0
|
d = super(SubListCreateAPIView, self).get_description_context()
|
524
|
0
|
d.update({
|
525
|
|
'parent_key': getattr(self, 'parent_key', None),
|
526
|
|
})
|
527
|
0
|
return d
|
528
|
|
|
529
|
3
|
def create(self, request, *args, **kwargs):
|
530
|
|
# If the object ID was not specified, it probably doesn't exist in the
|
531
|
|
# DB yet. We want to see if we can create it. The URL may choose to
|
532
|
|
# inject it's primary key into the object because we are posting to a
|
533
|
|
# subcollection. Use all the normal access control mechanisms.
|
534
|
|
|
535
|
|
# Make a copy of the data provided (since it's readonly) in order to
|
536
|
|
# inject additional data.
|
537
|
0
|
if hasattr(request.data, 'copy'):
|
538
|
0
|
data = request.data.copy()
|
539
|
|
else:
|
540
|
0
|
data = QueryDict('')
|
541
|
0
|
data.update(request.data)
|
542
|
|
|
543
|
|
# add the parent key to the post data using the pk from the URL
|
544
|
0
|
parent_key = getattr(self, 'parent_key', None)
|
545
|
0
|
if parent_key:
|
546
|
0
|
data[parent_key] = self.kwargs['pk']
|
547
|
|
|
548
|
|
# attempt to deserialize the object
|
549
|
0
|
serializer = self.get_serializer(data=data)
|
550
|
0
|
if not serializer.is_valid():
|
551
|
0
|
return Response(serializer.errors,
|
552
|
|
status=status.HTTP_400_BAD_REQUEST)
|
553
|
|
|
554
|
|
# Verify we have permission to add the object as given.
|
555
|
0
|
if not request.user.can_access(self.model, 'add', serializer.initial_data):
|
556
|
0
|
raise PermissionDenied()
|
557
|
|
|
558
|
|
# save the object through the serializer, reload and returned the saved
|
559
|
|
# object deserialized
|
560
|
0
|
obj = serializer.save()
|
561
|
0
|
serializer = self.get_serializer(instance=obj)
|
562
|
|
|
563
|
0
|
headers = {'Location': obj.get_absolute_url(request)}
|
564
|
0
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
565
|
|
|
566
|
|
|
567
|
3
|
class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
568
|
|
# Base class for a sublist view that allows for creating subobjects and
|
569
|
|
# attaching/detaching them from the parent.
|
570
|
|
|
571
|
3
|
def is_valid_relation(self, parent, sub, created=False):
|
572
|
0
|
return None
|
573
|
|
|
574
|
3
|
def get_description_context(self):
|
575
|
0
|
d = super(SubListCreateAttachDetachAPIView, self).get_description_context()
|
576
|
0
|
d.update({
|
577
|
|
"has_attach": True,
|
578
|
|
})
|
579
|
0
|
return d
|
580
|
|
|
581
|
3
|
def attach_validate(self, request):
|
582
|
0
|
sub_id = request.data.get('id', None)
|
583
|
0
|
res = None
|
584
|
0
|
if sub_id and not isinstance(sub_id, int):
|
585
|
0
|
data = dict(msg=_('"id" field must be an integer.'))
|
586
|
0
|
res = Response(data, status=status.HTTP_400_BAD_REQUEST)
|
587
|
0
|
return (sub_id, res)
|
588
|
|
|
589
|
3
|
def attach(self, request, *args, **kwargs):
|
590
|
0
|
created = False
|
591
|
0
|
parent = self.get_parent_object()
|
592
|
0
|
relationship = getattrd(parent, self.relationship)
|
593
|
0
|
data = request.data
|
594
|
|
|
595
|
0
|
sub_id, res = self.attach_validate(request)
|
596
|
0
|
if res:
|
597
|
0
|
return res
|
598
|
|
|
599
|
|
# Create the sub object if an ID is not provided.
|
600
|
0
|
if not sub_id:
|
601
|
0
|
response = self.create(request, *args, **kwargs)
|
602
|
0
|
if response.status_code != status.HTTP_201_CREATED:
|
603
|
0
|
return response
|
604
|
0
|
sub_id = response.data['id']
|
605
|
0
|
data = response.data
|
606
|
0
|
try:
|
607
|
0
|
location = response['Location']
|
608
|
0
|
except KeyError:
|
609
|
0
|
location = None
|
610
|
0
|
created = True
|
611
|
|
|
612
|
|
# Retrive the sub object (whether created or by ID).
|
613
|
0
|
sub = get_object_or_400(self.model, pk=sub_id)
|
614
|
|
|
615
|
|
# Verify that the relationship to be added is valid.
|
616
|
0
|
attach_errors = self.is_valid_relation(parent, sub, created=created)
|
617
|
0
|
if attach_errors is not None:
|
618
|
0
|
if created:
|
619
|
0
|
sub.delete()
|
620
|
0
|
return Response(attach_errors, status=status.HTTP_400_BAD_REQUEST)
|
621
|
|
|
622
|
|
# Attach the object to the collection.
|
623
|
0
|
if sub not in relationship.all():
|
624
|
0
|
relationship.add(sub)
|
625
|
|
|
626
|
0
|
if created:
|
627
|
0
|
headers = {}
|
628
|
0
|
if location:
|
629
|
0
|
headers['Location'] = location
|
630
|
0
|
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
|
631
|
|
else:
|
632
|
0
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
633
|
|
|
634
|
3
|
def unattach_validate(self, request):
|
635
|
0
|
sub_id = request.data.get('id', None)
|
636
|
0
|
res = None
|
637
|
0
|
if not sub_id:
|
638
|
0
|
data = dict(msg=_('"id" is required to disassociate'))
|
639
|
0
|
res = Response(data, status=status.HTTP_400_BAD_REQUEST)
|
640
|
0
|
elif not isinstance(sub_id, int):
|
641
|
0
|
data = dict(msg=_('"id" field must be an integer.'))
|
642
|
0
|
res = Response(data, status=status.HTTP_400_BAD_REQUEST)
|
643
|
0
|
return (sub_id, res)
|
644
|
|
|
645
|
3
|
def unattach_by_id(self, request, sub_id):
|
646
|
0
|
parent = self.get_parent_object()
|
647
|
0
|
parent_key = getattr(self, 'parent_key', None)
|
648
|
0
|
relationship = getattrd(parent, self.relationship)
|
649
|
0
|
sub = get_object_or_400(self.model, pk=sub_id)
|
650
|
|
|
651
|
0
|
if parent_key:
|
652
|
0
|
sub.delete()
|
653
|
|
else:
|
654
|
0
|
relationship.remove(sub)
|
655
|
|
|
656
|
0
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
657
|
|
|
658
|
3
|
def unattach(self, request, *args, **kwargs):
|
659
|
0
|
(sub_id, res) = self.unattach_validate(request)
|
660
|
0
|
if res:
|
661
|
0
|
return res
|
662
|
0
|
return self.unattach_by_id(request, sub_id)
|
663
|
|
|
664
|
3
|
def post(self, request, *args, **kwargs):
|
665
|
0
|
if not isinstance(request.data, dict):
|
666
|
0
|
return Response('invalid type for post data',
|
667
|
|
status=status.HTTP_400_BAD_REQUEST)
|
668
|
0
|
if 'disassociate' in request.data:
|
669
|
0
|
return self.unattach(request, *args, **kwargs)
|
670
|
|
else:
|
671
|
0
|
return self.attach(request, *args, **kwargs)
|
672
|
|
|
673
|
|
|
674
|
3
|
class SubListAttachDetachAPIView(SubListCreateAttachDetachAPIView):
|
675
|
|
'''
|
676
|
|
Derived version of SubListCreateAttachDetachAPIView that prohibits creation
|
677
|
|
'''
|
678
|
3
|
metadata_class = SublistAttachDetatchMetadata
|
679
|
|
|
680
|
3
|
def post(self, request, *args, **kwargs):
|
681
|
0
|
sub_id = request.data.get('id', None)
|
682
|
0
|
if not sub_id:
|
683
|
0
|
return Response(
|
684
|
|
dict(msg=_("{} 'id' field is missing.".format(
|
685
|
|
self.model._meta.verbose_name.title()))),
|
686
|
|
status=status.HTTP_400_BAD_REQUEST)
|
687
|
0
|
return super(SubListAttachDetachAPIView, self).post(request, *args, **kwargs)
|
688
|
|
|
689
|
3
|
def update_raw_data(self, data):
|
690
|
0
|
request_method = getattr(self, '_raw_data_request_method', None)
|
691
|
0
|
response_status = getattr(self, '_raw_data_response_status', 0)
|
692
|
0
|
if request_method == 'POST' and response_status in range(400, 500):
|
693
|
0
|
return super(SubListAttachDetachAPIView, self).update_raw_data(data)
|
694
|
0
|
return {'id': None}
|
695
|
|
|
696
|
|
|
697
|
3
|
class DeleteLastUnattachLabelMixin(object):
|
698
|
|
'''
|
699
|
|
Models for which you want the last instance to be deleted from the database
|
700
|
|
when the last disassociate is called should inherit from this class. Further,
|
701
|
|
the model should implement is_detached()
|
702
|
|
'''
|
703
|
3
|
def unattach(self, request, *args, **kwargs):
|
704
|
0
|
(sub_id, res) = super(DeleteLastUnattachLabelMixin, self).unattach_validate(request)
|
705
|
0
|
if res:
|
706
|
0
|
return res
|
707
|
|
|
708
|
0
|
res = super(DeleteLastUnattachLabelMixin, self).unattach_by_id(request, sub_id)
|
709
|
|
|
710
|
0
|
obj = self.model.objects.get(id=sub_id)
|
711
|
|
|
712
|
0
|
if obj.is_detached():
|
713
|
0
|
obj.delete()
|
714
|
|
|
715
|
0
|
return res
|
716
|
|
|
717
|
|
|
718
|
3
|
class SubDetailAPIView(ParentMixin, generics.RetrieveAPIView, GenericAPIView):
|
719
|
3
|
pass
|
720
|
|
|
721
|
|
|
722
|
3
|
class RetrieveAPIView(generics.RetrieveAPIView, GenericAPIView):
|
723
|
3
|
pass
|
724
|
|
|
725
|
|
|
726
|
3
|
class RetrieveUpdateAPIView(RetrieveAPIView, generics.RetrieveUpdateAPIView):
|
727
|
|
|
728
|
3
|
def update(self, request, *args, **kwargs):
|
729
|
3
|
self.update_filter(request, *args, **kwargs)
|
730
|
3
|
return super(RetrieveUpdateAPIView, self).update(request, *args, **kwargs)
|
731
|
|
|
732
|
3
|
def partial_update(self, request, *args, **kwargs):
|
733
|
3
|
self.update_filter(request, *args, **kwargs)
|
734
|
3
|
return super(RetrieveUpdateAPIView, self).partial_update(request, *args, **kwargs)
|
735
|
|
|
736
|
3
|
def update_filter(self, request, *args, **kwargs):
|
737
|
|
'''
|
738
|
|
scrub any fields the user cannot/should not put/patch, based on user context.
|
739
|
|
This runs after read-only serialization filtering
|
740
|
|
'''
|
741
|
3
|
pass
|
742
|
|
|
743
|
|
|
744
|
3
|
class RetrieveDestroyAPIView(RetrieveAPIView, generics.DestroyAPIView):
|
745
|
3
|
pass
|
746
|
|
|
747
|
|
|
748
|
3
|
class RetrieveUpdateDestroyAPIView(RetrieveUpdateAPIView, generics.DestroyAPIView):
|
749
|
3
|
pass
|
750
|
|
|
751
|
|
|
752
|
3
|
def trigger_delayed_deep_copy(*args, **kwargs):
|
753
|
0
|
from cyborgbackup.main.tasks import deep_copy_model_obj
|
754
|
0
|
connection.on_commit(lambda: deep_copy_model_obj.delay(*args, **kwargs))
|