Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 53e0b8fe

History | View | Annotate | Download (70.1 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
#     AstakosGroup, Membership
74
    EmailChange, GroupKind,
75
    RESOURCE_SEPARATOR, AstakosUserAuthProvider,
76
    PendingThirdPartyUser,
77
    ProjectApplication, ProjectMembership, Project)
78
from astakos.im.util import (
79
    get_context, prepare_response, get_query, restrict_next)
80
from astakos.im.forms import (
81
    LoginForm, InvitationForm, ProfileForm,
82
    FeedbackForm, SignApprovalTermsForm,
83
    EmailChangeForm,
84
#     AstakosGroupCreationForm, AstakosGroupSearchForm,
85
#     AstakosGroupUpdateForm, AddGroupMembersForm,
86
#     MembersSortForm, AstakosGroupSortForm,
87
#     TimelineForm, PickResourceForm,
88
#     AstakosGroupCreationSummaryForm,
89
    ProjectApplicationForm, ProjectSortForm,
90
    AddProjectMembersForm, ProjectSearchForm,
91
    ProjectMembersSortForm)
92
from astakos.im.functions import (
93
    send_feedback, SendMailError,
94
    logout as auth_logout,
95
    activate as activate_func,
96
    invite,
97
    send_activation as send_activation_func,
98
#     send_group_creation_notification,
99
    SendNotificationError,
100
    accept_membership, reject_membership, remove_membership,
101
    leave_project, join_project)
102
# from astakos.im.endpoints.qh import timeline_charge
103
from astakos.im.settings import (
104
    COOKIE_DOMAIN, LOGOUT_NEXT,
105
    LOGGING_LEVEL, PAGINATE_BY,
106
    RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL)
107
#from astakos.im.tasks import request_billing
108
from astakos.im.api.callpoint import AstakosCallpoint
109

    
110
from astakos.im import settings
111
from astakos.im import auth_providers
112

    
113
logger = logging.getLogger(__name__)
114

    
115
callpoint = AstakosCallpoint()
116

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

    
131
def requires_auth_provider(provider_id, **perms):
132
    """
133
    """
134
    def decorator(func, *args, **kwargs):
135
        @wraps(func)
136
        def wrapper(request, *args, **kwargs):
137
            provider = auth_providers.get_provider(provider_id)
138

    
139
            if not provider or not provider.is_active():
140
                raise PermissionDenied
141

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

    
152

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

    
167

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

    
183

    
184
def required_auth_methods_assigned(only_warn=False):
185
    """
186
    Decorator that checks whether the request.user has all required auth providers
187
    assigned.
188
    """
189
    required_providers = auth_providers.REQUIRED_PROVIDERS.keys()
190

    
191
    def decorator(func):
192
        if not required_providers:
193
            return func
194

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

    
211

    
212
def valid_astakos_user_required(func):
213
    return signed_terms_required(required_auth_methods_assigned()(login_required(func)))
214

    
215

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

222
    **Arguments**
223

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

228
    ``profile_template_name``
229
        A custom profile template to use. This is optional; if not specified,
230
        this will default to ``im/profile.html``.
231

232
    ``extra_context``
233
        An dictionary of variables to add to the template context.
234

235
    **Template:**
236

237
    im/profile.html or im/login.html or ``template_name`` keyword argument.
238

239
    """
240
    extra_context = extra_context or {}
241
    template_name = login_template_name
242
    if request.user.is_authenticated():
243
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
244

    
245
    third_party_token = request.GET.get('key', False)
246
    if third_party_token:
247
        messages.info(request, astakos_messages.AUTH_PROVIDER_LOGIN_TO_ADD)
248

    
249
    return render_response(
250
        template_name,
251
        login_form = LoginForm(request=request),
252
        context_instance = get_context(request, extra_context)
253
    )
254

    
255

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

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

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

270
    If the user isn't logged in, redirects to settings.LOGIN_URL.
271

272
    **Arguments**
273

274
    ``template_name``
275
        A custom template to use. This is optional; if not specified,
276
        this will default to ``im/invitations.html``.
277

278
    ``extra_context``
279
        An dictionary of variables to add to the template context.
280

281
    **Template:**
282

283
    im/invitations.html or ``template_name`` keyword argument.
284

285
    **Settings:**
286

287
    The view expectes the following settings are defined:
288

289
    * LOGIN_URL: login uri
290
    """
291
    extra_context = extra_context or {}
292
    status = None
293
    message = None
294
    form = InvitationForm()
295

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

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

    
333

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

342
    In case of GET request renders a form for displaying the user information.
343
    In case of POST updates the user informantion and redirects to ``next``
344
    url parameter if exists.
345

346
    If the user isn't logged in, redirects to settings.LOGIN_URL.
347

348
    **Arguments**
349

350
    ``template_name``
351
        A custom template to use. This is optional; if not specified,
352
        this will default to ``im/profile.html``.
353

354
    ``extra_context``
355
        An dictionary of variables to add to the template context.
356

357
    **Template:**
358

359
    im/profile.html or ``template_name`` keyword argument.
360

361
    **Settings:**
362

363
    The view expectes the following settings are defined:
364

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

    
401
    # existing providers
402
    user_providers = request.user.get_active_auth_providers()
403

    
404
    # providers that user can add
405
    user_available_providers = request.user.get_available_auth_providers()
406

    
407
    return render_response(template_name,
408
                           profile_form = form,
409
                           user_providers = user_providers,
410
                           user_available_providers = user_available_providers,
411
                           context_instance = get_context(request,
412
                                                          extra_context))
413

    
414

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

421
    In case of GET request renders a form for entering the user information.
422
    In case of POST handles the signup.
423

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

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

432
    On unsuccessful creation, renders ``template_name`` with an error message.
433

434
    **Arguments**
435

436
    ``template_name``
437
        A custom template to render. This is optional;
438
        if not specified, this will default to ``im/signup.html``.
439

440
    ``on_success``
441
        A custom template to render in case of success. This is optional;
442
        if not specified, this will default to ``im/signup_complete.html``.
443

444
    ``extra_context``
445
        An dictionary of variables to add to the template context.
446

447
    **Template:**
448

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

    
456
    provider = get_query(request).get('provider', 'local')
457
    if not auth_providers.get_provider(provider).is_available_for_create():
458
        raise PermissionDenied
459

    
460
    id = get_query(request).get('id')
461
    try:
462
        instance = AstakosUser.objects.get(id=id) if id else None
463
    except AstakosUser.DoesNotExist:
464
        instance = None
465

    
466
    third_party_token = request.REQUEST.get('third_party_token', None)
467
    if third_party_token:
468
        pending = get_object_or_404(PendingThirdPartyUser,
469
                                    token=third_party_token)
