Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (21.4 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.contrib.auth.decorators import login_required
44
from django.core.urlresolvers import reverse
45
from django.http import Http404
46
from django.shortcuts import redirect
47
from django.utils.html import escape
48
from django.utils.translation import ugettext as _
49
from django.views.generic.list_detail import object_list, object_detail
50
from django.core.exceptions import PermissionDenied
51
from django.views.decorators.http import require_http_methods
52
from django.db.models import Q
53

    
54
from snf_django.lib.db.transaction import commit_on_success_strict
55

    
56
import astakos.im.messages as astakos_messages
57

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

    
74
logger = logging.getLogger(__name__)
75

    
76

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

    
83

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

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

    
115
    response = None
116
    with ExceptionHandler(request):
117
        response = _create_object(
118
            request,
119
            template_name='im/projects/projectapplication_form.html',
120
            extra_context=extra_context,
121
            post_save_redirect=reverse('project_list'),
122
            form_class=ProjectApplicationForm,
123
            msg=_("The %(verbose_name)s has been received and "
124
                  "is under consideration."),
125
            )
126

    
127
    if response is not None:
128
        return response
129

    
130
    next = reverse('astakos.im.views.project_list')
131
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
132
    return redirect(next)
133

    
134

    
135
@require_http_methods(["GET"])
136
@cookie_fix
137
@valid_astakos_user_required
138
def project_list(request):
139
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
140
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
141
                                                prefix="my_projects_")
142
    RequestConfig(request, paginate={"per_page": settings.PAGINATE_BY}).configure(table)
143

    
144
    return object_list(
145
        request,
146
        projects,
147
        template_name='im/projects/project_list.html',
148
        extra_context={
149
            'is_search':False,
150
            'table': table,
151
        })
152

    
153

    
154
@require_http_methods(["POST"])
155
@cookie_fix
156
@valid_astakos_user_required
157
def project_app_cancel(request, application_id):
158
    next = request.GET.get('next')
159
    chain_id = None
160

    
161
    with ExceptionHandler(request):
162
        chain_id = _project_app_cancel(request, application_id)
163

    
164
    if not next:
165
        if chain_id:
166
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
167
        else:
168
            next = reverse('astakos.im.views.project_list')
169

    
170
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
171
    return redirect(next)
172

    
173
@commit_on_success_strict()
174
def _project_app_cancel(request, application_id):
175
    chain_id = None
176
    try:
177
        application_id = int(application_id)
178
        chain_id = get_related_project_id(application_id)
179
        cancel_application(application_id, request.user)
180
    except (IOError, PermissionDenied), e:
181
        messages.error(request, e)
182

    
183
    else:
184
        msg = _(astakos_messages.APPLICATION_CANCELLED)
185
        messages.success(request, msg)
186
        return chain_id
187

    
188

    
189
@require_http_methods(["GET", "POST"])
190
@cookie_fix
191
@valid_astakos_user_required
192
def project_modify(request, application_id):
193

    
194
    try:
195
        app = ProjectApplication.objects.get(id=application_id)
196
    except ProjectApplication.DoesNotExist:
197
        raise Http404
198

    
199
    user = request.user
200
    if not (user.owns_application(app) or user.is_project_admin(app.id)):
201
        m = _(astakos_messages.NOT_ALLOWED)
202
        raise PermissionDenied(m)
203

    
204
    if not user.is_project_admin():
205
        owner = app.owner
206
        ok, limit = check_pending_app_quota(owner, precursor=app)
207
        if not ok:
208
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
209
            messages.error(request, m)
210
            next = reverse('astakos.im.views.project_list')
211
            next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
212
            return redirect(next)
213

    
214
    details_fields = ["name", "homepage", "description", "start_date",
215
                      "end_date", "comments"]
216
    membership_fields = ["member_join_policy", "member_leave_policy",
217
                         "limit_on_members_number"]
218
    resource_catalog, resource_groups = _resources_catalog(for_project=True)
219
    if resource_catalog is False:
220
        # on fail resource_groups contains the result object
221
        result = resource_groups
222
        messages.error(request, 'Unable to retrieve system resources: %s' %
223
                       result.reason)
224
    extra_context = {
225
        'resource_catalog': resource_catalog,
226
        'resource_groups': resource_groups,
227
        'show_form': True,
228
        'details_fields': details_fields,
229
        'update_form': True,
230
        'membership_fields': membership_fields
231
    }
232

    
233
    response = None
234
    with ExceptionHandler(request):
