Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views / im.py @ 1808f7bc

History | View | Annotate | Download (34.4 kB)

1
# Copyright 2011, 2012, 2013 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
import astakos.im.messages as astakos_messages
58

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

    
78
logger = logging.getLogger(__name__)
79

    
80

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

88
    **Arguments**
89

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

94
    ``extra_context``
95
        An dictionary of variables to add to the template context.
96
    """
97

    
98
    extra_context = extra_context or {}
99

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

    
104
    if request.user.is_authenticated():
105
        return HttpResponseRedirect(reverse('landing'))
106

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

    
113

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

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

    
128

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

    
142

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

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

156
    The number of the user invitations is going to be updated only if the email
157
    has been successfully sent.
158

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

161
    **Arguments**
162

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

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

170
    **Template:**
171

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

174
    **Settings:**
175

176
    The view expectes the following settings are defined:
177

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

    
185
    inviter = request.user
186
    if request.method == 'POST':
187
        form = InvitationForm(request.POST)
188
        if inviter.invitations > 0:
189
            if form.is_valid():
190
                email = form.cleaned_data.get('username')
191
                realname = form.cleaned_data.get('realname')
192
                invite_func(inviter, email, realname)
193
                message = _(astakos_messages.INVITATION_SENT) % locals()
194
                messages.success(request, message)
195
        else:
196
            message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
197
            messages.error(request, message)
198

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

    
210

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

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

    
225
    url = get_public_endpoint(settings.astakos_services, 'identity')
226

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

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

    
244

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

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

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

    
268

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

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

282
    If the user isn't logged in, redirects to settings.LOGIN_URL.
283

284
    **Arguments**
285

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

290
    ``extra_context``
291
        An dictionary of variables to add to the template context.
292

293
    **Template:**
294

295
    im/profile.html or ``template_name`` keyword argument.
296

297
    **Settings:**
298

299
    The view expectes the following settings are defined:
300

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

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

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

    
343
    # existing providers
344
    user_providers = request.user.get_enabled_auth_providers()
345
    user_disabled_providers = request.user.get_disabled_auth_providers()
346

    
347
    # providers that user can add
348
    user_available_providers = request.user.get_available_auth_providers()
349

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

    
359

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

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

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

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

382
    On unsuccessful creation, renders ``template_name`` with an error message.
383

384
    **Arguments**
385

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

390
    ``extra_context``
391
        An dictionary of variables to add to the template context.
392

393
    ``on_success``
394
        Resolvable view name to redirect on registration success.
395

396
    **Template:**
397

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

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

    
411
    instance = None
412

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

    
423
        provider = pending.provider
424

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

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

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

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

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

    
450
    if pending:
451
        form_kwargs['initial'] = {
452
            'first_name': pending.first_name,
453
            'last_name': pending.last_name,
454
            'email': pending.email
455
        }
456

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

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

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

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

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

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

    
491
    return render_response(
492
        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',
505
             email_template_name='im/feedback_mail.txt', extra_context=None):
506
    """
507
    Allows a user to send feedback.
508

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

513
    If the user isn't logged in, redirects to settings.LOGIN_URL.
514

515
    **Arguments**
516

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

521
    ``extra_context``
522
        An dictionary of variables to add to the template context.
523

524
    **Template:**
525

526
    im/signup.html or ``template_name`` keyword argument.
527

528
    **Settings:**
529

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

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

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

    
553

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

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

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

    
595

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

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

    
610
    if not token:
611
        raise PermissionDenied
612

    
613
    if request.user.is_authenticated():
614
        message = _(astakos_messages.LOGGED_IN_WARNING)
615
        messages.error(request, message)
616
        return HttpResponseRedirect(reverse('index'))
617

    
618
    try:
619
        user = AstakosUser.objects.get(verification_code=token)
620
    except AstakosUser.DoesNotExist:
621
        raise Http404
622

    
623
    if user.email_verified:
624
        message = _(astakos_messages.ACCOUNT_ALREADY_VERIFIED)
625
        messages.error(request, message)
626
        return HttpResponseRedirect(reverse('index'))
627

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

    
639
    return response
640

    
641

    
642
@login_required
643
def _approval_terms_post(request, template_name, terms, extra_context):
644
    next = restrict_next(
645
        request.POST.get('next'),
646
        domain=settings.COOKIE_DOMAIN
647
    )
648
    if not next:
649
        next = reverse('index')
650
    form = SignApprovalTermsForm(request.POST, instance=request.user)
651
    if not form.is_valid():
652
        return render_response(template_name,
653
                               terms=terms,
654
                               approval_terms_form=form,
655
                               context_instance=get_context(request,
656
                                                            extra_context))
657
    user = form.save()
658
    return HttpResponseRedirect(next)
659

    
660

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

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

    
690
    terms = f.read()
691

    
692
    if request.method == 'POST':
