Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ e2497bc5

History | View | Annotate | Download (48.7 kB)

1
# Copyright 2011-2012 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 calendar
36
import inflect
37

    
38
engine = inflect.engine()
39

    
40
from urllib import quote
41
from functools import wraps
42
from datetime import datetime
43

    
44
from django.shortcuts import get_object_or_404
45
from django.contrib import messages
46
from django.contrib.auth.decorators import login_required
47
from django.core.urlresolvers import reverse
48
from django.db import transaction
49
from django.db.utils import IntegrityError
50
from django.http import (
51
    HttpResponse, HttpResponseBadRequest,
52
    HttpResponseForbidden, HttpResponseRedirect,
53
    HttpResponseBadRequest, Http404)
54
from django.shortcuts import redirect
55
from django.template import RequestContext, loader as template_loader
56
from django.utils.http import urlencode
57
from django.utils.translation import ugettext as _
58
from django.views.generic.create_update import (
59
    apply_extra_context, lookup_object, delete_object, get_model_and_form_class)
60
from django.views.generic.list_detail import object_list, object_detail
61
from django.core.xheaders import populate_xheaders
62
from django.core.exceptions import ValidationError, PermissionDenied
63
from django.template.loader import render_to_string
64
from django.views.decorators.http import require_http_methods
65
from django.db.models import Q
66
from django.core.exceptions import PermissionDenied
67

    
68
import astakos.im.messages as astakos_messages
69

    
70
from astakos.im.activation_backends import get_backend, SimpleBackend
71
from astakos.im.models import (
72
    AstakosUser, ApprovalTerms,
73
    EmailChange, RESOURCE_SEPARATOR,
74
    AstakosUserAuthProvider, PendingThirdPartyUser,
75
    ProjectApplication, ProjectMembership, Project)
76
from astakos.im.util import (
77
    get_context, prepare_response, get_query, restrict_next)
78
from astakos.im.forms import (
79
    LoginForm, InvitationForm, ProfileForm,
80
    FeedbackForm, SignApprovalTermsForm,
81
    EmailChangeForm,
82
    ProjectApplicationForm, ProjectSortForm,
83
    AddProjectMembersForm, ProjectSearchForm,
84
    ProjectMembersSortForm)
85
from astakos.im.functions import (
86
    send_feedback, SendMailError,
87
    logout as auth_logout,
88
    activate as activate_func,
89
    invite,
90
    send_activation as send_activation_func,
91
    SendNotificationError,
92
    accept_membership, reject_membership, remove_membership,
93
    leave_project, join_project, enroll_member)
94
# from astakos.im.endpoints.qh import timeline_charge
95
from astakos.im.settings import (
96
    COOKIE_DOMAIN, LOGOUT_NEXT,
97
    LOGGING_LEVEL, PAGINATE_BY,
98
    RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL,
99
    MODERATION_ENABLED)
100
from astakos.im import settings as astakos_settings
101
#from astakos.im.tasks import request_billing
102
from astakos.im.api.callpoint import AstakosCallpoint
103
from astakos.im import auth_providers
104
from astakos.im.templatetags.filters import ResourcePresentation
105

    
106
logger = logging.getLogger(__name__)
107

    
108
callpoint = AstakosCallpoint()
109

    
110
def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
111
    """
112
    Calls ``django.template.loader.render_to_string`` with an additional ``tab``
113
    keyword argument and returns an ``django.http.HttpResponse`` with the
114
    specified ``status``.
115
    """
116
    if tab is None:
117
        tab = template.partition('_')[0].partition('.html')[0]
118
    kwargs.setdefault('tab', tab)
119
    html = template_loader.render_to_string(
120
        template, kwargs, context_instance=context_instance)
121
    response = HttpResponse(html, status=status)
122
    return response
123

    
124
def requires_auth_provider(provider_id, **perms):
125
    """
126
    """
127
    def decorator(func, *args, **kwargs):
128
        @wraps(func)
129
        def wrapper(request, *args, **kwargs):
130
            provider = auth_providers.get_provider(provider_id)
131

    
132
            if not provider or not provider.is_active():
133
                raise PermissionDenied
134

    
135
            if provider:
136
                for pkey, value in perms.iteritems():
137
                    attr = 'is_available_for_%s' % pkey.lower()
138
                    if getattr(provider, attr)() != value:
139
                        #TODO: add session message
140
                        return HttpResponseRedirect(reverse('login'))
141
            return func(request, *args)
142
        return wrapper
143
    return decorator
144

    
145

    
146
def requires_anonymous(func):
147
    """
148
    Decorator checkes whether the request.user is not Anonymous and in that case
149
    redirects to `logout`.
150
    """
151
    @wraps(func)
152
    def wrapper(request, *args):
153
        if not request.user.is_anonymous():
154
            next = urlencode({'next': request.build_absolute_uri()})
155
            logout_uri = reverse(logout) + '?' + next
156
            return HttpResponseRedirect(logout_uri)
157
        return func(request, *args)
158
    return wrapper
159

    
160

    
161
def signed_terms_required(func):
162
    """
163
    Decorator checks whether the request.user is Anonymous and in that case
164
    redirects to `logout`.
165
    """
166
    @wraps(func)
167
    def wrapper(request, *args, **kwargs):
168
        if request.user.is_authenticated() and not request.user.signed_terms:
169
            params = urlencode({'next': request.build_absolute_uri(),
170
                                'show_form': ''})
171
            terms_uri = reverse('latest_terms') + '?' + params
172
            return HttpResponseRedirect(terms_uri)
173
        return func(request, *args, **kwargs)
174
    return wrapper
175

    
176

    
177
def required_auth_methods_assigned(only_warn=False):
178
    """
179
    Decorator that checks whether the request.user has all required auth providers
180
    assigned.
181
    """