470
        provider = pending.provider
471
        instance = pending.get_user_instance()
472

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

    
488
                form.store_user(user, request)
489

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

    
531

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

540
    In case of GET request renders a form for providing the feedback information.
541
    In case of POST sends an email to support team.
542

543
    If the user isn't logged in, redirects to settings.LOGIN_URL.
544

545
    **Arguments**
546

547
    ``template_name``
548
        A custom template to use. This is optional; if not specified,
549
        this will default to ``im/feedback.html``.
550

551
    ``extra_context``
552
        An dictionary of variables to add to the template context.
553

554
    **Template:**
555

556
    im/signup.html or ``template_name`` keyword argument.
557

558
    **Settings:**
559

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

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

    
584

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

    
601
    next = restrict_next(
602
        request.GET.get('next'),
603
        domain=COOKIE_DOMAIN
604
    )
605

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

    
618

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

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

    
637
    if user.is_active:
638
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
639
        messages.error(request, message)
640
        return index(request)
641

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

    
660

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

    
677
    if not term:
678
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
679
        return HttpResponseRedirect(reverse('index'))
680
    f = open(term.location, 'r')
681
    terms = f.read()
682

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

    
707

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

    
718

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

    
734
        return render_response(confirm_template_name,
735
                               modified_user=user if 'user' in locals() \
736
                               else None, context_instance=get_context(request,
737
                                                            extra_context))
738

    
739
    if not request.user.is_authenticated():
740
        path = quote(request.get_full_path())
741
        url = request.build_absolute_uri(reverse('index'))
742
        return HttpResponseRedirect(url + '?next=' + path)
743

    
744
    # clean up expired email changes
745
    if request.user.email_change_is_pending():
746
        change = request.user.emailchanges.get()
747
        if change.activation_key_expired():
748
            change.delete()
749
            transaction.commit()
750
            return HttpResponseRedirect(reverse('email_change'))
751

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

    
769
    if request.user.email_change_is_pending():
770
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
771

    
772
    return render_response(
773
        form_template_name,
774
        form=form,
775
        context_instance=get_context(request, extra_context)
776
    )
777

    
778

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

    
781
    if request.user.is_authenticated():
782
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
783
        return HttpResponseRedirect(reverse('edit_profile'))
784

    
785
    if settings.MODERATION_ENABLED:
786
        raise PermissionDenied
