Statistics
| Branch: | Tag: | Revision:

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

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

    
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
            summary_template_name='im/projects/projectapplication_form_summary.html',
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
    return object_list(
164
        request,
165
        projects,
166
        template_name='im/projects/project_list.html',
167
        extra_context={
168
            'is_search': False,
169
            'table': table,
170
        })
171

    
172

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

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

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

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

    
192

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

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

    
208

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

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

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

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

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

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

    
258
    if response is not None:
259
        return response
260

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

    
265

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

    
282

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

    
289

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

    
296

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

    
309

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

    
315

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

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

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

    
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
@commit_on_success_strict()
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
@commit_on_success_strict()
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
@commit_on_success_strict()
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
@commit_on_success_strict()
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
@commit_on_success_strict()
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
@commit_on_success_strict()
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
@commit_on_success_strict()
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
@commit_on_success_strict()
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')