Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (23.8 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
            'chain_id': chain_or_app_id,
359
            'application': application,
360
            'addmembers_form': addmembers_form,
361
            'approved_members_count': approved_members_count,
362
            'pending_members_count': pending_members_count,
363
            'members_table': members_table,
364
            'owner_mode': is_owner,
365
            'admin_mode': is_project_admin,
366
            'modifications_table': modifications_table,
367
            'mem_display': mem_display,
368
            'can_join_request': can_join_req,
369
            'can_leave_request': can_leave_req,
370
            'members_status_filter':members_status_filter,
371
            })
372

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

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

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

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

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

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

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

    
426
    with ExceptionHandler(request):
427
        _project_join(request, chain_id)
428

    
429

    
430
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
431
    return redirect(next)
432

    
433

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

    
447

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

    
456
    with ExceptionHandler(request):
457
        _project_leave(request, chain_id)
458

    
459
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
460
    return redirect(next)
461

    
462

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

    
476

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

    
485
    with ExceptionHandler(request):
486
        _project_cancel(request, chain_id)
487

    
488
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
489
    return redirect(next)
490

    
491

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

    
502

    
503

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

    
509
    with ExceptionHandler(request):
510
        _project_accept_member(request, chain_id, memb_id)
511

    
512
    return redirect(reverse('project_detail', args=(chain_id,)))
513

    
514

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

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

    
529

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

    
535
    with ExceptionHandler(request):
536
        _project_remove_member(request, chain_id, memb_id)
537

    
538
    return redirect(reverse('project_detail', args=(chain_id,)))
539

    
540

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

    
554

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

    
560
    with ExceptionHandler(request):
561
        _project_reject_member(request, chain_id, memb_id)
562

    
563
    return redirect(reverse('project_detail', args=(chain_id,)))
564

    
565

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

    
579

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

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

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

    
595
    with ExceptionHandler(request):
596
        _project_app_approve(request, application_id)
597

    
598
    chain_id = get_related_project_id(application_id)
599
    return redirect(reverse('project_detail', args=(chain_id,)))
600

    
601

    
602
@commit_on_success_strict()
603
def _project_app_approve(request, application_id):
604
    approve_application(application_id)
605

    
606

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

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

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

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

    
626
    with ExceptionHandler(request):
627
        _project_app_deny(request, application_id, reason)
628

    
629
    return redirect(reverse('project_list'))
630

    
631

    
632
@commit_on_success_strict()
633
def _project_app_deny(request, application_id, reason):
634
    deny_application(application_id, reason=reason)
635

    
636

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

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

    
651
    with ExceptionHandler(request):
652
        _project_app_dismiss(request, application_id)
653

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

    
662

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

    
667

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

    
674
    user = request.user
675
    if not user.owns_project(project) and not user.is_project_admin():
676
        return redirect(reverse('index'))
677

    
678
    return common_detail(request, chain_id,
679
                         members_status_filter=members_status_filter,
680
                         template_name=template_name)
681

    
682

    
683
@require_http_methods(["POST"])
684
@valid_astakos_user_required
685
def project_members_action(request, chain_id, action=None, redirect_to=''):
686

    
687
    actions_map = {
688
        'remove': _project_remove_member,
689
        'accept': _project_accept_member,
690
        'reject': _project_reject_member
691
    }
692

    
693
    if not action in actions_map.keys():
694
        raise PermissionDenied
695

    
696
    member_ids = request.POST.getlist('members')
697
    project, application = get_by_chain_or_404(chain_id)
698

    
699
    user = request.user
700
    if not user.owns_project(project) and not user.is_project_admin():
701
        return redirect(reverse('index'))
702

    
703
    logger.info("Batch members action from %s (action: %s, members: %r)",
704
                user.log_display, action, member_ids)
705

    
706
    action_func = actions_map.get(action)
707
    for member_id in member_ids:
708
        member_id = int(member_id)
709
        with ExceptionHandler(request):
710
            action_func(request, chain_id, member_id)
711

    
712
    return redirect(reverse('project_members', args=(chain_id,)))