182
    required_providers = auth_providers.REQUIRED_PROVIDERS.keys()
183

    
184
    def decorator(func):
185
        if not required_providers:
186
            return func
187

    
188
        @wraps(func)
189
        def wrapper(request, *args, **kwargs):
190
            if request.user.is_authenticated():
191
                for required in required_providers:
192
                    if not request.user.has_auth_provider(required):
193
                        provider = auth_providers.get_provider(required)
194
                        if only_warn:
195
                            messages.error(request,
196
                                           _(astakos_messages.AUTH_PROVIDER_REQUIRED  % {
197
                                               'provider': provider.get_title_display}))
198
                        else:
199
                            return HttpResponseRedirect(reverse('edit_profile'))
200
            return func(request, *args, **kwargs)
201
        return wrapper
202
    return decorator
203

    
204

    
205
def valid_astakos_user_required(func):
206
    return signed_terms_required(required_auth_methods_assigned()(login_required(func)))
207

    
208

    
209
@require_http_methods(["GET", "POST"])
210
@signed_terms_required
211
def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context=None):
212
    """
213
    If there is logged on user renders the profile page otherwise renders login page.
214

215
    **Arguments**
216

217
    ``login_template_name``
218
        A custom login template to use. This is optional; if not specified,
219
        this will default to ``im/login.html``.
220

221
    ``profile_template_name``
222
        A custom profile template to use. This is optional; if not specified,
223
        this will default to ``im/profile.html``.
224

225
    ``extra_context``
226
        An dictionary of variables to add to the template context.
227

228
    **Template:**
229

230
    im/profile.html or im/login.html or ``template_name`` keyword argument.
231

232
    """
233
    extra_context = extra_context or {}
234
    template_name = login_template_name
235
    if request.user.is_authenticated():
236
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
237

    
238
    third_party_token = request.GET.get('key', False)
239
    if third_party_token:
240
        messages.info(request, astakos_messages.AUTH_PROVIDER_LOGIN_TO_ADD)
241

    
242
    return render_response(
243
        template_name,
244
        login_form = LoginForm(request=request),
245
        context_instance = get_context(request, extra_context)
246
    )
247

    
248

    
249
@require_http_methods(["GET", "POST"])
250
@valid_astakos_user_required
251
@transaction.commit_manually
252
def invite(request, template_name='im/invitations.html', extra_context=None):
253
    """
254
    Allows a user to invite somebody else.
255

256
    In case of GET request renders a form for providing the invitee information.
257
    In case of POST checks whether the user has not run out of invitations and then
258
    sends an invitation email to singup to the service.
259

260
    The view uses commit_manually decorator in order to ensure the number of the
261
    user invitations is going to be updated only if the email has been successfully sent.
262

263
    If the user isn't logged in, redirects to settings.LOGIN_URL.
264

265
    **Arguments**
266

267
    ``template_name``
268
        A custom template to use. This is optional; if not specified,
269
        this will default to ``im/invitations.html``.
270

271
    ``extra_context``
272
        An dictionary of variables to add to the template context.
273

274
    **Template:**
275

276
    im/invitations.html or ``template_name`` keyword argument.
277

278
    **Settings:**
279

280
    The view expectes the following settings are defined:
281

282
    * LOGIN_URL: login uri
283
    """
284
    extra_context = extra_context or {}
285
    status = None
286
    message = None
287
    form = InvitationForm()
288

    
289
    inviter = request.user
290
    if request.method == 'POST':
291
        form = InvitationForm(request.POST)
292
        if inviter.invitations > 0:
293
            if form.is_valid():
294
                try:
295
                    email = form.cleaned_data.get('username')
296
                    realname = form.cleaned_data.get('realname')
297
                    invite(inviter, email, realname)
298
                    message = _(astakos_messages.INVITATION_SENT) % locals()
299
                    messages.success(request, message)
300
                except SendMailError, e:
301
                    message = e.message
302
                    messages.error(request, message)
303
                    transaction.rollback()
304
                except BaseException, e:
305
                    message = _(astakos_messages.GENERIC_ERROR)
306
                    messages.error(request, message)
307
                    logger.exception(e)
308
                    transaction.rollback()
309
                else:
310
                    transaction.commit()
311
        else:
312
            message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
313
            messages.error(request, message)
314

    
315
    sent = [{'email': inv.username,
316
             'realname': inv.realname,
317
             'is_consumed': inv.is_consumed}
318
            for inv in request.user.invitations_sent.all()]
319
    kwargs = {'inviter': inviter,
320
              'sent': sent}
321
    context = get_context(request, extra_context, **kwargs)
322
    return render_response(template_name,
323
                           invitation_form=form,
324
                           context_instance=context)
325

    
326

    
327
@require_http_methods(["GET", "POST"])
328
@required_auth_methods_assigned(only_warn=True)
329
@login_required
330
@signed_terms_required
331
def edit_profile(request, template_name='im/profile.html', extra_context=None):
332
    """
333
    Allows a user to edit his/her profile.
334

335
    In case of GET request renders a form for displaying the user information.
336
    In case of POST updates the user informantion and redirects to ``next``
337
    url parameter if exists.
338

339
    If the user isn't logged in, redirects to settings.LOGIN_URL.
340

341
    **Arguments**
342

343
    ``template_name``
344
        A custom template to use. This is optional; if not specified,
345
        this will default to ``im/profile.html``.
346

347
    ``extra_context``
348
        An dictionary of variables to add to the template context.
349

350
    **Template:**
351

352
    im/profile.html or ``template_name`` keyword argument.
353

354
    **Settings:**
355

356
    The view expectes the following settings are defined:
357

358
    * LOGIN_URL: login uri
359
    """
360
    extra_context = extra_context or {}
361
    form = ProfileForm(
362
        instance=request.user,
363
        session_key=request.session.session_key
364
    )
