Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (24.8 kB)

1
# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import logging
35
import inflect
36

    
37
engine = inflect.engine()
38

    
39
from django_tables2 import RequestConfig
40

    
41
from django.shortcuts import get_object_or_404
42
from django.contrib import messages
43
from django.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

    
52
from snf_django.lib.db.transaction import commit_on_success_strict
53

    
54
import astakos.im.messages as astakos_messages
55

    
56
from astakos.im import tables
57
from astakos.im.models import ProjectApplication, ProjectMembership, Project
58
from astakos.im.util import get_context, restrict_next
59
from astakos.im.forms import ProjectApplicationForm, AddProjectMembersForm, \
60
    ProjectSearchForm
61
from astakos.im.functions import check_pending_app_quota, accept_membership, \
62
    reject_membership, remove_membership, cancel_membership, leave_project, \
63
    join_project, enroll_member, can_join_request, can_leave_request, \
64
    get_related_project_id, approve_application, \
65
    deny_application, cancel_application, dismiss_application, ProjectError
66
from astakos.im import settings
67
from astakos.im.util import redirect_back
68
from astakos.im.views.util import render_response, _create_object, \
69
    _update_object, _resources_catalog, ExceptionHandler
70
from astakos.im.views.decorators import cookie_fix, signed_terms_required,\
71
    valid_astakos_user_required, login_required
72

    
73
logger = logging.getLogger(__name__)
74

    
75

    
76
@cookie_fix
77
def how_it_works(request):
78
    return render_response(
79
        'im/how_it_works.html',
80
        context_instance=get_context(request))
81

    
82

    
83
@require_http_methods(["GET", "POST"])
84
@cookie_fix
85
@valid_astakos_user_required
86
def project_add(request):
87
    user = request.user
88
    if not user.is_project_admin():
89
        ok, limit = check_pending_app_quota(user)
90
        if not ok:
91
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_ADD) % limit
92
            messages.error(request, m)
93
            next = reverse('astakos.im.views.project_list')
94
            next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
95
            return redirect(next)
96

    
97
    details_fields = ["name", "homepage", "description", "start_date",
98
                      "end_date", "comments"]
99
    membership_fields = ["member_join_policy", "member_leave_policy",
100
                         "limit_on_members_number"]
101
    resource_catalog, resource_groups = _resources_catalog(for_project=True)
102
    if resource_catalog is False:
103
        # on fail resource_groups contains the result object
104
        result = resource_groups
105
        messages.error(request, 'Unable to retrieve system resources: %s' %
106
                       result.reason)
107
    extra_context = {
108
        'resource_catalog': resource_catalog,
109
        'resource_groups': resource_groups,
110
        'show_form': True,
111
        'details_fields': details_fields,
112
        'membership_fields': membership_fields}
113

    
114
    response = None
115
    with ExceptionHandler(request):
116
        response = create_app_object(request, extra_context=extra_context)
117

    
118
    if response is not None:
119
        return response
120

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

    
125

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

    
140

    
141
def get_user_projects_table(projects, user, prefix):
142
    apps = ProjectApplication.objects.pending_per_project(projects)
143
    memberships = user.projectmembership_set.one_per_project()
144
    objs = ProjectMembership.objects
145
    accepted_ms = objs.any_accepted_per_project(projects)
146
    requested_ms = objs.requested_per_project(projects)
147
    return tables.UserProjectsTable(projects, user=user,
148
                                    prefix=prefix,
149
                                    pending_apps=apps,
150
                                    memberships=memberships,
151
                                    accepted=accepted_ms,
152
                                    requested=requested_ms)
153

    
154

    
155
@require_http_methods(["GET"])
156
@cookie_fix
157
@valid_astakos_user_required
158
def project_list(request):
159
    projects = Project.objects.user_accessible_projects(request.user)
160
    table = get_user_projects_table(projects, user=request.user,
161
                                    prefix="my_projects_")
162
    RequestConfig(request,
163
                  paginate={"per_page": settings.PAGINATE_BY}).configure(table)
164

    
165
    return object_list(
166
        request,
167
        projects,
168
        template_name='im/projects/project_list.html',
169
        extra_context={
170
            'is_search': False,
171
            'table': table,
172
        })
173

    
174

    
175
@require_http_methods(["POST"])
176
@cookie_fix
177
@valid_astakos_user_required
178
def project_app_cancel(request, application_id):
179
    next = request.GET.get('next')
180
    chain_id = None
181

    
182
    with ExceptionHandler(request):
183
        chain_id = _project_app_cancel(request, application_id)
184

    
185
    if not next:
186
        if chain_id:
187
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
188
        else:
189
            next = reverse('astakos.im.views.project_list')
190

    
191
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
192
    return redirect(next)
193

    
194

    
195
@commit_on_success_strict()
196
def _project_app_cancel(request, application_id):
197
    chain_id = None
198
    try:
199
        application_id = int(application_id)
200
        chain_id = get_related_project_id(application_id)
201
        cancel_application(application_id, request.user)
202
    except ProjectError as e:
203
        messages.error(request, e)
204

    
205
    else:
206
        msg = _(astakos_messages.APPLICATION_CANCELLED)
207
        messages.success(request, msg)
208
        return chain_id
209

    
210

    
211
@require_http_methods(["GET", "POST"])
212
@cookie_fix
213
@valid_astakos_user_required
214
def project_modify(request, application_id):
215

    
216
    try:
217
        app = ProjectApplication.objects.get(id=application_id)
218
    except ProjectApplication.DoesNotExist:
219
        raise Http404
220

    
221
    user = request.user
222
    if not (user.owns_application(app) or user.is_project_admin(app.id)):
223
        m = _(astakos_messages.NOT_ALLOWED)
224
        raise PermissionDenied(m)
225

    
226
    if not user.is_project_admin():
227
        owner = app.owner
228
        ok, limit = check_pending_app_quota(owner, project=app.chain)
229
        if not ok:
230
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
231
            messages.error(request, m)
232
            next = reverse('astakos.im.views.project_list')
233
            next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
234
            return redirect(next)
235

    
236
    details_fields = ["name", "homepage", "description", "start_date",
237
                      "end_date", "comments"]
238
    membership_fields = ["member_join_policy", "member_leave_policy",
239
                         "limit_on_members_number"]
240
    resource_catalog, resource_groups = _resources_catalog(for_project=True)
241
    if resource_catalog is False:
242
        # on fail resource_groups contains the result object
243
        result = resource_groups
244
        messages.error(request, 'Unable to retrieve system resources: %s' %
245
                       result.reason)
246
    extra_context = {
247
        'resource_catalog': resource_catalog,
248
        'resource_groups': resource_groups,
249
        'show_form': True,
250
        'details_fields': details_fields,
251
        'update_form': True,
252
        'membership_fields': membership_fields
253
    }
254

    
255
    response = None
256
    with ExceptionHandler(request):
257
        response = update_app_object(request, application_id,
258
                                     extra_context=extra_context)
259

    
260
    if response is not None:
261
        return response
262

    
263
    next = reverse('astakos.im.views.project_list')
264
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
265
    return redirect(next)
266

    
267

    
268
@commit_on_success_strict()
269
def update_app_object(request, object_id, extra_context=None):
270
    try:
271
        return _update_object(
272
            request,
273
            object_id=object_id,
274
            template_name='im/projects/projectapplication_form.html',
275
            extra_context=extra_context,
276
            post_save_redirect=reverse('project_list'),
277
            form_class=ProjectApplicationForm,
278
            msg=_("The %(verbose_name)s has been received and is under "
279
                  "consideration."))
280
    except ProjectError as e:
281
        messages.error(request, e)
282

    
283

    
284
@require_http_methods(["GET", "POST"])
285
@cookie_fix
286
@valid_astakos_user_required
287
def project_app(request, application_id):
288
    return common_detail(request, application_id, project_view=False)
289

    
290

    
291
@require_http_methods(["GET", "POST"])
292
@cookie_fix
293
@valid_astakos_user_required
294
def project_detail(request, chain_id):
295
    return common_detail(request, chain_id)
296

    
297

    
298
@commit_on_success_strict()
299
def addmembers(request, chain_id, addmembers_form):
300
    if addmembers_form.is_valid():
301
        try:
302
            chain_id = int(chain_id)
303
            map(lambda u: enroll_member(chain_id,
304
                                        u,
305
                                        request_user=request.user),
306
                addmembers_form.valid_users)
307
        except ProjectError as e:
308
            messages.error(request, e)
309

    
310

    
311
MEMBERSHIP_STATUS_FILTER = {
312
    0: lambda x: x.requested(),
313
    1: lambda x: x.any_accepted(),
314
}
315

    
316

    
317
def common_detail(request, chain_or_app_id, project_view=True,
318
                  template_name='im/projects/project_detail.html',
319
                  members_status_filter=None):
320
    project = None
321
    approved_members_count = 0
322
    pending_members_count = 0
323
    remaining_memberships_count = None
324
    if project_view:
325
        chain_id = chain_or_app_id
326
        if request.method == 'POST':
327
            addmembers_form = AddProjectMembersForm(
328
                request.POST,
329
                chain_id=int(chain_id),
330
                request_user=request.user)
