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