365
    extra_context['next'] = request.GET.get('next')
366
    if request.method == 'POST':
367
        form = ProfileForm(
368
            request.POST,
369
            instance=request.user,
370
            session_key=request.session.session_key
371
        )
372
        if form.is_valid():
373
            try:
374
                prev_token = request.user.auth_token
375
                user = form.save()
376
                form = ProfileForm(
377
                    instance=user,
378
                    session_key=request.session.session_key
379
                )
380
                next = restrict_next(
381
                    request.POST.get('next'),
382
                    domain=COOKIE_DOMAIN
383
                )
384
                if next:
385
                    return redirect(next)
386
                msg = _(astakos_messages.PROFILE_UPDATED)
387
                messages.success(request, msg)
388
            except ValueError, ve:
389
                messages.success(request, ve)
390
    elif request.method == "GET":
391
        request.user.is_verified = True
392
        request.user.save()
393

    
394
    # existing providers
395
    user_providers = request.user.get_active_auth_providers()
396

    
397
    # providers that user can add
398
    user_available_providers = request.user.get_available_auth_providers()
399

    
400
    return render_response(template_name,
401
                           profile_form = form,
402
                           user_providers = user_providers,
403
                           user_available_providers = user_available_providers,
404
                           context_instance = get_context(request,
405
                                                          extra_context))
406

    
407

    
408
@transaction.commit_manually
409
@require_http_methods(["GET", "POST"])
410
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
411
    """
412
    Allows a user to create a local account.
413

414
    In case of GET request renders a form for entering the user information.
415
    In case of POST handles the signup.
416

417
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
418
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
419
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
420
    (see activation_backends);
421

422
    Upon successful user creation, if ``next`` url parameter is present the user is redirected there
423
    otherwise renders the same page with a success message.
424

425
    On unsuccessful creation, renders ``template_name`` with an error message.
426

427
    **Arguments**
428

429
    ``template_name``
430
        A custom template to render. This is optional;
431
        if not specified, this will default to ``im/signup.html``.
432

433
    ``on_success``
434
        A custom template to render in case of success. This is optional;
435
        if not specified, this will default to ``im/signup_complete.html``.
436

437
    ``extra_context``
438
        An dictionary of variables to add to the template context.
439

440
    **Template:**
441

442
    im/signup.html or ``template_name`` keyword argument.
443
    im/signup_complete.html or ``on_success`` keyword argument.
444
    """
445
    extra_context = extra_context or {}
446
    if request.user.is_authenticated():
447
        return HttpResponseRedirect(reverse('edit_profile'))
448

    
449
    provider = get_query(request).get('provider', 'local')
450
    if not auth_providers.get_provider(provider).is_available_for_create():
451
        raise PermissionDenied
452

    
453
    id = get_query(request).get('id')
454
    try:
455
        instance = AstakosUser.objects.get(id=id) if id else None
456
    except AstakosUser.DoesNotExist:
457
        instance = None
458

    
459
    third_party_token = request.REQUEST.get('third_party_token', None)
460
    if third_party_token:
461
        pending = get_object_or_404(PendingThirdPartyUser,
462
                                    token=third_party_token)
463
        provider = pending.provider
464
        instance = pending.get_user_instance()
465

    
466
    try:
467
        if not backend:
468
            backend = get_backend(request)
469
        form = backend.get_signup_form(provider, instance)
470
    except Exception, e:
471
        form = SimpleBackend(request).get_signup_form(provider)
472
        messages.error(request, e)
473
    if request.method == 'POST':
474
        if form.is_valid():
475
            user = form.save(commit=False)
476
            try:
477
                result = backend.handle_activation(user)
478
                status = messages.SUCCESS
479
                message = result.message
480

    
481
                form.store_user(user, request)
482

    
483
                if 'additional_email' in form.cleaned_data:
484
                    additional_email = form.cleaned_data['additional_email']
485
                    if additional_email != user.email:
486
                        user.additionalmail_set.create(email=additional_email)
487
                        msg = 'Additional email: %s saved for user %s.' % (
488
                            additional_email,
489
                            user.email
490
                        )
491
                        logger._log(LOGGING_LEVEL, msg, [])
492
                if user and user.is_active:
493
                    next = request.POST.get('next', '')
494
                    response = prepare_response(request, user, next=next)
495
                    transaction.commit()
496
                    return response
497
                messages.add_message(request, status, message)
498
                transaction.commit()
499
                return render_response(
500
                    on_success,
501
                    context_instance=get_context(
502
                        request,
503
                        extra_context
504
                    )
505
                )
506
            except SendMailError, e:
507
                logger.exception(e)
508
                status = messages.ERROR
509
                message = e.message
510
                messages.error(request, message)
511
                transaction.rollback()
512
            except BaseException, e:
513
                logger.exception(e)
514
                message = _(astakos_messages.GENERIC_ERROR)
515
                messages.error(request, message)
516
                logger.exception(e)
517
                transaction.rollback()
518
    return render_response(template_name,
519
                           signup_form=form,
520
                           third_party_token=third_party_token,
521
                           provider=provider,
522
                           context_instance=get_context(request, extra_context))
523

    
524

    
525
@require_http_methods(["GET", "POST"])
526
@required_auth_methods_assigned(only_warn=True)
527
@login_required
528
@signed_terms_required
529
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
530
    """
531
    Allows a user to send feedback.
532

533
    In case of GET request renders a form for providing the feedback information.
534
    In case of POST sends an email to support team.
535

536
    If the user isn't logged in, redirects to settings.LOGIN_URL.
537

538
    **Arguments**
539

540
    ``template_name``
541
        A custom template to use. This is optional; if not specified,
542
        this will default to ``im/feedback.html``.
543

544
    ``extra_context``
545
        An dictionary of variables to add to the template context.
546

547
    **Template:**
548

549
    im/signup.html or ``template_name`` keyword argument.
550

551
    **Settings:**
552

553
    * LOGIN_URL: login uri
554
    """
