Issues (34)

frontend/ore/views.py (1 issue)

Severity
1
import json
2
import logging
3
4
from django.contrib import auth, messages
5
from django.contrib.auth.decorators import login_required
6
from django.contrib.auth.models import User
7
from django.db.models import Q
8
from django.template import RequestContext
9
from django.http import HttpResponseBadRequest
10
from django.shortcuts import get_object_or_404, redirect, render
11
from django.views.decorators.http import require_http_methods
12
from django.http import Http404
13
14
from ore.models import Graph, Project, notations, Sharing
15
16
17
logger = logging.getLogger('ore')
18
19
GREETINGS = [
20
    'Loading the ORE cookie generator...',
21
    'Trying to find your Data... it was here somewhere',
22
    'Fiddeling with your Graph... Stand by!',
23
    'Loading good Karma into your Browser...',
24
    'Calculating the Answer to Life...',
25
    'Man, this takes like forever to load...',
26
    'This may take a moment, time to grab some coffee...'
27
]
28
29
30
def index(request):
31
    """
32
    Function: index
33
34
    This is the view handler for loading the landing page of the editor. The index.html is rendered unless the user is
35
    already signed in and being redirected to his or her dashboard.
36
37
    Parameters:
38
     {HttpRequest} request - a django request object
39
40
    Returns:
41
     {HttpResponse} a django response object
42
    """
43
    if 'logout' in request.GET:
44
        auth.logout(request)
45
46
    if request.user.is_authenticated():
47
        if 'next' in request.GET and len(request.GET['next']) > 0:
48
            return redirect(request.GET['next'])
49
        else:
50
            return redirect('projects')
51
52
    if 'next' in request.GET:
53
        # Makes this a hidden form parameter for the OpenID auth form
54
        # submission
55
        return render(request, 'index.html', {'next': request.GET['next'], 'pwlogin': ('pwlogin' in request.GET)},
56
                      context_instance=RequestContext(request))
57
    else:
58
        return render(request, 'index.html', {'pwlogin': ('pwlogin' in request.GET)},
59
                      context_instance=RequestContext(request))
60
61
62
def about(request):
63
    """
64
    Function: about
65
66
    Simple rendering of the about page.
67
68
    Parameters:
69
     {HttpRequest} request - a django request
70
71
    Returns:
72
     {HttpResponse} a django response object
73
    """
74
    return render(request, 'util/about.html')
75
76
77
def privacy(request):
78
    return render(request, 'util/privacy.html')
79
80
81
@login_required
82
def projects(request):
83
    """
84
    Function: projects
85
86
    This view handler renders a project overview containing all projects which are not marked as deleted, as well as in
87
    which the actual user is the owner or a project member. The resulting list of projects is ordered descending by its
88
    creation date. Also, a user is able to create a new project, as well as to delete a certain project if he is the owner.
89
90
    Parameters:
91
     {HttpRequest} request - a django request object
92
93
    Returns:
94
     {HttpResponse} a django response object
95
    """
96
    user = request.user
97
98
    projects = (
99
        user.projects.filter(
100
            deleted=False) | user.own_projects.filter(
101
            deleted=False)).order_by('-created')
102
103
    parameters = {'projects': [project.to_dict() for project in projects],
104
                  'user': user
105
                  }
106
107
    # provide notification box on the projects overview page, if something is
108
    # available for this user
109
    try:
110
        notification = request.user.notification_set.latest('modified')
111
        parameters['notification'] = notification
112
    except Exception:
113
        pass
114
115
    return render(request, 'project_menu/projects.html', parameters)
116
117
118
@login_required
119
def project_new(request):
120
    """
121
    Function: project_new
122
123
    This handler is responsible for rendering a dialog to the user to create a new project. It is also responsible for
124
    processing a save request of such a 'new project' request and forwards the user to the project overview site after doing so.
125
126
    Parameters:
127
     {HttpRequest} request - a django request object
128
129
    Returns:
130
     {HttpResponse} a django response object
131
    """
