Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (21.5 kB)

1
# Copyright 2011, 2012, 2013 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.views.util import render_response, _create_object, \
70
    _update_object, _resources_catalog, ExceptionHandler
71
from astakos.im.views.decorators import cookie_fix, signed_terms_required,\
72
    valid_astakos_user_required
73

    
74
logger = logging.getLogger(__name__)
75

    
76

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

    
83

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

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

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

    
127
    if response is not None:
128
        return response
129

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

    
134

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

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

    
153

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

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

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

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

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

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

    
188

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

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

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

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

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

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

    
245
    if response is not None:
246
        return response
247

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

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

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

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

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

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

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

    
307
        else:
308
            members_table = None
309

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

    
316
    modifications_table = None
317

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

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

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

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

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

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

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

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

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

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

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

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

    
411
    with ExceptionHandler(request):
412
        _project_join(request, chain_id)
413

    
414

    
415
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
416
    return redirect(next)
417

    
418

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

    
432

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

    
441
    with ExceptionHandler(request):
442
        _project_leave(request, chain_id)
443

    
444
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
445
    return redirect(next)
446

    
447

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

    
461

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

    
470
    with ExceptionHandler(request):
471
        _project_cancel(request, chain_id)
472

    
473
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
474
    return redirect(next)
475

    
476

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

    
487

    
488

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

    
494
    with ExceptionHandler(request):
495
        _project_accept_member(request, chain_id, memb_id)
496

    
497
    return redirect(reverse('project_detail', args=(chain_id,)))
498

    
499

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

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

    
514

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

    
520
    with ExceptionHandler(request):
521
        _project_remove_member(request, chain_id, memb_id)
522

    
523
    return redirect(reverse('project_detail', args=(chain_id,)))
524

    
525

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

    
539

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

    
545
    with ExceptionHandler(request):
546
        _project_reject_member(request, chain_id, memb_id)
547

    
548
    return redirect(reverse('project_detail', args=(chain_id,)))
549

    
550

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

    
564

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

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

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

    
580
    with ExceptionHandler(request):
581
        _project_app_approve(request, application_id)
582

    
583
    chain_id = get_related_project_id(application_id)
584
    return redirect(reverse('project_detail', args=(chain_id,)))
585

    
586

    
587
@commit_on_success_strict()
588
def _project_app_approve(request, application_id):
589
    approve_application(application_id)
590

    
591

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

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

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

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

    
611
    with ExceptionHandler(request):
612
        _project_app_deny(request, application_id, reason)
613

    
614
    return redirect(reverse('project_list'))
615

    
616

    
617
@commit_on_success_strict()
618
def _project_app_deny(request, application_id, reason):
619
    deny_application(application_id, reason=reason)
620

    
621

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

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

    
636
    with ExceptionHandler(request):
637
        _project_app_dismiss(request, application_id)
638

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

    
647

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