787

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

    
809
# class ResourcePresentation():
810
# 
811
#     def __init__(self, data):
812
#         self.data = data
813
# 
814
#     def update_from_result(self, result):
815
#         if result.is_success:
816
#             for r in result.data:
817
#                 rname = '%s%s%s' % (r.get('service'), RESOURCE_SEPARATOR, r.get('name'))
818
#                 if not rname in self.data['resources']:
819
#                     self.data['resources'][rname] = {}
820
# 
821
#                 self.data['resources'][rname].update(r)
822
#                 self.data['resources'][rname]['id'] = rname
823
#                 group = r.get('group')
824
#                 if not group in self.data['groups']:
825
#                     self.data['groups'][group] = {}
826
# 
827
#                 self.data['groups'][r.get('group')].update({'name': r.get('group')})
828
# 
829
#     def test(self, quota_dict):
830
#         for k, v in quota_dict.iteritems():
831
#             rname = k
832
#             value = v
833
#             if not rname in self.data['resources']:
834
#                 self.data['resources'][rname] = {}
835
# 
836
# 
837
#             self.data['resources'][rname]['value'] = value
838
# 
839
# 
840
#     def update_from_result_report(self, result):
841
#         if result.is_success:
842
#             for r in result.data:
843
#                 rname = r.get('name')
844
#                 if not rname in self.data['resources']:
845
#                     self.data['resources'][rname] = {}
846
# 
847
#                 self.data['resources'][rname].update(r)
848
#                 self.data['resources'][rname]['id'] = rname
849
#                 group = r.get('group')
850
#                 if not group in self.data['groups']:
851
#                     self.data['groups'][group] = {}
852
# 
853
#                 self.data['groups'][r.get('group')].update({'name': r.get('group')})
854
# 
855
#     def get_group_resources(self, group):
856
#         return dict(filter(lambda t: t[1].get('group') == group, self.data['resources'].iteritems()))
857
# 
858
#     def get_groups_resources(self):
859
#         for g in self.data['groups']:
860
#             yield g, self.get_group_resources(g)
861
# 
862
#     def get_quota(self, group_quotas):
863
#         for r, v in group_quotas:
864
#             rname = str(r)
865
#             quota = self.data['resources'].get(rname)
866
#             quota['value'] = v
867
#             yield quota
868
# 
869
# 
870
#     def get_policies(self, policies_data):
871
#         for policy in policies_data:
872
#             rname = '%s%s%s' % (policy.get('service'), RESOURCE_SEPARATOR, policy.get('resource'))
873
#             policy.update(self.data['resources'].get(rname))
874
#             yield policy
875
# 
876
#     def __repr__(self):
877
#         return self.data.__repr__()
878
# 
879
#     def __iter__(self, *args, **kwargs):
880
#         return self.data.__iter__(*args, **kwargs)
881
# 
882
#     def __getitem__(self, *args, **kwargs):
883
#         return self.data.__getitem__(*args, **kwargs)
884
# 
885
#     def get(self, *args, **kwargs):
886
#         return self.data.get(*args, **kwargs)
887

    
888

    
889

    
890
# @require_http_methods(["GET", "POST"])
891
# @signed_terms_required
892
# @login_required
893
# def group_add(request, kind_name='default'):
894
#     result = callpoint.list_resources()
895
#     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
896
#     resource_catalog.update_from_result(result)
897
# 
898
#     if not result.is_success:
899
#         messages.error(
900
#             request,
901
#             'Unable to retrieve system resources: %s' % result.reason
902
#     )
903
# 
904
#     try:
905
#         kind = GroupKind.objects.get(name=kind_name)
906
#     except:
907
#         return HttpResponseBadRequest(_(astakos_messages.GROUPKIND_UNKNOWN))
908
# 
909
# 
910
# 
911
#     post_save_redirect = '/im/group/%(id)s/'
912
#     context_processors = None
913
#     model, form_class = get_model_and_form_class(
914
#         model=None,
915
#         form_class=AstakosGroupCreationForm
916
#     )
917
# 
918
#     if request.method == 'POST':
919
#         form = form_class(request.POST, request.FILES)
920
#         if form.is_valid():
921
#             policies = form.policies()
922
#             return render_response(
923
#                 template='im/astakosgroup_form_summary.html',
924
#                 context_instance=get_context(request),
925
#                 form=AstakosGroupCreationSummaryForm(form.cleaned_data),
926
#                 policies=resource_catalog.get_policies(policies)
927
#             )
928
#     else:
929
#         now = datetime.now()
930
#         data = {
931
#             'kind': kind,
932
#         }
933
#         for group, resources in resource_catalog.get_groups_resources():
934
#             data['is_selected_%s' % group] = False
935
#             for resource in resources:
936
#                 data['%s_uplimit' % resource] = ''
937
# 
938
#         form = form_class(data)
939
# 
940
#     # Create the template, context, response
941
#     template_name = "%s/%s_form.html" % (
942
#         model._meta.app_label,
943
#         model._meta.object_name.lower()
944
#     )
945
#     t = template_loader.get_template(template_name)
946
#     c = RequestContext(request, {
947
#         'form': form,
948
#         'kind': kind,
949
#         'resource_catalog':resource_catalog,
950
#     }, context_processors)
951
#     return HttpResponse(t.render(c))
952

    
953

    
954
##@require_hsttp_methods(["POST"])
955
# @require_http_methods(["GET", "POST"])
956
# @signed_terms_required
957
# @login_required
958
# @transaction.commit_manually
959
# def group_add_complete(request):
960
#     model = AstakosGroup
961
#     form = AstakosGroupCreationSummaryForm(request.POST)
962
#     if form.is_valid():
963
#         d = form.cleaned_data
964
#         d['owners'] = [request.user]
965
#         result = callpoint.create_groups((d,)).next()
966
#         if result.is_success:
967
#             new_object = result.data[0]
968
#             # send notification
969
#             try:
970
#                 send_group_creation_notification(
971
#                     template_name='im/group_creation_notification.txt',
972
#                     dictionary={
973
#                         'group': new_object,
974
#                         'owner': request.user,
975
#                         'policies': d.get('policies', [])
976
#                     }
977
#                 )
978
#             except SendNotificationError, e:
979
#                 messages.error(request, e, fail_silently=True)
980
#                 transaction.rollback()
981
#             else:
982
#                 msg = _(astakos_messages.OBJECT_CREATED) %\
983
#                     {"verbose_name": model._meta.verbose_name}
984
#                 message = _(astakos_messages.NOTIFICATION_SENT) % 'a project'
985
#                 messages.success(request, msg, fail_silently=True)
986
#                 transaction.commit()
987
#         else:
988
#             d = {"verbose_name": model._meta.verbose_name,
989
#                  "reason":result.reason}
990
#             msg = _(astakos_messages.OBJECT_CREATED_FAILED) % d
991
#             messages.error(request, msg, fail_silently=True)
992
#     return render_response(
993
#         template='im/astakosgroup_form_summary.html',
994
#         context_instance=get_context(request),
995
#         form=form,
996
#         policies=form.cleaned_data.get('policies')
997
#     )
998

    
999
##@require_http_methods(["GET"])
1000
# @require_http_methods(["GET", "POST"])
1001
# @signed_terms_required
1002
# @login_required
1003
# def group_list(request):
1004
#     none = request.user.astakos_groups.none()
1005
#     query = """
1006
#         SELECT auth_group.id,
1007
#         auth_group.name AS groupname,
1008
#         im_groupkind.name AS kindname,
1009
#         im_astakosgroup.*,
1010
#         owner.email AS groupowner,
1011
#         (SELECT COUNT(*) FROM im_membership
1012
#             WHERE group_id = im_astakosgroup.group_ptr_id
1013
#             AND date_joined IS NOT NULL) AS approved_members_num,
1014
#         (SELECT CASE WHEN(
1015
#                     SELECT date_joined FROM im_membership
1016
#                     WHERE group_id = im_astakosgroup.group_ptr_id
1017
#                     AND person_id = %(id)s) IS NULL
1018
#                     THEN 0 ELSE 1 END) AS membership_status
1019
#         FROM im_astakosgroup
1020
#         INNER JOIN im_membership ON (
1021
#             im_astakosgroup.group_ptr_id = im_membership.group_id)
1022
#         INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
1023
#         INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
1024
#         LEFT JOIN im_astakosuser_owner ON (
1025
#             im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
1026
#         LEFT JOIN auth_user as owner ON (
1027
#             im_astakosuser_owner.astakosuser_id = owner.id)
1028
#         WHERE im_membership.person_id = %(id)s
1029
#         AND im_groupkind.name != 'default'
1030
#         """ % request.user.__dict__
1031
# 
1032
#     # validate sorting
1033
#     sorting = 'groupname'
1034
#     sort_form = AstakosGroupSortForm(request.GET)
1035
#     if sort_form.is_valid():
1036
#         sorting = sort_form.cleaned_data.get('sorting')
1037
#     query = query+" ORDER BY %s ASC" %sorting
1038
#     
1039
#     q = AstakosGroup.objects.raw(query)
1040
#     
1041
#     # Create the template, context, response
1042
#     template_name = "%s/%s_list.html" % (
1043
#         q.model._meta.app_label,
1044
#         q.model._meta.object_name.lower()
1045
#     )
1046
#     extra_context = dict(
1047
#         is_search=False,
1048
#         q=q,
1049
#         sorting=sorting,
1050
#         page=request.GET.get('page', 1)
1051
#     )
1052
#     return render_response(template_name,
1053
#                            context_instance=get_context(request, extra_context)
1054
#     )
1055

    
1056

    
1057
# @require_http_methods(["GET", "POST"])
1058
# @signed_terms_required
1059
# @login_required
1060
# def group_detail(request, group_id):
1061
#     q = AstakosGroup.objects.select_related().filter(pk=group_id)
1062
#     q = q.extra(select={
1063
#         'is_member': """SELECT CASE WHEN EXISTS(
1064
#                             SELECT id FROM im_membership
1065
#                             WHERE group_id = im_astakosgroup.group_ptr_id
1066
#                             AND person_id = %s)
1067
#                         THEN 1 ELSE 0 END""" % request.user.id,
1068
#         'is_owner': """SELECT CASE WHEN EXISTS(
1069
#                         SELECT id FROM im_astakosuser_owner
1070
#                         WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1071
#                         AND astakosuser_id = %s)
1072
#                         THEN 1 ELSE 0 END""" % request.user.id,
1073
#         'is_active_member': """SELECT CASE WHEN(
1074
#                         SELECT date_joined FROM im_membership
1075
#                         WHERE group_id = im_astakosgroup.group_ptr_id
1076
#                         AND person_id = %s) IS NULL
1077
#                         THEN 0 ELSE 1 END""" % request.user.id,
1078
#         'kindname': """SELECT name FROM im_groupkind
1079
#                        WHERE id = im_astakosgroup.kind_id"""})
1080
# 
1081
#     model = q.model
1082
#     context_processors = None
1083
#     mimetype = None
1084
#     try:
1085
#         obj = q.get()
1086
#     except AstakosGroup.DoesNotExist:
1087
#         raise Http404("No %s found matching the query" % (
1088
#             model._meta.verbose_name))
1089
# 
1090
#     update_form = AstakosGroupUpdateForm(instance=obj)
1091
#     addmembers_form = AddGroupMembersForm()
1092
#     if request.method == 'POST':
1093
#         update_data = {}
1094
#         addmembers_data = {}
1095
#         for k, v in request.POST.iteritems():
1096
#             if k in update_form.fields:
1097
#                 update_data[k] = v
1098
#             if k in addmembers_form.fields:
1099
#                 addmembers_data[k] = v
1100
#         update_data = update_data or None
1101
#         addmembers_data = addmembers_data or None
1102
#         update_form = AstakosGroupUpdateForm(update_data, instance=obj)
1103
#         addmembers_form = AddGroupMembersForm(addmembers_data)
1104
#         if update_form.is_valid():
1105
#             update_form.save()
1106
#         if addmembers_form.is_valid():
1107
#             try:
1108
#                 map(obj.approve_member, addmembers_form.valid_users)
1109
#             except AssertionError:
1110
#                 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1111
#                 messages.error(request, msg)
1112
#             addmembers_form = AddGroupMembersForm()
1113
# 
1114
#     template_name = "%s/%s_detail.html" % (
1115
#         model._meta.app_label, model._meta.object_name.lower())
1116
#     t = template_loader.get_template(template_name)
1117
#     c = RequestContext(request, {
1118
#         'object': obj,
1119
#     }, context_processors)
1120
# 
1121
#     # validate sorting
1122
#     sorting = 'person__email'
1123
#     form = MembersSortForm(request.GET)
1124
#     if form.is_valid():
1125
#         sorting = form.cleaned_data.get('sorting')
1126
#     
1127
#     result = callpoint.list_resources()
1128
#     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1129
#     resource_catalog.update_from_result(result)
1130
# 
1131
# 
1132
#     if not result.is_success:
1133
#         messages.error(
1134
#             request,
1135
#             'Unable to retrieve system resources: %s' % result.reason
1136
#     )
1137
# 
1138
#     extra_context = {'update_form': update_form,
1139
#                      'addmembers_form': addmembers_form,
1140
#                      'page': request.GET.get('page', 1),
1141
#                      'sorting': sorting,
1142
#                      'resource_catalog':resource_catalog,
1143
#                      'quota':resource_catalog.get_quota(obj.quota)}
1144
#     for key, value in extra_context.items():
1145
#         if callable(value):
1146
#             c[key] = value()
1147
#         else:
1148
#             c[key] = value
1149
#     response = HttpResponse(t.render(c), mimetype=mimetype)
1150
#     populate_xheaders(
1151
#         request, response, model, getattr(obj, obj._meta.pk.name))
1152
#     return response
1153

    
1154

    
1155
# @require_http_methods(["GET", "POST"])
1156
# @signed_terms_required
1157
# @login_required
1158
# def group_search(request, extra_context=None, **kwargs):
1159
#     q = request.GET.get('q')
1160
#     if request.method == 'GET':
1161
#         form = AstakosGroupSearchForm({'q': q} if q else None)
1162
#     else:
1163
#         form = AstakosGroupSearchForm(get_query(request))
1164
#         if form.is_valid():
1165
#             q = form.cleaned_data['q'].strip()
1166
#     
1167
#     sorting = 'groupname'
1168
#     if q:
1169
#         queryset = AstakosGroup.objects.select_related()
1170
#         queryset = queryset.filter(~Q(kind__name='default'))
1171
#         queryset = queryset.filter(name__contains=q)
1172
#         queryset = queryset.filter(approval_date__isnull=False)
1173
#         queryset = queryset.extra(select={
1174
#                                   'groupname': "auth_group.name",
1175
#                                   'kindname': "im_groupkind.name",
1176
#                                   'approved_members_num': """
1177
#                     SELECT COUNT(*) FROM im_membership
1178
#                     WHERE group_id = im_astakosgroup.group_ptr_id
1179
#                     AND date_joined IS NOT NULL""",
1180
#                                   'membership_approval_date': """
1181
#                     SELECT date_joined FROM im_membership
1182
#                     WHERE group_id = im_astakosgroup.group_ptr_id
1183
#                     AND person_id = %s""" % request.user.id,
1184
#                                   'is_member': """
1185
#                     SELECT CASE WHEN EXISTS(
1186
#                     SELECT date_joined FROM im_membership
1187
#                     WHERE group_id = im_astakosgroup.group_ptr_id
1188
#                     AND person_id = %s)
1189
#                     THEN 1 ELSE 0 END""" % request.user.id,
1190
#                                   'is_owner': """
1191
#                     SELECT CASE WHEN EXISTS(
1192
#                     SELECT id FROM im_astakosuser_owner
1193
#                     WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1194
#                     AND astakosuser_id = %s)
1195
#                     THEN 1 ELSE 0 END""" % request.user.id,
1196
#                     'is_owner': """SELECT CASE WHEN EXISTS(
1197
#                         SELECT id FROM im_astakosuser_owner
1198
#                         WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1199
#                         AND astakosuser_id = %s)
1200
#                         THEN 1 ELSE 0 END""" % request.user.id,
1201
#                     })
1202
#         
1203
#         # validate sorting
1204
#         sort_form = AstakosGroupSortForm(request.GET)
1205
#         if sort_form.is_valid():
1206
#             sorting = sort_form.cleaned_data.get('sorting')
1207
#         queryset = queryset.order_by(sorting)
1208
# 
1209
#     else:
1210
#         queryset = AstakosGroup.objects.none()
1211
#     return object_list(
1212
#         request,
1213
#         queryset,
1214
#         paginate_by=PAGINATE_BY_ALL,
1215
#         page=request.GET.get('page') or 1,
1216
#         template_name='im/astakosgroup_list.html',
1217
#         extra_context=dict(form=form,
1218
#                            is_search=True,
1219
#                            q=q,
1220
#                            sorting=sorting))
1221

    
1222

    
1223
# @require_http_methods(["GET", "POST"])
1224
# @signed_terms_required
1225
# @login_required
1226
# def group_all(request, extra_context=None, **kwargs):
1227
#     q = AstakosGroup.objects.select_related()
1228
#     q = q.filter(~Q(kind__name='default'))
1229
#     q = q.filter(approval_date__isnull=False)
1230
#     q = q.extra(select={
1231
#                 'groupname': "auth_group.name",
1232
#                 'kindname': "im_groupkind.name",
1233
#                 'approved_members_num': """
1234
#                     SELECT COUNT(*) FROM im_membership
1235
#                     WHERE group_id = im_astakosgroup.group_ptr_id
1236
#                     AND date_joined IS NOT NULL""",
1237
#                 'membership_approval_date': """
1238
#                     SELECT date_joined FROM im_membership
1239
#                     WHERE group_id = im_astakosgroup.group_ptr_id
1240
#                     AND person_id = %s""" % request.user.id,
1241
#                 'is_member': """
1242
#                     SELECT CASE WHEN EXISTS(
1243
#                     SELECT date_joined FROM im_membership
1244
#                     WHERE group_id = im_astakosgroup.group_ptr_id
1245
#                     AND person_id = %s)
1246
#                     THEN 1 ELSE 0 END""" % request.user.id,
1247
#                  'is_owner': """SELECT CASE WHEN EXISTS(
1248
#                         SELECT id FROM im_astakosuser_owner
1249
#                         WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1250
#                         AND astakosuser_id = %s)
1251
#                         THEN 1 ELSE 0 END""" % request.user.id,   })
1252
#     
1253
#     # validate sorting
1254
#     sorting = 'groupname'
1255
#     sort_form = AstakosGroupSortForm(request.GET)
1256
#     if sort_form.is_valid():
1257
#         sorting = sort_form.cleaned_data.get('sorting')
1258
#     q = q.order_by(sorting)
1259
#     
1260
#     return object_list(
1261
#         request,
1262
#         q,
1263
#         paginate_by=PAGINATE_BY_ALL,
1264
#         page=request.GET.get('page') or 1,
1265
#         template_name='im/astakosgroup_list.html',
1266
#         extra_context=dict(form=AstakosGroupSearchForm(),
1267
#                            is_search=True,
1268
#                            sorting=sorting))
1269

    
1270

    
1271
##@require_http_methods(["POST"])
1272
# @require_http_methods(["POST", "GET"])
1273
# @signed_terms_required
1274
# @login_required
1275
# def group_join(request, group_id):
1276
#     m = Membership(group_id=group_id,
1277
#                    person=request.user,
1278
#                    date_requested=datetime.now())
1279
#     try:
1280
#         m.save()
1281
#         post_save_redirect = reverse(
1282
#             'group_detail',
1283
#             kwargs=dict(group_id=group_id))
1284
#         return HttpResponseRedirect(post_save_redirect)
1285
#     except IntegrityError, e:
1286
#         logger.exception(e)
1287
#         msg = _(astakos_messages.GROUP_JOIN_FAILURE)
1288
#         messages.error(request, msg)
1289
#         return group_search(request)
1290
# 
1291
# 
1292
# @require_http_methods(["POST"])
1293
# @signed_terms_required
1294
# @login_required
1295
# def group_leave(request, group_id):
1296
#     try:
1297
#         m = Membership.objects.select_related().get(
1298
#             group__id=group_id,
1299
#             person=request.user)
1300
#     except Membership.DoesNotExist:
1301
#         return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1302
#     if request.user in m.group.owner.all():
1303
#         return HttpResponseForbidden(_(astakos_messages.OWNER_CANNOT_LEAVE_GROUP))
1304
#     return delete_object(
1305
#         request,
1306
#         model=Membership,
1307
#         object_id=m.id,
1308
#         template_name='im/astakosgroup_list.html',
1309
#         post_delete_redirect=reverse(
1310
#             'group_detail',
1311
#             kwargs=dict(group_id=group_id)))
1312

    
1313

    
1314
# def handle_membership(func):
1315
#     @wraps(func)
1316
#     def wrapper(request, group_id, user_id):
1317
#         try:
1318
#             m = Membership.objects.select_related().get(
1319
#                 group__id=group_id,
1320
#                 person__id=user_id)
1321
#         except Membership.DoesNotExist:
1322
#             return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1323
#         else:
1324
#             if request.user not in m.group.owner.all():
1325
#                 return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
1326
#             func(request, m)
1327
#             return group_detail(request, group_id)
1328
#     return wrapper
1329

    
1330

    
1331
#@require_http_methods(["POST"])
1332
# @require_http_methods(["POST", "GET"])
1333
# @signed_terms_required
1334
# @login_required
1335
# @handle_membership
1336
# def approve_member(request, membership):
1337
#     try:
1338
#         membership.approve()
1339
#         realname = membership.person.realname
1340
#         msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
1341
#         messages.success(request, msg)
1342
#     except AssertionError:
1343
#         msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1344
#         messages.error(request, msg)
1345
#     except BaseException, e:
1346
#         logger.exception(e)
1347
#         realname = membership.person.realname
1348
#         msg = _(astakos_messages.GENERIC_ERROR)
1349
#         messages.error(request, msg)
1350

    
1351

    
1352
# @signed_terms_required
1353
# @login_required
1354
# @handle_membership
1355
# def disapprove_member(request, membership):
1356
#     try:
1357
#         membership.disapprove()
1358
#         realname = membership.person.realname
1359
#         msg = astakos_messages.MEMBER_REMOVED % locals()
1360
#         messages.success(request, msg)
1361
#     except BaseException, e:
1362
#         logger.exception(e)
1363
#         msg = _(astakos_messages.GENERIC_ERROR)
1364
#         messages.error(request, msg)
1365

    
1366

    
1367
@require_http_methods(["GET"])
1368
@require_http_methods(["POST", "GET"])
1369
@valid_astakos_user_required
1370
def resource_usage(request):
1371
    def with_class(entry):
