Statistics
| Branch: | Tag: | Revision:

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

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
    mem_display = user.membership_display(project) if project else None
358
    can_join_req = can_join_request(project, user) if project else False
359
    can_leave_req = can_leave_request(project, user) if project else False
360

    
361
    return object_detail(
362
        request,
363
        queryset=ProjectApplication.objects.select_related(),
364
        object_id=application.id,
365
        template_name=template_name,
366
        extra_context={
367
            'project_view': project_view,
368
            'chain_id': chain_or_app_id,
369
            'application': application,
370
            'addmembers_form': addmembers_form,
371
            'approved_members_count': approved_members_count,
372
            'pending_members_count': pending_members_count,
373
            'members_table': members_table,
374
            'owner_mode': is_owner,
375
            'admin_mode': is_project_admin,
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
            'remaining_memberships_count': remaining_memberships_count,
381
        })
382

    
383

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

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

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

    
406
        projects = Project.objects.search_by_name(q)
407
        projects = projects.filter(Project.o_state_q(Project.O_ACTIVE))
408
        projects = projects.exclude(id__in=accepted)
409

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

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

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

    
431

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

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

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

    
447

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

    
461

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

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

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

    
476

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

    
490

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

    
499
    with ExceptionHandler(request):
500
        _project_cancel(request, chain_id)
501

    
502
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
503
    return redirect(next)
504

    
505

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

    
516

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

    
522
    with ExceptionHandler(request):
523
        _project_accept_member(request, chain_id, memb_id)
524

    
525
    return redirect_back(request, 'project_list')
526

    
527

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

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

    
542

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

    
548
    with ExceptionHandler(request):
549
        _project_remove_member(request, chain_id, memb_id)
550

    
551
    return redirect_back(request, 'project_list')
552

    
553

    
554
@commit_on_success_strict()
555
def _project_remove_member(request, chain_id, memb_id):
556
    try:
557
        chain_id = int(chain_id)
558
        memb_id = int(memb_id)
559
        m = remove_membership(chain_id, 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, chain_id, memb_id):
572

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

    
576
    return redirect_back(request, 'project_list')
577

    
578

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

    
592

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

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

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

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

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

    
616

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

    
621

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

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

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

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

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

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

    
646

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

    
651

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

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

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

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

    
677

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

    
682

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

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

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

    
697

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

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

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

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

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

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

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

    
727
    return redirect_back(request, 'project_list')