Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (23.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.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, Project
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, 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 = Project.objects.user_accessible_projects(
139
        request.user).select_related()
140
    table = tables.UserProjectsTable(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, project=app.chain)
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
MEMBERSHIP_STATUS_FILTER = {
283
    0: lambda x: x.requested(),
284
    1: lambda x: x.any_accepted(),
285
}
286

    
287

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

    
305
            if addmembers_form.is_valid():
306
                addmembers_form = AddProjectMembersForm()  # clear form data
307
        else:
308
            addmembers_form = AddProjectMembersForm()  # initialize form
309

    
310
        project = get_object_or_404(Project, pk=chain_id)
311
        application = project.application
312
        if project:
313
            members = project.projectmembership_set
314
            approved_members_count = project.members_count()
315
            pending_members_count = project.count_pending_memberships()
316
            _limit = application.limit_on_members_number
317
            if _limit is not None:
318
                remaining_memberships_count = \
319
                    max(0, _limit - approved_members_count)
320
            flt = MEMBERSHIP_STATUS_FILTER.get(members_status_filter)
321
            if flt is not None:
322
                members = flt(members)
323
            else:
324
                members = members.associated()
325
            members = members.select_related()
326
            members_table = tables.ProjectMembersTable(project,
327
                                                       members,
328
                                                       user=request.user,
329
                                                       prefix="members_")
330
            RequestConfig(request, paginate={"per_page": settings.PAGINATE_BY}
331
                          ).configure(members_table)
332

    
333
        else:
334
            members_table = None
335

    
336
    else:
337
        # is application
338
        application_id = chain_or_app_id
339
        application = get_object_or_404(ProjectApplication, pk=application_id)
340
        members_table = None
341
        addmembers_form = None
342

    
343
    user = request.user
344
    is_project_admin = user.is_project_admin(application_id=application.id)
345
    is_owner = user.owns_application(application)
346
    if not (is_owner or is_project_admin) and not project_view:
347
        m = _(astakos_messages.NOT_ALLOWED)
348
        raise PermissionDenied(m)
349

    
350
    if (
351
        not (is_owner or is_project_admin) and project_view and
352
        not user.non_owner_can_view(project)
353
    ):
354
        m = _(astakos_messages.NOT_ALLOWED)
355
        raise PermissionDenied(m)
356

    
357
    membership = user.get_membership(project) if project else None
358
    membership_id = membership.id if membership else None
359
    mem_display = user.membership_display(project) if project else None
360
    can_join_req = can_join_request(project, user) if project else False
361
    can_leave_req = can_leave_request(project, user) if project else False
362

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

    
386

    
387
@require_http_methods(["GET", "POST"])
388
@cookie_fix
389
@valid_astakos_user_required
390
def project_search(request):
391
    q = request.GET.get('q', '')
392
    form = ProjectSearchForm()
393
    q = q.strip()
394

    
395
    if request.method == "POST":
396
        form = ProjectSearchForm(request.POST)
397
        if form.is_valid():
398
            q = form.cleaned_data['q'].strip()
399
        else:
400
            q = None
401

    
402
    if q is None:
403
        projects = Project.objects.none()
404
    else:
405
        accepted = request.user.projectmembership_set.filter(
406
            state__in=ProjectMembership.ACCEPTED_STATES).values_list(
407
                'project', flat=True)
408

    
409
        projects = Project.objects.search_by_name(q)
410
        projects = projects.filter(Project.o_state_q(Project.O_ACTIVE))
411
        projects = projects.exclude(id__in=accepted)
412

    
413
    table = tables.UserProjectsTable(projects, user=request.user,
414
                                     prefix="my_projects_")
415
    if request.method == "POST":
416
        table.caption = _('SEARCH RESULTS')
417
    else:
418
        table.caption = _('ALL PROJECTS')
419

    
420
    RequestConfig(request,
421
                  paginate={"per_page": settings.PAGINATE_BY}).configure(table)
422

    
423
    return object_list(
424
        request,
425
        projects,
426
        template_name='im/projects/project_list.html',
427
        extra_context={
428
            'form': form,
429
            'is_search': True,
430
            'q': q,
431
            'table': table
432
        })
433

    
434

    
435
@require_http_methods(["POST"])
436
@cookie_fix
437
@valid_astakos_user_required
438
def project_join(request, chain_id):
439
    next = request.GET.get('next')
440
    if not next:
441
        next = reverse('astakos.im.views.project_detail',
442
                       args=(chain_id,))
443

    
444
    with ExceptionHandler(request):
445
        _project_join(request, chain_id)
446

    
447
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
448
    return redirect(next)
449

    
450

    
451
@commit_on_success_strict()
452
def _project_join(request, chain_id):
453
    try:
454
        chain_id = int(chain_id)
455
        membership = join_project(chain_id, request.user)
456
        if membership.state != membership.REQUESTED:
457
            m = _(astakos_messages.USER_JOINED_PROJECT)
458
        else:
459
            m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
460
        messages.success(request, m)
461
    except (IOError, PermissionDenied), e:
462
        messages.error(request, e)
463

    
464

    
465
@require_http_methods(["POST"])
466
@cookie_fix
467
@valid_astakos_user_required
468
def project_leave(request, memb_id):
469
    next = request.GET.get('next')
470
    if not next:
471
        next = reverse('astakos.im.views.project_list')
472

    
473
    with ExceptionHandler(request):
474
        _project_leave(request, memb_id)
475

    
476
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
477
    return redirect(next)
478

    
479

    
480
@commit_on_success_strict()
481
def _project_leave(request, memb_id):
482
    try:
483
        memb_id = int(memb_id)
484
        auto_accepted = leave_project(memb_id, request.user)
485
        if auto_accepted:
486
            m = _(astakos_messages.USER_LEFT_PROJECT)
487
        else:
488
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
489
        messages.success(request, m)
490
    except (IOError, PermissionDenied), e:
491
        messages.error(request, e)
492

    
493

    
494
@require_http_methods(["POST"])
495
@cookie_fix
496
@valid_astakos_user_required
497
def project_cancel_member(request, memb_id):
498
    next = request.GET.get('next')
499
    if not next:
500
        next = reverse('astakos.im.views.project_list')
501

    
502
    with ExceptionHandler(request):
503
        _project_cancel_member(request, memb_id)
504

    
505
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
506
    return redirect(next)
507

    
508

    
509
@commit_on_success_strict()
510
def _project_cancel_member(request, memb_id):
511
    try:
512
        cancel_membership(memb_id, request.user)
513
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
514
        messages.success(request, m)
515
    except (IOError, PermissionDenied), e:
516
        messages.error(request, e)
517

    
518

    
519
@require_http_methods(["POST"])
520
@cookie_fix
521
@valid_astakos_user_required
522
def project_accept_member(request, memb_id):
523

    
524
    with ExceptionHandler(request):
525
        _project_accept_member(request, memb_id)
526

    
527
    return redirect_back(request, 'project_list')
528

    
529

    
530
@commit_on_success_strict()
531
def _project_accept_member(request, memb_id):
532
    try:
533
        memb_id = int(memb_id)
534
        m = accept_membership(memb_id, request.user)
535
    except (IOError, PermissionDenied), e:
536
        messages.error(request, e)
537

    
538
    else:
539
        email = escape(m.person.email)
540
        msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
541
        messages.success(request, msg)
542

    
543

    
544
@require_http_methods(["POST"])
545
@cookie_fix
546
@valid_astakos_user_required
547
def project_remove_member(request, memb_id):
548

    
549
    with ExceptionHandler(request):
550
        _project_remove_member(request, memb_id)
551

    
552
    return redirect_back(request, 'project_list')
553

    
554

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

    
567

    
568
@require_http_methods(["POST"])
569
@cookie_fix
570
@valid_astakos_user_required
571
def project_reject_member(request, memb_id):
572

    
573
    with ExceptionHandler(request):
574
        _project_reject_member(request, memb_id)
575

    
576
    return redirect_back(request, 'project_list')
577

    
578

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

    
591

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

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

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

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

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

    
615

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

    
620

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

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

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

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

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

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

    
645

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

    
650

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

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

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

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

    
676

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

    
681

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

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

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

    
696

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

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

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

    
710
    member_ids = request.POST.getlist('members')
711
    project = get_object_or_404(Project, pk=chain_id)
712

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

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

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

    
726
    return redirect_back(request, 'project_list')