Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views / im.py @ 11d5fd8b

History | View | Annotate | Download (34.7 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
@transaction.commit_on_success
133
def update_token(request):
134
    """
135
    Update api token view.
136
    """
137
    user = AstakosUser.objects.select_for_update().get(id=request.user.id)
138
    user.renew_token()
139
    user.save()
140
    messages.success(request, astakos_messages.TOKEN_UPDATED)
141
    return HttpResponseRedirect(reverse('api_access'))
142

    
143

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

152
    In case of GET request renders a form for providing the invitee
153
    information.
154
    In case of POST checks whether the user has not run out of invitations and
155
    then 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
@require_http_methods(["GET", "POST"])
213
@required_auth_methods_assigned()
214
@login_required
215
@cookie_fix
216
@signed_terms_required
217
def api_access_config(request, template_name='im/api_access_config.html',
218
                      content_type='text/plain', extra_context=None,
219
                      filename='.kamakirc'):
220

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

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

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

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

    
245

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

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

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

    
269

    
270
@require_http_methods(["GET", "POST"])
271
@required_auth_methods_assigned(allow_access=True)
272
@login_required
273
@cookie_fix
274
@signed_terms_required
275
@transaction.commit_on_success
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

    
306
    request.user = AstakosUser.objects.select_for_update().\
307
        get(id=request.user.id)
308
    extra_context = extra_context or {}
309
    form = ProfileForm(
310
        instance=request.user,
311
        session_key=request.session.session_key
312
    )
313
    extra_context['next'] = request.GET.get('next')
314
    if request.method == 'POST':
315
        form = ProfileForm(
316
            request.POST,
317
            instance=request.user,
318
            session_key=request.session.session_key
319
        )
320
        if form.is_valid():
321
            try:
322
                prev_token = request.user.auth_token
323
                user = form.save(request=request)
324
                next = restrict_next(
325
                    request.POST.get('next'),
326
                    domain=settings.COOKIE_DOMAIN
327
                )
328
                msg = _(astakos_messages.PROFILE_UPDATED)
329
                messages.success(request, msg)
330

    
331
                if form.email_changed:
332
                    msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
333
                    messages.success(request, msg)
334
                if form.password_changed:
335
                    msg = _(astakos_messages.PASSWORD_CHANGED)
336
                    messages.success(request, msg)
337

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

    
348
    # existing providers
349
    user_providers = request.user.get_enabled_auth_providers()
350
    user_disabled_providers = request.user.get_disabled_auth_providers()
351

    
352
    # providers that user can add
353
    user_available_providers = request.user.get_available_auth_providers()
354

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

    
364

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

373
    In case of GET request renders a form for entering the user information.
374
    In case of POST handles the signup.
375

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

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

387
    On unsuccessful creation, renders ``template_name`` with an error message.
388

389
    **Arguments**
390

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

395
    ``extra_context``
396
        An dictionary of variables to add to the template context.
397

398
    ``on_success``
399
        Resolvable view name to redirect on registration success.
400

401
    **Template:**
402

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

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

    
416
    instance = None
417

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

    
428
        provider = pending.provider
429

    
430
        # clone third party instance into the corresponding AstakosUser
431
        instance = pending.get_user_instance()
432
        get_unverified = AstakosUserAuthProvider.objects.unverified
433

    
434
        # check existing unverified entries
435
        unverified = get_unverified(pending.provider,
436
                                    identifier=pending.third_party_identifier)
437

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

    
447
    # prepare activation backend based on current request
448
    if not activation_backend:
449
        activation_backend = activation_backends.get_backend()
450

    
451
    form_kwargs = {'instance': instance, 'request': request}
452
    if third_party_token:
453
        form_kwargs['third_party_token'] = third_party_token
454

    
455
    if pending:
456
        form_kwargs['initial'] = {
457
            'first_name': pending.first_name,
458
            'last_name': pending.last_name,
459
            'email': pending.email
460
        }
461

    
462
    form = activation_backend.get_signup_form(
463
        provider, None, **form_kwargs)
464

    
465

    
466
    if request.method == 'POST':
467
        form = activation_backend.get_signup_form(
468
            provider,
469
            request.POST,
470
            **form_kwargs)
471

    
472
        if form.is_valid():
473
            user = form.create_user()
474
            result = activation_backend.handle_registration(user)
475
            if result.status == \
476
                    activation_backend.Result.PENDING_MODERATION:
477
                # user should be warned that his account is not active yet
478
                status = messages.WARNING
479
            else:
480
                status = messages.SUCCESS
481
            message = result.message
482
            activation_backend.send_result_notifications(result, user)
483

    
484
            # commit user entry
485
            transaction.commit()
486

    
487
            if user and user.is_active:
488
                # activation backend directly activated the user
489
                # log him in
490
                next = request.POST.get('next', '')
491
                response = prepare_response(request, user, next=next)
492
                return response
493

    
494
            messages.add_message(request, status, message)
495
            return HttpResponseRedirect(reverse(on_success))
496

    
497
    return render_response(
498
        template_name,
499
        signup_form=form,
500
        third_party_token=third_party_token,
501
        provider=provider,
502
        context_instance=get_context(request, extra_context))
503

    
504

    
505
@require_http_methods(["GET", "POST"])
506
@required_auth_methods_assigned(allow_access=True)
507
@login_required
508
@cookie_fix
509
@signed_terms_required
510
def feedback(request, template_name='im/feedback.html',
511
             email_template_name='im/feedback_mail.txt', extra_context=None):
512
    """
513
    Allows a user to send feedback.
514

515
    In case of GET request renders a form for providing the feedback
516
    information.
517
    In case of POST sends an email to support team.
518

519
    If the user isn't logged in, redirects to settings.LOGIN_URL.
520

521
    **Arguments**
522

523
    ``template_name``
524
        A custom template to use. This is optional; if not specified,
525
        this will default to ``im/feedback.html``.
526

527
    ``extra_context``
528
        An dictionary of variables to add to the template context.
529

530
    **Template:**
531

532
    im/signup.html or ``template_name`` keyword argument.
533

534
    **Settings:**
535

536
    * LOGIN_URL: login uri
537
    """
538
    extra_context = extra_context or {}
539
    if request.method == 'GET':
540
        form = FeedbackForm()
541
    if request.method == 'POST':
542
        if not request.user:
543
            return HttpResponse('Unauthorized', status=401)
544

    
545
        form = FeedbackForm(request.POST)
546
        if form.is_valid():
547
            msg = form.cleaned_data['feedback_msg']
548
            data = form.cleaned_data['feedback_data']
549
            send_feedback(msg, data, request.user, email_template_name)
550
            message = _(astakos_messages.FEEDBACK_SENT)
551
            messages.success(request, message)
552
            return HttpResponseRedirect(reverse('feedback'))
553

    
554
    return render_response(template_name,
555
                           feedback_form=form,
556
                           context_instance=get_context(request,
557
                                                        extra_context))
558

    
559

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

    
577
    next = restrict_next(
578
        request.GET.get('next'),
579
        domain=settings.COOKIE_DOMAIN
580
    )
581

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

    
601

    
602
@require_http_methods(["GET", "POST"])
603
@cookie_fix
604
@transaction.commit_on_success
605
def activate(request, greeting_email_template_name='im/welcome_email.txt',
606
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
607
    """
608
    Activates the user identified by the ``auth`` request parameter, sends a
609
    welcome email and renews the user token.
610

611
    The user state will be updated only if the email will be send successfully.
612
    """
613
    token = request.GET.get('auth', None)
614
    next = request.GET.get('next', None)
615

    
616
    if not token:
617
        raise PermissionDenied
618

    
619
    if request.user.is_authenticated():
620
        message = _(astakos_messages.LOGGED_IN_WARNING)
621
        messages.error(request, message)
622
        return HttpResponseRedirect(reverse('index'))
623

    
624
    try:
625
        user = AstakosUser.objects.select_for_update().\
626
            get(verification_code=token)
627
    except AstakosUser.DoesNotExist:
628
        raise Http404
629

    
630
    if user.email_verified:
631
        message = _(astakos_messages.ACCOUNT_ALREADY_VERIFIED)
632
        messages.error(request, message)
633
        return HttpResponseRedirect(reverse('index'))
634

    
635
    backend = activation_backends.get_backend()
636
    result = backend.handle_verification(user, token)
637
    backend.send_result_notifications(result, user)
638
    next = settings.ACTIVATION_REDIRECT_URL or next or reverse('index')
639
    if user.is_active:
640
        response = prepare_response(request, user, next, renew=True)
641
        messages.success(request, _(result.message))
642
    else:
643
        response = HttpResponseRedirect(reverse('index'))
644
        messages.warning(request, _(result.message))
645

    
646
    return response
647

    
648

    
649
@login_required
650
def _approval_terms_post(request, template_name, terms, extra_context):
651
    next = restrict_next(
652
        request.POST.get('next'),
653
        domain=settings.COOKIE_DOMAIN
654
    )
655
    if not next:
656
        next = reverse('index')
657
    form = SignApprovalTermsForm(request.POST, instance=request.user)
658
    if not form.is_valid():
659
        return render_response(template_name,
660
                               terms=terms,
661
                               approval_terms_form=form,
662
                               context_instance=get_context(request,
663
                                                            extra_context))
664
    user = form.save()
665
    return HttpResponseRedirect(next)
666

    
667

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

    
686
    if not terms_record:
687
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
688
        return HttpResponseRedirect(reverse('index'))
689
    try:
690
        f = open(terms_record.location, 'r')
691
    except IOError:
692
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
693
        return render_response(
694
            template_name, context_instance=get_context(request,
695
                                                        extra_context))
696

    
697
    terms = f.read()
698

    
699
    if request.method == 'POST':
700
        return _approval_terms_post(request, template_name, terms,
701
                                    extra_context)
702
    else:
703
        form = None
704
        if request.user.is_authenticated() and not request.user.signed_terms:
705
            form = SignApprovalTermsForm(instance=request.user)
706
        return render_response(template_name,
707
                               terms=terms,
708
                               approval_terms_form=form,
709
                               context_instance=get_context(request,
710
                                                            extra_context))
711

    
712

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

    
723
    if not settings.EMAILCHANGE_ENABLED:
724
        raise PermissionDenied
725

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

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

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

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

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

    
774
    form = EmailChangeForm(request.POST or None)
775
    if request.method == 'POST' and form.is_valid():
776
        ec = form.save(request, email_template_name, request)
777
        msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
778
        messages.success(request, msg)
779
        transaction.commit()
780
        return HttpResponseRedirect(reverse('edit_profile'))
781

    
782
    if request.user.email_change_is_pending():
783
        messages.warning(request,
784
                         astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
785

    
786
    return render_response(
787
        form_template_name,
788
        form=form,
789
        context_instance=get_context(request, extra_context)
790
    )
791

    
792

    
793
@cookie_fix
794
@transaction.commit_on_success
795
def send_activation(request, user_id, template_name='im/login.html',
796
                    extra_context=None):
797

    
798
    if request.user.is_authenticated():
799
        return HttpResponseRedirect(reverse('index'))
800

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

    
811
            messages.error(request,
812
                           _(astakos_messages.ACCOUNT_ALREADY_VERIFIED))
813
        else:
814
            activation_backend = activation_backends.get_backend()
815
            activation_backend.send_user_verification_email(u)
816
            messages.success(request, astakos_messages.ACTIVATION_SENT)
817

    
818
    return HttpResponseRedirect(reverse('index'))
819

    
820

    
821
@require_http_methods(["GET"])
822
@cookie_fix
823
@valid_astakos_user_required
824
def resource_usage(request):
825

    
826
    resources_meta = presentation.RESOURCES
827

    
828
    current_usage = quotas.get_user_quotas(request.user)
829
    current_usage = json.dumps(current_usage['system'])
830
    resource_catalog, resource_groups = _resources_catalog()
831
    if resource_catalog is False:
832
        # on fail resource_groups contains the result object
833
        result = resource_groups
834
        messages.error(request, 'Unable to retrieve system resources: %s' %
835
                       result.reason)
836

    
837
    resource_catalog = json.dumps(resource_catalog)
838
    resource_groups = json.dumps(resource_groups)
839
    resources_order = json.dumps(resources_meta.get('resources_order'))
840

    
841
    return render_response('im/resource_usage.html',
842
                           context_instance=get_context(request),
843
                           resource_catalog=resource_catalog,
844
                           resource_groups=resource_groups,
845
                           resources_order=resources_order,
846
                           current_usage=current_usage,
847
                           token_cookie_name=settings.COOKIE_NAME,
848
                           usage_update_interval=
849
                           settings.USAGE_UPDATE_INTERVAL)
850

    
851

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

    
862
    if provider.get_remove_policy:
863
        messages.success(request, provider.get_removed_msg)
864
        provider.remove_from_user()
865
        return HttpResponseRedirect(reverse('edit_profile'))
866
    else:
867
        raise PermissionDenied
868

    
869

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

    
881

    
882
@cookie_fix
883
def get_menu(request, with_extra_links=False, with_signout=True):
884
    user = request.user
885
    index_url = reverse('index')
886

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

    
906
        if with_extra_links:
907
            if settings.INVITATIONS_ENABLED:
908
                append(item(url=request.build_absolute_uri(reverse('invite')),
909
                            name="Invitations"))
910

    
911
            append(item(url=request.build_absolute_uri(reverse('api_access')),
912
                        name="API access"))
913

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

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

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

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

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

    
943
    return HttpResponse(content=data, mimetype=mimetype)
944

    
945

    
946
class MenuItem(dict):
947
    current_path = ''
948

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

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

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

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

    
980

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

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

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