235
        response = _update_object(
236
            request,
237
            object_id=application_id,
238
            template_name='im/projects/projectapplication_form.html',
239
            extra_context=extra_context,
240
            post_save_redirect=reverse('project_list'),
241
            form_class=ProjectApplicationForm,
242
            msg=_("The %(verbose_name)s has been received and is under "
243
                  "consideration."))
244

    
245
    if response is not None:
246
        return response
247

    
248
    next = reverse('astakos.im.views.project_list')
249
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
250
    return redirect(next)
251

    
252
@require_http_methods(["GET", "POST"])
253
@cookie_fix
254
@valid_astakos_user_required
255
def project_app(request, application_id):
256
    return common_detail(request, application_id, project_view=False)
257

    
258
@require_http_methods(["GET", "POST"])
259
@cookie_fix
260
@valid_astakos_user_required
261
def project_detail(request, chain_id):
262
    return common_detail(request, chain_id)
263

    
264
@commit_on_success_strict()
265
def addmembers(request, chain_id, addmembers_form):
266
    if addmembers_form.is_valid():
267
        try:
268
            chain_id = int(chain_id)
269
            map(lambda u: enroll_member(
270
                    chain_id,
271
                    u,
272
                    request_user=request.user),
273
                addmembers_form.valid_users)
274
        except (IOError, PermissionDenied), e:
275
            messages.error(request, e)
276

    
277
def common_detail(request, chain_or_app_id, project_view=True):
278
    project = None
279
    if project_view:
280
        chain_id = chain_or_app_id
281
        if request.method == 'POST':
282
            addmembers_form = AddProjectMembersForm(
283
                request.POST,
284
                chain_id=int(chain_id),
285
                request_user=request.user)
286
            with ExceptionHandler(request):
287
                addmembers(request, chain_id, addmembers_form)
288

    
289
            if addmembers_form.is_valid():
290
                addmembers_form = AddProjectMembersForm()  # clear form data
291
        else:
292
            addmembers_form = AddProjectMembersForm()  # initialize form
293

    
294
        project, application = get_by_chain_or_404(chain_id)
295
        if project:
296
            members = project.projectmembership_set.select_related()
297
            members_table = tables.ProjectMembersTable(project,
298
                                                       members,
299
                                                       user=request.user,
300
                                                       prefix="members_")
301
            RequestConfig(request, paginate={"per_page": settings.PAGINATE_BY}
302
                          ).configure(members_table)
303

    
304
        else:
305
            members_table = None
306

    
307
    else: # is application
308
        application_id = chain_or_app_id
309
        application = get_object_or_404(ProjectApplication, pk=application_id)
310
        members_table = None
311
        addmembers_form = None
312

    
313
    modifications_table = None
314

    
315
    user = request.user
316
    is_project_admin = user.is_project_admin(application_id=application.id)
317
    is_owner = user.owns_application(application)
318
    if not (is_owner or is_project_admin) and not project_view:
319
        m = _(astakos_messages.NOT_ALLOWED)
320
        raise PermissionDenied(m)
321

    
322
    if (not (is_owner or is_project_admin) and project_view and
323
        not user.non_owner_can_view(project)):
324
        m = _(astakos_messages.NOT_ALLOWED)
325
        raise PermissionDenied(m)
326

    
327
    following_applications = list(application.pending_modifications())
328
    following_applications.reverse()
329
    modifications_table = (
330
        tables.ProjectModificationApplicationsTable(following_applications,
331
                                                    user=request.user,
332
                                                    prefix="modifications_"))
333

    
334
    mem_display = user.membership_display(project) if project else None
335
    can_join_req = can_join_request(project, user) if project else False
336
    can_leave_req = can_leave_request(project, user) if project else False
337

    
338
    return object_detail(
339
        request,
340
        queryset=ProjectApplication.objects.select_related(),
341
        object_id=application.id,
342
        template_name='im/projects/project_detail.html',
343
        extra_context={
344
            'project_view': project_view,
345
            'addmembers_form':addmembers_form,
346
            'members_table': members_table,
347
            'owner_mode': is_owner,
348
            'admin_mode': is_project_admin,
349
            'modifications_table': modifications_table,
350
            'mem_display': mem_display,
351
            'can_join_request': can_join_req,
352
            'can_leave_request': can_leave_req,
353
            })
354

    
355
@require_http_methods(["GET", "POST"])
356
@cookie_fix
357
@valid_astakos_user_required
358
def project_search(request):
359
    q = request.GET.get('q', '')
