Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (22.2 kB)

1
# Copyright 2011-2014 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 functools import wraps
40
from django_tables2 import RequestConfig
41

    
42
from django.shortcuts import get_object_or_404, render_to_response
43
from django.contrib import messages
44
from django.core.urlresolvers import reverse
45
from django.http import Http404, HttpResponse
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 import transaction
53
from django.template import RequestContext
54

    
55
import astakos.im.messages as astakos_messages
56

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

    
76
from astakos.api import projects as api
77
from astakos.im import functions as project_actions
78

    
79
logger = logging.getLogger(__name__)
80

    
81

    
82
def no_transaction(func):
83
    return func
84

    
85

    
86
def project_view(get=True, post=False, transaction=False):
87
    methods = []
88
    if get:
89
        methods.append("GET")
90
    if post:
91
        methods.append("POST")
92

    
93
    if transaction:
94
        transaction_method = transaction.commit_on_success
95
    else:
96
        transaction_method = no_transaction
97

    
98
    def wrapper(func):
99
        return \
100
            wraps(func)(
101
                require_http_methods(methods)(
102
                    cookie_fix(
103
                        valid_astakos_user_required(
104
                            transaction_method(
105
                                func)))))
106
    return wrapper
107

    
108

    
109
@project_view()
110
def how_it_works(request):
111
    return render_response('im/how_it_works.html',
112
                           context_instance=get_context(request))
113

    
114

    
115
@project_view()
116
def project_list(request, template_name="im/projects/project_list.html"):
117
    query = api.make_project_query({})
118
    projects = api._get_projects(query, request_user=request.user)
119

    
120
    table = None
121
    if projects.count():
122
        table = get_user_projects_table(projects, user=request.user,
123
                                        prefix="my_projects_")
124

    
125
    context = {'is_search': False, 'table': table}
126
    return object_list(request, projects, template_name=template_name,
127
                       extra_context=context)
128

    
129

    
130
@project_view(post=True)
131
def project_add_or_modify(request, project_uuid=None):
132
    user = request.user
133

    
134
    # only check quota for non project admin users
135
    if not user.is_project_admin():
136
        ok, limit = check_pending_app_quota(user)
137
        if not ok:
138
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_ADD) % limit
139
            messages.error(request, m)
140
            return redirect(restrict_reverse(
141
                'astakos.im.views.project_list'))
142

    
143
    project = None
144
    if project_uuid:
145
        project = get_object_or_404(Project, uuid=project_uuid)
146

    
147
        if not user.owns_project(project) and not user.is_project_admin():
148
            m = _(astakos_messages.NOT_ALLOWED)
149
            raise PermissionDenied(m)
150

    
151
    details_fields = ["name", "homepage", "description", "start_date",
152
                      "end_date", "comments"]
153
    membership_fields = ["member_join_policy", "member_leave_policy",
154
                         "limit_on_members_number"]
155

    
156
    resource_catalog, resource_groups = _resources_catalog()
157
    resource_catalog_dict, resource_groups_dict = \
158
            _resources_catalog(as_dict=True)
159

    
160
    extra_context = {
161
        'resource_catalog': resource_catalog,
162
        'resource_groups': resource_groups,
163
        'resource_catalog_dict': resource_catalog_dict,
164
        'resource_groups_dict': resource_groups_dict,
165
        'show_form': True,
166
        'details_fields': details_fields,
167
        'membership_fields': membership_fields,
168
        'object': project
169
    }
170

    
171
    with transaction.commit_on_success():
172
        template_name = 'im/projects/projectapplication_form.html'
173
        summary_template_name = \
174
                'im/projects/projectapplication_form_summary.html'
175
        success_msg = _("The project application has been received and "
176
                        "is under consideration.")
177
        form_class = ProjectApplicationForm
178

    
179
        if project:
180
            template_name = 'im/projects/projectmodification_form.html'
181
            summary_template_name = \
182
                    'im/projects/projectmodification_form_summary.html'
183
            success_msg = _("The project modification has been received and "
184
                            "is under consideration.")
185
            form_class = ProjectModificationForm
186
            details_fields.remove('start_date')
187

    
188
        extra_context['edit'] = 0
189
        if request.method == 'POST':
190
            form = form_class(request.POST, request.FILES, instance=project)
191
            if form.is_valid():
192
                verify = request.GET.get('verify')
193
                edit = request.GET.get('edit')
194
                if verify == '1':
195
                    extra_context['show_form'] = False
196
                    extra_context['form_data'] = form.cleaned_data
197
                    template_name = summary_template_name
198
                elif edit == '1':
199
                    extra_context['show_form'] = True
200
                else:
201
                    new_object = form.save()
202
                    messages.success(request, success_msg,
203
                                     fail_silently=True)
204
                    return redirect(restrict_reverse('project_list'))