555
    extra_context = extra_context or {}
556
    if request.method == 'GET':
557
        form = FeedbackForm()
558
    if request.method == 'POST':
559
        if not request.user:
560
            return HttpResponse('Unauthorized', status=401)
561

    
562
        form = FeedbackForm(request.POST)
563
        if form.is_valid():
564
            msg = form.cleaned_data['feedback_msg']
565
            data = form.cleaned_data['feedback_data']
566
            try:
567
                send_feedback(msg, data, request.user, email_template_name)
568
            except SendMailError, e:
569
                messages.error(request, message)
570
            else:
571
                message = _(astakos_messages.FEEDBACK_SENT)
572
                messages.success(request, message)
573
    return render_response(template_name,
574
                           feedback_form=form,
575
                           context_instance=get_context(request, extra_context))
576

    
577

    
578
@require_http_methods(["GET"])
579
@signed_terms_required
580
def logout(request, template='registration/logged_out.html', extra_context=None):
581
    """
582
    Wraps `django.contrib.auth.logout`.
583
    """
584
    extra_context = extra_context or {}
585
    response = HttpResponse()
586
    if request.user.is_authenticated():
587
        email = request.user.email
588
        auth_logout(request)
589
    else:
590
        response['Location'] = reverse('index')
591
        response.status_code = 301
592
        return response
593

    
594
    next = restrict_next(
595
        request.GET.get('next'),
596
        domain=COOKIE_DOMAIN
597
    )
598

    
599
    if next:
600
        response['Location'] = next
601
        response.status_code = 302
602
    elif LOGOUT_NEXT:
603
        response['Location'] = LOGOUT_NEXT
604
        response.status_code = 301
605
    else:
606
        messages.add_message(request, messages.SUCCESS, _(astakos_messages.LOGOUT_SUCCESS))
607
        response['Location'] = reverse('index')
608
        response.status_code = 301
609
    return response