331
            with ExceptionHandler(request):
332
                addmembers(request, chain_id, addmembers_form)
333

    
334
            if addmembers_form.is_valid():
335
                addmembers_form = AddProjectMembersForm()  # clear form data
336
        else:
337
            addmembers_form = AddProjectMembersForm()  # initialize form
338

    
339
        project = get_object_or_404(Project, pk=chain_id)
340
        application = project.application
341
        if project:
342
            members = project.projectmembership_set
343
            approved_members_count = project.members_count()
344
            pending_members_count = project.count_pending_memberships()
345
            _limit = application.limit_on_members_number
346
            if _limit is not None:
347
                remaining_memberships_count = \
348
                    max(0, _limit - approved_members_count)
349
            flt = MEMBERSHIP_STATUS_FILTER.get(members_status_filter)
350
            if flt is not None:
351
                members = flt(members)
352
            else:
353
                members = members.associated()
354
            members = members.select_related()
355
            members_table = tables.ProjectMembersTable(project,
356
                                                       members,
357
                                                       user=request.user,
358
                                                       prefix="members_")
359
            RequestConfig(request, paginate={"per_page": settings.PAGINATE_BY}
360
                          ).configure(members_table)
361

    
362
        else:
363
            members_table = None
364

    
365
    else:
366
        # is application
367
        application_id = chain_or_app_id
368
        application = get_object_or_404(ProjectApplication, pk=application_id)
369
        members_table = None
370
        addmembers_form = None
371

    
372
    user = request.user
373
    is_project_admin = user.is_project_admin(application_id=application.id)
374
    is_owner = user.owns_application(application)
375
    if not (is_owner or is_project_admin) and not project_view:
376
        m = _(astakos_messages.NOT_ALLOWED)
377
        raise PermissionDenied(m)
378

    
379
    if (
380
        not (is_owner or is_project_admin) and project_view and
381
        not user.non_owner_can_view(project)
382
    ):
383
        m = _(astakos_messages.NOT_ALLOWED)
384
        raise PermissionDenied(m)
385

    
386
    membership = user.get_membership(project) if project else None
387
    membership_id = membership.id if membership else None
388
    mem_display = user.membership_display(project) if project else None
389
    can_join_req = can_join_request(project, user) if project else False
390
    can_leave_req = can_leave_request(project, user) if project else False
391

    
392
    return object_detail(
393
        request,
394
        queryset=ProjectApplication.objects.select_related(),
395
        object_id=application.id,
396
        template_name=template_name,
397
        extra_context={
398
            'project_view': project_view,
399
            'chain_id': chain_or_app_id,
400
            'application': application,
401
            'addmembers_form': addmembers_form,
402
            'approved_members_count': approved_members_count,
403
            'pending_members_count': pending_members_count,
404
            'members_table': members_table,
405
            'owner_mode': is_owner,
406
            'admin_mode': is_project_admin,
407
            'mem_display': mem_display,
408
            'membership_id': membership_id,
409
            'can_join_request': can_join_req,
410
            'can_leave_request': can_leave_req,
411
            'members_status_filter': members_status_filter,
412
            'remaining_memberships_count': remaining_memberships_count,
413
        })
414

    
415

    
416
@require_http_methods(["GET", "POST"])
417
@cookie_fix
418
@valid_astakos_user_required
419
def project_search(request):
420
    q = request.GET.get('q', '')
421
    form = ProjectSearchForm()
422
    q = q.strip()
423

    
424
    if request.method == "POST":
425
        form = ProjectSearchForm(request.POST)
426
        if form.is_valid():
427
            q = form.cleaned_data['q'].strip()
428
        else:
429
            q = None
430

    
431
    if q is None:
432
        projects = Project.objects.none()
433
    else:
434
        accepted = request.user.projectmembership_set.filter(
435
            state__in=ProjectMembership.ACCEPTED_STATES).values_list(
436
                'project', flat=True)
437

    
438
        projects = Project.objects.search_by_name(q)
439
        projects = projects.filter(Project.o_state_q(Project.O_ACTIVE))
440
        projects = projects.exclude(id__in=accepted).select_related(
441
            'application', 'application__owner', 'application__applicant')
442

    
443
    table = get_user_projects_table(projects, user=request.user,
444
                                    prefix="my_projects_")
445
    if request.method == "POST":
446
        table.caption = _('SEARCH RESULTS')
447
    else:
448
        table.caption = _('ALL PROJECTS')
449

    
450
    RequestConfig(request,
451
                  paginate={"per_page": settings.PAGINATE_BY}).configure(table)