1372
        entry['load_class'] = 'red'
1373
        max_value = float(entry['maxValue'])
1374
        curr_value = float(entry['currValue'])
1375
        entry['ratio_limited']= 0
1376
        if max_value > 0 :
1377
            entry['ratio'] = (curr_value / max_value) * 100
1378
        else:
1379
            entry['ratio'] = 0
1380
        if entry['ratio'] < 66:
1381
            entry['load_class'] = 'yellow'
1382
        if entry['ratio'] < 33:
1383
            entry['load_class'] = 'green'
1384
        if entry['ratio']<0:
1385
            entry['ratio'] = 0
1386
        if entry['ratio']>100:
1387
            entry['ratio_limited'] = 100
1388
        else:
1389
            entry['ratio_limited'] = entry['ratio']
1390

    
1391
        return entry
1392

    
1393
    def pluralize(entry):
1394
        entry['plural'] = engine.plural(entry.get('name'))
1395
        return entry
1396

    
1397
    result = callpoint.get_user_usage(request.user.id)
1398
    if result.is_success:
1399
        backenddata = map(with_class, result.data)
1400
        data = map(pluralize, result.data)
1401
    else:
1402
        data = None
1403
        messages.error(request, result.reason)
1404
    resource_catalog = result.data
1405
#     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1406
#     resource_catalog.update_from_result_report(result)
1407
    return render_response('im/resource_usage.html',
1408
                           data=data,
1409
                           context_instance=get_context(request),
1410
                           resource_catalog=resource_catalog,
1411
                           result=result)
