Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (23.9 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

    
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
    approved_members_count = 0
283
    pending_members_count = 0
284
    remaining_memberships_count = 0
285
    if project_view:
286
        chain_id = chain_or_app_id
287
        if request.method == 'POST':
288
            addmembers_form = AddProjectMembersForm(
289
                request.POST,
290
                chain_id=int(chain_id),
291
                request_user=request.user)
292
            with ExceptionHandler(request):
293
                addmembers(request, chain_id, addmembers_form)
294

    
295
            if addmembers_form.is_valid():
296
                addmembers_form = AddProjectMembersForm()  # clear form data
297
        else:
298
            addmembers_form = AddProjectMembersForm()  # initialize form
299

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

    
319
        else:
320
            members_table = None
321

    
322
    else:
323
        # is application
324
        application_id = chain_or_app_id
325
        application = get_object_or_404(ProjectApplication, pk=application_id)
326
        members_table = None
327
        addmembers_form = None
328

    
329
    modifications_table = None
330

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

    
338
    if (not (is_owner or is_project_admin) and project_view and
339
        not user.non_owner_can_view(project)):
340
        m = _(astakos_messages.NOT_ALLOWED)
341
        raise PermissionDenied(m)
342

    
343
    following_applications = list(application.pending_modifications())
344
    following_applications.reverse()
345
    modifications_table = (
346
        tables.ProjectModificationApplicationsTable(following_applications,
347
                                                    user=request.user,
348
                                                    prefix="modifications_"))
349

    
350
    mem_display = user.membership_display(project) if project else None
351
    can_join_req = can_join_request(project, user) if project else False
352
    can_leave_req = can_leave_request(project, user) if project else False
353

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

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

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

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

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

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

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

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

    
429
    with ExceptionHandler(request):
430
        _project_join(request, chain_id)
431

    
432

    
433
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
434
    return redirect(next)
435

    
436

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

    
450

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

    
459
    with ExceptionHandler(request):
460
        _project_leave(request, chain_id)
461

    
462
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
463
    return redirect(next)
464

    
465

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

    
479

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

    
488
    with ExceptionHandler(request):
489
        _project_cancel(request, chain_id)
490

    
491
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
492
    return redirect(next)
493

    
494

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

    
505

    
506

    
507
@require_http_methods(["POST"])
508
@cookie_fix
509
@valid_astakos_user_required
510
def project_accept_member(request, chain_id, memb_id):
511

    
512
    with ExceptionHandler(request):
513
        _project_accept_member(request, chain_id, memb_id)
514

    
515
    return redirect_back(request, 'project_list')
516

    
517

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

    
527
    else:
528
        email = escape(m.person.email)
529
        msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
530
        messages.success(request, msg)
531

    
532

    
533
@require_http_methods(["POST"])
534
@cookie_fix
535
@valid_astakos_user_required
536
def project_remove_member(request, chain_id, memb_id):
537

    
538
    with ExceptionHandler(request):
539
        _project_remove_member(request, chain_id, memb_id)
540

    
541
    return redirect_back(request, 'project_list')
542

    
543

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

    
557

    
558
@require_http_methods(["POST"])
559
@cookie_fix
560
@valid_astakos_user_required
561
def project_reject_member(request, chain_id, memb_id):
562

    
563
    with ExceptionHandler(request):
564
        _project_reject_member(request, chain_id, memb_id)
565

    
566
    return redirect_back(request, 'project_list')
567

    
568

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

    
582

    
583
@require_http_methods(["POST"])
584
@signed_terms_required
585
@login_required
586
@cookie_fix
587
def project_app_approve(request, application_id):
588

    
589
    if not request.user.is_project_admin():
590
        m = _(astakos_messages.NOT_ALLOWED)
591
        raise PermissionDenied(m)
592

    
593
    try:
594
        app = ProjectApplication.objects.get(id=application_id)
595
    except ProjectApplication.DoesNotExist:
596
        raise Http404
597

    
598
    with ExceptionHandler(request):
599
        _project_app_approve(request, application_id)
600

    
601
    chain_id = get_related_project_id(application_id)
602
    if not chain_id:
603
        return redirect_back(request, 'project_list')
604
    return redirect(reverse('project_detail', args=(chain_id,)))
605

    
606

    
607
@commit_on_success_strict()
608
def _project_app_approve(request, application_id):
609
    approve_application(application_id)
610

    
611

    
612
@require_http_methods(["POST"])
613
@signed_terms_required
614
@login_required
615
@cookie_fix
616
def project_app_deny(request, application_id):
617

    
618
    reason = request.POST.get('reason', None)
619
    if not reason:
620
        reason = None
621

    
622
    if not request.user.is_project_admin():
623
        m = _(astakos_messages.NOT_ALLOWED)
624
        raise PermissionDenied(m)
625

    
626
    try:
627
        app = ProjectApplication.objects.get(id=application_id)
628
    except ProjectApplication.DoesNotExist:
629
        raise Http404
630

    
631
    with ExceptionHandler(request):
632
        _project_app_deny(request, application_id, reason)
633

    
634
    return redirect(reverse('project_list'))
635

    
636

    
637
@commit_on_success_strict()
638
def _project_app_deny(request, application_id, reason):
639
    deny_application(application_id, reason=reason)
640

    
641

    
642
@require_http_methods(["POST"])
643
@signed_terms_required
644
@login_required
645
@cookie_fix
646
def project_app_dismiss(request, application_id):
647
    try:
648
        app = ProjectApplication.objects.get(id=application_id)
649
    except ProjectApplication.DoesNotExist:
650
        raise Http404
651

    
652
    if not request.user.owns_application(app):
653
        m = _(astakos_messages.NOT_ALLOWED)
654
        raise PermissionDenied(m)
655

    
656
    with ExceptionHandler(request):
657
        _project_app_dismiss(request, application_id)
658

    
659
    chain_id = None
660
    chain_id = get_related_project_id(application_id)
661
    if chain_id:
662
        next = reverse('project_detail', args=(chain_id,))
663
    else:
664
        next = reverse('project_list')
665
    return redirect(next)
666

    
667

    
668
def _project_app_dismiss(request, application_id):
669
    # XXX: dismiss application also does authorization
670
    dismiss_application(application_id, request_user=request.user)
671

    
672

    
673
@require_http_methods(["GET", "POST"])
674
@valid_astakos_user_required
675
def project_members(request, chain_id, members_status_filter=None,
676
                    template_name='im/projects/project_members.html'):
677
    project, application = get_by_chain_or_404(chain_id)
678

    
679
    user = request.user
680
    if not user.owns_project(project) and not user.is_project_admin():
681
        return redirect(reverse('index'))
682

    
683
    return common_detail(request, chain_id,
684
                         members_status_filter=members_status_filter,
685
                         template_name=template_name)
686

    
687

    
688
@require_http_methods(["POST"])
689
@valid_astakos_user_required
690
def project_members_action(request, chain_id, action=None, redirect_to=''):
691

    
692
    actions_map = {
693
        'remove': _project_remove_member,
694
        'accept': _project_accept_member,
695
        'reject': _project_reject_member
696
    }
697

    
698
    if not action in actions_map.keys():
699
        raise PermissionDenied
700

    
701
    member_ids = request.POST.getlist('members')
702
    project, application = get_by_chain_or_404(chain_id)
703

    
704
    user = request.user
705
    if not user.owns_project(project) and not user.is_project_admin():
706
        return redirect(reverse('index'))
707

    
708
    logger.info("Batch members action from %s (chain: %r, action: %s, "
709
                "members: %r)", user.log_display, chain_id, action, member_ids)
710

    
711
    action_func = actions_map.get(action)
712
    for member_id in member_ids:
713
        member_id = int(member_id)
714
        with ExceptionHandler(request):
715
            action_func(request, chain_id, member_id)
716

    
717
    return redirect_back(request, 'project_list')