Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views / projects.py @ 8fb8d0cf

History | View | Annotate | Download (24 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.core.urlresolvers import reverse
44
from django.http import Http404
45
from django.shortcuts import redirect
46
from django.utils.html import escape
47
from django.utils.translation import ugettext as _
48
from django.views.generic.list_detail import object_list, object_detail
49
from django.core.exceptions import PermissionDenied
50
from django.views.decorators.http import require_http_methods
51
from django.db.models import Q
52

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

    
55
import astakos.im.messages as astakos_messages
56

    
57
from astakos.im import tables
58
from astakos.im.models import ProjectApplication, ProjectMembership
59
from astakos.im.util import get_context, restrict_next
60
from astakos.im.forms import ProjectApplicationForm, AddProjectMembersForm, \
61
    ProjectSearchForm
62
from astakos.im.functions import check_pending_app_quota, accept_membership, \
63
    reject_membership, remove_membership, cancel_membership, leave_project, \
64
    join_project, enroll_member, can_join_request, can_leave_request, \
65
    get_related_project_id, get_by_chain_or_404, approve_application, \
66
    deny_application, cancel_application, dismiss_application
67
from astakos.im import settings
68
from astakos.im.util import redirect_back
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, login_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
    if response is not None:
127
        return response
128

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

    
133

    
134
@require_http_methods(["GET"])
135
@cookie_fix
136
@valid_astakos_user_required
137
def project_list(request):
138
    projects = ProjectApplication.objects.user_accessible_projects(
139
        request.user).select_related()
140
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
141
                                                prefix="my_projects_")
