Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views / projects.py @ 9cd9bfe7

History | View | Annotate | Download (21.4 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import logging
35
import inflect
36

    
37
engine = inflect.engine()
38

    
39
from django_tables2 import RequestConfig
40

    
41
from django.shortcuts import get_object_or_404
42
from django.contrib import messages
43
from django.contrib.auth.decorators import login_required
44
from django.core.urlresolvers import reverse
45
from django.http import Http404
46
from django.shortcuts import redirect
47
from django.utils.html import escape
48
from django.utils.translation import ugettext as _
49
from django.views.generic.list_detail import object_list, object_detail
50
from django.core.exceptions import PermissionDenied
51
from django.views.decorators.http import require_http_methods
52
from django.db.models import Q
53

    
54
from snf_django.lib.db.transaction import commit_on_success_strict
55

    
56
import astakos.im.messages as astakos_messages
57

    
58
from astakos.im import tables
59
from astakos.im.models import ProjectApplication
60
from astakos.im.util import get_context, restrict_next
61
from astakos.im.forms import ProjectApplicationForm, AddProjectMembersForm, \
62
    ProjectSearchForm
63
from astakos.im.functions import check_pending_app_quota, accept_membership, \
64
    reject_membership, remove_membership, cancel_membership, leave_project, \
65
    join_project, enroll_member, can_join_request, can_leave_request, \
66
    get_related_project_id, get_by_chain_or_404, approve_application, \
67
    deny_application, cancel_application, dismiss_application
68
from astakos.im import settings
69
from astakos.im.ctx import ExceptionHandler
70
from astakos.im.views.util import render_response, _create_object, \
71
    _update_object, _resources_catalog
72
from astakos.im.views.decorators import cookie_fix, signed_terms_required,\
73
    valid_astakos_user_required
74

    
75
logger = logging.getLogger(__name__)
76

    
77

    
78
@cookie_fix
79
def how_it_works(request):
80
    return render_response(
81
        'im/how_it_works.html',
82
        context_instance=get_context(request))
83

    
84

    
85
@require_http_methods(["GET", "POST"])
86
@cookie_fix
87
@valid_astakos_user_required
88
def project_add(request):
89
    user = request.user
90
    if not user.is_project_admin():
91
        ok, limit = check_pending_app_quota(user)
92
        if not ok:
93
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_ADD) % limit
94
            messages.error(request, m)
95
            next = reverse('astakos.im.views.project_list')
96
            next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
97
            return redirect(next)
98

    
99
    details_fields = ["name", "homepage", "description", "start_date",
100
                      "end_date", "comments"]
101
    membership_fields = ["member_join_policy", "member_leave_policy",
102
                         "limit_on_members_number"]
103
    resource_catalog, resource_groups = _resources_catalog(for_project=True)
104
    if resource_catalog is False:
105
        # on fail resource_groups contains the result object
106
        result = resource_groups
107
        messages.error(request, 'Unable to retrieve system resources: %s' %
108
                       result.reason)
109
    extra_context = {
110
        'resource_catalog': resource_catalog,
111
        'resource_groups': resource_groups,
112
        'show_form': True,
113
        'details_fields': details_fields,
114
        'membership_fields': membership_fields}
115

    
116
    response = None
117
    with ExceptionHandler(request):
118
        response = _create_object(
119
            request,
120
            template_name='im/projects/projectapplication_form.html',
121
            extra_context=extra_context,
122
            post_save_redirect=reverse('project_list'),
123
            form_class=ProjectApplicationForm,
124
            msg=_("The %(verbose_name)s has been received and "
125
                  "is under consideration."),
126
            )
127

    
128
    if response is not None:
129
        return response
130

    
131
    next = reverse('astakos.im.views.project_list')
132
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
133
    return redirect(next)
134

    
135

    
136
@require_http_methods(["GET"])
137
@cookie_fix
138
@valid_astakos_user_required
139
def project_list(request):
140
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
141
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
142
                                                prefix="my_projects_")
143
    RequestConfig(request, paginate={"per_page": settings.PAGINATE_BY}).configure(table)