610

    
611

    
612
@require_http_methods(["GET", "POST"])
613
@transaction.commit_manually
614
def activate(request, greeting_email_template_name='im/welcome_email.txt',
615
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
616
    """
617
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
618
    and renews the user token.
619

620
    The view uses commit_manually decorator in order to ensure the user state will be updated
621
    only if the email will be send successfully.
622
    """
623
    token = request.GET.get('auth')
624
    next = request.GET.get('next')
625
    try:
626
        user = AstakosUser.objects.get(auth_token=token)
627
    except AstakosUser.DoesNotExist:
628
        return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
629

    
630
    if user.is_active:
631
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
632
        messages.error(request, message)
633
        return index(request)
634

    
635
    try:
636
        activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
637
        response = prepare_response(request, user, next, renew=True)
638
        transaction.commit()
639
        return response
640
    except SendMailError, e:
641
        message = e.message
642
        messages.add_message(request, messages.ERROR, message)
643
        transaction.rollback()
644
        return index(request)
645
    except BaseException, e:
646
        status = messages.ERROR
647
        message = _(astakos_messages.GENERIC_ERROR)
648
        messages.add_message(request, messages.ERROR, message)
649
        logger.exception(e)
650
        transaction.rollback()
651
        return index(request)
652

    
653

    
654
@require_http_methods(["GET", "POST"])
655
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
656
    extra_context = extra_context or {}
657
    term = None
658
    terms = None
659
    if not term_id:
660
        try:
661
            term = ApprovalTerms.objects.order_by('-id')[0]
662
        except IndexError:
663
            pass
664
    else:
665
        try:
666
            term = ApprovalTerms.objects.get(id=term_id)
667
        except ApprovalTerms.DoesNotExist, e:
668
            pass
669

    
670
    if not term:
671
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
672
        return HttpResponseRedirect(reverse('index'))
673
    f = open(term.location, 'r')
674
    terms = f.read()
675

    
676
    if request.method == 'POST':
677
        next = restrict_next(
678
            request.POST.get('next'),
679
            domain=COOKIE_DOMAIN
680
        )
681
        if not next:
682
            next = reverse('index')
683
        form = SignApprovalTermsForm(request.POST, instance=request.user)
684
        if not form.is_valid():
685
            return render_response(template_name,
686
                                   terms=terms,
687
                                   approval_terms_form=form,
688
                                   context_instance=get_context(request, extra_context))
689
        user = form.save()
690
        return HttpResponseRedirect(next)
691
    else:
692
        form = None
693
        if request.user.is_authenticated() and not request.user.signed_terms:
694
            form = SignApprovalTermsForm(instance=request.user)
695
        return render_response(template_name,
696
                               terms=terms,
697
                               approval_terms_form=form,
698
                               context_instance=get_context(request, extra_context))
699

    
700

    
701
@require_http_methods(["GET", "POST"])
702
@valid_astakos_user_required
703
@transaction.commit_manually
704
def change_email(request, activation_key=None,
705
                 email_template_name='registration/email_change_email.txt',
706
                 form_template_name='registration/email_change_form.html',
707
                 confirm_template_name='registration/email_change_done.html',
708
                 extra_context=None):
709
    extra_context = extra_context or {}
710

    
711

    
712
    if activation_key:
713
        try:
714
            user = EmailChange.objects.change_email(activation_key)
715
            if request.user.is_authenticated() and request.user == user:
716
                msg = _(astakos_messages.EMAIL_CHANGED)
717
                messages.success(request, msg)
718
                auth_logout(request)
719
                response = prepare_response(request, user)
720
                transaction.commit()
721
                return HttpResponseRedirect(reverse('edit_profile'))
722
        except ValueError, e:
723
            messages.error(request, e)
724
            transaction.rollback()
725
            return HttpResponseRedirect(reverse('index'))
726

    
727
        return render_response(confirm_template_name,
728
                               modified_user=user if 'user' in locals() \
729
                               else None, context_instance=get_context(request,
730
                                                            extra_context))
731

    
732
    if not request.user.is_authenticated():
733
        path = quote(request.get_full_path())
734
        url = request.build_absolute_uri(reverse('index'))
735
        return HttpResponseRedirect(url + '?next=' + path)
736

    
737
    # clean up expired email changes
738
    if request.user.email_change_is_pending():
739
        change = request.user.emailchanges.get()
740
        if change.activation_key_expired():
741
            change.delete()
742
            transaction.commit()
743
            return HttpResponseRedirect(reverse('email_change'))
744

    
745
    form = EmailChangeForm(request.POST or None)
746
    if request.method == 'POST' and form.is_valid():
747
        try:
748
            # delete pending email changes
749
            request.user.emailchanges.all().delete()
750
            ec = form.save(email_template_name, request)
751
        except SendMailError, e:
752
            msg = e
753
            messages.error(request, msg)
754
            transaction.rollback()
755
            return HttpResponseRedirect(reverse('edit_profile'))
756
        else:
757
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
758
            messages.success(request, msg)
759
            transaction.commit()
760
            return HttpResponseRedirect(reverse('edit_profile'))
761

    
762
    if request.user.email_change_is_pending():
763
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
764

    
765
    return render_response(
766
        form_template_name,
767
        form=form,
768
        context_instance=get_context(request, extra_context)
769
    )
770

    
771

    
772
def send_activation(request, user_id, template_name='im/login.html', extra_context=None):
773

    
774
    if request.user.is_authenticated():
775
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
776
        return HttpResponseRedirect(reverse('edit_profile'))
777

    
778
    if astakos_settings.MODERATION_ENABLED:
779
        raise PermissionDenied
780

    
781
    extra_context = extra_context or {}
782
    try:
783
        u = AstakosUser.objects.get(id=user_id)
784
    except AstakosUser.DoesNotExist:
785
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
786
    else:
787
        try:
788
            send_activation_func(u)
789
            msg = _(astakos_messages.ACTIVATION_SENT)
790
            messages.success(request, msg)
791
        except SendMailError, e:
792
            messages.error(request, e)
793
    return render_response(
794
        template_name,
795
        login_form = LoginForm(request=request),
796
        context_instance = get_context(
797
            request,
798
            extra_context
799
        )
800
    )
801

    
802

    
803
@require_http_methods(["GET"])
804
@valid_astakos_user_required
805
def resource_usage(request):
806

    
807
    def with_class(entry):
808
         entry['load_class'] = 'red'
809
         max_value = float(entry['maxValue'])
810
         curr_value = float(entry['currValue'])
811
         entry['ratio_limited']= 0
812
         if max_value > 0 :
813
             entry['ratio'] = (curr_value / max_value) * 100
814
         else:
815
             entry['ratio'] = 0
816
         if entry['ratio'] < 66:
817
             entry['load_class'] = 'yellow'
818
         if entry['ratio'] < 33:
819
             entry['load_class'] = 'green'
820
         if entry['ratio']<0:
821
             entry['ratio'] = 0
822
         if entry['ratio']>100:
823
             entry['ratio_limited'] = 100
824
         else:
825
             entry['ratio_limited'] = entry['ratio']
826
         return entry
827

    
828
    def pluralize(entry):
829
        entry['plural'] = engine.plural(entry.get('name'))
830
        return entry
831

    
832
    resource_usage = None
833
    result = callpoint.get_user_usage(request.user.id)
834
    if result.is_success:
835
        resource_usage = result.data
836
        backenddata = map(with_class, result.data)
837
        backenddata = map(pluralize , backenddata)
838
    else:
839
        messages.error(request, result.reason)
840
    return render_response('im/resource_usage.html',
841
                           context_instance=get_context(request),
842
                           resource_usage=backenddata,
843
                           result=result)
844

    
845

    
846
##@require_http_methods(["GET"])
847
#@require_http_methods(["POST", "GET"])
848
#@signed_terms_required
849
#@login_required
850
#def billing(request):
851
#
852
#    today = datetime.today()
853
#    month_last_day = calendar.monthrange(today.year, today.month)[1]
854
#    start = request.POST.get('datefrom', None)
855
#    if start:
856
#        today = datetime.fromtimestamp(int(start))
857
#        month_last_day = calendar.monthrange(today.year, today.month)[1]
858
#
859
#    start = datetime(today.year, today.month, 1).strftime("%s")
860
#    end = datetime(today.year, today.month, month_last_day).strftime("%s")
861
#    r = request_billing.apply(args=('pgerakios@grnet.gr',
862
#                                    int(start) * 1000,
863
#                                    int(end) * 1000))
864
#    data = {}
865
#
866
#    try:
867
#        status, data = r.result
868
#        data = _clear_billing_data(data)
869
#        if status != 200:
870
#            messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
871
#    except:
872
#        messages.error(request, r.result)
873
#
874
#    return render_response(
875
#        template='im/billing.html',
876
#        context_instance=get_context(request),
877
#        data=data,
878
#        zerodate=datetime(month=1, year=1970, day=1),
879
#        today=today,
880
#        start=int(start),
881
#        month_last_day=month_last_day)
882

    
883

    
884
#def _clear_billing_data(data):
885
#
886
#    # remove addcredits entries
887
#    def isnotcredit(e):
888
#        return e['serviceName'] != "addcredits"
889
#
890
#    # separate services
891
#    def servicefilter(service_name):
892
#        service = service_name
893
#
894
#        def fltr(e):
895
#            return e['serviceName'] == service
896
#        return fltr
897
#
898
#    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
899
#    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
900
#    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
901
#    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
902
#
903
#    return data
904

    
905

    
906
# #@require_http_methods(["GET"])
907
# @require_http_methods(["POST", "GET"])
908
# @signed_terms_required
909
# @login_required
910
# def timeline(request):
911
# #    data = {'entity':request.user.email}
912
#     timeline_body = ()
913
#     timeline_header = ()
914
# #    form = TimelineForm(data)
915
#     form = TimelineForm()
916
#     if request.method == 'POST':
917
#         data = request.POST
918
#         form = TimelineForm(data)
919
#         if form.is_valid():
920
#             data = form.cleaned_data
921
#             timeline_header = ('entity', 'resource',
922
#                                'event name', 'event date',
923
#                                'incremental cost', 'total cost')
924
#             timeline_body = timeline_charge(
925
#                 data['entity'], data['resource'],
926
#                 data['start_date'], data['end_date'],
927
#                 data['details'], data['operation'])
928
#
929
#     return render_response(template='im/timeline.html',
930
#                            context_instance=get_context(request),
931
#                            form=form,
932
#                            timeline_header=timeline_header,
933
#                            timeline_body=timeline_body)
934
#     return data
935

    
936

    
937
# TODO: action only on POST and user should confirm the removal
938
@require_http_methods(["GET", "POST"])
939
@login_required
940
@signed_terms_required
941
def remove_auth_provider(request, pk):
942
    try:
943
        provider = request.user.auth_providers.get(pk=pk)
944
    except AstakosUserAuthProvider.DoesNotExist:
945
        raise Http404
946

    
947
    if provider.can_remove():
948
        provider.delete()
949
        return HttpResponseRedirect(reverse('edit_profile'))
950
    else:
951
        raise PermissionDenied
952

    
953

    
954
def how_it_works(request):
955
    return render_response(
956
        'im/how_it_works.html',
957
        context_instance=get_context(request))
958

    
959
@transaction.commit_manually
960
def _create_object(request, model=None, template_name=None,
961
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
962
        login_required=False, context_processors=None, form_class=None ):
963
    """
964
    Based of django.views.generic.create_update.create_object which displays a
965
    summary page before creating the object.
966
    """
967
    rollback = False
968
    response = None
969

    
970
    if extra_context is None: extra_context = {}
971
    if login_required and not request.user.is_authenticated():
972
        return redirect_to_login(request.path)
973
    try:
974

    
975
        model, form_class = get_model_and_form_class(model, form_class)
976
        extra_context['edit'] = 0
977
        if request.method == 'POST':
978
            form = form_class(request.POST, request.FILES)
979
            if form.is_valid():
980
                verify = request.GET.get('verify')
981
                edit = request.GET.get('edit')
982
                if verify == '1':
983
                    extra_context['show_form'] = False
984
                    extra_context['form_data'] = form.cleaned_data
985
                elif edit == '1':
986
                    extra_context['show_form'] = True
987
                else:
988
                    new_object = form.save()
989

    
990
                    msg = _("The %(verbose_name)s has been received and is under consideration .") %\
991
                                {"verbose_name": model._meta.verbose_name}
992
                    messages.success(request, msg, fail_silently=True)
993
                    response = redirect(post_save_redirect, new_object)
994
        else:
995
            form = form_class()
996
    except BaseException, e:
997
        logger.exception(e)
998
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
999
        rollback = True
1000
    finally:
1001
        if rollback:
1002
            transaction.rollback()
1003
        else:
1004
            transaction.commit()
1005

    
1006
        if response == None:
1007
            # Create the template, context, response
1008
            if not template_name:
1009
                template_name = "%s/%s_form.html" %\
1010
                     (model._meta.app_label, model._meta.object_name.lower())
1011
            t = template_loader.get_template(template_name)
1012
            c = RequestContext(request, {
1013
                'form': form
1014
            }, context_processors)
1015
            apply_extra_context(extra_context, c)
1016
            response = HttpResponse(t.render(c))
1017
        return response
1018

    
1019
@transaction.commit_manually
1020
def _update_object(request, model=None, object_id=None, slug=None,
1021
        slug_field='slug', template_name=None, template_loader=template_loader,
1022
        extra_context=None, post_save_redirect=None, login_required=False,
1023
        context_processors=None, template_object_name='object',
1024
        form_class=None):
1025
    """
1026
    Based of django.views.generic.create_update.update_object which displays a
1027
    summary page before updating the object.
1028
    """
1029
    rollback = False
1030
    response = None
1031

    
1032
    if extra_context is None: extra_context = {}
1033
    if login_required and not request.user.is_authenticated():
1034
        return redirect_to_login(request.path)
1035

    
1036
    try:
1037
        model, form_class = get_model_and_form_class(model, form_class)
1038
        obj = lookup_object(model, object_id, slug, slug_field)
1039

    
1040
        if request.method == 'POST':
1041
            form = form_class(request.POST, request.FILES, instance=obj)
1042
            if form.is_valid():
1043
                verify = request.GET.get('verify')
1044
                edit = request.GET.get('edit')
1045
                if verify == '1':
1046
                    extra_context['show_form'] = False
1047
                    extra_context['form_data'] = form.cleaned_data
1048
                elif edit == '1':
1049
                    extra_context['show_form'] = True
1050
                else:
1051
                    obj = form.save()
1052
                    msg = _("The %(verbose_name)s has been received and is under consideration .") %\
1053
                                {"verbose_name": model._meta.verbose_name}
1054
                    messages.success(request, msg, fail_silently=True)
1055
                    response = redirect(post_save_redirect, obj)
1056
        else:
1057
            form = form_class(instance=obj)
1058
    except BaseException, e:
1059
        logger.exception(e)
1060
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1061
        rollback = True
1062
    finally:
1063
        if rollback:
1064
            transaction.rollback()
1065
        else:
1066
            transaction.commit()
1067
        if response == None:
1068
            if not template_name:
1069
                template_name = "%s/%s_form.html" %\
1070
                    (model._meta.app_label, model._meta.object_name.lower())
1071
            t = template_loader.get_template(template_name)
1072
            c = RequestContext(request, {
1073
                'form': form,
1074
                template_object_name: obj,
1075
            }, context_processors)
1076
            apply_extra_context(extra_context, c)
1077
            response = HttpResponse(t.render(c))
1078
            populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname))
