Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (24.7 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
        else:
362
            members_table = None
363

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

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

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

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

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

    
414

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

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

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

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

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

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

    
460

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

    
470
    with ExceptionHandler(request):
471
        _project_join(request, chain_id)
472

    
473
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
474
    return redirect(next)
475

    
476

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

    
490

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

    
499
    with ExceptionHandler(request):
500
        _project_leave(request, memb_id)
501

    
502
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
503
    return redirect(next)
504

    
505

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

    
519

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

    
528
    with ExceptionHandler(request):
529
        _project_cancel_member(request, memb_id)
530

    
531
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
532
    return redirect(next)
533

    
534

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

    
544

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

    
550
    with ExceptionHandler(request):
551
        _project_accept_member(request, memb_id)
552

    
553
    return redirect_back(request, 'project_list')
554

    
555

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

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

    
569

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

    
575
    with ExceptionHandler(request):
576
        _project_remove_member(request, memb_id)
577

    
578
    return redirect_back(request, 'project_list')
579

    
580

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

    
593

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

    
599
    with ExceptionHandler(request):
600
        _project_reject_member(request, memb_id)
601

    
602
    return redirect_back(request, 'project_list')
603

    
604

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

    
617

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

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

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

    
633
    with ExceptionHandler(request):
634
        _project_app_approve(request, application_id)
635

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

    
641

    
642
@transaction.commit_on_success
643
def _project_app_approve(request, application_id):
644
    approve_application(application_id)
645

    
646

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

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

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

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

    
666
    with ExceptionHandler(request):
667
        _project_app_deny(request, application_id, reason)
668

    
669
    return redirect(reverse('project_list'))
670

    
671

    
672
@transaction.commit_on_success
673
def _project_app_deny(request, application_id, reason):
674
    deny_application(application_id, reason=reason)
675

    
676

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

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

    
691
    with ExceptionHandler(request):
692
        _project_app_dismiss(request, application_id)
693

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

    
702

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

    
707

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

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

    
718
    return common_detail(request, chain_id,
719
                         members_status_filter=members_status_filter,
720
                         template_name=template_name)
721

    
722

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

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

    
733
    if not action in actions_map.keys():
734
        raise PermissionDenied
735

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

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

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

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

    
752
    return redirect_back(request, 'project_list')