693
        return _approval_terms_post(request, template_name, terms,
694
                                    extra_context)
695
    else:
696
        form = None
697
        if request.user.is_authenticated() and not request.user.signed_terms:
698
            form = SignApprovalTermsForm(instance=request.user)
699
        return render_response(template_name,
700
                               terms=terms,
701
                               approval_terms_form=form,
702
                               context_instance=get_context(request,
703
                                                            extra_context))
704

    
705

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

    
716
    if not settings.EMAILCHANGE_ENABLED:
717
        raise PermissionDenied
718

    
719
    if activation_key:
720
        try:
721
            try:
722
                email_change = EmailChange.objects.get(
723
                    activation_key=activation_key)
724
            except EmailChange.DoesNotExist:
725
                logger.error("[change-email] Invalid or used activation "
726
                             "code, %s", activation_key)
727
                raise Http404
728

    
729
            if (
730
                request.user.is_authenticated() and
731
                request.user == email_change.user or not
732
                request.user.is_authenticated()
733
            ):
734
                user = EmailChange.objects.change_email(activation_key)
735
                msg = _(astakos_messages.EMAIL_CHANGED)
736
                messages.success(request, msg)
737
                transaction.commit()
738
                return HttpResponseRedirect(reverse('edit_profile'))
739
            else:
740
                logger.error("[change-email] Access from invalid user, %s %s",
741
                             email_change.user, request.user.log_display)
742
                raise PermissionDenied
743
        except ValueError, e:
744
            messages.error(request, e)
745
            transaction.rollback()
746
            return HttpResponseRedirect(reverse('index'))
747

    
748
        return render_response(confirm_template_name,
749
                               modified_user=user if 'user' in locals()
750
                               else None,
751
                               context_instance=get_context(request,
752
                                                            extra_context))
753

    
754
    if not request.user.is_authenticated():
755
        path = quote(request.get_full_path())
756
        url = request.build_absolute_uri(reverse('index'))
757
        return HttpResponseRedirect(url + '?next=' + path)
758

    
759
    # clean up expired email changes
760
    if request.user.email_change_is_pending():
761
        change = request.user.emailchanges.get()
762
        if change.activation_key_expired():
763
            change.delete()
764
            transaction.commit()
765
            return HttpResponseRedirect(reverse('email_change'))
766

    
767
    form = EmailChangeForm(request.POST or None)
768
    if request.method == 'POST' and form.is_valid():
769
        ec = form.save(request, email_template_name, request)
770
        msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
771
        messages.success(request, msg)
772
        transaction.commit()
773
        return HttpResponseRedirect(reverse('edit_profile'))
774

    
775
    if request.user.email_change_is_pending():
