Statistics
| Branch: | Tag: | Revision:

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

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
from django.db import transaction
52

    
53
import astakos.im.messages as astakos_messages
54

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

    
72
logger = logging.getLogger(__name__)
73

    
74

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

    
81

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

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

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

    
117
    if response is not None:
118
        return response
119

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

    
124

    
125
@transaction.commit_on_success
126
def create_app_object(request, extra_context=None):
127
    try:
128
        summary = 'im/projects/projectapplication_form_summary.html'
129
        return _create_object(
130
            request,
131
            template_name='im/projects/projectapplication_form.html',
132
            summary_template_name=summary,
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
def get_user_projects_table(projects, user, prefix):
143
    apps = ProjectApplication.objects.pending_per_project(projects)
144
    memberships = user.projectmembership_set.one_per_project()
145
    objs = ProjectMembership.objects
146
    accepted_ms = objs.any_accepted_per_project(projects)
147
    requested_ms = objs.requested_per_project(projects)
148
    return tables.UserProjectsTable(projects, user=user,
149
                                    prefix=prefix,
150
                                    pending_apps=apps,
151
                                    memberships=memberships,
152
                                    accepted=accepted_ms,
153
                                    requested=requested_ms)
154

    
155

    
156
@require_http_methods(["GET"])
157
@cookie_fix
158
@valid_astakos_user_required
159
def project_list(request):
160
    projects = Project.objects.user_accessible_projects(request.user)
161
    table = (get_user_projects_table(projects, user=request.user,
162
                                     prefix="my_projects_")
163
             if list(projects) else None)
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
@transaction.commit_on_success
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()
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
@transaction.commit_on_success
269
def update_app_object(request, object_id, extra_context=None):
270
    try:
271
        summary = 'im/projects/projectapplication_form_summary.html'
272
        return _update_object(
273
            request,
274
            object_id=object_id,
275
            template_name='im/projects/projectapplication_form.html',
276
            summary_template_name=summary,
277
            extra_context=extra_context,
278
            post_save_redirect=reverse('project_list'),
279
            form_class=ProjectApplicationForm,
280
            msg=_("The %(verbose_name)s has been received and is under "
281
                  "consideration."))
282
    except ProjectError as e:
283
        messages.error(request, e)
284

    
285

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

    
292

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

    
299

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

    
312

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

    
318

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

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

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

    
364
        else:
365
            members_table = None
366

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

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

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

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

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

    
417

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

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

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

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

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

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

    
463

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

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

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

    
479

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

    
493

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

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

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

    
508

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

    
522

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

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

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

    
537

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

    
547

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

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

    
556
    return redirect_back(request, 'project_list')
557

    
558

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

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

    
572

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

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

    
581
    return redirect_back(request, 'project_list')
582

    
583

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

    
596

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

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

    
605
    return redirect_back(request, 'project_list')
606

    
607

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

    
620

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

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

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

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

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

    
644

    
645
@transaction.commit_on_success
646
def _project_app_approve(request, application_id):
647
    approve_application(application_id)
648

    
649

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

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

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

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

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

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

    
674

    
675
@transaction.commit_on_success
676
def _project_app_deny(request, application_id, reason):
677
    deny_application(application_id, reason=reason)
678

    
679

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

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

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

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

    
705

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

    
710

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

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

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

    
725

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

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

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

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

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

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

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

    
755
    return redirect_back(request, 'project_list')