452

    
453
    return object_list(
454
        request,
455
        projects,
456
        template_name='im/projects/project_list.html',
457
        extra_context={
458
            'form': form,
459
            'is_search': True,
460
            'q': q,
461
            'table': table
462
        })
463

    
464

    
465
@require_http_methods(["POST"])
466
@cookie_fix
467
@valid_astakos_user_required
468
def project_join(request, chain_id):
469
    next = request.GET.get('next')
470
    if not next:
471
        next = reverse('astakos.im.views.project_detail',
472
                       args=(chain_id,))
473

    
474
    with ExceptionHandler(request):
475
        _project_join(request, chain_id)
476

    
477
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
478
    return redirect(next)
479

    
480

    
481
@commit_on_success_strict()
482
def _project_join(request, chain_id):
483
    try:
484
        chain_id = int(chain_id)
485
        membership = join_project(chain_id, request.user)
486
        if membership.state != membership.REQUESTED:
487
            m = _(astakos_messages.USER_JOINED_PROJECT)
488
        else:
489
            m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
490
        messages.success(request, m)
491
    except ProjectError as e:
492
        messages.error(request, e)
493

    
494

    
495
@require_http_methods(["POST"])
496
@cookie_fix
497
@valid_astakos_user_required
498
def project_leave(request, memb_id):
499
    next = request.GET.get('next')
500
    if not next:
501
        next = reverse('astakos.im.views.project_list')
502

    
503
    with ExceptionHandler(request):
504
        _project_leave(request, memb_id)
505

    
506
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
507
    return redirect(next)
508

    
509

    
510
@commit_on_success_strict()
511
def _project_leave(request, memb_id):
512
    try:
513
        memb_id = int(memb_id)
514
        auto_accepted = leave_project(memb_id, request.user)
515
        if auto_accepted:
516
            m = _(astakos_messages.USER_LEFT_PROJECT)
517
        else:
518
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
519
        messages.success(request, m)
520
    except ProjectError as e:
521
        messages.error(request, e)
522

    
523

    
524
@require_http_methods(["POST"])
525
@cookie_fix
526
@valid_astakos_user_required
527
def project_cancel_member(request, memb_id):
528
    next = request.GET.get('next')
529
    if not next:
530
        next = reverse('astakos.im.views.project_list')
531

    
532
    with ExceptionHandler(request):
533
        _project_cancel_member(request, memb_id)
534

    
535
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
536
    return redirect(next)
537

    
538

    
539
@commit_on_success_strict()
540
def _project_cancel_member(request, memb_id):
541
    try:
542
        cancel_membership(memb_id, request.user)
543
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
544
        messages.success(request, m)
545
    except ProjectError as e:
546
        messages.error(request, e)
547

    
548

    
549
@require_http_methods(["POST"])
550
@cookie_fix
551
@valid_astakos_user_required
552
def project_accept_member(request, memb_id):
553

    
554
    with ExceptionHandler(request):
555
        _project_accept_member(request, memb_id)
556

    
557
    return redirect_back(request, 'project_list')
558

    
559

    
560
@commit_on_success_strict()
561
def _project_accept_member(request, memb_id):
562
    try:
563
        memb_id = int(memb_id)
564
        m = accept_membership(memb_id, request.user)
565
    except ProjectError as e:
566
        messages.error(request, e)
567

    
568
    else:
569
        email = escape(m.person.email)
570
        msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
571
        messages.success(request, msg)
572

    
573

    
574
@require_http_methods(["POST"])
575
@cookie_fix
576
@valid_astakos_user_required
577
def project_remove_member(request, memb_id):
578

    
579
    with ExceptionHandler(request):
580
        _project_remove_member(request, memb_id)
581

    
582
    return redirect_back(request, 'project_list')
583

    
584

    
585
@commit_on_success_strict()
586
def _project_remove_member(request, memb_id):
587
    try:
588
        memb_id = int(memb_id)
589
        m = remove_membership(memb_id, request.user)
590
    except ProjectError as e:
591
        messages.error(request, e)
592
    else:
593
        email = escape(m.person.email)
594
        msg = _(astakos_messages.USER_MEMBERSHIP_REMOVED) % email
595
        messages.success(request, msg)
596

    
597

    
598
@require_http_methods(["POST"])
599
@cookie_fix
600
@valid_astakos_user_required
601
def project_reject_member(request, memb_id):
602

    
603
    with ExceptionHandler(request):
604
        _project_reject_member(request, memb_id)
605

    
606
    return redirect_back(request, 'project_list')
607

    
608

    
609
@commit_on_success_strict()
610
def _project_reject_member(request, memb_id):
611
    try:
612
        memb_id = int(memb_id)
613
        m = reject_membership(memb_id, request.user)
614
    except ProjectError as e:
615
        messages.error(request, e)
616
    else:
617
        email = escape(m.person.email)
618
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email
619
        messages.success(request, msg)
620

    
621

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

    
628
    if not request.user.is_project_admin():
629
        m = _(astakos_messages.NOT_ALLOWED)
630
        raise PermissionDenied(m)
631

    
632
    try:
633
        ProjectApplication.objects.get(id=application_id)
634
    except ProjectApplication.DoesNotExist:
635
        raise Http404
636

    
637
    with ExceptionHandler(request):
638
        _project_app_approve(request, application_id)
639

    
640
    chain_id = get_related_project_id(application_id)
641
    if not chain_id:
642
        return redirect_back(request, 'project_list')
643
    return redirect(reverse('project_detail', args=(chain_id,)))
644

    
645

    
646
@commit_on_success_strict()
647
def _project_app_approve(request, application_id):
648
    approve_application(application_id)
649

    
650

    
651
@require_http_methods(["POST"])
652
@signed_terms_required
653
@login_required
654
@cookie_fix
655
def project_app_deny(request, application_id):
656

    
657
    reason = request.POST.get('reason', None)
658
    if not reason:
659
        reason = None
660

    
661
    if not request.user.is_project_admin():
662
        m = _(astakos_messages.NOT_ALLOWED)
663
        raise PermissionDenied(m)
664

    
665
    try:
666
        ProjectApplication.objects.get(id=application_id)
667
    except ProjectApplication.DoesNotExist:
668
        raise Http404
669

    
670
    with ExceptionHandler(request):
671
        _project_app_deny(request, application_id, reason)
672

    
673
    return redirect(reverse('project_list'))
674

    
675

    
676
@commit_on_success_strict()
677
def _project_app_deny(request, application_id, reason):
678
    deny_application(application_id, reason=reason)
679

    
680

    
681
@require_http_methods(["POST"])
682
@signed_terms_required
683
@login_required
684
@cookie_fix
685
def project_app_dismiss(request, application_id):
686
    try:
687
        app = ProjectApplication.objects.get(id=application_id)
688
    except ProjectApplication.DoesNotExist:
689
        raise Http404
690

    
691
    if not request.user.owns_application(app):
692
        m = _(astakos_messages.NOT_ALLOWED)
693
        raise PermissionDenied(m)
694

    
695
    with ExceptionHandler(request):
696
        _project_app_dismiss(request, application_id)
697

    
698
    chain_id = None
699
    chain_id = get_related_project_id(application_id)
700
    if chain_id:
701
        next = reverse('project_detail', args=(chain_id,))
702
    else:
703
        next = reverse('project_list')
704
    return redirect(next)
705

    
706

    
707
def _project_app_dismiss(request, application_id):
708
    # XXX: dismiss application also does authorization
709
    dismiss_application(application_id, request_user=request.user)
710

    
711

    
712
@require_http_methods(["GET", "POST"])
713
@valid_astakos_user_required
714
def project_members(request, chain_id, members_status_filter=None,
715
                    template_name='im/projects/project_members.html'):
716
    project = get_object_or_404(Project, pk=chain_id)
717

    
718
    user = request.user
719
    if not user.owns_project(project) and not user.is_project_admin():
720
        return redirect(reverse('index'))
721

    
722
    return common_detail(request, chain_id,
723
                         members_status_filter=members_status_filter,
724
                         template_name=template_name)
725

    
726

    
727
@require_http_methods(["POST"])
728
@valid_astakos_user_required
729
def project_members_action(request, chain_id, action=None, redirect_to=''):
730

    
731
    actions_map = {
732
        'remove': _project_remove_member,
733
        'accept': _project_accept_member,
734
        'reject': _project_reject_member
735
    }
736

    
737
    if not action in actions_map.keys():
738
        raise PermissionDenied
739

    
740
    member_ids = request.POST.getlist('members')
741
    project = get_object_or_404(Project, pk=chain_id)
742

    
743
    user = request.user
744
    if not user.owns_project(project) and not user.is_project_admin():
745
        return redirect(reverse('index'))
746

    
747
    logger.info("Batch members action from %s (chain: %r, action: %s, "
748
                "members: %r)", user.log_display, chain_id, action, member_ids)
749

    
750
    action_func = actions_map.get(action)
751
    for member_id in member_ids:
752
        member_id = int(member_id)
753
        with ExceptionHandler(request):
754
            action_func(request, member_id)
755

    
756
    return redirect_back(request, 'project_list')