Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views / im.py @ 31240d2c

History | View | Annotate | Download (35.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 inflect
36

    
37
engine = inflect.engine()
38

    
39
from urllib import quote
40

    
41
from django.shortcuts import get_object_or_404
42
from django.contrib import messages
43
from django.contrib.auth.models import User
44
from django.core.urlresolvers import reverse
45
from django.db import transaction
46
from django.http import HttpResponse, HttpResponseRedirect, Http404
47
from django.shortcuts import redirect
48
from django.utils.translation import ugettext as _
49
from django.core.exceptions import PermissionDenied
50
from django.views.decorators.http import require_http_methods
51
from django.utils import simplejson as json
52
from django.template import RequestContext
53

    
54
from synnefo_branding import utils as branding
55
from synnefo_branding import settings as branding_settings
56

    
57
from synnefo.lib import join_urls
58

    
59
import astakos.im.messages as astakos_messages
60

    
61
from astakos.im import activation_backends
62
from astakos.im.models import AstakosUser, ApprovalTerms, EmailChange, \
63
    AstakosUserAuthProvider, PendingThirdPartyUser, Component
64
from astakos.im.util import get_context, prepare_response, get_query, \
65
    restrict_next
66
from astakos.im.forms import LoginForm, InvitationForm, FeedbackForm, \
67
    SignApprovalTermsForm, EmailChangeForm
68
from astakos.im.forms import ExtendedProfileForm as ProfileForm
69
from synnefo.lib.services import get_public_endpoint
70
from astakos.im.functions import send_feedback, logout as auth_logout, \
71
    invite as invite_func
72
from astakos.im import settings
73
from astakos.im import presentation
74
from astakos.im import auth_providers as auth
75
from astakos.im import quotas
76
from astakos.im.views.util import render_response, _resources_catalog
77
from astakos.im.views.decorators import cookie_fix, signed_terms_required,\
78
    required_auth_methods_assigned, valid_astakos_user_required, login_required
79

    
80
logger = logging.getLogger(__name__)
81

    
82

    
83
@require_http_methods(["GET", "POST"])
84
@cookie_fix
85
@signed_terms_required
86
def login(request, template_name='im/login.html', extra_context=None):
87
    """
88
    Renders login page.
89

90
    **Arguments**
91

92
    ``template_name``
93
        A custom login template to use. This is optional; if not specified,
94
        this will default to ``im/login.html``.
95

96
    ``extra_context``
97
        An dictionary of variables to add to the template context.
98
    """
99

    
100
    extra_context = extra_context or {}
101

    
102
    third_party_token = request.GET.get('key', False)
103
    if third_party_token:
104
        messages.info(request, astakos_messages.AUTH_PROVIDER_LOGIN_TO_ADD)
105

    
106
    if request.user.is_authenticated():
107
        return HttpResponseRedirect(reverse('landing'))
108

    
109
    return render_response(
110
        template_name,
111
        login_form=LoginForm(request=request),
112
        context_instance=get_context(request, extra_context)
113
    )
114

    
115

    
116
@require_http_methods(["GET", "POST"])
117
@cookie_fix
118
@signed_terms_required
119
def index(request, authenticated_redirect='landing',
120
          anonymous_redirect='login', extra_context=None):
121
    """
122
    If user is authenticated redirect to ``authenticated_redirect`` url.
123
    Otherwise redirects to ``anonymous_redirect`` url.
124

125
    """
126
    if request.user.is_authenticated():
127
        return HttpResponseRedirect(reverse(authenticated_redirect))
128
    return HttpResponseRedirect(reverse(anonymous_redirect))
129

    
130

    
131
@require_http_methods(["POST"])
132
@cookie_fix
133
@valid_astakos_user_required
134
def update_token(request):
135
    """
136
    Update api token view.
137
    """
138
    user = request.user
139
    user.renew_token()
140
    user.save()
141
    messages.success(request, astakos_messages.TOKEN_UPDATED)
142
    return HttpResponseRedirect(reverse('api_access'))
143

    
144

    
145
@require_http_methods(["GET", "POST"])
146
@cookie_fix
147
@valid_astakos_user_required
148
@transaction.commit_manually
149
def invite(request, template_name='im/invitations.html', extra_context=None):
150
    """
151
    Allows a user to invite somebody else.
152

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

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

160
    If the user isn't logged in, redirects to settings.LOGIN_URL.
161

162
    **Arguments**
163

164
    ``template_name``
165
        A custom template to use. This is optional; if not specified,
166
        this will default to ``im/invitations.html``.
167

168
    ``extra_context``
169
        An dictionary of variables to add to the template context.
170

171
    **Template:**
172

173
    im/invitations.html or ``template_name`` keyword argument.
174

175
    **Settings:**
176

177
    The view expectes the following settings are defined:
178

179
    * LOGIN_URL: login uri
180
    """
181
    extra_context = extra_context or {}
182
    status = None
183
    message = None
184
    form = InvitationForm()
185

    
186
    inviter = request.user
187
    if request.method == 'POST':
188
        form = InvitationForm(request.POST)
189
        if inviter.invitations > 0:
190
            if form.is_valid():
191
                try:
192
                    email = form.cleaned_data.get('username')
193
                    realname = form.cleaned_data.get('realname')
194
                    invite_func(inviter, email, realname)
195
                    message = _(astakos_messages.INVITATION_SENT) % locals()
196
                    messages.success(request, message)
197
                except Exception, e:
198
                    transaction.rollback()
199
                    raise
200
                else:
201
                    transaction.commit()
202
        else:
203
            message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
204
            messages.error(request, message)
205

    
206
    sent = [{'email': inv.username,
207
             'realname': inv.realname,
208
             'is_consumed': inv.is_consumed}
209
            for inv in request.user.invitations_sent.all()]
210
    kwargs = {'inviter': inviter,
211
              'sent': sent}
212
    context = get_context(request, extra_context, **kwargs)
213
    return render_response(template_name,
214
                           invitation_form=form,
215
                           context_instance=context)
216

    
217

    
218

    
219
@require_http_methods(["GET", "POST"])
220
@required_auth_methods_assigned()
221
@login_required
222
@cookie_fix
223
@signed_terms_required
224
def api_access_config(request, template_name='im/api_access_config.html',
225
                      content_type='text/plain', extra_context=None,
226
                      filename='.kamakirc'):
227

    
228
    if settings.KAMAKI_CONFIG_CLOUD_NAME:
229
        cloud_name = settings.KAMAKI_CONFIG_CLOUD_NAME
230
    else:
231
        cloud_name = branding_settings.SERVICE_NAME.replace(' ', '_').lower()
232

    
233
    url = get_public_endpoint(settings.astakos_services, 'identity')
234

    
235
    context = {
236
        'user': request.user,
237
        'services': Component.catalog(),
238
        'token_url': url,
239
        'cloud_name': cloud_name
240
    }
241

    
242
    extra_context = extra_context or {}
243
    context.update(extra_context)
244
    content = branding.render_to_string(template_name, context,
245
                                        RequestContext(request))
246
    response = HttpResponse()
247
    response.status_code = 200
248
    response.content_type = content_type
249
    response['Content-Disposition'] = 'attachment; filename="%s"' % filename
250
    response.content = content
251
    return response
252

    
253

    
254
@required_auth_methods_assigned()
255
@login_required
256
@cookie_fix
257
@signed_terms_required
258
def api_access(request, template_name='im/api_access.html',
259
               extra_context=None):
260
    """
261
    API access view.
262
    """
263
    context = {}
264

    
265
    url = get_public_endpoint(settings.astakos_services, 'identity')
266
    context['services'] = Component.catalog()
267
    context['token_url'] = url
268
    context['user'] = request.user
269
    context['client_url'] = settings.API_CLIENT_URL
270

    
271
    if extra_context:
272
        context.update(extra_context)
273
    context_instance = get_context(request, context)
274
    return render_response(template_name,
275
                           context_instance=context_instance)
276

    
277

    
278
@require_http_methods(["GET", "POST"])
279
@required_auth_methods_assigned(allow_access=True)
280
@login_required
281
@cookie_fix
282
@signed_terms_required
283
def edit_profile(request, template_name='im/profile.html', extra_context=None):
284
    """
285
    Allows a user to edit his/her profile.
286

287
    In case of GET request renders a form for displaying the user information.
288
    In case of POST updates the user informantion and redirects to ``next``
289
    url parameter if exists.
290

291
    If the user isn't logged in, redirects to settings.LOGIN_URL.
292

293
    **Arguments**
294

295
    ``template_name``
296
        A custom template to use. This is optional; if not specified,
297
        this will default to ``im/profile.html``.
298

299
    ``extra_context``
300
        An dictionary of variables to add to the template context.
301

302
    **Template:**
303

304
    im/profile.html or ``template_name`` keyword argument.
305

306
    **Settings:**
307

308
    The view expectes the following settings are defined:
309

310
    * LOGIN_URL: login uri
311
    """
312
    extra_context = extra_context or {}
313
    form = ProfileForm(
314
        instance=request.user,
315
        session_key=request.session.session_key
316
    )
317
    extra_context['next'] = request.GET.get('next')
318
    if request.method == 'POST':
319
        form = ProfileForm(
320
            request.POST,
321
            instance=request.user,
322
            session_key=request.session.session_key
323
        )
324
        if form.is_valid():
325
            try:
326
                prev_token = request.user.auth_token
327
                user = form.save(request=request)
328
                next = restrict_next(
329
                    request.POST.get('next'),
330
                    domain=settings.COOKIE_DOMAIN
331
                )
332
                msg = _(astakos_messages.PROFILE_UPDATED)
333
                messages.success(request, msg)
334

    
335
                if form.email_changed:
336
                    msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
337
                    messages.success(request, msg)
338
                if form.password_changed:
339
                    msg = _(astakos_messages.PASSWORD_CHANGED)
340
                    messages.success(request, msg)
341

    
342
                if next:
343
                    return redirect(next)
344
                else:
345
                    return redirect(reverse('edit_profile'))
346
            except ValueError, ve:
347
                messages.success(request, ve)
348
    elif request.method == "GET":
349
        request.user.is_verified = True
350
        request.user.save()
351

    
352
    # existing providers
353
    user_providers = request.user.get_enabled_auth_providers()
354
    user_disabled_providers = request.user.get_disabled_auth_providers()
355

    
356
    # providers that user can add
357
    user_available_providers = request.user.get_available_auth_providers()
358

    
359
    extra_context['services'] = Component.catalog().values()
360
    return render_response(template_name,
361
                           profile_form=form,
362
                           user_providers=user_providers,
363
                           user_disabled_providers=user_disabled_providers,
364
                           user_available_providers=user_available_providers,
365
                           context_instance=get_context(request,
366
                                                          extra_context))
367

    
368

    
369
@transaction.commit_manually
370
@require_http_methods(["GET", "POST"])
371
@cookie_fix
372
def signup(request, template_name='im/signup.html', on_success='index',
373
           extra_context=None, activation_backend=None):
374
    """
375
    Allows a user to create a local account.
376

377
    In case of GET request renders a form for entering the user information.
378
    In case of POST handles the signup.
379

380
    The user activation will be delegated to the backend specified by the
381
    ``activation_backend`` keyword argument if present, otherwise to the
382
    ``astakos.im.activation_backends.InvitationBackend`` if
383
    settings.ASTAKOS_INVITATIONS_ENABLED is True or
384
    ``astakos.im.activation_backends.SimpleBackend`` if not (see
385
    activation_backends);
386

387
    Upon successful user creation, if ``next`` url parameter is present the
388
    user is redirected there otherwise renders the same page with a success
389
    message.
390

391
    On unsuccessful creation, renders ``template_name`` with an error message.
392

393
    **Arguments**
394

395
    ``template_name``
396
        A custom template to render. This is optional;
397
        if not specified, this will default to ``im/signup.html``.
398

399
    ``extra_context``
400
        An dictionary of variables to add to the template context.
401

402
    ``on_success``
403
        Resolvable view name to redirect on registration success.
404

405
    **Template:**
406

407
    im/signup.html or ``template_name`` keyword argument.
408
    """
409
    extra_context = extra_context or {}
410
    if request.user.is_authenticated():
411
        logger.info("%s already signed in, redirect to index",
412
                    request.user.log_display)
413
        return HttpResponseRedirect(reverse('index'))
414

    
415
    provider = get_query(request).get('provider', 'local')
416
    if not auth.get_provider(provider).get_create_policy:
417
        logger.error("%s provider not available for signup", provider)
418
        raise PermissionDenied
419

    
420
    instance = None
421

    
422
    # user registered using third party provider
423
    third_party_token = request.REQUEST.get('third_party_token', None)
424
    unverified = None
425
    if third_party_token:
426
        # retreive third party entry. This was created right after the initial
427
        # third party provider handshake.
428
        pending = get_object_or_404(PendingThirdPartyUser,
429
                                    token=third_party_token)
430

    
431
        provider = pending.provider
432

    
433
        # clone third party instance into the corresponding AstakosUser
434
        instance = pending.get_user_instance()
435
        get_unverified = AstakosUserAuthProvider.objects.unverified
436

    
437
        # check existing unverified entries
438
        unverified = get_unverified(pending.provider,
439
                                    identifier=pending.third_party_identifier)
440

    
441
        if unverified and request.method == 'GET':
442
            messages.warning(request, unverified.get_pending_registration_msg)
443
            if unverified.user.moderated:
444
                messages.warning(request,
445
                                 unverified.get_pending_resend_activation_msg)
446
            else:
447
                messages.warning(request,
448
                                 unverified.get_pending_moderation_msg)
449

    
450
    # prepare activation backend based on current request
451
    if not activation_backend:
452
        activation_backend = activation_backends.get_backend()
453

    
454
    form_kwargs = {'instance': instance}
455
    if third_party_token:
456
        form_kwargs['third_party_token'] = third_party_token
457

    
458
    form = activation_backend.get_signup_form(
459
        provider, None, **form_kwargs)
460

    
461
    if request.method == 'POST':
462
        form = activation_backend.get_signup_form(
463
            provider,
464
            request.POST,
465
            **form_kwargs)
466

    
467
        if form.is_valid():
468
            commited = False
469
            try:
470
                user = form.save(commit=False)
471

    
472
                # delete previously unverified accounts
473
                if AstakosUser.objects.user_exists(user.email):
474
                    AstakosUser.objects.get_by_identifier(user.email).delete()
475

    
476
                # store_user so that user auth providers get initialized
477
                form.store_user(user, request)
478
                result = activation_backend.handle_registration(user)
479
                if result.status == \
480
                        activation_backend.Result.PENDING_MODERATION:
481
                    # user should be warned that his account is not active yet
482
                    status = messages.WARNING
483
                else:
484
                    status = messages.SUCCESS
485
                message = result.message
486
                activation_backend.send_result_notifications(result, user)
487

    
488
                # commit user entry
489
                transaction.commit()
490
                # commited flag
491
                # in case an exception get raised from this point
492
                commited = True
493

    
494
                if user and user.is_active:
495
                    # activation backend directly activated the user
496
                    # log him in
497
                    next = request.POST.get('next', '')
498
                    response = prepare_response(request, user, next=next)
499
                    return response
500

    
501
                messages.add_message(request, status, message)
502
                return HttpResponseRedirect(reverse(on_success))
503
            except Exception, e:
504
                if not commited:
505
                    transaction.rollback()
506
                raise
507

    
508
    return render_response(template_name,
509
                           signup_form=form,
510
                           third_party_token=third_party_token,
511
                           provider=provider,
512
                           context_instance=get_context(request, extra_context))
513

    
514

    
515
@require_http_methods(["GET", "POST"])
516
@required_auth_methods_assigned(allow_access=True)
517
@login_required
518
@cookie_fix
519
@signed_terms_required
520
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
521
    """
522
    Allows a user to send feedback.
523

524
    In case of GET request renders a form for providing the feedback information.
525
    In case of POST sends an email to support team.
526

527
    If the user isn't logged in, redirects to settings.LOGIN_URL.
528

529
    **Arguments**
530

531
    ``template_name``
532
        A custom template to use. This is optional; if not specified,
533
        this will default to ``im/feedback.html``.
534

535
    ``extra_context``
536
        An dictionary of variables to add to the template context.
537

538
    **Template:**
539

540
    im/signup.html or ``template_name`` keyword argument.
541

542
    **Settings:**
543

544
    * LOGIN_URL: login uri
545
    """
546
    extra_context = extra_context or {}
547
    if request.method == 'GET':
548
        form = FeedbackForm()
549
    if request.method == 'POST':
550
        if not request.user:
551
            return HttpResponse('Unauthorized', status=401)
552

    
553
        form = FeedbackForm(request.POST)
554
        if form.is_valid():
555
            msg = form.cleaned_data['feedback_msg']
556
            data = form.cleaned_data['feedback_data']
557
            send_feedback(msg, data, request.user, email_template_name)
558
            message = _(astakos_messages.FEEDBACK_SENT)
559
            messages.success(request, message)
560
            return HttpResponseRedirect(reverse('feedback'))
561

    
562
    return render_response(template_name,
563
                           feedback_form=form,
564
                           context_instance=get_context(request,
565
                                                        extra_context))
566

    
567

    
568
@require_http_methods(["GET"])
569
@cookie_fix
570
def logout(request, template='registration/logged_out.html',
571
           extra_context=None):
572
    """
573
    Wraps `django.contrib.auth.logout`.
574
    """
575
    extra_context = extra_context or {}
576
    response = HttpResponse()
577
    if request.user.is_authenticated():
578
        email = request.user.email
579
        auth_logout(request)
580
    else:
581
        response['Location'] = reverse('index')
582
        response.status_code = 301
583
        return response
584

    
585
    next = restrict_next(
586
        request.GET.get('next'),
587
        domain=settings.COOKIE_DOMAIN
588
    )
589

    
590
    if next:
591
        response['Location'] = next
592
        response.status_code = 302
593
    elif settings.LOGOUT_NEXT:
594
        response['Location'] = settings.LOGOUT_NEXT
595
        response.status_code = 301
596
    else:
597
        last_provider = request.COOKIES.get('astakos_last_login_method', 'local')
598
        provider = auth.get_provider(last_provider)
599
        message = provider.get_logout_success_msg
600
        extra = provider.get_logout_success_extra_msg
601
        if extra:
602
            message += "<br />"  + extra
603
        messages.success(request, message)
604
        response['Location'] = reverse('index')
605
        response.status_code = 301
606
    return response
607

    
608

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

618
    The view uses commit_manually decorator in order to ensure the user state
619
    will be updated only if the email will be send successfully.
620
    """
621
    token = request.GET.get('auth')
622
    next = request.GET.get('next')
623

    
624
    if request.user.is_authenticated():
625
        message = _(astakos_messages.LOGGED_IN_WARNING)
626
        messages.error(request, message)
627
        return HttpResponseRedirect(reverse('index'))
628

    
629
    try:
630
        user = AstakosUser.objects.get(verification_code=token)
631
    except AstakosUser.DoesNotExist:
632
        raise Http404
633

    
634
    if user.email_verified:
635
        message = _(astakos_messages.ACCOUNT_ALREADY_VERIFIED)
636
        messages.error(request, message)
637
        return HttpResponseRedirect(reverse('index'))
638

    
639
    try:
640
        backend = activation_backends.get_backend()
641
        result = backend.handle_verification(user, token)
642
        backend.send_result_notifications(result, user)
643
        next = settings.ACTIVATION_REDIRECT_URL or next
644
        response = HttpResponseRedirect(reverse('index'))
645
        if user.is_active:
646
            response = prepare_response(request, user, next, renew=True)
647
            messages.success(request, _(result.message))
648
        else:
649
            messages.warning(request, _(result.message))
650
    except Exception:
651
        transaction.rollback()
652
        raise
653
    else:
654
        transaction.commit()
655
        return response
656

    
657

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

    
676
    if not term:
677
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
678
        return HttpResponseRedirect(reverse('index'))
679
    try:
680
        f = open(term.location, 'r')
681
    except IOError:
682
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
683
        return render_response(
684
            template_name, context_instance=get_context(request,
685
                                                        extra_context))
686

    
687
    terms = f.read()
688

    
689
    if request.method == 'POST':
690
        next = restrict_next(
691
            request.POST.get('next'),
692
            domain=settings.COOKIE_DOMAIN
693
        )
694
        if not next:
695
            next = reverse('index')
696
        form = SignApprovalTermsForm(request.POST, instance=request.user)
697
        if not form.is_valid():
698
            return render_response(template_name,
699
                                   terms=terms,
700
                                   approval_terms_form=form,
701
                                   context_instance=get_context(request,
702
                                                                extra_context))
703
        user = form.save()
704
        return HttpResponseRedirect(next)
705
    else:
706
        form = None
707
        if request.user.is_authenticated() and not request.user.signed_terms:
708
            form = SignApprovalTermsForm(instance=request.user)
709
        return render_response(template_name,
710
                               terms=terms,
711
                               approval_terms_form=form,
712
                               context_instance=get_context(request,
713
                                                            extra_context))
714

    
715

    
716
@require_http_methods(["GET", "POST"])
717
@cookie_fix
718
@transaction.commit_manually
719
def change_email(request, activation_key=None,
720
                 email_template_name='registration/email_change_email.txt',
721
                 form_template_name='registration/email_change_form.html',
722
                 confirm_template_name='registration/email_change_done.html',
723
                 extra_context=None):
724
    extra_context = extra_context or {}
725

    
726
    if not settings.EMAILCHANGE_ENABLED:
727
        raise PermissionDenied
728

    
729
    if activation_key:
730
        try:
731
            try:
732
                email_change = EmailChange.objects.get(
733
                    activation_key=activation_key)
734
            except EmailChange.DoesNotExist:
735
                transaction.rollback()
736
                logger.error("[change-email] Invalid or used activation "
737
                             "code, %s", activation_key)
738
                raise Http404
739

    
740
            if (request.user.is_authenticated() and \
741
                request.user == email_change.user) or not \
742
                    request.user.is_authenticated():
743
                user = EmailChange.objects.change_email(activation_key)
744
                msg = _(astakos_messages.EMAIL_CHANGED)
745
                messages.success(request, msg)
746
                transaction.commit()
747
                return HttpResponseRedirect(reverse('edit_profile'))
748
            else:
749
                logger.error("[change-email] Access from invalid user, %s %s",
750
                             email_change.user, request.user.log_display)
751
                transaction.rollback()
752
                raise PermissionDenied
753
        except ValueError, e:
754
            messages.error(request, e)
755
            transaction.rollback()
756
            return HttpResponseRedirect(reverse('index'))
757

    
758
        return render_response(confirm_template_name,
759
                               modified_user=user if 'user' in locals()
760
                               else None, context_instance=get_context(request,
761
                               extra_context))
762

    
763
    if not request.user.is_authenticated():
764
        path = quote(request.get_full_path())
765
        url = request.build_absolute_uri(reverse('index'))
766
        return HttpResponseRedirect(url + '?next=' + path)
767

    
768
    # clean up expired email changes
769
    if request.user.email_change_is_pending():
770
        change = request.user.emailchanges.get()
771
        if change.activation_key_expired():
772
            change.delete()
773
            transaction.commit()
774
            return HttpResponseRedirect(reverse('email_change'))
775

    
776
    form = EmailChangeForm(request.POST or None)
777
    if request.method == 'POST' and form.is_valid():
778
        try:
779
            ec = form.save(request, email_template_name, request)
780
        except Exception, e:
781
            transaction.rollback()
782
            raise
783
        else:
784
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
785
            messages.success(request, msg)
786
            transaction.commit()
787
            return HttpResponseRedirect(reverse('edit_profile'))
788

    
789
    if request.user.email_change_is_pending():
790
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
791

    
792
    return render_response(
793
        form_template_name,
794
        form=form,
795
        context_instance=get_context(request, extra_context)
796
    )
797

    
798

    
799
@cookie_fix
800
def send_activation(request, user_id, template_name='im/login.html',
801
                    extra_context=None):
802

    
803
    if request.user.is_authenticated():
804
        return HttpResponseRedirect(reverse('index'))
805

    
806
    extra_context = extra_context or {}
807
    try:
808
        u = AstakosUser.objects.get(id=user_id)
809
    except AstakosUser.DoesNotExist:
810
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
811
    else:
812
        if u.email_verified:
813
            logger.warning("[resend activation] Account already verified: %s",
814
                           u.log_display)
815

    
816
            messages.error(request,
817
                           _(astakos_messages.ACCOUNT_ALREADY_VERIFIED))
818
        else:
819
            activation_backend = activation_backends.get_backend()
820
            activation_backend.send_user_verification_email(u)
821
            messages.success(request, astakos_messages.ACTIVATION_SENT)
822

    
823
    return HttpResponseRedirect(reverse('index'))
824

    
825

    
826
@require_http_methods(["GET"])
827
@cookie_fix
828
@valid_astakos_user_required
829
def resource_usage(request):
830

    
831
    resources_meta = presentation.RESOURCES
832

    
833
    current_usage = quotas.get_user_quotas(request.user)
834
    current_usage = json.dumps(current_usage['system'])
835
    resource_catalog, resource_groups = _resources_catalog(for_usage=True)
836
    if resource_catalog is False:
837
        # on fail resource_groups contains the result object
838
        result = resource_groups
839
        messages.error(request, 'Unable to retrieve system resources: %s' %
840
                       result.reason)
841

    
842
    resource_catalog = json.dumps(resource_catalog)
843
    resource_groups = json.dumps(resource_groups)
844
    resources_order = json.dumps(resources_meta.get('resources_order'))
845

    
846
    return render_response('im/resource_usage.html',
847
                           context_instance=get_context(request),
848
                           resource_catalog=resource_catalog,
849
                           resource_groups=resource_groups,
850
                           resources_order=resources_order,
851
                           current_usage=current_usage,
852
                           token_cookie_name=settings.COOKIE_NAME,
853
                           usage_update_interval=
854
                           settings.USAGE_UPDATE_INTERVAL)
855

    
856

    
857
# TODO: action only on POST and user should confirm the removal
858
@require_http_methods(["POST"])
859
@cookie_fix
860
@valid_astakos_user_required
861
def remove_auth_provider(request, pk):
862
    try:
863
        provider = request.user.auth_providers.get(pk=int(pk)).settings
864
    except AstakosUserAuthProvider.DoesNotExist:
865
        raise Http404
866

    
867
    if provider.get_remove_policy:
868
        messages.success(request, provider.get_removed_msg)
869
        provider.remove_from_user()
870
        return HttpResponseRedirect(reverse('edit_profile'))
871
    else:
872
        raise PermissionDenied
873

    
874

    
875
@require_http_methods(["GET"])
876
@required_auth_methods_assigned(allow_access=True)
877
@login_required
878
@cookie_fix
879
@signed_terms_required
880
def landing(request):
881
    context = {'services': Component.catalog(orderfor='dashboard')}
882
    return render_response(
883
        'im/landing.html',
884
        context_instance=get_context(request), **context)
885

    
886

    
887
@cookie_fix
888
def get_menu(request, with_extra_links=False, with_signout=True):
889
    user = request.user
890
    index_url = reverse('index')
891

    
892
    if isinstance(user, User) and user.is_authenticated():
893
        l = []
894
        append = l.append
895
        item = MenuItem
896
        item.current_path = request.build_absolute_uri(request.path)
897
        append(item(url=request.build_absolute_uri(reverse('index')),
898
                    name=user.email))
899
        if with_extra_links:
900
            append(item(url=request.build_absolute_uri(reverse('landing')),
901
                        name="Overview"))
902
        if with_signout:
903
            append(item(url=request.build_absolute_uri(reverse('landing')),
904
                        name="Dashboard"))
905
        if with_extra_links:
906
            append(item(url=request.build_absolute_uri(reverse('edit_profile')),
907
                        name="Profile"))
908

    
909
        if with_extra_links:
910
            if settings.INVITATIONS_ENABLED:
911
                append(item(url=request.build_absolute_uri(reverse('invite')),
912
                            name="Invitations"))
913

    
914
            append(item(url=request.build_absolute_uri(reverse('api_access')),
915
                        name="API access"))
916

    
917
            append(item(url=request.build_absolute_uri(reverse('resource_usage')),
918
                        name="Usage"))
919

    
920
            if settings.PROJECTS_VISIBLE:
921
                append(item(url=request.build_absolute_uri(reverse('project_list')),
922
                            name="Projects"))
923

    
924
            append(item(url=request.build_absolute_uri(reverse('feedback')),
925
                        name="Contact"))
926
        if with_signout:
927
            append(item(url=request.build_absolute_uri(reverse('logout')),
928
                        name="Sign out"))
929
    else:
930
        l = [{'url': request.build_absolute_uri(index_url),
931
              'name': _("Sign in")}]
932

    
933
    callback = request.GET.get('callback', None)
934
    data = json.dumps(tuple(l))
935
    mimetype = 'application/json'
936

    
937
    if callback:
938
        mimetype = 'application/javascript'
939
        data = '%s(%s)' % (callback, data)
940

    
941
    return HttpResponse(content=data, mimetype=mimetype)
942

    
943

    
944
class MenuItem(dict):
945
    current_path = ''
946

    
947
    def __init__(self, *args, **kwargs):
948
        super(MenuItem, self).__init__(*args, **kwargs)
949
        if kwargs.get('url') or kwargs.get('submenu'):
950
            self.__set_is_active__()
951

    
952
    def __setitem__(self, key, value):
953
        super(MenuItem, self).__setitem__(key, value)
954
        if key in ('url', 'submenu'):
955
            self.__set_is_active__()
956

    
957
    def __set_is_active__(self):
958
        if self.get('is_active'):
959
            return
960
        if self.current_path.startswith(self.get('url')):
961
            self.__setitem__('is_active', True)
962
        else:
963
            submenu = self.get('submenu', ())
964
            current = (i for i in submenu if i.get('url') == self.current_path)
965
            try:
966
                current_node = current.next()
967
                if not current_node.get('is_active'):
968
                    current_node.__setitem__('is_active', True)
969
                self.__setitem__('is_active', True)
970
            except StopIteration:
971
                return
972

    
973
    def __setattribute__(self, name, value):
974
        super(MenuItem, self).__setattribute__(name, value)
975
        if name == 'current_path':
976
            self.__set_is_active__()
977

    
978

    
979
def get_services(request):
980
    callback = request.GET.get('callback', None)
981
    mimetype = 'application/json'
982
    data = json.dumps(Component.catalog().values())
983

    
984
    if callback:
985
        # Consume session messages. When get_services is loaded from an astakos
986
        # page, messages should have already been consumed in the html
987
        # response. When get_services is loaded from another domain/service we
988
        # consume them here so that no stale messages to appear if user visits
989
        # an astakos view later on.
990
        # TODO: messages could be served to other services/sites in the dict
991
        # response of get_services and/or get_menu. Services could handle those
992
        # messages respectively.
993
        messages_list = list(messages.get_messages(request))
994
        mimetype = 'application/javascript'
995
        data = '%s(%s)' % (callback, data)
996

    
997
    return HttpResponse(content=data, mimetype=mimetype)