132
133
    if request.method == 'POST':
134
        project = Project(
135
            name=request.POST.get('name'),
136
            owner=request.user,
137
            deleted=False)
138
        project.save()
139
        return redirect('projects')
140
141
    return render(request, 'project_menu/project_new.html')
142
143
144
@login_required
145
def project_edit(request, project_id):
146
    """
147
    Function: project_edit
148
149
    This handler function is responsible for allowing the user to edit the properties of an already existing project.
150
    Therefore the system renders a edit dialog to the user where changes can be made and saved or the project can be
151
    deleted.
152
153
    Parameters:
154
     {HttpResponse} request    - a django request object
155
     {int}          project_id - the project to be edited
156
157
    Returns:
158
     {HttpResponse} a django response object
159
    """
160
    project = get_object_or_404(Project, pk=project_id, owner=request.user)
161
    POST = request.POST
162
163
    # deletion requested? do it and go back to project overview
164
    if POST.get('delete'):
165
        project.deleted = True
166
        project.save()
167
        messages.add_message(request, messages.SUCCESS, 'Project deleted.')
168
        return redirect('projects')
169
170
    # the owner made changes to the project's field, better save it (if we can)
171
    elif POST.get('save'):
172
        project.name = POST.get('name', '')
173
        project.save()
174
        messages.add_message(request, messages.SUCCESS, 'Project saved.')
175
        return redirect('projects')
176
177
    # please show the edit page to the user on get requests
178
    elif POST.get('edit') or request.method == 'GET':
179
        parameters = {'project': project.to_dict()}
180
        return render(request, 'project_menu/project_edit.html', parameters)
181
182
    # something was not quite right here
183
    raise HttpResponseBadRequest()
184
185
186
def shared_graphs_dashboard(request):
187
    """
188
    Function: shared_graphs
189
190
    This handler function is responsible for rendering a list of graphs that have been shared with the current user.
191
    Shared in this context means the user isn't the owner but is allowed to view a certain graph in read-only mode.
192
    The graphs are listed within a specific dashboard that offers the option to remove sharing of certain gaphs.
193
194
    Parameters:
195
     {HttpResponse} request  - a django request object
196
197
    Returns:
198
     {HttpResponse} a django response object
199
    """
200
    user = request.user
201
202
    if request.method == 'GET':
203
204
        sharings = user.sharings.all()
205
206
        if not sharings:
207
            return redirect('projects')
208
209
        shared_graphs = [sharing.graph for sharing in sharings]
210
211
        # projects in which the actual user is owner or member and that were
212
        # recently modified are proposed to the user
213
        proposal_limit = 3
214
        project_proposals = Project.objects.filter(Q(deleted=False), Q(
215
            users=request.user) | Q(owner=request.user)).order_by('-modified')[:proposal_limit]
216
217
        parameters = {'graphs': [(notations.by_kind[graph.kind]['name'], graph) for graph in shared_graphs],
218
                      'proposals': [project.to_dict() for project in project_proposals]
219
                      }
220
221
        return render(
222
            request, 'dashboard/shared_graphs_dashboard.html', parameters)
223
224
    elif request.method == 'POST':
225
        POST = request.POST
226
227
        if POST.get('unshare'):
228
229
            selected_graphs = POST.getlist('graph_id[]')
230
231
            sharings = [
232
                get_object_or_404(
233
                    Sharing,
234
                    user=user,
235
                    graph_id=graph_id) for graph_id in selected_graphs]
236
237
            for sharing in sharings:
238
                sharing.delete()
239
240
            return redirect('shared_graphs_dashboard')
241
242
    # something is not right with the request
243
    return HttpResponseBadRequest()
244
245
246
@login_required
247
def dashboard(request, project_id):
248
    """
249
    Function: dashboard
250
251
    This view handler renders the dashboard in the context of a certain project. It lists all the graphs belonging to the project that are not marked as
252
    deleted ordered descending by its creation date. Also, a user is able to add new graphs to the project, as well as to edit or delete
253
    existing graphs from here.
254
255
    Parameters:
256
     {HttpRequest} request - a django request object
257
258
    Returns:
259
     {HttpResponse} a django response object
260
    """
