Statistics
| Branch: | Tag: | Revision:

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

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
        summary = 'im/projects/projectapplication_form_summary.html'
130
        return _create_object(
131
            request,
132
            template_name='im/projects/projectapplication_form.html',
133
            summary_template_name=summary,
134
            extra_context=extra_context,
135
            post_save_redirect=reverse('project_list'),
136
            form_class=ProjectApplicationForm,
137
            msg=_("The %(verbose_name)s has been received and "
138
                  "is under consideration."))
139
    except ProjectError as e:
140
        messages.error(request, e)
141

    
142

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

    
156

    
157
@require_http_methods(["GET"])
158
@cookie_fix
159
@valid_astakos_user_required
160
def project_list(request):
161
    projects = Project.objects.user_accessible_projects(request.user)
162
    table = get_user_projects_table(projects, user=request.user,
163
                                    prefix="my_projects_")
164
    return object_list(
165
        request,
166
        projects,
167
        template_name='im/projects/project_list.html',
168
        extra_context={
169
            'is_search': False,
170
            'table': table,
171
        })
172

    
173

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

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

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

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

    
193

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

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

    
209

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

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

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

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

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

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

    
259
    if response is not None:
260
        return response
261

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

    
266

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

    
284

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

    
291

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

    
298

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

    
311

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

    
317

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

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

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

    
363
        else:
364
            members_table = None
365

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

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

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

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

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

    
416

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

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

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

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

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

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

    
462

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

    
472
    with ExceptionHandler(request):
473
        _project_join(request, chain_id)
474

    
475
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
476
    return redirect(next)
477

    
478

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

    
492

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

    
501
    with ExceptionHandler(request):
502
        _project_leave(request, memb_id)
503

    
504
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
505
    return redirect(next)
506

    
507

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

    
521

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

    
530
    with ExceptionHandler(request):
531
        _project_cancel_member(request, memb_id)
532

    
533
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
534
    return redirect(next)
535

    
536

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

    
546

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

    
552
    with ExceptionHandler(request):
553
        _project_accept_member(request, memb_id)
554

    
555
    return redirect_back(request, 'project_list')
556

    
557

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

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

    
571

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

    
577
    with ExceptionHandler(request):
578
        _project_remove_member(request, memb_id)
579

    
580
    return redirect_back(request, 'project_list')
581

    
582

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

    
595

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

    
601
    with ExceptionHandler(request):
602
        _project_reject_member(request, memb_id)
603

    
604
    return redirect_back(request, 'project_list')
605

    
606

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

    
619

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

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

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

    
635
    with ExceptionHandler(request):
636
        _project_app_approve(request, application_id)
637

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

    
643

    
644
@commit_on_success_strict()
645
def _project_app_approve(request, application_id):
646
    approve_application(application_id)
647

    
648

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

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

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

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

    
668
    with ExceptionHandler(request):
669
        _project_app_deny(request, application_id, reason)
670

    
671
    return redirect(reverse('project_list'))
672

    
673

    
674
@commit_on_success_strict()
675
def _project_app_deny(request, application_id, reason):
676
    deny_application(application_id, reason=reason)
677

    
678

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

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

    
693
    with ExceptionHandler(request):
694
        _project_app_dismiss(request, application_id)
695

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

    
704

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

    
709

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

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

    
720
    return common_detail(request, chain_id,
721
                         members_status_filter=members_status_filter,
722
                         template_name=template_name)
723

    
724

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

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

    
735
    if not action in actions_map.keys():
736
        raise PermissionDenied
737

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

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

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

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

    
754
    return redirect_back(request, 'project_list')