142
    RequestConfig(request,
143
                  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

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

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

    
190

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

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

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

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

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

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

    
247
    if response is not None:
248
        return response
249

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

    
254

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

    
261

    
262
@require_http_methods(["GET", "POST"])
263
@cookie_fix
264
@valid_astakos_user_required
265
def project_detail(request, chain_id):
266
    return common_detail(request, chain_id)
267

    
268

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

    
281

    
282
def common_detail(request, chain_or_app_id, project_view=True,
283
                  template_name='im/projects/project_detail.html',
284
                  members_status_filter=None):
285
    project = None
286
    approved_members_count = 0
287
    pending_members_count = 0
288
    remaining_memberships_count = 0
289
    if project_view:
290
        chain_id = chain_or_app_id
291
        if request.method == 'POST':
292
            addmembers_form = AddProjectMembersForm(
293
                request.POST,
294
                chain_id=int(chain_id),
295
                request_user=request.user)
296
            with ExceptionHandler(request):
297
                addmembers(request, chain_id, addmembers_form)
298

    
299
            if addmembers_form.is_valid():
300
                addmembers_form = AddProjectMembersForm()  # clear form data
301
        else:
302
            addmembers_form = AddProjectMembersForm()  # initialize form
303

    
304
        approved_members_count = 0
305
        pending_members_count = 0
306
        remaining_memberships_count = 0
307
        project, application = get_by_chain_or_404(chain_id)
308
        if project:
309
            members = project.projectmembership_set.select_related()
310
            approved_members_count = \
311
                project.count_actually_accepted_memberships()
312
            pending_members_count = project.count_pending_memberships()
313
            if members_status_filter in (ProjectMembership.REQUESTED,
314
                                         ProjectMembership.ACCEPTED):
315
                members = members.filter(state=members_status_filter)
316
            members_table = tables.ProjectMembersTable(project,
317
                                                       members,
318
                                                       user=request.user,
319
                                                       prefix="members_")
320
            RequestConfig(request, paginate={"per_page": settings.PAGINATE_BY}
321
                          ).configure(members_table)
322

    
323
        else:
324
            members_table = None
325

    
326
    else:
327
        # is application
328
        application_id = chain_or_app_id
329
        application = get_object_or_404(ProjectApplication, pk=application_id)
330
        members_table = None
331
        addmembers_form = None
332

    
333
    modifications_table = None
334

    
335
    user = request.user
336
    is_project_admin = user.is_project_admin(application_id=application.id)
337
    is_owner = user.owns_application(application)
338
    if not (is_owner or is_project_admin) and not project_view:
339
        m = _(astakos_messages.NOT_ALLOWED)
340
        raise PermissionDenied(m)
341

    
342
    if (
343
        not (is_owner or is_project_admin) and project_view and
344
        not user.non_owner_can_view(project)
345
    ):
346
        m = _(astakos_messages.NOT_ALLOWED)
347
        raise PermissionDenied(m)
348

    
349
    following_applications = list(application.pending_modifications())
350
    following_applications.reverse()
351
    modifications_table = (
352
        tables.ProjectModificationApplicationsTable(following_applications,
353
                                                    user=request.user,
354
                                                    prefix="modifications_"))
355

    
356
    mem_display = user.membership_display(project) if project else None
357
    can_join_req = can_join_request(project, user) if project else False
358
    can_leave_req = can_leave_request(project, user) if project else False
359

    
360
    return object_detail(
361
        request,
362
        queryset=ProjectApplication.objects.select_related(),
363
        object_id=application.id,
364
        template_name=template_name,
365
        extra_context={
366
            'project_view': project_view,
367
            'chain_id': chain_or_app_id,
368
            'application': application,
369
            'addmembers_form': addmembers_form,
370
            'approved_members_count': approved_members_count,
371
            'pending_members_count': pending_members_count,
372
            'members_table': members_table,
373
            'owner_mode': is_owner,
374
            'admin_mode': is_project_admin,
375
            'modifications_table': modifications_table,
376
            'mem_display': mem_display,
377
            'can_join_request': can_join_req,
378
            'can_leave_request': can_leave_req,
379
            'members_status_filter': members_status_filter,
380
        })
381

    
382

    
383
@require_http_methods(["GET", "POST"])
384
@cookie_fix
385
@valid_astakos_user_required
386
def project_search(request):
387
    q = request.GET.get('q', '')
388
    form = ProjectSearchForm()
389
    q = q.strip()
390

    
391
    if request.method == "POST":
392
        form = ProjectSearchForm(request.POST)
393
        if form.is_valid():
394
            q = form.cleaned_data['q'].strip()
395
        else:
396
            q = None
397

    
398
    if q is None:
399
        projects = ProjectApplication.objects.none()
400
    else:
401
        accepted_projects = request.user.projectmembership_set.filter(
402
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
403
        projects = ProjectApplication.objects.search_by_name(q)
404
        projects = projects.filter(
405
            ~Q(project__last_approval_date__isnull=True))
406
        projects = projects.exclude(project__in=accepted_projects)
407

    
408
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
409
                                                prefix="my_projects_")
410
    if request.method == "POST":
411
        table.caption = _('SEARCH RESULTS')
412
    else:
413
        table.caption = _('ALL PROJECTS')
414

    
415
    RequestConfig(request,
416
                  paginate={"per_page": settings.PAGINATE_BY}).configure(table)
417

    
418
    return object_list(
419
        request,
420
        projects,
421
        template_name='im/projects/project_list.html',
422
        extra_context={
423
            'form': form,
424
            'is_search': True,
425
            'q': q,
426
            'table': table
427
        })
428

    
429

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

    
439
    with ExceptionHandler(request):
440
        _project_join(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_join(request, chain_id):
448
    try:
449
        chain_id = int(chain_id)
450
        auto_accepted = join_project(chain_id, request.user)
451
        if auto_accepted:
452
            m = _(astakos_messages.USER_JOINED_PROJECT)
453
        else:
454
            m = _(astakos_messages.USER_JOIN_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_leave(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_leave(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_leave(request, chain_id):
477
    try:
478
        chain_id = int(chain_id)
479
        auto_accepted = leave_project(chain_id, request.user)
480
        if auto_accepted:
481
            m = _(astakos_messages.USER_LEFT_PROJECT)
482
        else:
483
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
484
        messages.success(request, m)
485
    except (IOError, PermissionDenied), e:
486
        messages.error(request, e)
487

    
488

    
489
@require_http_methods(["POST"])
490
@cookie_fix
491
@valid_astakos_user_required
492
def project_cancel(request, chain_id):
493
    next = request.GET.get('next')
494
    if not next:
495
        next = reverse('astakos.im.views.project_list')
496

    
497
    with ExceptionHandler(request):
498
        _project_cancel(request, chain_id)
499

    
500
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
501
    return redirect(next)
502

    
503

    
504
@commit_on_success_strict()
505
def _project_cancel(request, chain_id):
506
    try:
507
        chain_id = int(chain_id)
508
        cancel_membership(chain_id, request.user)
509
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
510
        messages.success(request, m)
511
    except (IOError, PermissionDenied), e:
512
        messages.error(request, e)
513

    
514

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

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

    
523
    return redirect_back(request, 'project_list')
524

    
525

    
526
@commit_on_success_strict()
527
def _project_accept_member(request, chain_id, memb_id):
528
    try:
529
        chain_id = int(chain_id)
530
        memb_id = int(memb_id)
531
        m = accept_membership(chain_id, memb_id, request.user)
532
    except (IOError, PermissionDenied), e:
533
        messages.error(request, e)
534

    
535
    else:
536
        email = escape(m.person.email)
537
        msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
538
        messages.success(request, msg)
539

    
540

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

    
546
    with ExceptionHandler(request):
547
        _project_remove_member(request, chain_id, memb_id)
548

    
549
    return redirect_back(request, 'project_list')
550

    
551

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

    
565

    
566
@require_http_methods(["POST"])
567
@cookie_fix
568
@valid_astakos_user_required
569
def project_reject_member(request, chain_id, memb_id):
570

    
571
    with ExceptionHandler(request):
572
        _project_reject_member(request, chain_id, memb_id)
573

    
574
    return redirect_back(request, 'project_list')
575

    
576

    
577
@commit_on_success_strict()
578
def _project_reject_member(request, chain_id, memb_id):
579
    try:
580
        chain_id = int(chain_id)
581
        memb_id = int(memb_id)
582
        m = reject_membership(chain_id, memb_id, request.user)
583
    except (IOError, PermissionDenied), e:
584
        messages.error(request, e)
585
    else:
586
        email = escape(m.person.email)
587
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email
588
        messages.success(request, msg)
589

    
590

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

    
597
    if not request.user.is_project_admin():
598
        m = _(astakos_messages.NOT_ALLOWED)
599
        raise PermissionDenied(m)
600

    
601
    try:
602
        app = ProjectApplication.objects.get(id=application_id)
603
    except ProjectApplication.DoesNotExist:
604
        raise Http404
605

    
606
    with ExceptionHandler(request):
607
        _project_app_approve(request, application_id)
608

    
609
    chain_id = get_related_project_id(application_id)
610
    if not chain_id:
611
        return redirect_back(request, 'project_list')
612
    return redirect(reverse('project_detail', args=(chain_id,)))
613

    
614

    
615
@commit_on_success_strict()
616
def _project_app_approve(request, application_id):
617
    approve_application(application_id)
618

    
619

    
620
@require_http_methods(["POST"])
621
@signed_terms_required
622
@login_required
623
@cookie_fix
624
def project_app_deny(request, application_id):
625

    
626
    reason = request.POST.get('reason', None)
627
    if not reason:
628
        reason = None
629

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

    
634
    try:
635
        app = ProjectApplication.objects.get(id=application_id)
636
    except ProjectApplication.DoesNotExist:
637
        raise Http404
638

    
639
    with ExceptionHandler(request):
640
        _project_app_deny(request, application_id, reason)
641

    
642
    return redirect(reverse('project_list'))
643

    
644

    
645
@commit_on_success_strict()
646
def _project_app_deny(request, application_id, reason):
647
    deny_application(application_id, reason=reason)
648

    
649

    
650
@require_http_methods(["POST"])
651
@signed_terms_required
652
@login_required
653
@cookie_fix
654
def project_app_dismiss(request, application_id):
655
    try:
656
        app = ProjectApplication.objects.get(id=application_id)
657
    except ProjectApplication.DoesNotExist:
658
        raise Http404
659

    
660
    if not request.user.owns_application(app):
661
        m = _(astakos_messages.NOT_ALLOWED)
662
        raise PermissionDenied(m)
663

    
664
    with ExceptionHandler(request):
665
        _project_app_dismiss(request, application_id)
666

    
667
    chain_id = None
668
    chain_id = get_related_project_id(application_id)
669
    if chain_id:
670
        next = reverse('project_detail', args=(chain_id,))
671
    else:
672
        next = reverse('project_list')
673
    return redirect(next)
674

    
675

    
676
def _project_app_dismiss(request, application_id):
677
    # XXX: dismiss application also does authorization
678
    dismiss_application(application_id, request_user=request.user)
679

    
680

    
681
@require_http_methods(["GET", "POST"])
682
@valid_astakos_user_required
683
def project_members(request, chain_id, members_status_filter=None,
684
                    template_name='im/projects/project_members.html'):
685
    project, application = get_by_chain_or_404(chain_id)
686

    
687
    user = request.user
688
    if not user.owns_project(project) and not user.is_project_admin():
689
        return redirect(reverse('index'))
690

    
691
    return common_detail(request, chain_id,
692
                         members_status_filter=members_status_filter,
693
                         template_name=template_name)
694

    
695

    
696
@require_http_methods(["POST"])
697
@valid_astakos_user_required
698
def project_members_action(request, chain_id, action=None, redirect_to=''):
699

    
700
    actions_map = {
701
        'remove': _project_remove_member,
702
        'accept': _project_accept_member,
703
        'reject': _project_reject_member
704
    }
705

    
706
    if not action in actions_map.keys():
707
        raise PermissionDenied
708

    
709
    member_ids = request.POST.getlist('members')
710
    project, application = get_by_chain_or_404(chain_id)
711

    
712
    user = request.user
713
    if not user.owns_project(project) and not user.is_project_admin():
714
        return redirect(reverse('index'))
715

    
716
    logger.info("Batch members action from %s (chain: %r, action: %s, "
717
                "members: %r)", user.log_display, chain_id, action, member_ids)
718

    
719
    action_func = actions_map.get(action)
720
    for member_id in member_ids:
721
        member_id = int(member_id)
722
        with ExceptionHandler(request):
723
            action_func(request, chain_id, member_id)
724

    
725
    return redirect_back(request, 'project_list')