144

    
145
    return object_list(
146
        request,
147
        projects,
148
        template_name='im/projects/project_list.html',
149
        extra_context={
150
            'is_search':False,
151
            'table': table,
152
        })
153

    
154

    
155
@require_http_methods(["POST"])
156
@cookie_fix
157
@valid_astakos_user_required
158
def project_app_cancel(request, application_id):
159
    next = request.GET.get('next')
160
    chain_id = None
161

    
162
    with ExceptionHandler(request):
163
        chain_id = _project_app_cancel(request, application_id)
164

    
165
    if not next:
166
        if chain_id:
167
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
168
        else:
169
            next = reverse('astakos.im.views.project_list')
170

    
171
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
172
    return redirect(next)
173

    
174
@commit_on_success_strict()
175
def _project_app_cancel(request, application_id):
176
    chain_id = None
177
    try:
178
        application_id = int(application_id)
179
        chain_id = get_related_project_id(application_id)
180
        cancel_application(application_id, request.user)
181
    except (IOError, PermissionDenied), e:
182
        messages.error(request, e)
183

    
184
    else:
185
        msg = _(astakos_messages.APPLICATION_CANCELLED)
186
        messages.success(request, msg)
187
        return chain_id
188

    
189

    
190
@require_http_methods(["GET", "POST"])
191
@cookie_fix
192
@valid_astakos_user_required
193
def project_modify(request, application_id):
194

    
195
    try:
196
        app = ProjectApplication.objects.get(id=application_id)
197
    except ProjectApplication.DoesNotExist:
198
        raise Http404
199

    
200
    user = request.user
201
    if not (user.owns_application(app) or user.is_project_admin(app.id)):
202
        m = _(astakos_messages.NOT_ALLOWED)
203
        raise PermissionDenied(m)
204

    
205
    if not user.is_project_admin():
206
        owner = app.owner
207
        ok, limit = check_pending_app_quota(owner, precursor=app)
208
        if not ok:
209
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
210
            messages.error(request, m)
211
            next = reverse('astakos.im.views.project_list')
212
            next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
213
            return redirect(next)
214

    
215
    details_fields = ["name", "homepage", "description", "start_date",
216
                      "end_date", "comments"]
217
    membership_fields = ["member_join_policy", "member_leave_policy",
218
                         "limit_on_members_number"]
219
    resource_catalog, resource_groups = _resources_catalog(for_project=True)
220
    if resource_catalog is False:
221
        # on fail resource_groups contains the result object
222
        result = resource_groups
223
        messages.error(request, 'Unable to retrieve system resources: %s' %
224
                       result.reason)
225
    extra_context = {
226
        'resource_catalog': resource_catalog,
227
        'resource_groups': resource_groups,
228
        'show_form': True,
229
        'details_fields': details_fields,
230
        'update_form': True,
231
        'membership_fields': membership_fields
232
    }
233

    
234
    response = None
235
    with ExceptionHandler(request):
236
        response = _update_object(
237
            request,
238
            object_id=application_id,
239
            template_name='im/projects/projectapplication_form.html',
240
            extra_context=extra_context,
241
            post_save_redirect=reverse('project_list'),
242
            form_class=ProjectApplicationForm,
243
            msg=_("The %(verbose_name)s has been received and is under "
244
                  "consideration."))
245

    
246
    if response is not None:
247
        return response
248

    
249
    next = reverse('astakos.im.views.project_list')
250
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
251
    return redirect(next)
252

    
253
@require_http_methods(["GET", "POST"])
254
@cookie_fix
255
@valid_astakos_user_required
256
def project_app(request, application_id):
257
    return common_detail(request, application_id, project_view=False)
258

    
259
@require_http_methods(["GET", "POST"])
260
@cookie_fix
261
@valid_astakos_user_required
262
def project_detail(request, chain_id):
263
    return common_detail(request, chain_id)
264

    
265
@commit_on_success_strict()
266
def addmembers(request, chain_id, addmembers_form):
267
    if addmembers_form.is_valid():
268
        try:
269
            chain_id = int(chain_id)
270
            map(lambda u: enroll_member(
271
                    chain_id,
272
                    u,
273
                    request_user=request.user),
274
                addmembers_form.valid_users)