261
    project = get_object_or_404(Project, pk=project_id)
262
263
    if not (project.is_authorized(request.user)):
264
        raise Http404
265
266
    # projects in which the actual user is owner or member and that were
267
    # recently modified are proposed to the user
268
    proposal_limit = 3
269
    project_proposals = Project.objects.filter(Q(deleted=False), Q(users=request.user) | Q(
270
        owner=request.user)).exclude(id=project.id).order_by('-modified')[:proposal_limit]
271
272
    graphs = project.graphs.filter(deleted=False).order_by('-created')
273
    parameters = {'graphs': [(notations.by_kind[graph.kind]['name'], graph) for graph in graphs],
274
                  'project': project.to_dict(),
275
                  'proposals': [pr.to_dict() for pr in project_proposals],
276
                  'user': request.user
277
                  }
278
279
    return render(request, 'dashboard/dashboard.html', parameters)
280
281
282
@login_required
283
def dashboard_import(request, project_id):
284
    """
285
    Handles POST request for GraphML file import in the dashboard,
286
    """
287
    project = get_object_or_404(Project, pk=project_id)
288
289
    parameters = {
290
        'project': project.to_dict()
291
    }
292
293
    # user can only create a graph if he is owner or member of the respective
294
    # project
295
    if not (project.is_authorized(request.user)):
296
        raise Http404
297
298
    # import the graph
299
    if request.POST.get('save'):
300
        for name, f in request.FILES.iteritems():
301
            graph = Graph(owner=request.user, project=project)
302
            try:
303
                graph.from_graphml(f.read())
304
                graph.name = request.POST.get("title", "Imported graph")
305
                graph.save()
306
                graph.ensure_default_nodes()
307
                return redirect('dashboard', project_id=project_id)
308
            except Exception as e:
309
                parameters['error_text'] = str(e)
310
311
    return render(request, 'dashboard/dashboard_import.html', parameters)
312
313
314
@login_required
315
def dashboard_new(request, project_id, kind):
316
    """
317
    Function: dashboard_new
318
319
    This handler is responsible for rendering a dialog to the user to create a new diagram. It is also responsible for
320
    processing a save request of such a 'new diagram' request and forwards the user to the dashboard after doing so.
321
322
    Parameters:
323
     {HttpRequest} request - a django http request object
324
325
    Returns:
326
     {HttpResponse} a django response object
327
    """
328
    project = get_object_or_404(Project, pk=project_id)
329
330
    # user can only create a graph if he is owner or member of the respective
331
    # project
332
    if not (project.is_authorized(request.user)):
333
        raise Http404
334
335
    POST = request.POST
336
337
    # save the graph
338
    if POST.get('save') and POST.get('name'):
339
        graph = Graph(
340
            kind=kind,
341
            name=POST['name'],
342
            owner=request.user,
343
            project=project)
344
        graph.save()
345
        graph.ensure_default_nodes()
346
        return redirect('dashboard', project_id=project_id)
347
348
    # render the create diagram if fuzztree
349
    elif kind in notations.by_kind:
350
        parameters = {
351
            'kind': kind,
352
            'name': notations.by_kind[kind]['name'],
353
            'project': project.to_dict()
354
        }
355
        return render(request, 'dashboard/dashboard_new.html', parameters)
356
357
    elif kind == "from_graphml":
358
        # Redirect to file upload dialogue
359
        return render(request, 'dashboard/dashboard_import.html',
360
                      {'project': project.to_dict()})
361
362
    # something is not right with the request
363
    return HttpResponseBadRequest()
