ore.api.common   B
last analyzed

Complexity

Total Complexity 51

Size/Duplication

Total Lines 346
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 51
eloc 218
dl 0
loc 346
rs 7.92
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A GraphSerializer.from_graphml() 0 7 1
A GraphSerializer.to_tex() 0 2 1
A GraphResource.dispatch_nodegroup() 0 3 1
F GraphResource.wrap_view() 0 86 19
A GraphAuthorization.delete_detail() 0 2 1
A GraphResource.dispatch_edges() 0 3 1
A GraphResource.dispatch_nodegroups() 0 3 1
A GraphResource.dispatch_results() 0 3 1
A GraphResource.prepend_urls() 0 35 1
A GraphResource.dispatch_nodes() 0 3 1
A GraphAuthorization.update_list() 0 7 4
A GraphAuthorization.read_list() 0 3 1
A GraphResource.dispatch_jobs() 0 3 1
A GraphSerializer.to_graphml() 0 2 1
A GraphResource.hydrate() 0 17 2
A GraphResource.dispatch_edge() 0 3 1
A GraphAuthorization.create_detail() 0 2 1
A GraphAuthorization.read_detail() 0 13 4
A GraphResource.dispatch_node() 0 3 1
A ProjectResource.get_object_list() 0 3 1
A GraphResource.dispatch_job() 0 3 1
A GraphAuthorization.create_list() 0 3 1
A GraphAuthorization.update_detail() 0 2 1
A GraphAuthorization.delete_list() 0 2 1
A GraphResource.obj_create() 0 17 2

How to fix   Complexity   

Complexity

Complex classes like ore.api.common often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import logging
2
from abc import abstractmethod
3
4
from django.conf.urls import url
5
from tastypie.resources import ModelResource
6
from tastypie.authorization import Authorization
7
from tastypie.exceptions import UnsupportedFormat, BadRequest, ImmediateHttpResponse
8
from django.core.exceptions import ValidationError
9
from tastypie.serializers import Serializer
10
from tastypie import fields
11
from tastypie.http import HttpForbidden, HttpBadRequest
12
from django.http import HttpResponse
13
from django.views.decorators.csrf import csrf_exempt
14
from django.conf import settings
15
from django.utils.cache import patch_cache_control, patch_vary_headers
16
17
from ore.models import Project, Graph
18
19
20
logger = logging.getLogger('ore')
21
22
23
class JobResource(ModelResource):
24
25
    """
26
        An API resource for jobs.
27
    """
28
    pass
29
30
31
class ProjectResource(ModelResource):
32
33
    """
34
        An API resource for projects.
35
    """
36
37
    class Meta:
38
        queryset = Project.objects.filter(deleted=False)
39
        list_allowed_methods = ['get']
40
        detail_allowed_methods = ['get']
41
        excludes = ['deleted', 'owner']
42
        nested = 'graph'
43
44
    graphs = fields.ToManyField('ore.api.external.GraphResource', 'graphs')
45
46
    def get_object_list(self, request):
47
        return super(ProjectResource, self).get_object_list(
48
            request).filter(owner=request.user)
49
50
51
class GraphSerializer(Serializer):
52
53
    """
54
        Our custom serializer / deserializer for graph formats we support.
55
        The XML format is GraphML, anything else is not supported.
56
        The non-implemented deserialization of Tex / JSON input leads to a Tastypie exception,
57
        which is translated to 401 for the client. There is no explicit way in Tastypie to
58
        differentiate between supported serialization / deserialization formats.
59
    """
60
    formats = ['json', 'tex', 'graphml']
61
    content_types = {
62
        'json': 'application/json',
63
        'tex': 'application/text',
64
        'graphml': 'application/xml',
65
    }
66
67
    def to_tex(self, data, options=None):
68
        return data.obj.to_tikz()
69
70
    def to_graphml(self, data, options=None):
71
        return data.obj.to_graphml()
72
73
    def from_graphml(self, content):
74
        '''
75
           Tastypie serialization demands a dictionary of (graph) model
76
           attribute values here. We return a dummy to support the
77
           format officially, and solve the rest in obj_create().
78
        '''
79
        return {}
80
81
82
class GraphAuthorization(Authorization):
83
84
    '''
85
        Tastypie authorization class. The main task of this class
86
        is to restrict the accessible objects to the ones that the currently
87
        logged-in user is allowed to use.
88
    '''
89
90
    def read_list(self, object_list, bundle):
91
        ''' User is only allowed to get the graphs he owns.'''
92
        return object_list.filter(owner=bundle.request.user)
93
94
    def read_detail(self, object_list, bundle):
95
        ''' User is only allowed to get the graph if he owns it.'''
96
97
        if bundle.obj.owner == bundle.request.user:
98
            return True
99
        elif bundle.obj.sharings.filter(user=bundle.request.user):
100
            bundle.obj.read_only = True
101
            return True
102
        elif bundle.request.user.is_staff:
103
            bundle.obj.read_only = True
104
            return True
105
        else:
106
            return False
107
108
    def create_list(self, object_list, bundle):
109
        # Assuming they're auto-assigned to ``user``.
110
        return object_list
111
112
    def create_detail(self, object_list, bundle):
113
        return bundle.obj.owner == bundle.request.user
114
115
    def update_list(self, object_list, bundle):
116
        allowed = []
117
        # Since they may not all be saved, iterate over them.
118
        for obj in object_list:
119
            if obj.owner == bundle.request.user and not bundle.obj.read_only:
120
                allowed.append(obj)
121
        return allowed
122
123
    def update_detail(self, object_list, bundle):
124
        return bundle.obj.owner == bundle.request.user and not bundle.obj.read_only
125
126
    def delete_list(self, object_list, bundle):
127
        return object_list.filter(owner=bundle.request.user)
128
129
    def delete_detail(self, object_list, bundle):
130
        return bundle.obj.owner == bundle.request.user
131
132
133
class GraphResource(ModelResource):
134
135
    """
136
        A graph resource with support for nested node / edge / job resources.
137
    """
138
139
    class Meta:
140
        queryset = Graph.objects.filter(deleted=False)
141
        authorization = GraphAuthorization()
142
        serializer = GraphSerializer()
143
        list_allowed_methods = ['get', 'post']
144
        detail_allowed_methods = ['get']
145
146
        excludes = ['deleted', 'owner', 'read_only']
147
        filtering = {"kind": ('exact')}
148
149
    project = fields.ToOneField(ProjectResource, 'project')
150
151
    def prepend_urls(self):
152
        return [
153
            url(r'^graphs/(?P<pk>\d+)$',
154
                self.wrap_view('dispatch_detail'),
155
                name='graph'),
156
            url(r'^graphs/$',
157
                self.wrap_view('dispatch_list'),
158
                name='graphs'),
159
            url(r'^graphs/(?P<pk>\d+)/edges/$',
160
                self.wrap_view('dispatch_edges'),
161
                name="edges"),
162
            url(r'^graphs/(?P<pk>\d+)/edges/(?P<client_id>\d+)$',
163
                self.wrap_view('dispatch_edge'),
164
                name="edge"),
165
            url(r'^graphs/(?P<pk>\d+)/nodes/$',
166
                self.wrap_view('dispatch_nodes'),
167
                name="nodes"),
168
            url(r'^graphs/(?P<pk>\d+)/nodes/(?P<client_id>\d+)$',
169
                self.wrap_view('dispatch_node'),
170
                name="node"),
171
            url(r'^graphs/(?P<pk>\d+)/jobs/$',
172
                self.wrap_view('dispatch_jobs'),
173
                name="jobs"),
174
            url(r'^graphs/(?P<pk>\d+)/jobs/(?P<secret>\S+)/results/$',
175
                self.wrap_view('dispatch_results'),
176
                name="results"),
177
            url(r'^graphs/(?P<pk>\d+)/jobs/(?P<secret>\S+)$',
178
                self.wrap_view('dispatch_job'),
179
                name="job"),
180
            url(r'^graphs/(?P<pk>\d+)/nodegroups/$',
181
                self.wrap_view('dispatch_nodegroups'),
182
                name="nodegroups"),
183
            url(r'^graphs/(?P<pk>\d+)/nodegroups/(?P<client_id>\d+)$',
184
                self.wrap_view('dispatch_nodegroup'),
185
                name="nodegroup"),
186
        ]
187
188
    def obj_create(self, bundle, **kwargs):
189
        bundle.obj = self._meta.object_class()
190
        bundle.obj.owner = bundle.request.user
191
        # Get the user-specified project, and make sure that it is his.
192
        # This is not an authorization problem for the (graph) resource itself,
193
        # so it must be handled here and not in the auth class.
194
        try:
195
            project = Project.objects.get(
196
                pk=bundle.request.GET['project'],
197
                owner=bundle.request.user)
198
            bundle.obj.project = project
199
        except Exception:
200
            raise ImmediateHttpResponse(
201
                response=HttpForbidden("You can't use this project for your new graph."))
202
            # Fill the graph with the GraphML data
203
        bundle.obj.from_graphml(bundle.request.body)
204
        return bundle.obj
205
206
    @abstractmethod
207
    def dispatch_edges(self, request, **kwargs):
208
        pass
209
210
    @abstractmethod
211
    def dispatch_edge(self, request, **kwargs):
212
        pass
213
214
    @abstractmethod