275
        except (IOError, PermissionDenied), e:
276
            messages.error(request, e)
277

    
278
def common_detail(request, chain_or_app_id, project_view=True):
279
    project = None
280
    if project_view:
281
        chain_id = chain_or_app_id
282
        if request.method == 'POST':
283
            addmembers_form = AddProjectMembersForm(
284
                request.POST,
285
                chain_id=int(chain_id),
286
                request_user=request.user)
287
            with ExceptionHandler(request):
288
                addmembers(request, chain_id, addmembers_form)
289

    
290
            if addmembers_form.is_valid():
291
                addmembers_form = AddProjectMembersForm()  # clear form data
292
        else:
293
            addmembers_form = AddProjectMembersForm()  # initialize form
294

    
295
        project, application = get_by_chain_or_404(chain_id)
296
        if project:
297
            members = project.projectmembership_set.select_related()
298
            members_table = tables.ProjectMembersTable(project,
299
                                                       members,
300
                                                       user=request.user,
301
                                                       prefix="members_")
302
            RequestConfig(request, paginate={"per_page": settings.PAGINATE_BY}
303
                          ).configure(members_table)
304

    
305
        else:
306
            members_table = None
307

    
308
    else: # is application
309
        application_id = chain_or_app_id
310
        application = get_object_or_404(ProjectApplication, pk=application_id)
311
        members_table = None
312
        addmembers_form = None
313

    
314
    modifications_table = None
315

    
316
    user = request.user
317
    is_project_admin = user.is_project_admin(application_id=application.id)
318
    is_owner = user.owns_application(application)
319
    if not (is_owner or is_project_admin) and not project_view:
320
        m = _(astakos_messages.NOT_ALLOWED)
321
        raise PermissionDenied(m)
322

    
323
    if (not (is_owner or is_project_admin) and project_view and
324
        not user.non_owner_can_view(project)):
325
        m = _(astakos_messages.NOT_ALLOWED)
326
        raise PermissionDenied(m)
327

    
328
    following_applications = list(application.pending_modifications())
329
    following_applications.reverse()
330
    modifications_table = (
331
        tables.ProjectModificationApplicationsTable(following_applications,
332
                                                    user=request.user,
333
                                                    prefix="modifications_"))
334

    
335
    mem_display = user.membership_display(project) if project else None
336
    can_join_req = can_join_request(project, user) if project else False
337
    can_leave_req = can_leave_request(project, user) if project else False
338

    
339
    return object_detail(
340
        request,
341
        queryset=ProjectApplication.objects.select_related(),
342
        object_id=application.id,
343
        template_name='im/projects/project_detail.html',
344
        extra_context={
345
            'project_view': project_view,
346
            'addmembers_form':addmembers_form,
347
            'members_table': members_table,
348
            'owner_mode': is_owner,
349
            'admin_mode': is_project_admin,
350
            'modifications_table': modifications_table,
351
            'mem_display': mem_display,
352
            'can_join_request': can_join_req,
353
            'can_leave_request': can_leave_req,
354
            })
355

    
356
@require_http_methods(["GET", "POST"])
357
@cookie_fix
358
@valid_astakos_user_required
359
def project_search(request):
360
    q = request.GET.get('q', '')
361
    form = ProjectSearchForm()
362
    q = q.strip()
363

    
364
    if request.method == "POST":
365
        form = ProjectSearchForm(request.POST)
366
        if form.is_valid():
367
            q = form.cleaned_data['q'].strip()
368
        else:
369
            q = None
370

    
371
    if q is None:
372
        projects = ProjectApplication.objects.none()
373
    else:
374
        accepted_projects = request.user.projectmembership_set.filter(
375
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
376
        projects = ProjectApplication.objects.search_by_name(q)
377
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
378
        projects = projects.exclude(project__in=accepted_projects)
379

    
380
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
381
                                                prefix="my_projects_")
382
    if request.method == "POST":
383
        table.caption = _('SEARCH RESULTS')
384
    else:
385
        table.caption = _('ALL PROJECTS')