364
365
366
@login_required
367
def dashboard_edit(request, project_id):
368
    """
369
    Function: dashboard_edit
370
371
    This handler function is responsible for allowing the user to perform certain actions (copying, deleting, creating snapshots, sharing) on multiple graphs simultaneously.
372
    For this purpose a button toolbar is rendered in the view with which the user can submit a list of graphs in order to perform a specific action.
373
374
    Parameters:
375
     {HttpResponse} request    - a django request object
376
     {int}          project_id - id of the dashboard specific project
377
378
    Returns:
379
     {HttpResponse} a django response object
380
    """
381
    project = get_object_or_404(Project, pk=project_id)
382
383
    POST = request.POST
384
385
    # Save determination of chosen graphs
386
    if "graph_id[]" in POST:
387
        # Coming directly from a form with <select> entries
388
        selected_graphs = POST.getlist('graph_id[]')
389
    elif "graph_id_list" in POST:
390
        # Coming from a stringified list stored by ourselves
391
        selected_graphs = json.loads(POST.get('graph_id_list'))
392
    graphs = [
393
        get_object_or_404(
394
            Graph,
395
            pk=graph_id,
396
            owner=request.user,
397
            deleted=False) for graph_id in selected_graphs]
0 ignored issues
show
The variable selected_graphs does not seem to be defined for all execution paths.
Loading history...
398
399
    if POST.get('share'):
400
        # "Share" button pressed for one or multiple graphs
401
        users = User.objects.exclude(pk=request.user.pk)
402
        parameters = {
403
            'project': project,
404
            'users': users,
405
            'graph_id_list': json.dumps([graph.pk for graph in graphs])
406
        }
407
        return render(request, 'dashboard/dashboard_share.html', parameters)
408
409
    elif POST.get("share_save"):
410
        # Save choice of users for the graphs
411
        user_ids = POST.getlist('users')
412
        users = [get_object_or_404(User, pk=user_id) for user_id in user_ids]
413
414
        for graph in graphs:
415
            for user in users:
416
                # check if graph is already shared with the specific user
417
                if not Sharing.objects.filter(user=user, graph=graph).exists():
418
                    sharing = Sharing(graph=graph, user=user)
419
                    sharing.save()
420
            users_str = ','.join([u.visible_name() for u in users])
421
            messages.add_message(
422
                request, messages.SUCCESS, "'%s' shared with %s." %
423
                (graph, users_str))
424
        return redirect('dashboard', project_id=project.id)
425
426
    elif POST.get('copy'):
427
        # "Copy" button pressed for one or multiple graphs
428
        for old_graph in graphs:
429
            graph = Graph(
430
                kind=old_graph.kind,
431
                name=old_graph.name +
432
                ' (copy)',
433
                owner=request.user,
434
                project=project)
435
            graph.save()
436
            graph.copy_values(old_graph)
437
            graph.save()
438
        messages.add_message(
439
            request,
440
            messages.SUCCESS,
441
            'Duplication successful.')
442
        return redirect('dashboard', project_id=project.id)
443
444
    elif POST.get('snapshot'):
445
        # "Snapshot" button pressed for one or multiple graphs
446
        for old_graph in graphs:
447
            graph = Graph(
448
                kind=old_graph.kind,
449
                name=old_graph.name +
450
                ' (snapshot)',
451
                owner=request.user,
452
                project=project)
453
            graph.save()
454
            graph.copy_values(old_graph)
455
            graph.read_only = True
456
            graph.save()
457
        messages.add_message(
458
            request,
459
            messages.SUCCESS,
460
            'Snapshot creation sucessful.')
461
        return redirect('dashboard', project_id=project.id)
462
463
    elif POST.get('delete'):
464
        # "Delete" button pressed for one or multiple graphs
465
        for graph in graphs:
466
            # all graph sharings will be deleted irretrievably
467
            graph.sharings.all().delete()
468
            graph.deleted = True
469
            graph.save()
470
471
        messages.add_message(request, messages.SUCCESS, 'Deletion sucessful.')
472
        return redirect('dashboard', project_id=project.id)
473
474
    return HttpResponseBadRequest()