1412

    
1413

    
1414
# def group_create_list(request):
1415
#     form = PickResourceForm()
1416
#     return render_response(
1417
#         template='im/astakosgroup_create_list.html',
1418
#         context_instance=get_context(request),)
1419

    
1420

    
1421
##@require_http_methods(["GET"])
1422
#@require_http_methods(["POST", "GET"])
1423
#@signed_terms_required
1424
#@login_required
1425
#def billing(request):
1426
#
1427
#    today = datetime.today()
1428
#    month_last_day = calendar.monthrange(today.year, today.month)[1]
1429
#    start = request.POST.get('datefrom', None)
1430
#    if start:
1431
#        today = datetime.fromtimestamp(int(start))
1432
#        month_last_day = calendar.monthrange(today.year, today.month)[1]
1433
#
1434
#    start = datetime(today.year, today.month, 1).strftime("%s")
1435
#    end = datetime(today.year, today.month, month_last_day).strftime("%s")
1436
#    r = request_billing.apply(args=('pgerakios@grnet.gr',
1437
#                                    int(start) * 1000,
1438
#                                    int(end) * 1000))
1439
#    data = {}
1440
#
1441
#    try:
1442
#        status, data = r.result
1443
#        data = _clear_billing_data(data)
1444
#        if status != 200:
1445
#            messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
1446
#    except:
1447
#        messages.error(request, r.result)
1448
#
1449
#    return render_response(
1450
#        template='im/billing.html',
1451
#        context_instance=get_context(request),
1452
#        data=data,
1453
#        zerodate=datetime(month=1, year=1970, day=1),
1454
#        today=today,
1455
#        start=int(start),
1456
#        month_last_day=month_last_day)
1457

    
1458

    
1459
#def _clear_billing_data(data):
1460
#
1461
#    # remove addcredits entries
1462
#    def isnotcredit(e):
1463
#        return e['serviceName'] != "addcredits"
1464
#
1465
#    # separate services
1466
#    def servicefilter(service_name):
1467
#        service = service_name
1468
#
1469
#        def fltr(e):
1470
#            return e['serviceName'] == service
1471
#        return fltr
1472
#
1473
#    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1474
#    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1475
#    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1476
#    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1477
#
1478
#    return data
1479

    
1480

    
1481
# #@require_http_methods(["GET"])
1482
# @require_http_methods(["POST", "GET"])
1483
# @signed_terms_required
1484
# @login_required
1485
# def timeline(request):
1486
# #    data = {'entity':request.user.email}
1487
#     timeline_body = ()
1488
#     timeline_header = ()
1489
# #    form = TimelineForm(data)
1490
#     form = TimelineForm()
1491
#     if request.method == 'POST':
1492
#         data = request.POST
1493
#         form = TimelineForm(data)
1494
#         if form.is_valid():
1495
#             data = form.cleaned_data
1496
#             timeline_header = ('entity', 'resource',
1497
#                                'event name', 'event date',
1498
#                                'incremental cost', 'total cost')
1499
#             timeline_body = timeline_charge(
1500
#                 data['entity'], data['resource'],
1501
#                 data['start_date'], data['end_date'],
1502
#                 data['details'], data['operation'])
1503
#
1504
#     return render_response(template='im/timeline.html',
1505
#                            context_instance=get_context(request),
1506
#                            form=form,
1507
#                            timeline_header=timeline_header,
1508
#                            timeline_body=timeline_body)
1509
#     return data
1510

    
1511

    
1512
# TODO: action only on POST and user should confirm the removal
1513
@require_http_methods(["GET", "POST"])
1514
@login_required
1515
@signed_terms_required
1516
def remove_auth_provider(request, pk):
1517
    try:
1518
        provider = request.user.auth_providers.get(pk=pk)
1519
    except AstakosUserAuthProvider.DoesNotExist:
1520
        raise Http404
1521

    
1522
    if provider.can_remove():
1523
        provider.delete()
1524
        return HttpResponseRedirect(reverse('edit_profile'))
1525
    else:
1526
        raise PermissionDenied
1527

    
1528

    
1529
def how_it_works(request):
1530
    return render_response(
1531
        'im/how_it_works.html',
1532
        context_instance=get_context(request))
1533

    
1534
@transaction.commit_manually
1535
def _create_object(request, model=None, template_name=None,
1536
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
1537
        login_required=False, context_processors=None, form_class=None):
1538
    """
1539
    Based of django.views.generic.create_update.create_object which displays a
1540
    summary page before creating the object.
1541
    """
1542
    rollback = False
1543
    response = None
1544

    
1545
    if extra_context is None: extra_context = {}
1546
    if login_required and not request.user.is_authenticated():
1547
        return redirect_to_login(request.path)
1548
    try:
1549

    
1550
        model, form_class = get_model_and_form_class(model, form_class)
1551
        extra_context['edit'] = 0
1552
        if request.method == 'POST':
1553
            form = form_class(request.POST, request.FILES)
1554
            if form.is_valid():
1555
                verify = request.GET.get('verify')
1556
                edit = request.GET.get('edit')
1557
                if verify == '1':
1558
                    extra_context['show_form'] = False
1559
                    extra_context['form_data'] = form.cleaned_data
1560
                elif edit == '1':
1561
                    extra_context['show_form'] = True
1562
                else:
1563
                    new_object = form.save()
1564

    
1565
                    msg = _("The %(verbose_name)s was created successfully.") %\
1566
                                {"verbose_name": model._meta.verbose_name}
1567
                    messages.success(request, msg, fail_silently=True)
1568
                    response = redirect(post_save_redirect, new_object)
1569
        else:
1570
            form = form_class()
1571
    except BaseException, e:
1572
        logger.exception(e)
1573
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1574
        rollback = True
1575
    finally:
1576
        if rollback:
1577
            transaction.rollback()
1578
        else:
1579
            transaction.commit()
1580

    
1581
        if response == None:
1582
            # Create the template, context, response
1583
            if not template_name:
1584
                template_name = "%s/%s_form.html" %\
1585
                     (model._meta.app_label, model._meta.object_name.lower())
1586
            t = template_loader.get_template(template_name)
1587
            c = RequestContext(request, {
1588
                'form': form
1589
            }, context_processors)
1590
            apply_extra_context(extra_context, c)
1591
            response = HttpResponse(t.render(c))
1592
        return response