360
    form = ProjectSearchForm()
361
    q = q.strip()
362

    
363
    if request.method == "POST":
364
        form = ProjectSearchForm(request.POST)
365
        if form.is_valid():
366
            q = form.cleaned_data['q'].strip()
367
        else:
368
            q = None
369

    
370
    if q is None:
371
        projects = ProjectApplication.objects.none()
372
    else:
373
        accepted_projects = request.user.projectmembership_set.filter(
374
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
375
        projects = ProjectApplication.objects.search_by_name(q)
376
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
377
        projects = projects.exclude(project__in=accepted_projects)
378

    
379
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
380
                                                prefix="my_projects_")
381
    if request.method == "POST":
382
        table.caption = _('SEARCH RESULTS')
383
    else:
384
        table.caption = _('ALL PROJECTS')
385

    
386
    RequestConfig(request, paginate={"per_page": settings.PAGINATE_BY}).configure(table)
387

    
388
    return object_list(
389
        request,
390
        projects,
391
        template_name='im/projects/project_list.html',
392
        extra_context={
393
          'form': form,
394
          'is_search': True,
395
          'q': q,
396
          'table': table
397
        })
398

    
399
@require_http_methods(["POST"])
400
@cookie_fix
401
@valid_astakos_user_required
402
def project_join(request, chain_id):
403
    next = request.GET.get('next')
404
    if not next:
405
        next = reverse('astakos.im.views.project_detail',
406
                       args=(chain_id,))
407

    
408
    with ExceptionHandler(request):
409
        _project_join(request, chain_id)
410

    
411

    
412
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
413
    return redirect(next)
414

    
415

    
416
@commit_on_success_strict()
417
def _project_join(request, chain_id):
418
    try:
419
        chain_id = int(chain_id)
420
        auto_accepted = join_project(chain_id, request.user)
421
        if auto_accepted:
422
            m = _(astakos_messages.USER_JOINED_PROJECT)
423
        else:
424
            m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
425
        messages.success(request, m)
426
    except (IOError, PermissionDenied), e:
427
        messages.error(request, e)
428

    
429

    
430
@require_http_methods(["POST"])
431
@cookie_fix
432
@valid_astakos_user_required
433
def project_leave(request, chain_id):
434
    next = request.GET.get('next')
435
    if not next:
436
        next = reverse('astakos.im.views.project_list')
437

    
438
    with ExceptionHandler(request):
439
        _project_leave(request, chain_id)
440

    
441
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
442
    return redirect(next)
443

    
444

    
445
@commit_on_success_strict()
446
def _project_leave(request, chain_id):
447
    try:
448
        chain_id = int(chain_id)
449
        auto_accepted = leave_project(chain_id, request.user)
450
        if auto_accepted:
451
            m = _(astakos_messages.USER_LEFT_PROJECT)
452
        else:
453
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
454
        messages.success(request, m)
455
    except (IOError, PermissionDenied), e:
456
        messages.error(request, e)
457

    
458

    
459
@require_http_methods(["POST"])
460
@cookie_fix
461
@valid_astakos_user_required
462
def project_cancel(request, chain_id):
463
    next = request.GET.get('next')
464
    if not next:
465
        next = reverse('astakos.im.views.project_list')
466

    
467
    with ExceptionHandler(request):
468
        _project_cancel(request, chain_id)
469

    
470
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
471
    return redirect(next)
472

    
473

    
474
@commit_on_success_strict()
475
def _project_cancel(request, chain_id):
476
    try:
477
        chain_id = int(chain_id)
478
        cancel_membership(chain_id, request.user)
479
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
480
        messages.success(request, m)
481
    except (IOError, PermissionDenied), e:
482
        messages.error(request, e)
483

    
484

    
485

    
486
@require_http_methods(["POST"])
487
@cookie_fix
488
@valid_astakos_user_required
489
def project_accept_member(request, chain_id, memb_id):
490

    
491
    with ExceptionHandler(request):
492
        _project_accept_member(request, chain_id, memb_id)
493

    
494
    return redirect(reverse('project_detail', args=(chain_id,)))
495

    
496

    
497
@commit_on_success_strict()
498
def _project_accept_member(request, chain_id, memb_id):
499
    try:
500
        chain_id = int(chain_id)
501
        memb_id = int(memb_id)
502
        m = accept_membership(chain_id, memb_id, request.user)
503
    except (IOError, PermissionDenied), e:
504
        messages.error(request, e)
505

    
506
    else:
507
        email = escape(m.person.email)
