Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (24.2 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 = ProjectApplication.objects.user_accessible_projects(
139
        request.user).select_related()
140
    table = tables.UserProjectApplicationsTable(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
            members = members.select_related()
324
            members_table = tables.ProjectMembersTable(project,
325
                                                       members,
326
                                                       user=request.user,
327
                                                       prefix="members_")
328
            RequestConfig(request, paginate={"per_page": settings.PAGINATE_BY}
329
                          ).configure(members_table)
330

    
331
        else:
332
            members_table = None
333

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

    
341
    modifications_table = 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
    following_applications = list(application.pending_modifications())
358
    following_applications.reverse()
359
    modifications_table = (
360
        tables.ProjectModificationApplicationsTable(following_applications,
361
                                                    user=request.user,
362
                                                    prefix="modifications_"))
363

    
364
    mem_display = user.membership_display(project) if project else None
365
    can_join_req = can_join_request(project, user) if project else False
366
    can_leave_req = can_leave_request(project, user) if project else False
367

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

    
391

    
392
@require_http_methods(["GET", "POST"])
393
@cookie_fix
394
@valid_astakos_user_required
395
def project_search(request):
396
    q = request.GET.get('q', '')
397
    form = ProjectSearchForm()
398
    q = q.strip()
399

    
400
    if request.method == "POST":
401
        form = ProjectSearchForm(request.POST)
402
        if form.is_valid():
403
            q = form.cleaned_data['q'].strip()
404
        else:
405
            q = None
406

    
407
    if q is None:
408
        projects = ProjectApplication.objects.none()
409
    else:
410
        accepted_projects = request.user.projectmembership_set.filter(
411
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
412
        projects = ProjectApplication.objects.search_by_name(q)
413
        projects = projects.filter(
414
            ~Q(project__last_approval_date__isnull=True))
415
        projects = projects.exclude(project__in=accepted_projects)
416

    
417
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
418
                                                prefix="my_projects_")
419
    if request.method == "POST":
420
        table.caption = _('SEARCH RESULTS')
421
    else:
422
        table.caption = _('ALL PROJECTS')
423

    
424
    RequestConfig(request,
425
                  paginate={"per_page": settings.PAGINATE_BY}).configure(table)
426

    
427
    return object_list(
428
        request,
429
        projects,
430
        template_name='im/projects/project_list.html',
431
        extra_context={
432
            'form': form,
433
            'is_search': True,
434
            'q': q,
435
            'table': table
436
        })
437

    
438

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

    
448
    with ExceptionHandler(request):
449
        _project_join(request, chain_id)
450

    
451
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
452
    return redirect(next)
453

    
454

    
455
@commit_on_success_strict()
456
def _project_join(request, chain_id):
457
    try:
458
        chain_id = int(chain_id)
459
        auto_accepted = join_project(chain_id, request.user)
460
        if auto_accepted:
461
            m = _(astakos_messages.USER_JOINED_PROJECT)
462
        else:
463
            m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
464
        messages.success(request, m)
465
    except (IOError, PermissionDenied), e:
466
        messages.error(request, e)
467

    
468

    
469
@require_http_methods(["POST"])
470
@cookie_fix
471
@valid_astakos_user_required
472
def project_leave(request, chain_id):
473
    next = request.GET.get('next')
474
    if not next:
475
        next = reverse('astakos.im.views.project_list')
476

    
477
    with ExceptionHandler(request):
478
        _project_leave(request, chain_id)
479

    
480
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
481
    return redirect(next)
482

    
483

    
484
@commit_on_success_strict()
485
def _project_leave(request, chain_id):
486
    try:
487
        chain_id = int(chain_id)
488
        auto_accepted = leave_project(chain_id, request.user)
489
        if auto_accepted:
490
            m = _(astakos_messages.USER_LEFT_PROJECT)
491
        else:
492
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
493
        messages.success(request, m)
494
    except (IOError, PermissionDenied), e:
495
        messages.error(request, e)
496

    
497

    
498
@require_http_methods(["POST"])
499
@cookie_fix
500
@valid_astakos_user_required
501
def project_cancel(request, chain_id):
502
    next = request.GET.get('next')
503
    if not next:
504
        next = reverse('astakos.im.views.project_list')
505

    
506
    with ExceptionHandler(request):
507
        _project_cancel(request, chain_id)
508

    
509
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
510
    return redirect(next)
511

    
512

    
513
@commit_on_success_strict()
514
def _project_cancel(request, chain_id):
515
    try:
516
        chain_id = int(chain_id)
517
        cancel_membership(chain_id, request.user)
518
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
519
        messages.success(request, m)
520
    except (IOError, PermissionDenied), e:
521
        messages.error(request, e)
522

    
523

    
524
@require_http_methods(["POST"])
525
@cookie_fix
526
@valid_astakos_user_required
527
def project_accept_member(request, chain_id, memb_id):
528

    
529
    with ExceptionHandler(request):
530
        _project_accept_member(request, chain_id, memb_id)
531

    
532
    return redirect_back(request, 'project_list')
533

    
534

    
535
@commit_on_success_strict()
536
def _project_accept_member(request, chain_id, memb_id):
537
    try:
538
        chain_id = int(chain_id)
539
        memb_id = int(memb_id)
540
        m = accept_membership(chain_id, memb_id, request.user)
541
    except (IOError, PermissionDenied), e:
542
        messages.error(request, e)
543

    
544
    else:
545
        email = escape(m.person.email)
546
        msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
547
        messages.success(request, msg)
548

    
549

    
550
@require_http_methods(["POST"])
551
@cookie_fix
552
@valid_astakos_user_required
553
def project_remove_member(request, chain_id, memb_id):
554

    
555
    with ExceptionHandler(request):
556
        _project_remove_member(request, chain_id, memb_id)
557

    
558
    return redirect_back(request, 'project_list')
559

    
560

    
561
@commit_on_success_strict()
562
def _project_remove_member(request, chain_id, memb_id):
563
    try:
564
        chain_id = int(chain_id)
565
        memb_id = int(memb_id)
566
        m = remove_membership(chain_id, memb_id, request.user)
567
    except (IOError, PermissionDenied), e:
568
        messages.error(request, e)
569
    else:
570
        email = escape(m.person.email)
571
        msg = _(astakos_messages.USER_MEMBERSHIP_REMOVED) % email
572
        messages.success(request, msg)
573

    
574

    
575
@require_http_methods(["POST"])
576
@cookie_fix
577
@valid_astakos_user_required
578
def project_reject_member(request, chain_id, memb_id):
579

    
580
    with ExceptionHandler(request):
581
        _project_reject_member(request, chain_id, memb_id)
582

    
583
    return redirect_back(request, 'project_list')
584

    
585

    
586
@commit_on_success_strict()
587
def _project_reject_member(request, chain_id, memb_id):
588
    try:
589
        chain_id = int(chain_id)
590
        memb_id = int(memb_id)
591
        m = reject_membership(chain_id, memb_id, request.user)
592
    except (IOError, PermissionDenied), e:
593
        messages.error(request, e)
594
    else:
595
        email = escape(m.person.email)
596
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email
597
        messages.success(request, msg)
598

    
599

    
600
@require_http_methods(["POST"])
601
@signed_terms_required
602
@login_required
603
@cookie_fix
604
def project_app_approve(request, application_id):
605

    
606
    if not request.user.is_project_admin():
607
        m = _(astakos_messages.NOT_ALLOWED)
608
        raise PermissionDenied(m)
609

    
610
    try:
611
        app = ProjectApplication.objects.get(id=application_id)
612
    except ProjectApplication.DoesNotExist:
613
        raise Http404
614

    
615
    with ExceptionHandler(request):
616
        _project_app_approve(request, application_id)
617

    
618
    chain_id = get_related_project_id(application_id)
619
    if not chain_id:
620
        return redirect_back(request, 'project_list')
621
    return redirect(reverse('project_detail', args=(chain_id,)))
622

    
623

    
624
@commit_on_success_strict()
625
def _project_app_approve(request, application_id):
626
    approve_application(application_id)
627

    
628

    
629
@require_http_methods(["POST"])
630
@signed_terms_required
631
@login_required
632
@cookie_fix
633
def project_app_deny(request, application_id):
634

    
635
    reason = request.POST.get('reason', None)
636
    if not reason:
637
        reason = None
638

    
639
    if not request.user.is_project_admin():
640
        m = _(astakos_messages.NOT_ALLOWED)
641
        raise PermissionDenied(m)
642

    
643
    try:
644
        app = ProjectApplication.objects.get(id=application_id)
645
    except ProjectApplication.DoesNotExist:
646
        raise Http404
647

    
648
    with ExceptionHandler(request):
649
        _project_app_deny(request, application_id, reason)
650

    
651
    return redirect(reverse('project_list'))
652

    
653

    
654
@commit_on_success_strict()
655
def _project_app_deny(request, application_id, reason):
656
    deny_application(application_id, reason=reason)
657

    
658

    
659
@require_http_methods(["POST"])
660
@signed_terms_required
661
@login_required
662
@cookie_fix
663
def project_app_dismiss(request, application_id):
664
    try:
665
        app = ProjectApplication.objects.get(id=application_id)
666
    except ProjectApplication.DoesNotExist:
667
        raise Http404
668

    
669
    if not request.user.owns_application(app):
670
        m = _(astakos_messages.NOT_ALLOWED)
671
        raise PermissionDenied(m)
672

    
673
    with ExceptionHandler(request):
674
        _project_app_dismiss(request, application_id)
675

    
676
    chain_id = None
677
    chain_id = get_related_project_id(application_id)
678
    if chain_id:
679
        next = reverse('project_detail', args=(chain_id,))
680
    else:
681
        next = reverse('project_list')
682
    return redirect(next)
683

    
684

    
685
def _project_app_dismiss(request, application_id):
686
    # XXX: dismiss application also does authorization
687
    dismiss_application(application_id, request_user=request.user)
688

    
689

    
690
@require_http_methods(["GET", "POST"])
691
@valid_astakos_user_required
692
def project_members(request, chain_id, members_status_filter=None,
693
                    template_name='im/projects/project_members.html'):
694
    project = get_object_or_404(Project, pk=chain_id)
695

    
696
    user = request.user
697
    if not user.owns_project(project) and not user.is_project_admin():
698
        return redirect(reverse('index'))
699

    
700
    return common_detail(request, chain_id,
701
                         members_status_filter=members_status_filter,
702
                         template_name=template_name)
703

    
704

    
705
@require_http_methods(["POST"])
706
@valid_astakos_user_required
707
def project_members_action(request, chain_id, action=None, redirect_to=''):
708

    
709
    actions_map = {
710
        'remove': _project_remove_member,
711
        'accept': _project_accept_member,
712
        'reject': _project_reject_member
713
    }
714

    
715
    if not action in actions_map.keys():
716
        raise PermissionDenied
717

    
718
    member_ids = request.POST.getlist('members')
719
    project = get_object_or_404(Project, pk=chain_id)
720

    
721
    user = request.user
722
    if not user.owns_project(project) and not user.is_project_admin():
723
        return redirect(reverse('index'))
724

    
725
    logger.info("Batch members action from %s (chain: %r, action: %s, "
726
                "members: %r)", user.log_display, chain_id, action, member_ids)
727

    
728
    action_func = actions_map.get(action)
729
    for member_id in member_ids:
730
        member_id = int(member_id)
731
        with ExceptionHandler(request):
732
            action_func(request, chain_id, member_id)
733

    
734
    return redirect_back(request, 'project_list')