Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (48.9 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_tables2 import RequestConfig
45

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

    
70
import astakos.im.messages as astakos_messages
71

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

    
109
logger = logging.getLogger(__name__)
110

    
111
callpoint = AstakosCallpoint()
112

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

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

    
135
            if not provider or not provider.is_active():
136
                raise PermissionDenied
137

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

    
148

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

    
163

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

    
179

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

    
187
    def decorator(func):
188
        if not required_providers:
189
            return func
190

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

    
207

    
208
def valid_astakos_user_required(func):
209
    return signed_terms_required(required_auth_methods_assigned()(login_required(func)))
210

    
211

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

218
    **Arguments**
219

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

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

228
    ``extra_context``
229
        An dictionary of variables to add to the template context.
230

231
    **Template:**
232

233
    im/profile.html or im/login.html or ``template_name`` keyword argument.
234

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

    
241
    third_party_token = request.GET.get('key', False)
242
    if third_party_token:
243
        messages.info(request, astakos_messages.AUTH_PROVIDER_LOGIN_TO_ADD)
244

    
245
    return render_response(
246
        template_name,
247
        login_form = LoginForm(request=request),
248
        context_instance = get_context(request, extra_context)
249
    )
250

    
251

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

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

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

266
    If the user isn't logged in, redirects to settings.LOGIN_URL.
267

268
    **Arguments**
269

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

274
    ``extra_context``
275
        An dictionary of variables to add to the template context.
276

277
    **Template:**
278

279
    im/invitations.html or ``template_name`` keyword argument.
280

281
    **Settings:**
282

283
    The view expectes the following settings are defined:
284

285
    * LOGIN_URL: login uri
286
    """
287
    extra_context = extra_context or {}
288
    status = None
289
    message = None
290
    form = InvitationForm()
291

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

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

    
329

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

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

342
    If the user isn't logged in, redirects to settings.LOGIN_URL.
343

344
    **Arguments**
345

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

350
    ``extra_context``
351
        An dictionary of variables to add to the template context.
352

353
    **Template:**
354

355
    im/profile.html or ``template_name`` keyword argument.
356

357
    **Settings:**
358

359
    The view expectes the following settings are defined:
360

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

    
397
    # existing providers
398
    user_providers = request.user.get_active_auth_providers()
399

    
400
    # providers that user can add
401
    user_available_providers = request.user.get_available_auth_providers()
402

    
403
    return render_response(template_name,
404
                           profile_form = form,
405
                           user_providers = user_providers,
406
                           user_available_providers = user_available_providers,
407
                           context_instance = get_context(request,
408
                                                          extra_context))
409

    
410

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

417
    In case of GET request renders a form for entering the user information.
418
    In case of POST handles the signup.
419

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

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

428
    On unsuccessful creation, renders ``template_name`` with an error message.
429

430
    **Arguments**
431

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

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

440
    ``extra_context``
441
        An dictionary of variables to add to the template context.
442

443
    **Template:**
444

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

    
452
    provider = get_query(request).get('provider', 'local')
453
    if not auth_providers.get_provider(provider).is_available_for_create():
454
        raise PermissionDenied
455

    
456
    id = get_query(request).get('id')
457
    try:
458
        instance = AstakosUser.objects.get(id=id) if id else None
459
    except AstakosUser.DoesNotExist:
460
        instance = None
461

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

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

    
484
                form.store_user(user, request)
485

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

    
527

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

536
    In case of GET request renders a form for providing the feedback information.
537
    In case of POST sends an email to support team.
538

539
    If the user isn't logged in, redirects to settings.LOGIN_URL.
540

541
    **Arguments**
542

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

547
    ``extra_context``
548
        An dictionary of variables to add to the template context.
549

550
    **Template:**
551

552
    im/signup.html or ``template_name`` keyword argument.
553

554
    **Settings:**
555

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

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

    
580

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

    
597
    next = restrict_next(
598
        request.GET.get('next'),
599
        domain=COOKIE_DOMAIN
600
    )
601

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

    
614

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

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

    
633
    if user.is_active:
634
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
635
        messages.error(request, message)
636
        return index(request)
637

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

    
656

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

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

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

    
703

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

    
714

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

    
730
        return render_response(confirm_template_name,
731
                               modified_user=user if 'user' in locals() \
732
                               else None, context_instance=get_context(request,
733
                                                            extra_context))
734

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

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

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

    
765
    if request.user.email_change_is_pending():
766
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
767

    
768
    return render_response(
769
        form_template_name,
770
        form=form,
771
        context_instance=get_context(request, extra_context)
772
    )
773

    
774

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

    
777
    if request.user.is_authenticated():
778
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
779
        return HttpResponseRedirect(reverse('edit_profile'))
780

    
781
    if astakos_settings.MODERATION_ENABLED:
782
        raise PermissionDenied
783

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

    
805

    
806
@require_http_methods(["GET"])
807
@valid_astakos_user_required
808
def resource_usage(request):
809

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

    
831
    def pluralize(entry):
832
        entry['plural'] = engine.plural(entry.get('name'))
833
        return entry
834

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

    
849

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

    
887

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

    
909

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

    
940

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

    
951
    if provider.can_remove():
952
        provider.delete()
953
        return HttpResponseRedirect(reverse('edit_profile'))
954
    else:
955
        raise PermissionDenied
956

    
957

    
958
def how_it_works(request):
959
    return render_response(
960
        'im/how_it_works.html',
961
        context_instance=get_context(request))
962

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

    
974
    if extra_context is None: extra_context = {}
975
    if login_required and not request.user.is_authenticated():
976
        return redirect_to_login(request.path)
977
    try:
978

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

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

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

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

    
1036
    if extra_context is None: extra_context = {}
1037
    if login_required and not request.user.is_authenticated():
1038
        return redirect_to_login(request.path)
1039

    
1040
    try:
1041
        model, form_class = get_model_and_form_class(model, form_class)
1042
        obj = lookup_object(model, object_id, slug, slug_field)
1043

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

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

    
1106

    
1107
@require_http_methods(["GET"])
1108
@signed_terms_required
1109
@login_required
1110
def project_list(request):
1111
    projects = ProjectApplication.objects.user_projects(request.user).select_related()
1112
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1113
                                                prefix="my_projects_")
1114
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1115

    
1116
    return object_list(
1117
        request,
1118
        projects,
1119
        template_name='im/projects/project_list.html',
1120
        extra_context={
1121
            'is_search':False,
1122
            'table': table,
1123
        }
1124
    )
1125

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

    
1150

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

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

    
1190
    rollback = False
1191

    
1192
    application = get_object_or_404(ProjectApplication, pk=application_id)
1193
    members = application.project.projectmembership_set.select_related()
1194
    members_table = tables.ProjectApplicationMembersTable(members,
1195
                                                          prefix="members_")
1196
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(members_table)
1197

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

    
1217

    
1218
@require_http_methods(["GET", "POST"])
1219
@signed_terms_required
1220
@login_required
1221
def project_search(request):
1222
    q = request.GET.get('q', '')
1223
    form = ProjectSearchForm()
1224
    q = q.strip()
1225

    
1226
    if request.method == "POST":
1227
        form = ProjectSearchForm(request.POST)
1228
        if form.is_valid():
1229
            q = form.cleaned_data['q'].strip()
1230
        else:
1231
            q = None
1232

    
1233
    if q is None:
1234
        projects = ProjectApplication.objects.none()
1235
    else:
1236
        projects = ProjectApplication.objects.search_by_name(q)
1237
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1238

    
1239
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1240
                                                prefix="my_projects_")
1241
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1242

    
1243
    return object_list(
1244
        request,
1245
        projects,
1246
        template_name='im/projects/project_list.html',
1247
        extra_context={
1248
          'form': form,
1249
          'is_search': True,
1250
          'q': q,
1251
          'table': table
1252
        })
1253

    
1254
@require_http_methods(["POST"])
1255
@signed_terms_required
1256
@login_required
1257
@transaction.commit_manually
1258
def project_join(request, application_id):
1259
    next = request.GET.get('next')
1260
    if not next:
1261
        return HttpResponseBadRequest(
1262
            _(astakos_messages.MISSING_NEXT_PARAMETER))
1263

    
1264
    rollback = False
1265
    try:
1266
        application_id = int(application_id)
1267
        join_project(application_id, request.user)
1268
    except (IOError, PermissionDenied), e:
1269
        messages.error(request, e)
1270
    except BaseException, e:
1271
        logger.exception(e)
1272
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1273
        rollback = True
1274
    finally:
1275
        if rollback:
1276
            transaction.rollback()
1277
        else:
1278
            transaction.commit()
1279
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1280
    return redirect(next)
1281

    
1282
@require_http_methods(["POST"])
1283
@signed_terms_required
1284
@login_required
1285
@transaction.commit_manually
1286
def project_leave(request, application_id):
1287
    next = request.GET.get('next')
1288
    if not next:
1289
        return HttpResponseBadRequest(
1290
            _(astakos_messages.MISSING_NEXT_PARAMETER))
1291

    
1292
    rollback = False
1293
    try:
1294
        application_id = int(application_id)
1295
        leave_project(application_id, request.user)
1296
    except (IOError, PermissionDenied), e:
1297
        messages.error(request, e)
1298
    except BaseException, e:
1299
        logger.exception(e)
1300
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1301
        rollback = True
1302
    finally:
1303
        if rollback:
1304
            transaction.rollback()
1305
        else:
1306
            transaction.commit()
1307

    
1308
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1309
    return redirect(next)
1310

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

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

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

    
1392