Statistics
| Branch: | Tag: | Revision:

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

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

    
283

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

    
290

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

    
297

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

    
310

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

    
316

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

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

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

    
362
        else:
363
            members_table = None
364

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

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

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

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

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

    
415

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

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

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

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

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

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

    
461

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

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

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

    
477

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

    
491

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

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

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

    
506

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

    
520

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

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

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

    
535

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

    
545

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

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

    
554
    return redirect_back(request, 'project_list')
555

    
556

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

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

    
570

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

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

    
579
    return redirect_back(request, 'project_list')
580

    
581

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

    
594

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

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

    
603
    return redirect_back(request, 'project_list')
604

    
605

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

    
618

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

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

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

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

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

    
642

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

    
647

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

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

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

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

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

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

    
672

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

    
677

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

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

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

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

    
703

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

    
708

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

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

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

    
723

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

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

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

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

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

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

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

    
753
    return redirect_back(request, 'project_list')