1079
        return response
1080

    
1081
@require_http_methods(["GET", "POST"])
1082
@signed_terms_required
1083
@login_required
1084
def project_add(request):
1085
    result = callpoint.list_resources()
1086
    details_fields = ["name", "homepage", "description","start_date","end_date", "comments"]
1087
    membership_fields =["member_join_policy", "member_leave_policy", "limit_on_members_number"] 
1088
    if not result.is_success:
1089
        messages.error(
1090
            request,
1091
            'Unable to retrieve system resources: %s' % result.reason
1092
    )
1093
    else:
1094
        resource_catalog = result.data
1095
    extra_context = {'resource_catalog':resource_catalog, 'show_form':True, 'details_fields':details_fields, 'membership_fields':membership_fields}
1096
    return _create_object(request, template_name='im/projects/projectapplication_form.html',
1097
        extra_context=extra_context, post_save_redirect=reverse('project_list'),
1098
        form_class=ProjectApplicationForm) 
1099

    
1100

    
1101
@require_http_methods(["GET"])
1102
@signed_terms_required
1103
@login_required
1104
def project_list(request):
1105
    q = ProjectApplication.objects.filter(owner=request.user)
1106
    q |= ProjectApplication.objects.filter(applicant=request.user)
1107
    q |= ProjectApplication.objects.filter(
1108
        project__in=request.user.projectmembership_set.values_list('project', flat=True)
1109
    )