215
    def dispatch_nodes(self, request, **kwargs):
216
        pass
217
218
    @abstractmethod
219
    def dispatch_node(self, request, **kwargs):
220
        pass
221
222
    @abstractmethod
223
    def dispatch_nodegroups(self, request, **kwargs):
224
        pass
225
226
    @abstractmethod
227
    def dispatch_nodegroup(self, request, **kwargs):
228
        pass
229
230
    @abstractmethod
231
    def dispatch_jobs(self, request, **kwargs):
232
        pass
233
234
    @abstractmethod
235
    def dispatch_results(self, request, **kwargs):
236
        pass
237
238
    @abstractmethod
239
    def dispatch_job(self, request, **kwargs):
240
        pass
241
242
    def hydrate(self, bundle):
243
        # Make sure that owners are assigned correctly
244
        bundle.obj.owner = bundle.request.user
245
        # Get the user-specified project, and make sure that it is his.
246
        # This is not an authorization problem for the (graph) resource itself,
247
        # so it must be handled here and not in the auth class.
248
        try:
249
            project = Project.objects.get(
250
                pk=bundle.request.GET['project'],
251
                owner=bundle.request.user)
252
            bundle.obj.project = project
253
        except Exception:
254
            raise ImmediateHttpResponse(
255
                response=HttpForbidden("You can't use this project for your new graph."))
256
            # Fill the graph with the GraphML data
257
        bundle.obj.from_graphml(bundle.request.body)
258
        return bundle
259
260
    def wrap_view(self, view):
261
        """
262
        Wrap the default Tastypie implementation to return custom error codes instead of generic 500's.
263
        It also takes care of the correct content disposition.
264
        """
265
266
        @csrf_exempt
267
        def wrapper(request, *args, **kwargs):
268
            ''' This is a straight copy from the Tastypie sources,
269
                extended with the 415 generation code.
270
            '''
271
            try:
272
                callback = getattr(self, view)
273
                response = callback(request, *args, **kwargs)
274
275
                # Our response can vary based on a number of factors, use
276
                # the cache class to determine what we should ``Vary`` on so
277
                # caches won't return the wrong (cached) version.
278
                varies = getattr(self._meta.cache, "varies", [])
279
280
                if varies:
281
                    patch_vary_headers(response, varies)
282
283
                if self._meta.cache.cacheable(request, response):
284
                    if self._meta.cache.cache_control():
285
                        # If the request is cacheable and we have a
286
                        # ``Cache-Control`` available then patch the header.
287
                        patch_cache_control(
288
                            response,
289
                            **self._meta.cache.cache_control())
290
291
                if request.is_ajax() and not response.has_header(
292
                        "Cache-Control"):
293
                    # IE excessively caches XMLHttpRequests, so we're disabling
294
                    # the browser cache here.
295
                    # See http://www.enhanceie.com/ie/bugs.asp for details.
296
                    patch_cache_control(response, no_cache=True)
297
298
                # Detect result content type and create some meaningful
299
                # disposition name
300
                if 'format' in request.GET:
301
                    if request.GET['format'] == 'graphml':
302
                        response[
303
                            'Content-Disposition'] = 'attachment; filename=graph.xml'
304
                    elif request.GET['format'] == 'tex':
305
                        response[
306
                            'Content-Disposition'] = 'attachment; filename=graph.tex'
307
                    elif request.GET['format'] == 'json':
308
                        response[
309
                            'Content-Disposition'] = 'attachment; filename=graph.json'
310
311
                return response
312
            except UnsupportedFormat as e:
313
                response = HttpResponse()
314
                response.status_code = 413
315
                return response
316
            except (BadRequest, fields.ApiFieldError) as e:
317
                data = {"error": e.args[0] if getattr(e, 'args') else ''}
318
                return self.error_response(
319
                    request, data, response_class=HttpBadRequest)
320
            except ValidationError as e:
321
                data = {"error": e.messages}
322
                return self.error_response(
323
                    request, data, response_class=HttpBadRequest)
324
            except Exception as e:
325
                if hasattr(e, 'response'):
326
                    return e.response
327
328
                # A real, non-expected exception.
329
                # Handle the case where the full traceback is more helpful
330
                # than the serialized error.
331
                if settings.DEBUG and getattr(
332
                        settings, 'TASTYPIE_FULL_DEBUG', False):
333
                    raise
334
335
                # Re-raise the error to get a proper traceback when the error
336
                # happend during a test case
337
                if request.META.get('SERVER_NAME') == 'testserver':
338
                    raise
339
340
                # Rather than re-raising, we're going to things similar to
341
                # what Django does. The difference is returning a serialized
342
                # error message.
343
                return self._handle_500(request, e)
344
345
        return wrapper
346