508
        msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
509
        messages.success(request, msg)
510

    
511

    
512
@require_http_methods(["POST"])
513
@cookie_fix
514
@valid_astakos_user_required
515
def project_remove_member(request, chain_id, memb_id):
516

    
517
    with ExceptionHandler(request):
518
        _project_remove_member(request, chain_id, memb_id)
519

    
520
    return redirect(reverse('project_detail', args=(chain_id,)))
521

    
522

    
523
@commit_on_success_strict()
524
def _project_remove_member(request, chain_id, memb_id):
525
    try:
526
        chain_id = int(chain_id)
527
        memb_id = int(memb_id)
528
        m = remove_membership(chain_id, memb_id, request.user)
529
    except (IOError, PermissionDenied), e:
530
        messages.error(request, e)
531
    else:
532
        email = escape(m.person.email)
533
        msg = _(astakos_messages.USER_MEMBERSHIP_REMOVED) % email
534
        messages.success(request, msg)
535

    
536

    
537
@require_http_methods(["POST"])
538
@cookie_fix
539
@valid_astakos_user_required
540
def project_reject_member(request, chain_id, memb_id):
541

    
542
    with ExceptionHandler(request):
543
        _project_reject_member(request, chain_id, memb_id)
544

    
545
    return redirect(reverse('project_detail', args=(chain_id,)))
546

    
547

    
548
@commit_on_success_strict()
549
def _project_reject_member(request, chain_id, memb_id):
550
    try:
551
        chain_id = int(chain_id)
552
        memb_id = int(memb_id)
553
        m = reject_membership(chain_id, memb_id, request.user)
554
    except (IOError, PermissionDenied), e:
555
        messages.error(request, e)
556
    else:
557
        email = escape(m.person.email)
558
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email
559
        messages.success(request, msg)
560

    
561

    
562
@require_http_methods(["POST"])
563
@signed_terms_required
564
@login_required
565
@cookie_fix
566
def project_app_approve(request, application_id):
567

    
568
    if not request.user.is_project_admin():
569
        m = _(astakos_messages.NOT_ALLOWED)
570
        raise PermissionDenied(m)
571

    
572
    try:
573
        app = ProjectApplication.objects.get(id=application_id)
574
    except ProjectApplication.DoesNotExist:
575
        raise Http404
576

    
577
    with ExceptionHandler(request):
578
        _project_app_approve(request, application_id)
579

    
580
    chain_id = get_related_project_id(application_id)
581
    return redirect(reverse('project_detail', args=(chain_id,)))
582

    
583

    
584
@commit_on_success_strict()
585
def _project_app_approve(request, application_id):
586
    approve_application(application_id)
587

    
588

    
589
@require_http_methods(["POST"])
590
@signed_terms_required
591
@login_required
592
@cookie_fix
593
def project_app_deny(request, application_id):
594

    
595
    reason = request.POST.get('reason', None)
596
    if not reason:
597
        reason = None
598

    
599
    if not request.user.is_project_admin():
600
        m = _(astakos_messages.NOT_ALLOWED)
601
        raise PermissionDenied(m)
602

    
603
    try:
604
        app = ProjectApplication.objects.get(id=application_id)
605
    except ProjectApplication.DoesNotExist:
606
        raise Http404
607

    
608
    with ExceptionHandler(request):
609
        _project_app_deny(request, application_id, reason)
610

    
611
    return redirect(reverse('project_list'))
612

    
613

    
614
@commit_on_success_strict()
615
def _project_app_deny(request, application_id, reason):
616
    deny_application(application_id, reason=reason)
617

    
618

    
619
@require_http_methods(["POST"])
620
@signed_terms_required
621
@login_required
622
@cookie_fix
623
def project_app_dismiss(request, application_id):
624
    try:
625
        app = ProjectApplication.objects.get(id=application_id)
626
    except ProjectApplication.DoesNotExist:
627
        raise Http404
628

    
629
    if not request.user.owns_application(app):
630
        m = _(astakos_messages.NOT_ALLOWED)
631
        raise PermissionDenied(m)
632

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

    
636
    chain_id = None
637
    chain_id = get_related_project_id(application_id)
638
    if chain_id:
639
        next = reverse('project_detail', args=(chain_id,))
640
    else:
641
        next = reverse('project_list')
642
    return redirect(next)
643

    
644

    
645
def _project_app_dismiss(request, application_id):
646
    # XXX: dismiss application also does authorization
647
    dismiss_application(application_id, request_user=request.user)