1593

    
1594
@transaction.commit_manually
1595
def _update_object(request, model=None, object_id=None, slug=None,
1596
        slug_field='slug', template_name=None, template_loader=template_loader,
1597
        extra_context=None, post_save_redirect=None, login_required=False,
1598
        context_processors=None, template_object_name='object',
1599
        form_class=None):
1600
    """
1601
    Based of django.views.generic.create_update.update_object which displays a
1602
    summary page before updating the object.
1603
    """
1604
    rollback = False
1605
    response = None
1606

    
1607
    if extra_context is None: extra_context = {}
1608
    if login_required and not request.user.is_authenticated():
1609
        return redirect_to_login(request.path)
1610

    
1611
    try:
1612
        model, form_class = get_model_and_form_class(model, form_class)
1613
        obj = lookup_object(model, object_id, slug, slug_field)
1614

    
1615
        if request.method == 'POST':
1616
            form = form_class(request.POST, request.FILES, instance=obj)
1617
            if form.is_valid():
1618
                verify = request.GET.get('verify')
1619
                edit = request.GET.get('edit')
1620
                if verify == '1':
1621
                    extra_context['show_form'] = False
1622
                    extra_context['form_data'] = form.cleaned_data
1623
                elif edit == '1':
1624
                    extra_context['show_form'] = True
1625
                else:
1626
                    obj = form.save()
1627
                    msg = _("The %(verbose_name)s was updated successfully.") %\
1628
                                {"verbose_name": model._meta.verbose_name}
1629
                    messages.success(request, msg, fail_silently=True)
1630
                    response = redirect(post_save_redirect, obj)
1631
        else:
1632
            form = form_class(instance=obj)
1633
    except BaseException, e:
1634
        logger.exception(e)
1635
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1636
        rollback = True
1637
    finally:
1638
        if rollback:
1639
            transaction.rollback()
1640
        else:
1641
            transaction.commit()
1642
        if response == None:
1643
            if not template_name:
1644
                template_name = "%s/%s_form.html" %\
1645
                    (model._meta.app_label, model._meta.object_name.lower())
1646
            t = template_loader.get_template(template_name)
1647
            c = RequestContext(request, {
1648
                'form': form,
1649
                template_object_name: obj,
1650
            }, context_processors)
1651
            apply_extra_context(extra_context, c)
1652
            response = HttpResponse(t.render(c))
1653
            populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname))
1654
        return response
1655

    
1656
@require_http_methods(["GET", "POST"])
1657
@signed_terms_required
1658
@login_required
1659
def project_add(request):
1660
    result = callpoint.list_resources()
1661
    if not result.is_success:
1662
        messages.error(
1663
            request,
1664
            'Unable to retrieve system resources: %s' % result.reason
1665
    )
1666
    else:
1667
        resource_catalog = result.data
1668
    extra_context = {'resource_catalog':resource_catalog, 'show_form':True}
1669
    return _create_object(request, template_name='im/projects/projectapplication_form.html',
1670
        extra_context=extra_context, post_save_redirect='/im/project/list/',
1671
        form_class=ProjectApplicationForm)
1672

    
1673

    
1674
@require_http_methods(["GET"])
1675
@signed_terms_required
1676
@login_required
1677
def project_list(request):
1678
    q = ProjectApplication.objects.filter(owner=request.user)
1679
    q |= ProjectApplication.objects.filter(
1680
        project__in=request.user.projectmembership_set.values_list('project', flat=True)
1681
    )
1682
    q = q.select_related()
1683
    sorting = 'name'
1684
    sort_form = ProjectSortForm(request.GET)
1685
    if sort_form.is_valid():
1686
        sorting = sort_form.cleaned_data.get('sorting')
1687
    q = q.order_by(sorting)
1688

    
1689
    return object_list(
1690
        request,
1691
        q,
1692
        paginate_by=PAGINATE_BY_ALL,
1693
        page=request.GET.get('page') or 1,
1694
        template_name='im/projects/project_list.html',
1695
        extra_context={
1696
            'is_search':False,
1697
            'sorting':sorting
1698
        }
1699
    )
1700

    
1701
@require_http_methods(["GET", "POST"])
1702
@signed_terms_required
1703
@login_required
1704
def project_update(request, application_id):
1705
    result = callpoint.list_resources()
1706
    if not result.is_success:
1707
        messages.error(
1708
            request,
1709
            'Unable to retrieve system resources: %s' % result.reason
1710
    )
1711
    else:
1712
        resource_catalog = result.data
1713
    extra_context = {'resource_catalog':resource_catalog, 'show_form':True}
1714
    return _update_object(
1715
        request,
1716
        object_id=application_id,
1717
        template_name='im/projects/projectapplication_form.html',
1718
        extra_context=extra_context, post_save_redirect='/im/project/list/',
1719
        form_class=ProjectApplicationForm)
1720

    
1721

    
1722
@require_http_methods(["GET", "POST"])
1723
@signed_terms_required
1724
@login_required
1725
@transaction.commit_manually
1726
def project_detail(request, application_id):
1727
    result = callpoint.list_resources()
1728
    if not result.is_success:
1729
        messages.error(
1730
            request,
1731
            'Unable to retrieve system resources: %s' % result.reason
1732
    )
1733
    else:
1734
        resource_catalog = result.data
1735
#     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1736
#     resource_catalog.update_from_result(result)
1737

    
1738
    addmembers_form = AddProjectMembersForm()
1739
    if request.method == 'POST':
1740
        addmembers_form = AddProjectMembersForm(request.POST)
1741
        if addmembers_form.is_valid():
1742
            try:
1743
                rollback = False
1744
                map(lambda u: accept_membership(
1745
                        application_id,
1746
                        u,
1747
                        request_user=request.user),
1748
                    addmembers_form.valid_users)
1749
            except (IOError, PermissionDenied), e:
1750
                messages.error(request, e)
1751
            except BaseException, e:
1752
                rollback = True
1753
                messages.error(request, e)
1754
            finally:
1755
                if rollback == True:
1756
                    transaction.rollback()
1757
                else:
1758
                    transaction.commit()
1759
            addmembers_form = AddProjectMembersForm()
1760

    
1761
    # validate sorting
1762
    sorting = 'person__email'
1763
    form = ProjectMembersSortForm(request.GET or request.POST)
1764
    if form.is_valid():
1765
        sorting = form.cleaned_data.get('sorting')
1766

    
1767
    return object_detail(
1768
        request,
1769
        queryset=ProjectApplication.objects.select_related(),
1770
        object_id=application_id,
1771
        template_name='im/projects/project_detail.html',
1772
        extra_context={
1773
            'resource_catalog':resource_catalog,
1774
            'sorting':sorting,
1775
            'addmembers_form':addmembers_form
1776
        }
1777
    )
