Total Complexity | 84 |
Total Lines | 699 |
Duplicated Lines | 18.74 % |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like ore.api.frontend 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 json |
||
2 | import logging |
||
3 | |||
4 | from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned |
||
5 | from django.core.urlresolvers import reverse |
||
6 | from django.http import HttpResponse |
||
7 | from django.conf import settings |
||
8 | from tastypie.authentication import SessionAuthentication |
||
9 | from tastypie.authorization import Authorization |
||
10 | from tastypie.bundle import Bundle |
||
11 | from tastypie.exceptions import ImmediateHttpResponse |
||
12 | from tastypie.http import HttpApplicationError, HttpAccepted, HttpForbidden, HttpNotFound, HttpMultipleChoices |
||
13 | from tastypie import fields |
||
14 | from django.core.mail import mail_managers |
||
15 | from tastypie.resources import ModelResource |
||
16 | from tastypie.serializers import Serializer |
||
17 | |||
18 | from ore.models import Job, Graph, Notification, Node, NodeGroup, Edge, Result |
||
19 | from . import common |
||
20 | |||
21 | logger = logging.getLogger('ore') |
||
22 | |||
23 | |||
24 | class GraphOwnerAuthorization(Authorization): |
||
25 | |||
26 | """ |
||
27 | A tastypie authorization class that checks if the 'graph' attribute |
||
28 | links to a graph that is owned by the requesting user. |
||
29 | """ |
||
30 | |||
31 | def read_list(self, object_list, bundle): |
||
32 | return object_list.filter(graph__owner=bundle.request.user) |
||
33 | |||
34 | def read_detail(self, object_list, bundle): |
||
35 | return bundle.obj.graph.owner == bundle.request.user |
||
36 | |||
37 | def create_list(self, object_list, bundle): |
||
38 | # Assuming they're auto-assigned to graphs that are owned by the |
||
39 | # requester |
||
40 | return object_list |
||
41 | |||
42 | def create_detail(self, object_list, bundle): |
||
43 | # graph = Graph.objects.get(pk=bundle.data['graph'], deleted=False) |
||
44 | return bundle.data['graph'].owner == bundle.request.user and not bundle.data[ |
||
45 | 'graph'].read_only |
||
46 | |||
47 | def update_list(self, object_list, bundle): |
||
48 | allowed = [] |
||
49 | # Since they may not all be saved, iterate over them. |
||
50 | for obj in object_list: |
||
51 | if obj.graph.owner == bundle.request.user and not bundle.obj.graph.read_only: |
||
52 | allowed.append(obj) |
||
53 | return allowed |
||
54 | |||
55 | def update_detail(self, object_list, bundle): |
||
56 | return bundle.obj.graph.owner == bundle.request.user and not bundle.obj.graph.read_only |
||
57 | |||
58 | def delete_list(self, object_list, bundle): |
||
59 | return object_list.filter(graph__owner=bundle.request.user) |
||
60 | |||
61 | def delete_detail(self, object_list, bundle): |
||
62 | return bundle.obj.graph.owner == bundle.request.user |
||
63 | |||
64 | |||
65 | class JobResource(common.JobResource): |
||
66 | |||
67 | """ |
||
68 | An API resource for jobs. |
||
69 | Jobs look different for the JS client than they look for the backend, |
||
70 | so we have a custom implementation here. |
||
71 | """ |
||
72 | |||
73 | class Meta: |
||
74 | queryset = Job.objects.all() |
||
75 | authorization = GraphOwnerAuthorization() |
||
76 | authentication = SessionAuthentication() |
||
77 | list_allowed_methods = ['post'] |
||
78 | detail_allowed_methods = ['get'] |
||
79 | |||
80 | graph = fields.ToOneField('ore.api.common.GraphResource', 'graph') |
||
81 | |||
82 | def get_resource_uri( |
||
83 | self, bundle_or_obj=None, url_name='api_dispatch_list'): |
||
84 | """ |
||
85 | Since we change the API URL format to nested resources, we need also to |
||
86 | change the location determination for a given resource object. |
||
87 | """ |
||
88 | job_secret = bundle_or_obj.obj.secret |
||
89 | graph_pk = bundle_or_obj.obj.graph.pk |
||
90 | # This is a quick fix for dealing with reverse() begind an SSL proxy |
||
91 | # Normally, Django should consider the X-FORWARDED header inside the reverse() |
||
92 | # implementation and figure out by itself what the correct base is |
||
93 | relative_url = reverse( |
||
94 | 'job', kwargs={'api_name': 'front', 'pk': graph_pk, 'secret': job_secret}) |
||
95 | return settings.SERVER + relative_url |
||
96 | |||
97 | def obj_create(self, bundle, **kwargs): |
||
98 | """ |
||
99 | Create a new job for the given graph. |
||
100 | The request body contains the information about the kind of job being requested. |
||
101 | The result is a job URL that is based on the generated job secret. |
||
102 | This is the only override that allows us to access 'kwargs', which contains the |
||
103 | graph_id from the original request. |
||
104 | """ |
||
105 | graph = Graph.objects.get(pk=kwargs['graph_id'], deleted=False) |
||
106 | # Check if we have a cached result, and deliver this job |
||
107 | job = Job.exists_with_result(graph=graph, kind=bundle.data['kind']) |
||
108 | if not job: |
||
109 | # We need a truly new job |
||
110 | bundle.data['graph'] = graph |
||
111 | bundle.data['graph_modified'] = graph.modified |
||
112 | bundle.data['kind'] = bundle.data['kind'] |
||
113 | bundle.obj = self._meta.object_class() |
||
114 | bundle = self.full_hydrate(bundle) |
||
115 | return self.save(bundle) |
||
116 | else: |
||
117 | logger.debug( |
||
118 | "Responding with cached job URL, instead of creating a new one") |
||
119 | bundle.obj = job |
||
120 | return bundle |
||
121 | |||
122 | def get_detail(self, request, **kwargs): |
||
123 | """ |
||
124 | Called by the request dispatcher in case somebody tries to GET a job resource. |
||
125 | For the frontend, deliver the current job status if pending, or the result. |
||
126 | """ |
||
127 | basic_bundle = self.build_bundle(request=request) |
||
128 | try: |
||
129 | job = self.cached_obj_get( |
||
130 | bundle=basic_bundle, |
||
131 | **self.remove_api_resource_names(kwargs)) |
||
132 | except ObjectDoesNotExist: |
||
133 | return HttpNotFound() |
||
134 | except MultipleObjectsReturned: |
||
135 | return HttpMultipleChoices( |
||
136 | "More than one resource is found at this URI.") |
||
137 | |||
138 | if job.done(): |
||
139 | if job.exit_code == 0: |
||
140 | response = {} |
||
141 | # We deliver the columns layout for the result tables + all |
||
142 | # global issues |
||
143 | relative_url = reverse( |
||
144 | 'results', |
||
145 | kwargs={ |
||
146 | 'api_name': 'front', |
||
147 | 'pk': job.graph.pk, |
||
148 | 'secret': job.secret}) |
||
149 | # This is a quick fix for dealing with reverse() begind an SSL proxy |
||
150 | # Normally, Django should consider the X-FORWARDED header inside the reverse() |
||
151 | # implementation and figure out by itself what the correct base is |
||
152 | results_url = settings.SERVER + relative_url |
||
153 | if not job.requires_download: |
||
154 | response['columns'] = [ |
||
155 | {'mData': key, 'sTitle': title} for key, title in job.result_titles] |
||
156 | response['axis_titles'] = job.axis_titles() |
||
157 | response['static_info'] = job.static_info() |
||
158 | try: |
||
159 | response['issues'] = Result.objects.get( |
||
160 | job=job, |
||
161 | kind=Result.GRAPH_ISSUES).issues |
||
162 | except Exception: |
||
163 | # no global issues recorded, that's fine |
||
164 | pass |
||
165 | response = HttpResponse(json.dumps(response)) |
||
166 | response["Location"] = results_url |
||
167 | return response |
||
168 | else: |
||
169 | logger.debug("Job is done, but with non-zero exit code.") |
||
170 | mail_managers( |
||
171 | 'Job %s for graph %u ended with non-zero exit code %u.' % ( |
||
172 | job.pk, |
||
173 | job.graph.pk, |
||
174 | job.exit_code), |
||
175 | job.graph.to_xml()) |
||
176 | return HttpApplicationError() |
||
177 | else: |
||
178 | # Job is pending, tell this by HTTP return code |
||
179 | return HttpAccepted() |
||
180 | |||
181 | def apply_authorization_limits(self, request, object_list): |
||
182 | # Prevent cross-checking of jobs by different users |
||
183 | return object_list.filter(graph__owner=request.user) |
||
184 | |||
185 | |||
186 | class NotificationResource(ModelResource): |
||
187 | |||
188 | """ |
||
189 | An API resource for notifications. |
||
190 | """ |
||
191 | |||
192 | class Meta: |
||
193 | queryset = Notification.objects.all() |
||
194 | detail_allowed_methods = ['delete'] |
||
195 | authentication = SessionAuthentication() |
||
196 | authorization = Authorization() |
||
197 | |||
198 | def obj_delete(self, bundle, **kwargs): |
||
199 | noti = self.obj_get(bundle=bundle, **kwargs) |
||
200 | noti.users.remove(bundle.request.user) |
||
201 | noti.save() |
||
202 | |||
203 | |||
204 | View Code Duplication | class NodeSerializer(Serializer): |
|
|
|||
205 | |||
206 | """ |
||
207 | Our custom node serializer. Using the default serializer would demand that the |
||
208 | graph reference is included, while we take it from the nested resource URL. |
||
209 | """ |
||
210 | formats = ['json'] |
||
211 | content_types = { |
||
212 | 'json': 'application/json' |
||
213 | } |
||
214 | |||
215 | def from_json(self, content): |
||
216 | data_dict = json.loads(content) |
||
217 | if 'properties' in data_dict: |
||
218 | props = data_dict['properties'] |
||
219 | for key, val in props.iteritems(): |
||
220 | # JS code: {'prop_name': {'value':'prop_value'}} |
||
221 | # All others: {'prop_name': 'prop_value'} |
||
222 | if isinstance(val, dict) and 'value' in val: |
||
223 | props[key] = val['value'] |
||
224 | return data_dict |
||
225 | |||
226 | def to_json(self, data): |
||
227 | return json.dumps(data) |
||
228 | |||
229 | |||
230 | class NodeResource(ModelResource): |
||
231 | |||
232 | """ |
||
233 | An API resource for nodes. |
||
234 | """ |
||
235 | |||
236 | class Meta: |
||
237 | queryset = Node.objects.filter(deleted=False) |
||
238 | authorization = GraphOwnerAuthorization() |
||
239 | authentication = SessionAuthentication() |
||
240 | serializer = NodeSerializer() |
||
241 | list_allowed_methods = ['get', 'post'] |
||
242 | detail_allowed_methods = ['get', 'post', 'patch', 'delete'] |
||
243 | excludes = ['deleted', 'id'] |
||
244 | |||
245 | graph = fields.ToOneField('ore.api.common.GraphResource', 'graph') |
||
246 | |||
247 | def get_resource_uri(self, bundle_or_obj): |
||
248 | """ |
||
249 | Since we change the API URL format to nested resources, we need also to |
||
250 | change the location determination for a given resource object. |
||
251 | """ |
||
252 | node_client_id = bundle_or_obj.obj.client_id |
||
253 | graph_pk = bundle_or_obj.obj.graph.pk |
||
254 | relative_url = reverse( |
||
255 | 'node', kwargs={'api_name': 'front', 'pk': graph_pk, 'client_id': node_client_id}) |
||
256 | # This is a quick fix for dealing with reverse() begind an SSL proxy |
||
257 | # Normally, Django should consider the X-FORWARDED header inside the reverse() |
||
258 | # implementation and figure out by itself what the correct base is |
||
259 | return settings.SERVER + relative_url |
||
260 | |||
261 | |||
262 | def obj_create(self, bundle, **kwargs): |
||
263 | """ |
||
264 | This is the only override that allows us to access 'kwargs', which contains the |
||
265 | graph_id from the original request. |
||
266 | """ |
||
267 | bundle.data['graph'] = Graph.objects.get( |
||
268 | pk=kwargs['graph_id'], |
||
269 | deleted=False) |
||
270 | bundle.obj = self._meta.object_class() |
||
271 | bundle = self.full_hydrate(bundle) |
||
272 | # Save node, so that set_attr has something to relate to |
||
273 | bundle.obj.save() |
||
274 | if 'properties' in bundle.data: |
||
275 | bundle.obj.set_attrs(bundle.data['properties']) |
||
276 | return self.save(bundle) |
||
277 | |||
278 | View Code Duplication | def patch_detail(self, request, **kwargs): |
|
279 | """ |
||
280 | Updates a resource in-place. We could also override obj_update, which is |
||
281 | the Tastypie intended-way of having a custom PATCH implementation, but this |
||
282 | method gets a full updated object bundle that is expected to be directly written |
||
283 | to the object. In this method, we still have access to what actually really |
||
284 | comes as part of the update payload. |
||
285 | |||
286 | If the resource is updated, return ``HttpAccepted`` (202 Accepted). |
||
287 | If the resource did not exist, return ``HttpNotFound`` (404 Not Found). |
||
288 | """ |
||
289 | try: |
||
290 | # Fetch relevant node object as Tastypie does it |
||
291 | basic_bundle = self.build_bundle(request=request) |
||
292 | obj = self.cached_obj_get( |
||
293 | bundle=basic_bundle, |
||
294 | **self.remove_api_resource_names(kwargs)) |
||
295 | except ObjectDoesNotExist: |
||
296 | return HttpNotFound() |
||
297 | except MultipleObjectsReturned: |
||
298 | return HttpMultipleChoices( |
||
299 | "More than one resource is found at this URI.") |
||
300 | |||
301 | # Deserialize incoming update payload JSON from request |
||
302 | deserialized = self.deserialize(request, request.body, |
||
303 | format=request.META.get('CONTENT_TYPE', 'application/json')) |
||
304 | if 'properties' in deserialized: |
||
305 | obj.set_attrs(deserialized['properties']) |
||
306 | # return the updated node object |
||
307 | return HttpResponse(obj.to_json(), 'application/json', status=202) |
||
308 | |||
309 | |||
310 | View Code Duplication | class NodeGroupSerializer(Serializer): |
|
311 | |||
312 | """ |
||
313 | Our custom node group serializer. Using the default serializer would demand that the |
||
314 | graph reference is included, while we take it from the nested resource URL. |
||
315 | """ |
||
316 | formats = ['json'] |
||
317 | content_types = { |
||
318 | 'json': 'application/json' |
||
319 | } |
||
320 | |||
321 | def from_json(self, content): |
||
322 | data_dict = json.loads(content) |
||
323 | if 'properties' in data_dict: |
||
324 | props = data_dict['properties'] |
||
325 | for key, val in props.iteritems(): |
||
326 | # JS code: {'prop_name': {'value':'prop_value'}} |
||
327 | # All others: {'prop_name': 'prop_value'} |
||
328 | if isinstance(val, dict) and 'value' in val: |
||
329 | props[key] = val['value'] |
||
330 | return data_dict |
||
331 | |||
332 | def to_json(self, data): |
||
333 | return json.dumps(data) |
||
334 | |||
335 | |||
336 | class NodeGroupResource(ModelResource): |
||
337 | |||
338 | """ |
||
339 | An API resource for node groups. |
||
340 | """ |
||
341 | |||
342 | class Meta: |
||
343 | queryset = NodeGroup.objects.filter(deleted=False) |
||
344 | authorization = GraphOwnerAuthorization() |
||
345 | authentication = SessionAuthentication() |
||
346 | serializer = NodeGroupSerializer() |
||
347 | list_allowed_methods = ['post'] |
||
348 | detail_allowed_methods = ['delete', 'patch'] |
||
349 | excludes = ['deleted'] |
||
350 | |||
351 | graph = fields.ToOneField('ore.api.common.GraphResource', 'graph') |
||
352 | |||
353 | def get_resource_uri(self, bundle_or_obj): |
||
354 | """ |
||
355 | Since we change the API URL format to nested resources, we need also to |
||
356 | change the location determination for a given resource object. |
||
357 | """ |
||
358 | group_client_id = bundle_or_obj.obj.client_id |
||
359 | graph_pk = bundle_or_obj.obj.graph.pk |
||
360 | relative_url = reverse('nodegroup', kwargs={ |
||
361 | 'api_name': 'front', 'pk': graph_pk, 'client_id': group_client_id}) |
||
362 | # This is a quick fix for dealing with reverse() begind an SSL proxy |
||
363 | # Normally, Django should consider the X-FORWARDED header inside the reverse() |
||
364 | # implementation and figure out by itself what the correct base is |
||
365 | return settings.SERVER + relative_url |
||
366 | |||
367 | def obj_create(self, bundle, **kwargs): |
||
368 | """ |
||
369 | The method called by the dispatcher when a NodeGroup resource is created. |
||
370 | |||
371 | This is the only override that allows us to access 'kwargs', which contains the |
||
372 | graph_id from the original request. |
||
373 | """ |
||
374 | try: |
||
375 | bundle.data['graph'] = Graph.objects.get( |
||
376 | pk=kwargs['graph_id'], |
||
377 | deleted=False) |
||
378 | except Exception: |
||
379 | raise ImmediateHttpResponse( |
||
380 | response=HttpForbidden("You can't use this graph.")) |
||
381 | bundle.obj = self._meta.object_class() |
||
382 | bundle = self.full_hydrate(bundle) |
||
383 | bundle = self.save(bundle) # Prepare ManyToMany relationship |
||
384 | for node_id in bundle.data['nodeIds']: |
||
385 | try: |
||
386 | # The client may refer to nodes that are already gone, |
||
387 | # we simply ignore them |
||
388 | node = Node.objects.get( |
||
389 | graph=bundle.data['graph'], |
||
390 | client_id=node_id, |
||
391 | deleted=False) |
||
392 | bundle.obj.nodes.add(node) |
||
393 | except ObjectDoesNotExist: |
||
394 | pass |
||
395 | if 'properties' in bundle.data: |
||
396 | bundle.obj.set_attrs(bundle.data['properties']) |
||
397 | bundle.obj.save() |
||
398 | return self.save(bundle) |
||
399 | |||
400 | def patch_detail(self, request, **kwargs): |
||
401 | """ |
||
402 | Updates a resource in-place. We could also override obj_update, which is |
||
403 | the Tastypie intended-way of having a custom PATCH implementation, but this |
||
404 | method gets a full updated object bundle that is expected to be directly written |
||
405 | to the object. In this method, we still have access to what actually really |
||
406 | comes as part of the update payload. |
||
407 | |||
408 | If the resource is updated, return ``HttpAccepted`` (202 Accepted). |
||
409 | If the resource did not exist, return ``HttpNotFound`` (404 Not Found). |
||
410 | """ |
||
411 | try: |
||
412 | # Fetch relevant node object as Tastypie does it |
||
413 | basic_bundle = self.build_bundle(request=request) |
||
414 | obj = self.cached_obj_get( |
||
415 | bundle=basic_bundle, |
||
416 | **self.remove_api_resource_names(kwargs)) |
||
417 | except ObjectDoesNotExist: |
||
418 | return HttpNotFound() |
||
419 | except MultipleObjectsReturned: |
||
420 | return HttpMultipleChoices( |
||
421 | "More than one resource is found at this URI.") |
||
422 | |||
423 | # Deserialize incoming update payload JSON from request |
||
424 | deserialized = self.deserialize(request, request.body, |
||
425 | format=request.META.get('CONTENT_TYPE', 'application/json')) |
||
426 | if 'properties' in deserialized: |
||
427 | obj.set_attrs(deserialized['properties']) |
||
428 | if 'nodeIds' in deserialized: |
||
429 | logger.debug("Updating nodes for node group") |
||
430 | obj.nodes.clear() # nodes_set is magically created by Django |
||
431 | node_objects = Node.objects.filter( |
||
432 | deleted=False, |
||
433 | graph=obj.graph, |
||
434 | client_id__in=deserialized['nodeIds']) |
||
435 | obj.nodes = node_objects |
||
436 | obj.save() |
||
437 | |||
438 | # return the updated node group object |
||
439 | return HttpResponse(obj.to_json(), 'application/json', status=202) |
||
440 | |||
441 | |||
442 | View Code Duplication | class EdgeSerializer(Serializer): |
|
443 | |||
444 | """ |
||
445 | Our custom edge serializer. Using the default serializer would demand that the |
||
446 | graph reference is included, while we take it from the nested resource URL. |
||
447 | It would also demand that nodes are referenced by their full URL's, which we do not |
||
448 | do. |
||
449 | """ |
||
450 | formats = ['json'] |
||
451 | content_types = { |
||
452 | 'json': 'application/json' |
||
453 | } |
||
454 | |||
455 | def from_json(self, content): |
||
456 | data_dict = json.loads(content) |
||
457 | if 'properties' in data_dict: |
||
458 | props = data_dict['properties'] |
||
459 | for key, val in props.iteritems(): |
||
460 | # JS code: {'prop_name': {'value':'prop_value'}} |
||
461 | # All others: {'prop_name': 'prop_value'} |
||
462 | if isinstance(val, dict) and 'value' in val: |
||
463 | props[key] = val['value'] |
||
464 | return data_dict |
||
465 | |||
466 | |||
467 | class EdgeResource(ModelResource): |
||
468 | |||
469 | """ |
||
470 | An API resource for edges. |
||
471 | """ |
||
472 | |||
473 | class Meta: |
||
474 | queryset = Edge.objects.filter(deleted=False) |
||
475 | serializer = EdgeSerializer() |
||
476 | authorization = GraphOwnerAuthorization() |
||
477 | authentication = SessionAuthentication() |
||
478 | list_allowed_methods = ['get', 'post'] |
||
479 | detail_allowed_methods = ['get', 'post', 'delete', 'patch'] |
||
480 | excludes = ['deleted', 'id'] |
||
481 | |||
482 | graph = fields.ToOneField('ore.api.common.GraphResource', 'graph') |
||
483 | source = fields.ToOneField(NodeResource, 'source') |
||
484 | target = fields.ToOneField(NodeResource, 'target') |
||
485 | |||
486 | def get_resource_uri(self, bundle_or_obj): |
||
487 | """ |
||
488 | Since we change the API URL format to nested resources, we need also to |
||
489 | change the location determination for a given resource object. |
||
490 | """ |
||
491 | edge_client_id = bundle_or_obj.obj.client_id |
||
492 | graph_pk = bundle_or_obj.obj.graph.pk |
||
493 | relative_url = reverse( |
||
494 | 'edge', kwargs={'api_name': 'front', 'pk': graph_pk, 'client_id': edge_client_id}) |
||
495 | return settings.SERVER + relative_url |
||
496 | |||
497 | def obj_create(self, bundle, **kwargs): |
||
498 | """ |
||
499 | This is the only override that allows us to access 'kwargs', which contains the |
||
500 | graph_id from the original request. |
||
501 | """ |
||
502 | graph = Graph.objects.get(pk=kwargs['graph_id'], deleted=False) |
||
503 | bundle.data['graph'] = graph |
||
504 | bundle.data['source'] = Node.objects.get( |
||
505 | client_id=bundle.data['source'], |
||
506 | graph=graph, |
||
507 | deleted=False) |
||
508 | bundle.data['target'] = Node.objects.get( |
||
509 | client_id=bundle.data['target'], |
||
510 | graph=graph, |
||
511 | deleted=False) |
||
512 | bundle.obj = self._meta.object_class() |
||
513 | bundle = self.full_hydrate(bundle) |
||
514 | bundle.obj.save() # to allow property changes |
||
515 | if 'properties' in bundle.data: |
||
516 | bundle.obj.set_attrs(bundle.data['properties']) |
||
517 | return self.save(bundle) |
||
518 | |||
519 | View Code Duplication | def patch_detail(self, request, **kwargs): |
|
520 | """ |
||
521 | Updates a resource in-place. We could also override obj_update, which is |
||
522 | the Tastypie intended-way of having a custom PATCH implementation, but this |
||
523 | method gets a full updated object bundle that is expected to be directly written |
||
524 | to the object. In this method, we still have access to what actually really |
||
525 | comes as part of the update payload. |
||
526 | |||
527 | If the resource is updated, return ``HttpAccepted`` (202 Accepted). |
||
528 | If the resource did not exist, return ``HttpNotFound`` (404 Not Found). |
||
529 | """ |
||
530 | try: |
||
531 | # Fetch relevant node object as Tastypie does it |
||
532 | basic_bundle = self.build_bundle(request=request) |
||
533 | obj = self.cached_obj_get( |
||
534 | bundle=basic_bundle, |
||
535 | **self.remove_api_resource_names(kwargs)) |
||
536 | except ObjectDoesNotExist: |
||
537 | return HttpNotFound() |
||
538 | except MultipleObjectsReturned: |
||
539 | return HttpMultipleChoices( |
||
540 | "More than one resource is found at this URI.") |
||
541 | |||
542 | # Deserialize incoming update payload JSON from request |
||
543 | deserialized = self.deserialize(request, request.body, |
||
544 | format=request.META.get('CONTENT_TYPE', 'application/json')) |
||
545 | if 'properties' in deserialized: |
||
546 | obj.set_attrs(deserialized['properties']) |
||
547 | # return the updated edge object |
||
548 | return HttpResponse(obj.to_json(), 'application/json', status=202) |
||
549 | |||
550 | |||
551 | class ProjectResource(common.ProjectResource): |
||
552 | |||
553 | class Meta(common.ProjectResource.Meta): |
||
554 | authentication = SessionAuthentication() |
||
555 | |||
556 | |||
557 | class GraphSerializer(common.GraphSerializer): |
||
558 | |||
559 | """ |
||
560 | The frontend gets its own JSON format for the graph information, |
||
561 | not the default HATEOAS format generated by Tastypie. For this reason, |
||
562 | we need a frontend API specific JSON serializer. |
||
563 | """ |
||
564 | |||
565 | def to_json(self, data, options=None): |
||
566 | if isinstance(data, Bundle): |
||
567 | return data.obj.to_json(use_value_dict=True) |
||
568 | elif isinstance(data, dict): |
||
569 | if 'objects' in data: # object list |
||
570 | graphs = [] |
||
571 | for graph in data['objects']: |
||
572 | relative_url = reverse('graph', kwargs={'api_name': 'front', 'pk': graph.obj.pk}) |
||
573 | # This is a quick fix for dealing with reverse() begind an SSL proxy |
||
574 | # Normally, Django should consider the X-FORWARDED header inside the reverse() |
||
575 | # implementation and figure out by itself what the correct base is |
||
576 | absolute_url = settings.SERVER + relative_url |
||
577 | |||
578 | graphs.append({'url': absolute_url, |
||
579 | 'name': graph.obj.name}) |
||
580 | return json.dumps({'graphs': graphs}) |
||
581 | else: |
||
582 | # Traceback error message, instead of a result |
||
583 | return json.dumps(data) |
||
584 | |||
585 | |||
586 | class GraphResource(common.GraphResource): |
||
587 | |||
588 | """ |
||
589 | Override our GraphResource Meta class to register the custom |
||
590 | frontend JSON serializer and the frontent auth method. |
||
591 | This version also configures the dispatching to the nested resource implementations in this file. |
||
592 | """ |
||
593 | |||
594 | class Meta(common.GraphResource.Meta): |
||
595 | authentication = SessionAuthentication() |
||
596 | serializer = GraphSerializer() |
||
597 | nodes = fields.ToManyField(NodeResource, 'nodes') |
||
598 | edges = fields.ToManyField(EdgeResource, 'edges') |
||
599 | |||
600 | def dispatch_edges(self, request, **kwargs): |
||
601 | edge_resource = EdgeResource() |
||
602 | return edge_resource.dispatch_list(request, graph_id=kwargs['pk']) |
||
603 | |||
604 | def dispatch_edge(self, request, **kwargs): |
||
605 | edge_resource = EdgeResource() |
||
606 | return edge_resource.dispatch_detail( |
||
607 | request, graph_id=kwargs['pk'], client_id=kwargs['client_id']) |
||
608 | |||
609 | def dispatch_nodes(self, request, **kwargs): |
||
610 | node_resource = NodeResource() |
||
611 | return node_resource.dispatch_list(request, graph_id=kwargs['pk']) |
||
612 | |||
613 | def dispatch_node(self, request, **kwargs): |
||
614 | node_resource = NodeResource() |
||
615 | return node_resource.dispatch_detail( |
||
616 | request, graph_id=kwargs['pk'], client_id=kwargs['client_id']) |
||
617 | |||
618 | def dispatch_nodegroups(self, request, **kwargs): |
||
619 | nodegroup_resource = NodeGroupResource() |
||
620 | return nodegroup_resource.dispatch_list(request, graph_id=kwargs['pk']) |
||
621 | |||
622 | def dispatch_nodegroup(self, request, **kwargs): |
||
623 | nodegroup_resource = NodeGroupResource() |
||
624 | return nodegroup_resource.dispatch_detail( |
||
625 | request, graph_id=kwargs['pk'], client_id=kwargs['client_id']) |
||
626 | |||
627 | def dispatch_jobs(self, request, **kwargs): |
||
628 | job_resource = JobResource() |
||
629 | return job_resource.dispatch_list(request, graph_id=kwargs['pk']) |
||
630 | |||
631 | def dispatch_job(self, request, **kwargs): |
||
632 | job_resource = JobResource() |
||
633 | return job_resource.dispatch_detail( |
||
634 | request, graph_id=kwargs['pk'], secret=kwargs['secret']) |
||
635 | |||
636 | def dispatch_results(self, request, **kwargs): |
||
637 | result_resource = ResultResource() |
||
638 | return result_resource.dispatch_list( |
||
639 | request, graph_id=kwargs['pk'], secret=kwargs['secret']) |
||
640 | |||
641 | |||
642 | class ResultResource(ModelResource): |
||
643 | |||
644 | """ |
||
645 | An API resource for results. |
||
646 | """ |
||
647 | |||
648 | class Meta: |
||
649 | queryset = Result.objects.all() |
||
650 | authorization = GraphOwnerAuthorization() |
||
651 | authentication = SessionAuthentication() |
||
652 | list_allowed_methods = ['get'] |
||
653 | |||
654 | def get_list(self, request, **kwargs): |
||
655 | """ |
||
656 | Called by the request dispatcher in case somebody tries to GET result resources |
||
657 | for a particular job. |
||
658 | """ |
||
659 | job = Job.objects.get( |
||
660 | secret=kwargs['secret'], |
||
661 | graph=kwargs['graph_id']) |
||
662 | |||
663 | if job.requires_download: |
||
664 | return job.result_download() |
||
665 | |||
666 | # It is an analysis result |
||
667 | |||
668 | # Determine options given by data tables |
||
669 | start = int( |
||
670 | request.GET.get( |
||
671 | 'iDisplayStart', |
||
672 | 0)) # Starts at 0, if given |
||
673 | length = int(request.GET.get('iDisplayLength', 10)) |
||
674 | sort_col_settings = int(request.GET.get('iSortingCols', 0)) |
||
675 | # Create sorted QuerySet |
||
676 | sort_fields = [] |
||
677 | for i in range(sort_col_settings): |
||
678 | # Consider strange datatables way of expressing sorting criteria |
||
679 | sort_col = int(request.GET['iSortCol_' + str(i)]) |
||
680 | sort_dir = request.GET['sSortDir_' + str(i)] |
||
681 | db_field_name = job.result_titles[sort_col][0] |
||
682 | logger.debug("Sorting result set for " + db_field_name) |
||
683 | if sort_dir == "desc": |
||
684 | db_field_name = "-" + db_field_name |
||
685 | sort_fields.append(db_field_name) |
||
686 | |||
687 | results = job.results.all().exclude(kind=Result.GRAPH_ISSUES) |
||
688 | if len(sort_fields) > 0: |
||
689 | results = results.order_by(*sort_fields) |
||
690 | all_count = results.count() |
||
691 | results = results[start:start + length] |
||
692 | |||
693 | assert ('sEcho' in request.GET) |
||
694 | response_data = {"sEcho": request.GET['sEcho'], "iTotalRecords": all_count, "iTotalDisplayRecords": all_count, |
||
695 | 'aaData': [result.to_dict() for result in results]} |
||
696 | logger.debug("Delivering result data: " + str(response_data)) |
||
697 | return HttpResponse( |
||
698 | json.dumps(response_data), content_type="application/json") |
||
699 |