475
476
477
def graph_settings(request, graph_id):
478
    """
479
    Function: graph_settings
480
481
    This handler function is responsible for allowing the user to edit the properties of an already existing graph.
482
    Therefore the system renders a settings dialog to the user where changes can be made and saved for the graph.
483
484
    Parameters:
485
     {HttpResponse} request  - a django request object
486
     {int}          graph_id - the graph to be edited
487
488
    Returns:
489
     {HttpResponse} a django response object
490
    """
491
    graph = get_object_or_404(Graph, pk=graph_id, owner=request.user)
492
    project = get_object_or_404(
493
        Project,
494
        pk=graph.project.pk,
495
        owner=request.user)
496
497
    POST = request.POST
498
499
    # the owner made changes to the graph's fields, better save it (if we can)
500
    if POST.get('save'):
501
        # changes in the graphs name
502
        graph.name = POST.get('name', '')
503
        graph.save()
504
505
        # added/removed viewers from the graph
506
        user_ids = POST.getlist('users')
507
508
        new_users = set([get_object_or_404(User, pk=user_id)
509
                         for user_id in user_ids])
510
        old_users = set([sharing.user for sharing in graph.sharings.all()])
511
512
        users_to_add = new_users - old_users
513
        users_to_remove = old_users - new_users
514
515
        for user in users_to_add:
516
            sharing = Sharing(graph=graph, user=user)
517
            sharing.save()
518
519
        for user in users_to_remove:
520
            sharing = Sharing.objects.get(graph=graph, user=user)
521
            sharing.delete()
522
523
        messages.add_message(
524
            request,
525
            messages.SUCCESS,
526
            'Saved new graph settings.')
527
        return redirect('dashboard', project_id=project.pk)
528
529
    # please show the edit page to the user on get requests
530
    elif POST.get('edit') or request.method == 'GET':
531
532
        users = User.objects.exclude(pk=request.user.pk)
533
        shared_users = [sharing.user for sharing in graph.sharings.all()]
534
535
        parameters = {
536
            'graph': graph,
537
            'kind': notations.by_kind[graph.kind]['name'],
538
            'users': users,
539
            'shared_users': shared_users
540
        }
541
        return render(request, 'dashboard/dashboard_edit.html', parameters)
542
543
    # something was not quite right here
544
    return HttpResponseBadRequest()
545
546
547
@login_required
548
def settings(request):
549
    """
550
    Function: settings
551
552
    The view for the settings page. The code remembers the last page (e.g. project overview or project details) and goes
553
    backe to it afterwards.
554
555
    Parameters:
556
     {HttpRequest} request - a django request object
557
558
    Returns:
559
     {HttpResponse} a django response object
560
    """
561
    try:
562
        comes_from = request.META["HTTP_REFERER"]
563
        if 'settings' not in comes_from:
564
            request.session['comes_from'] = comes_from
565
    except Exception:
566
        # deal with missing REFERER
567
        request.session['comes_from'] = '/projects/'
568
    POST = request.POST
569
570
    if POST.get('save'):
571
        user = request.user
572
        profile = user.profile
573
574
        user.first_name = POST.get('first_name', user.first_name)
575
        user.last_name = POST.get('last_name', user.last_name)
576
        user.email = POST.get('email', user.email)
577
        profile.newsletter = bool(POST.get('newsletter'))
578
579
        profile.save()
580
        user.save()
581
582
        messages.add_message(request, messages.SUCCESS, 'Settings saved.')
583
        return redirect(request.session['comes_from'])
584
    elif POST.get('generate'):
585
        from tastypie.models import ApiKey
586
        user = request.user
587
        # User may be new, without any previous API key
588
        ApiKey.objects.get_or_create(user=user, defaults={'user': user})
589
        # Save new API key
590
        user.api_key.key = user.api_key.generate_key()
591
        user.api_key.save()
592
    elif POST.get('cancel'):
593
        return redirect(request.session['comes_from'])
594
595
    return render(request, 'util/settings.html')