386

    
387
    RequestConfig(request, paginate={"per_page": settings.PAGINATE_BY}).configure(table)
388

    
389
    return object_list(
390
        request,
391
        projects,
392
        template_name='im/projects/project_list.html',
393
        extra_context={
394
          'form': form,
395
          'is_search': True,
396
          'q': q,
397
          'table': table
398
        })
399

    
400
@require_http_methods(["POST"])
401
@cookie_fix
402
@valid_astakos_user_required
403
def project_join(request, chain_id):
404
    next = request.GET.get('next')
405
    if not next:
406
        next = reverse('astakos.im.views.project_detail',
407
                       args=(chain_id,))
408

    
409
    with ExceptionHandler(request):
410
        _project_join(request, chain_id)
411

    
412

    
413
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
414
    return redirect(next)
415

    
416

    
417
@commit_on_success_strict()
418
def _project_join(request, chain_id):
419
    try:
420
        chain_id = int(chain_id)
421
        auto_accepted = join_project(chain_id, request.user)
422
        if auto_accepted:
423
            m = _(astakos_messages.USER_JOINED_PROJECT)
424
        else:
425
            m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
426
        messages.success(request, m)
427
    except (IOError, PermissionDenied), e:
428
        messages.error(request, e)
429

    
430

    
431
@require_http_methods(["POST"])
432
@cookie_fix
433
@valid_astakos_user_required
434
def project_leave(request, chain_id):
435
    next = request.GET.get('next')
436
    if not next:
437
        next = reverse('astakos.im.views.project_list')
438

    
439
    with ExceptionHandler(request):
440
        _project_leave(request, chain_id)
441

    
442
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
443
    return redirect(next)
444

    
445

    
446
@commit_on_success_strict()
447
def _project_leave(request, chain_id):
448
    try:
449
        chain_id = int(chain_id)
450
        auto_accepted = leave_project(chain_id, request.user)
451
        if auto_accepted:
452
            m = _(astakos_messages.USER_LEFT_PROJECT)
453
        else:
454
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
455
        messages.success(request, m)
456
    except (IOError, PermissionDenied), e:
457
        messages.error(request, e)
458

    
459

    
460
@require_http_methods(["POST"])
461
@cookie_fix
462
@valid_astakos_user_required
463
def project_cancel(request, chain_id):
464
    next = request.GET.get('next')
465
    if not next:
466
        next = reverse('astakos.im.views.project_list')
467

    
468
    with ExceptionHandler(request):
469
        _project_cancel(request, chain_id)
470

    
471
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
472
    return redirect(next)
473

    
474

    
475
@commit_on_success_strict()
476
def _project_cancel(request, chain_id):
477
    try:
478
        chain_id = int(chain_id)
479
        cancel_membership(chain_id, request.user)
480
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
481
        messages.success(request, m)
482
    except (IOError, PermissionDenied), e:
483
        messages.error(request, e)
484

    
485

    
486

    
487
@require_http_methods(["POST"])
488
@cookie_fix
489
@valid_astakos_user_required
490
def project_accept_member(request, chain_id, memb_id):
491

    
492
    with ExceptionHandler(request):
493
        _project_accept_member(request, chain_id, memb_id)
494

    
495
    return redirect(reverse('project_detail', args=(chain_id,)))
496

    
497

    
498
@commit_on_success_strict()
499
def _project_accept_member(request, chain_id, memb_id):
500
    try:
501
        chain_id = int(chain_id)
502
        memb_id = int(memb_id)
503
        m = accept_membership(chain_id, memb_id, request.user)
504
    except (IOError, PermissionDenied), e:
505
        messages.error(request, e)
506

    
507
    else:
508
        email = escape(m.person.email)
509
        msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
510
        messages.success(request, msg)
511

    
512

    
513
@require_http_methods(["POST"])
514
@cookie_fix
515
@valid_astakos_user_required
516
def project_remove_member(request, chain_id, memb_id):
517

    
518
    with ExceptionHandler(request):
519
        _project_remove_member(request, chain_id, memb_id)
520

    
521
    return redirect(reverse('project_detail', args=(chain_id,)))
