Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (24.1 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, ProjectError
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_app_object(request, extra_context=extra_context)
118

    
119
    if response is not None:
120
        return response
121

    
122
    next = reverse('astakos.im.views.project_list')
123
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
124
    return redirect(next)
125

    
126

    
127
@commit_on_success_strict()
128
def create_app_object(request, extra_context=None):
129
    try:
130
        return _create_object(
131
            request,
132
            template_name='im/projects/projectapplication_form.html',
133
            extra_context=extra_context,
134
            post_save_redirect=reverse('project_list'),
135
            form_class=ProjectApplicationForm,
136
            msg=_("The %(verbose_name)s has been received and "
137
                  "is under consideration."))
138
    except ProjectError as e:
139
        messages.error(request, e)
140

    
141

    
142
@require_http_methods(["GET"])
143
@cookie_fix
144
@valid_astakos_user_required
145
def project_list(request):
146
    projects = Project.objects.user_accessible_projects(
147
        request.user).select_related()
148
    table = tables.UserProjectsTable(projects, user=request.user,
149
                                     prefix="my_projects_")
150
    RequestConfig(request,
151
                  paginate={"per_page": settings.PAGINATE_BY}).configure(table)
152

    
153
    return object_list(
154
        request,
155
        projects,
156
        template_name='im/projects/project_list.html',
157
        extra_context={
158
            'is_search': False,
159
            'table': table,
160
        })
161

    
162

    
163
@require_http_methods(["POST"])
164
@cookie_fix
165
@valid_astakos_user_required
166
def project_app_cancel(request, application_id):
167
    next = request.GET.get('next')
168
    chain_id = None
169

    
170
    with ExceptionHandler(request):
171
        chain_id = _project_app_cancel(request, application_id)
172

    
173
    if not next:
174
        if chain_id:
175
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
176
        else:
177
            next = reverse('astakos.im.views.project_list')
178

    
179
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
180
    return redirect(next)
181

    
182

    
183
@commit_on_success_strict()
184
def _project_app_cancel(request, application_id):
185
    chain_id = None
186
    try:
187
        application_id = int(application_id)
188
        chain_id = get_related_project_id(application_id)
189
        cancel_application(application_id, request.user)
190
    except ProjectError as e:
191
        messages.error(request, e)
192

    
193
    else:
194
        msg = _(astakos_messages.APPLICATION_CANCELLED)
195
        messages.success(request, msg)
196
        return chain_id
197

    
198

    
199
@require_http_methods(["GET", "POST"])
200
@cookie_fix
201
@valid_astakos_user_required
202
def project_modify(request, application_id):
203

    
204
    try:
205
        app = ProjectApplication.objects.get(id=application_id)
206
    except ProjectApplication.DoesNotExist:
207
        raise Http404
208

    
209
    user = request.user
210
    if not (user.owns_application(app) or user.is_project_admin(app.id)):
211
        m = _(astakos_messages.NOT_ALLOWED)
212
        raise PermissionDenied(m)
213

    
214
    if not user.is_project_admin():
215
        owner = app.owner
216
        ok, limit = check_pending_app_quota(owner, project=app.chain)
217
        if not ok:
218
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
219
            messages.error(request, m)
220
            next = reverse('astakos.im.views.project_list')
221
            next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
222
            return redirect(next)
223

    
224
    details_fields = ["name", "homepage", "description", "start_date",
225
                      "end_date", "comments"]
226
    membership_fields = ["member_join_policy", "member_leave_policy",
227
                         "limit_on_members_number"]
228
    resource_catalog, resource_groups = _resources_catalog(for_project=True)
229
    if resource_catalog is False:
230
        # on fail resource_groups contains the result object
231
        result = resource_groups
232
        messages.error(request, 'Unable to retrieve system resources: %s' %
233
                       result.reason)
234
    extra_context = {
235
        'resource_catalog': resource_catalog,
236
        'resource_groups': resource_groups,
237
        'show_form': True,
238
        'details_fields': details_fields,
239
        'update_form': True,
240
        'membership_fields': membership_fields
241
    }
242

    
243
    response = None
244
    with ExceptionHandler(request):
245
        response = update_app_object(request, application_id,
246
                                     extra_context=extra_context)
247

    
248
    if response is not None:
249
        return response
250

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

    
255

    
256
@commit_on_success_strict()
257
def update_app_object(request, object_id, extra_context=None):
258
    try:
259
        return _update_object(
260
            request,
261
            object_id=object_id,
262
            template_name='im/projects/projectapplication_form.html',
263
            extra_context=extra_context,
264
            post_save_redirect=reverse('project_list'),
265
            form_class=ProjectApplicationForm,
266
            msg=_("The %(verbose_name)s has been received and is under "
267
                  "consideration."))
268
    except ProjectError as e:
269
        messages.error(request, e)
270

    
271

    
272
@require_http_methods(["GET", "POST"])
273
@cookie_fix
274
@valid_astakos_user_required
275
def project_app(request, application_id):
276
    return common_detail(request, application_id, project_view=False)
277

    
278

    
279
@require_http_methods(["GET", "POST"])
280
@cookie_fix
281
@valid_astakos_user_required
282
def project_detail(request, chain_id):
283
    return common_detail(request, chain_id)
284

    
285

    
286
@commit_on_success_strict()
287
def addmembers(request, chain_id, addmembers_form):
288
    if addmembers_form.is_valid():
289
        try:
290
            chain_id = int(chain_id)
291
            map(lambda u: enroll_member(chain_id,
292
                                        u,
293
                                        request_user=request.user),
294
                addmembers_form.valid_users)
295
        except ProjectError as e:
296
            messages.error(request, e)
297

    
298

    
299
MEMBERSHIP_STATUS_FILTER = {
300
    0: lambda x: x.requested(),
301
    1: lambda x: x.any_accepted(),
302
}
303

    
304

    
305
def common_detail(request, chain_or_app_id, project_view=True,
306
                  template_name='im/projects/project_detail.html',
307
                  members_status_filter=None):
308
    project = None
309
    approved_members_count = 0
310
    pending_members_count = 0
311
    remaining_memberships_count = None
312
    if project_view:
313
        chain_id = chain_or_app_id
314
        if request.method == 'POST':
315
            addmembers_form = AddProjectMembersForm(
316
                request.POST,
317
                chain_id=int(chain_id),
318
                request_user=request.user)
319
            with ExceptionHandler(request):
320
                addmembers(request, chain_id, addmembers_form)
321

    
322
            if addmembers_form.is_valid():
323
                addmembers_form = AddProjectMembersForm()  # clear form data
324
        else:
325
            addmembers_form = AddProjectMembersForm()  # initialize form
326

    
327
        project = get_object_or_404(Project, pk=chain_id)
328
        application = project.application
329
        if project:
330
            members = project.projectmembership_set
331
            approved_members_count = project.members_count()
332
            pending_members_count = project.count_pending_memberships()
333
            _limit = application.limit_on_members_number
334
            if _limit is not None:
335
                remaining_memberships_count = \
336
                    max(0, _limit - approved_members_count)
337
            flt = MEMBERSHIP_STATUS_FILTER.get(members_status_filter)
338
            if flt is not None:
339
                members = flt(members)
340
            else:
341
                members = members.associated()
342
            members = members.select_related()
343
            members_table = tables.ProjectMembersTable(project,
344
                                                       members,
345
                                                       user=request.user,
346
                                                       prefix="members_")
347
            RequestConfig(request, paginate={"per_page": settings.PAGINATE_BY}
348
                          ).configure(members_table)
349

    
350
        else:
351
            members_table = None
352

    
353
    else:
354
        # is application
355
        application_id = chain_or_app_id
356
        application = get_object_or_404(ProjectApplication, pk=application_id)
357
        members_table = None
358
        addmembers_form = None
359

    
360
    user = request.user
361
    is_project_admin = user.is_project_admin(application_id=application.id)
362
    is_owner = user.owns_application(application)
363
    if not (is_owner or is_project_admin) and not project_view:
364
        m = _(astakos_messages.NOT_ALLOWED)
365
        raise PermissionDenied(m)
366

    
367
    if (
368
        not (is_owner or is_project_admin) and project_view and
369
        not user.non_owner_can_view(project)
370
    ):
371
        m = _(astakos_messages.NOT_ALLOWED)
372
        raise PermissionDenied(m)
373

    
374
    membership = user.get_membership(project) if project else None
375
    membership_id = membership.id if membership else None
376
    mem_display = user.membership_display(project) if project else None
377
    can_join_req = can_join_request(project, user) if project else False
378
    can_leave_req = can_leave_request(project, user) if project else False
379

    
380
    return object_detail(
381
        request,
382
        queryset=ProjectApplication.objects.select_related(),
383
        object_id=application.id,
384
        template_name=template_name,
385
        extra_context={
386
            'project_view': project_view,
387
            'chain_id': chain_or_app_id,
388
            'application': application,
389
            'addmembers_form': addmembers_form,
390
            'approved_members_count': approved_members_count,
391
            'pending_members_count': pending_members_count,
392
            'members_table': members_table,
393
            'owner_mode': is_owner,
394
            'admin_mode': is_project_admin,
395
            'mem_display': mem_display,
396
            'membership_id': membership_id,
397
            'can_join_request': can_join_req,
398
            'can_leave_request': can_leave_req,
399
            'members_status_filter': members_status_filter,
400
            'remaining_memberships_count': remaining_memberships_count,
401
        })
402

    
403

    
404
@require_http_methods(["GET", "POST"])
405
@cookie_fix
406
@valid_astakos_user_required
407
def project_search(request):
408
    q = request.GET.get('q', '')
409
    form = ProjectSearchForm()
410
    q = q.strip()
411

    
412
    if request.method == "POST":
413
        form = ProjectSearchForm(request.POST)
414
        if form.is_valid():
415
            q = form.cleaned_data['q'].strip()
416
        else:
417
            q = None
418

    
419
    if q is None:
420
        projects = Project.objects.none()
421
    else:
422
        accepted = request.user.projectmembership_set.filter(
423
            state__in=ProjectMembership.ACCEPTED_STATES).values_list(
424
                'project', flat=True)
425

    
426
        projects = Project.objects.search_by_name(q)
427
        projects = projects.filter(Project.o_state_q(Project.O_ACTIVE))
428
        projects = projects.exclude(id__in=accepted)
429

    
430
    table = tables.UserProjectsTable(projects, user=request.user,
431
                                     prefix="my_projects_")
432
    if request.method == "POST":
433
        table.caption = _('SEARCH RESULTS')
434
    else:
435
        table.caption = _('ALL PROJECTS')
436

    
437
    RequestConfig(request,
438
                  paginate={"per_page": settings.PAGINATE_BY}).configure(table)
439

    
440
    return object_list(
441
        request,
442
        projects,
443
        template_name='im/projects/project_list.html',
444
        extra_context={
445
            'form': form,
446
            'is_search': True,
447
            'q': q,
448
            'table': table
449
        })
450

    
451

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

    
461
    with ExceptionHandler(request):
462
        _project_join(request, chain_id)
463

    
464
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
465
    return redirect(next)
466

    
467

    
468
@commit_on_success_strict()
469
def _project_join(request, chain_id):
470
    try:
471
        chain_id = int(chain_id)
472
        membership = join_project(chain_id, request.user)
473
        if membership.state != membership.REQUESTED:
474
            m = _(astakos_messages.USER_JOINED_PROJECT)
475
        else:
476
            m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
477
        messages.success(request, m)
478
    except ProjectError as e:
479
        messages.error(request, e)
480

    
481

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

    
490
    with ExceptionHandler(request):
491
        _project_leave(request, memb_id)
492

    
493
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
494
    return redirect(next)
495

    
496

    
497
@commit_on_success_strict()
498
def _project_leave(request, memb_id):
499
    try:
500
        memb_id = int(memb_id)
501
        auto_accepted = leave_project(memb_id, request.user)
502
        if auto_accepted:
503
            m = _(astakos_messages.USER_LEFT_PROJECT)
504
        else:
505
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
506
        messages.success(request, m)
507
    except ProjectError as e:
508
        messages.error(request, e)
509

    
510

    
511
@require_http_methods(["POST"])
512
@cookie_fix
513
@valid_astakos_user_required
514
def project_cancel_member(request, memb_id):
515
    next = request.GET.get('next')
516
    if not next:
517
        next = reverse('astakos.im.views.project_list')
518

    
519
    with ExceptionHandler(request):
520
        _project_cancel_member(request, memb_id)
521

    
522
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
523
    return redirect(next)
524

    
525

    
526
@commit_on_success_strict()
527
def _project_cancel_member(request, memb_id):
528
    try:
529
        cancel_membership(memb_id, request.user)
530
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
531
        messages.success(request, m)
532
    except ProjectError as e:
533
        messages.error(request, e)
534

    
535

    
536
@require_http_methods(["POST"])
537
@cookie_fix
538
@valid_astakos_user_required
539
def project_accept_member(request, memb_id):
540

    
541
    with ExceptionHandler(request):
542
        _project_accept_member(request, memb_id)
543

    
544
    return redirect_back(request, 'project_list')
545

    
546

    
547
@commit_on_success_strict()
548
def _project_accept_member(request, memb_id):
549
    try:
550
        memb_id = int(memb_id)
551
        m = accept_membership(memb_id, request.user)
552
    except ProjectError as e:
553
        messages.error(request, e)
554

    
555
    else:
556
        email = escape(m.person.email)
557
        msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
558
        messages.success(request, msg)
559

    
560

    
561
@require_http_methods(["POST"])
562
@cookie_fix
563
@valid_astakos_user_required
564
def project_remove_member(request, memb_id):
565

    
566
    with ExceptionHandler(request):
567
        _project_remove_member(request, memb_id)
568

    
569
    return redirect_back(request, 'project_list')
570

    
571

    
572
@commit_on_success_strict()
573
def _project_remove_member(request, memb_id):
574
    try:
575
        memb_id = int(memb_id)
576
        m = remove_membership(memb_id, request.user)
577
    except ProjectError as e:
578
        messages.error(request, e)
579
    else:
580
        email = escape(m.person.email)
581
        msg = _(astakos_messages.USER_MEMBERSHIP_REMOVED) % email
582
        messages.success(request, msg)
583

    
584

    
585
@require_http_methods(["POST"])
586
@cookie_fix
587
@valid_astakos_user_required
588
def project_reject_member(request, memb_id):
589

    
590
    with ExceptionHandler(request):
591
        _project_reject_member(request, memb_id)
592

    
593
    return redirect_back(request, 'project_list')
594

    
595

    
596
@commit_on_success_strict()
597
def _project_reject_member(request, memb_id):
598
    try:
599
        memb_id = int(memb_id)
600
        m = reject_membership(memb_id, request.user)
601
    except ProjectError as e:
602
        messages.error(request, e)
603
    else:
604
        email = escape(m.person.email)
605
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email
606
        messages.success(request, msg)
607

    
608

    
609
@require_http_methods(["POST"])
610
@signed_terms_required
611
@login_required
612
@cookie_fix
613
def project_app_approve(request, application_id):
614

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

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

    
624
    with ExceptionHandler(request):
625
        _project_app_approve(request, application_id)
626

    
627
    chain_id = get_related_project_id(application_id)
628
    if not chain_id:
629
        return redirect_back(request, 'project_list')
630
    return redirect(reverse('project_detail', args=(chain_id,)))
631

    
632

    
633
@commit_on_success_strict()
634
def _project_app_approve(request, application_id):
635
    approve_application(application_id)
636

    
637

    
638
@require_http_methods(["POST"])
639
@signed_terms_required
640
@login_required
641
@cookie_fix
642
def project_app_deny(request, application_id):
643

    
644
    reason = request.POST.get('reason', None)
645
    if not reason:
646
        reason = None
647

    
648
    if not request.user.is_project_admin():
649
        m = _(astakos_messages.NOT_ALLOWED)
650
        raise PermissionDenied(m)
651

    
652
    try:
653
        app = ProjectApplication.objects.get(id=application_id)
654
    except ProjectApplication.DoesNotExist:
655
        raise Http404
656

    
657
    with ExceptionHandler(request):
658
        _project_app_deny(request, application_id, reason)
659

    
660
    return redirect(reverse('project_list'))
661

    
662

    
663
@commit_on_success_strict()
664
def _project_app_deny(request, application_id, reason):
665
    deny_application(application_id, reason=reason)
666

    
667

    
668
@require_http_methods(["POST"])
669
@signed_terms_required
670
@login_required
671
@cookie_fix
672
def project_app_dismiss(request, application_id):
673
    try:
674
        app = ProjectApplication.objects.get(id=application_id)
675
    except ProjectApplication.DoesNotExist:
676
        raise Http404
677

    
678
    if not request.user.owns_application(app):
679
        m = _(astakos_messages.NOT_ALLOWED)
680
        raise PermissionDenied(m)
681

    
682
    with ExceptionHandler(request):
683
        _project_app_dismiss(request, application_id)
684

    
685
    chain_id = None
686
    chain_id = get_related_project_id(application_id)
687
    if chain_id:
688
        next = reverse('project_detail', args=(chain_id,))
689
    else:
690
        next = reverse('project_list')
691
    return redirect(next)
692

    
693

    
694
def _project_app_dismiss(request, application_id):
695
    # XXX: dismiss application also does authorization
696
    dismiss_application(application_id, request_user=request.user)
697

    
698

    
699
@require_http_methods(["GET", "POST"])
700
@valid_astakos_user_required
701
def project_members(request, chain_id, members_status_filter=None,
702
                    template_name='im/projects/project_members.html'):
703
    project = get_object_or_404(Project, pk=chain_id)
704

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

    
709
    return common_detail(request, chain_id,
710
                         members_status_filter=members_status_filter,
711
                         template_name=template_name)
712

    
713

    
714
@require_http_methods(["POST"])
715
@valid_astakos_user_required
716
def project_members_action(request, chain_id, action=None, redirect_to=''):
717

    
718
    actions_map = {
719
        'remove': _project_remove_member,
720
        'accept': _project_accept_member,
721
        'reject': _project_reject_member
722
    }
723

    
724
    if not action in actions_map.keys():
725
        raise PermissionDenied
726

    
727
    member_ids = request.POST.getlist('members')
728
    project = get_object_or_404(Project, pk=chain_id)
729

    
730
    user = request.user
731
    if not user.owns_project(project) and not user.is_project_admin():
732
        return redirect(reverse('index'))
733

    
734
    logger.info("Batch members action from %s (chain: %r, action: %s, "
735
                "members: %r)", user.log_display, chain_id, action, member_ids)
736

    
737
    action_func = actions_map.get(action)
738
    for member_id in member_ids:
739
        member_id = int(member_id)
740
        with ExceptionHandler(request):
741
            action_func(request, member_id)
742

    
743
    return redirect_back(request, 'project_list')