Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views / projects.py @ 0b058d6f

History | View | Annotate | Download (22.7 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, ProjectMembership
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

    
278
def common_detail(request, chain_or_app_id, project_view=True,
279
                  template_name='im/projects/project_detail.html',
280
                  members_status_filter=None):
281
    project = None
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
        approved_members_count = 0
298
        pending_members_count = 0
299
        remaining_memberships_count = 0
300
        project, application = get_by_chain_or_404(chain_id)
301
        if project:
302
            members = project.projectmembership_set.select_related()
303
            approved_members_count = \
304
                    project.count_actually_accepted_memberships()
305
            pending_members_count = project.count_pending_memberships()
306
            if members_status_filter in (ProjectMembership.REQUESTED,
307
                ProjectMembership.ACCEPTED):
308
                members = members.filter(state=members_status_filter)
309
            members_table = tables.ProjectMembersTable(project,
310
                                                       members,
311
                                                       user=request.user,
312
                                                       prefix="members_")
313
            RequestConfig(request, paginate={"per_page": settings.PAGINATE_BY}
314
                          ).configure(members_table)
315

    
316
        else:
317
            members_table = None
318

    
319
    else:
320
        # is application
321
        application_id = chain_or_app_id
322
        application = get_object_or_404(ProjectApplication, pk=application_id)
323
        members_table = None
324
        addmembers_form = None
325

    
326
    modifications_table = None
327

    
328
    user = request.user
329
    is_project_admin = user.is_project_admin(application_id=application.id)
330
    is_owner = user.owns_application(application)
331
    if not (is_owner or is_project_admin) and not project_view:
332
        m = _(astakos_messages.NOT_ALLOWED)
333
        raise PermissionDenied(m)
334

    
335
    if (not (is_owner or is_project_admin) and project_view and
336
        not user.non_owner_can_view(project)):
337
        m = _(astakos_messages.NOT_ALLOWED)
338
        raise PermissionDenied(m)
339

    
340
    following_applications = list(application.pending_modifications())
341
    following_applications.reverse()
342
    modifications_table = (
343
        tables.ProjectModificationApplicationsTable(following_applications,
344
                                                    user=request.user,
345
                                                    prefix="modifications_"))
346

    
347
    mem_display = user.membership_display(project) if project else None
348
    can_join_req = can_join_request(project, user) if project else False
349
    can_leave_req = can_leave_request(project, user) if project else False
350

    
351
    return object_detail(
352
        request,
353
        queryset=ProjectApplication.objects.select_related(),
354
        object_id=application.id,
355
        template_name=template_name,
356
        extra_context={
357
            'project_view': project_view,
358
            'addmembers_form': addmembers_form,
359
            'approved_members_count': approved_members_count,
360
            'pending_members_count': pending_members_count,
361
            'members_table': members_table,
362
            'owner_mode': is_owner,
363
            'admin_mode': is_project_admin,
364
            'modifications_table': modifications_table,
365
            'mem_display': mem_display,
366
            'can_join_request': can_join_req,
367
            'can_leave_request': can_leave_req,
368
            'members_status_filter':members_status_filter,
369
            })
370

    
371
@require_http_methods(["GET", "POST"])
372
@cookie_fix
373
@valid_astakos_user_required
374
def project_search(request):
375
    q = request.GET.get('q', '')
376
    form = ProjectSearchForm()
377
    q = q.strip()
378

    
379
    if request.method == "POST":
380
        form = ProjectSearchForm(request.POST)
381
        if form.is_valid():
382
            q = form.cleaned_data['q'].strip()
383
        else:
384
            q = None
385

    
386
    if q is None:
387
        projects = ProjectApplication.objects.none()
388
    else:
389
        accepted_projects = request.user.projectmembership_set.filter(
390
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
391
        projects = ProjectApplication.objects.search_by_name(q)
392
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
393
        projects = projects.exclude(project__in=accepted_projects)
394

    
395
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
396
                                                prefix="my_projects_")
397
    if request.method == "POST":
398
        table.caption = _('SEARCH RESULTS')
399
    else:
400
        table.caption = _('ALL PROJECTS')
401

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

    
404
    return object_list(
405
        request,
406
        projects,
407
        template_name='im/projects/project_list.html',
408
        extra_context={
409
          'form': form,
410
          'is_search': True,
411
          'q': q,
412
          'table': table
413
        })
414

    
415
@require_http_methods(["POST"])
416
@cookie_fix
417
@valid_astakos_user_required
418
def project_join(request, chain_id):
419
    next = request.GET.get('next')
420
    if not next:
421
        next = reverse('astakos.im.views.project_detail',
422
                       args=(chain_id,))
423

    
424
    with ExceptionHandler(request):
425
        _project_join(request, chain_id)
426

    
427

    
428
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
429
    return redirect(next)
430

    
431

    
432
@commit_on_success_strict()
433
def _project_join(request, chain_id):
434
    try:
435
        chain_id = int(chain_id)
436
        auto_accepted = join_project(chain_id, request.user)
437
        if auto_accepted:
438
            m = _(astakos_messages.USER_JOINED_PROJECT)
439
        else:
440
            m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
441
        messages.success(request, m)
442
    except (IOError, PermissionDenied), e:
443
        messages.error(request, e)
444

    
445

    
446
@require_http_methods(["POST"])
447
@cookie_fix
448
@valid_astakos_user_required
449
def project_leave(request, chain_id):
450
    next = request.GET.get('next')
451
    if not next:
452
        next = reverse('astakos.im.views.project_list')
453

    
454
    with ExceptionHandler(request):
455
        _project_leave(request, chain_id)
456

    
457
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
458
    return redirect(next)
459

    
460

    
461
@commit_on_success_strict()
462
def _project_leave(request, chain_id):
463
    try:
464
        chain_id = int(chain_id)
465
        auto_accepted = leave_project(chain_id, request.user)
466
        if auto_accepted:
467
            m = _(astakos_messages.USER_LEFT_PROJECT)
468
        else:
469
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
470
        messages.success(request, m)
471
    except (IOError, PermissionDenied), e:
472
        messages.error(request, e)
473

    
474

    
475
@require_http_methods(["POST"])
476
@cookie_fix
477
@valid_astakos_user_required
478
def project_cancel(request, chain_id):
479
    next = request.GET.get('next')
480
    if not next:
481
        next = reverse('astakos.im.views.project_list')
482

    
483
    with ExceptionHandler(request):
484
        _project_cancel(request, chain_id)
485

    
486
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
487
    return redirect(next)
488

    
489

    
490
@commit_on_success_strict()
491
def _project_cancel(request, chain_id):
492
    try:
493
        chain_id = int(chain_id)
494
        cancel_membership(chain_id, request.user)
495
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
496
        messages.success(request, m)
497
    except (IOError, PermissionDenied), e:
498
        messages.error(request, e)
499

    
500

    
501

    
502
@require_http_methods(["POST"])
503
@cookie_fix
504
@valid_astakos_user_required
505
def project_accept_member(request, chain_id, memb_id):
506

    
507
    with ExceptionHandler(request):
508
        _project_accept_member(request, chain_id, memb_id)
509

    
510
    return redirect(reverse('project_detail', args=(chain_id,)))
511

    
512

    
513
@commit_on_success_strict()
514
def _project_accept_member(request, chain_id, memb_id):
515
    try:
516
        chain_id = int(chain_id)
517
        memb_id = int(memb_id)
518
        m = accept_membership(chain_id, memb_id, request.user)
519
    except (IOError, PermissionDenied), e:
520
        messages.error(request, e)
521

    
522
    else:
523
        email = escape(m.person.email)
524
        msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
525
        messages.success(request, msg)
526

    
527

    
528
@require_http_methods(["POST"])
529
@cookie_fix
530
@valid_astakos_user_required
531
def project_remove_member(request, chain_id, memb_id):
532

    
533
    with ExceptionHandler(request):
534
        _project_remove_member(request, chain_id, memb_id)
535

    
536
    return redirect(reverse('project_detail', args=(chain_id,)))
537

    
538

    
539
@commit_on_success_strict()
540
def _project_remove_member(request, chain_id, memb_id):
541
    try:
542
        chain_id = int(chain_id)
543
        memb_id = int(memb_id)
544
        m = remove_membership(chain_id, memb_id, request.user)
545
    except (IOError, PermissionDenied), e:
546
        messages.error(request, e)
547
    else:
548
        email = escape(m.person.email)
549
        msg = _(astakos_messages.USER_MEMBERSHIP_REMOVED) % email
550
        messages.success(request, msg)
551

    
552

    
553
@require_http_methods(["POST"])
554
@cookie_fix
555
@valid_astakos_user_required
556
def project_reject_member(request, chain_id, memb_id):
557

    
558
    with ExceptionHandler(request):
559
        _project_reject_member(request, chain_id, memb_id)
560

    
561
    return redirect(reverse('project_detail', args=(chain_id,)))
562

    
563

    
564
@commit_on_success_strict()
565
def _project_reject_member(request, chain_id, memb_id):
566
    try:
567
        chain_id = int(chain_id)
568
        memb_id = int(memb_id)
569
        m = reject_membership(chain_id, memb_id, request.user)
570
    except (IOError, PermissionDenied), e:
571
        messages.error(request, e)
572
    else:
573
        email = escape(m.person.email)
574
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email
575
        messages.success(request, msg)
576

    
577

    
578
@require_http_methods(["POST"])
579
@signed_terms_required
580
@login_required
581
@cookie_fix
582
def project_app_approve(request, application_id):
583

    
584
    if not request.user.is_project_admin():
585
        m = _(astakos_messages.NOT_ALLOWED)
586
        raise PermissionDenied(m)
587

    
588
    try:
589
        app = ProjectApplication.objects.get(id=application_id)
590
    except ProjectApplication.DoesNotExist:
591
        raise Http404
592

    
593
    with ExceptionHandler(request):
594
        _project_app_approve(request, application_id)
595

    
596
    chain_id = get_related_project_id(application_id)
597
    return redirect(reverse('project_detail', args=(chain_id,)))
598

    
599

    
600
@commit_on_success_strict()
601
def _project_app_approve(request, application_id):
602
    approve_application(application_id)
603

    
604

    
605
@require_http_methods(["POST"])
606
@signed_terms_required
607
@login_required
608
@cookie_fix
609
def project_app_deny(request, application_id):
610

    
611
    reason = request.POST.get('reason', None)
612
    if not reason:
613
        reason = None
614

    
615
    if not request.user.is_project_admin():
616
        m = _(astakos_messages.NOT_ALLOWED)
617
        raise PermissionDenied(m)
618

    
619
    try:
620
        app = ProjectApplication.objects.get(id=application_id)
621
    except ProjectApplication.DoesNotExist:
622
        raise Http404
623

    
624
    with ExceptionHandler(request):
625
        _project_app_deny(request, application_id, reason)
626

    
627
    return redirect(reverse('project_list'))
628

    
629

    
630
@commit_on_success_strict()
631
def _project_app_deny(request, application_id, reason):
632
    deny_application(application_id, reason=reason)
633

    
634

    
635
@require_http_methods(["POST"])
636
@signed_terms_required
637
@login_required
638
@cookie_fix
639
def project_app_dismiss(request, application_id):
640
    try:
641
        app = ProjectApplication.objects.get(id=application_id)
642
    except ProjectApplication.DoesNotExist:
643
        raise Http404
644

    
645
    if not request.user.owns_application(app):
646
        m = _(astakos_messages.NOT_ALLOWED)
647
        raise PermissionDenied(m)
648

    
649
    with ExceptionHandler(request):
650
        _project_app_dismiss(request, application_id)
651

    
652
    chain_id = None
653
    chain_id = get_related_project_id(application_id)
654
    if chain_id:
655
        next = reverse('project_detail', args=(chain_id,))
656
    else:
657
        next = reverse('project_list')
658
    return redirect(next)
659

    
660

    
661
def _project_app_dismiss(request, application_id):
662
    # XXX: dismiss application also does authorization
663
    dismiss_application(application_id, request_user=request.user)
664

    
665

    
666
@require_http_methods(["GET", "POST"])
667
@valid_astakos_user_required
668
def project_members(request, chain_id, members_status_filter=None,
669
                    template_name='im/projects/project_members.html'):
670
    project, application = get_by_chain_or_404(chain_id)
671

    
672
    user = request.user
673
    if not user.owns_project(project) or user.is_project_admin():
674
        return redirect(reverse('index'))
675

    
676
    return common_detail(request, chain_id,
677
                         members_status_filter=members_status_filter,
678
                         template_name=template_name)