596
597
598
@login_required
599
def editor(request, graph_id):
600
    """
601
    Function: editor
602
603
    View handler for loading the editor. It just tries to locate the graph to be opened in the editor and passes it to
604
    its according view.
605
606
    Parameters:
607
     {HttpRequest} request  - a django request object
608
     {int}         graph_id - the id of the graph to be opened in the editor
609
610
    Returns:
611
     {HttpResponse} a django response object
612
    """
613
    if request.user.is_staff:
614
        graph = get_object_or_404(Graph, pk=graph_id)
615
    else:
616
        graph = get_object_or_404(
617
            Graph,
618
            pk=graph_id,
619
            owner=request.user,
620
            deleted=False)
621
    if graph.read_only:
622
        return HttpResponseBadRequest()
623
624
    project = graph.project
625
    notation = notations.by_kind[graph.kind]
626
    nodes = notation['nodes']
627
628
    parameters = {
629
        'graph': graph,
630
        'graph_notation': notation,
631
        'nodes': [(node, nodes[node]) for node in notation['shapeMenuNodeDisplayOrder']],
632
        'greetings': GREETINGS,
633
        'project': project,
634
        'user': request.user
635
    }
636
637
    return render(request, 'editor/editor.html', parameters)
638
639
640
@login_required
641
def snapshot(request, graph_id):
642
    """
643
    Function: snapshot
644
645
    View handler for loading the snapshot viewer. It just tries to locate the graph to be opened
646
    and passes it to its according view. For the moment, this is the editor itself, were the JavaScript
647
    code handles the read-only mode from UI perspective.
648
649
    Parameters:
650
     {HttpRequest} request  - a django request object
651
     {int}         graph_id - the id of the graph to be opened in the snapshot viewer
652
653
    Returns:
654
     {HttpResponse} a django response object
655
    """
656
657
    graph = get_object_or_404(Graph, pk=graph_id)
658
659
    # either current user is admin, owner of the graph, or graph is shared
660
    # with the user
661
    if not (request.user.is_staff or graph.owner ==
662
            request.user or graph.sharings.filter(user=request.user)):
663
        raise Http404
664
665
    project = graph.project
666
    notation = notations.by_kind[graph.kind]
667
    nodes = notation['nodes']
668
669
    parameters = {
670
        'graph': graph,
671
        'graph_notation': notation,
672
        'nodes': [(node, nodes[node]) for node in notation['shapeMenuNodeDisplayOrder']],
673
        'greetings': GREETINGS,
674
        'project': project,
675
        'user': request.user
676
    }
677
678
    return render(request, 'editor/editor.html', parameters)
679
680
681
@require_http_methods(['GET', 'POST'])
682
def login(request):
683
    """
684
    Function: login
685
686
    View handler for loging in a user using OpenID. If the user is not yet know to the system a new profile is created
687
    for him using his or her personal information as provided by the OpenID provider.
688
689
    The login view always redirects to the index view - and not directly to the project view - in order to keep
690
    the 'next' parameter handling in one place. Otherwise, the project view would need to consider the parameter too.
691
692
    Parameters:
693
     {HttpRequest} request - a django request object
694
695
    Returns:
696
     {HttpResponse} a django response object
697
    """
698
    POST = request.POST
699
700
    # Consider the 'next' redirection
701
    if 'next' in POST:
702
        # TODO: Security issue ?
703
        redirect_params = '?next=' + POST['next']
704
    else:
705
        redirect_params = ''
706
707
    # Ordinary password login. Since this is normally disabled in favour of OpenID login, all such users
708
    # got garbage passwords. This means that this code can remain in here as last ressort fallback for
709
    # the admin users that have real passwords.
710
    if 'username' in POST and 'password' in POST:
711
        user = auth.authenticate(
712
            username=POST['username'],
713
            password=POST['password'])
714
        # user found? sign-on
715
        if user is not None and user.is_active:
716
            auth.login(request, user)
717
        return redirect('/projects/' + redirect_params)
718
719
    return redirect('/' + redirect_params)
720