1110
    q = q.select_related()
1111
    sorting = 'name'
1112
    sort_form = ProjectSortForm(request.GET)
1113
    if sort_form.is_valid():
1114
        sorting = sort_form.cleaned_data.get('sorting')
1115
    q = q.order_by(sorting)
1116

    
1117
    return object_list(
1118
        request,
1119
        q,
1120
        paginate_by=PAGINATE_BY,
1121
        page=request.GET.get('page') or 1,
1122
        template_name='im/projects/project_list.html',
1123
        extra_context={
1124
            'is_search':False,
1125
            'sorting':sorting
1126
        }
1127
    )
1128

    
1129
@require_http_methods(["GET", "POST"])
1130
@signed_terms_required
1131
@login_required
1132
def project_update(request, application_id):
1133
    result = callpoint.list_resources()
1134
    if not result.is_success:
1135
        messages.error(
1136
            request,
1137
            'Unable to retrieve system resources: %s' % result.reason
1138
    )
1139
    else:
1140
        resource_catalog = result.data
1141
    extra_context = {'resource_catalog':resource_catalog, 'show_form':True}
1142
    return _update_object(
1143
        request,
1144
        object_id=application_id,
1145
        template_name='im/projects/projectapplication_form.html',
1146
        extra_context=extra_context, post_save_redirect='/im/project/list/',
1147
        form_class=ProjectApplicationForm)
1148

    
1149

    
1150
@require_http_methods(["GET", "POST"])
1151
@signed_terms_required
1152
@login_required
1153
@transaction.commit_manually
1154
def project_detail(request, application_id):
1155
    resource_catalog = None
1156
    result = callpoint.list_resources()
1157
    if not result.is_success:
1158
        messages.error(
1159
            request,
1160
            'Unable to retrieve system resources: %s' % result.reason
1161
    )
1162
    else:
1163
        resource_catalog = result.data
1164

    
1165
    addmembers_form = AddProjectMembersForm()
1166
    if request.method == 'POST':
1167
        addmembers_form = AddProjectMembersForm(request.POST)
1168
        if addmembers_form.is_valid():
1169
            try:
1170
                rollback = False
1171
                application_id = int(application_id)
1172
                map(lambda u: enroll_member(
1173
                        application_id,
1174
                        u,
1175
                        request_user=request.user),
1176
                    addmembers_form.valid_users)
1177
            except (IOError, PermissionDenied), e:
1178
                messages.error(request, e)
1179
            except BaseException, e:
1180
                rollback = True
1181
                messages.error(request, e)
1182
            finally:
1183
                if rollback == True:
1184
                    transaction.rollback()
1185
                else:
1186
                    transaction.commit()
1187
            addmembers_form = AddProjectMembersForm()
1188

    
1189
    # validate sorting
1190
    sorting = 'person__email'
1191
    form = ProjectMembersSortForm(request.GET or request.POST)
1192
    if form.is_valid():
1193
        sorting = form.cleaned_data.get('sorting')
1194

    
1195
    rollback = False
1196
    try:
1197
        return object_detail(
1198
            request,
1199
            queryset=ProjectApplication.objects.select_related(),
1200
            object_id=application_id,
1201
            template_name='im/projects/project_detail.html',
1202
            extra_context={
1203
                'resource_catalog':resource_catalog,
1204
                'sorting':sorting,
1205
                'addmembers_form':addmembers_form
1206
                }
1207
            )
1208
    except:
1209
        rollback = True
1210
    finally:
1211
        if rollback == True:
1212
            transaction.rollback()
1213
        else:
1214
            transaction.commit()
1215

    
1216
@require_http_methods(["GET", "POST"])
1217
@signed_terms_required
1218
@login_required
1219
def project_search(request):
1220
    q = request.GET.get('q', '')