205
        else:
206
            form = form_class(instance=project)
207

    
208
        extra_context['form'] = form
209
        return render_to_response(template_name, extra_context,
210
                                  context_instance=RequestContext(request))
211

    
212

    
213
@project_view(get=False, post=True)
214
def project_app_cancel(request, project_uuid, application_id):
215
    with ExceptionHandler(request):
216
        with transaction.commit_on_success():
217
            cancel_application(application_id, project_uuid,
218
                               request_user=request.user)
219
            messages.success(request, _(astakos_messages.APPLICATION_CANCELLED))
220
    return redirect(reverse('project_list'))
221

    
222

    
223

    
224
@project_view(post=True)
225
def project_or_app_detail(request, project_uuid, app_id=None):
226

    
227
    project = get_object_or_404(Project, uuid=project_uuid)
228
    application = None
229
    if app_id:
230
        application = get_object_or_404(ProjectApplication, id=app_id)
231
        if request.method == "POST":
232
            raise PermissionDenied
233

    
234

    
235
    if project.state in [Project.O_PENDING] and not application:
236
        return redirect(reverse('project_app',
237
                                args=(project.uuid,
238
                                      project.last_application.id,)))
239

    
240
    members = project.projectmembership_set
241

    
242
    # handle members
243
    if request.method == 'POST':
244
        addmembers_form = AddProjectMembersForm(
245
            request.POST,
246
            project_id=project.pk,
247
            request_user=request.user)
248
        with ExceptionHandler(request):
249
            handle_valid_members_form(request, project.pk, addmembers_form)
250

    
251
        if addmembers_form.is_valid():
252
            addmembers_form = AddProjectMembersForm()  # clear form data
253
    else:
254
        addmembers_form = AddProjectMembersForm()  # initialize form
255

    
256
    approved_members_count = project.members_count()
257
    pending_members_count = project.count_pending_memberships()
258
    _limit = project.limit_on_members_number
259
    remaining_memberships_count = (max(0, _limit - approved_members_count)
260
                                   if _limit is not None else None)
261
    members = members.associated()
262
    members = members.select_related()
263
    members_table = tables.ProjectMembersTable(project,
264
                                               members,
265
                                               user=request.user,
266
                                               prefix="members_")
267
    paginate = {"per_page": settings.PAGINATE_BY}
268
    RequestConfig(request, paginate=paginate).configure(members_table)
269

    
270
    user = request.user
271
    is_project_admin = user.is_project_admin()
272
    is_owner = user.owns_project(project)
273

    
274
    if not (is_owner or is_project_admin) and \
275
            not user.non_owner_can_view(project):
276
        m = _(astakos_messages.NOT_ALLOWED)
277
        raise PermissionDenied(m)
278

    
279
    membership = user.get_membership(project) if project else None
280
    membership_id = membership.id if membership else None
281
    mem_display = user.membership_display(project) if project else None
282
    can_join_req = can_join_request(project, user) if project else False
283
    can_leave_req = can_leave_request(project, user) if project else False
284
    can_cancel_req = can_cancel_join_request(project, user) if project else False
285

    
286
    is_modification = application.is_modification() if application else False
287

    
288
    queryset = Project.objects.select_related()
289
    object_id = project.pk
290
    resources_set = project.resource_set
291
    template_name = "im/projects/project_detail.html"
292
    if application:
293
        queryset = ProjectApplication.objects.select_related()
294
        object_id = application.pk
295
        resources_set = application.resource_set
296
        template_name = "im/projects/project_application_detail.html"
297

    
298
    return object_detail(
299
        request,
300
        queryset=queryset,
301
        object_id=object_id,
302
        template_name=template_name,
303
        extra_context={
304
            'project': project,
305
            'application': application,
306
            'is_application': bool(application),
307
            'is_modification': is_modification,
308
            'addmembers_form': addmembers_form,
309
            'approved_members_count': approved_members_count,
310
            'pending_members_count': pending_members_count,
311
            'members_table': members_table,
312
            'owner_mode': is_owner,
313
            'admin_mode': is_project_admin,
314
            'mem_display': mem_display,
315
            'membership_id': membership_id,
316
            'can_join_request': can_join_req,
317
            'can_leave_request': can_leave_req,
318
            'can_cancel_join_request': can_cancel_req,
319
            'resources_set': resources_set,
320
            'last_app': None if application else project.last_application,
321
            'remaining_memberships_count': remaining_memberships_count
322
        })
323

    
324

    
325
MEMBERSHIP_STATUS_FILTER = {
326
    0: {'state': ProjectMembership.REQUESTED},
327
    1: {'state__in': ProjectMembership.ACCEPTED_STATES}
328
}
329

    
330

    
331
@require_http_methods(["GET", "POST"])
332
@cookie_fix
333
@valid_astakos_user_required
334
def project_search(request):
335
    q = request.GET.get('q', '')
