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))

Read our documentation on viewing source code .

Loading