1778

    
1779
@require_http_methods(["GET", "POST"])
1780
@signed_terms_required
1781
@login_required
1782
def project_search(request):
1783
    queryset = ProjectApplication.objects
1784
    if request.method == 'GET':
1785
        form = ProjectSearchForm()
1786
        queryset = queryset.none()
1787
    else:
1788
        form = ProjectSearchForm(request.POST)
1789
        if form.is_valid():
1790
            q = form.cleaned_data['q'].strip()
1791
            queryset = queryset.filter(~Q(project__last_approval_date__isnull=True))
1792
            queryset = queryset.filter(name__contains=q)
1793
    sorting = 'name'
1794
    # validate sorting
1795
    sort_form = ProjectSortForm(request.GET)
1796
    if sort_form.is_valid():
1797
        sorting = sort_form.cleaned_data.get('sorting')
1798
    queryset = queryset.order_by(sorting)
1799
    return object_list(
1800
        request,
1801
        queryset,
1802
        paginate_by=PAGINATE_BY_ALL,
1803
        page=request.GET.get('page') or 1,
1804
        template_name='im/projects/project_list.html',
1805
        extra_context=dict(
1806
            form=form,
1807
            is_search=True,
1808
            sorting=sorting
1809
        )
1810
    )
1811

    
1812

    
1813
@require_http_methods(["GET"])
1814
@signed_terms_required
1815
@login_required
1816
def project_all(request):
1817
    q = ProjectApplication.objects.filter(
1818
        ~Q(project__last_approval_date__isnull=True))
1819
    q = q.select_related()
1820
    sorting = 'name'
1821
    sort_form = ProjectSortForm(request.GET)
1822
    if sort_form.is_valid():
1823
        sorting = sort_form.cleaned_data.get('sorting')
1824
    q = q.order_by(sorting)
1825

    
1826
    return object_list(
1827
        request,
1828
        q,
1829
        paginate_by=PAGINATE_BY_ALL,
1830
        page=request.GET.get('page') or 1,
1831
        template_name='im/projects/project_list.html',
1832
        extra_context={
1833
            'form':ProjectSearchForm(),
1834
            'is_search':True,
1835
            'sorting':sorting
1836
        }
1837
    )
1838

    
1839
@require_http_methods(["POST"])
1840
@signed_terms_required
1841
@login_required
1842
@transaction.commit_manually
1843
def project_join(request, application_id):
1844
    next = request.POST.get('next')
1845
    if not next:
1846
        return HttpResponseBadRequest(
1847
            _(astakos_messages.MISSING_NEXT_PARAMETER))
1848

    
1849
    rollback = False
1850
    try:
1851
        join_project(application_id, request.user)
1852
    except (IOError, PermissionDenied), e:
1853
        messages.error(request, e)
1854
    except BaseException, e:
1855
        logger.exception(e)
1856
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1857
        rollback = True
1858
    else:
1859
        return project_detail(request, id)
1860
    finally:
1861
        if rollback:
1862
            transaction.rollback()
1863
        else:
1864
            transaction.commit()
1865

    
1866
    next = restrict_next(
1867
        request.POST.get('next'),
1868
        domain=COOKIE_DOMAIN)
1869
    return redirect(next)
1870

    
1871
@require_http_methods(["POST"])
1872
@signed_terms_required
1873
@login_required
1874
@transaction.commit_manually
1875
def project_leave(request, application_id):
1876
    next = request.POST.get('next')
1877
    if not next:
1878
        return HttpResponseBadRequest(
1879
            _(astakos_messages.MISSING_NEXT_PARAMETER))
1880

    
1881
    rollback = False
1882
    try:
1883
        leave_project(application_id, request.user)
1884
    except (IOError, PermissionDenied), e:
1885
        messages.error(request, e)
1886
    except BaseException, e:
1887
        logger.exception(e)
1888
        messages.error(_(astakos_messages.GENERIC_ERRO))
1889
        rollback = True
1890
    finally:
1891
        if rollback:
1892
            transaction.rollback()
1893
        else:
1894
            transaction.commit()
1895

    
1896
    next = restrict_next(
1897
        request.POST.get('next'),
1898
        domain=COOKIE_DOMAIN)
1899
    return redirect(next)
1900

    
1901
@require_http_methods(["GET"])
1902
@signed_terms_required
1903
@login_required
1904
@transaction.commit_manually
1905
def project_approve_member(request, application_id, user_id):
1906
    rollback = False
1907
    try:
1908
        m = accept_membership(application_id, user_id, request.user)
1909
    except (IOError, PermissionDenied), e:
1910
        messages.error(request, e)
1911
    except BaseException, e:
1912
        logger.exception(e)
1913
        messages.error(_(astakos_messages.GENERIC_ERRO))
1914
        rollback = True
1915
    else:
1916
        realname = m.person.realname
1917
        msg = _(astakos_messages.USER_JOINED_PROJECT) % locals()
1918
        messages.success(request, msg)
1919
    finally:
1920
        if rollback:
1921
            transaction.rollback()
1922
        else:
1923
            transaction.commit()
1924
    return project_detail(request, application_id)
1925

    
1926
@require_http_methods(["GET"])
1927
@signed_terms_required
1928
@login_required
1929
@transaction.commit_manually
1930
def project_remove_member(request, application_id, user_id):
1931
    rollback = False
1932
    try:
1933
        m = remove_membership(application_id, user_id, request.user)
1934
    except (IOError, PermissionDenied), e:
1935
        messages.error(request, e)
1936
    except BaseException, e:
1937
        logger.exception(e)
1938
        messages.error(_(astakos_messages.GENERIC_ERRO))
1939
        rollback = True
1940
    else:
1941
        realname = m.person.realname
1942
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1943
        messages.success(request, msg)
1944
    finally:
1945
        if rollback:
1946
            transaction.rollback()
1947
        else:
1948
            transaction.commit()
1949
    return project_detail(request, application_id)
1950

    
1951
@require_http_methods(["GET"])
1952
@signed_terms_required
1953
@login_required
1954
@transaction.commit_manually
1955
def project_reject_member(request, application_id, user_id):
1956
    rollback = False
1957
    try:
1958
        m = reject_membership(application_id, user_id, request.user)
1959
    except (IOError, PermissionDenied), e:
1960
        messages.error(request, e)
1961
    except BaseException, e:
1962
        logger.exception(e)
1963
        messages.error(_(astakos_messages.GENERIC_ERRO))
1964
        rollback = True
1965
    else:
1966
        realname = m.person.realname
1967
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1968
        messages.success(request, msg)
1969
    finally:
1970
        if rollback:
1971
            transaction.rollback()
1972
        else:
1973
            transaction.commit()
1974
    return project_detail(request, application_id)
1975

    
1976