336
    form = ProjectSearchForm()
337
    q = q.strip()
338

    
339
    if request.method == "POST":
340
        form = ProjectSearchForm(request.POST)
341
        if form.is_valid():
342
            q = form.cleaned_data['q'].strip()
343
        else:
344
            q = None
345

    
346
    if q is None:
347
        projects = Project.objects.none()
348
    else:
349
        accepted = request.user.projectmembership_set.filter(
350
            state__in=ProjectMembership.ACCEPTED_STATES).values_list(
351
            'project', flat=True)
352

    
353
        projects = Project.objects.search_by_name(q)
354
        projects = projects.filter(state=Project.NORMAL)
355
        projects = projects.exclude(id__in=accepted).select_related(
356
            'application', 'application__owner', 'application__applicant')
357

    
358
    table = get_user_projects_table(projects, user=request.user,
359
                                    prefix="my_projects_")
360
    if request.method == "POST":
361
        table.caption = _('SEARCH RESULTS')
362
    else:
363
        table.caption = _('ALL PROJECTS')
364

    
365
    return object_list(
366
        request,
367
        projects,
368
        template_name='im/projects/project_list.html',
369
        extra_context={
370
            'form': form,
371
            'is_search': True,
372
            'q': q,
373
            'table': table
374
        })
375

    
376

    
377
@project_view(get=False, post=True)
378
def project_join(request, project_uuid):
379
    project = get_object_or_404(Project, uuid=project_uuid)
380
    with ExceptionHandler(request):
381
        with transaction.commit_on_success():
382
            membership = join_project(project_uuid, request.user)
383
            if membership.state != membership.REQUESTED:
384
                m = _(astakos_messages.USER_JOINED_PROJECT)
385
            else:
386
                m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
387
            messages.success(request, m)
388
    return redirect_to_next(request, 'project_detail', args=(project.uuid,))
389

    
390

    
391
@project_view(get=False, post=True)
392
def project_leave(request, project_uuid):
393
    project = get_object_or_404(Project, uuid=project_uuid)
394
    with ExceptionHandler(request):
395
        with transaction.commit_on_success():
396
            memb_id = request.user.get_membership(project).pk
397
            auto_accepted = leave_project(memb_id, request.user)
398
            if auto_accepted:
399
                m = _(astakos_messages.USER_LEFT_PROJECT)
400
            else:
401
                m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
402
            messages.success(request, m)
403
    return redirect_to_next(request, 'project_detail', args=(project.uuid,))
404

    
405

    
406
@project_view(get=False, post=True)
407
def project_cancel_join(request, project_uuid):
408
    project = get_object_or_404(Project, uuid=project_uuid)
409
    with ExceptionHandler(request):
410
        with transaction.commit_on_success():
411
            project = get_object_or_404(Project, uuid=project_uuid)
412
            memb_id = request.user.get_membership(project).pk
413
            cancel_membership(memb_id, request.user)
414
            m = _(astakos_messages.USER_REQUEST_CANCELLED)
415
            messages.success(request, m)
416
    return redirect_to_next(request, 'project_detail', args=(project.uuid,))
417

    
418

    
419
@project_view(get=False, post=True)
420
def project_app_approve(request, project_uuid, application_id):
421
    with ExceptionHandler(request):
422
        with transaction.commit_on_success():
423
            approve_application(application_id, project_uuid,
424
                                request_user=request.user)
425
            messages.success(request, _(astakos_messages.APPLICATION_APPROVED))
426
    return redirect(reverse('project_detail', args=(project_uuid,)))
427

    
428

    
429
@project_view(get=False, post=True)
430
def project_app_deny(request, project_uuid, application_id):
431
    with ExceptionHandler(request):
432
        reason = request.POST.get("reason", "")
433
        with transaction.commit_on_success():
434
            deny_application(application_id, project_uuid,
435
                                request_user=request.user, reason=reason)
436
            messages.success(request, _(astakos_messages.APPLICATION_DENIED))
437
    return redirect(reverse("project_list"))
438

    
439

    
440
@project_view(get=False, post=True)
441
def project_app_dismiss(request, project_uuid, application_id):
442
    with ExceptionHandler(request):
443
        with transaction.commit_on_success():
444
            dismiss_application(application_id, project_uuid,
445
                                request_user=request.user)
446
            messages.success(request,
447
                             _(astakos_messages.APPLICATION_DISMISSED))
448
    return redirect(reverse("project_list"))
449

    
450

    
451
@project_view(post=True)
452
def project_members(request, project_uuid, members_status_filter=None,
453
                    template_name='im/projects/project_members.html'):
454
    project = get_object_or_404(Project, uuid=project_uuid)
455

    
456
    user = request.user
