Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (34.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_on_success
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 number of the user invitations is going to be updated only if the email
158
    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
                email = form.cleaned_data.get('username')
192
                realname = form.cleaned_data.get('realname')
193
                invite_func(inviter, email, realname)
194
                message = _(astakos_messages.INVITATION_SENT) % locals()
195
                messages.success(request, message)
196
        else:
197
            message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
198
            messages.error(request, message)
199

    
200
    sent = [{'email': inv.username,
201
             'realname': inv.realname,
202
             'is_consumed': inv.is_consumed}
203
            for inv in request.user.invitations_sent.all()]
204
    kwargs = {'inviter': inviter,
205
              'sent': sent}
206
    context = get_context(request, extra_context, **kwargs)
207
    return render_response(template_name,
208
                           invitation_form=form,
209
                           context_instance=context)
210

    
211

    
212

    
213
@require_http_methods(["GET", "POST"])
214
@required_auth_methods_assigned()
215
@login_required
216
@cookie_fix
217
@signed_terms_required
218
def api_access_config(request, template_name='im/api_access_config.html',
219
                      content_type='text/plain', extra_context=None,
220
                      filename='.kamakirc'):
221

    
222
    if settings.KAMAKI_CONFIG_CLOUD_NAME:
223
        cloud_name = settings.KAMAKI_CONFIG_CLOUD_NAME
224
    else:
225
        cloud_name = branding_settings.SERVICE_NAME.replace(' ', '_').lower()
226

    
227
    url = get_public_endpoint(settings.astakos_services, 'identity')
228

    
229
    context = {
230
        'user': request.user,
231
        'services': Component.catalog(),
232
        'token_url': url,
233
        'cloud_name': cloud_name
234
    }
235

    
236
    extra_context = extra_context or {}
237
    context.update(extra_context)
238
    content = branding.render_to_string(template_name, context,
239
                                        RequestContext(request))
240
    response = HttpResponse(content_type=content_type)
241
    response.status_code = 200
242
    response['Content-Disposition'] = 'attachment; filename="%s"' % filename
243
    response.content = content
244
    return response
245

    
246

    
247
@required_auth_methods_assigned()
248
@login_required
249
@cookie_fix
250
@signed_terms_required
251
def api_access(request, template_name='im/api_access.html',
252
               extra_context=None):
253
    """
254
    API access view.
255
    """
256
    context = {}
257

    
258
    url = get_public_endpoint(settings.astakos_services, 'identity')
259
    context['services'] = Component.catalog()
260
    context['token_url'] = url
261
    context['user'] = request.user
262
    context['client_url'] = settings.API_CLIENT_URL
263

    
264
    if extra_context:
265
        context.update(extra_context)
266
    context_instance = get_context(request, context)
267
    return render_response(template_name,
268
                           context_instance=context_instance)
269

    
270

    
271
@require_http_methods(["GET", "POST"])
272
@required_auth_methods_assigned(allow_access=True)
273
@login_required
274
@cookie_fix
275
@signed_terms_required
276
def edit_profile(request, template_name='im/profile.html', extra_context=None):
277
    """
278
    Allows a user to edit his/her profile.
279

280
    In case of GET request renders a form for displaying the user information.
281
    In case of POST updates the user informantion and redirects to ``next``
282
    url parameter if exists.
283

284
    If the user isn't logged in, redirects to settings.LOGIN_URL.
285

286
    **Arguments**
287

288
    ``template_name``
289
        A custom template to use. This is optional; if not specified,
290
        this will default to ``im/profile.html``.
291

292
    ``extra_context``
293
        An dictionary of variables to add to the template context.
294

295
    **Template:**
296

297
    im/profile.html or ``template_name`` keyword argument.
298

299
    **Settings:**
300

301
    The view expectes the following settings are defined:
302

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

    
328
                if form.email_changed:
329
                    msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
330
                    messages.success(request, msg)
331
                if form.password_changed:
332
                    msg = _(astakos_messages.PASSWORD_CHANGED)
333
                    messages.success(request, msg)
334

    
335
                if next:
336
                    return redirect(next)
337
                else:
338
                    return redirect(reverse('edit_profile'))
339
            except ValueError, ve:
340
                messages.success(request, ve)
341
    elif request.method == "GET":
342
        request.user.is_verified = True
343
        request.user.save()
344

    
345
    # existing providers
346
    user_providers = request.user.get_enabled_auth_providers()
347
    user_disabled_providers = request.user.get_disabled_auth_providers()
348

    
349
    # providers that user can add
350
    user_available_providers = request.user.get_available_auth_providers()
351

    
352
    extra_context['services'] = Component.catalog().values()
353
    return render_response(template_name,
354
                           profile_form=form,
355
                           user_providers=user_providers,
356
                           user_disabled_providers=user_disabled_providers,
357
                           user_available_providers=user_available_providers,
358
                           context_instance=get_context(request,
359
                                                          extra_context))
360

    
361

    
362
@transaction.commit_on_success
363
@require_http_methods(["GET", "POST"])
364
@cookie_fix
365
def signup(request, template_name='im/signup.html', on_success='index',
366
           extra_context=None, activation_backend=None):
367
    """
368
    Allows a user to create a local account.
369

370
    In case of GET request renders a form for entering the user information.
371
    In case of POST handles the signup.
372

373
    The user activation will be delegated to the backend specified by the
374
    ``activation_backend`` keyword argument if present, otherwise to the
375
    ``astakos.im.activation_backends.InvitationBackend`` if
376
    settings.ASTAKOS_INVITATIONS_ENABLED is True or
377
    ``astakos.im.activation_backends.SimpleBackend`` if not (see
378
    activation_backends);
379

380
    Upon successful user creation, if ``next`` url parameter is present the
381
    user is redirected there otherwise renders the same page with a success
382
    message.
383

384
    On unsuccessful creation, renders ``template_name`` with an error message.
385

386
    **Arguments**
387

388
    ``template_name``
389
        A custom template to render. This is optional;
390
        if not specified, this will default to ``im/signup.html``.
391

392
    ``extra_context``
393
        An dictionary of variables to add to the template context.
394

395
    ``on_success``
396
        Resolvable view name to redirect on registration success.
397

398
    **Template:**
399

400
    im/signup.html or ``template_name`` keyword argument.
401
    """
402
    extra_context = extra_context or {}
403
    if request.user.is_authenticated():
404
        logger.info("%s already signed in, redirect to index",
405
                    request.user.log_display)
406
        return HttpResponseRedirect(reverse('index'))
407

    
408
    provider = get_query(request).get('provider', 'local')
409
    if not auth.get_provider(provider).get_create_policy:
410
        logger.error("%s provider not available for signup", provider)
411
        raise PermissionDenied
412

    
413
    instance = None
414

    
415
    # user registered using third party provider
416
    third_party_token = request.REQUEST.get('third_party_token', None)
417
    unverified = None
418
    if third_party_token:
419
        # retreive third party entry. This was created right after the initial
420
        # third party provider handshake.
421
        pending = get_object_or_404(PendingThirdPartyUser,
422
                                    token=third_party_token)
423

    
424
        provider = pending.provider
425

    
426
        # clone third party instance into the corresponding AstakosUser
427
        instance = pending.get_user_instance()
428
        get_unverified = AstakosUserAuthProvider.objects.unverified
429

    
430
        # check existing unverified entries
431
        unverified = get_unverified(pending.provider,
432
                                    identifier=pending.third_party_identifier)
433

    
434
        if unverified and request.method == 'GET':
435
            messages.warning(request, unverified.get_pending_registration_msg)
436
            if unverified.user.moderated:
437
                messages.warning(request,
438
                                 unverified.get_pending_resend_activation_msg)
439
            else:
440
                messages.warning(request,
441
                                 unverified.get_pending_moderation_msg)
442

    
443
    # prepare activation backend based on current request
444
    if not activation_backend:
445
        activation_backend = activation_backends.get_backend()
446

    
447
    form_kwargs = {'instance': instance, 'request': request}
448
    if third_party_token:
449
        form_kwargs['third_party_token'] = third_party_token
450

    
451
    form = activation_backend.get_signup_form(
452
        provider, None, **form_kwargs)
453

    
454
    if request.method == 'POST':
455
        form = activation_backend.get_signup_form(
456
            provider,
457
            request.POST,
458
            **form_kwargs)
459

    
460
        if form.is_valid():
461
            user = form.save(commit=False)
462

    
463
            # delete previously unverified accounts
464
            if AstakosUser.objects.user_exists(user.email):
465
                AstakosUser.objects.get_by_identifier(user.email).delete()
466

    
467
            # store_user so that user auth providers get initialized
468
            form.store_user(user, request)
469
            result = activation_backend.handle_registration(user)
470
            if result.status == \
471
                    activation_backend.Result.PENDING_MODERATION:
472
                # user should be warned that his account is not active yet
473
                status = messages.WARNING
474
            else:
475
                status = messages.SUCCESS
476
            message = result.message
477
            activation_backend.send_result_notifications(result, user)
478

    
479
            # commit user entry
480
            transaction.commit()
481

    
482
            if user and user.is_active:
483
                # activation backend directly activated the user
484
                # log him in
485
                next = request.POST.get('next', '')
486
                response = prepare_response(request, user, next=next)
487
                return response
488

    
489
            messages.add_message(request, status, message)
490
            return HttpResponseRedirect(reverse(on_success))
491

    
492
    return render_response(template_name,
493
                           signup_form=form,
494
                           third_party_token=third_party_token,
495
                           provider=provider,
496
                           context_instance=get_context(request, extra_context))
497

    
498

    
499
@require_http_methods(["GET", "POST"])
500
@required_auth_methods_assigned(allow_access=True)
501
@login_required
502
@cookie_fix
503
@signed_terms_required
504
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
505
    """
506
    Allows a user to send feedback.
507

508
    In case of GET request renders a form for providing the feedback information.
509
    In case of POST sends an email to support team.
510

511
    If the user isn't logged in, redirects to settings.LOGIN_URL.
512

513
    **Arguments**
514

515
    ``template_name``
516
        A custom template to use. This is optional; if not specified,
517
        this will default to ``im/feedback.html``.
518

519
    ``extra_context``
520
        An dictionary of variables to add to the template context.
521

522
    **Template:**
523

524
    im/signup.html or ``template_name`` keyword argument.
525

526
    **Settings:**
527

528
    * LOGIN_URL: login uri
529
    """
530
    extra_context = extra_context or {}
531
    if request.method == 'GET':
532
        form = FeedbackForm()
533
    if request.method == 'POST':
534
        if not request.user:
535
            return HttpResponse('Unauthorized', status=401)
536

    
537
        form = FeedbackForm(request.POST)
538
        if form.is_valid():
539
            msg = form.cleaned_data['feedback_msg']
540
            data = form.cleaned_data['feedback_data']
541
            send_feedback(msg, data, request.user, email_template_name)
542
            message = _(astakos_messages.FEEDBACK_SENT)
543
            messages.success(request, message)
544
            return HttpResponseRedirect(reverse('feedback'))
545

    
546
    return render_response(template_name,
547
                           feedback_form=form,
548
                           context_instance=get_context(request,
549
                                                        extra_context))
550

    
551

    
552
@require_http_methods(["GET"])
553
@cookie_fix
554
def logout(request, template='registration/logged_out.html',
555
           extra_context=None):
556
    """
557
    Wraps `django.contrib.auth.logout`.
558
    """
559
    extra_context = extra_context or {}
560
    response = HttpResponse()
561
    if request.user.is_authenticated():
562
        email = request.user.email
563
        auth_logout(request)
564
    else:
565
        response['Location'] = reverse('index')
566
        response.status_code = 301
567
        return response
568

    
569
    next = restrict_next(
570
        request.GET.get('next'),
571
        domain=settings.COOKIE_DOMAIN
572
    )
573

    
574
    if next:
575
        response['Location'] = next
576
        response.status_code = 302
577
    elif settings.LOGOUT_NEXT:
578
        response['Location'] = settings.LOGOUT_NEXT
579
        response.status_code = 301
580
    else:
581
        last_provider = request.COOKIES.get('astakos_last_login_method', 'local')
582
        provider = auth.get_provider(last_provider)
583
        message = provider.get_logout_success_msg
584
        extra = provider.get_logout_success_extra_msg
585
        if extra:
586
            message += "<br />"  + extra
587
        messages.success(request, message)
588
        response['Location'] = reverse('index')
589
        response.status_code = 301
590
    return response
591

    
592

    
593
@require_http_methods(["GET", "POST"])
594
@cookie_fix
595
@transaction.commit_on_success
596
def activate(request, greeting_email_template_name='im/welcome_email.txt',
597
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
598
    """
599
    Activates the user identified by the ``auth`` request parameter, sends a
600
    welcome email and renews the user token.
601

602
    The user state will be updated only if the email will be send successfully.
603
    """
604
    token = request.GET.get('auth', None)
605
    next = request.GET.get('next', None)
606

    
607
    if not token:
608
        raise PermissionDenied
609

    
610
    if request.user.is_authenticated():
611
        message = _(astakos_messages.LOGGED_IN_WARNING)
612
        messages.error(request, message)
613
        return HttpResponseRedirect(reverse('index'))
614

    
615
    try:
616
        user = AstakosUser.objects.get(verification_code=token)
617
    except AstakosUser.DoesNotExist:
618
        raise Http404
619

    
620
    if user.email_verified:
621
        message = _(astakos_messages.ACCOUNT_ALREADY_VERIFIED)
622
        messages.error(request, message)
623
        return HttpResponseRedirect(reverse('index'))
624

    
625
    backend = activation_backends.get_backend()
626
    result = backend.handle_verification(user, token)
627
    backend.send_result_notifications(result, user)
628
    next = settings.ACTIVATION_REDIRECT_URL or next or reverse('index')
629
    if user.is_active:
630
        response = prepare_response(request, user, next, renew=True)
631
        messages.success(request, _(result.message))
632
    else:
633
        response = HttpResponseRedirect(reverse('index'))
634
        messages.warning(request, _(result.message))
635

    
636
    return response
637

    
638

    
639
@require_http_methods(["GET", "POST"])
640
@cookie_fix
641
def approval_terms(request, term_id=None,
642
                   template_name='im/approval_terms.html', extra_context=None):
643
    extra_context = extra_context or {}
644
    term = None
645
    terms = None
646
    if not term_id:
647
        try:
648
            term = ApprovalTerms.objects.order_by('-id')[0]
649
        except IndexError:
650
            pass
651
    else:
652
        try:
653
            term = ApprovalTerms.objects.get(id=term_id)
654
        except ApprovalTerms.DoesNotExist, e:
655
            pass
656

    
657
    if not term:
658
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
659
        return HttpResponseRedirect(reverse('index'))
660
    try:
661
        f = open(term.location, 'r')
662
    except IOError:
663
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
664
        return render_response(
665
            template_name, context_instance=get_context(request,
666
                                                        extra_context))
667

    
668
    terms = f.read()
669

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

    
696

    
697
@require_http_methods(["GET", "POST"])
698
@cookie_fix
699
@transaction.commit_on_success
700
def change_email(request, activation_key=None,
701
                 email_template_name='registration/email_change_email.txt',
702
                 form_template_name='registration/email_change_form.html',
703
                 confirm_template_name='registration/email_change_done.html',
704
                 extra_context=None):
705
    extra_context = extra_context or {}
706

    
707
    if not settings.EMAILCHANGE_ENABLED:
708
        raise PermissionDenied
709

    
710
    if activation_key:
711
        try:
712
            try:
713
                email_change = EmailChange.objects.get(
714
                    activation_key=activation_key)
715
            except EmailChange.DoesNotExist:
716
                logger.error("[change-email] Invalid or used activation "
717
                             "code, %s", activation_key)
718
                raise Http404
719

    
720
            if (request.user.is_authenticated() and \
721
                request.user == email_change.user) or not \
722
                    request.user.is_authenticated():
723
                user = EmailChange.objects.change_email(activation_key)
724
                msg = _(astakos_messages.EMAIL_CHANGED)
725
                messages.success(request, msg)
726
                transaction.commit()
727
                return HttpResponseRedirect(reverse('edit_profile'))
728
            else:
729
                logger.error("[change-email] Access from invalid user, %s %s",
730
                             email_change.user, request.user.log_display)
731
                raise PermissionDenied
732
        except ValueError, e:
733
            messages.error(request, e)
734
            transaction.rollback()
735
            return HttpResponseRedirect(reverse('index'))
736

    
737
        return render_response(confirm_template_name,
738
                               modified_user=user if 'user' in locals()
739
                               else None, context_instance=get_context(request,
740
                               extra_context))
741

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

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

    
755
    form = EmailChangeForm(request.POST or None)
756
    if request.method == 'POST' and form.is_valid():
757
        ec = form.save(request, email_template_name, request)
758
        msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
759
        messages.success(request, msg)
760
        transaction.commit()
761
        return HttpResponseRedirect(reverse('edit_profile'))
762

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

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

    
772

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

    
777
    if request.user.is_authenticated():
778
        return HttpResponseRedirect(reverse('index'))
779

    
780
    extra_context = extra_context or {}
781
    try:
782
        u = AstakosUser.objects.get(id=user_id)
783
    except AstakosUser.DoesNotExist:
784
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
785
    else:
786
        if u.email_verified:
787
            logger.warning("[resend activation] Account already verified: %s",
788
                           u.log_display)
789

    
790
            messages.error(request,
791
                           _(astakos_messages.ACCOUNT_ALREADY_VERIFIED))
792
        else:
793
            activation_backend = activation_backends.get_backend()
794
            activation_backend.send_user_verification_email(u)
795
            messages.success(request, astakos_messages.ACTIVATION_SENT)
796

    
797
    return HttpResponseRedirect(reverse('index'))
798

    
799

    
800
@require_http_methods(["GET"])
801
@cookie_fix
802
@valid_astakos_user_required
803
def resource_usage(request):
804

    
805
    resources_meta = presentation.RESOURCES
806

    
807
    current_usage = quotas.get_user_quotas(request.user)
808
    current_usage = json.dumps(current_usage['system'])
809
    resource_catalog, resource_groups = _resources_catalog(for_usage=True)
810
    if resource_catalog is False:
811
        # on fail resource_groups contains the result object
812
        result = resource_groups
813
        messages.error(request, 'Unable to retrieve system resources: %s' %
814
                       result.reason)
815

    
816
    resource_catalog = json.dumps(resource_catalog)
817
    resource_groups = json.dumps(resource_groups)
818
    resources_order = json.dumps(resources_meta.get('resources_order'))
819

    
820
    return render_response('im/resource_usage.html',
821
                           context_instance=get_context(request),
822
                           resource_catalog=resource_catalog,
823
                           resource_groups=resource_groups,
824
                           resources_order=resources_order,
825
                           current_usage=current_usage,
826
                           token_cookie_name=settings.COOKIE_NAME,
827
                           usage_update_interval=
828
                           settings.USAGE_UPDATE_INTERVAL)
829

    
830

    
831
# TODO: action only on POST and user should confirm the removal
832
@require_http_methods(["POST"])
833
@cookie_fix
834
@valid_astakos_user_required
835
def remove_auth_provider(request, pk):
836
    try:
837
        provider = request.user.auth_providers.get(pk=int(pk)).settings
838
    except AstakosUserAuthProvider.DoesNotExist:
839
        raise Http404
840

    
841
    if provider.get_remove_policy:
842
        messages.success(request, provider.get_removed_msg)
843
        provider.remove_from_user()
844
        return HttpResponseRedirect(reverse('edit_profile'))
845
    else:
846
        raise PermissionDenied
847

    
848

    
849
@require_http_methods(["GET"])
850
@required_auth_methods_assigned(allow_access=True)
851
@login_required
852
@cookie_fix
853
@signed_terms_required
854
def landing(request):
855
    context = {'services': Component.catalog(orderfor='dashboard')}
856
    return render_response(
857
        'im/landing.html',
858
        context_instance=get_context(request), **context)
859

    
860

    
861
@cookie_fix
862
def get_menu(request, with_extra_links=False, with_signout=True):
863
    user = request.user
864
    index_url = reverse('index')
865

    
866
    if isinstance(user, User) and user.is_authenticated():
867
        l = []
868
        append = l.append
869
        item = MenuItem
870
        item.current_path = request.build_absolute_uri(request.path)
871
        append(item(url=request.build_absolute_uri(reverse('index')),
872
                    name=user.email))
873
        if with_extra_links:
874
            append(item(url=request.build_absolute_uri(reverse('landing')),
875
                        name="Overview"))
876
        if with_signout:
877
            append(item(url=request.build_absolute_uri(reverse('landing')),
878
                        name="Dashboard"))
879
        if with_extra_links:
880
            append(item(url=request.build_absolute_uri(reverse('edit_profile')),
881
                        name="Profile"))
882

    
883
        if with_extra_links:
884
            if settings.INVITATIONS_ENABLED:
885
                append(item(url=request.build_absolute_uri(reverse('invite')),
886
                            name="Invitations"))
887

    
888
            append(item(url=request.build_absolute_uri(reverse('api_access')),
889
                        name="API access"))
890

    
891
            append(item(url=request.build_absolute_uri(reverse('resource_usage')),
892
                        name="Usage"))
893

    
894
            if settings.PROJECTS_VISIBLE:
895
                append(item(url=request.build_absolute_uri(reverse('project_list')),
896
                            name="Projects"))
897

    
898
            append(item(url=request.build_absolute_uri(reverse('feedback')),
899
                        name="Contact"))
900
        if with_signout:
901
            append(item(url=request.build_absolute_uri(reverse('logout')),
902
                        name="Sign out"))
903
    else:
904
        l = [{'url': request.build_absolute_uri(index_url),
905
              'name': _("Sign in")}]
906

    
907
    callback = request.GET.get('callback', None)
908
    data = json.dumps(tuple(l))
909
    mimetype = 'application/json'
910

    
911
    if callback:
912
        mimetype = 'application/javascript'
913
        data = '%s(%s)' % (callback, data)
914

    
915
    return HttpResponse(content=data, mimetype=mimetype)
916

    
917

    
918
class MenuItem(dict):
919
    current_path = ''
920

    
921
    def __init__(self, *args, **kwargs):
922
        super(MenuItem, self).__init__(*args, **kwargs)
923
        if kwargs.get('url') or kwargs.get('submenu'):
924
            self.__set_is_active__()
925

    
926
    def __setitem__(self, key, value):
927
        super(MenuItem, self).__setitem__(key, value)
928
        if key in ('url', 'submenu'):
929
            self.__set_is_active__()
930

    
931
    def __set_is_active__(self):
932
        if self.get('is_active'):
933
            return
934
        if self.current_path.startswith(self.get('url')):
935
            self.__setitem__('is_active', True)
936
        else:
937
            submenu = self.get('submenu', ())
938
            current = (i for i in submenu if i.get('url') == self.current_path)
939
            try:
940
                current_node = current.next()
941
                if not current_node.get('is_active'):
942
                    current_node.__setitem__('is_active', True)
943
                self.__setitem__('is_active', True)
944
            except StopIteration:
945
                return
946

    
947
    def __setattribute__(self, name, value):
948
        super(MenuItem, self).__setattribute__(name, value)
949
        if name == 'current_path':
950
            self.__set_is_active__()
951

    
952

    
953
def get_services(request):
954
    callback = request.GET.get('callback', None)
955
    mimetype = 'application/json'
956
    data = json.dumps(Component.catalog().values())
957

    
958
    if callback:
959
        # Consume session messages. When get_services is loaded from an astakos
960
        # page, messages should have already been consumed in the html
961
        # response. When get_services is loaded from another domain/service we
962
        # consume them here so that no stale messages to appear if user visits
963
        # an astakos view later on.
964
        # TODO: messages could be served to other services/sites in the dict
965
        # response of get_services and/or get_menu. Services could handle those
966
        # messages respectively.
967
        messages_list = list(messages.get_messages(request))
968
        mimetype = 'application/javascript'
969
        data = '%s(%s)' % (callback, data)
970

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