1221
    queryset = ProjectApplication.objects
1222

    
1223
    if request.method == 'GET':
1224
        form = ProjectSearchForm()
1225
        q = q.strip()
1226
        queryset = queryset.filter(~Q(project__last_approval_date__isnull=True))
1227
        queryset = queryset.filter(name__contains=q)
1228
    else:
1229
        form = ProjectSearchForm(request.POST)
1230

    
1231
        if form.is_valid():
1232
            q = form.cleaned_data['q'].strip()
1233

    
1234
            queryset = queryset.filter(~Q(project__last_approval_date__isnull=True))
1235

    
1236
            queryset = queryset.filter(name__contains=q)
1237
        else:
1238
            queryset = queryset.none()
1239

    
1240
    sorting = 'name'
1241
    # validate sorting
1242
    sort_form = ProjectSortForm(request.GET)
1243
    if sort_form.is_valid():
1244
        sorting = sort_form.cleaned_data.get('sorting')
1245
    queryset = queryset.order_by(sorting)
1246
 
1247
    return object_list(
1248
        request,
1249
        queryset,
1250
        paginate_by=PAGINATE_BY_ALL,
1251
        page=request.GET.get('page') or 1,
1252
        template_name='im/projects/project_list.html',
1253
        extra_context=dict(
1254
            form=form,
1255
            is_search=True,
1256
            sorting=sorting,
1257
            q=q,
1258
        )
1259
    )
1260

    
1261
@require_http_methods(["POST"])
1262
@signed_terms_required
1263
@login_required
1264
@transaction.commit_manually
1265
def project_join(request, application_id):
1266
    next = request.GET.get('next')
1267
    if not next:
1268
        return HttpResponseBadRequest(
1269
            _(astakos_messages.MISSING_NEXT_PARAMETER))
1270

    
1271
    rollback = False
1272
    try:
1273
        application_id = int(application_id)
1274
        join_project(application_id, request.user)
1275
    except (IOError, PermissionDenied), e:
1276
        messages.error(request, e)
1277
    except BaseException, e:
1278
        logger.exception(e)
1279
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1280
        rollback = True
1281
    finally:
1282
        if rollback:
1283
            transaction.rollback()
1284
        else:
1285
            transaction.commit()
1286
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1287
    return redirect(next)
1288

    
1289
@require_http_methods(["POST"])
1290
@signed_terms_required
1291
@login_required
1292
@transaction.commit_manually
1293
def project_leave(request, application_id):
1294
    next = request.GET.get('next')
1295
    if not next:
1296
        return HttpResponseBadRequest(
1297
            _(astakos_messages.MISSING_NEXT_PARAMETER))
1298

    
1299
    rollback = False
1300
    try:
1301
        application_id = int(application_id)
1302
        leave_project(application_id, request.user)
1303
    except (IOError, PermissionDenied), e:
1304
        messages.error(request, e)
1305
    except BaseException, e:
1306
        logger.exception(e)
1307
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1308
        rollback = True
1309
    finally:
1310
        if rollback:
1311
            transaction.rollback()
1312
        else:
1313
            transaction.commit()
1314

    
1315
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1316
    return redirect(next)
1317

    
1318
@require_http_methods(["GET"])
1319
@signed_terms_required
1320
@login_required
1321
@transaction.commit_manually
1322
def project_accept_member(request, application_id, user_id):
1323
    rollback = False
1324
    try:
1325
        application_id = int(application_id)
1326
        user_id = int(user_id)
1327
        m = accept_membership(application_id, user_id, request.user)
1328
    except (IOError, PermissionDenied), e:
1329
        messages.error(request, e)
1330
    except BaseException, e:
1331
        logger.exception(e)
1332
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1333
        rollback = True
1334
    else:
1335
        realname = m.person.realname
1336
        msg = _(astakos_messages.USER_JOINED_PROJECT) % locals()
1337
        messages.success(request, msg)
1338
    finally:
1339
        if rollback:
1340
            transaction.rollback()
1341
        else:
1342
            transaction.commit()
1343
    return redirect(reverse('project_detail', args=(application_id,)))
1344

    
1345
@require_http_methods(["GET"])
1346
@signed_terms_required
1347
@login_required
1348
@transaction.commit_manually
1349
def project_remove_member(request, application_id, user_id):
1350
    rollback = False
1351
    try:
1352
        application_id = int(application_id)
1353
        user_id = int(user_id)
1354
        m = remove_membership(application_id, user_id, request.user)
1355
    except (IOError, PermissionDenied), e:
1356
        messages.error(request, e)
1357
    except BaseException, e:
1358
        logger.exception(e)
1359
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1360
        rollback = True
1361
    else:
1362
        realname = m.person.realname
1363
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1364
        messages.success(request, msg)
1365
    finally:
1366
        if rollback:
1367
            transaction.rollback()
1368
        else:
1369
            transaction.commit()
1370
    return redirect(reverse('project_detail', args=(application_id,)))
1371

    
1372
@require_http_methods(["GET"])
1373
@signed_terms_required
1374
@login_required
1375
@transaction.commit_manually
1376
def project_reject_member(request, application_id, user_id):
1377
    rollback = False
1378
    try:
1379
        application_id = int(application_id)
1380
        user_id = int(user_id)
1381
        m = reject_membership(application_id, user_id, request.user)
1382
    except (IOError, PermissionDenied), e:
1383
        messages.error(request, e)
1384
    except BaseException, e:
1385
        logger.exception(e)
1386
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1387
        rollback = True
1388
    else:
1389
        realname = m.person.realname
1390
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1391
        messages.success(request, msg)
1392
    finally:
1393
        if rollback:
1394
            transaction.rollback()
1395
        else:
1396
            transaction.commit()
1397
    return redirect(reverse('project_detail', args=(application_id,)))
1398

    
1399