457
    if not user.owns_project(project) and not user.is_project_admin():
458
        return redirect(reverse('index'))
459

    
460
    if request.method == 'POST':
461
        addmembers_form = AddProjectMembersForm(
462
            request.POST,
463
            chain_id=int(chain_id),
464
            request_user=request.user)
465
        with ExceptionHandler(request):
466
            handle_valid_members_form(request, chain_id, addmembers_form)
467

    
468
        if addmembers_form.is_valid():
469
            addmembers_form = AddProjectMembersForm()  # clear form data
470
    else:
471
        addmembers_form = AddProjectMembersForm()  # initialize form
472

    
473
    query = api.make_membership_query({'project': project_uuid})
474
    members = api._get_memberships(query, request_user=user)
475
    approved_members_count = project.members_count()
476
    pending_members_count = project.count_pending_memberships()
477
    _limit = project.limit_on_members_number
478
    if _limit is not None:
479
        remaining_memberships_count = \
480
            max(0, _limit - approved_members_count)
481
    flt = MEMBERSHIP_STATUS_FILTER.get(members_status_filter)
482
    if flt is not None:
483
        members = members.filter(**flt)
484
    else:
485
        members = members.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
486

    
487
    members = members.select_related()
488
    members_table = tables.ProjectMembersTable(project,
489
                                               members,
490
                                               user=request.user,
491
                                               prefix="members_")
492
    RequestConfig(request, paginate={"per_page": settings.PAGINATE_BY}
493
                  ).configure(members_table)
494

    
495

    
496
    user = request.user
497
    is_project_admin = user.is_project_admin()
498
    is_owner = user.owns_application(project)
499
    if (
500
        not (is_owner or is_project_admin) and
501
        not user.non_owner_can_view(project)
502
    ):
503
        m = _(astakos_messages.NOT_ALLOWED)
504
        raise PermissionDenied(m)
505

    
506
    membership = user.get_membership(project) if project else None
507
    membership_id = membership.id if membership else None
508
    mem_display = user.membership_display(project) if project else None
509
    can_join_req = can_join_request(project, user) if project else False
510
    can_leave_req = can_leave_request(project, user) if project else False
511

    
512
    return object_detail(
513
        request,
514
        queryset=Project.objects.select_related(),
515
        object_id=project.id,
516
        template_name='im/projects/project_members.html',
517
        extra_context={
518
            'addmembers_form': addmembers_form,
519
            'approved_members_count': approved_members_count,
520
            'pending_members_count': pending_members_count,
521
            'members_table': members_table,
522
            'owner_mode': is_owner,
523
            'admin_mode': is_project_admin,
524
            'mem_display': mem_display,
525
            'membership_id': membership_id,
526
            'can_join_request': can_join_req,
527
            'can_leave_request': can_leave_req,
528
            'members_status_filter': members_status_filter,
529
            'project': project,
530
            'remaining_memberships_count': remaining_memberships_count,
531
        })
532

    
533

    
534
@project_view(get=False, post=True)
535
def project_members_action(request, project_uuid, action=None, redirect_to='',
536
                           memb_id=None):
537

    
538
    actions_map = {
539
        'remove': {
540
            'method': 'remove_membership',
541
            'msg': _(astakos_messages.USER_MEMBERSHIP_REMOVED)
542
        },
543
        'accept': {
544
            'method': 'accept_membership',
545
            'msg': _(astakos_messages.USER_MEMBERSHIP_ACCEPTED)
546
        },
547
        'reject': {
548
            'method': 'reject_membership',
549
            'msg': _(astakos_messages.USER_MEMBERSHIP_REJECTED)
550
        }
551
    }
552

    
553

    
554
    if not action in actions_map.keys():
555
        raise PermissionDenied
556

    
557
    if memb_id:
558
        member_ids = [memb_id]
559
    else:
560
        member_ids = request.POST.getlist('members')
561

    
562
    project = get_object_or_404(Project, uuid=project_uuid)
563

    
564
    user = request.user
565
    if not user.owns_project(project) and not user.is_project_admin():
566
        return redirect(reverse('index'))
567

    
568
    logger.info("Member(s) action from %s (project: %r, action: %s, "
569
                "members: %r)", user.log_display, project.uuid, action, member_ids)
570

    
571
    action = actions_map.get(action)
572
    action_func = getattr(project_actions, action.get('method'))
573
    for member_id in member_ids:
574
        member_id = int(member_id)
575
        with ExceptionHandler(request):
576
            with transaction.commit_on_success():
577
                try:
578
                    m = action_func(member_id, request.user)
579
                except ProjectError as e:
580
                    messages.error(request, e)
581
                else:
582
                    email = escape(m.person.email)
583
                    msg =  action.get('msg') % email
584
                    messages.success(request, msg)
585

    
586
    return redirect_back(request, 'project_list')