Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (34.6 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
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

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

    
467
        if form.is_valid():
468
            user = form.save(commit=False)
469

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

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

    
486
            # commit user entry
487
            transaction.commit()
488

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

    
496
            messages.add_message(request, status, message)
497
            return HttpResponseRedirect(reverse(on_success))
498

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

    
506

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

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

521
    If the user isn't logged in, redirects to settings.LOGIN_URL.
522

523
    **Arguments**
524

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

529
    ``extra_context``
530
        An dictionary of variables to add to the template context.
531

532
    **Template:**
533

534
    im/signup.html or ``template_name`` keyword argument.
535

536
    **Settings:**
537

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

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

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

    
561

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

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

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

    
603

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

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

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

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

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

    
634
    backend = activation_backends.get_backend()
635
    result = backend.handle_verification(user, token)
636
    backend.send_result_notifications(result, user)
637
    next = settings.ACTIVATION_REDIRECT_URL or next
638
    response = HttpResponseRedirect(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
        messages.warning(request, _(result.message))
644

    
645
    return response
646

    
647

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

    
666

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

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

    
696
    terms = f.read()
697

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

    
711

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

    
722
    if not settings.EMAILCHANGE_ENABLED:
723
        raise PermissionDenied
724

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

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

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

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

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

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

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

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

    
791

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

    
796
    if request.user.is_authenticated():
797
        return HttpResponseRedirect(reverse('index'))
798

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

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

    
816
    return HttpResponseRedirect(reverse('index'))
817

    
818

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

    
824
    resources_meta = presentation.RESOURCES
825

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

    
835
    resource_catalog = json.dumps(resource_catalog)
836
    resource_groups = json.dumps(resource_groups)
837
    resources_order = json.dumps(resources_meta.get('resources_order'))
838

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

    
849

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

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

    
867

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

    
879

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

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

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

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

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

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

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

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

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

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

    
943

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

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

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

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

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

    
978

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

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

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