522

    
523

    
524
@commit_on_success_strict()
525
def _project_remove_member(request, chain_id, memb_id):
526
    try:
527
        chain_id = int(chain_id)
528
        memb_id = int(memb_id)
529
        m = remove_membership(chain_id, memb_id, request.user)
530
    except (IOError, PermissionDenied), e:
531
        messages.error(request, e)
532
    else:
533
        email = escape(m.person.email)
534
        msg = _(astakos_messages.USER_MEMBERSHIP_REMOVED) % email
535
        messages.success(request, msg)
536

    
537

    
538
@require_http_methods(["POST"])
539
@cookie_fix
540
@valid_astakos_user_required
541
def project_reject_member(request, chain_id, memb_id):
542

    
543
    with ExceptionHandler(request):
544
        _project_reject_member(request, chain_id, memb_id)
545

    
546
    return redirect(reverse('project_detail', args=(chain_id,)))
547

    
548

    
549
@commit_on_success_strict()
550
def _project_reject_member(request, chain_id, memb_id):
551
    try:
552
        chain_id = int(chain_id)
553
        memb_id = int(memb_id)
554
        m = reject_membership(chain_id, memb_id, request.user)
555
    except (IOError, PermissionDenied), e:
556
        messages.error(request, e)
557
    else:
558
        email = escape(m.person.email)
559
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email
560
        messages.success(request, msg)
561

    
562

    
563
@require_http_methods(["POST"])
564
@signed_terms_required
565
@login_required
566
@cookie_fix
567
def project_app_approve(request, application_id):
568

    
569
    if not request.user.is_project_admin():
570
        m = _(astakos_messages.NOT_ALLOWED)
571
        raise PermissionDenied(m)
572

    
573
    try:
574
        app = ProjectApplication.objects.get(id=application_id)
575
    except ProjectApplication.DoesNotExist:
576
        raise Http404
577

    
578
    with ExceptionHandler(request):
579
        _project_app_approve(request, application_id)
580

    
581
    chain_id = get_related_project_id(application_id)
582
    return redirect(reverse('project_detail', args=(chain_id,)))
583

    
584

    
585
@commit_on_success_strict()
586
def _project_app_approve(request, application_id):
587
    approve_application(application_id)
588

    
589

    
590
@require_http_methods(["POST"])
591
@signed_terms_required
592
@login_required
593
@cookie_fix
594
def project_app_deny(request, application_id):
595

    
596
    reason = request.POST.get('reason', None)
597
    if not reason:
598
        reason = None
599

    
600
    if not request.user.is_project_admin():
601
        m = _(astakos_messages.NOT_ALLOWED)
602
        raise PermissionDenied(m)
603

    
604
    try:
605
        app = ProjectApplication.objects.get(id=application_id)
606
    except ProjectApplication.DoesNotExist:
607
        raise Http404
608

    
609
    with ExceptionHandler(request):
610
        _project_app_deny(request, application_id, reason)
611

    
612
    return redirect(reverse('project_list'))
613

    
614

    
615
@commit_on_success_strict()
616
def _project_app_deny(request, application_id, reason):
617
    deny_application(application_id, reason=reason)
618

    
619

    
620
@require_http_methods(["POST"])
621
@signed_terms_required
622
@login_required
623
@cookie_fix
624
def project_app_dismiss(request, application_id):
625
    try:
626
        app = ProjectApplication.objects.get(id=application_id)
627
    except ProjectApplication.DoesNotExist:
628
        raise Http404
629

    
630
    if not request.user.owns_application(app):
631
        m = _(astakos_messages.NOT_ALLOWED)
632
        raise PermissionDenied(m)
633

    
634
    with ExceptionHandler(request):
635
        _project_app_dismiss(request, application_id)
636

    
637
    chain_id = None
638
    chain_id = get_related_project_id(application_id)
639
    if chain_id:
640
        next = reverse('project_detail', args=(chain_id,))
641
    else:
642
        next = reverse('project_list')
643
    return redirect(next)
644

    
645

    
646
def _project_app_dismiss(request, application_id):
647
    # XXX: dismiss application also does authorization
648
    dismiss_application(application_id, request_user=request.user)