776
        messages.warning(request,
777
                         astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
778

    
779
    return render_response(
780
        form_template_name,
781
        form=form,
782
        context_instance=get_context(request, extra_context)
783
    )
784

    
785

    
786
@cookie_fix
787
def send_activation(request, user_id, template_name='im/login.html',
788
                    extra_context=None):
789

    
790
    if request.user.is_authenticated():
791
        return HttpResponseRedirect(reverse('index'))
792

    
793
    extra_context = extra_context or {}
794
    try:
795
        u = AstakosUser.objects.get(id=user_id)
796
    except AstakosUser.DoesNotExist:
797
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
798
    else:
799
        if u.email_verified:
800
            logger.warning("[resend activation] Account already verified: %s",
801
                           u.log_display)
802

    
803
            messages.error(request,
804
                           _(astakos_messages.ACCOUNT_ALREADY_VERIFIED))
805
        else:
806
            activation_backend = activation_backends.get_backend()
807
            activation_backend.send_user_verification_email(u)
808
            messages.success(request, astakos_messages.ACTIVATION_SENT)
809

    
810
    return HttpResponseRedirect(reverse('index'))
811

    
812

    
813
@require_http_methods(["GET"])
814
@cookie_fix
815
@valid_astakos_user_required
816
def resource_usage(request):
817

    
818
    resources_meta = presentation.RESOURCES
819

    
820
    current_usage = quotas.get_user_quotas(request.user)
821
    current_usage = json.dumps(current_usage['system'])
822
    resource_catalog, resource_groups = _resources_catalog()
823
    if resource_catalog is False:
824
        # on fail resource_groups contains the result object
825
        result = resource_groups
826
        messages.error(request, 'Unable to retrieve system resources: %s' %
827
                       result.reason)
828

    
829
    resource_catalog = json.dumps(resource_catalog)
830
    resource_groups = json.dumps(resource_groups)
831
    resources_order = json.dumps(resources_meta.get('resources_order'))
832

    
833
    return render_response('im/resource_usage.html',
834
                           context_instance=get_context(request),
835
                           resource_catalog=resource_catalog,
836
                           resource_groups=resource_groups,
837
                           resources_order=resources_order,
838
                           current_usage=current_usage,
839
                           token_cookie_name=settings.COOKIE_NAME,
840
                           usage_update_interval=
841
                           settings.USAGE_UPDATE_INTERVAL)
842

    
843

    
844
# TODO: action only on POST and user should confirm the removal
845
@require_http_methods(["POST"])
846
@cookie_fix
847
@valid_astakos_user_required
848
def remove_auth_provider(request, pk):
849
    try:
850
        provider = request.user.auth_providers.get(pk=int(pk)).settings
851
    except AstakosUserAuthProvider.DoesNotExist:
852
        raise Http404
853

    
854
    if provider.get_remove_policy:
855
        messages.success(request, provider.get_removed_msg)
856
        provider.remove_from_user()
857
        return HttpResponseRedirect(reverse('edit_profile'))
858
    else:
859
        raise PermissionDenied
860

    
861

    
862
@require_http_methods(["GET"])
863
@required_auth_methods_assigned(allow_access=True)
864
@login_required
865
@cookie_fix
866
@signed_terms_required
867
def landing(request):
868
    context = {'services': Component.catalog(orderfor='dashboard')}
869
    return render_response(
870
        'im/landing.html',
871
        context_instance=get_context(request), **context)
872

    
873

    
874
@cookie_fix
875
def get_menu(request, with_extra_links=False, with_signout=True):
876
    user = request.user
877
    index_url = reverse('index')
878

    
879
    if isinstance(user, User) and user.is_authenticated():
880
        l = []
881
        append = l.append
882
        item = MenuItem
883
        item.current_path = request.build_absolute_uri(request.path)
884
        append(item(url=request.build_absolute_uri(reverse('index')),
885
                    name=user.email))
886
        if with_extra_links:
887
            append(item(url=request.build_absolute_uri(reverse('landing')),
888
                        name="Overview"))
889
        if with_signout:
890
            append(item(url=request.build_absolute_uri(reverse('landing')),
891
                        name="Dashboard"))
892
        if with_extra_links:
893
            append(
894
                item(
895
                    url=request.build_absolute_uri(reverse('edit_profile')),
896
                    name="Profile"))
897

    
898
        if with_extra_links:
899
            if settings.INVITATIONS_ENABLED:
900
                append(item(url=request.build_absolute_uri(reverse('invite')),
901
                            name="Invitations"))
902

    
903
            append(item(url=request.build_absolute_uri(reverse('api_access')),
904
                        name="API access"))
905

    
906
            append(
907
                item(
908
                    url=request.build_absolute_uri(reverse('resource_usage')),
909
                    name="Usage"))
910

    
911
            if settings.PROJECTS_VISIBLE:
912
                append(
913
                    item(
914
                        url=request.build_absolute_uri(
915
                            reverse('project_list')),
916
                        name="Projects"))
917

    
918
            append(item(url=request.build_absolute_uri(reverse('feedback')),
919
                        name="Contact"))
920
        if with_signout:
921
            append(item(url=request.build_absolute_uri(reverse('logout')),
922
                        name="Sign out"))
923
    else:
924
        l = [{'url': request.build_absolute_uri(index_url),
925
              'name': _("Sign in")}]
926

    
927
    callback = request.GET.get('callback', None)
928
    data = json.dumps(tuple(l))
929
    mimetype = 'application/json'
930

    
931
    if callback:
932
        mimetype = 'application/javascript'
933
        data = '%s(%s)' % (callback, data)
934

    
935
    return HttpResponse(content=data, mimetype=mimetype)
936

    
937

    
938
class MenuItem(dict):
939
    current_path = ''
940

    
941
    def __init__(self, *args, **kwargs):
942
        super(MenuItem, self).__init__(*args, **kwargs)
943
        if kwargs.get('url') or kwargs.get('submenu'):
944
            self.__set_is_active__()
945

    
946
    def __setitem__(self, key, value):
947
        super(MenuItem, self).__setitem__(key, value)
948
        if key in ('url', 'submenu'):
949
            self.__set_is_active__()
950

    
951
    def __set_is_active__(self):
952
        if self.get('is_active'):
953
            return
954
        if self.current_path.startswith(self.get('url')):
955
            self.__setitem__('is_active', True)
956
        else:
957
            submenu = self.get('submenu', ())
958
            current = (i for i in submenu if i.get('url') == self.current_path)
959
            try:
960
                current_node = current.next()
961
                if not current_node.get('is_active'):
962
                    current_node.__setitem__('is_active', True)
963
                self.__setitem__('is_active', True)
964
            except StopIteration:
965
                return
966

    
967
    def __setattribute__(self, name, value):
968
        super(MenuItem, self).__setattribute__(name, value)
969
        if name == 'current_path':
970
            self.__set_is_active__()
971

    
972

    
973
def get_services(request):
974
    callback = request.GET.get('callback', None)
975
    mimetype = 'application/json'
976
    data = json.dumps(Component.catalog().values())
977

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

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