ore.api.frontend.NodeResource.get_resource_uri()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 13
rs 10
c 0
b 0
f 0
cc 1
nop 2
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):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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